USDT 多签钱包合约分析
之前看过 USDT 的合约,了解到它有一个 issue 函数用于增发,只有 owner 调用 issue 才可以增发,但仔细分析并没有那么简单,它的 owner 不是普通地址,是一个合约地址 ,这个合约是一个多签钱包。多签钱包主要特点是有多个 owner,执行增发操作时,并不需要满足所有 owner 的批准,在合约里指定 required 的数量,只要获得批准的 owner 数量满足 required 数量即可执行操作。
USDT 的多签钱包地址是 https://etherscan.io/address/0xc6cde7c39eb2f0f0095f41570af89efc2c1ea828#readContract, 目前是有 6 个 owner,增发币只要满足 3 个 owner 的批准即可。下面我们来具体分析。
具体代码
比如要执行一个增发币,主要会有三个事件,一开始是 Submission 提交一个交易,提交后不会马上执行,要等待 owner 们批准,批准事件是 Confirmation ,最终执行事件是 Execution。
1 2 3 4 |
event Confirmation(address indexed sender, uint indexed transactionId); event Submission(uint indexed transactionId); event Execution(uint indexed transactionId); |
提交交易需要构造 Transaction 结构体,由 4 个参数组成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
mapping (uint => Transaction) public transactions; //记录合约里的所有交易 mapping (uint => mapping (address => bool)) public confirmations; //所有确认的交易信息 mapping (address => bool) public isOwner; //判断某个地址是否是 owner address[] public owners; //所有的 owner 地址 uint public required; uint public transactionCount; struct Transaction { address destination; //发送的目标地址 uint value; //发送的数量 bytes data; //如果目标地址是合约,data的作用是用于执行合约里的函数 bool executed; 本次交易是否已经被执行 } |
接下来我们看一下构造函数,和合约名称相同的函数是构造函数,里面有两个参数,owners 是一个数组,_required 是多签的最低确认数,不管 owner 有多少个,只要满足 _required 的确认数量即可执行交易操作,比如 owner 有 6 个,required 是 3,那么只要有 3 个 owner 确认,交易即可发送。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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; } |
要执行一个操作,先调用 submitTransaction 函数,提供三个参数,destination 是 USDT 合约地址,value 是主币 ETH,data 是 USDT 合约里的函数和参数。addTransaction 函数构造 Transaction 结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function submitTransaction(address destination, uint value, bytes data) public returns (uint transactionId) { transactionId = addTransaction(destination, value, data); //构造交易 confirmTransaction(transactionId); } 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); } |
confirmTransaction 函数是用于批准操作,如果需要 3 个 owner 批准,那 3 个 owner 都调用该函数,提供需要操作的 transactionId 即可,将 transactionId 的状态置为 true。 revokeConfirmation 函数是拒绝操作,将 transactionId 的状态置为 false。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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); //执行 } function revokeConfirmation(uint transactionId) public ownerExists(msg.sender) confirmed(transactionId, msg.sender) notExecuted(transactionId) { confirmations[transactionId][msg.sender] = false; Revocation(msg.sender, transactionId); } |
executeTransaction 是用于最终执行交易操作,使用的是 call 指令调用 USDT 合约。不过其实在 confirmTransaction 函数中,如果是 owner 批准的数量足够,会调用 executeTransaction。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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)) //call 调用合约 Execution(transactionId); else { ExecutionFailure(transactionId); tx.executed = false; } } } |
分析 USDT 增发的过程
接下来我们来分析 USDT 的增发过程,加深对多签钱包合约的理解。2021年11月8日 Tether 在以太坊链上增发 10 亿USDT,交易哈希为:0xb313f71038cbb873919707eb81618bed35678cbb578bd214f630204cde00dfe1。我们来分析下这个增发的过程,通过 etherscan 浏览器查到这笔交易的详情,可以看到调用了 confirmTransaction 函数,参数十六进制 619,转为十进制是 1561,MethodID 为 0xc01a8c84 是函数的签名,如下图所示:
点击 Logs 标签栏,可以看到有 3 个事件,Conformation 事件是 owner 批准操作,批准的交易 ID 是 1561,Issue 事件从地址上可以看出是 USDT 合约的增发函数 issue 发出的事件,从参数上可以看出增发数量是 1000000000000000,USDT 的最小精度是 6,相当于 10 亿个 USDT,Execution 是执行交易操作,如下图所示:
回到多签钱包合约代码上,在 transctions 输入交易 ID 1561,可以看到交易结构体里的 4 个参数,参数的含义上面的代码有讲过,其中 data 是用于调用 USDT 合约参数,前面 0xcc872b66 是 USDT 合约里的增发函数 issue 的签名值,后面是 038d7ea4c68000 是增发的数据,转换为十进制是 1000000000000000,即 10 亿个,如下图所示。
增发函数的调用可以转为以下伪代码:
1 2 3 4 |
0xdAC17F958D2ee523a2206206994597C13D831ec7.call.value(0)( bytes4(keccak256("issue(100000000000000)")) ) |
关于函数签名
函数签名也称为选择器,将函数的名称和函数的参数打包在一起计算出哈希,然后取哈希前 4 个字节,下面我们编写两个合约,一个合约是 FunctionSelector 用于获取函数的签名,一个是 FunctionTest 用于打印日志,将 data 数据打印出来,和函数签名做对比,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
pragma solidity ^0.8.10; contract FunctionSelector{ function getSelector(string calldata _func) public pure returns (bytes4){ return bytes4(keccak256(bytes(_func));) } } contract FunctionTest { event Log(bytes data); function transfer(address to, uint amount) public{ emit Log(msg.data); //0xa9059cbb 签名 //0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 参数1 //000000000000000000000000000000000000000000000000000000000000000b 参数2 } function issue(uint amount) public{ emit Log(msg.data); //0xcc872b66 签名 //00000000000000000000000000000000000000000000000000038d7ea4c68000 参数1 } } |
布署 FunctionTest 合约,输入参数 100000000000000,执行后会在控制台打印日志,找到 data 数据,如下图所示:
再布署 FunctionSelector 合约,输入 issue(uint256),得出的签名值是 0xcc872b66,与 USDT 增发函数的签名值是一样的,如下图所示。由于 EVM 是通过函数签名去寻找函数的,所以一个合约里可以存在同名的函数,只要参数不一样签名就不同。
转载请注明:exchen's blog » USDT 多签钱包合约分析