diff --git a/Custodian.sol b/Custodian.sol deleted file mode 100644 index 52e03b8..0000000 --- a/Custodian.sol +++ /dev/null @@ -1,327 +0,0 @@ -pragma solidity ^0.4.21; - -/** @title A dual control contract. - * - * @notice A general purpose contract that implements dual control over - * co-operating contracts through a callback mechanism. - * - * @dev This contract implements dual control through a 2-of-N - * threshold multi-signature scheme. The contract recognizes a set of N signers, - * and will unlock requests with signatures from any distinct pair of them. - * This contract signals the unlocking through a co-operative callback - * scheme. - * This contract also provides time lock and revocation features. - * Requests made by a 'primary' account have a default time lock applied. - * All other request must pay a 1 ETH stake and have an extended time lock - * applied. - * A request that is completed will prevent all previous pending requests - * that share the same callback from being completed: this is the - * revocation feature. - * - * @author Gemini Trust Company, LLC - */ -contract Custodian { - - // TYPES - /** @dev The `Request` struct stores a pending unlocking. - * `callbackAddress` and `callbackSelector` are the data required to - * make a callback. The custodian completes the process by - * calling `callbackAddress.call(callbackSelector, lockId)`, which - * signals to the contract co-operating with the Custodian that - * the 2-of-N signatures have been provided and verified. - */ - struct Request { - bytes32 lockId; - bytes4 callbackSelector; // bytes4 and address can be packed into 1 word - address callbackAddress; - uint256 idx; - uint256 timestamp; - bool extended; - } - - // EVENTS - /// @dev Emitted by successful `requestUnlock` calls. - event Requested( - bytes32 _lockId, - address _callbackAddress, - bytes4 _callbackSelector, - uint256 _nonce, - address _whitelistedAddress, - bytes32 _requestMsgHash, - uint256 _timeLockExpiry - ); - - /// @dev Emitted by `completeUnlock` calls on requests in the time-locked state. - event TimeLocked( - uint256 _timeLockExpiry, - bytes32 _requestMsgHash - ); - - /// @dev Emitted by successful `completeUnlock` calls. - event Completed( - bytes32 _lockId, - bytes32 _requestMsgHash, - address _signer1, - address _signer2 - ); - - /// @dev Emitted by `completeUnlock` calls where the callback failed. - event Failed( - bytes32 _lockId, - bytes32 _requestMsgHash, - address _signer1, - address _signer2 - ); - - /// @dev Emitted by successful `extendRequestTimeLock` calls. - event TimeLockExtended( - uint256 _timeLockExpiry, - bytes32 _requestMsgHash - ); - - // MEMBERS - /** @dev The count of all requests. - * This value is used as a nonce, incorporated into the request hash. - */ - uint256 public requestCount; - - /// @dev The set of signers: signatures from two signers unlock a pending request. - mapping (address => bool) public signerSet; - - /// @dev The map of request hashes to pending requests. - mapping (bytes32 => Request) public requestMap; - - /// @dev The map of callback addresses to callback selectors to request indexes. - mapping (address => mapping (bytes4 => uint256)) public lastCompletedIdxs; - - /** @dev The default period of time (in seconds) to time-lock requests. - * All requests will be subject to this default time lock, and the duration - * is fixed at contract creation. - */ - uint256 public defaultTimeLock; - - /** @dev The extended period of time (in seconds) to time-lock requests. - * Requests not from the primary account are subject to this time lock. - * The primary account may also elect to extend the time lock on requests - * that originally received the default. - */ - uint256 public extendedTimeLock; - - /// @dev The primary account is the privileged account for making requests. - address public primary; - - // CONSTRUCTOR - function Custodian( - address[] _signers, - uint256 _defaultTimeLock, - uint256 _extendedTimeLock, - address _primary - ) - public - { - // check for at least two `_signers` - require(_signers.length >= 2); - - // validate time lock params - require(_defaultTimeLock <= _extendedTimeLock); - defaultTimeLock = _defaultTimeLock; - extendedTimeLock = _extendedTimeLock; - - primary = _primary; - - // explicitly initialize `requestCount` to zero - requestCount = 0; - // turn the array into a set - for (uint i = 0; i < _signers.length; i++) { - // no zero addresses or duplicates - require(_signers[i] != address(0) && !signerSet[_signers[i]]); - signerSet[_signers[i]] = true; - } - } - - // MODIFIERS - modifier onlyPrimary { - require(msg.sender == primary); - _; - } - - // METHODS - /** @notice Requests an unlocking with a lock identifier and a callback. - * - * @dev If called by an account other than the primary a 1 ETH stake - * must be paid. This is an anti-spam measure. As well as the callback - * and the lock identifier parameters a 'whitelisted address' is required - * for compatibility with existing signature schemes. - * - * @param _lockId The identifier of a pending request in a co-operating contract. - * @param _callbackAddress The address of a co-operating contract. - * @param _callbackSelector The function selector of a function within - * the co-operating contract at address `_callbackAddress`. - * @param _whitelistedAddress An address whitelisted in existing - * offline control protocols. - * - * @return requestMsgHash The hash of a request message to be signed. - */ - 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); - } - - /** @notice Completes a pending unlocking with two signatures. - * - * @dev Given a request message hash as two signatures of it from - * two distinct signers in the signer set, this function completes the - * unlocking of the pending request by executing the callback. - * - * @param _requestMsgHash The request message hash of a pending request. - * @param _recoveryByte1 The public key recovery byte (27 or 28) - * @param _ecdsaR1 The R component of an ECDSA signature (R, S) pair - * @param _ecdsaS1 The S component of an ECDSA signature (R, S) pair - * @param _recoveryByte2 The public key recovery byte (27 or 28) - * @param _ecdsaR2 The R component of an ECDSA signature (R, S) pair - * @param _ecdsaS2 The S component of an ECDSA signature (R, S) pair - * - * @return success True if the callback successfully executed. - */ - 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); - } - } - } - - /** @notice Reclaim the storage of a pending request that is uncompleteable. - * - * @dev If a pending request shares the callback (address and selector) of - * a later request has has been completed, then the request can no longer - * be completed. This function will reclaim the contract storage of the - * pending request. - * - * @param _requestMsgHash The request message hash of a pending request. - */ - function deleteUncompletableRequest(bytes32 _requestMsgHash) public { - Request storage request = requestMap[_requestMsgHash]; - - uint256 idx = request.idx; - - require(0 < idx && idx < lastCompletedIdxs[request.callbackAddress][request.callbackSelector]); - - delete requestMap[_requestMsgHash]; - } - - /** @notice Extend the time lock of a pending request. - * - * @dev Requests made by the primary account receive the default time lock. - * This function allows the primary account to apply the extended time lock - * to one its own requests. - * - * @param _requestMsgHash The request message hash of a pending request. - */ - function extendRequestTimeLock(bytes32 _requestMsgHash) public onlyPrimary { - Request storage request = requestMap[_requestMsgHash]; - - // reject ‘null’ results from the map lookup - // this can only be the case if an unknown `_requestMsgHash` is received - require(request.callbackAddress != address(0)); - - // `extendRequestTimeLock` must be idempotent - require(request.extended != true); - - // set the `extended` flag; note that this is never unset - request.extended = true; - - emit TimeLockExtended(request.timestamp + extendedTimeLock, _requestMsgHash); - } -} diff --git a/CustodianUpgradeable.sol b/CustodianUpgradeable.sol index 1a6dc1f..ac856d8 100644 --- a/CustodianUpgradeable.sol +++ b/CustodianUpgradeable.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; import "./LockRequestable.sol"; @@ -14,7 +16,7 @@ import "./LockRequestable.sol"; * * @author Gemini Trust Company, LLC */ -contract CustodianUpgradeable is LockRequestable { +abstract contract CustodianUpgradeable is LockRequestable { // TYPES /// @dev The struct type for pending custodian changes. @@ -30,11 +32,10 @@ contract CustodianUpgradeable is LockRequestable { mapping (bytes32 => CustodianChangeRequest) public custodianChangeReqs; // CONSTRUCTOR - function CustodianUpgradeable( + constructor( address _custodian ) LockRequestable() - public { custodian = _custodian; } @@ -91,7 +92,7 @@ contract CustodianUpgradeable is LockRequestable { // reject ‘null’ results from the map lookup // this can only be the case if an unknown `_lockId` is received - require(changeRequest.proposedNew != 0); + require(changeRequest.proposedNew != address(0)); return changeRequest.proposedNew; } diff --git a/CustodianUpgradeableV1dot1.sol b/CustodianUpgradeableV1dot1.sol new file mode 100644 index 0000000..f1e83aa --- /dev/null +++ b/CustodianUpgradeableV1dot1.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; + +import "./LockRequestableV1dot1.sol"; + +/** @title A contract to inherit upgradeable custodianship. + * + * @notice A contract that provides re-usable code for upgradeable + * custodianship. That custodian may be an account or another contract. + * + * @dev This contract is intended to be inherited by any contract + * requiring a custodian to control some aspect of its functionality. + * This contract provides the mechanism for that custodianship to be + * passed from one custodian to the next. + * + * @author Gemini Trust Company, LLC + */ +abstract contract CustodianUpgradeableV1dot1 is LockRequestableV1dot1 { + + // TYPES + /// @dev The struct type for pending custodian changes. + struct CustodianChangeRequest { + address proposedNew; + } + + // MEMBERS + /// @dev The address of the account or contract that acts as the custodian. + address public custodian; + + /// @dev The map of lock ids to pending custodian changes. + mapping (bytes32 => CustodianChangeRequest) public custodianChangeReqs; + + // CONSTRUCTOR + constructor( + address _custodian + ) + LockRequestableV1dot1() + { + custodian = _custodian; + } + + // MODIFIERS + modifier onlyCustodian { + require(msg.sender == custodian, "unauthorized"); + _; + } + + // PUBLIC FUNCTIONS + // (UPGRADE) + + /** @notice Requests a change of the custodian associated with this contract. + * + * @dev Returns a unique lock id associated with the request. + * Anyone can call this function, but confirming the request is authorized + * by the custodian. + * + * @param _proposedCustodian The address of the new custodian. + * @return lockId A unique identifier for this request. + */ + function requestCustodianChange(address _proposedCustodian) external returns (bytes32 lockId) { + require(_proposedCustodian != address(0), "zero address"); + + (bytes32 preLockId, uint256 lockRequestIdx) = generatePreLockId(); + lockId = keccak256( + abi.encodePacked( + preLockId, + this.requestCustodianChange.selector, + _proposedCustodian + ) + ); + + custodianChangeReqs[lockId] = CustodianChangeRequest({ + proposedNew: _proposedCustodian + }); + + emit CustodianChangeRequested(lockId, msg.sender, _proposedCustodian, lockRequestIdx); + } + + /** @notice Confirms a pending change of the custodian associated with this contract. + * + * @dev When called by the current custodian with a lock id associated with a + * pending custodian change, the `address custodian` member will be updated with the + * requested address. + * + * @param _lockId The identifier of a pending change request. + */ + function confirmCustodianChange(bytes32 _lockId) external onlyCustodian { + custodian = getCustodianChangeReq(_lockId); + + delete custodianChangeReqs[_lockId]; + + emit CustodianChangeConfirmed(_lockId, custodian); + } + + // PRIVATE FUNCTIONS + function getCustodianChangeReq(bytes32 _lockId) private view returns (address _proposedNew) { + CustodianChangeRequest storage changeRequest = custodianChangeReqs[_lockId]; + + // reject ‘null’ results from the map lookup + // this can only be the case if an unknown `_lockId` is received + require(changeRequest.proposedNew != address(0), "no such lockId"); + + return changeRequest.proposedNew; + } + + /// @dev Emitted by successful `requestCustodianChange` calls. + event CustodianChangeRequested( + bytes32 _lockId, + address _msgSender, + address _proposedCustodian, + uint256 _lockRequestIdx + ); + + /// @dev Emitted by successful `confirmCustodianChange` calls. + event CustodianChangeConfirmed(bytes32 _lockId, address _newCustodian); +} diff --git a/ERC20Impl.sol b/ERC20Impl.sol index 7dea00d..16f4a8b 100644 --- a/ERC20Impl.sol +++ b/ERC20Impl.sol @@ -1,6 +1,8 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; -import "./CustodianUpgradeable.sol"; +import "./CustodianUpgradeableV1dot1.sol"; import "./ERC20Proxy.sol"; import "./ERC20Store.sol"; @@ -20,7 +22,7 @@ import "./ERC20Store.sol"; * * @author Gemini Trust Company, LLC */ -contract ERC20Impl is CustodianUpgradeable { +contract ERC20Impl is CustodianUpgradeableV1dot1 { // TYPES /// @dev The struct type for pending increases to the token supply (print). @@ -31,60 +33,29 @@ contract ERC20Impl is CustodianUpgradeable { // MEMBERS /// @dev The reference to the proxy. - ERC20Proxy public erc20Proxy; + ERC20Proxy immutable public erc20Proxy; /// @dev The reference to the store. - ERC20Store public erc20Store; - - /// @dev The sole authorized caller of delegated transfer control ('sweeping'). - address public sweeper; - - /** @dev The static message to be signed by an external account that - * signifies their permission to forward their balance to any arbitrary - * address. This is used to consolidate the control of all accounts - * backed by a shared keychain into the control of a single key. - * Initialized as the concatenation of the address of this contract - * and the word "sweep". This concatenation is done to prevent a replay - * attack in a subsequent contract, where the sweep message could - * potentially be replayed to re-enable sweeping ability. - */ - bytes32 public sweepMsg; - - /** @dev The mapping that stores whether the address in question has - * enabled sweeping its contents to another account or not. - * If an address maps to "true", it has already enabled sweeping, - * and thus does not need to re-sign the `sweepMsg` to enact the sweep. - */ - mapping (address => bool) public sweptSet; + ERC20Store immutable public erc20Store; /// @dev The map of lock ids to pending token increases. mapping (bytes32 => PendingPrint) public pendingPrintMap; // CONSTRUCTOR - function ERC20Impl( + constructor( address _erc20Proxy, address _erc20Store, - address _custodian, - address _sweeper + address _custodian ) - CustodianUpgradeable(_custodian) - public + CustodianUpgradeableV1dot1(_custodian) { - require(_sweeper != 0); erc20Proxy = ERC20Proxy(_erc20Proxy); erc20Store = ERC20Store(_erc20Store); - - sweeper = _sweeper; - sweepMsg = keccak256(address(this), "sweep"); } // MODIFIERS modifier onlyProxy { - require(msg.sender == address(erc20Proxy)); - _; - } - modifier onlySweeper { - require(msg.sender == sweeper); + require(msg.sender == address(erc20Proxy), "unauthorized"); _; } @@ -104,11 +75,11 @@ contract ERC20Impl is CustodianUpgradeable { address _spender, uint256 _value ) - public + external onlyProxy returns (bool success) { - require(_spender != address(0)); // disallow unspendable approvals + require(_spender != address(0), "zero address"); // disallow unspendable approvals erc20Store.setAllowance(_sender, _spender, _value); erc20Proxy.emitApproval(_sender, _spender, _value); return true; @@ -129,19 +100,22 @@ contract ERC20Impl is CustodianUpgradeable { address _spender, uint256 _addedValue ) - public + external onlyProxy returns (bool success) { - require(_spender != address(0)); // disallow unspendable approvals + require(_spender != address(0), "zero address"); // disallow unspendable approvals uint256 currentAllowance = erc20Store.allowed(_sender, _spender); - uint256 newAllowance = currentAllowance + _addedValue; - require(newAllowance >= currentAllowance); + unchecked { + // unchecked `+` is safe as overflow check follows + uint256 newAllowance = currentAllowance + _addedValue; + require(newAllowance >= currentAllowance, "overflow"); - erc20Store.setAllowance(_sender, _spender, newAllowance); - erc20Proxy.emitApproval(_sender, _spender, newAllowance); - return true; + erc20Store.setAllowance(_sender, _spender, newAllowance); + erc20Proxy.emitApproval(_sender, _spender, newAllowance); + return true; + } } /** @notice Core logic of the `decreaseApproval` function. @@ -159,18 +133,22 @@ contract ERC20Impl is CustodianUpgradeable { address _spender, uint256 _subtractedValue ) - public + external onlyProxy returns (bool success) { - require(_spender != address(0)); // disallow unspendable approvals + require(_spender != address(0), "zero address"); // disallow unspendable approvals uint256 currentAllowance = erc20Store.allowed(_sender, _spender); - uint256 newAllowance = currentAllowance - _subtractedValue; - require(newAllowance <= currentAllowance); + unchecked { + // unchecked `-` is safe as overflow check follows + uint256 newAllowance = currentAllowance - _subtractedValue; + require(newAllowance <= currentAllowance, "overflow"); + + erc20Store.setAllowance(_sender, _spender, newAllowance); + erc20Proxy.emitApproval(_sender, _spender, newAllowance); + } - erc20Store.setAllowance(_sender, _spender, newAllowance); - erc20Proxy.emitApproval(_sender, _spender, newAllowance); return true; } @@ -188,17 +166,46 @@ contract ERC20Impl is CustodianUpgradeable { * * @return lockId A unique identifier for this request. */ - function requestPrint(address _receiver, uint256 _value) public returns (bytes32 lockId) { - require(_receiver != address(0)); - - lockId = generateLockId(); + function requestPrint(address _receiver, uint256 _value) external returns (bytes32 lockId) { + require(_receiver != address(0), "zero address"); + + (bytes32 preLockId, uint256 lockRequestIdx) = generatePreLockId(); + lockId = keccak256( + abi.encodePacked( + preLockId, + this.requestPrint.selector, + _receiver, + _value + ) + ); pendingPrintMap[lockId] = PendingPrint({ receiver: _receiver, value: _value }); - emit PrintingLocked(lockId, _receiver, _value); + emit PrintingLocked(lockId, _receiver, _value, lockRequestIdx); + } + + function _executePrint(address _receiver, uint256 _value) private returns (bool success) { + uint256 supply = erc20Store.totalSupply(); + + unchecked { + // unchecked `+` is safe as overflow check follows + uint256 newSupply = supply + _value; + if (newSupply >= supply) { + erc20Store.setTotalSupply(newSupply); + erc20Store.addBalance(_receiver, _value); + + erc20Proxy.emitTransfer(address(0), _receiver, _value); + return true; + } + } + } + + function executePrint(address _receiver, uint256 _value) external onlyCustodian { + require(_receiver != address(0), "zero address"); + _executePrint(_receiver, _value); } /** @notice Confirms a pending increase in the token supply. @@ -211,26 +218,38 @@ contract ERC20Impl is CustodianUpgradeable { * * @param _lockId The identifier of a pending print request. */ - function confirmPrint(bytes32 _lockId) public onlyCustodian { + function confirmPrint(bytes32 _lockId) external onlyCustodian { PendingPrint storage print = pendingPrintMap[_lockId]; // reject ‘null’ results from the map lookup // this can only be the case if an unknown `_lockId` is received address receiver = print.receiver; - require (receiver != address(0)); + require (receiver != address(0), "no such lockId"); uint256 value = print.value; delete pendingPrintMap[_lockId]; - uint256 supply = erc20Store.totalSupply(); - uint256 newSupply = supply + value; - if (newSupply >= supply) { - erc20Store.setTotalSupply(newSupply); - erc20Store.addBalance(receiver, value); + if (_executePrint(receiver, value)) { + emit PrintingConfirmed(_lockId, receiver, value); + } + } - emit PrintingConfirmed(_lockId, receiver, value); - erc20Proxy.emitTransfer(address(0), receiver, value); + function _executeBurn(address _owner, uint256 _value) private returns (bool success) { + uint256 balanceOfSender = erc20Store.balances(_owner); + require(_value <= balanceOfSender, "insufficient balance"); + + unchecked { + // unchecked `-` is safe as `_value <= balanceOfSender` + erc20Store.setBalance(_owner, balanceOfSender - _value); + // unchecked `-` is safe as the global invariants is that + // the sum of all balances equals the total supply, therefore + // as `_value <= balanceOfSender`, then `_value <= totalSupply` + erc20Store.setTotalSupply(erc20Store.totalSupply() - _value); } + + erc20Proxy.emitTransfer(_owner, address(0), _value); + + return true; } /** @notice Burns the specified value from the sender's balance. @@ -241,16 +260,19 @@ contract ERC20Impl is CustodianUpgradeable { * * @return success true if the burn succeeded. */ - function burn(uint256 _value) public returns (bool success) { - uint256 balanceOfSender = erc20Store.balances(msg.sender); - require(_value <= balanceOfSender); - - erc20Store.setBalance(msg.sender, balanceOfSender - _value); - erc20Store.setTotalSupply(erc20Store.totalSupply() - _value); - - erc20Proxy.emitTransfer(msg.sender, address(0), _value); + function burn(uint256 _value) external returns (bool success) { + return _executeBurn(msg.sender, _value); + } - return true; + /** @notice Burns the specified value from the owner's balance. + * + * @dev Owner's balance is subtracted by the amount the custodian wishes to burn. + * + * @param _owner The owner of the tokens. + * @param _value The amount to burn. + */ + function burn(address _owner, uint256 _value) external onlyCustodian { + _executeBurn(_owner, _value); } /** @notice A function for a sender to issue multiple transfers to multiple @@ -271,17 +293,17 @@ contract ERC20Impl is CustodianUpgradeable { * @param _values The values for each destination address. * @return success If transfers succeeded. */ - function batchTransfer(address[] _tos, uint256[] _values) public returns (bool success) { - require(_tos.length == _values.length); + function batchTransfer(address[] calldata _tos, uint256[] calldata _values) external returns (bool success) { + require(_tos.length == _values.length, "inconsistent length"); uint256 numTransfers = _tos.length; uint256 senderBalance = erc20Store.balances(msg.sender); for (uint256 i = 0; i < numTransfers; i++) { address to = _tos[i]; - require(to != address(0)); + require(to != address(0), "zero address"); uint256 v = _values[i]; - require(senderBalance >= v); + require(senderBalance >= v, "insufficient balance"); if (msg.sender != to) { senderBalance -= v; @@ -295,95 +317,6 @@ contract ERC20Impl is CustodianUpgradeable { return true; } - /** @notice Enables the delegation of transfer control for many - * accounts to the sweeper account, transferring any balances - * as well to the given destination. - * - * @dev An account delegates transfer control by signing the - * value of `sweepMsg`. The sweeper account is the only authorized - * caller of this function, so it must relay signatures on behalf - * of accounts that delegate transfer control to it. Enabling - * delegation is idempotent and permanent. If the account has a - * balance at the time of enabling delegation, its balance is - * also transfered to the given destination account `_to`. - * NOTE: transfers to the zero address are disallowed. - * - * @param _vs The array of recovery byte components of the ECDSA signatures. - * @param _rs The array of 'R' components of the ECDSA signatures. - * @param _ss The array of 'S' components of the ECDSA signatures. - * @param _to The destination for swept balances. - */ - function enableSweep(uint8[] _vs, bytes32[] _rs, bytes32[] _ss, address _to) public onlySweeper { - require(_to != address(0)); - require((_vs.length == _rs.length) && (_vs.length == _ss.length)); - - uint256 numSignatures = _vs.length; - uint256 sweptBalance = 0; - - for (uint256 i=0; i 0) { - sweptBalance += fromBalance; - - erc20Store.setBalance(from, 0); - - erc20Proxy.emitTransfer(from, _to, fromBalance); - } - } - } - - if (sweptBalance > 0) { - erc20Store.addBalance(_to, sweptBalance); - } - } - - /** @notice For accounts that have delegated, transfer control - * to the sweeper, this function transfers their balances to the given - * destination. - * - * @dev The sweeper account is the only authorized caller of - * this function. This function accepts an array of addresses to have their - * balances transferred for gas efficiency purposes. - * NOTE: any address for an account that has not been previously enabled - * will be ignored. - * NOTE: transfers to the zero address are disallowed. - * - * @param _froms The addresses to have their balances swept. - * @param _to The destination address of all these transfers. - */ - function replaySweep(address[] _froms, address _to) public onlySweeper { - require(_to != address(0)); - uint256 lenFroms = _froms.length; - uint256 sweptBalance = 0; - - for (uint256 i=0; i 0) { - sweptBalance += fromBalance; - - erc20Store.setBalance(from, 0); - - erc20Proxy.emitTransfer(from, _to, fromBalance); - } - } - } - - if (sweptBalance > 0) { - erc20Store.addBalance(_to, sweptBalance); - } - } - /** @notice Core logic of the ERC20 `transferFrom` function. * * @dev This function can only be called by the referenced proxy, @@ -400,22 +333,28 @@ contract ERC20Impl is CustodianUpgradeable { address _to, uint256 _value ) - public + external onlyProxy returns (bool success) { - require(_to != address(0)); // ensure burn is the cannonical transfer to 0x0 + require(_to != address(0), "zero address"); // ensure burn is the cannonical transfer to 0x0 uint256 balanceOfFrom = erc20Store.balances(_from); - require(_value <= balanceOfFrom); + require(_value <= balanceOfFrom, "insufficient balance"); uint256 senderAllowance = erc20Store.allowed(_from, _sender); - require(_value <= senderAllowance); + require(_value <= senderAllowance, "insufficient allowance"); - erc20Store.setBalance(_from, balanceOfFrom - _value); + unchecked { + // unchecked `-` is safe as `_value <= balanceOfFrom` + erc20Store.setBalance(_from, balanceOfFrom - _value); + } erc20Store.addBalance(_to, _value); - erc20Store.setAllowance(_from, _sender, senderAllowance - _value); + unchecked { + // unchecked `-` is safe as `_value <= senderAllowance` + erc20Store.setAllowance(_from, _sender, senderAllowance - _value); + } erc20Proxy.emitTransfer(_from, _to, _value); @@ -437,16 +376,19 @@ contract ERC20Impl is CustodianUpgradeable { address _to, uint256 _value ) - public + external onlyProxy returns (bool success) { - require(_to != address(0)); // ensure burn is the cannonical transfer to 0x0 + require(_to != address(0), "zero address"); // ensure burn is the cannonical transfer to 0x0 uint256 balanceOfSender = erc20Store.balances(_sender); - require(_value <= balanceOfSender); + require(_value <= balanceOfSender, "insufficient balance"); - erc20Store.setBalance(_sender, balanceOfSender - _value); + unchecked { + // unchecked `-` is safe as `_value <= balanceOfSender` + erc20Store.setBalance(_sender, balanceOfSender - _value); + } erc20Store.addBalance(_to, _value); erc20Proxy.emitTransfer(_sender, _to, _value); @@ -456,23 +398,23 @@ contract ERC20Impl is CustodianUpgradeable { // METHODS (ERC20 sub interface impl.) /// @notice Core logic of the ERC20 `totalSupply` function. - function totalSupply() public view returns (uint256) { + function totalSupply() external view returns (uint256) { return erc20Store.totalSupply(); } /// @notice Core logic of the ERC20 `balanceOf` function. - function balanceOf(address _owner) public view returns (uint256 balance) { + function balanceOf(address _owner) external view returns (uint256 balance) { return erc20Store.balances(_owner); } /// @notice Core logic of the ERC20 `allowance` function. - function allowance(address _owner, address _spender) public view returns (uint256 remaining) { + function allowance(address _owner, address _spender) external view returns (uint256 remaining) { return erc20Store.allowed(_owner, _spender); } // EVENTS /// @dev Emitted by successful `requestPrint` calls. - event PrintingLocked(bytes32 _lockId, address _receiver, uint256 _value); + event PrintingLocked(bytes32 _lockId, address _receiver, uint256 _value, uint256 _lockRequestIdx); /// @dev Emitted by successful `confirmPrint` calls. event PrintingConfirmed(bytes32 _lockId, address _receiver, uint256 _value); } diff --git a/ERC20ImplUpgradeable.sol b/ERC20ImplUpgradeable.sol index 88a090f..ee0b51b 100644 --- a/ERC20ImplUpgradeable.sol +++ b/ERC20ImplUpgradeable.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; import "./CustodianUpgradeable.sol"; import "./ERC20Impl.sol"; @@ -17,7 +19,7 @@ import "./ERC20Impl.sol"; * * @author Gemini Trust Company, LLC */ -contract ERC20ImplUpgradeable is CustodianUpgradeable { +abstract contract ERC20ImplUpgradeable is CustodianUpgradeable { // TYPES /// @dev The struct type for pending implementation changes. @@ -33,8 +35,8 @@ contract ERC20ImplUpgradeable is CustodianUpgradeable { mapping (bytes32 => ImplChangeRequest) public implChangeReqs; // CONSTRUCTOR - function ERC20ImplUpgradeable(address _custodian) CustodianUpgradeable(_custodian) public { - erc20Impl = ERC20Impl(0x0); + constructor(address _custodian) CustodianUpgradeable(_custodian) { + erc20Impl = ERC20Impl(address(0)); } // MODIFIERS diff --git a/ERC20Interface.sol b/ERC20Interface.sol index c58958d..d5a134c 100644 --- a/ERC20Interface.sol +++ b/ERC20Interface.sol @@ -1,6 +1,8 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; -contract ERC20Interface { +interface ERC20Interface { // METHODS // NOTE: @@ -17,22 +19,22 @@ contract ERC20Interface { // function decimals() public view returns (uint8); // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#totalsupply - function totalSupply() public view returns (uint256); + function totalSupply() external view returns (uint256); // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#balanceof - function balanceOf(address _owner) public view returns (uint256 balance); + function balanceOf(address _owner) external view returns (uint256 balance); // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#transfer - function transfer(address _to, uint256 _value) public returns (bool success); + function transfer(address _to, uint256 _value) external returns (bool success); // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#transferfrom - function transferFrom(address _from, address _to, uint256 _value) public returns (bool success); + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#approve - function approve(address _spender, uint256 _value) public returns (bool success); + function approve(address _spender, uint256 _value) external returns (bool success); // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#allowance - function allowance(address _owner, address _spender) public view returns (uint256 remaining); + function allowance(address _owner, address _spender) external view returns (uint256 remaining); // EVENTS // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#transfer-1 diff --git a/ERC20Proxy.sol b/ERC20Proxy.sol index a273986..53b8426 100644 --- a/ERC20Proxy.sol +++ b/ERC20Proxy.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; import "./ERC20Interface.sol"; import "./ERC20ImplUpgradeable.sol"; @@ -30,14 +32,13 @@ contract ERC20Proxy is ERC20Interface, ERC20ImplUpgradeable { uint8 public decimals; // CONSTRUCTOR - function ERC20Proxy( - string _name, - string _symbol, + constructor( + string memory _name, + string memory _symbol, uint8 _decimals, address _custodian ) ERC20ImplUpgradeable(_custodian) - public { name = _name; symbol = _symbol; @@ -50,7 +51,7 @@ contract ERC20Proxy is ERC20Interface, ERC20ImplUpgradeable { * * @return the total token supply. */ - function totalSupply() public view returns (uint256) { + function totalSupply() public override view returns (uint256) { return erc20Impl.totalSupply(); } @@ -59,7 +60,7 @@ contract ERC20Proxy is ERC20Interface, ERC20ImplUpgradeable { * * @return balance the balance of account with address `_owner`. */ - function balanceOf(address _owner) public view returns (uint256 balance) { + function balanceOf(address _owner) public override view returns (uint256 balance) { return erc20Impl.balanceOf(_owner); } @@ -76,7 +77,7 @@ contract ERC20Proxy is ERC20Interface, ERC20ImplUpgradeable { * * @return success true if transfer completes. */ - function transfer(address _to, uint256 _value) public returns (bool success) { + function transfer(address _to, uint256 _value) public override returns (bool success) { return erc20Impl.transferWithSender(msg.sender, _to, _value); } @@ -89,7 +90,7 @@ contract ERC20Proxy is ERC20Interface, ERC20ImplUpgradeable { * * @return success true if transfer completes. */ - function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { + function transferFrom(address _from, address _to, uint256 _value) public override returns (bool success) { return erc20Impl.transferFromWithSender(msg.sender, _from, _to, _value); } @@ -107,7 +108,7 @@ contract ERC20Proxy is ERC20Interface, ERC20ImplUpgradeable { * * @return success true if approval completes. */ - function approve(address _spender, uint256 _value) public returns (bool success) { + function approve(address _spender, uint256 _value) public override returns (bool success) { return erc20Impl.approveWithSender(msg.sender, _spender, _value); } @@ -143,7 +144,7 @@ contract ERC20Proxy is ERC20Interface, ERC20ImplUpgradeable { * * @return remaining the remaining allowance. */ - function allowance(address _owner, address _spender) public view returns (uint256 remaining) { + function allowance(address _owner, address _spender) public override view returns (uint256 remaining) { return erc20Impl.allowance(_owner, _spender); } } diff --git a/ERC20Store.sol b/ERC20Store.sol index cee97be..2aaded0 100644 --- a/ERC20Store.sol +++ b/ERC20Store.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; import "./ERC20ImplUpgradeable.sol"; @@ -28,7 +30,7 @@ contract ERC20Store is ERC20ImplUpgradeable { mapping (address => mapping (address => uint256)) public allowed; // CONSTRUCTOR - function ERC20Store(address _custodian) ERC20ImplUpgradeable(_custodian) public { + constructor(address _custodian) ERC20ImplUpgradeable(_custodian) { totalSupply = 0; } @@ -111,6 +113,10 @@ contract ERC20Store is ERC20ImplUpgradeable { public onlyImpl { - balances[_owner] = balances[_owner] + _balanceIncrease; + unchecked { + // unchecked `+` as caller is responsible + // see occurrences of `.addBalance` in `ERC20Impl` + balances[_owner] = balances[_owner] + _balanceIncrease; + } } } diff --git a/InitializerV1dot1.sol b/InitializerV1dot1.sol new file mode 100644 index 0000000..c854780 --- /dev/null +++ b/InitializerV1dot1.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; + +import "./ERC20Impl.sol"; + +contract InitializerV1dot1 { + ERC20Impl public immutable impl; + address public immutable printCustodian; + bool public initialized; + + constructor(address _impl, address _printCustodian) { + require(_impl != address(0), "zero impl"); + require(_printCustodian != address(0), "zero custodian"); + + impl = ERC20Impl(_impl); + printCustodian = _printCustodian; + } + + function initialize() external { + require(!initialized, "already initialized"); + initialized = true; + + // Pass custodianship of the pinned impl to the pinned print custodian. + impl.confirmCustodianChange(impl.requestCustodianChange(printCustodian)); + } +} diff --git a/LockRequestable.sol b/LockRequestable.sol index 06ffa17..1afcd47 100644 --- a/LockRequestable.sol +++ b/LockRequestable.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; /** @title A contract for generating unique identifiers * @@ -11,14 +13,14 @@ pragma solidity ^0.4.21; * * @author Gemini Trust Company, LLC */ -contract LockRequestable { +abstract contract LockRequestable { // MEMBERS /// @notice the count of all invocations of `generateLockId`. uint256 public lockRequestCount; // CONSTRUCTOR - function LockRequestable() public { + constructor() { lockRequestCount = 0; } @@ -33,9 +35,19 @@ contract LockRequestable { * following this scheme, and that future identifiers are * unpredictable. * - * @return a 32-byte unique identifier. + * @return lockId a 32-byte unique identifier. */ function generateLockId() internal returns (bytes32 lockId) { - return keccak256(block.blockhash(block.number - 1), address(this), ++lockRequestCount); + unchecked { + // unchecked `-` is safe as this can never be evaluated in the genesis (0) block + // unchecked `++` is safe as it is infeasible to exhaust 256-bit integer counter + return keccak256( + abi.encodePacked( + blockhash(block.number - 1), + address(this), + ++lockRequestCount + ) + ); + } } } diff --git a/LockRequestableV1dot1.sol b/LockRequestableV1dot1.sol new file mode 100644 index 0000000..f7034ee --- /dev/null +++ b/LockRequestableV1dot1.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; + +/** @title A contract for generating unique identifiers + * + * @notice A contract that provides a identifier generation scheme, + * guaranteeing uniqueness across all contracts that inherit from it, + * as well as unpredictability of future identifiers. + * + * @dev This contract is intended to be inherited by any contract that + * implements the callback software pattern for cooperative custodianship. + * + * @author Gemini Trust Company, LLC + */ +abstract contract LockRequestableV1dot1 { + + // MEMBERS + /// @notice the count of all invocations of `generatePreLockId`. + uint256 public lockRequestCount; + + // CONSTRUCTOR + constructor() { + lockRequestCount = 0; + } + + // FUNCTIONS + /** @notice Returns a fresh unique identifier. + * + * @dev the generation scheme uses three components. + * First, the blockhash of the previous block. + * Second, the deployed address. + * Third, the next value of the counter. + * This ensure that identifiers are unique across all contracts + * following this scheme, and that future identifiers are + * unpredictable. + * + * @return preLockId a 32-byte unique identifier. + * @return lockRequestIdx index of lock request + + */ + function generatePreLockId() internal returns (bytes32 preLockId, uint256 lockRequestIdx) { + unchecked { + // unchecked `-` is safe as this can never be evaluated in the genesis (0) block + // unchecked `++` is safe as it is infeasible to exhaust 256-bit integer counter + lockRequestIdx = ++lockRequestCount; + preLockId = keccak256( + abi.encodePacked( + blockhash(block.number - 1), + address(this), + lockRequestIdx + ) + ); + } + } +} diff --git a/PrintLimiter.sol b/PrintLimiter.sol index 42dde29..c48c3dd 100644 --- a/PrintLimiter.sol +++ b/PrintLimiter.sol @@ -1,7 +1,9 @@ -pragma solidity ^0.4.21; +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2020 Gemini Trust Company LLC. All Rights Reserved +pragma solidity ^0.8.0; import "./ERC20Impl.sol"; -import "./LockRequestable.sol"; +import "./LockRequestableV1dot1.sol"; /** @title A contact to govern hybrid control over increases to the token supply. * @@ -19,25 +21,25 @@ import "./LockRequestable.sol"; * * @author Gemini Trust Company, LLC */ -contract PrintLimiter is LockRequestable { +contract PrintLimiter is LockRequestableV1dot1 { // TYPES - /// @dev The struct type for pending ceiling raises. - struct PendingCeilingRaise { + /// @dev The struct type for ceiling/limit raises. + struct Raise { uint256 raiseBy; } // MEMBERS /// @dev The reference to the active token implementation. - ERC20Impl public erc20Impl; + ERC20Impl immutable public erc20Impl; /// @dev The address of the account or contract that acts as the custodian. - address public custodian; + address immutable public custodian; /** @dev The sole authorized caller of limited printing. * This account is also authorized to lower the supply ceiling. */ - address public limitedPrinter; + address immutable public limitedPrinter; /** @dev The maximum that the token supply can be increased to * through use of the limited printing feature. @@ -47,31 +49,42 @@ contract PrintLimiter is LockRequestable { */ uint256 public totalSupplyCeiling; + /** @dev The maximum that can be seized (burnt) through use of + * the limited burn feature. + * The value of the limit can only be increased by the custodian. + * The value of the limit can be decreased by the 'limited printer'. + */ + uint256 public seizureLimit; + /// @dev The map of lock ids to pending ceiling raises. - mapping (bytes32 => PendingCeilingRaise) public pendingRaiseMap; + mapping (bytes32 => Raise) public pendingRaiseMap; + + /// @dev The map of lock ids to pending seizure limit raises. + mapping (bytes32 => Raise) public pendingSeizureLimitRaiseMap; // CONSTRUCTOR - function PrintLimiter( + constructor( address _erc20Impl, address _custodian, address _limitedPrinter, - uint256 _initialCeiling + uint256 _initialCeiling, + uint256 _initialSeizureLimit ) - public { erc20Impl = ERC20Impl(_erc20Impl); custodian = _custodian; limitedPrinter = _limitedPrinter; totalSupplyCeiling = _initialCeiling; + seizureLimit = _initialSeizureLimit; } // MODIFIERS modifier onlyCustodian { - require(msg.sender == custodian); + require(msg.sender == custodian, "unauthorized"); _; } modifier onlyLimitedPrinter { - require(msg.sender == limitedPrinter); + require(msg.sender == limitedPrinter, "unauthorized"); _; } @@ -86,13 +99,45 @@ contract PrintLimiter is LockRequestable { * @param _value The number of tokens to add to the total supply and the * balance of the receiving address. */ - function limitedPrint(address _receiver, uint256 _value) public onlyLimitedPrinter { + function limitedPrint(address _receiver, uint256 _value) external onlyLimitedPrinter { uint256 totalSupply = erc20Impl.totalSupply(); - uint256 newTotalSupply = totalSupply + _value; - require(newTotalSupply >= totalSupply); - require(newTotalSupply <= totalSupplyCeiling); - erc20Impl.confirmPrint(erc20Impl.requestPrint(_receiver, _value)); + unchecked { + // unchecked `+` is safe as overflow check follow + uint256 newTotalSupply = totalSupply + _value; + require(newTotalSupply >= totalSupply, "overflow"); + + require(newTotalSupply <= totalSupplyCeiling, "ceiling exceeded"); + erc20Impl.executePrint(_receiver, _value); + } + } + + function limitedBurn(address _owner, uint256 _value) external onlyLimitedPrinter { + require(_value <= seizureLimit, "limit exceeded"); + + unchecked { + // unchecked `-` is safe as `_value <= seizureLimit` + seizureLimit = seizureLimit - _value; + } + erc20Impl.burn(_owner, _value); + } + + function requestRaise(mapping(bytes32 => Raise) storage _m, bytes4 _selector, uint256 _raiseBy) private returns (bytes32 lockId, uint256 lockRequestIdx) { + require(_raiseBy != 0, "zero"); + + (bytes32 preLockId, uint256 idx) = generatePreLockId(); + lockId = keccak256( + abi.encodePacked( + preLockId, + _selector, + _raiseBy + ) + ); + lockRequestIdx = idx; + + _m[lockId] = Raise({ + raiseBy: _raiseBy + }); } /** @notice Requests an increase to the supply ceiling. @@ -105,16 +150,37 @@ contract PrintLimiter is LockRequestable { * * @return lockId A unique identifier for this request. */ - function requestCeilingRaise(uint256 _raiseBy) public returns (bytes32 lockId) { - require(_raiseBy != 0); + function requestCeilingRaise(uint256 _raiseBy) external returns (bytes32 lockId) { + (bytes32 id, uint256 idx) = requestRaise(pendingRaiseMap, this.requestCeilingRaise.selector, _raiseBy); - lockId = generateLockId(); + emit CeilingRaiseLocked(id, _raiseBy, idx); + return id; + } - pendingRaiseMap[lockId] = PendingCeilingRaise({ - raiseBy: _raiseBy - }); + /** @notice Requests an increase to the seizure limit. + * + * @dev Returns a unique lock id associated with the request. + * Anyone can call this function, but confirming the request is authorized + * by the custodian. + * + * @param _raiseBy The amount by which to raise the seizure limit. + * + * @return lockId A unique identifier for this request. + */ + function requestSeizureLimitRaise(uint256 _raiseBy) external returns (bytes32 lockId) { + (bytes32 id, uint256 idx) = requestRaise(pendingSeizureLimitRaiseMap, this.requestSeizureLimitRaise.selector, _raiseBy); + + emit SeizureLimitRaiseLocked(id, _raiseBy, idx); + return id; + } - emit CeilingRaiseLocked(lockId, _raiseBy); + function getRequest(mapping(bytes32 => Raise) storage _m, bytes32 _lockId) private view returns (uint256 raiseBy) { + Raise storage r = _m[_lockId]; + + // reject ‘null’ results from the map lookup + // this can only be the case if an unknown `_lockId` is received + raiseBy = r.raiseBy; + require(raiseBy != 0, "no such lockId"); } /** @notice Confirms a pending increase in the token supply. @@ -127,22 +193,45 @@ contract PrintLimiter is LockRequestable { * * @param _lockId The identifier of a pending ceiling raise request. */ - function confirmCeilingRaise(bytes32 _lockId) public onlyCustodian { - PendingCeilingRaise storage pendingRaise = pendingRaiseMap[_lockId]; - - // copy locals of references to struct members - uint256 raiseBy = pendingRaise.raiseBy; - // accounts for a gibberish _lockId - require(raiseBy != 0); + function confirmCeilingRaise(bytes32 _lockId) external onlyCustodian { + uint256 raiseBy = getRequest(pendingRaiseMap, _lockId); delete pendingRaiseMap[_lockId]; - uint256 newCeiling = totalSupplyCeiling + raiseBy; - // overflow check - if (newCeiling >= totalSupplyCeiling) { - totalSupplyCeiling = newCeiling; + unchecked { + // unchecked `+` is safe as overflow check follows + uint256 newCeiling = totalSupplyCeiling + raiseBy; + if (newCeiling >= totalSupplyCeiling) { + totalSupplyCeiling = newCeiling; + + emit CeilingRaiseConfirmed(_lockId, raiseBy, newCeiling); + } + } + } + + /** @notice Confirms a pending increase in the seizure limit. + * + * @dev When called by the custodian with a lock id associated with a + * pending seizure limit increase, the amount requested is added to the + * current limit. + * NOTE: this function will not execute any raise that would overflow the + * seizure limit, but it will not revert either. + * + * @param _lockId The identifier of a pending seizure limit raise request. + */ + function confirmSeizureLimitRaise(bytes32 _lockId) external onlyCustodian { + uint256 raiseBy = getRequest(pendingSeizureLimitRaiseMap, _lockId); + + delete pendingSeizureLimitRaiseMap[_lockId]; + + unchecked { + // unchecked `+` is safe as overflow check follows + uint256 newSeizureLimit = seizureLimit + raiseBy; + if (newSeizureLimit >= seizureLimit) { + seizureLimit = newSeizureLimit; - emit CeilingRaiseConfirmed(_lockId, raiseBy, newCeiling); + emit SeizureLimitRaiseConfirmed(_lockId, raiseBy, newSeizureLimit); + } } } @@ -155,13 +244,36 @@ contract PrintLimiter is LockRequestable { * * @param _lowerBy The amount by which to lower the supply ceiling. */ - function lowerCeiling(uint256 _lowerBy) public onlyLimitedPrinter { - uint256 newCeiling = totalSupplyCeiling - _lowerBy; - // overflow check - require(newCeiling <= totalSupplyCeiling); - totalSupplyCeiling = newCeiling; + function lowerCeiling(uint256 _lowerBy) external onlyLimitedPrinter { + unchecked { + // unchecked `-` is safe as overflow check follows + uint256 newCeiling = totalSupplyCeiling - _lowerBy; + require(newCeiling <= totalSupplyCeiling, "overflow"); - emit CeilingLowered(_lowerBy, newCeiling); + totalSupplyCeiling = newCeiling; + + emit CeilingLowered(_lowerBy, newCeiling); + } + } + + /** @notice Lowers the seizure limit, further constraining the bound of + * what can be seized (burnt) by the limited printer. + * + * @dev The limited printer is the sole authorized caller of this function, + * so it is the only account that can elect to lower its limit to seize assets. + * + * @param _lowerBy The amount by which to lower the seizure limit. + */ + function lowerSeizureLimit(uint256 _lowerBy) external onlyLimitedPrinter { + unchecked { + // unchecked `-` is safe as overflow check follows + uint256 newSeizureLimit = seizureLimit - _lowerBy; + require(newSeizureLimit <= seizureLimit, "overflow"); + + seizureLimit = newSeizureLimit; + + emit SeizureLimitLowered(_lowerBy, newSeizureLimit); + } } /** @notice Pass-through control of print confirmation, allowing this @@ -177,7 +289,7 @@ contract PrintLimiter is LockRequestable { * @param _lockId The identifier of a pending print request in * the associated active token implementation. */ - function confirmPrintProxy(bytes32 _lockId) public onlyCustodian { + function confirmPrintProxy(bytes32 _lockId) external onlyCustodian { erc20Impl.confirmPrint(_lockId); } @@ -194,16 +306,24 @@ contract PrintLimiter is LockRequestable { * @param _lockId The identifier of a pending custodian change request * in the associated active token implementation. */ - function confirmCustodianChangeProxy(bytes32 _lockId) public onlyCustodian { + function confirmCustodianChangeProxy(bytes32 _lockId) external onlyCustodian { erc20Impl.confirmCustodianChange(_lockId); } // EVENTS /// @dev Emitted by successful `requestCeilingRaise` calls. - event CeilingRaiseLocked(bytes32 _lockId, uint256 _raiseBy); + event CeilingRaiseLocked(bytes32 _lockId, uint256 _raiseBy, uint256 _lockRequestIdx); /// @dev Emitted by successful `confirmCeilingRaise` calls. event CeilingRaiseConfirmed(bytes32 _lockId, uint256 _raiseBy, uint256 _newCeiling); + /// @dev Emitted by successful `requestSeizureLimitRaise` calls. + event SeizureLimitRaiseLocked(bytes32 _lockId, uint256 _raiseBy, uint256 _lockRequestIdx); + /// @dev Emitted by successful `confirmSeizureLimitRaise` calls. + event SeizureLimitRaiseConfirmed(bytes32 _lockId, uint256 _raiseBy, uint256 _newSeizureLimit); + /// @dev Emitted by successful `lowerCeiling` calls. event CeilingLowered(uint256 _lowerBy, uint256 _newCeiling); + + /// @dev Emitted by successful `lowerSeizureLimit` calls. + event SeizureLimitLowered(uint256 _lowerBy, uint256 _newSeizureLimit); } diff --git a/README.md b/README.md index aae5982..6067ce7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,25 @@ The Gemini dollar is an ERC20 compliant token backed by the U.S. dollar. +This repository contains the source of the Gemini dollar V1.1 token logic and supply controls, deployed to Ethereum +mainnet in October 2026. The original 2018 sources are in this repository's first commit. + +## Deployed contracts (Ethereum mainnet) + +| Contract | Address | Source | +| --- | --- | --- | +| ERC20Proxy (the token) | [`0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd`](https://etherscan.io/address/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd#code) | unchanged since 2018 | +| ERC20Store | [`0xc42B14e49744538e3C239f8ae48A1Eaaf35e68a0`](https://etherscan.io/address/0xc42B14e49744538e3C239f8ae48A1Eaaf35e68a0#code) | unchanged since 2018 | +| ERC20Impl V1.1 | [`0x483A1E7F16F46266400f1bAF7abdF445c8D140A2`](https://etherscan.io/address/0x483A1E7F16F46266400f1bAF7abdF445c8D140A2#code) | `ERC20Impl.sol` | +| PrintLimiter V1.1 | [`0x342c1B88c2108f159a4Ef37e322B4170b18C42f6`](https://etherscan.io/address/0x342c1B88c2108f159a4Ef37e322B4170b18C42f6#code) | `PrintLimiter.sol` | +| InitializerV1dot1 | [`0x9De27967a2aD2D4bF0cD51C7c112a69D79f0b6bF`](https://etherscan.io/address/0x9De27967a2aD2D4bF0cD51C7c112a69D79f0b6bF#code) | `InitializerV1dot1.sol` | + +The V1.1 contracts are verified on Etherscan with an exact match. The files here are byte-identical to the verified +sources, compiled with solc `0.8.30+commit.73712a01`, EVM version `shanghai`, optimizer enabled with 100,000 runs. + +`ERC20Proxy.sol` and `ERC20Store.sol` here are the source the V1.1 contracts compile against. The Proxy and Store +deployed in 2018 were not redeployed; their deployed source is in the first commit. + ## Contract Separation The Gemini dollar token is not a single smart contract; instead, it is separated into three cooperating contracts. @@ -22,13 +41,9 @@ This contract owns the storage of the Gemini dollar ledger. Calls to the update Each smart contract in the Gemini dollar system looks to a custodian for approval of important actions. This custodian may be another smart contract or a keyset (online or offline). -### CustodianUpgradeable +### CustodianUpgradeable and CustodianUpgradeableV1dot1 -This contract provides re-usable code for custodianship and provision for passing the custodian role. All of _ERC20Proxy_, _ERC20Impl_, and _ERC20Store_ inherit from this contract. This contract itself inherits from _LockRequestable_, which provides a cooperatively universally unique identifier generation scheme. - -### Custodian - -This contract provides general-purpose custodianship designed to be backed by an offline keyset. Approval requires dual control; time lock and revocation features are also provided. +These contracts provide re-usable code for custodianship and provision for passing the custodian role. _ERC20Proxy_ and _ERC20Store_ inherit from _CustodianUpgradeable_, and _ERC20Impl_ inherits from _CustodianUpgradeableV1dot1_. Each inherits from its _LockRequestable_ counterpart, which provides a cooperatively universally unique identifier generation scheme. ## Upgradeability @@ -38,13 +53,17 @@ The logical separation between the exposed ERC20 interface (_ERC20Proxy_), contr This contract provides re-usable code for replacing the active token logic: the active instance of _ERC20Impl_. It itself inherits from _CustodianUpgradeable_ as the upgrade process is controlled by the custodian. Both _ERC20Proxy_ and _ERC20Store_ inherit from this contract. +### InitializerV1dot1 + +A one-time helper used during the V1.1 deployment to hand custodianship of the new _ERC20Impl_ to the new _PrintLimiter_ in a single transaction. + ## Control of the Token Supply Management of the token supply demands the security of an offline approval mechanism yet the flexibility of an online approval mechanism. ### PrintLimiter -This contract provides custodianship of _ERC20Impl_ to govern increases to supply of Gemini dollar tokens, with an offline approval mechanism granting limited privileges to an online approval mechanism. +This contract provides custodianship of _ERC20Impl_ to govern changes to the supply of Gemini dollar tokens, with an offline approval mechanism granting limited privileges to an online approval mechanism. The online key may increase the supply up to a ceiling, and may burn tokens from an address up to a seizure limit that each burn reduces. Only the custodian can raise the ceiling or the seizure limit; the online key can lower either. The custodian retains the ability to print directly and to change the custodian of _ERC20Impl_. ## Copyright