首页 > 其他分享 >Solidity极简应用

Solidity极简应用

时间:2024-12-07 22:42:51浏览次数:3  
标签:function 极简 tokenId uint256 代币 Solidity value 应用 address

01.ERC20

目标:发行自己的测试代币;

ERC20是什么?

ERC20是以太坊上的代币标准,实现了代币转账的基本逻辑:

  • 账户余额(balanceOf())
  • 转账(transfer())
  • 授权转账(transferFrom())
  • 授权(approve())
  • 代币总供给(totalSupply())
  • 授权转账额度(allowance())
  • 代币信息(可选)
    • 名称(name())
    • 代号(symbol())
    • 小数位数(decimals())
    • ...

IERC20是什么?

IERC20ERC20代币标准的接口合约,规定了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);
    }
}

发行代币

初始化代币名称和符号:

image-20241202011209501

铸造代币:

image-20241202011359699

查看余额:

image-20241202011453218

可以利用多个钱包来实施转账、授权等操作;

0x5B38Da6a701c568545dCfcB03FcB875f56beddC4授权给0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2300代币;

0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2再转给0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db100代币;

期待的结果:

0x5B38Da6a701c568545dCfcB03FcB875f56beddC4:700
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2:200
0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db:100

0xAb...授权300代币:

image-20241202012109471

授权转账:

image-20241202012420177

查看0x4B...的代币余额:

image-20241202012509742

查看0x5B...的代币余额:

image-20241202012612051

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

演示

首先铸造代币到钱包中:

image-20241203164438527

部署代币水龙头合约,并附上代币合约的地址

image-20241203172243910

调用transfer将代币转给TokenFaucet合约地址,因为需要将其作为代币发放地址:

image-20241203172506380

查看余额:

image-20241203172537932

切换钱包,获取免费的代币:

image-20241203172626639

查看余额:

image-20241203172729406 image-20241203172817190

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代币:

image-20241203185136043

部署空投合约,并调用approve函数,使空投合约能够使用铸造的代币:

image-20241203185443485

随便选两个钱包,并分别给它们空投100,200代币:

addrs = [0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB, 0x617F2E2fD72FD9D5503197092aC168c91465E7f2]
amounts = [100, 200]

注意:钱包得切回授权的地址:

image-20241203190018314

查看余额:

image-20241203190144742 image-20241203190219809

04.ERC721

目标:发行自己的一款NFT

同质化和非同质化

BTCETH这类代币都是属于同质化代币,矿工挖出的第1枚和第100枚没有什么不同,是等价的;

但是,世界上有很多不同质的东西,比如房产、古董、虚拟艺术品等等,这类代币无法用同质化代币来抽象;

因此,以太坊EIP721提出了ERC721标准,来抽象非同质化的物品。

EIP和ERC

上面提到EIP721提出了ERC721;这两个是什么且关系又是什么?

EIP

EIP全称Ethereum Improvement Proposals(以太坊改进建议),是以太坊开发者社区提出的改进建议,是一系列以编号排定的文件,类似互联网上IETF的RFC;

EIP可以是以太坊生态中任意领域的改进,比如新特性、ERC、协议改进、编程工具等等。

ERC

ERC全称Ethereum Request For Comment(以太坊意见征求稿),用以记录以太坊上应用级的各种开发标准和协议。如典型的Token标准(ERC20ERC721),名字注册(ERC26ERC13)、URI范式(ERC67)、Library/Package格式(EIP82)、钱包格式(EIP75EIP85)。

ERC协议标准是影响以太坊发展的重要因素;

两者关系

RIP包含ERC

ERC165

通过ERC165标准,智能合约可以声明它支持的接口,供其他合约检查。

简单来说,就是检查一个智能合约是否支持了ERC721ERC1155的接口。

此接口合约只声明了一个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;
}

当查询的是IERC721IERC165的接口id时,返回true

ERC721

IERC721ERC721标准的接口合约,规定了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()函数(返回onERC721Receivedselector):

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

IERC721MetadataERC721的拓展接口,实现了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合约:

image-20241207182304376

给一个账户铸造0号NFT:

image-20241207182554645

查看那个账户的余额:

image-20241207182708532

通过tokenId来查看哪个账户拥有:

image-20241207182821565

05.荷兰拍卖

目标:通过荷兰拍卖发售ERC721标准的NFT;

什么是荷兰拍卖?

荷兰拍卖(Dutch Auction)是一种特殊的拍卖形式,也叫"减价拍卖";

它是指拍卖标的竞价从高到低依次递减直到第一个竞买人应价(达到或超过低价)时成交的一种拍卖。

很多NFT通过此来发售,其中包括AzukiWorld of Women,其中Azuki通过荷兰拍卖筹集了超过8000枚ETH。

很多项目方喜欢这个方式,因为:

  1. 价格是从高往下慢慢降,能获得最大收入;
  2. 拍卖持续时间长,通常6小时以上,可以避免gas war

Dutch Auction合约

代码基于Azuki代码简化而成。

需要准备的合约:

  1. 自己的ERC721标准合约
  2. 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);
        }
    }
}

进行拍卖

部署合约:

image-20241207222147542

设置拍卖开始时间,可以利用这个来获取:

image-20241207222427635

换钱包拍卖,并携带ETH:

image-20241207222905727

尝试用刚才付款的钱包取钱:

image-20241207223040805

换回部署的钱包取款:

image-20241207223341456

标签:function,极简,tokenId,uint256,代币,Solidity,value,应用,address
From: https://www.cnblogs.com/WZM1230/p/18592789

相关文章

  • 神经网络的定义,组成,工作原理及应用
    **神经网络(NeuralNetwork)**是一种受人类大脑启发的计算模型,是深度学习的核心构成。它模仿生物神经元之间的连接,能够通过大量数据的训练完成分类、回归、生成等任务。以下从概念、结构、工作原理和常见类型等方面详细解释神经网络。1.神经网络的定义神经网络是由多个**人工......
  • 提升代码质量:利用策略模式优化Spring Boot应用的设计!
    ......
  • Spring Boot 从入门到精通:构建高效的 Java 应用
    摘要: 本文全面深入地介绍了SpringBoot框架,涵盖从入门基础到精通应用的各个层面。从SpringBoot的基本概念与特性出发,详细阐述其环境搭建过程、核心组件的使用方法,包括自动配置、起步依赖等。深入探讨数据访问层的构建,如与数据库的集成、使用MyBatis等持久化框架。在We......
  • 2022 年全国职业院校技能大赛网络搭建与应用赛题
    2022年全国职业院校技能大赛网络搭建与应用赛项正式赛卷 第二部分网络搭建及安全部署竞赛总分450分竞赛时长3小时2022年(中职组)网络搭建与应用赛项专家组2022年8月竞赛说明:1.禁止携带和使用移动存储设备、计算器、通信工具及参考资料。2.请根据大赛所......
  • .NET线程池技术详解与优化策略:提升高并发应用性能的关键
    在现代应用程序中,线程池是管理和调度线程的一种重要技术。它通过提供一个可重用的线程集合,避免了频繁创建和销毁线程的开销,从而提升了系统的性能和响应能力。.NET平台的线程池是一个高度优化的资源管理机制,它支持高并发应用的开发,尤其是在Web应用和服务中,广泛用于后台任务处理......
  • 从传统IT架构到云原生应用:迈向现代化IT基础设施
    随着数字化转型的浪潮席卷全球,越来越多的企业正在逐步从传统的IT架构迁移到云原生应用架构。云原生技术不仅让企业能够更加灵活地应对市场变化,还为创新、成本效益、可扩展性和敏捷开发提供了新的动力。与传统IT架构相比,云原生架构通过容器化、微服务、自动化编排等技术,使得企......
  • 最新多项目必备:最强苹果iOS免越狱手机群控系统在多个应用场景中的优势
    随着移动设备管理需求的不断增长,如何高效管理和优化多个iPhone或iPad设备成为了企业和个人用户面临的共同挑战。最新的苹果iOS免越狱手机群控系统提供了一种安全、便捷且高效的解决方案,适用于从自媒体运营到跨境电商等多个领域。本章将详细介绍这款系统的强大功能及其在不同应用场......
  • 实验5 c语言指针应用编程
    实验任务1task1_1.c1#include<stdio.h>2#defineN534voidinput(intx[],intn);5voidoutput(intx[],intn);6voidfind_min_max(intx[],intn,int*pmin,int*pmax);78intmain(){9inta[N];10intmin,max;1112......
  • 一片代码让你明白栈的原理与代码应用
    #include<iostream>usingnamespacestd;#include<stdexcept>template<typenamet>classstack{ //定义一个栈类private:   t*data; //栈中t类型的数据   introngliang; //容量(英语不好)   intsize; //当前大小public:   stack():dat......
  • 顺序栈的应用 ——计算器
    顺序栈的应用——计算器一、写一个程序模拟一个计算器的简单计算操作思路:1.可以使用顺序栈实现,先创建两个顺序栈sop(存放运算符,如()、+、-、*、/)和snum(存放数字)。2.根据输入的表达式,判断数字和运算符,各自放入对应栈中。其中对于运算符的判断如下:如果一开始运算符栈为空或......