timezone |
---|
Asia/Shanghai |
- 自我介绍 java程序员转web3,啥都想学
- 你认为你会完成本次残酷学习吗? 会
今天复习solidity-101,重温一下solidity基础部分,把solidity-101测试题全部做到100%。记录一下
- 函数重载
Solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,Solidity不允许修饰器(modifier)重载。
在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256:
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
我们调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。
- 库合约
库合约是一种特殊的合约,为了提升Solidity代码的复用性和减少gas而存在。库合约是一系列的函数合集,由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。
他和普通合约主要有以下几点不同:
不能存在状态变量 不能够继承或被继承 不能接收以太币 不可以被销毁 需要注意的是,库合约重的函数可见性如果被设置为public或者external,则在调用函数时会触发一次delegatecall。而如果被设置为internal,则不会引起。对于设置为private可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。
如何使用库合约 我们用Strings库合约的toHexString()来演示两种使用库合约中函数的办法。
利用using for指令
指令using A for B;可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数:
// 利用using for指令
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory){
// 库合约中的函数会自动添加为uint256型变量的成员
return _number.toHexString();
}
通过库合约名称调用函数
// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
return Strings.toHexString(_number);
}
- Import
在Solidity中,import语句可以帮助我们在一个文件中引用另一个文件的内容,提高代码的可重用性和组织性。本教程将向你介绍如何在Solidity中使用import语句。
import用法 1.通过源文件相对位置导入,例子:
// 通过文件相对位置import
import './Yeye.sol';
2.通过源文件网址导入网上的合约的全局符号,例子:
// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
3.通过npm的目录导入,例子:
import '@openzeppelin/contracts/access/Ownable.sol';
4.通过指定全局符号导入合约特定的全局符号,例子:
import {Yeye} from './Yeye.sol';
5.引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。
- 接收ETH receive和fallback
Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用:
接收ETH 处理合约中不存在的函数调用(代理合约proxy contract)
1.接收ETH函数 receive
receive()函数是在合约收到ETH转账时被调用的函数。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。
2.回退函数 fallback
fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }。
3.receive和fallback的区别 receive和fallback都能够用于接收ETH,他们触发的规则如下:
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/ \
是 否
/ \
receive()存在? fallback()
/ \
是 否
/ \
receive() fallback()
- 发送ETH
Solidity有三种方法向其他合约发送ETH,他们是:transfer(),send()和call(),其中call()是被鼓励的用法。
1.transfer 用法是接收方地址.transfer(发送ETH数额)。 transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。 transfer()如果转账失败,会自动revert(回滚交易)。
// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external payable{
_to.transfer(amount);
}
2.send 用法是接收方地址.send(发送ETH数额)。 send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。 send()如果转账失败,不会revert。 send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。
error SendFailed(); // 用send发送ETH失败error
// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
// 处理下send的返回值,如果失败,revert交易并发送error
bool success = _to.send(amount);
if(!success){
revert SendFailed();
}
}
3.call 用法是接收方地址.call{value: 发送ETH数额}("")。 call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。 call()如果转账失败,不会revert。 call()的返回值是(bool, bytes),其中bool代表着转账成功或失败,需要额外代码处理一下。
error CallFailed(); // 用call发送ETH失败error
// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
// 处理下call的返回值,如果失败,revert交易并发送error
(bool success,) = _to.call{value: amount}("");
if(!success){
revert CallFailed();
}
}
-
调用其他合约
-
传入合约地址
我们可以在函数里传入目标合约地址,生成目标合约的引用,然后调用目标函数。以调用OtherContract合约的setX函数为例,我们在新合约中写一个callSetX函数,传入已部署好的OtherContract合约地址_Address和setX的参数x:
function callSetX(address _Address, uint256 x) external{
OtherContract(_Address).setX(x);
}
复制OtherContract合约的地址,填入callSetX函数的参数中,成功调用后,调用OtherContract合约中的getX验证x变为123
- 传入合约变量
我们可以直接在函数里传入合约的引用,只需要把上面参数的address类型改为目标合约名,比如OtherContract。下面例子实现了调用目标合约的getX()函数。
注意:该函数参数OtherContract _Address底层类型仍然是address,生成的ABI中、调用callGetX时传入的参数都是address类型
function callGetX(OtherContract _Address) external view returns(uint x){
x = _Address.getX();
}
复制OtherContract合约的地址,填入callGetX函数的参数中,调用后成功获取x的值
- 创建合约变量
我们可以创建合约变量,然后通过它来调用目标函数。下面例子,我们给变量oc存储了OtherContract合约的引用:
function callGetX2(address _Address) external view returns(uint x){
OtherContract oc = OtherContract(_Address);
x = oc.getX();
}
复制OtherContract合约的地址,填入callGetX2函数的参数中,调用后成功获取x的值
- 调用合约并发送ETH 如果目标合约的函数是payable的,那么我们可以通过调用它来给合约转账:_Name(_Address).f{value: _Value}(),其中_Name是合约名,_Address是合约地址,f是目标函数名,_Value是要转的ETH数额(以wei为单位)。
OtherContract合约的setX函数是payable的,在下面这个例子中我们通过调用setX来往目标合约转账。
function setXTransferETH(address otherContract, uint256 x) payable external{
OtherContract(otherContract).setX{value: msg.value}(x);
}
复制OtherContract合约的地址,填入setXTransferETH函数的参数中,并转入10ETH
- Call
call 是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, bytes memory),分别对应call是否成功以及目标函数的返回值。
call是Solidity官方推荐的通过触发fallback或receive函数发送ETH的方法。 不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数,见第21讲:调用其他合约 当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数。 call的使用规则 call的使用规则如下:
目标合约地址.call(字节码);
其中字节码利用结构化编码函数abi.encodeWithSignature获得:
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
函数签名为"函数名(逗号分隔的参数类型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)。
另外call在调用合约时可以指定交易发送的ETH数额和gas数额:
目标合约地址.call{value:发送数额, gas:gas数额}(字节码);
- Delegatecall
- 在合约中创建新合约 在以太坊链上,用户(外部账户,EOA)可以创建智能合约,智能合约同样也可以创建新的智能合约。去中心化交易所uniswap就是利用工厂合约(PairFactory)创建了无数个币对合约(Pair)。 Uniswap V2核心合约中包含两个合约:
UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。
UniswapV2Factory: 工厂合约,用于创建新的币对,并管理币对地址。
下面我们用create方法实现一个极简版的Uniswap:Pair币对合约负责管理币对地址,PairFactory工厂合约用于创建新的币对,并管理币对地址。 Pair合约
contract Pair{
address public factory; // 工厂合约地址
address public token0; // 代币1
address public token1; // 代币2
constructor() payable {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
}
PairFactory
contract PairFactory{
mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
address[] public allPairs; // 保存所有Pair地址
function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
// 创建新合约
Pair pair = new Pair();
// 调用新合约的initialize方法
pair.initialize(tokenA, tokenB);
// 更新地址map
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
- CREATE2 CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。Uniswap创建Pair合约用的就是CREATE2而不是CREATE。这一讲,我将介绍CREATE2的用法
CREATE2如何计算地址 CREATE2的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。用CREATE2创建的合约地址由4个部分决定:
0xFF:一个常数,避免和CREATE冲突 CreatorAddress: 调用 CREATE2 的当前合约(创建合约)地址。 salt(盐):一个创建者指定的bytes32类型的值,它的主要目的是用来影响新创建的合约的地址。 initcode: 新合约的初始字节码(合约的Creation Code和构造函数的参数)。 新地址 = hash("0xFF",创建者地址, salt, initcode) Copy CREATE2 确保,如果创建者使用 CREATE2 和提供的 salt 部署给定的合约initcode,它将存储在 新地址 中。
如何使用CREATE2
-
删除合约
selfdestruct命令可以用来删除智能合约,并将该合约剩余ETH转到指定地址。selfdestruct是为了应对合约出错的极端情况而设计的。它最早被命名为suicide(自杀),但是这个词太敏感。为了保护抑郁的程序员,改名为selfdestruct;在 v0.8.18 版本中,selfdestruct 关键字被标记为「不再建议使用」,在一些情况下它会导致预期之外的合约语义,但由于目前还没有代替方案,目前只是对开发者做了编译阶段的警告,相关内容可以查看 EIP-6049。
然而,在以太坊坎昆(Cancun)升级中,EIP-6780被纳入升级以实现对Verkle Tree更好的支持。EIP-6780减少了SELFDESTRUCT操作码的功能。根据提案描述,当前SELFDESTRUCT仅会被用来将合约中的ETH转移到指定地址,而原先的删除功能只有在合约创建-自毁这两个操作处在同一笔交易时才能生效。所以目前来说:
已经部署的合约无法被SELFDESTRUCT了。 如果要使用原先的SELFDESTRUCT功能,必须在同一笔交易中创建并SELFDESTRUCT。
-
ABI编码解码
ABI (Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。 ABI编码有4个函数:abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelector。而ABI解码有1个函数:abi.decode
未更新
-
Hash
Hash的性质 一个好的哈希函数应该具有以下几个特性:
单向性:从输入的消息到它的哈希的正向运算简单且唯一确定,而反过来非常难,只能靠暴力枚举。 灵敏性:输入的消息改变一点对它的哈希改变很大。 高效性:从输入的消息到哈希的运算高效。 均一性:每个哈希值被取到的概率应该基本相等。 抗碰撞性: 弱抗碰撞性:给定一个消息x,找到另一个消息x',使得hash(x) = hash(x')是困难的。 强抗碰撞性:找到任意x和x',使得hash(x) = hash(x')是困难的。 Hash的应用 生成数据唯一标识 加密签名 安全加密 Keccak256 Keccak256函数是Solidity中最常用的哈希函数,用法非常简单:
哈希 = keccak256(数据);
- 函数选择器Selector
- Try Catch
在Solidity中,try-catch只能被用于external函数或创建合约时constructor(被视为external函数)的调用。基本语法如下:
try externalContract.f() {
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}
其中externalContract.f()是某个外部合约的函数调用,try模块在调用成功的情况下运行,而catch模块则在调用失败时运行。
同样可以使用this.f()来替代externalContract.f(),this.f()也被视作为外部调用,但不可在构造函数中使用,因为此时合约还未创建。
如果调用的函数有返回值,那么必须在try之后声明returns(returnType val),并且在try模块中可以使用返回的变量;如果是创建合约,那么返回值是新创建的合约变量。
try externalContract.f() returns(returnType val){
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}
另外,catch模块支持捕获特殊的异常原因:
try externalContract.f() returns(returnType){
// call成功的情况下 运行一些代码
} catch Error(string memory /*reason*/) {
// 捕获revert("reasonString") 和 require(false, "reasonString")
} catch Panic(uint /*errorCode*/) {
// 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界
} catch (bytes memory /*lowLevelData*/) {
// 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支
// 例如revert() require(false) revert自定义类型的error
}
ERC20 ERC20是以太坊上的代币标准,来自2015年11月V神参与的EIP20。它实现了代币转账的基本逻辑:
账户余额(balanceOf()) 转账(transfer()) 授权转账(transferFrom()) 授权(approve()) 代币总供给(totalSupply()) 授权转账额度(allowance()) 代币信息(可选):名称(name()),代号(symbol()),小数位数(decimals()) 事件 IERC20定义了2个事件:Transfer事件和Approval事件,分别在转账和授权时被释放
/**
* @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
函数 IERC20定义了6个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。
totalSupply()返回代币总供给
/**
* @dev 返回代币总供给.
*/
function totalSupply() external view returns (uint256);
balanceOf()返回账户余额
/**
* @dev 返回账户`account`所持有的代币数.
*/
function balanceOf(address account) external view returns (uint256);
transfer()转账
/**
* @dev 转账 `amount` 单位代币,从调用者账户到另一账户 `to`.
*
* 如果成功,返回 `true`.
*
* 释放 {Transfer} 事件.
*/
function transfer(address to, uint256 amount) external returns (bool);
allowance()返回授权额度
/**
* @dev 返回`owner`账户授权给`spender`账户的额度,默认为0。
*
* 当{approve} 或 {transferFrom} 被调用时,`allowance`会改变.
*/
function allowance(address owner, address spender) external view returns (uint256);
approve()授权
/**
* @dev 调用者账户给`spender`账户授权 `amount`数量代币。
*
* 如果成功,返回 `true`.
*
* 释放 {Approval} 事件.
*/
function approve(address spender, uint256 amount) external returns (bool);
transferFrom()授权转账
/**
* @dev 通过授权机制,从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。
*
* 如果成功,返回 `true`.
*
* 释放 {Transfer} 事件.
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
实现ERC20
mapping(address => uint256) public override balanceOf;
mapping(address => mapping(address => uint256)) public override allowance;
uint256 public override totalSupply; // 代币总供给
string public name; // 名称
string public symbol; // 代号
uint8 public decimals = 18; // 小数位数
函数 构造函数:初始化代币名称、代号。
constructor(string memory name_, string memory symbol_){
name = name_;
symbol = symbol_;
}
transfer()函数:实现IERC20中的transfer函数,代币转账逻辑。调用方扣除amount数量代币,接收方增加相应代币。土狗币会魔改这个函数,加入税收、分红、抽奖等逻辑。
function transfer(address recipient, uint amount) public override returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
approve()函数:实现IERC20中的approve函数,代币授权逻辑。被授权方spender可以支配授权方的amount数量的代币。spender可以是EOA账户,也可以是合约账户:当你用uniswap交易代币时,你需要将代币授权给uniswap合约。
function approve(address spender, uint amount) public override returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
transferFrom()函数:实现IERC20中的transferFrom函数,授权转账逻辑。被授权方将授权方sender的amount数量的代币转账给接收方recipient。
function transferFrom(
address sender,
address recipient,
uint amount
) public override returns (bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
mint()函数:铸造代币函数,不在IERC20标准中。这里为了教程方便,任何人可以铸造任意数量的代币,实际应用中会加权限管理,只有owner可以铸造代币:
function mint(uint amount) external {
balanceOf[msg.sender] += amount;
totalSupply += amount;
emit Transfer(address(0), msg.sender, amount);
}
burn()函数:销毁代币函数,不在IERC20标准中。
function burn(uint amount) external {
balanceOf[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
- 代币水龙头 我们在水龙头合约中定义3个状态变量
amountAllowed设定每次能领取代币数量(默认为100,不是一百枚,因为代币有小数位数)。
tokenContract记录发放的ERC20代币合约地址。
requestedAddress记录领取过代币的地址。
uint256 public amountAllowed = 100; // 每次领 100 单位代币
address public tokenContract; // token合约地址
mapping(address => bool) public requestedAddress;
事件 水龙头合约中定义了1个SendToken事件,记录了每次领取代币的地址和数量,在requestTokens()函数被调用时释放。
// SendToken事件
event SendToken(address indexed Receiver, uint256 indexed Amount);
函数 合约中只有两个函数:
构造函数:初始化tokenContract状态变量,确定发放的ERC20代币地址。
// 部署时设定ERC20代币合约
constructor(address _tokenContract) {
tokenContract = _tokenContract; // set token contract
}
requestTokens()函数,用户调用它可以领取ERC20代币。
// 用户领取代币函数
function requestTokens() external {
require(!requestedAddress[msg.sender], "Can't Request Multiple Times!"); // 每个地址只能领一次
IERC20 token = IERC20(tokenContract); // 创建IERC20合约对象
require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // 水龙头空了
token.transfer(msg.sender, amountAllowed); // 发送token
requestedAddress[msg.sender] = true; // 记录领取地址
emit SendToken(msg.sender, amountAllowed); // 释放SendToken事件
}
- 空投合约
投代币合约 Airdrop空投合约逻辑非常简单:利用循环,一笔交易将ERC20代币发送给多个地址。合约中包含两个函数
getSum()函数:返回uint数组的和。
// 数组求和函数
function getSum(uint256[] calldata _arr) public pure returns(uint sum){
for(uint i = 0; i < _arr.length; i++)
sum = sum + _arr[i];
}
multiTransferToken()函数:发送ERC20代币空投,包含3个参数:
_token:代币合约地址(address类型) _addresses:接收空投的用户地址数组(address[]类型) _amounts:空投数量数组,对应_addresses里每个地址的数量(uint[]类型) 该函数有两个检查:第一个require检查了_addresses和_amounts两个数组长度是否相等;第二个require检查了空投合约的授权额度大于要空投的代币数量总和。
/// @notice 向多个地址转账ERC20代币,使用前需要先授权
///
/// @param _token 转账的ERC20代币地址
/// @param _addresses 空投地址数组
/// @param _amounts 代币数量数组(每个地址的空投数量)
function multiTransferToken(
address _token,
address[] calldata _addresses,
uint256[] calldata _amounts
) external {
// 检查:_addresses和_amounts数组的长度相等
require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
IERC20 token = IERC20(_token); // 声明IERC合约变量
uint _amountSum = getSum(_amounts); // 计算空投代币总量
// 检查:授权代币数量 >= 空投代币总量
require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token");
// for循环,利用transferFrom函数发送空投
for (uint8 i; i < _addresses.length; i++) {
token.transferFrom(msg.sender, _addresses[i], _amounts[i]);
}
}
multiTransferETH()函数:发送ETH空投,包含2个参数:
_addresses:接收空投的用户地址数组(address[]类型) _amounts:空投数量数组,对应_addresses里每个地址的数量(uint[]类型)
/// 向多个地址转账ETH
function multiTransferETH(
address payable[] calldata _addresses,
uint256[] calldata _amounts
) public payable {
// 检查:_addresses和_amounts数组的长度相等
require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
uint _amountSum = getSum(_amounts); // 计算空投ETH总量
// 检查转入ETH等于空投总量
require(msg.value == _amountSum, "Transfer amount error");
// for循环,利用transfer函数发送ETH
for (uint256 i = 0; i < _addresses.length; i++) {
// 注释代码有Dos攻击风险, 并且transfer 也是不推荐写法
// Dos攻击 具体参考 https://github.com/AmazingAng/WTF-Solidity/blob/main/S09_DoS/readme.md
// _addresses[i].transfer(_amounts[i]);
(bool success, ) = _addresses[i].call{value: _amounts[i]}("");
if (!success) {
failTransferList[_addresses[i]] = _amounts[i];
}
}
}
- ERC721(目前卡在ERC721编写)
/**
* @dev ERC165标准接口, 详见
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* 合约可以声明支持的接口,供其他合约检查
*
*/
interface IERC165 {
/**
* @dev 如果合约实现了查询的`interfaceId`,则返回true
* 规则详见:https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
*
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
interface IERC721Metadata {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 tokenId) external view returns (string memory);
}
// ERC721接收者接口:合约必须实现这个接口来通过安全转账接收ERC721
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external returns (bytes4);
}
interface IERC721 is IERC165{
event Transfer(address indexed from,address indexed to,uint256 indexed tokenId);
event Approval(address indexed owner,address indexed approved,uint256 indexed tokenId);
event ApprovalForAll(address indexed owner,address indexed operator,bool approved);
function ownerOf(uint256 tokenId) external view returns (address owner);
function balanceOf(address owner) external view returns (uint256 tokenId);
function transferFrom(address from,address to,uint256 tokenId) external ;
function safeTransferFrom(address from,address to,uint256 tokenId)external ;
function safeTransferFrom(address from,address to,uint256 tokenId,bytes calldata data) external ;
function approve(address operator,uint256 tokenId) external ;
function setApprovalForAll(address operator,bool approved) external ;
function getApproved(uint256 tokenId) external view returns (address owner);
function isApprovedForAll(address owner,address operator) external view returns (bool);
}
ERC721主合约
// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.21;
import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./String.sol";
contract ERC721 is IERC721, IERC721Metadata{
using Strings for uint256; // 使用String库,
// Token名称
string public override name;
// Token代号
string public override symbol;
// tokenId 到 owner address 的持有人映射
mapping(uint => address) private _owners;
// address 到 持仓数量 的持仓量映射
mapping(address => uint) private _balances;
// tokenID 到 授权地址 的授权映射
mapping(uint => address) private _tokenApprovals;
// owner地址。到operator地址 的批量授权映射
mapping(address => mapping(address => bool)) private _operatorApprovals;
// 错误 无效的接收者
error ERC721InvalidReceiver(address receiver);
/**
* 构造函数,初始化`name` 和`symbol` .
*/
constructor(string memory name_, string memory symbol_) {
name = name_;
symbol = symbol_;
}
// 实现IERC165接口supportsInterface
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId;
}
// 实现IERC721的balanceOf,利用_balances变量查询owner地址的balance。
function balanceOf(address owner) external view override returns (uint) {
require(owner != address(0), "owner = zero address");
return _balances[owner];
}
// 实现IERC721的ownerOf,利用_owners变量查询tokenId的owner。
function ownerOf(uint tokenId) public view override returns (address owner) {
owner = _owners[tokenId];
require(owner != address(0), "token doesn't exist");
}
// 实现IERC721的isApprovedForAll,利用_operatorApprovals变量查询owner地址是否将所持NFT批量授权给了operator地址。
function isApprovedForAll(address owner, address operator)
external
view
override
returns (bool)
{
return _operatorApprovals[owner][operator];
}
// 实现IERC721的setApprovalForAll,将持有代币全部授权给operator地址。调用_setApprovalForAll函数。
function setApprovalForAll(address operator, bool approved) external override {
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
// 实现IERC721的getApproved,利用_tokenApprovals变量查询tokenId的授权地址。
function getApproved(uint tokenId) external view override returns (address) {
require(_owners[tokenId] != address(0), "token doesn't exist");
return _tokenApprovals[tokenId];
}
// 授权函数。通过调整_tokenApprovals来,授权 to 地址操作 tokenId,同时释放Approval事件。
function _approve(
address owner,
address to,
uint tokenId
) private {
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
// 实现IERC721的approve,将tokenId授权给 to 地址。条件:to不是owner,且msg.sender是owner或授权地址。调用_approve函数。
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);
}
// 查询 spender地址是否可以使用tokenId(需要是owner或被授权地址)
function _isApprovedOrOwner(
address owner,
address spender,
uint tokenId
) private view returns (bool) {
return (spender == owner ||
_tokenApprovals[tokenId] == spender ||
_operatorApprovals[owner][spender]);
}
/*
* 转账函数。通过调整_balances和_owner变量将 tokenId 从 from 转账给 to,同时释放Transfer事件。
* 条件:
* 1. tokenId 被 from 拥有
* 2. to 不是0地址
*/
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);
}
// 实现IERC721的transferFrom,非安全转账,不建议使用。调用_transfer函数
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);
}
/**
* 安全转账,安全地将 tokenId 代币从 from 转移到 to,会检查合约接收者是否了解 ERC721 协议,以防止代币被永久锁定。调用了_transfer函数和_checkOnERC721Received函数。条件:
* from 不能是0地址.
* to 不能是0地址.
* tokenId 代币必须存在,并且被 from拥有.
* 如果 to 是智能合约, 他必须支持 IERC721Receiver-onERC721Received.
*/
function _safeTransfer(
address owner,
address from,
address to,
uint tokenId,
bytes memory _data
) private {
_transfer(owner, from, to, tokenId);
_checkOnERC721Received(from, to, tokenId, _data);
}
/**
* 实现IERC721的safeTransferFrom,安全转账,调用了_safeTransfer函数。
*/
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);
}
// safeTransferFrom重载函数
function safeTransferFrom(
address from,
address to,
uint tokenId
) external override {
safeTransferFrom(from, to, tokenId, "");
}
/**
* 铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to,同时释放Transfer事件。铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to,同时释放Transfer事件。
* 这个mint函数所有人都能调用,实际使用需要开发人员重写,加上一些条件。
* 条件:
* 1. tokenId尚不存在。
* 2. to不是0地址.
*/
function _mint(address to, uint tokenId) internal virtual {
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);
}
// 销毁函数,通过调整_balances和_owners变量来销毁tokenId,同时释放Transfer事件。条件:tokenId存在。
function _burn(uint tokenId) internal virtual {
address owner = ownerOf(tokenId);
require(msg.sender == owner, "not owner of token");
_approve(owner, address(0), tokenId);
_balances[owner] -= 1;
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
}
// _checkOnERC721Received:函数,用于在 to 为合约的时候调用IERC721Receiver-onERC721Received, 以防 tokenId 被不小心转入黑洞。
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
revert ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
revert ERC721InvalidReceiver(to);
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}
/**
* 实现IERC721Metadata的tokenURI函数,查询metadata。
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_owners[tokenId] != address(0), "Token Not Exist");
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
/**
* 计算{tokenURI}的BaseURI,tokenURI就是把baseURI和tokenId拼接在一起,需要开发重写。
* BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
}
DutchAuction荷兰拍卖,递减拍卖,它是指拍卖标的的竞价由高到低依次递减直到第一个竞买人应价(达到或超过底价)时击槌成交的一种拍卖。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/access/Ownable.sol";
import "contracts/ERC721.sol";
contract DutchAuction is Ownable ,ERC721{
uint256 public constant COLLECTION_SIZE =10000;
uint256 public constant AUCTION_START_PRICE = 1 ether;
uint256 public constant AUCTION_END_PRICE = 0.1 ether;
uint256 public constant AUCTION_TIME = 10 minutes;
uint256 public constant AUCTION_DROP_INTERVAL = 1 minutes;
uint256 public constant AUCTION_DROP_PRE_STEP = (AUCTION_START_PRICE-AUCTION_END_PRICE)/(AUCTION_TIME/AUCTION_DROP_INTERVAL);
uint256 public auctionStartTime;
string private _baseTokenURI;
uint256[] private _allTokens;
constructor() Ownable(msg.sender) ERC721("kurt","kurt"){
auctionStartTime = block.timestamp;
}
function totalSupply() public view virtual returns (uint256){
return _allTokens.length;
}
function _addTokenToAllTokenEnumeration(uint256 tokenId) private {
_allTokens.push(tokenId);
}
function auctiomMint(uint256 quantity) external payable {
uint256 _saleStartTime = uint256(auctionStartTime);
require(_saleStartTime != 0 && block.timestamp>= _saleStartTime,"sale has not started yet");
require(totalSupply()+quantity <= COLLECTION_SIZE,"not enough remaining reserved for auction to support desired mint amount");
uint256 totalCost = getAuctionPrice() * quantity;
require(msg.value>totalCost,"need more token");
for (uint8 i =1 ; i<quantity;i++){
uint256 mintIndex = totalSupply();
_mint(msg.sender, mintIndex);
_addTokenToAllTokenEnumeration(mintIndex);
}
if(msg.value>totalCost){
payable (msg.sender).transfer(msg.value-totalCost);
}
}
function getAuctionPrice() public view returns (uint256){
if(block.timestamp<auctionStartTime){
return AUCTION_START_PRICE;
}else if(block.timestamp -auctionStartTime >= AUCTION_TIME){
return AUCTION_END_PRICE;
}else {
uint256 steps = (block.timestamp - auctionStartTime)/AUCTION_DROP_INTERVAL;
return AUCTION_START_PRICE-(steps*AUCTION_DROP_PRE_STEP);
}
}
function setAuctionStartTime(uint32 timestamp)external onlyOwner{
auctionStartTime = timestamp;
}
function _baseURI() internal view virtual override returns (string memory){
return _baseTokenURI;
}
function setBaseURI(string calldata baseURI) external onlyOwner{
_baseTokenURI = baseURI;
}
function withdrawMoney() external onlyOwner{
(bool success,)=msg.sender.call{value:address(this).balance}("");
require(success,"transfer fail");
}
}
MerkleProof
pragma solidity ^0.8.26;
import "contracts/ERC721.sol";
library MerkleProof {
function verify(bytes32 leaf,bytes32 root,bytes32[] memory proof) internal pure returns (bool){
return processProof(leaf,proof) == root;
}
function processProof(bytes32 leaf,bytes32[] memory proof) internal pure returns (bytes32){
bytes32 computedHash = leaf;
for(uint256 i = 0;i<proof.length;i++){
computedHash = _hashPair(computedHash,proof[i]);
}
return computedHash;
}
function _hashPair(bytes32 a,bytes32 b) private pure returns (bytes32){
return a < b ? keccak256(abi.encodePacked(a,b)) :keccak256(abi.encodePacked(b,a));
}
}
contract merkleTree is ERC721 {
bytes32 public immutable root;
mapping (address=>bool) public minedAddress;
constructor(string memory name,string memory symbol,bytes32 merkleroot) ERC721(name,symbol){
root = merkleroot;
}
function mint(address account,uint256 tokenId,bytes32[] calldata proof) external {
require(_verify(_leaf(account),proof),"Invalid merkle proo");
require(!minedAddress[account],"already mined");
minedAddress[account] = true;
_mint(account, tokenId);
}
function _leaf(address account) internal pure returns (bytes32){
return keccak256(abi.encodePacked(account));
}
function _verify(bytes32 leaf,bytes32[] memory proof) internal view returns (bool){
return MerkleProof.verify(leaf, root, proof);
}
}
signature
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "contracts/ERC721.sol";
library ECDSA{
function verify(bytes32 _msgHash, bytes memory _signature,address signer) internal pure returns (bool){
return recoverSigner(_msgHash,_signature) == signer;
}
function recoverSigner(bytes32 _msgHash,bytes memory _signature) internal pure returns (address){
require(_signature.length == 65,"invalid signature length");
bytes32 r;
bytes32 s;
uint8 v;
assembly{
r:= mload(add(_signature,0x20))
s:= mload(add(_signature,0x40))
v:= byte(0,mload(add(_signature,0x60)))
}
ecrecover(_msgHash, v, r, s);//唉亏卡微
}
function toEthSignedMessage(bytes32 hash) public pure returns (bytes32){
return keccak256(abi.encodePacked("\x19Ethereum signed Message:\n32",hash));
}
}
contract SignatureNFT is ERC721 {
address immutable public signer;
mapping(address => bool) public mintedAddress;
constructor(string memory _name, string memory _symbol, address _signer)
ERC721(_name, _symbol)
{
signer = _signer;
}
function mint(address _account, uint256 _tokenId, bytes memory _signature)
external
{
bytes32 _msgHash = getMessageHash(_account, _tokenId); // 将_account和_tokenId打包消息
bytes32 _ethSignedMessageHash = ECDSA.toEthSignedMessageHash(_msgHash); // 计算以太坊签名消息
require(verify(_ethSignedMessageHash, _signature), "Invalid signature"); // ECDSA检验通过
require(!mintedAddress[_account], "Already minted!"); // 地址没有mint过
mintedAddress[_account] = true; // 记录mint过的地址
_mint(_account, _tokenId); // mint
}
/*
* 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash
* _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
* _tokenId: 0
* 对应的消息msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
*/
function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
return keccak256(abi.encodePacked(_account, _tokenId));
}
// ECDSA验证,调用ECDSA库的verify()函数
function verify(bytes32 _msgHash, bytes memory _signature)
public view returns (bool)
{
return ECDSA.verify(_msgHash, _signature, signer);
}
}
contract VerifySignature {
function getMessageHash(
address _addr,
uint256 _tokenId
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_addr, _tokenId));
}
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
return
keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
function verify(
address _signer,
address _addr,
uint _tokenId,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_addr, _tokenId);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recoverSigner(ethSignedMessageHash, signature) == _signer;
}
function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature)
public
pure
returns (address)
{
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
function splitSignature(bytes memory sig)
public
pure
returns (
bytes32 r,
bytes32 s,
uint8 v
)
{
// 检查签名长度,65是标准r,s,v签名的长度
require(sig.length == 65, "invalid signature length");
assembly {
// first 32 bytes, after the length prefix
r := mload(add(sig, 0x20))
// second 32 bytes
s := mload(add(sig, 0x40))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 0x60)))
}
// implicitly return (r, s, v)
}
}
NFTSwap
pragma solidity ^0.8.21;
import "../34_ERC721/IERC721.sol";
import "../34_ERC721/IERC721Receiver.sol";
import "../34_ERC721/WTFApe.sol";
contract NFTSwap is IERC721Receiver {
event List(
address indexed seller,
address indexed nftAddr,
uint256 indexed tokenId,
uint256 price
);
event Purchase(
address indexed buyer,
address indexed nftAddr,
uint256 indexed tokenId,
uint256 price
);
event Revoke(
address indexed seller,
address indexed nftAddr,
uint256 indexed tokenId
);
event Update(
address indexed seller,
address indexed nftAddr,
uint256 indexed tokenId,
uint256 newPrice
);
// 定义order结构体
struct Order {
address owner;
uint256 price;
}
// NFT Order映射
mapping(address => mapping(uint256 => Order)) public nftList;
fallback() external payable {}
// 挂单: 卖家上架NFT,合约地址为_nftAddr,tokenId为_tokenId,价格_price为以太坊(单位是wei)
function list(address _nftAddr, uint256 _tokenId, uint256 _price) public {
IERC721 _nft = IERC721(_nftAddr); // 声明IERC721接口合约变量
require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // 合约得到授权
require(_price > 0); // 价格大于0
Order storage _order = nftList[_nftAddr][_tokenId]; //设置NF持有人和价格
_order.owner = msg.sender;
_order.price = _price;
// 将NFT转账到合约
_nft.safeTransferFrom(msg.sender, address(this), _tokenId);
// 释放List事件
emit List(msg.sender, _nftAddr, _tokenId, _price);
}
// 购买: 买家购买NFT,合约为_nftAddr,tokenId为_tokenId,调用函数时要附带ETH
function purchase(address _nftAddr, uint256 _tokenId) public payable {
Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order
require(_order.price > 0, "Invalid Price"); // NFT价格大于0
require(msg.value >= _order.price, "Increase price"); // 购买价格大于标价
// 声明IERC721接口合约变量
IERC721 _nft = IERC721(_nftAddr);
require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中
// 将NFT转给买家
_nft.safeTransferFrom(address(this), msg.sender, _tokenId);
// 将ETH转给卖家,多余ETH给买家退款
payable(_order.owner).transfer(_order.price);
payable(msg.sender).transfer(msg.value - _order.price);
delete nftList[_nftAddr][_tokenId]; // 删除order
// 释放Purchase事件
emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price);
}
// 撤单: 卖家取消挂单
function revoke(address _nftAddr, uint256 _tokenId) public {
Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order
require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起
// 声明IERC721接口合约变量
IERC721 _nft = IERC721(_nftAddr);
require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中
// 将NFT转给卖家
_nft.safeTransferFrom(address(this), msg.sender, _tokenId);
delete nftList[_nftAddr][_tokenId]; // 删除order
// 释放Revoke事件
emit Revoke(msg.sender, _nftAddr, _tokenId);
}
// 调整价格: 卖家调整挂单价格
function update(
address _nftAddr,
uint256 _tokenId,
uint256 _newPrice
) public {
require(_newPrice > 0, "Invalid Price"); // NFT价格大于0
Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order
require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起
// 声明IERC721接口合约变量
IERC721 _nft = IERC721(_nftAddr);
require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中
// 调整NFT价格
_order.price = _newPrice;
// 释放Update事件
emit Update(msg.sender, _nftAddr, _tokenId, _newPrice);
}
// 实现{IERC721Receiver}的onERC721Received,能够接收ERC721代币
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}