![](https://miro.medium.com/max/1060/1*DZopze1Xtir7mZHraVNZ_w.png) ### Smart Contract Deep Dive By Jefferson Tang --- ![](https://i.ibb.co/7jqWcHn/Screen-Shot-2022-02-21-at-5-03-31-pm.png) --- ### What is a multi-signature wallet? --- ![](https://coincodecap.com/wp-content/uploads/2021/03/image-18.jpeg.webp) --- ![](https://miro.medium.com/max/1400/1*OymI4OIm23KYTXwrUwJFeA.png) --- ### Why a multi-signature wallet? - Single-signature wallet vulnerable if single private key compromised - For individual - require multiple devices to sign transactions. Can lose one private key. - For teams - require multiple team members to sign every transaction. Helps to prevent unauthorized access to the company/DAO wallet. --- A Gnosis Safe account takes just 60 seconds to set up https://twitter.com/econoar/status/1194731123340763136 --- ### Gnosis have performed formal verification of their contracts - Time-intensive method to mathematically verify that a contract works as intended - https://blog.gnosis.pm/formal-verification-a-journey-deep-into-the-gnosis-safe-smart-contracts-b00daf354a9c --- ### Gnosis Safe Components - **Gnosis UI** - **Gnosis Safe Smart Contracts** - **Safe transaction service:** API to store off-chain signatures - **Safe Apps:** Enable extra functionality such as governance, fair Auctions --- ### 'Bread & Butter' Call Flow ```sequence ProxyFactory->Proxy: createProxy() Proxy->GnosisSafe: setup() Proxy-->GnosisSafe: execTransaction() ``` --- ### ProxyFactory.sol - Provides a simple way to create a new proxy contract pointing to a mastercopy, and executing a function in the newly deployed proxy all in one transaction. - The additional transaction is generally used to execute the setup function to initialise the Proxy contract state --- ### EIP1167 - Minimal Proxy Contract ```graphviz digraph { ProxyFactory -> Proxy1; ProxyFactory -> Proxy2; ProxyFactory -> Proxy3; ProxyFactory -> "Proxy ..."; Proxy1 -> GnosisSafe; Proxy2 -> GnosisSafe; Proxy3 -> GnosisSafe; "Proxy ..." -> GnosisSafe; } ``` - Proxies delegatecall to GnosisSafe - GnosisSafe is the 'mastercopy' or logic contract --- ### Minimal Proxy Contract rationale - Contract creation is expensive - Enable cheap deployment of clone 'minimal proxy contracts' - Each proxy points to a single 'mastercopy' (or singleton, logic contract, implementation contract) - Also allows upgrading contract logic by reploying and updating address of mastercopy --- ### Proxy.sol - Only has a constructor and a fallback function! - Fallback function will delegatecall to GnosisSafe.sol for logic execution - Storage state (i.e. token balances) maintained in Proxy --- ### GnosisSafe.sol - setup() - Set up initial Gnosis Safe state - execTransaction() - execute transactions --- ### GnosisSafe.sol inheritance ```graphviz digraph { OwnerManager -> GnosisSafe; ModuleManager -> GnosisSafe; FallbackManager -> GnosisSafe; "..." -> GnosisSafe; } ``` --- ### GnosisSafe.setup() - OwnerManager.setupOwners() - FallbackManager.internalSetFallbackHandler() - ModuleManager.setupModules() - handlePayment() --- ### OwnerManager.sol - Add, remove and replace owners - View list of owners - View and change threshold number of owners required to confirm a transaction --- ### GnosisSafe.setup() -> OwnerManager.setupOwners() - Interesting linked list data structure for storing addresses `mapping(address => address) internal owners;` - If you have address a, b and c as owners, stored as ```solidity= owners[address(0x1)] = a owners[a] = b owners[b] = c owners[c] = address(0x1) ``` - If owners[z] = 0, z is not a current owner --- ### FallbackManager.sol - Handle fallback functions - Required to make Safe 100% compatible with the ERC721 token standard --- ### GnosisSafe.setup() -> FallbackManager.internalSetFallbackHandler() ```solidity= // keccak256("fallback_manager.handler.address") bytes32 internal constant FALLBACK_HANDLER_STORAGE_SLOT = 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5; function internalSetFallbackHandler(address handler) internal { bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT; // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, handler) } } ``` --- ### ModuleManager.sol - Modules are smart contracts which add additional functionaly to the GnosisSafe contracts, while separating module logic from the Safe's core contract - A basic safe does not require any modules - Adding and removing a module requires confirmation from all owners - Modules can include daily spending allowances, amounts that can be spent without approval of other owners, etc. Modules enable developers to include their own features via a separate smart contract --- ### GnosisSafe.setup() -> ModuleManager.setupModules() ```solidity= function setupModules(address to, bytes memory data) internal { require(modules[SENTINEL_MODULES] == address(0), "GS100"); modules[SENTINEL_MODULES] = SENTINEL_MODULES; if (to != address(0)) // Setup has to complete successfully or transaction fails. require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000"); } ``` --- ### GnosisSafe.setup() -> ModuleManager.setupModules() -> Executor.execute() - Makes a specified delegatecall --- ### GnosisSafe.setup() -> GnosisSafe.handlePayment() - Optional - handle payment of ETH or ERC20 token to a specified address - Relayer can send the transaction (and pays the ETH gas fee), and will receive ETH or ERC20 token from the Safe - Enable user to pay for transaction using ERC20 token --- ### GnosisSafe.execTransaction() - GnosisSafe.encodeTransactionData() - GnosisSafe.checkSignatures() - GuardManager.getGuard() - GuardManager.checkTransaction() - Executor.execute() - GnosisSafe.handlePayment() - Guard.checkAfterExecution() --- ### GnosisSafe.execTransaction() -> GnosisSafe.encodeTransactionData() - Gets EIP712-compliant hash to be signed by multisig wallet owners --- ### GnosisSafe.execTransaction() -> GnosisSafe.checkSignatures() -> GnosisSafe.checkNSignatures() - Checks whether signatures provided are valid for provided transaction parameters and owners - Store multiple 65-byte Ethereum signatures in 'bytes memory signatures' - Ethereum signature format - {bytes32 r}{bytes32 s}{uint8 v} - Gnosis use different 'v' variable to mark different signature type - v = 27 or v = 28 in Ethereum ECDSA signature standard - v = 0 is contract signature, v = 1 is approved hash, v > 30 then adjusted for eth_sign flow --- ### GnosisSafe.execTransaction() -> GuardManager.checkTransaction() - Could not find official deployed implementation on Github repo - Seems intended to perform sanity checks and enforce restrictions on transaction parameters --- ### GnosisSafe.execTransaction() features - Use block-scoping to limit variable lifetime and prevent 'stack too deep' error --- ### GnosisSafe.execTransaction() -> GuardManager.checkAfterExecution() - Appears to guard against replay attack (nonce tracking) and re-entrancy --- ```sequence ProxyFactory->Proxy: createProxy() Proxy->GnosisSafe: setup() Proxy-->GnosisSafe: execTransaction() ``` --- ### Takeaways p1 - Lots of assembly use - very gas optimized - Using a mapping & uint256 to store owner addresses as a linked list - O(1) addition and removal of addresses (vs O(n) worst case in dynamic array). - If owner[address] = address(0), then address is not an owner. O(1) to check if address is an owner. - O(n) to iterate over list of owners, same as dynamic array. View functions are free in Solidity. - GnosisSafe functionality neatly segmented into modules, which are each inherited by GnosisSafe.sol --- ### Takeaways p2 - Require error strings - numbered error codes - Bitwise-and with bitwise mask 0xff..ff, retrieve data that is stored in data chunks smaller than standard 32-byte Solidity word - Use block-scoping within function body to limit variable lifetime and prevent 'stack too deep' error --- ### Resources p1 - https://github.com/gnosis/safe-contracts - https://safe-docs.dev.gnosisdev.com/safe/docs/contracts_architecture/ - https://slideslive.com/38911778/gnosis-safe-make-dealing-with-crypto-a-less-scary-thing - https://medium.com/gauntlet-networks/multisig-transactions-with-gnosis-safe-f5dbe67c1c2d --- ### Resource p2 - https://medium.com/coinmonks/diving-into-smart-contracts-minimal-proxy-eip-1167-3c4e7f1a41b8 - https://coincodecap.com/multi-signature-wallet - https://www.evm.codes/ --- ### Appendix p1 - Code snippet to see variables set in assembly ```solidity= function a() public view returns (bytes32) { assembly { let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) mstore(0x0, _singleton) return(0x0, 0x20) } } ``` --- ### Appendix p2 - ProxyFactory.sol createProxy() ```solidity= assembly { // call opcode => create new subcontext and execute code of the given account, then reusme the current one // returns 0 if the subcontext reverted, 1 otherwise // mload(data) => return size in bytes of 'data', first 32-byte word will be length of dynamic array allocated in memory // add(data, 0x20) => skip past first 32-byte word (contains length in bytes of data), to get memory location to start reading 'data' variable // => essentially loads 'data' argument into calldata to call newly constructed Proxy contract if eq(call(gas(), proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) { revert(0, 0) } } ...} ``` --- ### Appendix p3 - Proxy.sol fallback() ```solidity= assembly {... // Create new stack slot reserved for _singleton variable // Assign 32-byte word with address singleton (left-padded with 0s) // sload(0) => Load storage variable at slot 0 (address variable) // and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) => Bitwise and, with bitmask of 0x11...11 let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) ...} ``` --- ### Appendix p4 - Proxy.sol fallback() ```solidity= assembly {... // 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s // If send transaction with calldata = 0xa619486e ( function selector for masterCopy() ) // Return address singleton if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) { mstore(0, _singleton) return(0, 0x20) } ...} ``` --- ### Appendix p5 - Proxy.sol fallback() ```solidity= assembly {... // Copy calldata into memory calldatacopy(0, 0, calldatasize()) // Attempt delegatecall with tx data given // Returns 0 if subcontext reverted, returns 1 otherwise let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0) ...} ``` --- ### Appendix p6 - Proxy.sol fallback() ```solidity= assembly {... // Take return data from delegatecall, and copy to memory returndatacopy(0, 0, returndatasize()) // If delegatecall reverted in subcontext (GnosisSafe.sol, success == 0), revert but provide returndata if eq(success, 0) { revert(0, returndatasize()) } // If delegate call did not revert, return with returndata return(0, returndatasize()) ...} ``` --- ### Appendix p7 - SignatureDecoder.signatureSplit() ```solidity= // The signature format is a compact form of: // {bytes32 r}{bytes32 s}{uint8 v} // Compact means, uint8 is not padded to 32 bytes. assembly { // 0x41 = 65, signature stored in 65-bytes // So signature[0] starts at 'signature + 0' in memory // signature[2] starts at 'signature + 2*65 bytes' in memory let signaturePos := mul(0x41, pos) // Add 32-bytes to where signature[i] starts, first 32-bytes contains size r := mload(add(signatures, add(signaturePos, 0x20))) // Add 64-bytes to where signature[i] starts s := mload(add(signatures, add(signaturePos, 0x40))) // Get 32-byte word starting from 'signature[i] + 65-bytes. // Bitwise and with bitmask 0xff to grab the first two bytes. // Returned to caller as uint8-cast of those first two bytes // Do this because no mload8 in Solidity parser v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) ...} ``` ---
{"metaMigratedAt":"2023-06-16T19:57:02.396Z","metaMigratedFrom":"YAML","title":"Gnosis Multisig Safe","breaks":true,"description":"Presentation on Gnosis multisig wallet","slideOptions":"{\"theme\":\"white\",\"transition\":\"fade\"}","contributors":"[{\"id\":\"5bd646a2-a277-4837-9de1-ee14aa493235\",\"add\":15619,\"del\":5368}]"}
    2933 views