适合谁读:刚开始学区块链开发、会一点 Solidity 的同学。
本文讲解 TRON 链上 USDT 合约
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t的源码(在 Tronscan 上可以查看)。源码一共 10 个.sol文件,我们一个文件一个文件地看:它做什么、代码长什么样(已换成中文注释)、有哪些方法、要注意什么。最后简单讲讲怎么自己部署一份来练手。
目录
- 开始之前:先看全貌
- 1. SafeMath.sol:安全的加减乘除
- 2. Ownable.sol:谁是管理员
- 3. Pausable.sol:紧急暂停开关
- 4. BlackList.sol:黑名单
- 5. BasicToken.sol:余额和转账
- 6. StandardToken.sol:授权别人帮你转账
- 7. StandardTokenWithFees.sol:手续费和代币信息
- 8. TetherToken.sol:最终的 USDT 合约
- 9. MultiSigWallet.sol:多签钱包
- 10. Migrations.sol:部署工具的记录文件
- 动手:自己部署一份来练习
- 新手容易踩的坑
- 补充:和以太坊版 USDT 有什么不同
开始之前:先看全貌
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 {} | 构造函数,部署时执行一次 |
constant | view | 只读函数,不修改链上数据 |
Transfer(...) | emit Transfer(...) | 触发事件(写入日志,供钱包和浏览器读取) |
throw | revert() | 报错,交易回滚 |
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;
}
}
方法一览
| 方法 | 参数 | 返回值 | 例子 |
|---|---|---|---|
mul | a, b | a * b | mul(3, 4) = 12 |
div | a, b | a / b(向下取整) | div(7, 2) = 3 |
sub | a, b | a - b | sub(5, 3) = 2;sub(3, 5) 交易失败 |
add | a, b | a + b | add(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) |
transferOwnership | newOwner 新管理员地址 | 无 | 只有当前管理员能调用,不能传零地址 |
小知识:修饰器(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 额度 | true | A 调用 approve(C, 300000000),额度直接改成 300 枚 |
allowance | _owner 授权人,_spender 被授权人 | 剩余额度 | allowance(A, C) = 500000000 |
transferFrom | _from 付款人,_to 收款人,_value 数量 | true | C 调用 transferFrom(A, B, 200000000):A 少 200 枚,B 多 200 枚,额度剩 300 枚 |
increaseApproval | _spender,_addedValue 增加量 | true | A 调用 increaseApproval(C, 100000000),额度变为 600 枚 |
decreaseApproval | _spender,_subtractedValue 减少量 | true | 减少量超过当前额度时,额度变为 0(不会报错) |
为什么有了 approve 还要 increase/decreaseApproval? approve 是直接覆盖旧额度的。假设 A 想把给 C 的额度从 100 改成 50,C 可以在改动生效前抢先用掉 100,改完后再用掉 50,一共拿走 150。按“增加 / 减少”来修改额度,就不会出现这个问题。
7. StandardTokenWithFees.sol:手续费和代币信息
它做什么:两件事。
- 存放代币信息:名称
name、符号symbol、小数位decimals、总量_totalSupply。 - 预留转账手续费功能:可以按比例收费,收到的费用归管理员。链上 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 |
setParams | newBasisPoints 费率,newMaxFee 单笔上限(整币) | 无 | 管理员调用 setParams(10, 5):费率 0.1%,单笔最多收 5 枚 |
transfer | _to,_value | false(缺陷,见下) | 设置 (10, 5) 后转 100 枚:对方收到 99.9 枚,管理员收到 0.1 枚 |
transferFrom | _from,_to,_value | true | 授权额度为最大值 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,_value | false(同第 7 节) | 任何人 | 未暂停、自己没被拉黑才能转 |
transferFrom | _from,_to,_value | true | 被授权人 | 未暂停、付款人没被拉黑才能转 |
approve / increaseApproval / decreaseApproval | 同第 6 节 | true | 任何人 | 暂停时不能调用 |
balanceOf / allowance / totalSupply | 同前 | 数值 | 任何人 | 查询,暂停时也能用 |
issue | amount 数量 | 无 | 管理员 | 增发,新币进入管理员账户。例:issue(500000000000) 增发 50 万枚 |
redeem | amount 数量 | 无 | 管理员 | 从管理员账户销毁,余额不足会失败 |
destroyBlackFunds | 被拉黑的地址 | 无 | 管理员 | 清空该地址余额,总量同步减少;地址必须先被拉黑 |
deprecate | 新合约地址 | 无 | 管理员 | 升级。之后所有调用都转发到新合约 |
oldBalanceOf | 地址 | 旧合约里的余额 | 任何人 | 只在升级后有意义,未升级时返回 0 |
看点:黑名单只拦“转出”。被拉黑的地址不能转出,但仍然可以收款(实测过)。所以发行方通常先拉黑,再用 destroyBlackFunds 清空余额。
9. MultiSigWallet.sol:多签钱包
它做什么:一个由多人共同管理的钱包合约。比如 3 个人管理、至少 2 人同意才能执行操作(称为 2/3 多签)。
为什么需要它?USDT 的管理员能增发、冻结,权力非常大。如果管理员只是一个私钥,私钥被盗就全完了。所以链上 USDT 的管理员其实是一个多签钱包(地址 TBPxhVAsuzoFnKyXtc1o2UySEydPHgATto,2/3 多签)。
使用流程:
- 管理员 1 调用
submitTransaction(目标合约, 0, 调用数据)提交一个操作,自动算作 1 票; - 管理员 2 调用
confirmTransaction(交易编号)确认,票数达到 2,自动执行; - 执行时,多签合约去调用目标合约(例如 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) |
submitTransaction | destination 目标地址,value 附带的 TRX,data 调用数据 | 交易编号 | 提交并自动投 1 票,第一笔交易的编号是 0 |
confirmTransaction | 交易编号 | 无 | 投票,票数够了就自动执行 |
revokeConfirmation | 交易编号 | 无 | 执行前撤回自己的票 |
executeTransaction | 交易编号 | 无 | 手动执行(票数够的情况下) |
isConfirmed | 交易编号 | true / false | 票数是否已够 |
getConfirmationCount | 交易编号 | 票数 | |
getConfirmations | 交易编号 | 投票人列表 | 例:[O, A] |
getOwners | 无 | 管理员列表 | 例:[O, A, B] |
getTransactionCount | pending 是否算未执行的,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 钱包:
- 安装 TronLink 浏览器插件钱包,切换到 Nile 测试网,在 Nile 水龙头领取免费的测试 TRX。
- 打开
TronIDE,新建以下 8 个文件,把上面对应的代码粘贴进去(多签和 Migrations 先不用):
SafeMath.sol、Ownable.sol、Pausable.sol、BlackList.sol、BasicToken.sol、StandardToken.sol、StandardTokenWithFees.sol、TetherToken.sol - 编译:编译器版本选
0.4.25(和链上 USDT 一致),编译TetherToken.sol。出现的黄色警告是老语法导致的,可以忽略。 - 部署:环境选择 TronLink,合约选择 TetherToken,填写参数:
点击部署并在钱包里确认。TRON 上执行合约要消耗“能量”,不够时会燃烧 TRX,测试网的 TRX 是免费的。_initialSupply: 10000000 // 10 枚 _name: "My Test Dollar" _symbol: "MTD" _decimals: 6 - 玩一玩:调用
balanceOf(你的地址)查余额,issue(1000000)增发 1 枚,transfer转给另一个地址,pause()后再转账看看会发生什么。
TronIDE 的界面可能会随版本更新而变化,以实际页面为准。熟练之后可以改用 TronBox(命令行部署工具)和 TronWeb(JavaScript 库)。
新手容易踩的坑
- 数量要乘以 10^6:转 1 枚 USDT,参数要填
1000000,不是1。 transfer返回false不代表失败:TRON 版 USDT 的老缺陷,判断转账是否成功要看交易状态和余额变化。- 被拉黑的地址仍然能收钱:黑名单只拦转出。
- 多签执行要给足手续费:否则可能“交易成功但没有执行”。
- 名字不代表真假:只认合约地址。
补充:和以太坊版 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 SHOULDthrowif the message caller’s account balance does not have enough tokens to spend.”- 返回值:“Callers MUST handle
falsefromreturns (bool success). Callers MUST NOT assume thatfalseis never returned!”approve:“clients SHOULD make sure to create user interfaces in such a way that they set the allowance first to0… THOUGH The contract itself shouldn’t enforce it”- 增发事件:“A token contract which creates new tokens SHOULD trigger a Transfer event with the
_fromaddress set to0x0”- 另外,
name、symbol、decimals在标准中都标为 OPTIONAL(可选)。OpenZeppelin 的说法出自其
CHANGELOG 5.0.0(2023-10-05):“
ERC20: Removed the non-standardincreaseAllowanceanddecreaseAllowancefunctions.”小知识:EIP 文档里的 MUST 表示“必须”,SHOULD 表示“应该(强烈建议,但不强制)”。
简单来说,TRON 版是以太坊版的“整理升级版”。
你可能会问:返回值和标准不一样,为什么还能部署成功?因为 ERC20 只是社区约定的接口规范,不是链的强制规则。区块链在部署合约时只检查代码能不能运行,不会检查它是否符合某个标准。所以只要有 transfer、balanceOf 这些方法,钱包和交易所就能正常识别和使用它。
真正受影响的是其他合约:如果你的合约按标准写成 require(usdt.transfer(...)),在以太坊上会因为拿不到返回值而报错,在 TRON 上会因为拿到 false 而报错。所以和 USDT 交互时,不要依赖它的返回值,要看交易是否成功、余额是否变化。