Platform: Code4rena
Start Date: 14/03/2024
Pot Size: $49,000 USDC
Total HM: 3
Participants: 51
Period: 7 days
Judge: 3docSec
Id: 350
League: ETH
Rank: 31/51
Findings: 2
Award: $34.97
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: K42
Also found by: 0x11singh99, 0xAnah, Hajime, SAQ, SM3_SS, albahaca, clara, dharma09, hunter_w3b, naman1778, shamsulhaq123, slvDev
13.6948 USDC - $13.69
immutable
for the entryPoint
addressSince the entryPoint
address is constant and set at construction, use the immutable keyword to save gas
function entryPoint() public view virtual returns (address) { return 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; }
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L304-L306
function entryPoint() public pure returns (address) { return 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; }
+ address immutable ENTRY_POINT = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; + function entryPoint() public view returns (address) { + return ENTRY_POINT; }
Use the unchecked keyword to disable overflow and underflow checks for arithmetic operations that cannot underflow or overflow.
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L133-L138
if (address(this).balance < withdrawAmount) { revert InsufficientBalance(withdrawAmount, address(this).balance); } // NOTE: Do not include the gas part in withdrawable funds as it will be handled in `postOp()`. _withdrawableETH[userOp.sender] += withdrawAmount - maxCost;
MultiOwnable.sol::isOwner
MappingInstead of storing the owner's raw bytes as keys in the isOwner
mapping, which can lead to high storage costs and gas usage, we can store a hash of the owner's bytes and use that as the key.
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L24
Replace:
mapping(bytes account => bool isOwner_) isOwner;
With:
mapping(bytes32 => bool) isOwnerHash;
And update functions accordingly to use hashes.
Avoid using libraries for simple functions that can be implemented in the contract. This way, you can avoid the extra SLOAD and SSTORE operations required to store the library address and call the library functions.
For example, instead of using the SafeTransferLib
library for simple ETH transfers, you can implement the transfer function directly in the contract.
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L334
161 SafeTransferLib.forceSafeTransferETH(account, withdrawable, SafeTransferLib.GAS_STIPEND_NO_STORAGE_WRITES); 212 function entryPointDeposit(uint256 amount) external payable onlyOwner { 213 SafeTransferLib.safeTransferETH(entryPoint(), amount); 214 } 334 function _withdraw(address asset, address to, uint256 amount) internal { 335 if (asset == address(0)) { 336 SafeTransferLib.safeTransferETH(to, amount); 337 } else { 338 SafeTransferLib.safeTransfer(asset, to, amount); 339 }
MultiOwnable::removeOwnerAtIndex
functionUpdate functions to avoid redundant storage reads. For example, cache commonly used values locally to avoid multiple SLOAD operations.
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L102
Before:
function removeOwnerAtIndex(uint256 index) public virtual onlyOwner { bytes memory owner = ownerAtIndex(index); // Use owner... }
After:
function removeOwnerAtIndex(uint256 index) public virtual onlyOwner { bytes storage owner = _getMultiOwnableStorage().ownerAtIndex[index]; // Use owner... }
MultiOwnable::MultiOwnableStorage
structThe storage layout can be optimized by rearranging the order of struct members to reduce SLOAD operations.
Instead of:
struct MultiOwnableStorage { uint256 nextOwnerIndex; mapping(uint256 index => bytes owner) ownerAtIndex; mapping(bytes account => bool) isOwner_; }
Consider:
struct MultiOwnableStorage { mapping(uint256 => bytes) ownerAtIndex; mapping(bytes32 => bool) isOwnerHash; uint256 nextOwnerIndex; }
This layout minimizes the number of SLOADs
required to access storage variables.
Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.
File: src/SmartWallet/MultiOwnable.sol 201 function _checkOwner() internal view virtual {
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L201
File: src/SmartWallet/ERC1271.sol 121 function _eip712Hash(bytes32 hash) internal view virtual returns (bytes32) { 133 function _hashStruct(bytes32 hash) internal view virtual returns (bytes32) {
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/ERC1271.sol#L121
File: src/SmartWallet/CoinbaseSmartWallet.sol 291 function _validateSignature(bytes32 message, bytes calldata signature)
modifier payPrefund(uint256 missingAccountFunds) virtual { _; assembly ("memory-safe") { if missingAccountFunds { // Ignore failure (it's EntryPoint's job to verify, not the account's). pop(call(gas(), caller(), missingAccountFunds, codesize(), 0x00, codesize(), 0x00)) } } }
This modifier is only used in this function it should inilned to save extra JUMP instructions and additional stack operations
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) public payable virtual onlyEntryPoint payPrefund(missingAccountFunds) returns (uint256 validationData) {
File: src/SmartWallet/ERC1271.sol 23 bytes32 private constant _MESSAGE_TYPEHASH = keccak256("CoinbaseSmartWalletMessage(bytes32 hash)");
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/ERC1271.sol#L23
File: src/WebAuthnSol/WebAuthn.sol 55 bytes32 private constant EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"');
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/WebAuthnSol/WebAuthn.sol#L55
Using named arguments for struct means that the compiler needs to organize the fields in memory before doing the assignment, which wastes gas. Set each field directly in storage (use dot-notation), or use the unnamed version of the constructor.
Leverage mapping and dot notation for struct assignment
In dot notation, values are directly written to storage variable, When we use the current method in the code the compiler will allocate some memory to store the struct instance first before writing it to storage.
File: src/SmartWallet/ERC1271.sol 70 if (_validateSignature({message: replaySafeHash(hash), signature: signature})) {
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/ERC1271.sol#L70
File: src/SmartWallet/CoinbaseSmartWallet.sol 321 return WebAuthn.verify({challenge: abi.encode(message), requireUV: false, webAuthnAuth: auth, x: x, y: y});
When you have a mapping, accessing its values through accessor functions involves an additional layer of indirection, which can incur some gas cost. This is because accessing a value from a mapping typically involves two steps: first, locating the key in the mapping, and second, retrieving the corresponding value.
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L299-L301
function nonceUsed(address account, uint256 nonce) external view returns (bool) { return _nonceUsed[nonce][account]; }
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L117-L148
function isOwnerAddress(address account) public view virtual returns (bool) { return _getMultiOwnableStorage().isOwner[abi.encode(account)]; } function isOwnerPublicKey(bytes32 x, bytes32 y) public view virtual returns (bool) { return _getMultiOwnableStorage().isOwner[abi.encode(x, y)]; } function isOwnerBytes(bytes memory account) public view virtual returns (bool) { return _getMultiOwnableStorage().isOwner[account]; } function ownerAtIndex(uint256 index) public view virtual returns (bytes memory) { return _getMultiOwnableStorage().ownerAtIndex[index]; }
ASSEMBLY can be used to shorten the array by changing the length slot, so that the entries don't have to be copied to a new, shorter array
bytes[] memory owners = new bytes[](1);
The loop is very expensive in every iteration has external call and send value.
function executeBatch(Call[] calldata calls) public payable virtual onlyEntryPointOrOwner { for (uint256 i; i < calls.length;) { _call(calls[i].target, calls[i].value, calls[i].data); unchecked { ++i; } } }
function _call(address target, uint256 value, bytes memory data) internal { (bool success, bytes memory result) = target.call{value: value}(data); if (!success) { assembly ("memory-safe") { revert(add(result, 32), mload(result)) } } }
#0 - raymondfam
2024-03-22T21:31:18Z
13 generic G.
#1 - c4-pre-sort
2024-03-22T21:31:22Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-27T13:18:06Z
3docSec marked the issue as grade-b
🌟 Selected for report: roguereggiant
Also found by: 0xbrett8571, 0xepley, Circolors, JCK, JcFichtner, LinKenji, MSK, Myd, SAQ, SBSecurity, albahaca, cheatc0d3, clara, emerald7017, fouzantanveer, foxb868, hunter_w3b, kaveyjoe, popeye, unique
21.2754 USDC - $21.28
Coinbase
Wallet ContestSmart Wallet
ContestA smart wallet is a type of cryptocurrency wallet that supports advanced features such as passkey
ownership, multi-owner
support, and signature-based
withdrawals. It is a smart contract wallet that is compliant with the ERC-4337
standard and can be used with paymasters such as MagicSpend
. The WebAuthnSol
library is used to verify WebAuthn Authentication Assertions on-chain, while the FreshCryptoLib
library is used to check for signature malleability. The MultiOwnable
contract allows for multiple owners to control a smart wallet, while the ERC1271
contract is used to validate signatures via WebAuthnSol
or FreshCryptoLib
. The MagicSpend
contract allows for signature-based
withdrawals and can also be used to pay transaction gas.
Functionality:
WebAuthnSol
, a library for verifying WebAuthn Authentication Assertions on-chainERC-4337
compliant, which is a new standard for account abstraction that aims to improve the user experience of interacting with smart contractsMagicSpend
, which is a contract that allows for signature-based withdrawals and also allows using withdrawals to pay transaction gasKey Features:
Multi-owner
support: allows for multiple owners to control the smart wallet, improving security and reducing the risk of loss due to a single point of failureERC-4337 compliance
: adheres to the new account abstraction standard, improving interoperability with other contracts and providing a better user experienceFreshCryptoLib
FCL.sol: The Fresh Crypto library
provides a comprehensive set of functionalities for ECDSA signature verification and elliptic curve operations. It includes methods to verify ECDSA signatures and perform operations related to elliptic curve arithmetic.
Key Features:
ECDSA Signature Verification: The ecdsa_verify
function takes a message hash and signature parameters (r
, s
) along with the public key coordinates (Qx
, Qy
) and verifies whether the signature is valid for the given message and public key.
Elliptic Curve Arithmetic: The library implements functions to perform arithmetic operations on elliptic curve points in short Weierstrass form, such as point addition, point doubling, and scalar multiplication.
Modular Inversion: Modular inversion functions (FCL_nModInv
, FCL_pModInv
) are provided using precompiled contracts to efficiently compute the multiplicative inverse modulo n
and p
, where n
is the order of the curve and p
is the prime field modulus.
Core Logic:
ECDSA Verification:
The ecdsa_verify
function first checks if the provided signature parameters (r
and s
) are within the valid range (0 < r, s < n
), where n
is the curve order.
It then verifies if the provided public key (Qx
, Qy
) lies on the curve using ecAff_isOnCurve
function.
Next, it computes intermediate values for signature verification using modular inverses and scalar multiplications.
Finally, it checks if the computed x1
value is equal to zero, which determines the validity of the signature.
Elliptic Curve Arithmetic:
The library provides functions for point addition (ecAff_add
), point doubling (ecZZ_Dbl
), and scalar multiplication (ecZZ_mulmuladd_S_asm
), implemented using assembly for efficiency.
These functions handle operations in affine coordinates as well as in projective coordinates (ZZ
representation).
Modular Inversion:
a^(n-2)
.Addition Features
Precompiled Contracts: The library utilizes precompiled contracts (MODEXP_PRECOMPILE = 0x0000000000000000000000000000000000000005
) for modular exponentiation and inversion, enhancing efficiency in cryptographic operations.
Constant Definitions: Constants such as curve parameters (p
, a
, b
, gx
, gy
) and precomputed values (minus_2
, minus_2modn
, minus_1
) are defined for the specific curve (sec256R1
) and used throughout the library.
al Features:**
Precompiled Contracts: The library utilizes precompiled contracts (MODEXP_PRECOMPILE = 0x0000000000000000000000000000000000000005
) for modular exponentiation and inversion, enhancing efficiency in cryptographic operations.
Constant Definitions: Constants such as curve parameters (p
, a
, b
, gx
, gy
) and precomputed values (minus_2
, minus_2modn
, minus_1
) are defined for the specific curve (sec256R1
) and used throughout the library.
MagicSpend
MagicSpend.sol: MagicSpend
contract is an implementation of the ERC-4337
Paymaster standard, which is used for sponsoring gas fees. It is compatible with the Entrypoint
v0.6 interface. The contract allows users to deposit ETH, which can be used to pay for gas fees on their behalf when they execute transactions. The contract also allows users to withdraw excess ETH that was not used for gas fees.
Key Features:
Core Logic:
validatePaymasterUserOp
: validates a UserOperation (transaction) and ensures that the user has enough ETH in the contract to cover the gas fees. It also checks that the withdrawRequest
included in the UserOperation is valid and has not expired or been replayed. If the UserOperation is valid, the contract sets the validationData
to include the expiry time of the withdrawRequest
and a flag indicating whether the signature in the withdrawRequest
is valid.
postOp
: called by the Entrypoint after a UserOperation has been executed. It transfers the remaining ETH that was not used for gas fees back to the user.
withdrawGasExcess
: allows a user to withdraw excess ETH that was not used for gas fees.
withdraw
: allows a user to withdraw ETH from the contract by providing a valid withdrawRequest
.
ownerWithdraw
: allows the owner of the contract to withdraw all ETH from the contract.
entryPointDeposit
, entryPointWithdraw
, entryPointAddStake
, entryPointUnlockStake
, entryPointWithdrawStake
: allow the owner of the contract to interact with the Entrypoint, including depositing ETH, withdrawing ETH, adding stake, unlocking stake, and withdrawing stake.
Additional Features:
onlyEntryPoint
modifier: ensures that only the Entrypoint can call certain functions in the contract.
_validateRequest
: validates a withdrawRequest
to ensure that it has not been replayed.
_withdraw
: low-level function to withdraw ETH from the contract.
isValidWithdrawSignature
: checks the signature in a withdrawRequest
to ensure that it is valid.
getHash
: generates a hash of a withdrawRequest
that can be used to validate its signature.
nonceUsed
: checks whether a nonce has already been used in a withdrawRequest
.
entryPoint
: returns the address of the canonical ERC-4337 Entrypoint v0.6 contract.
SmartWallet
**CoinbaseSmartWallet.sol:**The Coinbase Smart Wallet
allow users to securely store and manage their assets, and to execute transactions.
Functionality:
addOwnerAddress
and removeOwnerAtIndex
functions.execute
and executeBatch
functions._validateSignature
function that is used both for classic ERC-1271 signature validation and for UserOperation
validation.Key Features:
UUPS
pattern_validateSignature
functionREPLAYABLE_NONCE_KEY
Core Logic:
SignatureWrapper
struct to tie a signature to its signer, and a custom Call
struct to describe a raw call to execute.onlyEntryPoint
to ensure that only the EntryPoint (i.e. msg.sender
) can execute certain functions, such as validateUserOp
and executeWithoutChainIdValidation
.validateUserOp
function is used to validate a UserOperation
before it is executed. The function checks that the UserOperation
key is valid, that the signature is valid, and that the UserOperation
is authorized to skip the chain ID validation if necessary.execute
and executeBatch
functions are used to execute transactions on behalf of the user. The former executes a single transaction, while the latter executes a batch of transactions.entryPoint
function returns the address of the EntryPoint v0.6.getUserOpHashWithoutChainId
function computes the hash of a UserOperation
in the same way as EntryPoint v0.6, but leaves out the chain ID. This allows accounts to sign a hash that can be used on many chains.CoinbaseSmartWalletFactory.sol: Coinbase Smart Wallet Factory
contract is a factory for creating Coinbase Smart Wallets, which are based on the ERC-4337
standard. The factory deploys new accounts behind a minimal ERC1967
proxy whose implementation points to a registered ERC-4337
implementation.
Functionality:
createAccount
function deploys a new account and returns its deterministic address. It takes a list of initial owners and a nonce as inputs. The owners can be a set of addresses and/or public keys depending on the signature scheme used.getAddress
function returns the deterministic address of an account created via createAccount
using the given owners and nonce.initCodeHash
function returns the initialization code hash of the account (a minimal ERC1967 proxy)._getSalt
function returns the deterministic salt for a specific set of owners and nonce.Key Features:
Core Logic:
createAccount
function first checks if the owners array is empty and reverts with an error if it is.LibClone
library to create a deterministic ERC1967 account address using the provided implementation and salt.CoinbaseSmartWallet
and initialized with the provided owners if it has not been deployed before.getAddress
function uses the LibClone
library to predict the deterministic address of an account created with the given owners and nonce.initCodeHash
function uses the LibClone
library to return the initialization code hash of a minimal ERC1967 proxy._getSalt
function uses the keccak256
hash function to generate a deterministic salt from the provided owners and nonce.ERC1271.sol: This contract is an abstract implementation of ERC-1271
, a standard for creating contracts that can validate signatures. The contract extends the ERC-1271
standard by adding a layer of protection against cross-account
signature replay attacks. This is achieved by introducing a new EIP-712
compliant hash that includes the chain ID and address of the contract, making it impossible for the same signature to be validated on different accounts owned by the same signer.
Key Features:
_MESSAGE_TYPEHASH
which is a precomputed typeHash
used to produce EIP-712 compliant hash when applying the anti cross-account-replay layer.eip712Domain()
that returns information about the EIP712Domain
used to create EIP-712 compliant hashes.isValidSignature(bytes32 hash, bytes calldata signature)
that validates a signature against a given hash. The contract applies the anti cross-account-replay layer on the given hash before performing the signature validation.replaySafeHash(bytes32 hash)
that produces a replay-safe hash from the given hash. This function is a wrapper around _eip712Hash()
and returns an EIP-712 compliant replay-safe hash.domainSeparator()
that returns the domainSeparator
used to create EIP-712 compliant hashes._eip712Hash(bytes32 hash)
that returns the EIP-712 typed hash of the CoinbaseSmartWalletMessage(bytes32 hash)
data structure._hashStruct(bytes32 hash)
that returns the EIP-712 hashStruct
result of the CoinbaseSmartWalletMessage(bytes32 hash)
data structure._domainNameAndVersion()
and _validateSignature(bytes32 message, bytes calldata signature)
, that must be implemented by the contract's subclass.Core Logic:
isValidSignature(bytes32 hash, bytes calldata signature)
function, which validates a signature against a given hash. The function applies the anti cross-account-replay layer on the given hash before performing the signature validation.replaySafeHash(bytes32 hash)
function. This function produces a replay-safe hash from the given hash by including the chain ID and address of the contract in the EIP-712 compliant hash.Additional Features:
ERC-5267
, which defines a standard for EIP-712 domains.eip712Domain()
that returns information about the EIP712Domain
used to create EIP-712 compliant hashes. This function is used to ensure that the contract is compliant with ERC-5267.fields
that is a bitmap of used fields in the EIP712Domain
. The value of fields
is set to hex"0f"
, which is equivalent to 0b1111
._domainNameAndVersion()
that returns the domain name and version to use when creating EIP-712 signatures. This function must be implemented by the contract's subclass._validateSignature(bytes32 message, bytes calldata signature)
that validates the signature
against the given message
. This function must be implemented by the contract's subclass. The function's signature is designed to be flexible, and the implementation can choose to interpret the signature
argument differently depending on its use case.MultiOwnable.sol: It allows for multiple owners, each identified by raw bytes, and provides functionality for adding
, removing
, and verifying owners. The owners can be identified by Ethereum addresses
or passkeys
(public keys). The contract is designed to follow ERC-7201
, which provides a standard for storage layout in contracts.
Functionality:
Core Logic:
addOwnerAddress()
: Adds a new owner address.addOwnerPublicKey()
: Adds a new owner passkey (public key).removeOwnerAtIndex()
: Removes an owner from the given index.isOwnerAddress()
, isOwnerPublicKey()
, isOwnerBytes()
: Check if a given address, public key, or raw bytes is an owner.ownerAtIndex()
: Returns the owner bytes at a given index.nextOwnerIndex()
: Returns the next index that will be used to add a new owner._initializeOwners()
: Initializes the owners of this contract._addOwner()
and _addOwnerAtIndex()
: Adds an owner at a given index or the next available index.Additional Features:
_getMultiOwnableStorage()
function to get a storage reference to the MultiOwnableStorage
struct. This is done to comply with the ERC-7201 standard for storage layout.WebAuthnSol
WebAuthn.sol: This library for verifying WebAuthn
Authentication Assertions. It is built off the work of Daimo and uses the RIP-7212
precompile for signature verification, falling back to FreshCryptoLib
if precompile verification fails.
Functionality:
verify
that takes in a challenge, a boolean indicating whether user verification is required, a WebAuthnAuth
struct, and the x and y coordinates of the public key. It returns a boolean indicating whether the authentication assertion passed validation.Key Features:
RIP-7212
precompile for signature verification, falling back to FreshCryptoLib if precompile verification fails.Core Logic:
verify
function first checks that the s value of the signature is less than or equal to the secp256r1 curve order divided by 2 to prevent signature malleability.Owners
: addresses or public keys that have control over the smart contract wallet. Owners can add or remove other owners, execute transactions, and initialize the wallet.EntryPoint
: a specific address that is used to execute certain functions in the smart contract wallet, such as validateUserOp
and executeWithoutChainIdValidation
.User
: an address that initiates transactions through the smart contract wallet. Users can have one or more owners who control their wallet.Relying Party
: an entity that relies on the WebAuthn authentication assertion produced by the smart contract wallet. The Relying Party is responsible for providing the challenge that is used in the authentication process.Authenticator
: a device or service that produces WebAuthn authentication assertions. The authenticator is responsible for verifying the user's presence and enforcing user verification if required.Verifier
: a contract or library that verifies the WebAuthn authentication assertion produced by the smart contract wallet. The verifier checks that the assertion is well-formed and that the signature is valid.Factory
: a contract that deploys new instances of the smart contract wallet. The factory is responsible for initializing the wallet with its first set of owners.Paymaster
: a contract that can sponsor gas for a user's transaction. The paymaster is used in the context of ERC-4337 to allow users to execute transactions without having to pay gas fees themselves.MultiOwnable.sol
isOwner
and ownerAtIndex
Mappings:
isOwner
mapping should match the corresponding entries in the ownerAtIndex
mapping.i
, isOwner[ownerAtIndex[i]]
should be true if and only if the owner at index i
exists.ERC1271.sol
Signature Validation
: The _validateSignature
function should always correctly validate the signature against the given message. This is the core functionality of the ERC-1271 standard.CoinbaseSmartWallet.sol
The UserOperation
key must be either REPLAYABLE_NONCE_KEY
or a valid nonce for the account.
validateUserOp
function checks if the UserOperation
key is either REPLAYABLE_NONCE_KEY
or a valid nonce for the account. If it is not, the function reverts with the InvalidNonceKey
error.If the UserOperation
key is REPLAYABLE_NONCE_KEY
, then the UserOperation
call selector must be whitelisted to skip the chain ID validation.
executeWithoutChainIdValidation
function checks if the UserOperation
key is REPLAYABLE_NONCE_KEY
.If it is, the function checks if the UserOperation
call selector is whitelisted to skip the chain ID validation.If the call selector is not whitelisted, the function reverts with the SelectorNotAllowed
error.The UserOperation
signature must be valid for the account.
validateUserOp
function checks if the UserOperation
signature is valid for the account. If the signature is not valid, the function reverts with the InvalidSignature
error.MagicSpend.sol
The WithdrawRequest
signature must be valid for the given account
and withdrawRequest
.
isValidWithdrawSignature
function checks if the WithdrawRequest
signature is valid for the given account
and withdrawRequest
. If the signature is not valid, the function returns false
.The WithdrawRequest
nonce must not have been used before by the given account
.
_validateRequest
function checks if the WithdrawRequest
nonce has been used before by the given account
. If the nonce has been used before, the function reverts with the InvalidNonce
error.If either of these invariants is violated, the contract could be vulnerable to attack.
For example, an attacker could create a fake WithdrawRequest
with a forged signature and use it to withdraw funds from the contract. Or, an attacker could replay a previously used WithdrawRequest
to withdraw funds multiple times.
Smart Wallet
ProtocolAccordingly, I analyzed and audited the subject in the following steps;
Core Protocol Contract Overview:
I focused on thoroughly understanding the codebase and providing recommendations to improve its functionality.
The main goal was to take a close look at the important contracts and how they work together in the Smart Wallet
.
I start with the following contracts, which play crucial roles in the Smart Wallet
:
CoinbaseSmartWallet.sol CoinbaseSmartWalletFactory.sol ERC1271.sol WebAuthn.sol MagicSpend.sol
I started my analysis by examining the intricate structure and functionalities of the Smart Wallet
protocol, which includes the CoinbaseSmartWallet.sol
and CoinbaseSmartWalletFactory.sol
contracts. The Smart Wallet
allows users to securely store and manage their assets while supporting multiple owners and executing
transactions on behalf of the user. It employs a signature validation mechanism using either ERC1271 or WebAuthn authentication to ensure transactions are authorized by appropriate owners.
The Smart Wallet Factory is a factory for creating Coinbase Smart Wallets based on the ERC-4337
standard. It deploys new accounts behind a minimal ERC1967
proxy, whose implementation points to a registered ERC-4337
implementation. The factory uses a deterministic address generation mechanism and a minimal ERC1967 proxy to deploy new accounts, allowing for upgradeability and flexibility in terms of the ERC-4337 implementation used.
In addition to the Smart Wallet contracts, I also analyzed the FreshCryptoLib
(FCL) library, which provides functionalities for ECDSA
signature verification and elliptic curve operations. The FCL library includes methods to verify ECDSA
signatures and perform operations related to elliptic curve arithmetic, making it an essential component of the Smart Wallet protocol.
Furthermore, I examined the WebAuthn
library, which is used for verifying WebAuthn
Authentication Assertions in the Smart Wallet protocol. It employs the RIP-7212 precompile for signature verification and falls back to FreshCryptoLib if precompile verification fails.
By understanding the structure and functionalities of these contracts and libraries, I can gain insights into the overall design and security of the Smart Wallet protocol.
Documentation Review:
Then went to Review these README
of every file, for a more detailed and technical explanation see that video of the Smart Wallet
and play around with that site.
Compiling code and running provided tests with Solidity 0.8.23:
Manuel Code Review In this phase, I initially conducted a line-by-line analysis, following that, I engaged in a comparison mode.
Line by Line Analysis: Pay close attention to the contract's intended functionality and compare it with its actual behavior on a line-by-line basis.
Comparison Mode: Compare the implementation of each function with established standards or existing implementations, focusing on the function names to identify any deviations.
Overall, I consider the quality of the Smart Wallet from Coinbase Wallet
protocol codebase to be Good. The code appears to be mature and well-developed. We have noticed the implementation of various standards adhere to appropriately. Details are explained below:
Codebase Quality Categories | Comments |
---|---|
Architecture & Design | The protocol features a modular design, segregating functionality into distinct contracts (e.g., FreshCryptoLib , MagicSpend , SmartWallet , WebAuthnSol ) for clarity and ease of maintenance. |
Error Handling & Input Validation | Functions check for conditions and validate inputs to prevent invalid operations, though the depth of validation (e.g., for edge cases transactions) would benefit from closer examination. The Smart Wallet protocol use errors to define custom error messages that can be reverted with. This allows more descriptive errors than just generic revert messages.Input validation is done by defining expected input formats upfront (e.g. structured types for withdraw requests) and validating inputs match these formats on entry.Validation includes checks for things like:Valid signature formats and owners,Withdraw request field values like expiry , nonce , amount .Nonce validation prevents replay attacks by tracking already used nonces.Chain ID validation is done for operations that require it, with whitelist for certain functions.Validation is separated from logic - validated inputs are passed to internal methods that assume validity.Errors are thrown on failed validation rather than returning boolean, following EIP practices.Signature validation utilizes signature library for modular validation logic. |
Code Maintainability and Reliability | The contracts are written with emphasis on sustainability and simplicity. The functions are single-purpose with little branching and low cyclomatic complexity. The protocol includes a novel mechanism for collecting fees that offers an incentive for this work to be done by outside actors,thereby removing the associated complexity. |
Code Comments | The different types of comments are used for in the Smart Wallet protocol Single-line comments , Multi-line comments , Documentation comments : documentation, explanations, TODOs, and separating/structuring the code for readability. The NatSpec tags allow automatically generating documentation as well.The contracts are accompanied by comprehensive comments, facilitating an understanding of the functional logic and critical operations within the code. Functions are described purposefully, and complex sections are elucidated with comments to guide readers through the logic. Despite this, certain areas, particularly those involving intricate mechanics or tokenomics, could benefit from even more detailed commentary to ensure clarity and ease of understanding for developers new to the project or those auditing the code. |
Testing | The contracts exhibit a commendable level of test coverage 95% but with aim to 100% for indicative of a robust testing regime. This coverage ensures that a wide array of functionalities and edge cases are tested, contributing to the reliability and security of the code. However, to further enhance the testing framework, the incorporation of fuzz testing and invariant testing is recommended. These testing methodologies can uncover deeper, systemic issues by simulating extreme conditions and verifying the invariants of the contract logic, thereby fortifying the codebase against unforeseen vulnerabilities. |
Code Structure and Formatting | The codebase benefits from a consistent structure and formatting, adhering to the stylistic conventions and best practices of Solidity programming. Logical grouping of functions and adherence to naming conventions contribute significantly to the readability and navigability of the code. While the current structure supports clarity, further modularization and separation of concerns could be achieved by breaking down complex contracts into smaller, more focused components. This approach would not only simplify individual contract logic but also facilitate easier updates and maintenance. |
Strengths | The strength of the protocol lies in its comprehensive set of functionalities for ECDSA signature verification and elliptic curve operations . The FreshCryptoLib provides key features such as ECDSA signature verification, elliptic curve arithmetic, and modular inversion using precompiled contracts. The library also utilizes constant definitions for the specific curve (sec256R1 ) and precomputed values to enhance efficiency.The SmartWallet contract allows users to securely store and manage their assets, and to execute transactions, while supporting multiple owners and signature validation mechanisms. |
Documentation | While the NatSpec and README provides comprehensive details for all external functions and there are helpful inline comments throughout, there is currently no external documentation available for users or integrators. It is crucial to develop external documentation to offer a comprehensive understanding of the contract's functionality, purpose, and interaction methods. This documentation should encompass detailed explanations of the contract's features, guidance on utilizing and integrating its functions, along with relevant use cases and examples. By providing clear and thorough external documentation, users and integrators can gain a complete understanding of the contract and effectively utilize its capabilities |
MultiOwnable.sol
Systemic Risks:
Owner index manipulation
: By manipulating the owner indexes through addition/removal, it may enable vectoring in replay attacks if indexes are ever used to gate access or privileges. This centralizes control using indexes.
Lack of owner key revocation
: Once an account is added as an owner, its privileges are permanent and cannot be revoked even if the private key is compromised.
Technical Risks:
Manipulable owner indexes
: Owner indexes can be manipulated by removal and addition of owners, allowing replay attacks.
Unchecked Array Length
: In the _initializeOwners
function, there's a loop that iterates over the provided owners array without checking its length against potential gas limits. If the array is excessively large, it could lead to out-of-gas
errors function calls.
function _initializeOwners(bytes[] memory owners) internal virtual { for (uint256 i; i < owners.length; i++) { if (owners[i].length != 32 && owners[i].length != 64) { revert InvalidOwnerBytesLength(owners[i]); } if (owners[i].length == 32 && uint256(bytes32(owners[i])) > type(uint160).max) { revert InvalidEthereumAddressOwner(owners[i]); } _addOwnerAtIndex(owners[i], _getMultiOwnableStorage().nextOwnerIndex++); } }
ERC1271.sol
Systemic Risks:
block.chainid
: Uses block.chainid potentially lead to issues if the chain ID is not properly handled. For example, if the chain ID is not correctly set, it could lead to the creation of an invalid domain separator.function domainSeparator() public view returns (bytes32) { (string memory name, string memory version) = _domainNameAndVersion(); return keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, address(this) ) ); }
Technical Risks:
Complexity in Signature Validation
: The ERC1271.sol
contract abstract the signature validation process, leaving it to be implemented by inheriting contracts. Depending on the implementation of _validateSignature
, there could be risks related to incorrect or insufficient validation logic, leading to vulnerabilities such as signature forgery.CoinbaseSmartWalletFactory.sol
Stemic Risks:
Predictive A Cross-Chain Replayable Transactions
: The contract uses a reserved nonce key (REPLAYABLE_NONCE_KEY) for cross-chain replayable transactions. If there's a flaw in the implementation, it could potentially lead to replay attacks across different chains.Technical Risks:
Unvalidated inputs
: The CoinbaseSmartWalletFactory
contract does not validate the input parameters, such as the erc4337
address provided in the constructor or the owners and nonce parameters
in the createAccount
function.
Deterministic Address Generation
: The contract relies on deterministic
address generation for deploying smart wallets. While deterministic address generation can provide predictability
, any flaws in the salt generation
or address computation logic
could lead to address collisions
or vulnerabilities in the deployed smart wallets.
Centralization Risks:
Denial-of-service attack on the factory
: A potential risk is an attacker deliberately deploying a large number of accounts through the factory contract
, causing a state bloat that could lead to increased gas costs and potential Denial-of-Service for legitimate users. ThisIntegration Risks:
Custom owner representation
: The CoinbaseSmartWalletFactory.sol
contract accepts owners as a bytes[]
parameter, which could represent a mix of addresses
and public keys
, depending on the signature
scheme used. Integrating this contract with other systems or contracts that do not support this custom owner representation or require a different format could lead to compatibility issues.CoinbaseSmartWallet.sol
Systemic Risks:
Cross-Chain Replayable Transactions
: The contract uses a reserved nonce key (REPLAYABLE_NONCE_KEY
) for cross-chain replayable transactions. If there's a flaw in the implementation, it could potentially lead to replay attacks
across different chains.
Replayable transactions without chain ID validation
: The CoinbaseSmartWallet.sol
allows for certain transactions
to be replayed across chains
without validating the chain ID
. This could allow an attacker to execute
a transaction
on one chain that has already been executed on another chain, even if the two chains have different chain IDs
.
Custom signature verification function
: The contract uses a custom signature verification
function that is not based on any standard. This could make it difficult to verify the authenticity of signatures and could increase the risk of signature forgery.
Centralization Risks:
Hardcoded EntryPoint Address
: The contract has a hardcoded EntryPoint
address. This could lead to centralization risks as the hardcoded EntryPoint
has significant control over the contract's operations.Technical Risks:
Unvalidated Inputs
: The contract does not validate inputs in some functions. For example, in the execute
and executeBatch
functions, there are no checks on the target
, value
, and data parameters
.function executeBatch(Call[] calldata calls) public payable virtual onlyEntryPointOrOwner { for (uint256 i; i < calls.length;) { _call(calls[i].target, calls[i].value, calls[i].data); unchecked { ++i; } } }
Use of payPrefund modifier
: The payPrefund modifier is used to send missing funds to the EntryPoint
. If there's a bug in the implementation, it could lead to loss of funds.
modifier payPrefund(uint256 missingAccountFunds) virtual { _; assembly ("memory-safe") { if missingAccountFunds { // Ignore failure (it's EntryPoint's job to verify, not the account's). pop(call(gas(), caller(), missingAccountFunds, codesize(), 0x00, codesize(), 0x00)) } } }
Integration Risks:
Use of WebAuthn.verify
: The contract uses the WebAuthn.verify
function from the "WebAuthnSol
" library for signature validation. If there are issues with the WebAuthn.verify
function or if it's not integrated correctly, this could lead to integration risks.WebAuthn.sol
Technical Risks:
Signature Malleability
: The contract checks if the 's' value of the secp256r1.s
signature is greater than P256_N_DIV_2
to guard against signature malleability. However, this only covers one type of malleability attack
. Other types of malleability attacks could still be possible.if (webAuthnAuth.s > P256_N_DIV_2) {
Integration Risks:
clientDataJSON
: The contract assumes that the clientDataJSON
is well-formed
and does not contain any malicious data
. If the clientDataJSON
is not properly validated before being passed to the contract, it could lead to issues.
Public Key
: The contract assumes that the x
and y
coordinates of the public key
are valid. If the public key is not properly validated before being passed to the contract, it could lead to issues.
Challenge
: The contract assumes that the challenge
provided by the relying party
is valid. If the challenge is not properly validated before being passed to the contract, it could lead to issues.
requireUV
: The contract assumes that the requireUV
flag is set correctly
. If this flag is not set correctly, it could lead to issues with user verification.
Fallback to FreshCryptoLib:
The contract falls back to FreshCryptoLib
if precompile verification
fails. If FreshCryptoLib
is not available or contains bugs, this could lead to issues.
Gas Limit
: The contract does not have any checks to prevent exceeding the gas limit
. If the contract's operations require more gas than available, it could lead to issues.
FCL.sol
Systemic Risks:
Bias in random number generation
: Functions like ecZZ_mulmuladd_S_asm
rely on unbiased randomness which chain-based
execution may not provide.Centralization Risks:
Single Point of Failure
: The contract uses a single precompiled
contract for certain operations. If this precompiled contract becomes unavailable or is compromised, it could lead to issues with the contract.Integration Risks:
Incorrect Inputs
: The contract assumes that the inputs provided to it (message, r, s, Qx, Qy
) are valid. If these inputs are not properly validated before being passed to the contract, it could lead to issues.MagicSpend.sol
Systemic Risks:
Nonce Reuse Vulnerability
: The contract uses nonces to prevent replay attacks on withdraw requests
. However, if nonces are not generated securely or if they are reused, it could lead to unauthorized fund withdrawals or denial service attacks.
Contract Balance Depletion
: During the postOp()
function execution, all remaining funds are transferred
to the user account. If the contract's balance
is insufficient to cover the remaining funds, it could result in failed transactions and unexpected behavior.
There is a tight
coupling with the EntryPoint
contract (0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
). If the EntryPoint
contract is upgraded or changes its interface, this contract may break.
Centralization Risks:
constructor
) who has significant control over the contract. The owner can withdraw funds
, deposit/withdraw
from the EntryPoint
, add/unlock/withdraw
stake from the EntryPoint, and potentially sign withdraw requests.Technical Risks:
Signature replay attacks
: Signatures are not hashed with chainId
or contract address
.Integration Risks:
MagicSpend
assumes that the actualGasCost
returned by the EntryPoint
contract in the postOp
function is correct. If the EntryPoint
contract returns an incorrect actualGasCost
, it could lead to incorrect calculations
and fund transfers
.Improve Signature Validation: The ERC1271
contract could be extended to support additional signature schemes beyond just ERC-1271 and WebAuthn. For example, it could add support for other common signature schemes like EIP-2098
or EIP-2612
.
Enhance Replay Protection: The ERC1271
contract's anti-replay protection could be further strengthened by incorporating additional contextual information, such as the transaction nonce or the current block timestamp, into the EIP-712 hash to make it even more difficult to replay signatures.
Modularize Functionality: The various contracts (FreshCryptoLib
, MagicSpend
, CoinbaseSmartWallet
, CoinbaseSmartWalletFactory
, ERC1271
, MultiOwnable
, WebAuthn
) could be further modularized and decoupled, allowing for easier maintainability and the ability to mix-and-match functionalities as needed.
Add Support for More Curves: The FreshCryptoLib
could be extended to support additional elliptic curves beyond just secp256r1
, allowing for greater flexibility and compatibility with a wider range of cryptographic applications.
Implement Batch Signature Verification: The FreshCryptoLib
could be enhanced to support batch verification of multiple ECDSA signatures, improving the efficiency of verifying large sets of signatures.
Add Support for ERC-4337 Meta-Transactions: The CoinbaseSmartWallet
and CoinbaseSmartWalletFactory
contracts could be further enhanced to support the ERC-4337 meta-transaction standard, allowing users to execute transactions without having to pay gas fees directly.
Gas Sponsorship via ERC-4337 Paymaster (MagicSpend): The protocol incorporates the ERC-4337 Paymaster standard, which allows users to deposit ETH into the MagicSpend contract to sponsor gas fees for their transactions. This feature eliminates the need for users to hold ETH for gas payments, making it more accessible and user-friendly.
Flexible Multi-Signature Wallet (CoinbaseSmartWallet): The CoinbaseSmartWallet supports multiple owners with flexible signature validation mechanisms, including both traditional ERC-1271 and modern WebAuthn authentication. This allows for increased security and adaptability to different authentication methods.
Cross-Account Replay Protection (ERC1271): The ERC1271 implementation in the protocol introduces a layer of protection against cross-account signature replay attacks. This is achieved by using EIP-712 compliant hashes that include the chain ID and contract address, making it impossible for the same signature to be validated on different accounts owned by the same signer.
Deterministic Wallet Deployment (CoinbaseSmartWalletFactory): The CoinbaseSmartWalletFactory generates deterministic addresses for new wallets based on the owners and a nonce. This ensures that the same set of owners and nonce will always produce the same wallet address, simplifying wallet management and recovery.
WebAuthn Authentication Support (WebAuthn): The protocol includes support for WebAuthn, a modern authentication standard that utilizes hardware security keys or platform authenticators (e.g., fingerprint sensors, facial recognition) for enhanced security. This aligns with the growing adoption of WebAuthn in the industry.
Efficient Cryptographic Operations (FreshCryptoLib): The FreshCryptoLib library provides efficient implementations of cryptographic operations, such as ECDSA signature verification and elliptic curve arithmetic, by utilizing precompiled contracts. This improves the overall performance and gas efficiency of the protocol.
Batch Transaction Execution (CoinbaseSmartWallet): The CoinbaseSmartWallet supports batch execution of transactions, allowing users to bundle multiple transactions into a single operation. This can save gas costs and improve the overall efficiency of the wallet.
The overall workflow
involves users creating a CoinbaseSmartWallet
instance through the CoinbaseSmartWalletFactory
, depositing ETH into the MagicSpend
contract for gas sponsorship, and executing transactions through their wallet. The wallet validates signatures using ERC1271
or WebAuthn
, and the MagicSpend
contract handles gas fee sponsorship and excess ETH refunds. The FreshCryptoLib
and WebAuthn
libraries provide essential cryptographic functions for signature verification and authentication.
User Setup:
CoinbaseSmartWalletFactory
.Transaction Execution:
CoinbaseSmartWallet
contract validates the signatures and ensures that the transaction is authorized by the appropriate owners.MagicSpend
contract as a paymaster.Gas Fee Payment:
MagicSpend
contract pays for the gas fees associated with the transaction using ETH deposited by the user.Cross-Chain Transactions:
CoinbaseSmartWallet
contract supports cross-chain transactions using a replayable nonce.WebAuthn Authentication:
Wallet Management:
MultiOwnable
contract.Signature Verification:
ERC1271
contract provides a standard for verifying signatures.CoinbaseSmartWallet
contract uses an anti cross-account-replay layer to prevent signature replay attacks.WebAuthn
library is used to verify WebAuthn authentication assertions.Withdrawal:
File Name | Core Functionality | Technical Characteristics | Importance and Management |
---|---|---|---|
MultiOwnable.sol | The MultiOwnable contract allows for multiple owners to be registered, each identified by their raw byte s, enabling decentralized control and management of contract operations. | Utilizing a storage layout specified by ERC-7201 , the contract employs mappings and modifiers to manage owner registration and access control, ensuring secure of contract functions. | The MultiOwnable provides functions to add and remove owners , check owner status, and initialize owners during deployment, facilitating flexible management of ownership rights and responsibilities within the contract's structure. |
ERC1271.sol | The ERC1271.sol contract provides an abstract implementation of ERC-1271 with additional safeguards against cross-account replay attacks , ensuring secure validation of signatures while preventing misuse of signatures across different accounts owned by the same signer. | Leveraging EIP-712 compliant hashing mechanisms, the contract introduces an anti cross-account-replay layer to mitigate the risk of replay attacks, enhancing the security and reliability of signature verification processes within Ethereum smart contracts. | The ERC1271.sol defines functions and internal methods to compute replay-safe hashes, validate signatures, and retrieve domain separators, facilitating seamless integration and management of signature validation functionalities within contracts. |
CoinbaseSmartWalletFactory.sol | The CoinbaseSmartWalletFactory.sol serves a factory for deploying Coinbase Smart Wallets, utilizing ERC-4337 implementation behind minimal ERC1967 proxies, allowing for the creation of deterministic addresses for each account. | Leveraging Solady's ERC4337Factory , the contract enables the creation of new Coinbase Smart Wallets with a specified set of owners and a nonce, ensuring secure and efficient deployment of smart wallet contracts. | The CoinbaseSmartWalletFactory.sol contract defines functions to create ERC-4337 accounts, retrieve deterministic addresses, and compute initialization code hashes and salts, providing essential tools for managing and deploying Coinbase Smart Wallets within Ethereum smart contracts. |
CoinbaseSmartWallet.sol | The CoinbaseSmartWallet.sol functions as an ERC4337-compatible smart contract wallet, enabling the creation and management of smart wallets with multi-owner functionality, signature validation , and execution of arbitrary calls. | Leveraging Solady's ERC4337 implementation and incorporating features from Alchemy's LightAccount and Daimo's DaimoAccount , the contract provides a secure and efficient management of funds and operations, including signature validation , execution of batch calls , and support for WebAuthn authentication. | The contract facilitates account initialization with specified owners, validates user operations including nonce-based replay protection, and supports dynamic upgrades via UUPSUpgradeable , offering a comprehensive solution for managing smart wallet functionality. |
WebAuthn.sol | The contract provides a library for verifying WebAuthn Authentication Assertions , incorporating the WebAuthn protocol and leveraging precompiles for signature verification. | Utilizing concepts from Daimo's work , it verifies assertions by checking authenticator data, client data JSON, and signatures, with fallback to FreshCryptoLib if precompile verification fails. | The library facilitates the verification of WebAuthn assertions by validating challenge values, user verification flags, and signature authenticity. |
FCL.sol | The Solidity library FCL provides functions for elliptic curve digital signature algorithm (ECDSA) verification, implementing core operations such as verifying signatures and checking if points are on the curve . | The library leverages optimized algorithms and assembly code to efficiently perform elliptic curve arithmetic, including point addition, doubling, inversion, and modular exponentiation, ensuring secure and reliable ECDSA verification. | This FCL.sol is crucial for secure cryptographic operations relying on ECDSA for digital signature verification. |
MagicSpend.sol | MagicSpend is an ERC4337 Paymaster contract facilitating signed withdraw requests for ETH and ensuring validation of requests before executing fund transfers. | MagicSpend utilizes ECDSA signatures, maintains nonces to prevent replays, Signed Message hashes for signature validation. | MagicSpend validating withdraw requests, manages nonces to prevent replay attacks, and ensures efficient fund transfers while minimizing gas usage. |
Gas Fee Calculation Logic:
Signature Generation & Usage:
Reuse of Signatures:
Front-running:
ERC4337 Compliance:
Implementation Protection:
Unintended Transaction Failure:
35 hours
#0 - c4-pre-sort
2024-03-22T21:14:48Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-03-27T10:29:54Z
3docSec marked the issue as grade-b