01.ERC20
目标:发行自己的测试代币;
ERC20是什么?
ERC20
是以太坊上的代币标准,实现了代币转账的基本逻辑:
- 账户余额(
balanceOf()
) - 转账(
transfer()
) - 授权转账(
transferFrom()
) - 授权(
approve()
) - 代币总供给(
totalSupply()
) - 授权转账额度(
allowance()
) - 代币信息(可选)
- 名称(
name()
) - 代号(
symbol()
) - 小数位数(
decimals()
) - ...
- 名称(
IERC20是什么?
IERC20
是ERC20
代币标准的接口合约,规定了ERC20
代币需要实现的函数和事件。
之所以需要定义接口,是因为有了规范之后,
ERC20
代币都存在通用的函数名称、输入参数、输出参数。
事件
IERC20
定义了2个事件:
/**
* @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个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。
/**
* @dev 返回代币总供给.
*/
function totalSupply() external view returns (uint256);
/**
* @dev 返回账户`account`所持有的代币数.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev 转账 `amount` 单位代币,从调用者账户到另一账户 `to`.
*
* 如果成功,返回 `true`.
*
* 释放 {Transfer} 事件.
*/
function transfer(address to, uint256 amount) external returns (bool);
/**
* @dev 返回`owner`账户授权给`spender`账户的额度,默认为0。
*
* 当{approve} 或 {transferFrom} 被调用时,`allowance`会改变.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev 调用者账户给`spender`账户授权 `amount`数量代币。
*
* 如果成功,返回 `true`.
*
* 释放 {Approval} 事件.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev 通过授权机制,从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。
*
* 如果成功,返回 `true`.
*
* 释放 {Transfer} 事件.
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
实现ERC20
首先把接口合约IERC20.sol
的内容拷贝一下放在当前目录下:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
代币合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./IERC20.sol";
contract ERC20Demo is IERC20{
// 状态变量,记录账户余额
mapping (address => uint256) public override balanceOf;
// 状态变量,记录授权额度
mapping (address => mapping (address => uint256)) public override allowance;
// 状态变量,记录代币总供给
uint256 public override totalSupply;
// 上面这三个状态变量声明为public类型时,会自动生成一个同名的getter函数,以此来实现IERC20接口中的三个同名函数
// 代币名字
string public name;
// 代币符号
string public symbol;
// 小数位数
uint8 public decomals = 18;
// 构造函数,为代币初始化名称、符号
constructor(string memory _name, string memory _symbol){
name = _name;
symbol = _symbol;
}
// 实现接口函数
// 代币转账逻辑
// 调用方扣除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;
}
// 实现接口函数
// 代币授权逻辑
// 被授权方spender可以支配授权方amount数量多代币
// spender可以是EOA账户,也可以是合约账户
// 当用uniswap交易代币时,需要将代币授权给uniswap合约
function approve(address spender, uint amount) public override returns (bool){
// allowance映射:[授权方][被授权方][代币额度]
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// 实现接口函数
// 授权转账逻辑
// 被授权方将授权方sender的amount数量代币转账给recipient
function transferFrom(address sender, address recipient, uint amount) public override returns (bool){
// msg.sender --> 被授权方
// sender --> 授权方
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);
}
}
发行代币
初始化代币名称和符号:
铸造代币:
查看余额:
可以利用多个钱包来实施转账、授权等操作;
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
授权给0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
300代币;
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
再转给0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db
100代币;
期待的结果:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4:700
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2:200
0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db:100
给0xAb...
授权300代币:
授权转账:
查看0x4B...
的代币余额:
查看0x5B...
的代币余额:
02.代币水龙头
目标:给用户免费发放ERC20
代币;
什么是代币水龙头?
口渴时,需要去水龙头接水喝;
用户想要免费代币时,就需要去代币水龙头领;
实现ERC20水龙头
下面实现一个简易的ERC20
水龙头,逻辑很简单:将一些ERC20
代币转到水龙头合约中,用户可以通过合约的requestToken()
函数来领取100
单位的代币,且每个地址只能领一次。
需要准备的合约
IERC20.sol
:ERC20代币标准的合约接口;
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
ERC20Demo.sol
:自己实现的代币合约;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./IERC20.sol";
contract ERC20Demo is IERC20{
// 状态变量,记录账户余额
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 decomals = 18;
// 构造函数,为代币初始化名称、符号
constructor(string memory _name, string memory _symbol){
name = _name;
symbol = _symbol;
}
// 实现接口函数
// 代币转账逻辑
// 调用方扣除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;
}
// 实现接口函数
// 代币授权逻辑
// 被授权方spender可以支配授权方amount数量多代币
// spender可以是EOA账户,也可以是合约账户
// 当用uniswap交易代币时,需要将代币授权给uniswap合约
function approve(address spender, uint amount) public override returns (bool){
// allowance映射:[授权方][被授权方][代币额度]
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// 实现接口函数
// 授权转账逻辑
// 被授权方将授权方sender的amount数量代币转账给recipient
function transferFrom(address sender, address recipient, uint amount) public override returns (bool){
// msg.sender --> 被授权方
// sender --> 授权方
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.26;
// 引入标准(后面需要创建合约对象)
import "./IERC20.sol";
contract TokenFaucet{
// 每个用户能领取的代币数量
uint256 public amountAllowed = 100;
// 记录ERC20代币合约地址
address public tokenContract;
// 记录领取过代币的地址
mapping (address => bool) public requestAddress;
// 记录了每次领取代币的地址和数量
event SendToken(address indexed Receiver, uint256 indexed Amount);
// 初始化设定的代币发放合约
constructor(address addr){
tokenContract = addr;
}
// 用户领取代币函数
function requestToken() external {
// 每个地址只能领一次
require(!requestAddress[msg.sender], "Can't Request Multiple Times!");
// 创建代币合约对象
IERC20 token = IERC20(tokenContract);
// 查询发放代币的余额是否还有
require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!");
// 发送token
token.transfer(msg.sender, amountAllowed);
requestAddress[msg.sender] = true;
emit SendToken(msg.sender, amountAllowed);
}
}
演示
首先铸造代币到钱包中:
部署代币水龙头合约,并附上代币合约的地址
:
调用transfer
将代币转给TokenFaucet合约
地址,因为需要将其作为代币发放地址:
查看余额:
切换钱包,获取免费的代币:
查看余额:
03.空投合约
目标:给用户发送ERC20代币空投;
什么是空投?
Airdrop,也称空投,是币圈一种营销手段,项目方将代币免费发给特定的用户群体;
为了拿到空投资格,用户通常需要做一些简单的任务,如测试产品、分享新闻等等,项目方可以通过过空投获得种子用户,用户可以获得财富,两全其美;
由于每次接收空投的用户很多,项目方不可能一笔一笔转账,所以需要智能合约来批量发放代币;
实现空投代币合约
需要准备的合约
IERC20.sol
:ERC20代币标准的合约接口;
ERC20Demo.sol
:自己实现的代币合约;
空投合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./IERC20.sol";
contract AirDrop{
// 计算amount总数
function getSum(uint256[] calldata arr) public pure returns(uint sum){
for (uint i = 0; i < arr.length; i++){
sum = sum + arr[i];
}
return sum;
}
// 向多个地址转代币
function multiTransferToken(address tokenaddr, address[] calldata addrs, uint256[] calldata amounts) external {
// 检查需要发放的地址数和需要发放的代币数的数组长度是否一样
require(addrs.length == amounts.length, "Addrs.length != Amounts.length");
// 声明代币合约对象
IERC20 token = IERC20(tokenaddr);
// 计算此次空投代币总量
uint amountSum = getSum(amounts);
// 检查授权的代币数量是否大于此次发放的代币总数
require(token.allowance(msg.sender, address(this)) > amountSum, "Need Approve ERC20 Token");
// 利用for循环发送代币空投
for (uint i; i < addrs.length; i++){
token.transferFrom(msg.sender, addrs[i], amounts[i]);
}
}
// mapping (address => uint) failTransferList;
// // 空投失败提供主动操作机会
// function withdrawFromFailLists(address to) public {
// // 获取其需要空投的代币数量
// uint faliAmount = failTransferList[msg.sender];
// require(faliAmount > 0, "You are not in failed list");
// failTransferList[msg.sender] = 0;
// (bool success, ) = to.call{value:faliAmount}("");
// require(success, "Fail withdraw");
// }
// 向多个地址转ETH
// function multiTransferETH(address payable[] calldata addrs, uint256[] calldata amounts) public payable {
// // 检查需要发放的地址数和需要发放的代币数的数组长度是否一样
// require(addrs.length == amounts.length, "Addrs.length != Amounts.length");
// // 计算此次ETH总量
// uint amountSum = getSum(amounts);
// // 检查转入ETH是否等于空投总量
// require(msg.value == amountSum, "Transfer amount error");
// // 利用for循环发送ETH
// for (uint i = 0; i < addrs.length; i++){
// // 有被ddos的风险,且transfer并不是推荐写法
// // addrs[i].transfer(amounts[i]);
// // Dos攻击 具体参考 https://github.com/AmazingAng/WTF-Solidity/blob/main/S09_DoS/readme.md
// (bool success, ) = addrs[i].call{value:amounts[i]}("");
// if (!success){
// failTransferList[addrs[i]] = amounts[i];
// }
// }
// }
}
演示
部署自己的代币合约,并铸造1000代币:
部署空投合约,并调用approve
函数,使空投合约能够使用铸造的代币:
随便选两个钱包,并分别给它们空投100,200代币:
addrs = [0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB, 0x617F2E2fD72FD9D5503197092aC168c91465E7f2]
amounts = [100, 200]
注意:钱包得切回授权的地址:
查看余额:
04.ERC721
目标:发行自己的一款NFT
;
同质化和非同质化
BTC
和ETH
这类代币都是属于同质化代币,矿工挖出的第1枚和第100枚没有什么不同,是等价的;
但是,世界上有很多不同质的东西,比如房产、古董、虚拟艺术品等等,这类代币无法用同质化代币来抽象;
因此,以太坊EIP721提出了ERC721
标准,来抽象非同质化的物品。
EIP和ERC
上面提到EIP721
提出了ERC721
;这两个是什么且关系又是什么?
EIP
EIP
全称Ethereum Improvement Proposals(以太坊改进建议)
,是以太坊开发者社区提出的改进建议,是一系列以编号排定的文件,类似互联网上IETF的RFC;
EIP
可以是以太坊生态中任意领域的改进,比如新特性、ERC、协议改进、编程工具等等。
ERC
ERC
全称Ethereum Request For Comment(以太坊意见征求稿)
,用以记录以太坊上应用级的各种开发标准和协议。如典型的Token标准(ERC20
、ERC721
),名字注册(ERC26
、ERC13
)、URI范式(ERC67
)、Library/Package格式(EIP82
)、钱包格式(EIP75
、EIP85
)。
ERC协议标准是影响以太坊发展的重要因素;
两者关系
RIP
包含ERC
;
ERC165
通过ERC165
标准,智能合约可以声明它支持的接口,供其他合约检查。
简单来说,就是检查一个智能合约是否支持了ERC721
、ERC1155
的接口。
此接口合约只声明了一个supportsInterface
函数,输入要查询的interfaceId
接口id,若合约实现了该接口id,则返回true
:
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);
}
ERC721
是如何实现supportsInterface()
函数的:
function supportsInterface(bytes4 interfaceId) external pure override returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}
当查询的是IERC721
或IERC165
的接口id时,返回true
。
ERC721
IERC721
是ERC721
标准的接口合约,规定了ERC721
要实现的基本函数。
它利用tokenId
来表示特定的非同质化代币,授权或转账都要明确tokenId
;
而ERC20
只需要明确转账的数额即可;
/**
* @dev ERC721标准接口.
*/
interface IERC721 is IERC165 {
// 事件
// 在转账时被释放,记录代币的发出地址from、接收地址to和tokenid
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
// 事件
// 在授权时被释放,记录授权的地址owner、被授权的地址approved和tokenid
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
// 事件
// 在批量授权时被释放,记录授权地址owner、被授权地址operator和授权与否approved
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// 返回某地址的NFT持有量
function balanceOf(address owner) external view returns (uint256 balance);
// 返回某tokenid主人地址
function ownerOf(uint256 tokenId) external view returns (address owner);
// 安全转账
// 如果接收方是合约地址,会要求实现ERC721Receiver接口
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
// 安全转账
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
// 普通转账
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
// 授权
function approve(address to, uint256 tokenId) external;
// 将自己持有的该系列NFT批量授权给某个地址operator
function setApprovalForAll(address operator, bool _approved) external;
// 查询tokenId被批准给了哪个地址
function getApproved(uint256 tokenId) external view returns (address operator);
// 查询owner地址的NFT是否批量授权给了operator
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
IERC721Receiver
如果一个合约没有实现ERC721
的相关函数,转入的NFT就进了黑洞,永远出不来。
为了防止误转账,ERC721
实现了safeTransferFrom()
安全转账函数,目标合约必须实现了IERC721Receiver
接口才能接收NFT,不然会Revert
。
IERC721Receiver
接口只包含了一个onERC721Received()
函数:
// ERC721接收者接口:合约必须实现这个接口来通过安全转账接收ERC721
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external returns (bytes4);
}
看一看ERC721
利用_checkOnERC721Received
来确保目标合约实现了onERC721Received()
函数(返回onERC721Received
的selector
):
function _checkOnERC721Received(
address operator,
address from,
address to,
uint256 tokenId,
bytes memory data
) internal {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
// Token rejected
revert IERC721Errors.ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-IERC721Receiver implementer
revert IERC721Errors.ERC721InvalidReceiver(to);
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}
IERC721Metadata
IERC721Metadata
是ERC721
的拓展接口,实现了3个查询metadata
元数据的常用函数:
interface IERC721Metadata is IERC721 {
// 返回代币名称
function name() external view returns (string memory);
// 返回代币代号
function symbol() external view returns (string memory);
// 通过tokenid查询metadata的链接url,ERC721特有的函数
function tokenURI(uint256 tokenId) external view returns (string memory);
}
实现ERC721
需要准备的合约
IERC165.sol
:文件,可以对外声称自己实现了什么接口;
IERC721.sol
:文件,ERC721标准的接口合约;
IERC721Receiver.sol
:文件,给那些想使用safeTransfer函数
合约的接口合约;
IERC721Metadata.sol
:文件,ERC721的拓展合约,查询元数据;
Strings.sol
:文件,由于原先的很多暂用不到,拷几个关键函数就行:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol)
pragma solidity ^0.8.21;
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20;
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
}
return toHexString(value, length);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
}
}
MyNFT合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./String.sol";
// MyNFT,继承IERC721和IERC721Metadata
// 回顾:辈分从高到低排
contract MyNFT is IERC721, IERC721Metadata{
// uint256类型可以使用Strings库
using Strings for uint256;
// IERC721Metadata中的两个接口函数
// Token名称、符号
string public override name;
string public override symbol;
// 四个私有状态变量
// tokenId 到 地址 持有人映射
mapping (uint => address) private _owners;
// 地址 到 持仓量 余额
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_;
}
function supportsInterface(bytes4 interfaceId) external pure override returns (bool){
return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC721Metadata).interfaceId;
}
// 实现IREC721接口合约的各个接口函数
// 查看某账户余额
function balanceOf(address owner) external view override returns (uint){
require(owner != address(0), "owner is 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");
}
// 查询owner地址是否将所持有的NFT批量授权给了operator地址
function isApprovedForAll(address owner, address operator) external view override returns (bool){
return _operatorApprovals[owner][operator];
}
// 将所持有的代币全部授权给operator地址
function setApprovalForAll(address operator, bool approved) external override {
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
// 查询tokenId的授权地址
function getApproved(uint tokenId) external view override returns (address){
require(_owners[tokenId] != address(0), "token doesn't exist");
return _tokenApprovals[tokenId];
}
// 授权函数(私有的)
function _approval(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 or not be approved");
_approval(owner, to, tokenId);
}
// 查询spender地址是否能使用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 zero address");
// 无论之前是否被授权过,转账过后,该代币想当如被重置了,只属于to
_approval(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 or not be approved");
_transfer(owner, from, to, tokenId);
}
// IERC721Receiver
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{
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}
// 安全转账(私有的)
function _safeTransfer(address owner, address from, address to, uint tokenId, bytes memory _data) private {
_transfer(owner, from, to, tokenId);
_checkOnERC721Received(from, to, tokenId, _data);
}
// 安全转账(父亲的)
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 or approved");
_safeTransfer(owner, from, to, tokenId, _data);
}
// 安全转账(父亲的)
function safeTransferFrom(address from, address to, uint tokenId) external override {
safeTransferFrom(from, to, tokenId, "");
}
// 计算tokenURI,tokenURI就是把baseURI和tokenId拼接在一起,需要重写
function _baseURI() internal view virtual returns (string memory){
return "";
}
// IERC721Metadata中的最后一个接口函数
// 返回代币的URI
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())) : "";
}
// 铸造函数
// 通过调整balances和_owners变量来铸造tokenId并转账给to,同时释放Transfer事件
// 由于是demo,这个mint所有人可以调用,但实际上需要加限制条件
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事件
function _burn(uint tokenId) internal virtual {
address owner = ownerOf(tokenId);
require(msg.sender == owner, "not owner of token");
_approval(owner, address(0), tokenId);
_balances[owner] -= 1;
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
}
}
铸造合约
根据MyNFT.sol
写一个免费铸造的合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./MyNFT.sol";
contract WZM is MyNFT{
// 总量
uint public MAX_NUM = 100;
// 构造函数
// 将name_和symbol_传递给MyNFT合约的构造函数
constructor(string memory name_, string memory symbol_) MyNFT(name_, symbol_){}
// 设置baseURI
function _baseURI() internal pure override returns (string memory){
return "wzmwzmwzmwzmwzm";
}
// 铸造函数
function mint(address to, uint tokenId) external {
require(tokenId >= 0 && tokenId < MAX_NUM, "tokenId out of range");
_mint(to, tokenId);
}
}
发行NFT
直接部署WZM.sol
合约:
给一个账户铸造0号NFT:
查看那个账户的余额:
通过tokenId来查看哪个账户拥有:
05.荷兰拍卖
目标:通过荷兰拍卖发售ERC721标准的NFT;
什么是荷兰拍卖?
荷兰拍卖(Dutch Auction)
是一种特殊的拍卖形式,也叫"减价拍卖";
它是指拍卖标的竞价从高到低依次递减直到第一个竞买人应价(达到或超过低价)时成交的一种拍卖。
很多NFT通过此来发售,其中包括Azuki
和World of Women
,其中Azuki
通过荷兰拍卖筹集了超过8000枚ETH。
很多项目方喜欢这个方式,因为:
- 价格是从高往下慢慢降,能获得最大收入;
- 拍卖持续时间长,通常6小时以上,可以避免
gas war
。
Dutch Auction合约
代码基于Azuki
的代码简化而成。
需要准备的合约:
- 自己的
ERC721标准合约
; Ownable.sol
:合约地址,该合约中只提供了一个账户地址,只能这个账户来操作;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./MyNFT.sol";
contract DutchAuctionDemo is Ownable, MyNFT{
// NFT总量
uint256 public constant COLLECTION_SIZE = 100;
// 起拍价
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_PER_STEP = (AUCTION_START_PRICE - AUCTION_END_PRICE) / (AUCTION_TIME / AUCTION_DROP_INTERVAL);
// 拍卖开始时间戳
uint256 public auctionStartTime;
// NFT的URI
string private _baseTokenURI;
// 记录所有存在的tokenId
uint256[] private _allTokens;
// 构造函数
// 设置只能部署的地址使用该合约、名字、符号、时间
constructor() Ownable(msg.sender) MyNFT("WZM_Auction_Name", "WZM_Auction_Symbol"){
auctionStartTime = block.timestamp;
}
// 返回总供给
// ERC721Enumerable中也有
function totalSupply() public view virtual returns (uint256) {
return _allTokens.length;
}
// 添加新tokenId
function _addTokenToAllTokens(uint256 tokenId) private {
_allTokens.push(tokenId);
}
// 重写MyNFT的_baseURI
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}
// 只能持有者设置baseURI
function setBaseURI(string calldata baseURI) external onlyOwner {
_baseTokenURI = baseURI;
}
// 提取筹集的ETH,只能持有者提取
function withdrawMoney() external onlyOwner {
(bool success, ) = msg.sender.call{value:address(this).balance}("");
require(success, "Transfer failed");
}
// 设置拍卖开始时间
function setAuctionStartTime(uint32 timestamp) external onlyOwner {
auctionStartTime = timestamp;
}
// 获得拍卖实时价格
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_PER_STEP);
}
}
// 拍卖mint函数
function auctionMint(uint256 quantity) external payable {
// 检查是否已开始拍卖
uint256 _saleStartTime = uint256(auctionStartTime);
require(_saleStartTime <= block.timestamp && _saleStartTime != 0, "auction has not started");
// 检查当前NFT是否超过总量
require(totalSupply() + quantity <= COLLECTION_SIZE, "mint amount too much");
// 计算价格
uint256 totalCost = getAuctionPrice() * quantity;
// 检查用户是否支付了足够的金额
require(msg.value >= totalCost, "Need to send more ETH");
// 铸造NFT
for (uint i = 0; i < quantity; i++){
uint256 mintIndex = totalSupply();
_mint(msg.sender, mintIndex);
_addTokenToAllTokens(mintIndex);
}
// 多余的ETH退款
if (msg.value > totalCost) {
payable(msg.sender).transfer(msg.value - totalCost);
}
}
}
进行拍卖
部署合约:
设置拍卖开始时间,可以利用这个来获取:
换钱包拍卖,并携带ETH:
尝试用刚才付款的钱包取钱:
换回部署的钱包取款:
标签:function,极简,tokenId,uint256,代币,Solidity,value,应用,address From: https://www.cnblogs.com/WZM1230/p/18592789