接学习笔记 http://www.kosamet.cn/3887.html
参考代码文档 https://web3dao-cn.github.io/solidity-example

小猪存钱罐练习题

contract PiggyBank {
    event Deposit(uint amount);
    event Withdraw(uint amount);

    address public owner = msg.sender;

    receive() external payable { //存钱时直接输入金额在点Transact按钮
        emit Deposit(msg.value);
    }

    function withdraw() external {
        require(msg.sender == owner, "not owner");
        emit Withdraw(address(this).balance);
        selfdestruct(payable(msg.sender)); //msg.sender要加payable用于支付
    }
}

ERC20合约编写实例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol
interface IERC20 {
    function totalSupply() external view returns (uint);

    function balanceOf(address account) external view returns (uint);

    function transfer(address recipient, uint amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint amount) external returns (bool);

    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);
}

---------------------------------------------------------------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "./IERC20.sol";

contract ERC20 is IERC20 {
    uint public totalSupply;
    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;
    string public name = "Solidity by Example";
    string public symbol = "SOLBYEX";
    uint8 public decimals = 18;

    function transfer(address recipient, uint amount) external returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool) {
        allowance[sender][msg.sender] -= amount;
        balanceOf[sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    }

    function mint(uint amount) external {
        balanceOf[msg.sender] += amount;
        totalSupply += amount;
        emit Transfer(address(0), msg.sender, amount);
    }

    function burn(uint amount) external {
        balanceOf[msg.sender] -= amount;
        totalSupply -= amount;
        emit Transfer(msg.sender, address(0), amount);
    }
}

多签钱包授权转账管理

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract MultiSigWallet {
    //创建事件通知
    event Deposit(address indexed sender, uint amount);
    event Submit(uint indexed txId);
    event Approve(address indexed owner, uint indexed txId);
    event Revoke(address indexed owner, uint indexed txId);
    event Execute(uint indexed txId);

    struct Transaction {
        address to; //发送地址
        uint value;
        bytes data; //给合约带数据
        bool executed; //是否已经执行
    }

    //多签成员owners
    address[] public owners;
    mapping(address => bool) public isOwner;
    //多签需要的通过确认数
    uint public required;

    Transaction[] public transactions;
    //是否批准
    mapping(uint => mapping(address => bool)) public approved;

    //必须是签名成员
    modifier onlyOwner() {
        require(isOwner[msg.sender], "not owner");
        _;
    }

    modifier txExists(uint _txId) {
        require(_txId < transactions.length, "tx does not exist"); _; } modifier notApproved(uint _txId) { require(!approved[_txId][msg.sender], "tx already approved"); _; } modifier notExecuted(uint _txId) { require(!transactions[_txId].executed, "tx already executed"); _; } //传入多签成员,批准数 constructor(address[] memory _owners, uint _required) { require(_owners.length > 0, "owners required");
        require(
            _required > 0 && _required <= _owners.length,
            "invalid required number of owners"
        );

        for (uint i; i < _owners.length; i++) {
            address owner = _owners[i];

            require(owner != address(0), "invalid owner");
            require(!isOwner[owner], "owner is not unique"); //地址不存在于当前管理员列表

            isOwner[owner] = true;
            owners.push(owner);
        }

        required = _required;
    }

    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    //发起交易请求
    function submit(address _to, uint _value, bytes calldata _data)
        external
        onlyOwner
    {
        transactions.push(Transaction({
            to: _to,
            value: _value,
            data: _data,
            executed: false
        }));
        emit Submit(transactions.length - 1);
    }

    //成员批准转账
    function approve(uint _txId)
        external
        onlyOwner
        txExists(_txId)
        notApproved(_txId)
        notExecuted(_txId)
    {
        approved[_txId][msg.sender] = true;
        emit Approve(msg.sender, _txId);
    }

    //获取批准数量 私有方法
    function _getApprovalCount(uint _txId) private view returns (uint count) {
        for (uint i; i < owners.length; i++) { if (approved[_txId][owners[i]]) { count += 1; } } } //符合批准数执行转账 function execute(uint _txId) external txExists(_txId) notExecuted(_txId) { require(_getApprovalCount(_txId) >= required, "approvals < required");
        Transaction storage transaction = transactions[_txId];

        transaction.executed = true;
        (bool success, ) = transaction.to.call{value: transaction.value}(
            transaction.data
        );
        require(success, "tx failed");

        emit Execute(_txId);
    }

    //撤销批准
    function revoke(uint _txId)
        external
        onlyOwner
        txExists(_txId)
        notExecuted(_txId)
    {
        require(approved[_txId][msg.sender], "tx not approved");
        approved[_txId][msg.sender] = false;
        emit Revoke(msg.sender, _txId);
    }
}

函数签名 – 虚拟机data数据如何找到一个函数 通过函数选择器区分函数
// 通过实验可以得出 可以出现同名函数 只要类型不同 名称可以相同

contract FunctionSelector {
    //参数1手动输入 "transfer(address,uint256)" 输出 bytes4: 0xa9059cbb
    function getSelector(string calldata _func) external pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }
}

contract Receiver {
    event Log(bytes data);

    function transfer(address _to, uint _amount) external {
        emit Log(msg.data); // 输出data数据
        //0xa9059cbb 函数选择器签名,通过函数名称和参数类型打包取哈希取前4位(8个字符)16进制
        //0000000000000000000000004b20993bc481177ec7e8f571cecae8a9e22c02db 第一个参数
        //000000000000000000000000000000000000000000000000000000000000000b 第二个参数 输入11显示b
    }
}

荷兰拍卖 减价拍卖
流程:先部署一个标准ERC721合约进行部署mint一个erc721
注意: ERC20和ERC721都要对使用的合约进行approve批准才可以进行合约之间的操作

interface IERC721 {
    function transferFrom(
        address _from,
        address _to,
        uint _nftId
    ) external;
}
contract DutchAuction {
    //持续时间
    uint private constant DURATION = 7 days;

    IERC721 public immutable nft;
    uint public immutable nftId;

    //卖家
    address payable public immutable seller;
    //起拍价
    uint public immutable startingPrice;
    uint public immutable startAt;
    uint public immutable expiresAt;
    //每秒折扣率 1
    uint public immutable discountRate;

    // 参数传递 1000000,1,以太坊地址,777
    constructor(
        uint _startingPrice,
        uint _discountRate, //设置为1
        address _nft,
        uint _nftId
    ) {
        seller = payable(msg.sender);
        startingPrice = _startingPrice;
        discountRate = _discountRate;
        startAt = block.timestamp;
        expiresAt = block.timestamp + DURATION;

        // 价格设定防止减为负数
        require(
            _startingPrice >= _discountRate * DURATION,
            "starting price < discount"
        );

        nft = IERC721(_nft);
        nftId = _nftId;
    }

    function getPrice() public view returns (uint) {
        // 折扣率 * 时间的流逝,折扣率为1 每秒-1
        uint timeElapsed = block.timestamp - startAt;
        uint discount = discountRate * timeElapsed;
        //起拍价 - 折扣价
        return startingPrice - discount;
    }

    function buy() external payable {
        // 判断这次拍卖是否过期
        require(block.timestamp < expiresAt, "auction expired"); uint price = getPrice(); require(msg.value >= price,"ETH < price not enough"); nft.transferFrom(seller, msg.sender, nftId); // 时间差问题将差额金额退回 uint refund = msg.value - price; if (refund > 0) {
            payable(msg.sender).transfer(refund);
        }
        // 自毁合约,将销售所得发给卖家
        selfdestruct(seller);
    }
}

英式拍卖 竞价高者拍卖成功

部署合约 NFT地址 使用ERC721 mint合约 (注意一定要授权approve给拍卖合约
拍卖合约 填写NFT地址,id为77,起拍价为1wei

interface IERC721 {
    function transferFrom(
        address _from,
        address _to,
        uint _nftId
    ) external;
}

contract EnglishAuction {
    event Start();
    event Bid(address indexed sender, uint amount);
    event Withdraw(address indexed bidder, uint amount);
    event End(address highestBidder, uint amount);


    IERC721 public immutable nft;
    uint public immutable nftId;

    address payable public immutable seller;
    uint32 public endAt;
    bool public started;
    bool public ended;

    address public highestBidder;
    uint public highestBid;

    //出价累计金额
    mapping(address => uint) public bids;

    constructor(
        address _nft,
        uint _nftId,
        uint _startingBid
    ) {
        nft = IERC721(_nft);
        nftId = _nftId;
        seller = payable(msg.sender);
        highestBid = _startingBid;
    }

    // 竞拍开始 将NFT转入合约
    function start() external {
        require(msg.sender == seller, "not seller auth");
        require(!started, "started");

        started = true;
        endAt = uint32(block.timestamp + 300); // 300秒后结束拍卖
        nft.transferFrom(seller, address(this), nftId);

        emit Start();
    }

    // 出价
    function bid() external payable {
        require(started, "not started");
        require(block.timestamp < endAt, "ended"); require(msg.value > highestBid, "value < highest bid"); // 第一次最高出价会是0地址 要排除 if (highestBidder != address(0)) { bids[highestBidder] += highestBid; } highestBid = msg.value; highestBidder = msg.sender; emit Bid(msg.sender, msg.value); } // 取回出价的未使用余额 function withdraw() external { uint bal = bids[msg.sender]; payable(msg.sender).transfer(bal); emit Withdraw(msg.sender, bal); } //结束拍卖 任何人都可以调用 无需确认调用者身份 function end() external { //状态要开始没有结束 require(started, "not started"); require(!ended, "ended"); //时间上要结束了 require(block.timestamp >= endAt, "not ended");

        //改变状态
        ended = true;
        if ( highestBidder != address(0)) {
            nft.transferFrom(address(this), highestBidder, nftId);
            seller.transfer(highestBid);
        } else {
            //没有任何一个人出价 退还NFT
            nft.transferFrom(address(this), seller, nftId);
        }

        emit End(highestBidder, highestBid);
        
    }
}

众筹合约
众筹合约是由一个人发布活动并且筹集ERC20代币的演示

先用ERC20合约铸造代币,一定要进行approve足额批准才可参与众筹活动,如果批准额度不足要重新批准。
测试众筹合约时候,目标填写100,开始时间戳+100,结束时间戳+200进行测试(获取时间戳方法在chrome中Console 命令 new Date().getTime() / 1000)

IERC20.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol
interface IERC20 {
    function totalSupply() external view returns (uint);

    function balanceOf(address account) external view returns (uint);

    function transfer(address recipient, uint amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint amount) external returns (bool);

    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);
}

contract ERC20 is IERC20 {
    uint public totalSupply;
    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;
    string public name = "Solidity by Example";
    string public symbol = "SOLBYEX";
    uint8 public decimals = 18;

    function transfer(address recipient, uint amount) external returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool) {
        allowance[sender][msg.sender] -= amount;
        balanceOf[sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    }

    function mint(uint amount) external {
        balanceOf[msg.sender] += amount;
        totalSupply += amount;
        emit Transfer(address(0), msg.sender, amount);
    }

    function burn(uint amount) external {
        balanceOf[msg.sender] -= amount;
        totalSupply -= amount;
        emit Transfer(msg.sender, address(0), amount);
    }
}

众筹合约 CrowdFund.sol

import "./IERC20.sol";

contract CrowdFund {
    event Launch(
        uint id,
        address indexed creator,
        uint goal,
        uint32 startAt,
        uint32 endAt
    );

    event Cancel(uint id);
    event Pledge(uint indexed id, address indexed caller, uint amount);
    event Unpledge(uint indexed id, address indexed caller, uint amount);
    event Claim(uint id);
    event Refund(uint indexed id, address indexed caller, uint amount);

    //众筹活动结构体
    struct Campaign {
        address creator;
        uint goal;
        uint pledged; // 记录已筹款数量
        uint32 startAt; // 时间戳
        uint32 endAt;
        bool claimed; //领取状态
    }

    IERC20 public immutable token;
    uint public count; // 筹款活动计数器
    mapping(uint => Campaign) public campaigns; // 筹款列表
    mapping(uint => mapping(address => uint)) public pledgedAmount; // 记录用户筹款承诺数量

    constructor(address _token) {
        token = IERC20(_token);
    }

    // 创建众筹
    function launch(
        uint _goal, //众筹目标
        uint32 _startAt, //开始时间
        uint32 _endAt //结束时间
    ) external {
        require(_startAt >= block.timestamp, "start at < now"); require(_endAt >= _startAt, "end at < start at");
        // 筹款结束时间不能超过90天
        require(_endAt <= block.timestamp + 90 days, "end at > max duration");

        count += 1;
        campaigns[count] = Campaign({
            creator: msg.sender,
            goal: _goal,
            pledged: 0,
            startAt: _startAt,
            endAt: _endAt,
            claimed: false
        });

        emit Launch(count, msg.sender, _goal, _startAt, _endAt);
    }

    // 开始之前取消众筹
    function cancel(uint _id) external {
        Campaign memory campaign = campaigns[_id];
        require(msg.sender == campaign.creator, "not creator");
        require(block.timestamp < campaign.startAt, "started"); delete campaigns[_id]; emit Cancel(_id); } // 参与众筹 function pledge(uint _id, uint _amount) external { Campaign storage campaign = campaigns[_id]; //判断时间是否有效 require(block.timestamp >= campaign.startAt, "not started");
        require(block.timestamp <= campaign.endAt, "ended");

        campaign.pledged += _amount;
        pledgedAmount[_id][msg.sender] += _amount;
        token.transferFrom(msg.sender, address(this), _amount);

        emit Pledge(_id, msg.sender, _amount);
    }

    // 取消众筹数量
    function unpledge(uint _id, uint _amount) external {
        Campaign storage campaign = campaigns[_id];
        require(block.timestamp <= campaign.endAt, "ended"); campaign.pledged -= _amount; pledgedAmount[_id][msg.sender] -= _amount; token.transfer(msg.sender, _amount); emit Unpledge(_id, msg.sender, _amount); } //创建者达到目标把输量领取出来 function claim(uint _id) external { Campaign storage campaign = campaigns[_id]; require(msg.sender == campaign.creator, "not creator"); require(block.timestamp > campaign.endAt, "not ended");
        require(campaign.pledged >= campaign.goal, "goal targer not enough");
        require(!campaign.claimed, "claimed");

        campaign.claimed = true;
        token.transfer(msg.sender, campaign.pledged);

        emit Claim(_id);
    }

    // 没有达到目标,用户把token领取回去
    function refund(uint _id) external {
        Campaign storage campaign = campaigns[_id];
        //时间结束且众筹目标失败
        require(block.timestamp > campaign.endAt, "not ended");
        require(campaign.pledged < campaign.goal, "pledged < goal");
        //读取筹款数量 重置并发送给用户
        uint bal = pledgedAmount[_id][msg.sender];
        pledgedAmount[_id][msg.sender] = 0;
        token.transfer(msg.sender, bal);

        emit Refund(_id, msg.sender, bal);
    }
}

Create2部署合约
测试流程先获取bytecode用自己的地址,然后执行getAddress将bytecode和盐放入获取地址合约,再通过deploy部署后可以得到相同的地址

contract DeployWithCreate2 {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }
}

contract Create2Factory {
    event Deploy(address addr);

    //盐和新部署合约不变,地址不会变化,如果已经部署不能再部署,如果自毁合约再创建还是可以
    function deploy(uint _salt) external {
        DeployWithCreate2 _contract = new DeployWithCreate2{
            salt: bytes32(_salt)
        }(msg.sender);
        emit Deploy(address(_contract));
    }

    // 计算合约地址方法 bytecode就是机器码
    function getAddress(bytes memory bytecode, uint _salt)
        public
        view
        returns (address)
    {
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), address(this), _salt, keccak256(bytecode)
            )
        );

        // 转换为uint160地址标准格式
        return address(uint160(uint(hash)));
    }

    // 获取bytecode
    function getBytecode(address _owner) public pure returns (bytes memory) {
        bytes memory bytecode = type(DeployWithCreate2).creationCode;
        return abi.encodePacked(bytecode, abi.encode(_owner));
    }
}

Multi Call 合约
限制:同一个页面对合约进行几十次调用,而一个链RPC节点限制客户端对链的调用为,20秒间隔之内只能调用一次
需要把多个调用打包一起进行一次调用

测试流程:
1、部署测试合约和MultiCall合约
2、获取func1和func2的data
3、将要调用和合约地址数组和data数组传入MultiCall
4、获得返回数据是 ABI编码格式需要解码

contract TestMultiCall {
    function func1() external view returns (uint, uint) {
        return (1, block.timestamp);
    }

    function func2() external view returns (uint, uint) {
        return (2, block.timestamp);
    }

    //获取函数1调用data的方法 链外也可以用ethers或者web3这样SDK工具获取
    function getData1() external pure returns (bytes memory) {
        // abi.encodeWithSignature("func1()")
        return abi.encodeWithSelector(this.func1.selector);
    }

    //获取函数2调用data的方法
    function getData2() external pure returns (bytes memory) {
        // abi.encodeWithSignature("func2()")
        return abi.encodeWithSelector(this.func2.selector);
    }
}

contract MultiCall {
    // 参数1 地址数组 参数2 呼叫所需要调用的data数据
    function multiCall(address[] calldata targets, bytes[] calldata data)
        external
        view
        returns (bytes[] memory)
    {
        require(targets.length == data.length, "target length != data length");
        //定义返回值
        bytes[] memory results = new bytes[](data.length);

        for (uint i; i < targets.length; i++) {
            // 对目标地址进行静态调用
            (bool success, bytes memory result) = targets[i].staticcall(data[i]);
            require(success, "call failed");
            results[i] = result;
        }

        //返回值是ABI编码格式需要解码
        return results;
    }
}

多重委托调用合约 与 Multi Call区别
alice -> multi call — call –> test (msg.sender = multi call)
alice -> test — delegatecall –> test (msg.sender = alice)

多重委托调用合约
1、只可以应用自己的合约委托
2、不可以修改目标合约地址任何数据值
3、必须是自己编写的合约(要继承)

delegatecall使用起来很棘手,错误的使用或不正确的理解会导致毁灭性的后果。
漏洞:
1、合约漏洞一个ETH调用3次mint方法,会凭空出现3个ETH
解决方法
1、不打开支付币功能,不接受主币ETH
2、合约逻辑中不要重复计算主币数量

测试流程:(委托调用使用方法也是一样,漏洞测试就是多重委托调用合约)
部署测试合约和助手合约,通过助手合约获得铸造方法data为0x1249c58b
使用多重委托调用方法 进行调用三次并且附带1个eth币[“0x1249c58b”,”0x1249c58b”,”0x1249c58b”] 再进行余额查询将发现有3个ETH明显出错

contract MultiDelegatecall {
    error DelegatecakkFailed();

    function multiDelegatecall(bytes[] calldata data)
        external
        payable
        returns (bytes[] memory results)
    {
        results = new bytes[](data.length);

        for (uint i; i < data.length; i++) { (bool ok, bytes memory res)= address(this).delegatecall(data[i]); if (!ok) { revert DelegatecakkFailed(); } results[i] = res; } } } //委托调用合约继承到当前合约 contract TestMultiDelegatecall is MultiDelegatecall { event Log(address caller, string func, uint i); function func1(uint x, uint y) external { emit Log(msg.sender, "func1", x + y); } function func2() external returns (uint) { emit Log(msg.sender, "func2", 2); return 111; } mapping(address => uint) public balanceOf;

    //危险漏洞方法测试 铸造余额
    function mint() external payable {
        balanceOf[msg.sender] += msg.value;
    }
}

//函数调用需要的data数据
contract Helper {
    function getFunc1Data(uint x, uint y) external pure returns (bytes memory) {
        return abi.encodeWithSelector(TestMultiDelegatecall.func1.selector, x, y);
    }

    function getFunc2Data() external pure returns (bytes memory) {
         return abi.encodeWithSelector(TestMultiDelegatecall.func2.selector);
    }

    //漏洞测试
    function getMintData() external pure returns (bytes memory) {
        return abi.encodeWithSelector(TestMultiDelegatecall.mint.selector);
    }
}

ABI解码
测试流程:
1、部署合约编码输入参数
1 钱包地址0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 [1,2,3] [“okk”,[7,9]]
2、得到编码数据
0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000006f6b6b0000000000000000000000000000000000000000000000000000000000

3、将编码数据粘贴到解码函数中即可解码
0:
uint256: x 1
1:
address: addr 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
2:
uint256[]: arr 1,2,3
3:
tuple(string,uint256[2]): myStruct ,7,9
注意:这里元祖类型输入的字符串”okk” 在remix里无法显示,但是可以通过web3或者ethers其他工具把结果返回

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract AbiDecode{
    struct MyStruct {
        string name;
        uint[2] nums;
    }

    function encode(
        uint x,
        address addr,
        uint[] calldata arr,
        MyStruct calldata myStruct
    ) external pure returns (bytes memory) {
        // 编码多种数据格式进行测试
        return abi.encode(x, addr, arr, myStruct);
    }

    //解码参数的返回值要和编码参数一致
    function decode(bytes calldata data) 
        external pure
        returns (
            uint x,
            address addr,
            uint[] memory arr,
            MyStruct memory myStruct
        ) 
    {   
        //解码要知道数据的类型,否则无法解码
        (x, addr, arr, myStruct) = abi.decode(data, (uint, address, uint[], MyStruct));
    }
}