适合谁读:刚开始学区块链开发、会一点 Solidity 的同学。

本文讲解 TRON 链上 USDT 合约 TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t 的源码(在 Tronscan 上可以查看)。源码一共 10 个 .sol 文件,我们一个文件一个文件地看:它做什么、代码长什么样(已换成中文注释)、有哪些方法、要注意什么。最后简单讲讲怎么自己部署一份来练手。


目录


开始之前:先看全貌

USDT 是一种稳定币:1 USDT 约等于 1 美元,由 Tether 公司发行。用户给公司打美元,公司就在链上“印”出等量的 USDT;用户换回美元,公司就把这些 USDT“销毁”。

所以这个合约除了能转账,还要让发行方能增发、销毁、暂停、冻结坏人的地址。这些功能分散在不同的文件里,最后由 TetherToken.sol 组合到一起:

SafeMath(工具库,负责安全计算)

BasicToken      → 余额 + 转账
  └ StandardToken → 再加上“授权转账”
      └ StandardTokenWithFees → 再加上手续费、代币名称
Ownable         → 管理员
  ├ Pausable    → 暂停开关
  └ BlackList   → 黑名单

TetherToken = StandardTokenWithFees + Pausable + BlackList + 增发/销毁/升级

MultiSigWallet  → 多签钱包,用来当 TetherToken 的管理员(独立合约)
Migrations      → 部署工具用的记录文件(和业务无关)

小知识:contract A is B, C 表示 A 继承 B 和 C,可以直接使用它们的变量和方法。子合约中的 super.方法名() 表示调用父合约里的同名方法。

在 TRON 上,代币标准叫 TRC20,接口和以太坊的 ERC20 完全一样,所以下文统一称为 ERC20。

阅读源码时有几处 0.4 版本的老语法,先认识一下:

老写法(0.4)新写法(0.5+)含义
function 合约名() public {}constructor() public {}构造函数,部署时执行一次
constantview只读函数,不修改链上数据
Transfer(...)emit Transfer(...)触发事件(写入日志,供钱包和浏览器读取)
throwrevert()报错,交易回滚

1. SafeMath.sol:安全的加减乘除

它做什么:在 Solidity 0.8 之前,数字超出范围不会报错,而是“绕回去”。比如 0 - 1 会变成一个巨大的数,攻击者可以利用这一点凭空造钱。SafeMath 在每次计算后检查结果,一旦出错就让交易失败。

pragma solidity ^0.4.18;


/**
 * @title SafeMath 安全数学库
 * @dev 0.8 之前整数溢出会静默回绕,这里在每次运算后做校验,异常时让交易失败。
 *      通过 `using SafeMath for uint256;` 挂载后,可用 a.add(b) 的形式调用。
 */
library SafeMath {
  /**
   * @dev 乘法。用 c / a == b 反推校验是否溢出。
   * @param a 被乘数
   * @param b 乘数
   * @return a * b
   */
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    // a 为 0 时直接返回,同时避免下面 c / a 出现除零
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  /**
   * @dev 除法。除数为 0 时 EVM 本身会抛异常,无需额外检查。
   * @param a 被除数
   * @param b 除数
   * @return a / b(向下取整)
   */
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a / b;
    return c;
  }

  /**
   * @dev 减法。要求 b <= a,防止下溢;余额不足的兜底检查最终依赖它。
   * @param a 被减数
   * @param b 减数
   * @return a - b
   */
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  /**
   * @dev 加法。若发生回绕,结果会小于 a。
   * @param a 加数
   * @param b 加数
   * @return a + b
   */
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

方法一览

方法参数返回值例子
mula, ba * bmul(3, 4) = 12
diva, ba / b(向下取整)div(7, 2) = 3
suba, ba - bsub(5, 3) = 2;sub(3, 5) 交易失败
adda, ba + badd(1, 2) = 3

怎么用:在其他合约里写一句 using SafeMath for uint256;,之后就可以写 a.add(b)、a.sub(b)。

注意:如果你用 0.8 及以上版本写新合约,编译器已经自带溢出检查,不再需要 SafeMath。


2. Ownable.sol:谁是管理员

它做什么:记录一个管理员地址 owner,并提供 onlyOwner 修饰器。加了 onlyOwner 的方法只有管理员能调用。USDT 的增发、冻结等功能都靠它保护。

pragma solidity ^0.4.18;


/**
 * @title Ownable 单一管理员权限
 * @dev 合约记录一个 owner 地址,并提供 onlyOwner 修饰器做权限控制。
 */
contract Ownable {
  // 管理员地址;public 会自动生成 owner() 查询函数
  address public owner;


  // 管理员变更事件
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);


  /**
   * @dev 构造函数(0.4 语法:与合约同名的函数即构造函数),部署者成为 owner。
   */
  function Ownable() public {
    owner = msg.sender;
  }


  /**
   * @dev 仅 owner 可调用。`_;` 处会插入被修饰函数的函数体。
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }


  /**
   * @dev 转移管理权。一步完成,填错地址会导致权限永久丢失。
   * @param newOwner 新管理员地址,不能为零地址
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    // 先发事件再赋值,保证事件里的 previousOwner 是旧值
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }

}

方法一览

方法参数返回值说明
构造函数无—部署合约的人自动成为 owner
owner()无管理员地址自动生成的查询方法(因为变量是 public)
transferOwnershipnewOwner 新管理员地址无只有当前管理员能调用,不能传零地址

小知识:修饰器(modifier)。onlyOwner 里的 _; 代表“被修饰的函数本体”,意思是先执行 require 检查,通过后再执行函数本体。

注意:transferOwnership 一步就生效,地址填错就再也拿不回管理权了。


3. Pausable.sol:紧急暂停开关

它做什么:提供一个 paused 开关。管理员打开开关后,所有加了 whenNotPaused 的方法(在 USDT 里就是转账和授权)都会失败。用于出现安全事故时“先停下来”。

pragma solidity ^0.4.18;


import "./Ownable.sol";


/**
 * @title Pausable 紧急暂停
 * @dev 为子合约提供暂停开关;子合约用 whenNotPaused 修饰需要被暂停的函数。
 */
contract Pausable is Ownable {
  // 暂停 / 恢复事件
  event Pause();
  event Unpause();

  // 全局暂停状态,默认不暂停
  bool public paused = false;


  /**
   * @dev 仅在未暂停时可调用。
   */
  modifier whenNotPaused() {
    require(!paused);
    _;
  }

  /**
   * @dev 仅在暂停时可调用。
   */
  modifier whenPaused() {
    require(paused);
    _;
  }

  /**
   * @dev 暂停。仅 owner,且当前必须处于未暂停状态(避免重复发事件)。
   */
  function pause() onlyOwner whenNotPaused public {
    paused = true;
    Pause();
  }

  /**
   * @dev 恢复。仅 owner,且当前必须处于暂停状态。
   */
  function unpause() onlyOwner whenPaused public {
    paused = false;
    Unpause();
  }
}

方法一览

方法参数返回值说明
paused()无true / false当前是否已暂停
pause()无无管理员暂停(已经暂停时再调用会失败)
unpause()无无管理员恢复(未暂停时调用会失败)

注意:暂停只拦住普通用户的转账和授权,管理员的增发、销毁不受影响(实测过)。查询余额也不受影响。


4. BlackList.sol:黑名单

它做什么:记录哪些地址被拉黑。它本身只是一个“名单”,真正的拦截写在 TetherToken.sol 里:被拉黑的地址无法转出 USDT。

pragma solidity ^0.4.18;

import "./Ownable.sol";

/**
 * @title BlackList 地址黑名单
 * @dev 只负责记录标志位,是否拦截由 TetherToken 在各方法中检查。
 */
contract BlackList is Ownable {

    /**
     * @dev 查询黑名单状态。与 isBlackListed 的自动 getter 功能相同,
     *      单独提供 external 版本是为了让其他合约(包括升级后的新合约)复用同一份黑名单。
     * @param _maker 要查询的地址
     * @return 是否已被拉黑
     */
    function getBlackListStatus(address _maker) external constant returns (bool) {
        return isBlackListed[_maker];
    }

    // 地址 => 是否被拉黑
    mapping (address => bool) public isBlackListed;

    /**
     * @dev 拉黑地址。仅 owner;不检查是否已在名单中。
     * @param _evilUser 要拉黑的地址
     */
    function addBlackList (address _evilUser) public onlyOwner {
        isBlackListed[_evilUser] = true;
        AddedBlackList(_evilUser);
    }

    /**
     * @dev 解除拉黑。仅 owner。
     * @param _clearedUser 要解除的地址
     */
    function removeBlackList (address _clearedUser) public onlyOwner {
        isBlackListed[_clearedUser] = false;
        RemovedBlackList(_clearedUser);
    }

    // 拉黑事件
    event AddedBlackList(address indexed _user);

    // 解除拉黑事件
    event RemovedBlackList(address indexed _user);

}

方法一览

方法参数返回值例子
isBlackListed(地址)要查询的地址true / false自动生成的查询方法
getBlackListStatus(地址)要查询的地址true / false和上面一样,方便其他合约调用
addBlackList(地址)要拉黑的地址无管理员调用 addBlackList(A),A 从此无法转出
removeBlackList(地址)要解除的地址无管理员解除拉黑

小知识:mapping。mapping (address => bool) 可以理解成一张“地址 → 是/否”的表,没设置过的地址默认是 false。


5. BasicToken.sol:余额和转账

它做什么:这是代币最核心的部分。用一张表 balances 记录每个地址有多少钱,再提供 transfer 转账和 balanceOf 查余额。

文件开头的 ERC20Basic 只声明了方法、没有实现,作用相当于“接口”,规定代币必须具备哪些方法。

pragma solidity ^0.4.18;


import './SafeMath.sol';

/**
 * @title ERC20Basic 最小代币接口(EIP-179)
 * @dev 只有声明没有实现,相当于接口。
 */
contract ERC20Basic {
  // 查询总供应量
  function totalSupply() public constant returns (uint);
  // 查询某地址余额
  function balanceOf(address who) public view returns (uint256);
  // 从调用者账户转账给 to
  function transfer(address to, uint256 value) public returns (bool);
  // 转账事件(钱包、区块浏览器依赖它展示记录)
  event Transfer(address indexed from, address indexed to, uint256 value);
}

/**
 * @title BasicToken 基础代币
 * @dev 实现余额与转账,不含授权功能;totalSupply 留给子合约实现。
 */
contract BasicToken is ERC20Basic {
  using SafeMath for uint256;

  // 账本:地址 => 余额(默认 internal,子合约可直接访问)
  mapping(address => uint256) balances;

  /**
   * @dev 从调用者账户转账。
   * @param _to 接收地址,不能为零地址(销毁应走专门的方法)
   * @param _value 转账数量(最小单位)
   * @return 成功返回 true
   */
  function transfer(address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    // 显式检查余额:失败时退还剩余 gas,比依赖 sub 的 assert 更友好
    require(_value <= balances[msg.sender]);

    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    Transfer(msg.sender, _to, _value);
    return true;
  }

  /**
   * @dev 查询余额。
   * @param _owner 要查询的地址
   * @return balance 该地址余额,未出现过的地址返回 0
   */
  function balanceOf(address _owner) public view returns (uint256 balance) {
    return balances[_owner];
  }

}

方法一览

方法参数返回值例子
balanceOf_owner 地址余额balanceOf(A) = 1000000000(即 1000 枚,6 位小数)
transfer_to 收款地址,_value 数量true从调用者账户扣钱,给 _to 加钱

转账的四个步骤:① 收款地址不能是零地址 → ② 检查余额够不够 → ③ 扣钱、加钱 → ④ 触发 Transfer 事件(钱包靠它显示转账记录)。

小知识:数量都用“最小单位”。USDT 有 6 位小数,所以 1 USDT 在合约里写作 1000000。


6. StandardToken.sol:授权别人帮你转账

它做什么:在 BasicToken 的基础上增加“授权”功能:A 先授权 C 可以动用自己 500 枚,C 就能从 A 的账户转出最多 500 枚。去中心化交易所(DEX)就是靠这个机制帮你完成交易的。

pragma solidity ^0.4.18;


import './BasicToken.sol';


/**
 * @title ERC20 完整接口(EIP-20)
 * @dev 在 ERC20Basic 之上增加授权代扣能力。
 */
contract ERC20 is ERC20Basic {
  // 查询 owner 授权给 spender 的剩余额度
  function allowance(address owner, address spender) public view returns (uint256);
  // 调用者使用额度,从 from 转账给 to
  function transferFrom(address from, address to, uint256 value) public returns (bool);
  // 调用者授权 spender 可动用的额度
  function approve(address spender, uint256 value) public returns (bool);
  // 授权事件
  event Approval(address indexed owner, address indexed spender, uint256 value);
}

/**
 * @title StandardToken 标准 ERC20 代币
 * @dev 在 BasicToken 基础上实现授权相关方法。
 */
contract StandardToken is ERC20, BasicToken {

  // 授权表:owner => spender => 剩余额度
  mapping (address => mapping (address => uint256)) internal allowed;


  /**
   * @dev 调用者(spender)使用 _from 的授权额度转账。
   * @param _from 付款地址
   * @param _to 收款地址,不能为零地址
   * @param _value 转账数量
   * @return 成功返回 true
   */
  function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    require(_value <= balances[_from]);
    require(_value <= allowed[_from][msg.sender]);

    balances[_from] = balances[_from].sub(_value);
    balances[_to] = balances[_to].add(_value);
    allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
    Transfer(_from, _to, _value);
    return true;
  }

  /**
   * @dev 设置授权额度(直接覆盖旧值)。
   *      注意竞态:从非 0 额度直接改成另一个非 0 值时,spender 可能抢先用掉旧额度,
   *      再用掉新额度。建议先改为 0,或改用 increaseApproval / decreaseApproval。
   * @param _spender 被授权地址
   * @param _value 授权额度
   * @return 成功返回 true
   */
  function approve(address _spender, uint256 _value) public returns (bool) {
    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);
    return true;
  }

  /**
   * @dev 查询剩余授权额度。
   * @param _owner 授权方
   * @param _spender 被授权方
   * @return 剩余额度
   */
  function allowance(address _owner, address _spender) public view returns (uint256) {
    return allowed[_owner][_spender];
  }

  /**
   * @dev 在现有额度上增加,避免 approve 的竞态问题。
   * @param _spender 被授权地址
   * @param _addedValue 增加量
   * @return 成功返回 true;Approval 事件中携带的是新的总额度
   */
  function increaseApproval(address _spender, uint _addedValue) public returns (bool) {
    allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue);
    Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
    return true;
  }

  /**
   * @dev 在现有额度上减少;减少量超过当前额度时置 0,而不是回滚。
   * @param _spender 被授权地址
   * @param _subtractedValue 减少量
   * @return 成功返回 true;Approval 事件中携带的是新的总额度
   */
  function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) {
    uint oldValue = allowed[msg.sender][_spender];
    if (_subtractedValue > oldValue) {
      allowed[msg.sender][_spender] = 0;
    } else {
      allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
    }
    Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
    return true;
  }

}

方法一览(场景:A 有 1000 枚,已授权 C 500 枚)

方法参数返回值例子
approve_spender 被授权人,_value 额度trueA 调用 approve(C, 300000000),额度直接改成 300 枚
allowance_owner 授权人,_spender 被授权人剩余额度allowance(A, C) = 500000000
transferFrom_from 付款人,_to 收款人,_value 数量trueC 调用 transferFrom(A, B, 200000000):A 少 200 枚,B 多 200 枚,额度剩 300 枚
increaseApproval_spender,_addedValue 增加量trueA 调用 increaseApproval(C, 100000000),额度变为 600 枚
decreaseApproval_spender,_subtractedValue 减少量true减少量超过当前额度时,额度变为 0(不会报错)

为什么有了 approve 还要 increase/decreaseApproval? approve 是直接覆盖旧额度的。假设 A 想把给 C 的额度从 100 改成 50,C 可以在改动生效前抢先用掉 100,改完后再用掉 50,一共拿走 150。按“增加 / 减少”来修改额度,就不会出现这个问题。


7. StandardTokenWithFees.sol:手续费和代币信息

它做什么:两件事。

  1. 存放代币信息:名称 name、符号 symbol、小数位 decimals、总量 _totalSupply。
  2. 预留转账手续费功能:可以按比例收费,收到的费用归管理员。链上 USDT 的费率一直是 0,从来没有收过费。
pragma solidity ^0.4.18;

import "./StandardToken.sol";
import "./Ownable.sol";

/**
 * @title StandardTokenWithFees 带手续费的标准代币
 * @dev 在 ERC20 上增加可配置手续费(默认 0),并存放代币元数据。
 */
contract StandardTokenWithFees is StandardToken, Ownable {

  // 费率,单位为基点(1 bp = 0.01%),默认 0
  uint256 public basisPointsRate = 0;
  // 单笔手续费上限(最小单位),默认 0
  uint256 public maximumFee = 0;
  // 硬编码上限:费率必须 < 20 bp,单笔上限必须 < 50 个整币。
  // 写成常量是为了让任何人都能在源码中确认发行方最多能收多少费
  uint256 constant MAX_SETTABLE_BASIS_POINTS = 20;
  uint256 constant MAX_SETTABLE_FEE = 50;

  // 代币元数据
  string public name;
  string public symbol;
  uint8 public decimals;
  // 总供应量存储变量,由 TetherToken.totalSupply() 读取
  uint public _totalSupply;

  // 无限授权标记:额度等于它时 transferFrom 不再扣减额度
  uint public constant MAX_UINT = 2**256 - 1;

  /**
   * @dev 计算手续费:按比例计算后以 maximumFee 封顶。未写可见性,0.4 中默认 public。
   * @param _value 转账数量
   * @return 手续费数量
   */
  function calcFee(uint _value) constant returns (uint) {
    uint fee = (_value.mul(basisPointsRate)).div(10000);
    if (fee > maximumFee) {
        fee = maximumFee;
    }
    return fee;
  }

  /**
   * @dev 带手续费的转账:接收方收到扣费后的金额,手续费转给 owner。
   *      注意:函数没有 return 语句,因此始终返回默认值 false。
   * @param _to 接收地址
   * @param _value 发送方付出的总数量(含手续费)
   * @return 始终为 false(缺少 return 导致的缺陷)
   */
  function transfer(address _to, uint _value) public returns (bool) {
    uint fee = calcFee(_value);
    uint sendAmount = _value.sub(fee);

    // 两次调用都以 msg.sender 为付款方,发送方共计付出 _value
    super.transfer(_to, sendAmount);
    if (fee > 0) {
      super.transfer(owner, fee);
    }
  }

  /**
   * @dev 带手续费的授权转账。完全重写而不调用 super,因为父类会把全额转给 _to。
   * @param _from 付款地址
   * @param _to 收款地址
   * @param _value 付款方付出的总数量(含手续费)
   * @return 成功返回 true
   */
  function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    require(_value <= balances[_from]);
    require(_value <= allowed[_from][msg.sender]);

    uint fee = calcFee(_value);
    uint sendAmount = _value.sub(fee);

    balances[_from] = balances[_from].sub(_value);
    balances[_to] = balances[_to].add(sendAmount);
    // 无限授权时不扣减额度,省掉一次存储写入
    if (allowed[_from][msg.sender] < MAX_UINT) {
        allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
    }
    Transfer(_from, _to, sendAmount);
    if (fee > 0) {
      balances[owner] = balances[owner].add(fee);
      Transfer(_from, owner, fee);
    }
    return true;
  }

  /**
   * @dev 设置手续费参数。仅 owner,且不能超过硬编码上限。
   * @param newBasisPoints 新费率(基点),必须 < 20
   * @param newMaxFee 单笔上限(整币单位),必须 < 50;内部会乘以 10^decimals
   */
  function setParams(uint newBasisPoints, uint newMaxFee) public onlyOwner {
      require(newBasisPoints < MAX_SETTABLE_BASIS_POINTS);
      require(newMaxFee < MAX_SETTABLE_FEE);

      basisPointsRate = newBasisPoints;
      maximumFee = newMaxFee.mul(uint(10)**decimals);

      Params(basisPointsRate, maximumFee);
  }

  // 手续费参数变更事件
  event Params(uint feeBasisPoints, uint maxFee);

}

方法一览

方法参数返回值例子
name() / symbol() / decimals()无代币信息链上 USDT 为 "Tether USD" / "USDT" / 6
calcFee_value 转账数量手续费费率为 0 时始终返回 0
setParamsnewBasisPoints 费率,newMaxFee 单笔上限(整币)无管理员调用 setParams(10, 5):费率 0.1%,单笔最多收 5 枚
transfer_to,_valuefalse(缺陷,见下)设置 (10, 5) 后转 100 枚:对方收到 99.9 枚,管理员收到 0.1 枚
transferFrom_from,_to,_valuetrue授权额度为最大值 MAX_UINT 时,转账后额度不会减少(“无限授权”)

看点 1:手续费有上限。MAX_SETTABLE_BASIS_POINTS = 20 和 MAX_SETTABLE_FEE = 50 是写死在代码里的常量:费率最高 0.19%,单笔最多 49 枚。任何人都能看到这个上限,发行方无法偷偷提高。

看点 2:transfer 永远返回 false。这个函数声明了 returns (bool),却忘了写 return true;,于是返回默认值 false。钱其实转成功了,但如果别的合约写了 require(usdt.transfer(...)),就会误以为转账失败而报错。这是 TRON 版 USDT 一个真实存在的缺陷,和它交互的合约需要特别处理。


8. TetherToken.sol:最终的 USDT 合约

它做什么:把前面所有功能组合起来,形成最终部署上链的 USDT 合约,并额外增加:

  • 转账前检查“是否暂停”“是否被拉黑”;
  • issue 增发、redeem 销毁;
  • destroyBlackFunds 清空被拉黑地址的余额;
  • deprecate 升级:合约部署后无法修改,于是采用“旧合约把请求转发给新合约”的办法升级。

文件开头的 UpgradedStandardToken 规定了“新合约必须提供哪些方法”,平时用不到,升级时才会用到。

pragma solidity ^0.4.18;

import "./StandardTokenWithFees.sol";
import "./Pausable.sol";
import "./BlackList.sol";

/**
 * @title UpgradedStandardToken 升级目标合约的接口约定
 * @dev 旧合约废弃后会调用这些 ByLegacy 方法,把原调用者作为参数转发过来。
 *      新合约必须校验 msg.sender == 旧合约地址,否则任何人都能伪造 from 冒充他人。
 */
contract UpgradedStandardToken is StandardToken {
    uint public _totalSupply;
    // 转发 transfer:from 为原调用者
    function transferByLegacy(address from, address to, uint value) public returns (bool);
    // 转发 transferFrom:sender 为原调用者(spender);第三个参数实际传入的是收款地址
    function transferFromByLegacy(address sender, address from, address spender, uint value) public returns (bool);
    // 转发 approve:from 为原调用者
    function approveByLegacy(address from, address spender, uint value) public returns (bool);
    // 转发 increaseApproval
    function increaseApprovalByLegacy(address from, address spender, uint addedValue) public returns (bool);
    // 转发 decreaseApproval
    function decreaseApprovalByLegacy(address from, address spender, uint subtractedValue) public returns (bool);
}


/**
 * @title TetherToken 最终部署的代币合约
 * @dev 继承线性化顺序:TetherToken → BlackList → StandardTokenWithFees → Pausable
 *      → Ownable → StandardToken → BasicToken → ERC20 → ERC20Basic。
 *      在 ERC20 之上增加:暂停、黑名单、增发 / 销毁、黑名单资金销毁、升级转发。
 */
contract TetherToken is Pausable, StandardTokenWithFees, BlackList {

    // 升级后的新合约地址
    address public upgradedAddress;
    // 当前合约是否已废弃
    bool public deprecated;

    /**
     * @dev 构造函数:设置元数据,初始供应量全部发给 owner(即部署者)。
     *      注意没有发出 Transfer(0x0, owner, _initialSupply) 事件。
     * @param _initialSupply 初始供应量(最小单位,例如 6 位小数下 1 枚 = 1000000)
     * @param _name 代币名称
     * @param _symbol 代币符号
     * @param _decimals 小数位数
     */
    function TetherToken(uint _initialSupply, string _name, string _symbol, uint8 _decimals) public {
        _totalSupply = _initialSupply;
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        // 此时 Ownable 构造函数已执行完毕,owner 已是部署者
        balances[owner] = _initialSupply;
        deprecated = false;
    }

    /**
     * @dev 转账。检查暂停和发送方黑名单;已废弃时转发到新合约。
     * @param _to 接收地址
     * @param _value 数量
     * @return 未废弃时为 StandardTokenWithFees.transfer 的返回值(始终 false)
     */
    function transfer(address _to, uint _value) public whenNotPaused returns (bool) {
        require(!isBlackListed[msg.sender]);
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value);
        } else {
            return super.transfer(_to, _value);
        }
    }

    /**
     * @dev 授权转账。检查暂停和付款方黑名单(不检查调用者和收款方)。
     * @param _from 付款地址
     * @param _to 收款地址
     * @param _value 数量
     * @return 成功返回 true
     */
    function transferFrom(address _from, address _to, uint _value) public whenNotPaused returns (bool) {
        require(!isBlackListed[_from]);
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).transferFromByLegacy(msg.sender, _from, _to, _value);
        } else {
            return super.transferFrom(_from, _to, _value);
        }
    }

    /**
     * @dev 查询余额;已废弃时查询新合约。
     * @param who 要查询的地址
     * @return 余额
     */
    function balanceOf(address who) public constant returns (uint) {
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).balanceOf(who);
        } else {
            return super.balanceOf(who);
        }
    }

    /**
     * @dev 查询本合约中冻结的历史余额,供新合约迁移数据使用。
     * @param who 要查询的地址
     * @return 已废弃时返回本合约余额;未废弃时没有 return,返回 0
     */
    function oldBalanceOf(address who) public constant returns (uint) {
        if (deprecated) {
            return super.balanceOf(who);
        }
    }

    /**
     * @dev 设置授权额度;已废弃时转发到新合约。
     * @param _spender 被授权地址
     * @param _value 额度
     * @return 成功返回 true
     */
    function approve(address _spender, uint _value) public whenNotPaused returns (bool) {
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).approveByLegacy(msg.sender, _spender, _value);
        } else {
            return super.approve(_spender, _value);
        }
    }

    /**
     * @dev 增加授权额度;已废弃时转发到新合约。
     * @param _spender 被授权地址
     * @param _addedValue 增加量
     * @return 成功返回 true
     */
    function increaseApproval(address _spender, uint _addedValue) public whenNotPaused returns (bool) {
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).increaseApprovalByLegacy(msg.sender, _spender, _addedValue);
        } else {
            return super.increaseApproval(_spender, _addedValue);
        }
    }

    /**
     * @dev 减少授权额度;已废弃时转发到新合约。
     * @param _spender 被授权地址
     * @param _subtractedValue 减少量
     * @return 成功返回 true
     */
    function decreaseApproval(address _spender, uint _subtractedValue) public whenNotPaused returns (bool) {
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).decreaseApprovalByLegacy(msg.sender, _spender, _subtractedValue);
        } else {
            return super.decreaseApproval(_spender, _subtractedValue);
        }
    }

    /**
     * @dev 查询授权额度;已废弃时查询新合约。
     * @param _owner 授权方
     * @param _spender 被授权方
     * @return remaining 剩余额度
     */
    function allowance(address _owner, address _spender) public constant returns (uint remaining) {
        if (deprecated) {
            return StandardToken(upgradedAddress).allowance(_owner, _spender);
        } else {
            return super.allowance(_owner, _spender);
        }
    }

    /**
     * @dev 废弃当前合约,此后 ERC20 调用都转发到新合约。仅 owner。
     *      余额不会自动迁移;可以再次调用以指向另一个地址,但无法取消废弃。
     * @param _upgradedAddress 新合约地址,不能为零地址
     */
    function deprecate(address _upgradedAddress) public onlyOwner {
        require(_upgradedAddress != address(0));
        deprecated = true;
        upgradedAddress = _upgradedAddress;
        Deprecate(_upgradedAddress);
    }

    /**
     * @dev 查询总供应量;已废弃时查询新合约。
     * @return 总供应量
     */
    function totalSupply() public constant returns (uint) {
        if (deprecated) {
            return StandardToken(upgradedAddress).totalSupply();
        } else {
            return _totalSupply;
        }
    }

    /**
     * @dev 增发,新币只能进入 owner 账户。仅 owner,不受暂停影响。
     * @param amount 增发数量(最小单位)
     */
    function issue(uint amount) public onlyOwner {
        balances[owner] = balances[owner].add(amount);
        _totalSupply = _totalSupply.add(amount);
        Issue(amount);
        // 按惯例以“从零地址转出”表示铸造,区块浏览器据此识别增发
        Transfer(address(0), owner, amount);
    }

    /**
     * @dev 赎回销毁,只能销毁 owner 自己的余额。仅 owner,余额不足时失败。
     * @param amount 销毁数量(最小单位)
     */
    function redeem(uint amount) public onlyOwner {
        _totalSupply = _totalSupply.sub(amount);
        balances[owner] = balances[owner].sub(amount);
        Redeem(amount);
        Transfer(owner, address(0), amount);
    }

    /**
     * @dev 清零已拉黑地址的余额,并减少总供应量。仅 owner。
     *      注意:没有发出 Transfer 事件;废弃后 balanceOf 会读新合约的余额,导致数据错乱。
     * @param _blackListedUser 已在黑名单中的地址
     */
    function destroyBlackFunds (address _blackListedUser) public onlyOwner {
        require(isBlackListed[_blackListedUser]);
        uint dirtyFunds = balanceOf(_blackListedUser);
        balances[_blackListedUser] = 0;
        _totalSupply = _totalSupply.sub(dirtyFunds);
        DestroyedBlackFunds(_blackListedUser, dirtyFunds);
    }

    // 黑名单资金被销毁
    event DestroyedBlackFunds(address indexed _blackListedUser, uint _balance);

    // 增发
    event Issue(uint amount);

    // 赎回销毁
    event Redeem(uint amount);

    // 合约废弃并指向新地址
    event Deprecate(address newAddress);

}

构造函数:部署时传入 4 个参数。链上 USDT 部署时传入的是:

_initialSupply = 10000000      // 初始只发了 10 枚(6 位小数),之后全靠 issue 增发
_name          = "Tether USD"
_symbol        = "USDT"
_decimals      = 6

名字能不能也叫 USDT? 名字只是普通的字符串参数,技术上谁都能填 “USDT”,链上也确实有很多同名代币。但真假只看合约地址:钱包和交易所只认 TR7NHq... 这个地址。拿同名代币去骗人是常见的诈骗手法,所以练习时请用自己起的名字,比如 My Test Dollar / MTD,对代码运行没有任何影响。

方法一览

方法参数返回值谁能调用说明
transfer_to,_valuefalse(同第 7 节)任何人未暂停、自己没被拉黑才能转
transferFrom_from,_to,_valuetrue被授权人未暂停、付款人没被拉黑才能转
approve / increaseApproval / decreaseApproval同第 6 节true任何人暂停时不能调用
balanceOf / allowance / totalSupply同前数值任何人查询,暂停时也能用
issueamount 数量无管理员增发,新币进入管理员账户。例:issue(500000000000) 增发 50 万枚
redeemamount 数量无管理员从管理员账户销毁,余额不足会失败
destroyBlackFunds被拉黑的地址无管理员清空该地址余额,总量同步减少;地址必须先被拉黑
deprecate新合约地址无管理员升级。之后所有调用都转发到新合约
oldBalanceOf地址旧合约里的余额任何人只在升级后有意义,未升级时返回 0

看点:黑名单只拦“转出”。被拉黑的地址不能转出,但仍然可以收款(实测过)。所以发行方通常先拉黑,再用 destroyBlackFunds 清空余额。


9. MultiSigWallet.sol:多签钱包

它做什么:一个由多人共同管理的钱包合约。比如 3 个人管理、至少 2 人同意才能执行操作(称为 2/3 多签)。

为什么需要它?USDT 的管理员能增发、冻结,权力非常大。如果管理员只是一个私钥,私钥被盗就全完了。所以链上 USDT 的管理员其实是一个多签钱包(地址 TBPxhVAsuzoFnKyXtc1o2UySEydPHgATto,2/3 多签)。

使用流程:

  1. 管理员 1 调用 submitTransaction(目标合约, 0, 调用数据) 提交一个操作,自动算作 1 票;
  2. 管理员 2 调用 confirmTransaction(交易编号) 确认,票数达到 2,自动执行;
  3. 执行时,多签合约去调用目标合约(例如 USDT 的 issue)。
pragma solidity ^0.4.10;

/**
 * @title MultiSigWallet 多签钱包
 * @dev 多个管理员共同确认后才执行交易(ConsenSys / Gnosis 早期实现)。
 *      可作为 TetherToken 的 owner,使增发、冻结等操作需要 N/M 签名。
 */
contract MultiSigWallet {

    // 管理员数量上限
    uint constant public MAX_OWNER_COUNT = 50;

    event Confirmation(address indexed sender, uint indexed transactionId);   // 管理员确认
    event Revocation(address indexed sender, uint indexed transactionId);     // 管理员撤回确认
    event Submission(uint indexed transactionId);                             // 提交新交易
    event Execution(uint indexed transactionId);                              // 执行成功
    event ExecutionFailure(uint indexed transactionId);                       // 执行失败
    event Deposit(address indexed sender, uint value);                        // 收到原生币
    event OwnerAddition(address indexed owner);                               // 新增管理员
    event OwnerRemoval(address indexed owner);                                // 移除管理员
    event RequirementChange(uint required);                                   // 确认门槛变更

    // 交易 ID => 交易
    mapping (uint => Transaction) public transactions;
    // 交易 ID => 管理员 => 是否已确认
    mapping (uint => mapping (address => bool)) public confirmations;
    // 地址 => 是否为管理员
    mapping (address => bool) public isOwner;
    // 管理员列表
    address[] public owners;
    // 执行所需确认数
    uint public required;
    // 交易总数,同时是下一笔交易的 ID
    uint public transactionCount;

    // 一笔待执行的外部调用
    struct Transaction {
        address destination;   // 调用目标
        uint value;            // 附带的原生币数量
        bytes data;            // calldata,例如 issue(uint256) 的 ABI 编码
        bool executed;         // 是否已执行
    }

    // 只允许钱包自身调用:管理员增删改必须通过多签交易发起
    modifier onlyWallet() {
        if (msg.sender != address(this))
            throw;
        _;
    }

    // 地址必须还不是管理员
    modifier ownerDoesNotExist(address owner) {
        if (isOwner[owner])
            throw;
        _;
    }

    // 地址必须已是管理员
    modifier ownerExists(address owner) {
        if (!isOwner[owner])
            throw;
        _;
    }

    // 交易必须存在(以 destination 非零判断)
    modifier transactionExists(uint transactionId) {
        if (transactions[transactionId].destination == 0)
            throw;
        _;
    }

    // 该管理员必须已确认此交易
    modifier confirmed(uint transactionId, address owner) {
        if (!confirmations[transactionId][owner])
            throw;
        _;
    }

    // 该管理员必须尚未确认此交易
    modifier notConfirmed(uint transactionId, address owner) {
        if (confirmations[transactionId][owner])
            throw;
        _;
    }

    // 交易必须尚未执行
    modifier notExecuted(uint transactionId) {
        if (transactions[transactionId].executed)
            throw;
        _;
    }

    // 地址不能为零
    modifier notNull(address _address) {
        if (_address == 0)
            throw;
        _;
    }

    // 要求 0 < _required <= ownerCount <= MAX_OWNER_COUNT
    modifier validRequirement(uint ownerCount, uint _required) {
        if (   ownerCount > MAX_OWNER_COUNT
            || _required > ownerCount
            || _required == 0
            || ownerCount == 0)
            throw;
        _;
    }

    /**
     * @dev fallback 函数:接收原生币并记录事件。
     */
    function()
        payable
    {
        if (msg.value > 0)
            Deposit(msg.sender, msg.value);
    }

    /**
     * @dev 构造函数:设置初始管理员和确认门槛。
     * @param _owners 初始管理员列表,不能重复、不能含零地址
     * @param _required 执行所需确认数
     */
    function MultiSigWallet(address[] _owners, uint _required)
        public
        validRequirement(_owners.length, _required)
    {
        for (uint i=0; i<_owners.length; i++) {
            if (isOwner[_owners[i]] || _owners[i] == 0)
                throw;
            isOwner[_owners[i]] = true;
        }
        owners = _owners;
        required = _required;
    }

    /**
     * @dev 新增管理员。只能由钱包自身调用(走多签流程)。
     * @param owner 新管理员地址
     */
    function addOwner(address owner)
        public
        onlyWallet
        ownerDoesNotExist(owner)
        notNull(owner)
        validRequirement(owners.length + 1, required)
    {
        isOwner[owner] = true;
        owners.push(owner);
        OwnerAddition(owner);
    }

    /**
     * @dev 移除管理员。只能由钱包自身调用。
     *      用“末尾元素覆盖 + 长度减一”删除(不保序);门槛超过剩余人数时自动下调。
     * @param owner 要移除的管理员
     */
    function removeOwner(address owner)
        public
        onlyWallet
        ownerExists(owner)
    {
        isOwner[owner] = false;
        for (uint i=0; i<owners.length - 1; i++)
            if (owners[i] == owner) {
                owners[i] = owners[owners.length - 1];
                break;
            }
        owners.length -= 1;
        if (required > owners.length)
            changeRequirement(owners.length);
        OwnerRemoval(owner);
    }

    /**
     * @dev 替换管理员,人数不变。只能由钱包自身调用。
     * @param owner 被替换的管理员
     * @param newOwner 新管理员
     */
    function replaceOwner(address owner, address newOwner)
        public
        onlyWallet
        ownerExists(owner)
        ownerDoesNotExist(newOwner)
    {
        for (uint i=0; i<owners.length; i++)
            if (owners[i] == owner) {
                owners[i] = newOwner;
                break;
            }
        isOwner[owner] = false;
        isOwner[newOwner] = true;
        OwnerRemoval(owner);
        OwnerAddition(newOwner);
    }

    /**
     * @dev 修改确认门槛。只能由钱包自身调用。
     * @param _required 新的确认数
     */
    function changeRequirement(uint _required)
        public
        onlyWallet
        validRequirement(owners.length, _required)
    {
        required = _required;
        RequirementChange(_required);
    }

    /**
     * @dev 提交交易,并自动以提交人身份确认一次。
     * @param destination 调用目标地址
     * @param value 附带的原生币数量
     * @param data calldata
     * @return transactionId 新交易的 ID
     */
    function submitTransaction(address destination, uint value, bytes data)
        public
        returns (uint transactionId)
    {
        transactionId = addTransaction(destination, value, data);
        confirmTransaction(transactionId);
    }

    /**
     * @dev 管理员确认交易,随后自动尝试执行(达到门槛时真正执行)。
     * @param transactionId 交易 ID
     */
    function confirmTransaction(uint transactionId)
        public
        ownerExists(msg.sender)
        transactionExists(transactionId)
        notConfirmed(transactionId, msg.sender)
    {
        confirmations[transactionId][msg.sender] = true;
        Confirmation(msg.sender, transactionId);
        executeTransaction(transactionId);
    }

    /**
     * @dev 执行前撤回自己的确认。
     * @param transactionId 交易 ID
     */
    function revokeConfirmation(uint transactionId)
        public
        ownerExists(msg.sender)
        confirmed(transactionId, msg.sender)
        notExecuted(transactionId)
    {
        confirmations[transactionId][msg.sender] = false;
        Revocation(msg.sender, transactionId);
    }

    /**
     * @dev 执行已达到门槛的交易,任何人都可以调用。
     *      内部调用失败不会让外层交易回滚,只会发出 ExecutionFailure 并允许重试。
     * @param transactionId 交易 ID
     */
    function executeTransaction(uint transactionId)
        public
        notExecuted(transactionId)
    {
        if (isConfirmed(transactionId)) {
            Transaction tx = transactions[transactionId];
            // 先标记已执行再发起外部调用,防止目标合约重入导致重复执行
            tx.executed = true;
            if (tx.destination.call.value(tx.value)(tx.data))
                Execution(transactionId);
            else {
                ExecutionFailure(transactionId);
                tx.executed = false;
            }
        }
    }

    /**
     * @dev 交易确认数是否达到门槛。
     * @param transactionId 交易 ID
     * @return 达到返回 true;未达到时没有 return,返回默认值 false
     */
    function isConfirmed(uint transactionId)
        public
        constant
        returns (bool)
    {
        uint count = 0;
        for (uint i=0; i<owners.length; i++) {
            if (confirmations[transactionId][owners[i]])
                count += 1;
            if (count == required)
                return true;
        }
    }

    /**
     * @dev 创建交易记录(内部函数)。
     * @param destination 调用目标,不能为零地址
     * @param value 附带的原生币数量
     * @param data calldata
     * @return transactionId 新交易的 ID
     */
    function addTransaction(address destination, uint value, bytes data)
        internal
        notNull(destination)
        returns (uint transactionId)
    {
        transactionId = transactionCount;
        transactions[transactionId] = Transaction({
            destination: destination,
            value: value,
            data: data,
            executed: false
        });
        transactionCount += 1;
        Submission(transactionId);
    }

    /**
     * @dev 查询交易的确认数。
     * @param transactionId 交易 ID
     * @return count 确认数
     */
    function getConfirmationCount(uint transactionId)
        public
        constant
        returns (uint count)
    {
        for (uint i=0; i<owners.length; i++)
            if (confirmations[transactionId][owners[i]])
                count += 1;
    }

    /**
     * @dev 按状态统计交易数量。
     * @param pending 是否统计未执行的交易
     * @param executed 是否统计已执行的交易
     * @return count 符合条件的交易数
     */
    function getTransactionCount(bool pending, bool executed)
        public
        constant
        returns (uint count)
    {
        for (uint i=0; i<transactionCount; i++)
            if (   pending && !transactions[i].executed
                || executed && transactions[i].executed)
                count += 1;
    }

    /**
     * @dev 查询管理员列表。
     * @return 管理员地址数组
     */
    function getOwners()
        public
        constant
        returns (address[])
    {
        return owners;
    }

    /**
     * @dev 查询已确认某交易的管理员。
     * @param transactionId 交易 ID
     * @return _confirmations 已确认的管理员地址数组
     */
    function getConfirmations(uint transactionId)
        public
        constant
        returns (address[] _confirmations)
    {
        // 先按最大长度开临时数组,再拷贝到精确长度
        address[] memory confirmationsTemp = new address[](owners.length);
        uint count = 0;
        uint i;
        for (i=0; i<owners.length; i++)
            if (confirmations[transactionId][owners[i]]) {
                confirmationsTemp[count] = owners[i];
                count += 1;
            }
        _confirmations = new address[](count);
        for (i=0; i<count; i++)
            _confirmations[i] = confirmationsTemp[i];
    }

    /**
     * @dev 按状态筛选交易 ID,再截取 [from, to) 区间。
     *      to 超过筛选结果数量时,多出的位置会填 0。
     * @param from 起始下标(含)
     * @param to 结束下标(不含)
     * @param pending 是否包含未执行的交易
     * @param executed 是否包含已执行的交易
     * @return _transactionIds 交易 ID 数组
     */
    function getTransactionIds(uint from, uint to, bool pending, bool executed)
        public
        constant
        returns (uint[] _transactionIds)
    {
        uint[] memory transactionIdsTemp = new uint[](transactionCount);
        uint count = 0;
        uint i;
        for (i=0; i<transactionCount; i++)
            if (   pending && !transactions[i].executed
                || executed && transactions[i].executed)
            {
                transactionIdsTemp[count] = i;
                count += 1;
            }
        _transactionIds = new uint[](to - from);
        for (i=from; i<to; i++)
            _transactionIds[i - from] = transactionIdsTemp[i];
    }
}

方法一览(场景:管理员为 O、A、B,需要 2 票)

方法参数返回值说明
构造函数_owners 管理员列表,_required 所需票数—例:([O, A, B], 2)
submitTransactiondestination 目标地址,value 附带的 TRX,data 调用数据交易编号提交并自动投 1 票,第一笔交易的编号是 0
confirmTransaction交易编号无投票,票数够了就自动执行
revokeConfirmation交易编号无执行前撤回自己的票
executeTransaction交易编号无手动执行(票数够的情况下)
isConfirmed交易编号true / false票数是否已够
getConfirmationCount交易编号票数
getConfirmations交易编号投票人列表例:[O, A]
getOwners无管理员列表例:[O, A, B]
getTransactionCountpending 是否算未执行的,executed 是否算已执行的数量
getTransactionIds起止位置、筛选条件交易编号列表
addOwner / removeOwner / replaceOwner / changeRequirement地址或票数无增删管理员、改票数。必须走上面的投票流程,直接调用会失败

什么是“调用数据”:把“调用 issue(500000000000)”编码成一串十六进制数据,例如:

0xcc872b66000000000000000000000000000000000000000000000000000000746a528800
  └ 前 4 字节代表 issue 这个方法        └ 后面是参数 500000000000

可以用 ethers.js 的 Interface.encodeFunctionData 生成。

注意(实测踩过):投出最后一票时,要手动把手续费上限设高一点。如果钱包自动估算的资源不够,多签内部对 USDT 的调用会失败,但交易本身仍然显示“成功”,操作其实没有执行。执行后可以查看 transactions(编号) 的最后一个字段 executed 是否为 true 来确认。


10. Migrations.sol:部署工具的记录文件

它做什么:TronBox / Truffle 这类部署工具会自动用到它,用来记录“部署脚本已经执行到第几步”。和 USDT 的业务无关,学习时可以跳过。它出现在源码里,说明当年 USDT 很可能是用这类工具部署的。

pragma solidity ^0.4.4;
/* solhint-disable var-name-mixedcase */


/**
 * @title Migrations Truffle 部署记录
 * @dev 记录已执行到第几个部署脚本,与代币业务无关。
 */
contract Migrations {
    // 管理员
    address public owner;
    // 最后完成的迁移脚本编号
    uint public last_completed_migration;

    /**
     * @dev 仅 owner。用 if 而不是 require:非 owner 调用不会失败,只是什么也不做。
     */
    modifier restricted() {
        if (msg.sender == owner) _;
    }

    /**
     * @dev 构造函数,部署者成为 owner。
     */
    function Migrations() public {
        owner = msg.sender;
    }

    /**
     * @dev 记录最后完成的迁移编号。
     * @param completed 迁移编号
     */
    function setCompleted(uint completed) public restricted {
        last_completed_migration = completed;
    }

    /**
     * @dev 把迁移进度同步到新的 Migrations 合约。
     * @param newAddress 新 Migrations 合约地址(新合约的 owner 必须是本合约,否则调用不生效)
     */
    function upgrade(address newAddress) public restricted {
        Migrations upgraded = Migrations(newAddress);
        upgraded.setCompleted(last_completed_migration);
    }
}
方法参数返回值说明
setCompleted步骤编号无记录进度(非 owner 调用不会报错,但也没有效果)
upgrade新 Migrations 合约地址无把进度同步到新合约

动手:自己部署一份来练习

请在测试网练习,并使用自己起的代币名称。部署合约不等于发行了稳定币:真正的稳定币需要真实的储备资产,还要符合当地监管要求。

最简单的方式是用 TronIDE + TronLink 钱包:

  1. 安装 TronLink 浏览器插件钱包,切换到 Nile 测试网,在 Nile 水龙头领取免费的测试 TRX。
  2. 打开 TronIDE,新建以下 8 个文件,把上面对应的代码粘贴进去(多签和 Migrations 先不用): SafeMath.sol、Ownable.sol、Pausable.sol、BlackList.sol、BasicToken.sol、StandardToken.sol、StandardTokenWithFees.sol、TetherToken.sol
  3. 编译:编译器版本选 0.4.25(和链上 USDT 一致),编译 TetherToken.sol。出现的黄色警告是老语法导致的,可以忽略。
  4. 部署:环境选择 TronLink,合约选择 TetherToken,填写参数:
    _initialSupply: 10000000          // 10 枚
    _name:          "My Test Dollar"
    _symbol:        "MTD"
    _decimals:      6
    
    点击部署并在钱包里确认。TRON 上执行合约要消耗“能量”,不够时会燃烧 TRX,测试网的 TRX 是免费的。
  5. 玩一玩:调用 balanceOf(你的地址) 查余额,issue(1000000) 增发 1 枚,transfer 转给另一个地址,pause() 后再转账看看会发生什么。

TronIDE 的界面可能会随版本更新而变化,以实际页面为准。熟练之后可以改用 TronBox(命令行部署工具)和 TronWeb(JavaScript 库)。


新手容易踩的坑

  1. 数量要乘以 10^6:转 1 枚 USDT,参数要填 1000000,不是 1。
  2. transfer 返回 false 不代表失败:TRON 版 USDT 的老缺陷,判断转账是否成功要看交易状态和余额变化。
  3. 被拉黑的地址仍然能收钱:黑名单只拦转出。
  4. 多签执行要给足手续费:否则可能“交易成功但没有执行”。
  5. 名字不代表真假:只认合约地址。

补充:和以太坊版 USDT 有什么不同

以太坊上也有一个 USDT(地址 0xdAC17F958D2ee523a2206206994597C13D831ec7),部署时间更早。两者功能基本相同,几处主要区别如下:

对比项以太坊版TRON 版(本文)社区约定的 ERC20 标准(EIP-20)
源码组织所有合约写在一个文件里拆成 10 个文件,结构更清晰标准不做规定。现在的项目一般按功能拆分文件,并直接继承 OpenZeppelin 的实现
transfer 返回值没有返回值返回 bool,但永远是 false签名为 returns (bool success),用返回值表示是否成功;余额不足时应该直接回滚。调用方必须能处理返回 false 的情况
approve 修改额度必须先改成 0 才能改成别的值可以直接改允许直接改。标准建议由钱包等前端先改成 0 再改成新值,并明确说合约本身不应强制
increaseApproval / decreaseApproval没有有不属于标准,是非标准的扩展方法。OpenZeppelin 5.0 已移除同类的 increaseAllowance / decreaseAllowance
增发 / 销毁时的 Transfer 事件不触发会触发增发时应触发 from 为零地址的 Transfer 事件;销毁时触发 to 为零地址的事件是通行惯例,标准没有明确要求

标准原文出处(EIP-20):

  • transfer:“The function SHOULD throw if the message caller’s account balance does not have enough tokens to spend.”
  • 返回值:“Callers MUST handle false from returns (bool success). Callers MUST NOT assume that false is never returned!”
  • approve:“clients SHOULD make sure to create user interfaces in such a way that they set the allowance first to 0 … THOUGH The contract itself shouldn’t enforce it”
  • 增发事件:“A token contract which creates new tokens SHOULD trigger a Transfer event with the _from address set to 0x0”
  • 另外,name、symbol、decimals 在标准中都标为 OPTIONAL(可选)。

OpenZeppelin 的说法出自其 CHANGELOG 5.0.0(2023-10-05):“ERC20: Removed the non-standard increaseAllowance and decreaseAllowance functions.”

小知识:EIP 文档里的 MUST 表示“必须”,SHOULD 表示“应该(强烈建议,但不强制)”。

简单来说,TRON 版是以太坊版的“整理升级版”。

你可能会问:返回值和标准不一样,为什么还能部署成功?因为 ERC20 只是社区约定的接口规范,不是链的强制规则。区块链在部署合约时只检查代码能不能运行,不会检查它是否符合某个标准。所以只要有 transfer、balanceOf 这些方法,钱包和交易所就能正常识别和使用它。

真正受影响的是其他合约:如果你的合约按标准写成 require(usdt.transfer(...)),在以太坊上会因为拿不到返回值而报错,在 TRON 上会因为拿到 false 而报错。所以和 USDT 交互时,不要依赖它的返回值,要看交易是否成功、余额是否变化。