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));
}
}