USDT 代币合约分析
一个代币合约最基本功能有增发币、销毁币、转账、批准、查询等,我们以以太坊网络上的 USDT 为例看一下代币合约的细节,合约地址:https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7。在 etherscan 浏览器上找到 Contract 可以看到合约代码,可以看到有好几个合约,需要找到主合约,点击 Outline 找到黄底加粗的就是主合约,如下图所示。
在 etherscan 浏览器有一个功能是可以显示合约之间的继承关系图,在合约代码页面点击 More Option -> Sol2Umi ,找到 TetherToken 主合约的图,如下图所示。
从图上可以看出,constructor 构造函数有 4 个参数,分别是总量、币的名称,币的符号名,币的小数精度。在 remix 上填上这 4 个参数即可布署合约
增发、销毁、更新合约
USDT合约有 4 个事件,分别是增发币、销毁币、升级合约,调整手续费,这 4 个事件分别会在 issue、redeem, deprecate、setParams 这 4 个函数中触发。
(1) 增发函数 issue,这个函数只能由 Owner 调用,只有一个参数 amount,用于指定需要增发的数量,从代码可以看出,铸币是给 Owner 的地址增发相应的币数。
1 2 3 4 5 6 7 8 9 |
function issue(uint amount) public onlyOwner { require(_totalSupply + amount > _totalSupply); require(balances[owner] + amount > balances[owner]); balances[owner] += amount; _totalSupply += amount; Issue(amount); } |
(2) 销毁函数 redeem,这个函数也是只能由 Owner 调用,也是只有一个参数 amount,用于指定需要销毁的数量,Owner 的地址减少相应的币数。
1 2 3 4 5 6 7 8 9 |
function redeem(uint amount) public onlyOwner { require(_totalSupply >= amount); require(balances[owner] >= amount); _totalSupply -= amount; balances[owner] -= amount; Redeem(amount); } |
(3) 更新合约函数 deprecate,这个函数的功能可以作废当前合约,转到一个新的合约,有一个参数 _upgradedAddress,这个是更新后的合约地址。可以看到代码里的全局变量 deprecate 置为 true,一旦 deprecated 为 true,这个合约的功能已经失效,会转向新的合约,这也相当于一个后门。
1 2 3 4 5 6 7 |
bool public deprecated; function deprecate(address _upgradedAddress) public onlyOwner { deprecated = true; upgradedAddress = _upgradedAddress; Deprecate(_upgradedAddress); } |
转账与黑名单
与转账相关的有下面这 5 个函数。transfer 用于转账,transferFrom 也是用于转账,它与前者的区别是 transfer 只能从调用者自己账户里转账到目标,而 transferFrom 可以从第三者的账户里转账到目标,当然这需要第三者事先调用 approve 函数先批准授权指定哪个地址能够有权限操纵多少币。allowance 函数是获取某个地址授权给另一个地址可操纵的币的数量。balanceOf 函数用于获取指定地址当前的余额。
1 2 3 4 5 6 |
function transfer(address _to, uint _value) public whenNotPaused function transferFrom(address _from, address _to, uint _value) public whenNotPaused function balanceOf(address who) public constant returns (uint) function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) function allowance(address _owner, address _spender) public constant returns (uint remaining) |
之前总听说 USDT 不安全,有后门可以冻结。看一下 transfer 函数,第一句就是判断调用者的地址不能在黑名单列表里,transfer From 也类似的判断。
1 2 3 4 5 6 7 8 9 |
function transfer(address _to, uint _value) public whenNotPaused { require(!isBlackListed[msg.sender]); if (deprecated) { return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value); } else { return super.transfer(_to, _value); } } |
黑名单列表有两个函数可以操作,一个是添加 addBlackList,一个是删除 removeBlackList,参数都是提供需要操作的地址。操作黑名单的函数只有 Owner 可以调用。
1 2 3 4 5 6 7 8 9 10 11 12 |
mapping (address => bool) public isBlackListed; function addBlackList (address _evilUser) public onlyOwner { isBlackListed[_evilUser] = true; AddedBlackList(_evilUser); } function removeBlackList (address _clearedUser) public onlyOwner { isBlackListed[_clearedUser] = false; RemovedBlackList(_clearedUser); } |
看样子 USDT 的项目方真的是可以冻结任一地址,那么历史上有没有被冻结的地址呢?可以看出 addBlackList 函数会触发一个事件 AddedBlackList,那只要在区块链浏览器里过滤出这个事件,就知道有没有地址被冻结过,在 geth 以太坊的客户端控制台使用下面的命令可以得到 AddedBlackList 事件签名的 keccak256 值。
1 2 3 |
web3.sha3("AddedBlackList(address)") "0x42e160154868087d6bfdc0ca23d96a1c1cfa32f1b72ba9ba27b69b98a0d819dc" |
得到事件签名值之后,在 etherscan 浏览器事件的搜索框输入0x42e160154868087d6bfdc0ca23d96a1c1cfa32f1b72ba9ba27b69b98a0d819dc 即可查询到 AddedBlackList 事件的调用 ,topic0 即是过滤条件,下面的的参数是“拉黑”的地址,默认显示是 hex 格式,可以改成 Addr,看得更清楚,如下图所示。
除了可以冻结黑名单的地址,还可以销毁黑名单地址的币。destroyBlackFunds 函数用于销毁黑名单地址的币,首先会判断传入的地址是否在黑名单列表,如果不在则不会执行,然后获取该地址币的数量,将该地址的币清零,并且把总币数量减掉原来该地址的币数。
1 2 3 4 5 6 7 8 |
function destroyBlackFunds (address _blackListedUser) public onlyOwner { require(isBlackListed[_blackListedUser]); uint dirtyFunds = balanceOf(_blackListedUser); balances[_blackListedUser] = 0; _totalSupply -= dirtyFunds; DestroyedBlackFunds(_blackListedUser, dirtyFunds); } |
补充几个常用的函数
最后补充几个在合约审计经常会遇到的函数,获取合约地址、获取合约所有者的地址、获取发送者的地址、获取合约的余额、获取合约所有者的余额、获取发送者的余额。
(1) 获取合约的地址,即是 this 的地址。
1 2 3 4 |
function getAddressOfContract() public view returns(address) { return address(this); } |
(2) 获取合约所有者的地址,owner 地址一般定义成全局变量,在构造函数的时候获取 msg.sender 调用者的地址即是所有者。
1 2 3 4 5 6 7 8 9 |
address owner; constructor() { owner = msg.sender; } function getAddressOfOwner() public view returns(address) { return owner; } |
(3) 获取发送者的地址,即是 msg.sender。
1 2 3 4 |
function getAddressOfSender() public view returns(address) { return msg.sender; } |
(4) 获取合约的余额,即 this 的 balance。
1 2 3 4 |
function getBalanceOfContract() public view returns(uint) { return address(this).balance; } |
(5) 获取合约所有者的余额,owner 的余额,在获取时可以判断一下调用者是否是合约的 owner,不是的话不让获取。
1 2 3 4 5 6 |
function getBalanceOfOwner() public view returns(uint){ if(msg.sender == owner) return owner.balance; else return 0; } |
(6) 获取发送者的余额,得到 mag.sender.balance 即可。
1 2 3 4 |
function getBalanceOfSender() public view returns(uint) { return msg.sender.balance; } |
转载请注明:exchen's blog » USDT 代币合约分析