ERC20 Contract

2025-05-14T23:42:00

{collapse}
{collapse-item label="一、定义" open}

ERC20

ERC20是以太坊上的代币标准,来自2015年11月V神参与的EIP20。它实现了代币转账的基本逻辑:

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

IERC20

IERC20ERC20代币标准的接口合约,规定了ERC20代币需要实现的函数和事件。 之所以需要定义接口,是因为有了规范后,就存在所有的ERC20代币都通用的函数名称,输入参数,输出参数。 在接口函数中,只需要定义函数名称,输入参数,输出参数,并不关心函数内部如何实现。 由此,函数就分为内部和外部两个内容,一个重点是实现,另一个是对外接口,约定共同数据。 这就是为什么需要ERC20.solIERC20.sol两个文件实现一个合约。

事件

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

现在我们写一个ERC20,将IERC20规定的函数简单实现。

状态变量

我们需要状态变量来记录账户余额,授权额度和代币信息。其中balanceOf, allowancetotalSupplypublic类型,会自动生成一个同名getter函数,实现IERC20规定的balanceOf(), allowance()totalSupply()。而name, symbol, decimals则对应代币的名称,代号和小数位数。

注意:用override修饰public变量,会重写继承自父合约的与变量同名的getter函数,比如IERC20中的balanceOf()函数。

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函数,授权转账逻辑。被授权方将授权方senderamount数量的代币转账给接收方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);
    }

{/collapse-item}
{collapse-item label="二、应用" open}

1. 代币水龙头

代币水龙头就是让用户免费领代币的网站/应用。
实现一个简版的ERC20水龙头,逻辑非常简单:我们将一些ERC20代币转到水龙头合约里,用户可以通过合约的requestToken()函数来领取100单位的代币,每个地址只能领一次。

状态变量
我们在水龙头合约中定义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事件
}

在Remix上测试
(1).先将自定义的dog合约部署

查看日志部署成功 生成合约地址

(2).使用mint铸造1000个dog到自己钱包中

查看日志触发transfer事件铸造成功 从地址0x0转了100个dog到了自己的钱包地址

(3).选择水龙头合约部署 填写dog代币合约地址作为水龙头合约构造函数参数

查看日志合约部署成功 生成合约地址

(4).调用dog合约的transfer发送200个dog给水龙头合约

查看日志钱包转了200个dog到水龙头合约地址

(5).现在使用一个钱包和水龙头合约进行交互获取dog
钱包0x6......7f2收到了100个dog

查询dog合约看下钱包0x6......7f2拥有100个dog代币

2. Airdrop

由于通常接收空投的用户数量较多,项目方逐一发送转账并不实际。通过使用智能合约批量发送ERC20代币,可以显著提高空投的效率。

空投代币合约
空投合约的逻辑很简单:通过循环,单笔交易将ERC20代币发送到多个地址。合约包含2以下函数:

函数getSum():返回数组的总和uint。

// sum function for arrays
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[]类型)中每个地址对应空投金额数组
该函数包含2以下检查:第一个require检查数组的长度是否_addresses等于数组的长度_amounts。第二个require检查空投合约的授权限额是否大于要空投的代币总量。

/// @notice Transfer ERC20 tokens to multiple addresses, authorization is required before use
///
/// @param _token The address of ERC20 token for transfer
/// @param _addresses The array of airdrop addresses
/// @param _amounts The array of amount of tokens (airdrop amount for each address)
function multiTransferToken(
    address _token,
    address[] calldata _addresses,
    uint256[] calldata _amounts
    ) external {
    // Check: The length of _addresses array should be equal to the length of _amounts array
    require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
    IERC20 token = IERC20(_token); // Declare IERC contract variable
    uint _amountSum = getSum(_amounts); // Calculate the total amount of airdropped tokens
    // Check: The authorized amount of tokens should be greater than or equal to the total amount of airdropped tokens
    require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token");
    
    // for loop, use transferFrom function to send airdrops
    for (uint8 i; i < _addresses.length; i++) {
        token.transferFrom(msg.sender, _addresses[i], _amounts[i]);
    }
}

multiTransferETH()功能:发送ETH空投,2参数:
_addresses:接收空投的用户地址数组(address[]类型)
_amounts:空投金额数组,对应_addresses(uint[]类型)中每个地址的数量

/// Transfer ETH to multiple addresses
function multiTransferETH(
    address payable[] calldata _addresses,
    uint256[] calldata _amounts
) public payable {
    // Check: _addresses and _amounts arrays should have the same length
    require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
    // Calculate total amount of ETH to be airdropped
    uint _amountSum = getSum(_amounts);
    // Check: transferred ETH should equal total amount
    require(msg.value == _amountSum, "Transfer amount error");
    // Use a for loop to transfer ETH using transfer function
    for (uint256 i = 0; i < _addresses.length; i++) {
        _addresses[i].transfer(_amounts[i]);
    }
}

在Remix上测试
(1).先将自定义的dog合约部署

查看日志部署成功 生成合约地址

(2).使用mint铸造1000个dog到自己钱包中

查看日志触发transfer事件铸造成功 从地址0x0转了100个dog到了自己的钱包地址

(3).选择airdrop合约部署

查看日志airdrop合约部署成功 生成合约地址

(4).使用dog合约的approve函数授权airdrop合约拥有其中800个dog的权限

查看日志触发approve事件 授权成功 授权airdrop合约操作钱包地址的800个dog

(5).现在管理员就可以通过和airdrop合约交互 指定要发放空投的钱包地址对应的dog个数

查看日志钱包被空投了dog

(6)拿其中一个钱包去dog合约地址调用balanceof查询钱包的dog个数是100和空投的数量一致

{/collapse-item}
{/collapse}

当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »