前面分析过GUSD的一些功能和实现,其中还有一个重要部分就是Custodian合约,这个合约实现了多重签名机制,并且GUSD的其它一些安全特色,如时间锁定、操作取消也是通过这个合约来实现的。
Custodian合约概要
- 实现了2/N的签名机制,就是N个签名者只要有两个签名就可以通过回调机制调用GUSD合约的方法,如修改Custodian地址、增加发行量等;
- 合约提供了时间锁机制,主帐号有一个缺省的时间锁,其它请求方支持1ETH同时有一个扩展的时间锁;
- 合约提供了回退取消机制,一个已经完成的请求会阻止那些共享同一回调的正在进行的请求。
数据结构
Request
struct Request {
bytes32 lockId;
bytes4 callbackSelector; // bytes4 and address can be packed into 1 word
address callbackAddress;
uint256 idx;
uint256 timestamp;
bool extended;
}
lockId 请求的lockId号,例如gusd合约方法requestCustodianChange()返回的lockId
callbackSelector和callbackAddress,确定调用的合约和合约中函数,调用方式:callbackAddress.call(callbackSelector, lockId)
构造函数
function Custodian(
address[] _signers,
uint256 _defaultTimeLock,
uint256 _extendedTimeLock,
address _primary
)
创建合约时需要传入签名地址列表、缺省锁定时间、扩展锁定时间、主地址。
requestUnlock
function requestUnlock(
bytes32 _lockId,
address _callbackAddress,
bytes4 _callbackSelector,
address _whitelistedAddress
)
public
payable
returns (bytes32 requestMsgHash)
{
require(msg.sender == primary || msg.value >= 1 ether);
// disallow using a zero value for the callback address
require(_callbackAddress != address(0));
uint256 requestIdx = ++requestCount;
// compute a nonce value
// - the blockhash prevents prediction of future nonces
// - the address of this contract prevents conflicts with co-operating contracts using this scheme
// - the counter prevents conflicts arising from multiple txs within the same block
uint256 nonce = uint256(keccak256(block.blockhash(block.number - 1), address(this), requestIdx));
requestMsgHash = keccak256(nonce, _whitelistedAddress, uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF));
requestMap[requestMsgHash] = Request({
lockId: _lockId,
callbackSelector: _callbackSelector,
callbackAddress: _callbackAddress,
idx: requestIdx,
timestamp: block.timestamp,
extended: false
});
// compute the expiry time
uint256 timeLockExpiry = block.timestamp;
if (msg.sender == primary) {
timeLockExpiry += defaultTimeLock;
} else {
timeLockExpiry += extendedTimeLock;
// any sender that is not the creator will get the extended time lock
requestMap[requestMsgHash].extended = true;
}
emit Requested(_lockId, _callbackAddress, _callbackSelector, nonce, _whitelistedAddress, requestMsgHash, timeLockExpiry);
}
这是custodian合约中一个主要函数,发送一个解锁的请求,类似于bitshares中发一个提案,只有有了提案后才能对提案进行签名。
第一行代码有意思,就是 require(msg.sender == primary || msg.value >= 1 ether); 要不是主帐户要不就支付1ETH可以发送这个请求,既有主帐户的安全控制,也可以灵活使用其它帐户,需要1ETH可以阻止恶意请求。
在Request中还有三个参数没有说明,在这意思也明了了:
idx: 请求次数,就是++requestCount
timestamp:块时间
extended:如果是主帐户操作为false,否则为true
而nonce值和_whitelistedAddress用来生成map的hash键,同时生成的这个requestMsgHash很重要,在以后的签名函数(completeUnlock)中都需要使用,可以提供这个数据给签名者进行离线签名。
completeUnlock
function completeUnlock(
bytes32 _requestMsgHash,
uint8 _recoveryByte1, bytes32 _ecdsaR1, bytes32 _ecdsaS1,
uint8 _recoveryByte2, bytes32 _ecdsaR2, bytes32 _ecdsaS2
)
public
returns (bool success)
{
Request storage request = requestMap[_requestMsgHash];
// copy storage to locals before `delete`
bytes32 lockId = request.lockId;
address callbackAddress = request.callbackAddress;
bytes4 callbackSelector = request.callbackSelector;
// failing case of the lookup if the callback address is zero
require(callbackAddress != address(0));
// reject confirms of earlier withdrawals buried under later confirmed withdrawals
require(request.idx > lastCompletedIdxs[callbackAddress][callbackSelector]);
address signer1 = ecrecover(_requestMsgHash, _recoveryByte1, _ecdsaR1, _ecdsaS1);
require(signerSet[signer1]);
address signer2 = ecrecover(_requestMsgHash, _recoveryByte2, _ecdsaR2, _ecdsaS2);
require(signerSet[signer2]);
require(signer1 != signer2);
if (request.extended && ((block.timestamp - request.timestamp) < extendedTimeLock)) {
emit TimeLocked(request.timestamp + extendedTimeLock, _requestMsgHash);
return false;
} else if ((block.timestamp - request.timestamp) < defaultTimeLock) {
emit TimeLocked(request.timestamp + defaultTimeLock, _requestMsgHash);
return false;
} else {
if (address(this).balance > 0) {
// reward sender with anti-spam payments
// ignore send success (assign to `success` but this will be overwritten)
success = msg.sender.send(address(this).balance);
}
// raise the waterline for the last completed unlocking
lastCompletedIdxs[callbackAddress][callbackSelector] = request.idx;
// and delete the request
delete requestMap[_requestMsgHash];
// invoke callback
success = callbackAddress.call(callbackSelector, lockId);
if (success) {
emit Completed(lockId, _requestMsgHash, signer1, signer2);
} else {
emit Failed(lockId, _requestMsgHash, signer1, signer2);
}
}
}
completeUnlock函数用来对请求进行签名,参数包括requestUnlock()函数中的requestMsgHash,还有ECDSA签名数据(ECDSA签名验证单独再分析)。
在Custodian合约概要中说过,合约提供了回退取消机制,就如下两句代码:
require(request.idx > lastCompletedIdxs[callbackAddress][callbackSelector]);
...
lastCompletedIdxs[callbackAddress][callbackSelector] = request.idx;
只要签名通过后,request.idx就会记录下来,调用同样合约同样方法的请求在它之前的request.idx就会失效,这样可以防止更新Custodian之类操作前面的请求替换已经通过的请求。话说bitshares就碰到过好几次更新数据后发现数据又恢复原状了,后来才找到原因,原来是有多个提案修改帐号数据,但是提案更新不是增量更新而是覆盖更新,所以修改过的数据又被覆盖了。
合约提供了时间锁机制,也是在这个函数中做的控制,没有达到时间的请求不会被执行。
如果签名都通过,就会调用 callbackAddress.call(callbackSelector, lockId) 执行其它合约中的方法。
deleteUncompletableRequest
这个函数删除不可能完成的请求,就是前面说的idx小于已签名的idx的请求。
extendRequestTimeLock
这个函数会把主帐号的请求时间锁设成与扩展时间锁一样,有什么作用呢?防止过早被签名者确认?
Custodian部署
PrintLimiter
PrintLimiter的Custodian地址:0x1789cca7430aacbdb7c89f9b5695a9c06e4764eb
看看PrintLimiter的Custodian数据:
defaultTimeLock: 3600(1小时)
extendedTimeLock: 86400(1天)
requestCount: 0
也就是这个合约创建后截止2018年11月5日,还没有调用过增加PrintLimiter天花板的方法。
ERC20Proxy
ERC20Proxy的Custodian地址:0x9a7b5f6e453d0cda978163cb4a9a88367250a52d
defaultTimeLock: 172800(2天)
extendedTimeLock: 604800(7天)
requestCount: 4
这个合约的解锁方法是调用过4次,看看调用哪些方法。
从etherscan可以看到4次调用的都是如下方法:
Function: requestUnlock(bytes32 _lockId, address _callbackAddress, bytes4 _callbackSelector, address _whitelistedAddress)
看看具体的一个交易:
056fd409e1d7a124bd7017459dfea2f387b6d5cd这个就是gusd合约地址,8181b029就是其中一个方法,这个是什么方法也是可以分析出来的。
还有交易调用的是0xc42B14e49744538e3C239f8ae48A1Eaaf35e68a0,这个是ERC20Store合约地址。
调用者地址0xd24400ae8BfEBb18cA49Be86258a3C749cf46853,也是调用参数中白名单地址,也是合约的primary地址,在etherscan看是Gemini_1。
结论
我们对GUSD的多重签名进行了比较详细的剖析,从Custodian合约中了解了以太坊进行多重签名的方法,可以通过多重签名保证合约方法调用的安全。
同样以太坊的多重签名地址也是用合约实现的,有时间再去看看以太坊的多重签名实现。
Hello! Your post has been resteemed and upvoted by @ilovecoding because we love coding! Keep up good work! Consider upvoting this comment to support the @ilovecoding and increase your future rewards! ^_^ Steem On!
Reply !stop to disable the comment. Thanks!