// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /* 哈希算法具有两个特性: 1. 输入值相同,输出值一定相同 2. 不管输入值有多大,输出值是定长的,并且哈希算法是不可逆向运算的 通常把哈希算法用在签名运算,或者是获取一个特定的id 执行签名与验证签名操作步骤: 1. getMessageHash 输入"secret message" 得到: 0x9c97d796ed69b7e69790ae723f51163056db3d55a7a6a82065780460162d4812 2. 浏览器按f12打开控制台,输入ethereum.enable() 前提要装小狐狸钱包,这个函数会获取一个地址: "0xEc80445eb3363b49c93a902662bD11e3C8D197E8" 3. 在浏览器定义变量: account = "0xEc80445eb3363b49c93a902662bD11e3C8D197E8" hash = "0x9c97d796ed69b7e69790ae723f51163056db3d55a7a6a82065780460162d4812" 4. 执行函数得到签名 ethereum.request({method: "personal_sign", params: [account, hash]}) 在小狐狸钱包点确认签名,得到签名数据: 0x686a1e541227e4edce4270a2508b486372eaa7eb63171605743b392d35f96ae721a4f58cc763497f7eb24b108e5fc9f6df1a0679c973d897c2d9d9ec0611cf0f1b 5. 将getMessageHash得到的哈希值放在getEthSignedMessageHash再次签名,得到: 0x95a786464acc06fafc0d46036515722ec35acb840ecc291f251e086ebfeb9099 6. 恢复签名进行验证在recover输入,参数1:步骤5哈希值 参数3:步骤4哈希值 点击call得到地址: 0xEc80445eb3363b49c93a902662bD11e3C8D197E8 该地址与步骤2地址相同,代表恢复完成了 7. 再使用验证方法完整的验证一遍,在verfiy输入,参数1:步骤2地址 参数2:消息原文"secret message" 参数3:签名之后的数据即步骤4哈希值 */ contract HashFunc { // 哈希返回值bytes32定长值 function hash(string memory text, uint num, address addr) external pure returns (bytes32) { // 使用keccak256计算哈希,要先通过abi打包 return keccak256(abi.encodePacked(text, num, addr)); } // 使用abi.encode方式打包 会将结果哈希值补0 function encode(string memory text1, string memory text2) external pure returns (bytes memory) { return abi.encode(text1, text2); } // 使用abi.encodePacded方式打包 不会将结果哈希值补0 不同的参数会产生相同的结果 // "AAAA","BBB" 与 "AAA","ABBB" 结果都是 0x41414141424242 function encodePacded(string memory text1, string memory text2) external pure returns (bytes memory) { return abi.encodePacked(text1, text2); } // 哈希碰撞实验,输入不同的参数来得到相同的哈希值 // "AAAA","BBB" 与 "AAA","ABBB" 结果都是 0x11db58448f2a53848bef361744f19e6fdabef68b8267b1ff669de1b4c42da0da // 避免这种错误有两种解决方案:1. 使用encode打包 2. 在两个字符串之间添加一个数字类型:"AAAA",123,"BBB" 与 "AAA",123,"ABBB" function collision(string memory text1, string memory text2) external pure returns (bytes32) { return keccak256(abi.encodePacked(text1, text2)); } } /* 通过智能合约来验证签名,验证签名分4个步骤: 1. 将消息签名 2. 将消息进行哈希 3. 再把消息和私钥进行签名(链下完成) 4. 恢复签名 */ contract VerfiySig { /* 定义消息签名验证函数 参数1. 签名人地址 参数2. 消息原文 参数3. 签名的结果 */ function verfiy(address _signer, string memory _message, bytes memory _sig) external pure returns (bool) { bytes32 messageHash = getMessageHash(_message); bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); // 恢复签名地址 return recover(ethSignedMessageHash, _sig) == _signer; } // 将消息进行哈希运算 function getMessageHash(string memory _message) public pure returns (bytes32) { return keccak256(abi.encodePacked(_message)); } // 将哈希值再次进行哈希运算 function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { // 这里需要进行两次哈希运行,可以增加破解难度, 一次哈希运算有破解的可能性 return keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", _messageHash )); } // 定义恢复函数 参数1:上面函数运算结果 参数2:不定长签名结果 function recover(bytes32 _ethSignedMessageHash, bytes memory _sig) public pure returns (address) { // 返回非对称加密三个值r,s,v (bytes32 r, bytes32 s, uint8 v) = _split(_sig); // 使用智能合约内部函数恢复签名 return ecrecover(_ethSignedMessageHash, v, r, s); } // 定义分割签名函数 输入长度要65位 32+32+1 uint8是1位 function _split(bytes memory _sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { require(_sig.length == 65, "invalid signature length"); // 使用内联汇编将bytes参数分割,原理就是签名参数就是用r、s、v三个参数拼接出来的 assembly { // 使用mload内存读取,读取签名变量,使用add跳过32位长度,获取到_sig32位之后的32位 r := mload(add(_sig, 32)) s := mload(add(_sig, 64)) v := byte(0, mload(add(_sig, 96))) } } }
/* 权限控制合约 */ contract AccessControl { // role => account => bool 嵌套映射,给地址升级角色 mapping(bytes32 => mapping(address => bool)) public roles; // 定义角色名称,采用哈希值。因为string字符串消耗gas要比bytes32哈希值消耗的gas多的多 // 角色定义后不可修改,用常量 这里可以先用public先部署合约获取了哈希值,再改为私有变量 bytes32 private constant ADMIN = keccak256(abi.encodePacked("ADMIN")); // 0xdf8b4c520ffe197c5343c6f5aec59570151ef9a492f2c624fd45ddde6135ec42 bytes32 private constant USER = keccak256(abi.encodePacked("USER")); // 0x2db9fd3d099848027c2383d0a083396f6c41510d7acfd92adc99b6cffcf31e96 // 定义升级权限事件 参数1:角色名称 参数2:给哪个地址升级权限 方便在链外查询要加indexed索引 event GrantRole(bytes32 indexed role, address indexed account); // 定义撤销权限事件 event RevokeRole(bytes32 indexed role, address indexed account); // 定义函数修改器,代表只有某一个角色才可以调用的修改器 modifier onlyRole(bytes32 _role) { require(roles[_role][msg.sender], "not authorized"); _; } // 定义构造,把当前部署者先设置为管理员 验证是否是管理员:roles 参数1输入admin哈希值,参数2输入地址 返回true表示为管理员 constructor() { _grantRole(ADMIN, msg.sender); } // 定义升级权限函数,给一个地址升级权限 内部使用使用下划线定义,内部函数和私有函数通常都用下划线定义。 // 定义为内部可视,因为该合约可能会被继承 function _grantRole(bytes32 _role, address _account) internal { // 这里修改了变量的值,按智能合约的编写习惯,修改链上值就一定要报出一个事件,所以要定义一个事件 roles[_role][_account] = true; // 触发升级权限事件,向链外抛出 emit GrantRole(_role, _account); } // 定义升级权限函数 定义为外部可视 只允许管理员使用 要使用自定义的修改器验证一下 让修改器判断当前用户角色是否是admin权限 function grantRole(bytes32 _role, address _account) external onlyRole(ADMIN) { _grantRole(_role, _account); } // 撤销权限 function revokeRole(bytes32 _role, address _account) external onlyRole(ADMIN) { roles[_role][_account] = false; emit RevokeRole(_role, _account); } } // 自毁合约 selfdestruct 两个功能:1. 删除合约 2. 强制发送主币到一个地址 contract Kill { // 构造函数创建主币 constructor() payable {} function kill() external { // 把合约剩余的主币发送到指定的地址中 强制发送 selfdestruct(payable(msg.sender)); } // 如果合约自毁了,这个函数也无法返回 function testCall() external pure returns (uint) { return 123; } } // 测试自毁合约 contract TestSelfdestruct { function getBalance() external view returns (uint) { return address(this).balance; } // 调用kill合约的自毁合约,强制从Kill合约转账到本合约 参数Kill合约的地址 function kill(Kill _kill) external { _kill.kill(); } } /* 实战练习,小猪存钱罐合约 该合约可以让任何人的地址向该合约发送主币,存钱罐的拥有者可以从存钱罐取出主币,当取出之后,该合约会自毁掉,像存钱罐打碎一样 */ contract PiggyBank { address public owner = msg.sender; // 定义收款事件 event Deposit(uint amount); // 定义取款事件 event Withdraw(uint amount); // 创建收款方法,该方法是回退函数。 存款时切换另一个账号,输入1ETH,在最下面低级交互那里,点击Transact 完成存款 receive() external payable { emit Deposit(msg.value); } // 创建取款方法,取款方法必须由合约部署者调用 function withdraw() external { require(msg.sender == owner, "not owner"); emit Withdraw(address(this).balance); selfdestruct(payable(msg.sender)); } } /* ERC20标准合约,ERC20标准只包括接口。只要满足了IERC20接口,就代表满足了ERC20合约的标准 至于方法怎样实现,是自己的事情,方法怎样实现没有强制要求 */ interface IERC20 { // 代表当前合约的token总量 function totalSupply() external view returns (uint); // 代表某一个账户的当前余额 function balanceOf(address account) external view returns (uint); // 把账户中的余额由当前调用者发送到另一个账号中 该方法是写入发送,需要调用Transfer事件进行对外汇报,通过Transfer事件就能够查询token的扭转了 function transfer(address recipient, uint amount) external returns (bool); // 通过allowance查询某一个账户对另一个账号的批准额度有多少 function allowance(address owner, address spender) external view returns (uint); // 批准,代表把我账号中的数量批准给另一个账号 function approve(address spender, uint amount) external returns (bool); // 向另一个合约存款的时候,另一个合约必须要调用该方法才能够把我们账号的token转到他的合约中,需要和approve批准方法联合使用 function transferFrom(address sender, address recipient, uint amount) external returns (bool); event Transfer(address indexed from, address indexed to, uint amount); event Approval(address indexed owner, address indexed spender, uint amount); } contract ERC20 is IERC20 { // token总量 uint public totalSupply; // 地址到数字映射 一个地址对应一个数字就可以在组成一个账本 ERC20的核心账本 mapping(address => uint) public balanceOf; // 定义批准映射,嵌套行映射,由一个发送者的地址对应一个被批准的地址,再对应一个数量,才能够组成批准的映射 mapping(address => mapping(address => uint)) public allowance; // 定义ERC20 token名称 string public name = "Making Big Money"; // 定义ERC20 token简写 string public symbol = "MBM"; // 定义token精度 uint public decimals = 18; address public owner; constructor() { owner = msg.sender; } /* 发送函数 把账户中的余额由当前调用者发送到另一个账号中 基本逻辑,在发送者账号中减掉一个数量,在接受者账号中增加一个数量 操作的变量就是balanceOf */ function transfer(address recipient, uint amount) external returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[recipient] += amount; // 用于链外统计 emit Transfer(msg.sender, recipient, amount); return true; } // 通过allowance查询某一个账户对另一个账号的批准额度有多少 //function allowance(address owner, address spender) external view returns (uint); /* 批准函数,代表把我账号中的数量批准给另一个账号 批准基本逻辑就是修改批准映射,在批准映射中首先找到当前合约调用者 */ function approve(address spender, uint amount) external returns (bool) { // spender表示被授权账户,amount设置为0取消授权 allowance[msg.sender][spender] = amount; // 触发批准额度的事件 emit Approval(msg.sender, spender, amount); return true; } /* 向另一个合约存款的时候,另一个合约必须要调用该方法才能够把我们账号的token转到他的合约中,需要和approve批准方法联合使用 参数1:发送者,参数2:接收者,参数:3数量 该函数的调用者对应的就是批准额度中的被批准账户spender;发送者对应的就是批准额度中的调用者 */ 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; } modifier onlyOwner() { require(msg.sender == owner,"not owner"); _; } function setOwner(address _newOwner) external onlyOwner { require(_newOwner != address(0),"invalid address"); owner = _newOwner; } // 铸币方法,由合约管理员调用 合约开始部署后没有token function mint(uint amount) external onlyOwner { balanceOf[msg.sender] += amount; totalSupply += amount; // 从零地址发出的都是铸币事件 emit Transfer(address(0), msg.sender, amount); } // 定义销毁事件 销毁token function burn(uint amount) external onlyOwner { balanceOf[msg.sender] -= amount; totalSupply -= amount; emit Transfer(msg.sender, address(0), amount); } } /* 多签钱包 多签钱包的功能是必须在合约中由多个人同意的情况下,才能够将主币向外转出 执行步骤: 1. 执行submit 提交交易 在控制台找到交易id 2. 执行approve 批准交易,可能需多人批准,即多个签名人执行 3. 执行execute 满足条件发送token,完成交易 */ 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); // 定义数组 保存合约中的所有签名人 合约可能有多个拥有者 address[] private owners; /* 因为数组中不能够快速查找 查找数组中是否有某一个地址就必须循环这样做很浪费gas 所有再制作一个映射,由地址到布尔值的映射,如果想查找某一个用户是否是签名人地址,就去映射查询一下返回值 */ mapping(address => bool) private isOwner; // 定义确认数,不管合约中签名人有多少人,必须满足签名人的确认的数量,这笔钱才能够转出去 uint private required; /* 定义交易的结构体 该结构体保存着每一次对外发出主币的数据 这个数据由一个签名人发起提议,其他签名人去同意通过批准,最后完成这笔交易 */ struct Transaction { address to; // 这笔交易的目标发送地址 uint value; // 交易发送的主币数量 bytes data; // 如果目标地址是合约地址,那么还可以执行一些合约中的函数 bool executed; // 标记这笔交易是否执行成功 如果执行成功,就不能重复执行 } // 定义数组记录合约中所有的交易 数组的索引值就是交易的id号 Transaction[] private transactions; /* 定义映射,由交易的id号对应一个签名人地址,再对应一个布尔值 它的功能就是用来记录某一个交易id之下,某一个签名人的地址是否批准了这次交易,布尔默认是false */ mapping(uint => mapping(address => bool)) private approved; // 定义构造函数,参数1:合约所有签名人地址的数组,地址数组存储在内存中,使用memory标记,参数2,规定最小的确认数是多少 constructor(address[] memory _owners, uint _required) { // 签名人数组没人就没有意义了 require(_owners.length > 0, "owners required"); // 确认数不能小于0并且不能大于数组长度 require(_required > 0 && _required <= _owners.length , "invalid required number of owners"); // 把地址遍历出来,并且判断地址是不是有效地址,并且不能在isOwner中已存在 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; } // 定义回退函数,让合约可以接收主币(接收token) receive() external payable { // 触发收款事件 记录谁发送的,及收到的金额 emit Deposit(msg.sender, msg.value); } // 定义权证验证修改器 验证函数执行人是否是签名人数组中的成员 就是验证是否是管理员的意思 modifier onlyOwner() { require(isOwner[msg.sender], "not owner"); _; } // 验证交易id是否存在修改器 modifier txExists(uint _txId) { // 因为交易id用的是数组的索引,索引只需判断交易id小于数组的长度 require(_txId < transactions.length, "tx does not exist"); _; } // 验证当前交易id,当前签名人没有批准过这次交易 modifier notApproved(uint _txId) { require(!approved[_txId][msg.sender], "tx already approved"); _; } // 验证这个交易id没有被执行过 modifier notExecuted(uint _txId) { require(!transactions[_txId].executed, "tx already executed"); _; } /* 定义提交交易的函数 把交易结构体创建出来,推入到数组中,记录一笔交易 参数1:转账目标地址 参数2:转账金额 参数3:如果目标地址是合约地址,还可以触发它的一些函数 onlyOwner 验证该函数执行人是否是签名人 */ function submit(address _to, uint _value, bytes calldata _data) external onlyOwner { transactions.push(Transaction({ to: _to, value: _value, data: _data, executed: false })); // 触发交易事件,需要传入交易id。刚刚把数据推入到数组中了,所以数组的长度-1就是交易id emit Submit(transactions.length -1); } /* 定义批准函数 批准函数的功能是当一个签名人提交了一个交易申请之后,由其他签名人针对这次交易的id号进行批准 当达到了最小确认数的时候,这个交易就能够被执行了 当前函数也要由签名人执行,并且判断这个交易号是否存在,并且判断这个签名人针对这个交易号是否已经批准过了,并且判断这个交易id是否是已经执行过的 */ function approve(uint _txId) external onlyOwner txExists(_txId) notApproved(_txId) notExecuted(_txId) { approved[_txId][msg.sender] = true; // 触发交易批准事件 emit Approve(msg.sender, _txId); } // 定义内部函数,计算某一个交易id之下的签名人有多少人批准了这次交易的统计,方便其他业务逻辑使用 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) { // 首先判断当前交易id的确认数是不是满足最小确认数 require(_getApprovalCount(_txId) >= required, "approvals < required"); // 用当前交易id从当前交易的结构体中提取出来,定义到状态变量,因为还要修改值 Transaction storage transaction = transactions[_txId]; transaction.executed = true; // 使用低级函数向目标地址发送token 对目标地址进行低级call。 transaction.data对目标地址传输一些数据,如果目标地址是合约地址,合约地址的一些方法也会被我们执行了 (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); } }
满足条件,发送token成功,不满足条件抛出错误
/* 函数签名 函数签名也叫做函数的选择器,用来代表一个智能合约中虚拟机是如何找到一个函数的 */ contract Receiver { // 通过message data看一下到底什么样的数据发送到虚拟机中 // 定义事件,通过这个事件向链外反映一下当前的合约的函数收到了什么样的数据 event Log(bytes data); function transfer(address _to, uint _amount) external { // 控制台打印事件 emit Log(msg.data); /* 在控制台找到Log事件的data数据,这个事件就是向智能合约中调用函数链外向链上发送的数据 拆分该data数据: 第一部分4字节的bytes类型,这部分有8个字符 第二部分就是输入的地址 第三部分就是输入的金额,数字类型,输入的111,这里被转换成了16进制 0xa9059cbb 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc 4000000000000000000000000000000000000000000000000000000000000006f 在这里我们就能看出来,呼叫一个函数的数据有两部分组成: 1. 第一部分就是函数的选择器,也叫做函数的签名,第二部分就是参数 虚拟机是如何知道这个签名对应着这个函数呢? 这个函数的签名其实就是通过将函数的名称和它的参数类型打包在一起,进行哈西值,然后取哈希值的前4位十六进制数字得到的结果 */ } } contract FunctionSelector { /* 将方法转换成十六进制签名 参数写法: "transfer(address,uint256)" 要加引号。得到结果: 0xa9059cbb,这个值和刚才调用transfer方法得到的messagedata前4位十六进制bytes数据是一样的 结论:在智能合约的虚拟机中,调用一个函数,要通过函数的选择器去区分函数 */ function getSelector(string calldata _func) external pure returns (bytes4) { return bytes4(keccak256(bytes(_func))); } } /*************************************** 荷兰拍卖开始 **************************************/ /* 荷兰拍卖 报价越来越低 constant修饰常量,常量值一开始已知 immutable修饰常量的另一种形式,常量值一开始未知,后续设置上值后,不可修改 执行步骤: 1. 部署ERC721合约 2. 部署好ERC721合约之后,用mint方法给当前账号铸造一个nft,参数1:当前账号地址 参数2:nft编号,随便写,输入777 3. 部署荷兰拍卖合约,在构造中输入,参数1:起拍价,设置为1000000,参数2:每秒折扣,设置1,参数3:nft的合约地址,参数4,nft的id号 777 4. 部署好荷兰拍卖合约之后,在ERC721合约中对荷兰拍的合约进行批准操作,批准之后才有权利调用账户中编号为777的id的nft,才能够使用transferFrom方法 在nft合约approve方法输入,参数1:当前荷兰拍卖合约地址 参数2:tokenId 输入777 5. 查看荷兰拍卖合约的当前的拍卖价格,价格在不断减少,复制当前价格 6. 切换账户,输入刚拷贝的价格,在拍卖合约中点击buy,购买成功。在nft合约查询编号777现在的所有者,在ownerOf中输入777 获取当前所有者进行确认。 */ interface IERC165 { function supportsInterface(bytes4 interfaceID) external view returns (bool); } interface IERC721 is IERC165 { function balanceOf(address owner) external view returns (uint balance); function ownerOf(uint tokenId) external view returns (address owner); function safeTransferFrom( address from, address to, uint tokenId ) external; function safeTransferFrom( address from, address to, uint tokenId, bytes calldata data ) external; function transferFrom( address from, address to, uint tokenId ) external; function approve(address to, uint tokenId) external; function getApproved(uint tokenId) external view returns (address operator); function setApprovalForAll(address operator, bool _approved) external; function isApprovedForAll(address owner, address operator) external view returns (bool); } interface IERC721Receiver { function onERC721Received( address operator, address from, uint tokenId, bytes calldata data ) external returns (bytes4); } contract ERC721 is IERC721 { using Address for address; event Transfer(address indexed from, address indexed to, uint indexed tokenId); event Approval( address indexed owner, address indexed approved, uint indexed tokenId ); event ApprovalForAll( address indexed owner, address indexed operator, bool approved ); // Mapping from token ID to owner address mapping(uint => address) private _owners; // Mapping owner address to token count mapping(address => uint) private _balances; // Mapping from token ID to approved address mapping(uint => address) private _tokenApprovals; // Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private _operatorApprovals; function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC165).interfaceId; } function balanceOf(address owner) external view override returns (uint) { require(owner != address(0), "owner = zero address"); return _balances[owner]; } function ownerOf(uint tokenId) public view override returns (address owner) { owner = _owners[tokenId]; require(owner != address(0), "token doesn't exist"); } function isApprovedForAll(address owner, address operator) external view override returns (bool) { return _operatorApprovals[owner][operator]; } function setApprovalForAll(address operator, bool approved) external override { _operatorApprovals[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } function getApproved(uint tokenId) external view override returns (address) { require(_owners[tokenId] != address(0), "token doesn't exist"); return _tokenApprovals[tokenId]; } function _approve( address owner, address to, uint tokenId ) private { _tokenApprovals[tokenId] = to; emit Approval(owner, to, tokenId); } function approve(address to, uint tokenId) external override { address owner = _owners[tokenId]; require( msg.sender == owner || _operatorApprovals[owner][msg.sender], "not owner nor approved for all" ); _approve(owner, to, tokenId); } function _isApprovedOrOwner( address owner, address spender, uint tokenId ) private view returns (bool) { return (spender == owner || _tokenApprovals[tokenId] == spender || _operatorApprovals[owner][spender]); } function _transfer( address owner, address from, address to, uint tokenId ) private { require(from == owner, "not owner"); require(to != address(0), "transfer to the zero address"); _approve(owner, address(0), tokenId); _balances[from] -= 1; _balances[to] += 1; _owners[tokenId] = to; emit Transfer(from, to, tokenId); } function transferFrom( address from, address to, uint tokenId ) external override { address owner = ownerOf(tokenId); require( _isApprovedOrOwner(owner, msg.sender, tokenId), "not owner nor approved" ); _transfer(owner, from, to, tokenId); } function _checkOnERC721Received( address from, address to, uint tokenId, bytes memory _data ) private returns (bool) { if (to.isContract()) { return IERC721Receiver(to).onERC721Received( msg.sender, from, tokenId, _data ) == IERC721Receiver.onERC721Received.selector; } else { return true; } } function _safeTransfer( address owner, address from, address to, uint tokenId, bytes memory _data ) private { _transfer(owner, from, to, tokenId); require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver"); } function safeTransferFrom( address from, address to, uint tokenId, bytes memory _data ) public override { address owner = ownerOf(tokenId); require( _isApprovedOrOwner(owner, msg.sender, tokenId), "not owner nor approved" ); _safeTransfer(owner, from, to, tokenId, _data); } function safeTransferFrom( address from, address to, uint tokenId ) external override { safeTransferFrom(from, to, tokenId, ""); } function mint(address to, uint tokenId) external { require(to != address(0), "mint to zero address"); require(_owners[tokenId] == address(0), "token already minted"); _balances[to] += 1; _owners[tokenId] = to; emit Transfer(address(0), to, tokenId); } function burn(uint tokenId) external { address owner = ownerOf(tokenId); _approve(owner, address(0), tokenId); _balances[owner] -= 1; delete _owners[tokenId]; emit Transfer(owner, address(0), tokenId); } } library Address { function isContract(address account) internal view returns (bool) { uint size; assembly { size := extcodesize(account) } return size > 0; } } // 定义荷兰拍卖合约 contract DutchAuction { // 定义拍卖时间周期 7天 uint private constant DURATION = 7 days; // 这次拍卖的是IERC721合约 定义为不可变量,地址好设置后就不许更改 IERC721 public immutable nft; // 一个合约只对应一个nft uint public immutable nftId; // 销售者地址 当前nft持有者 由他的账号中拍卖出去 address payable public immutable seller; // nft起拍价格 不可变量,设置好后不许更改 uint public immutable startingPrice; // 拍卖的开始时间 uint public immutable startAt; // 拍卖的过期时间 拍卖过期后没卖出去就相当于流拍 uint public immutable expiresAt; // 定义每一秒的折扣率 随着时间的流逝,每一秒都在起拍价格的基础之上减去一个数量 uint public immutable discountRate; // 定义构造,在构造中传值,为immutable修饰的常量赋值 constructor( uint _startingPrice, // 起拍价格 uint _discountRate, // 每一秒的折扣率 address _nft, // nft的合约地址 uint _nftId // nft的id ) { seller = payable(msg.sender); // 销售者就是合约的部署者 销售成功之后,要把主币发送给销售者,需使用payable括起来 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) { // 流逝的时间 = 当前时间-起拍开始时间 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"); // 将nft从拍卖者账号转移到购买者账户 nft.transferFrom(seller, msg.sender, nftId); // 因为无法保证购买者从发送交易开始,到区块确认成功之后,这个价格没有发生变化 // 所以定义退款金额,如果购买者发送的价格大于销售价,把多付的钱退回给nft购买者 uint refund = msg.value - price; if (refund >0) { payable(msg.sender).transfer(refund); } // 当把nft发送给购买并且把多付的钱给购买者退回去之后,把这次拍卖销售所得的主币的数量发送到出售者的账户中 // 使用自毁合约,让这次销售的合约自毁掉,因为这次部署的拍卖合约在拍卖完成之后也就没有任何作用了 // 使用自毁合约,不仅能将合约上剩余的主币发送给销售者的账户中,还可以将这次合约部署的时候占用的空间消耗的gas也退还给销售者的账户中 selfdestruct(seller); } }
编号为777的nft持有人由拍卖人变成了账号2,账号2竞拍成功
/*************************************** 荷兰拍卖结束 **************************************/
/* 英式拍卖 执行步骤: 1. 部署ERC721合约 2. 部署好ERC721合约之后,用mint方法给当前账号铸造一个nft,参数1:当前账号地址 参数2:nft编号,随便写,输入77 3. 部署英国拍卖合约,填入构造参数,参数1:nft合约地址,参数2:nft编号 77 参数3:起拍价格,输入1 4. 部署好英国拍卖合约之后,在ERC721合约中对英国拍的合约进行批准操作,批准之后才有权利调用账户中编号为77的id的nft,才能够使用transferFrom方法 在nft合约approve方法输入,参数1:当前英国拍卖合约地址 参数2:tokenId 输入77 5. 切换至账户2,用第二个账户参与拍卖,输入10 wei,点击bid 6. 切换至账户3,用第三个账户参与拍卖,输入20 wei,点击bid 7. 切换至账户2,用第二个账户参与拍卖,输入30 wei,点击bid,再次参与竞拍 8. 这个时候拍卖可能结束了,设置的是60秒,用于测试 9. 查看当前最高出价、及最高出价地址 分别点击highestBid、highestBidder 确实为账户2,意味账户2竞拍成功,nft就会发送到他的账户地址上 10. 查看第二个账户有多少主币可以退还,在bids输入第二个账户地址,发现有10个,代表第一次出价的10个wei是可以退还的 11. 查看第三个账户有多少主币可以退还,在bids输入第三个账户地址,发现有20个,他没用竞拍成功,退还给他 12. 点击结束竞拍end,在nft合约ownerOf输入编号77,查询当77编号持有人已经变更为第二个账户 13. 第二、三个账号点击withdraw取回参与竞拍的主币,第二个账户退10个,第三个账号退20个。 英国拍卖全部流程结束 */ contract EnglishAuction { // nft合约的token地址 IERC721 public immutable nft; // nftId 这个合约只能拍卖一个nft,想拍卖更多,需要再创建该合约 uint public immutable nftId; // nft销售者地址 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; // 定义开始事件,无参数 event Start(); // 定义参与拍卖事件 参数1:参与拍卖的用户地址, 参数2:参与拍卖的金额 event Bid(address indexed sender, uint amount); // 定义参与取款事件 参数1:竞拍者,参数2:取款金额 event Withdraw(address indexed bidder, uint amount); // 定义结束拍卖事件 参数1:最高出价者,参数2:最高出价金额 结束事件只会触发一次,所以不需要加indexed索引 event End(address bighestBidder, uint amount); constructor( address _nft, // nft地址 uint _nftId, // nftId uint _startingBid // 起拍价格 ) { nft= IERC721(_nft); nftId = _nftId; seller = payable(msg.sender); // 销售者定义为当前合约部署者 highestBid = _startingBid; } // 定义开始拍卖函数 function start() external { // 这个函数只能由部署者(nft拍卖者)调用 require(msg.sender == seller, "not seller"); // 判断是否开始 require(!started, "started"); // 设置为拍卖已经开始 started = true; // 定义拍卖结束的时间 当前时间向后顺延多少时间 60为60秒, 7 days是7天,当前区块时间戳是uint256类型需要转换成uint32类型 endAt = uint32(block.timestamp + 60); // 把nft发送到当前合约 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 < highestBid"); // 这次产生了新的出价者,我们就要把上次最高出价者所支付的主币退回给他,该退回多少呢,退回的就是上一次的最高出价 // 也就是我们在定义这次最高出价者和最高出价数量之前,先把上一次的最高出价者,以他为主键,以他的出价为数值,进行一个累加的记录 // 这种情况之下,如果上一个最高出价者没有竞拍成功,我们可以依据这个累加记录把主币退还给他 // 如果第一次有人出价,那么上一个出价地址就是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]; // 把数取出来之后,把该值归零,防止一些漏洞发生 bids[msg.sender] = 0; // 发送主币,把之前出价退还回去 payable(msg.sender).transfer(bal); // 触发取款事件 emit Withdraw(msg.sender, bal); } // 定义结束拍卖 任何人都可以调用,不需要去确认调用者身份 function end() external { // 必须确认开始的状态是true的状态 require(started, "not started"); // 判断未结束 require(!ended, "ended"); // 判断是否结束 require(block.timestamp >= endAt, "not ended"); // 更改结束状态 ended = true; // 需要有人出过价 才可以发送nft if (highestBidder != address(0)) { // 向最高出价者发送nft,从当前账号地址发送到最高者账户地址 nft.transferFrom(address(this), highestBidder, nftId); // 把合约中的主币发送到销售者的账号上,发送的数量已最高出价为准 seller.transfer(highestBid); } else { // 流拍,把nft从当前合约发送给销售者,还给销售者 nft.transferFrom(address(this),seller,nftId); } // 触发结束拍卖事件 emit End(highestBidder, highestBid); } } /* 众筹合约 众筹合约中使用的IERC20.sol接口合约及实现合约在文章上面有定义 执行步骤: 1. 部署ERC20合约 2. 部署众筹合约,构造参数为ERC20合约地址 3. 部署好众筹合约之后,用第一个账户去创建众筹活动,用第二个账户参与这个众筹 4. 先用ERC20合约用mint方法铸造500个wei的token 5. 使用第一个账户创建一个众筹活动,方法launch,参数1:众筹目标100wei 参数2:开始时间 参数3:结束时间 在浏览器按f12,输入:new Date().getTime() / 1000 获取当前时间戳作为开始开始,结束时间在开始时间上稍微加100 用于测试 或者使用定义的getBlockTime方法 6. 给账号2进行approve授权, 参数1:账号2地址,参数2:100,授权后用transfer给账号2转账100个token 切换到账号2,用户账号2给在ERC合约给众筹合约approve授权,参数1:众筹合约地址,参数2:100。作用:众筹合约可以调用ERC20的transferFrom方法 7. 使用第二个账户参与活动,方法pledge,参数1:活动id,输入1 参数2:参与金额 输入100 8. 用id查询众筹 方法campaigns 9. 查询用户参与的众筹信息 pledgeAmount 参数1:id,参数2,用户地址 10. 切换到第一个账户,把众筹的钱取走 方法claim 参数:活动id,输入1 */ contract CrowdFund { // 定义众筹活动的结构体 struct Campaign { address creator; // 众筹创建者地址 uint goal; // 众筹金额目标,所要筹集的token的数量 uint pledged; // 已经参与的数量 uint32 startAt; // 众筹开始时间 都是时间戳 uint32 endAt; // 众筹结束时间 bool claimed; // 标记这次众筹是否被创建者领取 } // 每一个众筹都要有一个token IERC20 public immutable token; // 定义计数器,当做筹款活动id uint public count; // 定义映射,用筹款活动id对应众筹结构体 mapping(uint => Campaign) public campaigns; // 定义参与活动的映射,筹款活动id对应参与者地址及参与的金额 mapping(uint => mapping(address => uint)) public pledgedAmount; // 定义构造 constructor(address _token) { token = IERC20(_token); } // 定义众筹已经创建事件 id来自计数器 event Launch(uint id, address indexed creator, uint goal, uint32 startAt, uint32 endAt); // 取消众筹事件 event Cancel(uint id); // 定义参与众筹事件 代表有用户参与到了本次众筹 本地活动id加索引,因为该id会有很多人捐款 根据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); // 定义用户取回捐赠token事件 event Refund(uint indexed id, address indexed caller, uint amount); function getBlockTime() external view returns (uint256 blockTime) { blockTime = block.timestamp; } function getUserE20TokenNum() external view returns (uint256 amount) { amount = token.balanceOf(msg.sender); } // 创建众筹 参数要完成的众筹目标,开始时间,结束时间 function launch(uint _goal, uint32 _startAt, uint32 _endAt) external { // 判断开始时间和结束时间 require(_startAt >= block.timestamp, "start at < now"); require(_endAt >= _startAt, "end at < start at"); require(_endAt <= block.timestamp + 90 days, "end at > max duration"); count += 1; campaigns[count] = Campaign({ creator: msg.sender, // 创建者为当前方法调用者 goal: _goal, // 众筹目标 pledged: 0, // 已参与金额,默认值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"); // 取消众筹 把映射值删除 删除之后再根据id查询该众筹就查询不到了 delete campaigns[_id]; // 触发取消众筹事件 emit Cancel(_id); } // 参与众筹 就是其他用户捐钱给本合约 钱指token,token用钱买的 function pledge(uint _id, uint _amount) external { // 由其他用户参与众筹,其他用户要把他的token转移进来 要修改结构体需要使用storage修饰 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发到本合约 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返还给用户 由当前合约直接发送,不需要使用transferFrom了 token.transfer(msg.sender, _amount); // 触发用户撤销捐赠事件 emit Unpledge(_id, msg.sender, _amount); } // 当众筹的钱(token)达到设定目标的时候,众筹的创建者就可以把用户捐的钱(token)按照数量领取出来 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, "pledged < goal"); // 判断只能领取一次,不能重复领取 因为众筹合约中有别人的众筹,重复领取就把别人的领走了 require(!campaign.claimed, "claimed"); // 把已领取改为true campaign.claimed = true; // 把筹集到的钱给众筹发起人打过去。使用msg.sender,而不使用结构体中的creator,主要是节省gas,msg.sender在内存中不浪费gas token.transfer(msg.sender, campaign.pledged); // 触发众筹发起人已经领取事件 emit Claim(_id); } // 众筹活动结束没有达到众筹目标,用户还可以把自己的钱领取回去 这个方法在实际有必要定义吗? 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); // 触发用户取回自己捐赠的token事件 emit Refund(_id, msg.sender, bal); } }
众筹成功:
/* Create2部署合约 合约部署合约 之前new合约方法,在工厂合约里去部署合约,部署的新合约的地址,是通过工厂合约的地址和工厂合约对外发出交易的nonce值计算出来的新合约地址。 而Create2合法方法,是用工厂合约的地址,再加上一个盐,去计算未来新部署合约的地址,所以新部署合约的地址在部署之前就可以被预测出来 */ contract DeployWithCreate2 { address public owner; constructor(address _owner) { owner = _owner; } } contract Create2Factory { event Deploy(address addr); function deploy(uint _salt) external { // Create2部署就是加个大括号,括号内容: salt: bytes32 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) { // 打包四个元素,参数1:固定的字符串 参数2:当时合约的地址 参数3:盐 参数4:新合约源代码的机器码的哈希值 bytes32 hash = keccak256(abi.encodePacked( bytes1(0xff), address(this), _salt, keccak256(bytecode) )); // uint160 地址的标准格式 return address(uint160(uint(hash))); } // 获取机器码 function getBytecode(address _owner) public pure returns (bytes memory) { bytes memory bytecode = type(DeployWithCreate2).creationCode; // 打包新合约和新合约的构造 return abi.encodePacked(bytecode, abi.encode(_owner)); } }
使用Create2部署,在合约未部署之前,就可以知道要部署的地址:
/* MultiCall 多重呼叫 MultiCall功能是可以把对一个合约或多个合约的多次函数调用打包整合在一个交易中,对合约再进行调用 这样做的好处是,有时我们需要在同一个网站前端页面中,对合约进行几十次调用,而一个链的rpc节点又限制了每一个客户端对链的调用,在20秒间隔之内只能够调用一次 所以我们就要把多个合约的读取的调用打包在一起,成为一次调用,这样就可以在一次调用中把想要的数据都读取出来了 执行步骤: 1. 部署TestMultiCall合约 2. 部署MultiCall合约 3. 在测试合约分别点击getData1、getData2 获取两个函数的呼叫data数据 4. 打开MultiCall合约,在multiCall处输入参数,参数1:地址数组,两次test合约的数组,因为两次调用都是调用同一个合约,在数组中用引号包含地址 参数2:data数组,拷贝步骤3的数据,数组中也要用引号引起来,两个元素用逗号分隔 5. 步骤4执行完成之后得到bytes数组,这个数据是一组abi编码格式。 返回值1表示func1的参数1,返回值2表示func2的参数2. 我们看到fun1的时间戳和func2的时间戳相等,都是655b3b75,代表他们在同一个区块被调用出来的, 这样就完成了MultiCall多重呼叫的解决方案 0x0000000000000000000000000000000000000000000000000000000000000001 00000000000000000000000000000000000000000000000000000000655b3b75, 0x0000000000000000000000000000000000000000000000000000000000000002 00000000000000000000000000000000000000000000000000000000655b3b75 */ contract TestMultiCall { /* 调用func1和func2会有两次调用,有一个问题,调用func1的时候会返回当前时间戳,调用func2的时候还会返回当前时间戳 但是因为网络传输和节点调用的一些限制,返回的第二个时间戳,就可能会和第一个时间戳不一致, 如果我们需要获取两个函数在同一个区块中的状态时,这样的两次调用就无法获取到两个函数在同一个区块中的状态了 所以需要制作一个MultiCall合约,通过MultiCall合约把两个函数调用打包整合在一起,在一次调用中完成两个函数同时调用 */ function func1() external view returns (uint, uint) { return (1, block.timestamp); } function func2() external view returns (uint, uint) { return (2, block.timestamp); } /* 获取func1()的data multiCall传入的参数该怎么编写,传入的变量中有一个data数据,这个data数据就是在调用第一个函数和第二个函数时, 对于区块链上真实发送的交易的input数据,我们怎样得到它呢,可以通过编写一个函数去获取它,在链外也可以通过web3这样的sdk工具去编写脚本获取 */ function getData1() external pure returns (bytes memory) { // 使用带有选择器的编码形式 等价用签名的这种: abi.encodeWithSignature("func1()"); return abi.encodeWithSelector(this.func1.selector); } function getData2() external pure returns (bytes memory) { // 使用带有选择器的编码形式 等价用签名的这种: abi.encodeWithSignature("func2()"); return abi.encodeWithSelector(this.func2.selector); } } contract MultiCall { /* 参数1:两次调用,分别调用的哪一个合约的地址 参数2:两次调用对合约发出的datas数据 */ 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); // 对目标地址进行静态调用 返回值是用abi编码形式返回的 for (uint i; i使用MultiCall,两个函数在同一个区块被调出:
/* 多重委托调用 如果使用MultiCall多重调用,那看到的msg.sender地址是MultiCall合约的地址,而并不是当前真实调用者地址 使用多重委托调用看到的msg.sender地址是自己的地址 执行步骤: 1. 部署TestMultiDelegatecall合约 2. 部署MultiDelegatecallHelper合约 分别执行getFunc1Data、getFunc2Data两个方法,得到bytes数据 3. 在TestMultiDelegatecall合约的multDelegatecall方法,输入部署2得到的数组,用数组形式传递,示例:["Ox111","Ox222"] 4. 执行multDelegatecall,在控制台找到交易记录,查看汇报的事件,可以看到被执行的两个函数的caller地址都是当前真实者调用地址,说明多重委托调用成功了 多重的委托调用必须只能够调用合约的自身,可以把多重委托调用的合约制作成一个抽象合约,然后被自己的合约继承下来,这样就可以了 多重委托调用可能会为合约带来一定的漏洞,实际支付了1个ETH,实际得到了3个(看参数,有多少参数就是多少个),漏洞复现: 1. 执行getMintData得到bytes数据,复制该数据 2. 在multDelegatecall填写参数,填写刚才的数据,在数组中填写多次,例如:["0x1249c58b","0x1249c58b","0x1249c58b"] 3. 在以太币数量处填写1个Ether 4. 执行multDelegatecall方法,支持成功后,使用balanceOf查看当前当前账号,有3个ETH,实际只发送了一个,得到了3个 原因,多重委托调用重复调用mint()的时候,账户金额增加了3次 在使用多重委托调用的时候,要注意合约的逻辑中,不要重复计算主币数量,或者让多重委托调用方法不能接收主币 */ contract MultDelegatecall { error DelegatecallFailed(); // 参数:调用函数所需要发送的data数据 function multDelegatecall(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) { // 返回false ,委托调用失败,对外报出失败 revert DelegatecallFailed(); } results[i] = res; } } } contract TestMultiDelegatecall is MultDelegatecall { 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; // 为调用者铸造余额,每次铸造余额都是消息调用的主币数量就是msg.value function mint() external payable { balanceOf[msg.sender] += msg.value; } } // 获取TestMultiDelegatecall合约两个函数的data数据的工具合约 contract MultiDelegatecallHelper { 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. 部署AbiDecode合约 2. 在encode方法处输入参数,参数1:1,参数2:地址,参数3:[3,4,5] 参数4:结构体类型,也是一种数组 ["mystruct",[7,9]] 3. 执行得到一堆编码 4. 将编码放到decode方法处执行,得到解码结果,就顺利解开这个编码形式了 */ 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)); } }/* gas优化 如何节约gas 执行方法,初始状态gas消耗:50526 优化点1:将输入参数由memory,改为calldata 重新部署,gas消耗:48781 优化点2:if判断里每次都要写入状态变量,浪费gas 重新部署,gas消耗:48570 把total状态变量拿到循环体之外,拷贝到内存中,内次累加的是内存中的变量 在循环结束之后,再一次性的将结果写入到状态变量中,这样就完成了把状态变量拷贝到内存中,这样的方法来节约gas的消耗量 优化点3:把循环体中的i += 1 改成 ++i 优化点4:缓存数组的长度,在循环体重每一次都要读取数组的长度也会浪费gas 重新部署,gas消耗:48535 优化几十个,优化不多 优化点5:将循环体的nums[i]提前读取出来,用的时候不用每次读取了 重新部署,gas消耗:48151 */ contract GasGolf { uint public total; // 参数: [1,2,3,4,5,100] function sumIfEventAndLessThan99(uint[] calldata nums) external { uint _total = total; uint len = nums.length; for (uint i = 0; ibool) public queued; // 定义构造,把合约的部署者传递给管理员 constructor() { owner = msg.sender; } // 自定义报错 error NotOwnerError(); // 定义交易已存在报错 error AlreadyQueuedError(bytes32 txId); // 定义时间校验报错 error TimestampNotInRangeError(uint blockTimestamp, uint timestamp); // 定义当前交易id不在队列中报错错误 error NotQueuedError(bytes32 txId); // 定义为到达指定时间报错 error TimestampNotPassedError(uint blockTimestamp, uint timestamp); // 定义超期报错 error TimestampExpiredError(uint blockTimestamp, uint expiresAt); // 定义执行失败报错 error TxFailedError(); // 定义队列事件 哪个要查询哪个加索引,最多加三个索引 event Queue( bytes32 indexed txId, // 交易id address indexed target, // 目标的合约地址 uint value, // 数值 string func, // 想要执行的方法名称 bytes data, // 操作执行的具体的数据 uint timestamp // 时间戳 ); // 定义执行成功事件 event Execute( bytes32 indexed txId, // 交易id address indexed target, // 目标的合约地址 uint value, // 数值 string func, // 想要执行的方法名称 bytes data, // 操作执行的具体的数据 uint timestamp // 时间戳 ); // 定义取消执行事件 event Cancel(bytes32 indexed txId); // 最小延迟时间 单位秒 uint public constant MIN_DELAY = 10; // 最大延迟时间 uint public constant MAX_DELAY = 1000; // 宽限期 uint public constant GRACE_PERIOD = 1000; // 定义回退函数,用于接收主币 receive() external payable {} // 定义管理员权限验证 modifier onlyOwner() { if (msg.sender != owner) { revert NotOwnerError(); } _; } // 创建交易id 将这些参数统一打包成一个哈希值 只读函数 function getTxId( address _target, // 目标的合约地址 uint _value, // 数值 string calldata _func, // 想要执行的方法名称 bytes calldata _data, // 操作执行的具体的数据 uint _timestamp // 时间戳 ) public pure returns (bytes32 txId) { return keccak256( abi.encode(_target, _value, _func, _data, _timestamp) ); } // 创建队列方法 function createQueue( address _target, // 目标的合约地址 uint _value, // 数值 string calldata _func, // 想要执行的方法名称 bytes calldata _data, // 操作执行的具体的数据 uint _timestamp // 时间戳 ) external onlyOwner { // 获取交易id bytes32 txId = getTxId(_target, _value, _func, _data, _timestamp); // 检查交易id是否存在 if (queued[txId]) { revert AlreadyQueuedError(txId); } // 时间戳检查 if ( _timestamp < block.timestamp + MIN_DELAY || _timestamp > block.timestamp + MAX_DELAY ) { // 报错信息 当前时间戳不在有效范围之内 revert TimestampNotInRangeError(block.timestamp, _timestamp); } // 将交易id记录在队列中 queued[txId] = true; // 记录事件,这个交易被推入到了队列中 emit Queue(txId, _target, _value, _func, _data, _timestamp); } // 执行方法 执行过程中可以能会传送主币,加上payable属性 function execute( address _target, // 目标的合约地址 uint _value, // 数值 string calldata _func, // 想要执行的方法名称 bytes calldata _data, // 操作执行的具体的数据 uint _timestamp // 时间戳 ) external payable onlyOwner returns (bytes memory) { // 获取交易id bytes32 txId = getTxId(_target, _value, _func, _data, _timestamp); // 检查交易id是否存在 if (!queued[txId]) { revert NotQueuedError(txId); } // 时间戳没有到达指定的时间判断 if (block.timestamp < _timestamp) { revert TimestampNotPassedError(block.timestamp, _timestamp); } // 判断时间宽限期,可以超期,不能超过太久 if (block.timestamp > _timestamp + GRACE_PERIOD) { revert TimestampExpiredError(block.timestamp, _timestamp + GRACE_PERIOD); } // 删除交易id,就是改映射 queued[txId] = false; // 对数据进行编码 bytes memory data; // 可能调用的是对方合约的回退函数,所以需要判断方法长度大于0, 将方法编码为4位哈希值 if (bytes(_func).length >0) { data = abi.encodePacked( bytes4(keccak256(bytes(_func))), _data ); } else { data = _data; } // 执行交易,使用低级call 如果调用的是退回函数,只传data就可以。 (bool ok, bytes memory res) = _target.call{value: _value}(data); if (!ok) { revert TxFailedError(); } // 触发执行成功事件 emit Execute(txId, _target, _value, _func, _data, _timestamp); return res; } // 取消方法 如果队列中的交易被发现并不是一个合理的就取消掉 function cancel(bytes32 _txId) external onlyOwner { if (!queued[_txId]) { revert NotQueuedError(_txId); } queued[_txId] = false; // 报出取消交易事件 emit Cancel(_txId); } } contract TestTimeLock { address public timeLock; constructor(address _timeLock) { timeLock = _timeLock; } function test() external view { /* 确认调用者为时间锁合约 之后用户调用时间锁合约把想要进行的操作推入到时间锁合约的队列中, 然后再等待时间锁合约的时间到达,这个时候,调用执行方法,就会按照用户所指定的操作去执行 被执行的合约,因为把权限交给了时间锁合约,所以任何的用户都必须经过一个时间的锁定期 */ require(msg.sender == timeLock); } function getTimestamp() external view returns (uint256 blockTime) { blockTime = block.timestamp + 100; } }