solidity8.0 笔记2-学习案例
接学习笔记 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)); } }