By Jefferson Tang
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
Time-intensive method to mathematically verify that a contract works as intended
ProxyFactory->Proxy: createProxy()
Proxy->GnosisSafe: setup()
Proxy-->GnosisSafe: execTransaction()
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
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
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
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
setup() - Set up initial Gnosis Safe state
execTransaction() - execute transactions
digraph {
OwnerManager -> GnosisSafe;
ModuleManager -> GnosisSafe;
FallbackManager -> GnosisSafe;
"..." -> GnosisSafe;
}
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
owners[address(0x1)] = a owners[a] = b owners[b] = c owners[c] = address(0x1)
Handle fallback functions
Required to make Safe 100% compatible with the ERC721 token standard
// 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) } }
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
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"); }
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
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
Could not find official deployed implementation on Github repo
Seems intended to perform sanity checks and enforce restrictions on transaction parameters
ProxyFactory->Proxy: createProxy()
Proxy->GnosisSafe: setup()
Proxy-->GnosisSafe: execTransaction()
Lots of assembly use - very gas optimized
Using a mapping & uint256 to store owner addresses as a linked list
GnosisSafe functionality neatly segmented into modules, which are each inherited by GnosisSafe.sol
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
function a() public view returns (bytes32) { assembly { let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) mstore(0x0, _singleton) return(0x0, 0x20) } }
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) } } ...}
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) ...}
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) } ...}
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) ...}
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()) ...}
// 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) ...}