{"compilation":{"language":"Solidity","compiler":"solc","compilerVersion":"0.4.21+commit.dfe3193c","compilerSettings":{"optimizer":{"runs":200,"enabled":true}},"name":"Custodian","fullyQualifiedName":"Custodian.sol:Custodian"},"sources":{"Custodian.sol":{"content":"pragma solidity ^0.4.21;\r\n\r\n/** @title  A dual control contract.\r\n  *\r\n  * @notice  A general purpose contract that implements dual control over\r\n  * co-operating contracts through a callback mechanism.\r\n  *\r\n  * @dev  This contract implements dual control through a 2-of-N\r\n  * threshold multi-signature scheme. The contract recognizes a set of N signers,\r\n  * and will unlock requests with signatures from any distinct pair of them.\r\n  * This contract signals the unlocking through a co-operative callback\r\n  * scheme.\r\n  * This contract also provides time lock and revocation features.\r\n  * Requests made by a 'primary' account have a default time lock applied.\r\n  * All other request must pay a 1 ETH stake and have an extended time lock\r\n  * applied.\r\n  * A request that is completed will prevent all previous pending requests\r\n  * that share the same callback from being completed: this is the\r\n  * revocation feature.\r\n  *\r\n  * @author  Gemini Trust Company, LLC\r\n  */\r\ncontract Custodian {\r\n\r\n    // TYPES\r\n    /** @dev  The `Request` struct stores a pending unlocking.\r\n      * `callbackAddress` and `callbackSelector` are the data required to\r\n      * make a callback. The custodian completes the process by\r\n      * calling `callbackAddress.call(callbackSelector, lockId)`, which\r\n      * signals to the contract co-operating with the Custodian that\r\n      * the 2-of-N signatures have been provided and verified.\r\n      */\r\n    struct Request {\r\n        bytes32 lockId;\r\n        bytes4 callbackSelector; // bytes4 and address can be packed into 1 word\r\n        address callbackAddress;\r\n        uint256 idx;\r\n        uint256 timestamp;\r\n        bool extended;\r\n    }\r\n\r\n    // EVENTS\r\n    /// @dev  Emitted by successful `requestUnlock` calls.\r\n    event Requested(\r\n        bytes32 _lockId,\r\n        address _callbackAddress,\r\n        bytes4  _callbackSelector,\r\n        uint256 _nonce,\r\n        address _whitelistedAddress,\r\n        bytes32 _requestMsgHash,\r\n        uint256 _timeLockExpiry\r\n    );\r\n\r\n    /// @dev  Emitted by `completeUnlock` calls on requests in the time-locked state.\r\n    event TimeLocked(\r\n        uint256 _timeLockExpiry,\r\n        bytes32 _requestMsgHash\r\n    );\r\n\r\n    /// @dev  Emitted by successful `completeUnlock` calls.\r\n    event Completed(\r\n        bytes32 _lockId,\r\n        bytes32 _requestMsgHash,\r\n        address _signer1,\r\n        address _signer2\r\n    );\r\n\r\n    /// @dev  Emitted by `completeUnlock` calls where the callback failed.\r\n    event Failed(\r\n        bytes32 _lockId,\r\n        bytes32 _requestMsgHash,\r\n        address _signer1,\r\n        address _signer2\r\n    );\r\n\r\n    /// @dev  Emitted by successful `extendRequestTimeLock` calls.\r\n    event TimeLockExtended(\r\n        uint256 _timeLockExpiry,\r\n        bytes32 _requestMsgHash\r\n    );\r\n\r\n    // MEMBERS\r\n    /** @dev  The count of all requests.\r\n      * This value is used as a nonce, incorporated into the request hash.\r\n      */\r\n    uint256 public requestCount;\r\n\r\n    /// @dev  The set of signers: signatures from two signers unlock a pending request.\r\n    mapping (address => bool) public signerSet;\r\n\r\n    /// @dev  The map of request hashes to pending requests.\r\n    mapping (bytes32 => Request) public requestMap;\r\n\r\n    /// @dev  The map of callback addresses to callback selectors to request indexes.\r\n    mapping (address => mapping (bytes4 => uint256)) public lastCompletedIdxs;\r\n\r\n    /** @dev  The default period of time (in seconds) to time-lock requests.\r\n      * All requests will be subject to this default time lock, and the duration\r\n      * is fixed at contract creation.\r\n      */\r\n    uint256 public defaultTimeLock;\r\n\r\n    /** @dev  The extended period of time (in seconds) to time-lock requests.\r\n      * Requests not from the primary account are subject to this time lock.\r\n      * The primary account may also elect to extend the time lock on requests\r\n      * that originally received the default.\r\n      */\r\n    uint256 public extendedTimeLock;\r\n\r\n    /// @dev  The primary account is the privileged account for making requests.\r\n    address public primary;\r\n\r\n    // CONSTRUCTOR\r\n    function Custodian(\r\n        address[] _signers,\r\n        uint256 _defaultTimeLock,\r\n        uint256 _extendedTimeLock,\r\n        address _primary\r\n    )\r\n        public\r\n    {\r\n        // check for at least two `_signers`\r\n        require(_signers.length >= 2);\r\n\r\n        // validate time lock params\r\n        require(_defaultTimeLock <= _extendedTimeLock);\r\n        defaultTimeLock = _defaultTimeLock;\r\n        extendedTimeLock = _extendedTimeLock;\r\n\r\n        primary = _primary;\r\n\r\n        // explicitly initialize `requestCount` to zero\r\n        requestCount = 0;\r\n        // turn the array into a set\r\n        for (uint i = 0; i < _signers.length; i++) {\r\n            // no zero addresses or duplicates\r\n            require(_signers[i] != address(0) && !signerSet[_signers[i]]);\r\n            signerSet[_signers[i]] = true;\r\n        }\r\n    }\r\n\r\n    // MODIFIERS\r\n    modifier onlyPrimary {\r\n        require(msg.sender == primary);\r\n        _;\r\n    }\r\n\r\n    // METHODS\r\n    /** @notice  Requests an unlocking with a lock identifier and a callback.\r\n      *\r\n      * @dev  If called by an account other than the primary a 1 ETH stake\r\n      * must be paid. This is an anti-spam measure. As well as the callback\r\n      * and the lock identifier parameters a 'whitelisted address' is required\r\n      * for compatibility with existing signature schemes.\r\n      *\r\n      * @param  _lockId  The identifier of a pending request in a co-operating contract.\r\n      * @param  _callbackAddress  The address of a co-operating contract.\r\n      * @param  _callbackSelector  The function selector of a function within\r\n      * the co-operating contract at address `_callbackAddress`.\r\n      * @param  _whitelistedAddress  An address whitelisted in existing\r\n      * offline control protocols.\r\n      *\r\n      * @return  requestMsgHash  The hash of a request message to be signed.\r\n      */\r\n    function requestUnlock(\r\n        bytes32 _lockId,\r\n        address _callbackAddress,\r\n        bytes4 _callbackSelector,\r\n        address _whitelistedAddress\r\n    )\r\n        public\r\n        payable\r\n        returns (bytes32 requestMsgHash)\r\n    {\r\n        require(msg.sender == primary || msg.value >= 1 ether);\r\n\r\n        // disallow using a zero value for the callback address\r\n        require(_callbackAddress != address(0));\r\n\r\n        uint256 requestIdx = ++requestCount;\r\n        // compute a nonce value\r\n        // - the blockhash prevents prediction of future nonces\r\n        // - the address of this contract prevents conflicts with co-operating contracts using this scheme\r\n        // - the counter prevents conflicts arising from multiple txs within the same block\r\n        uint256 nonce = uint256(keccak256(block.blockhash(block.number - 1), address(this), requestIdx));\r\n\r\n        requestMsgHash = keccak256(nonce, _whitelistedAddress, uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF));\r\n\r\n        requestMap[requestMsgHash] = Request({\r\n            lockId: _lockId,\r\n            callbackSelector: _callbackSelector,\r\n            callbackAddress: _callbackAddress,\r\n            idx: requestIdx,\r\n            timestamp: block.timestamp,\r\n            extended: false\r\n        });\r\n\r\n        // compute the expiry time\r\n        uint256 timeLockExpiry = block.timestamp;\r\n        if (msg.sender == primary) {\r\n            timeLockExpiry += defaultTimeLock;\r\n        } else {\r\n            timeLockExpiry += extendedTimeLock;\r\n\r\n            // any sender that is not the creator will get the extended time lock\r\n            requestMap[requestMsgHash].extended = true;\r\n        }\r\n\r\n        emit Requested(_lockId, _callbackAddress, _callbackSelector, nonce, _whitelistedAddress, requestMsgHash, timeLockExpiry);\r\n    }\r\n\r\n    /** @notice  Completes a pending unlocking with two signatures.\r\n      *\r\n      * @dev  Given a request message hash as two signatures of it from\r\n      * two distinct signers in the signer set, this function completes the\r\n      * unlocking of the pending request by executing the callback.\r\n      *\r\n      * @param  _requestMsgHash  The request message hash of a pending request.\r\n      * @param  _recoveryByte1  The public key recovery byte (27 or 28)\r\n      * @param  _ecdsaR1  The R component of an ECDSA signature (R, S) pair\r\n      * @param  _ecdsaS1  The S component of an ECDSA signature (R, S) pair\r\n      * @param  _recoveryByte2  The public key recovery byte (27 or 28)\r\n      * @param  _ecdsaR2  The R component of an ECDSA signature (R, S) pair\r\n      * @param  _ecdsaS2  The S component of an ECDSA signature (R, S) pair\r\n      *\r\n      * @return  success  True if the callback successfully executed.\r\n      */\r\n    function completeUnlock(\r\n        bytes32 _requestMsgHash,\r\n        uint8 _recoveryByte1, bytes32 _ecdsaR1, bytes32 _ecdsaS1,\r\n        uint8 _recoveryByte2, bytes32 _ecdsaR2, bytes32 _ecdsaS2\r\n    )\r\n        public\r\n        returns (bool success)\r\n    {\r\n        Request storage request = requestMap[_requestMsgHash];\r\n\r\n        // copy storage to locals before `delete`\r\n        bytes32 lockId = request.lockId;\r\n        address callbackAddress = request.callbackAddress;\r\n        bytes4 callbackSelector = request.callbackSelector;\r\n\r\n        // failing case of the lookup if the callback address is zero\r\n        require(callbackAddress != address(0));\r\n\r\n        // reject confirms of earlier withdrawals buried under later confirmed withdrawals\r\n        require(request.idx > lastCompletedIdxs[callbackAddress][callbackSelector]);\r\n\r\n        address signer1 = ecrecover(_requestMsgHash, _recoveryByte1, _ecdsaR1, _ecdsaS1);\r\n        require(signerSet[signer1]);\r\n\r\n        address signer2 = ecrecover(_requestMsgHash, _recoveryByte2, _ecdsaR2, _ecdsaS2);\r\n        require(signerSet[signer2]);\r\n        require(signer1 != signer2);\r\n\r\n        if (request.extended && ((block.timestamp - request.timestamp) < extendedTimeLock)) {\r\n            emit TimeLocked(request.timestamp + extendedTimeLock, _requestMsgHash);\r\n            return false;\r\n        } else if ((block.timestamp - request.timestamp) < defaultTimeLock) {\r\n            emit TimeLocked(request.timestamp + defaultTimeLock, _requestMsgHash);\r\n            return false;\r\n        } else {\r\n            if (address(this).balance > 0) {\r\n                // reward sender with anti-spam payments\r\n                // ignore send success (assign to `success` but this will be overwritten)\r\n                success = msg.sender.send(address(this).balance);\r\n            }\r\n\r\n            // raise the waterline for the last completed unlocking\r\n            lastCompletedIdxs[callbackAddress][callbackSelector] = request.idx;\r\n            // and delete the request\r\n            delete requestMap[_requestMsgHash];\r\n\r\n            // invoke callback\r\n            success = callbackAddress.call(callbackSelector, lockId);\r\n\r\n            if (success) {\r\n                emit Completed(lockId, _requestMsgHash, signer1, signer2);\r\n            } else {\r\n                emit Failed(lockId, _requestMsgHash, signer1, signer2);\r\n            }\r\n        }\r\n    }\r\n\r\n    /** @notice  Reclaim the storage of a pending request that is uncompleteable.\r\n      *\r\n      * @dev  If a pending request shares the callback (address and selector) of\r\n      * a later request has has been completed, then the request can no longer\r\n      * be completed. This function will reclaim the contract storage of the\r\n      * pending request.\r\n      *\r\n      * @param  _requestMsgHash  The request message hash of a pending request.\r\n      */\r\n    function deleteUncompletableRequest(bytes32 _requestMsgHash) public {\r\n        Request storage request = requestMap[_requestMsgHash];\r\n\r\n        uint256 idx = request.idx;\r\n\r\n        require(0 < idx && idx < lastCompletedIdxs[request.callbackAddress][request.callbackSelector]);\r\n\r\n        delete requestMap[_requestMsgHash];\r\n    }\r\n\r\n    /** @notice  Extend the time lock of a pending request.\r\n      *\r\n      * @dev  Requests made by the primary account receive the default time lock.\r\n      * This function allows the primary account to apply the extended time lock\r\n      * to one its own requests.\r\n      *\r\n      * @param  _requestMsgHash  The request message hash of a pending request.\r\n      */\r\n    function extendRequestTimeLock(bytes32 _requestMsgHash) public onlyPrimary {\r\n        Request storage request = requestMap[_requestMsgHash];\r\n\r\n        // reject ‘null’ results from the map lookup\r\n        // this can only be the case if an unknown `_requestMsgHash` is received\r\n        require(request.callbackAddress != address(0));\r\n\r\n        // `extendRequestTimeLock` must be idempotent\r\n        require(request.extended != true);\r\n\r\n        // set the `extended` flag; note that this is never unset\r\n        request.extended = true;\r\n\r\n        emit TimeLockExtended(request.timestamp + extendedTimeLock, _requestMsgHash);\r\n    }\r\n}"}},"abi":[{"name":"signerSet","type":"function","inputs":[{"name":"","type":"address"}],"outputs":[{"name":"","type":"bool"}],"payable":false,"constant":true,"stateMutability":"view"},{"name":"completeUnlock","type":"function","inputs":[{"name":"_requestMsgHash","type":"bytes32"},{"name":"_recoveryByte1","type":"uint8"},{"name":"_ecdsaR1","type":"bytes32"},{"name":"_ecdsaS1","type":"bytes32"},{"name":"_recoveryByte2","type":"uint8"},{"name":"_ecdsaR2","type":"bytes32"},{"name":"_ecdsaS2","type":"bytes32"}],"outputs":[{"name":"success","type":"bool"}],"payable":false,"constant":false,"stateMutability":"nonpayable"},{"name":"requestCount","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256"}],"payable":false,"constant":true,"stateMutability":"view"},{"name":"defaultTimeLock","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256"}],"payable":false,"constant":true,"stateMutability":"view"},{"name":"extendRequestTimeLock","type":"function","inputs":[{"name":"_requestMsgHash","type":"bytes32"}],"outputs":[],"payable":false,"constant":false,"stateMutability":"nonpayable"},{"name":"requestUnlock","type":"function","inputs":[{"name":"_lockId","type":"bytes32"},{"name":"_callbackAddress","type":"address"},{"name":"_callbackSelector","type":"bytes4"},{"name":"_whitelistedAddress","type":"address"}],"outputs":[{"name":"requestMsgHash","type":"bytes32"}],"payable":true,"constant":false,"stateMutability":"payable"},{"name":"primary","type":"function","inputs":[],"outputs":[{"name":"","type":"address"}],"payable":false,"constant":true,"stateMutability":"view"},{"name":"extendedTimeLock","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256"}],"payable":false,"constant":true,"stateMutability":"view"},{"name":"requestMap","type":"function","inputs":[{"name":"","type":"bytes32"}],"outputs":[{"name":"lockId","type":"bytes32"},{"name":"callbackSelector","type":"bytes4"},{"name":"callbackAddress","type":"address"},{"name":"idx","type":"uint256"},{"name":"timestamp","type":"uint256"},{"name":"extended","type":"bool"}],"payable":false,"constant":true,"stateMutability":"view"},{"name":"lastCompletedIdxs","type":"function","inputs":[{"name":"","type":"address"},{"name":"","type":"bytes4"}],"outputs":[{"name":"","type":"uint256"}],"payable":false,"constant":true,"stateMutability":"view"},{"name":"deleteUncompletableRequest","type":"function","inputs":[{"name":"_requestMsgHash","type":"bytes32"}],"outputs":[],"payable":false,"constant":false,"stateMutability":"nonpayable"},{"type":"constructor","inputs":[{"name":"_signers","type":"address[]"},{"name":"_defaultTimeLock","type":"uint256"},{"name":"_extendedTimeLock","type":"uint256"},{"name":"_primary","type":"address"}],"payable":false,"stateMutability":"nonpayable"},{"name":"Requested","type":"event","inputs":[{"name":"_lockId","type":"bytes32","indexed":false},{"name":"_callbackAddress","type":"address","indexed":false},{"name":"_callbackSelector","type":"bytes4","indexed":false},{"name":"_nonce","type":"uint256","indexed":false},{"name":"_whitelistedAddress","type":"address","indexed":false},{"name":"_requestMsgHash","type":"bytes32","indexed":false},{"name":"_timeLockExpiry","type":"uint256","indexed":false}],"anonymous":false},{"name":"TimeLocked","type":"event","inputs":[{"name":"_timeLockExpiry","type":"uint256","indexed":false},{"name":"_requestMsgHash","type":"bytes32","indexed":false}],"anonymous":false},{"name":"Completed","type":"event","inputs":[{"name":"_lockId","type":"bytes32","indexed":false},{"name":"_requestMsgHash","type":"bytes32","indexed":false},{"name":"_signer1","type":"address","indexed":false},{"name":"_signer2","type":"address","indexed":false}],"anonymous":false},{"name":"Failed","type":"event","inputs":[{"name":"_lockId","type":"bytes32","indexed":false},{"name":"_requestMsgHash","type":"bytes32","indexed":false},{"name":"_signer1","type":"address","indexed":false},{"name":"_signer2","type":"address","indexed":false}],"anonymous":false},{"name":"TimeLockExtended","type":"event","inputs":[{"name":"_timeLockExpiry","type":"uint256","indexed":false},{"name":"_requestMsgHash","type":"bytes32","indexed":false}],"anonymous":false}],"matchId":"6482849","creationMatch":"match","runtimeMatch":"match","verifiedAt":"2025-06-21T09:08:14Z","match":"match","chainId":"1","address":"0x1789CcA7430aAcbDB7c89F9B5695A9C06E4764Eb"}