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: 16/51
Findings: 2
Award: $57.62
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xmystery
Also found by: 0xbrett8571, 0xhacksmithh, 7ashraf, Bigsam, Circolors, IceBear, Jorgect, Koala, Limbooo, SBSecurity, Tigerfrake, ZanyBonzy, aycozynfada, cheatc0d3, cryptphi, d3e4, doublespending, foxb868, gpersoon, imare, jesjupyter, lsaudit, robriks, shealtielanz, y4y
36.3397 USDC - $36.34
https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/MultiOwnable.sol#L85-L87 https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/MultiOwnable.sol#L93-L95
MultiOwnable contract allows adding owners by providing an Ethereum address or a WebAuthn public key. However, there is a lack of proper input validation when adding owners, which could potentially lead to the addition of invalid or malformed owner data. This may result in signature validation failures for the affected owners, effectively bricking their ability to execute transactions.
addOwnerAddress
and addOwnerPublicKey
, which are responsible for adding new owners to the SmartWallet contract.
addOwnerAddress
function:
function addOwnerAddress(address owner) public virtual onlyOwner { _addOwner(abi.encode(owner)); }
addOwnerPublicKey
function:
function addOwnerPublicKey(bytes32 x, bytes32 y) public virtual onlyOwner { _addOwner(abi.encode(x, y)); }
Both functions take the owner data (Ethereum address or WebAuthn public key) as input, encode it using abi.encode
, and pass it to the internal _addOwner
function.
The issue lies in the lack of proper input validation when adding owners using the addOwnerAddress
and addOwnerPublicKey
functions.
In the
addOwnerAddress
function, there is no explicit validation to ensure that the providedowner
parameter is a valid Ethereum address. Similarly, in theaddOwnerPublicKey
function, there is no validation to check if the providedx
andy
parameters form a valid WebAuthn public key.
_addOwner(abi.encode(owner));
and #L94
_addOwner(abi.encode(x, y));
These lines encode the owner data without any prior validation and pass it to the _addOwner
function, which adds the owner to the contract's storage.
The expected behavior is that only valid Ethereum addresses and WebAuthn public keys should be accepted as owners when calling the addOwnerAddress
and addOwnerPublicKey
functions, respectively. The functions should perform proper input validation to ensure the integrity and validity of the owner data before adding it to the contract.
Due to the lack of validation, the
addOwnerAddress
andaddOwnerPublicKey
functions allow the addition of invalid or malformed owner data. For example, an invalid Ethereum address or a malformed WebAuthn public key can be added as an owner without any checks or restrictions.
When attempting to execute transactions using the invalid owner data, the signature validation process in the
_validateSignature
function will fail. This is because the_validateSignature
function expects the owner data to be in a specific format (32 bytes for Ethereum address or 64 bytes for WebAuthn public key) and performs certain checks on the data.
If the owner data is invalid or malformed, the signature validation will always fail for that specific owner, effectively bricking their ability to execute transactions.
Owners with invalid Ethereum addresses or malformed WebAuthn public keys will be unable to execute transactions, as the signature validation will always fail for them.
For addOwnerAddress
function:
function addOwnerAddress(address owner) public virtual onlyOwner { + require(owner != address(0), "Invalid Ethereum address"); _addOwner(abi.encode(owner)); }
For addOwnerPublicKey
function:
function addOwnerPublicKey(bytes32 x, bytes32 y) public virtual onlyOwner { + require(isValidWebAuthnPublicKey(x, y), "Invalid WebAuthn public key"); _addOwner(abi.encode(x, y)); } function isValidWebAuthnPublicKey(bytes32 x, bytes32 y) internal pure returns (bool) { // Implement WebAuthn public key validation logic // Example: Check if the point (x, y) is on the secp256r1 curve // ... }
These measures ensure that only valid Ethereum addresses and WebAuthn public keys are accepted as owners. The addOwnerAddress
function checks if the provided address is not the zero address, while the addOwnerPublicKey
function includes a separate validation function (isValidWebAuthnPublicKey
) to check the validity of the WebAuthn public key.
Invalid Validation
#0 - raymondfam
2024-03-22T01:01:08Z
removeOwnerAtIndex
can always be used to counter mistakes done through addOwnerAddress
and addOwnerPublicKey
.
#1 - c4-pre-sort
2024-03-22T01:01:11Z
raymondfam marked the issue as insufficient quality report
#2 - c4-pre-sort
2024-03-22T01:01:15Z
raymondfam marked the issue as primary issue
#3 - 3docSec
2024-03-27T10:00:29Z
QA is more appropriate for user mistakes.
#4 - c4-judge
2024-03-27T10:00:41Z
3docSec changed the severity to QA (Quality Assurance)
#5 - c4-judge
2024-03-27T10:00:45Z
3docSec marked the issue as grade-a
🌟 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
The Coinbase Smart Wallet is an advanced, multi-owner wallet system built on the Ethereum blockchain. It leverages cutting-edge technologies such as WebAuthn authentication and the ERC-4337 standard to provide a secure and flexible solution for managing digital assets. The smart wallet aims to enhance user experience and security by offering features like multiple ownership, cross-chain compatibility, and upgradability.
SmartWallet
, WebAuthnSol
, FreshCryptoLib
, and MagicSpend
components.The Coinbase Smart Wallet utilizes the UUPS (Universal Upgradeable Proxy Standard) pattern for contract upgradability. While this pattern provides flexibility for future enhancements, it also introduces potential risks if not implemented securely.
Risks:
Recommendations:
The Coinbase Smart Wallet consists of several key components:
SmartWallet
: The core contract that manages the wallet's functionality, including owner management, transaction execution, and cross-chain replay protection.MultiOwnable
: A contract that handles the management of multiple owners, allowing them to collectively control the wallet.WebAuthnSol
: A library that integrates WebAuthn authentication into the smart wallet, enabling secure transaction authorization using WebAuthn-compatible devices.FreshCryptoLib
: A library that provides cryptographic functions for signature verification, specifically for the secp256r1 curve used in WebAuthn.MagicSpend
: A component that enables signature-based withdrawals and acts as a paymaster for gas fee management.These components work together to provide a comprehensive and secure smart wallet solution.
graph LR A[User] --> B[SmartWallet] B --> C[MultiOwnable] B --> D[WebAuthnSol] B --> E[FreshCryptoLib] B --> F[MagicSpend] G[EntryPoint] --> B H[Ethereum Network] --> B I[WebAuthn Providers] --> D
The diagram illustrates the high-level components of the Coinbase Smart Wallet and their interactions. The SmartWallet
contract acts as the central component, interacting with the MultiOwnable
, WebAuthnSol
, FreshCryptoLib
, and MagicSpend
contracts. The EntryPoint
contract and the Ethereum Network communicate with the SmartWallet
, while WebAuthn Providers integrate with the WebAuthnSol
library.
graph LR A[User] --> |Owns| B[SmartWallet] B --> |Uses| C[MultiOwnable] C --> |Manages| D[Owners] B --> |Authenticates with| E[WebAuthnSol] E --> |Verifies with| F[FreshCryptoLib] B --> |Withdraws with| G[MagicSpend] H[EntryPoint] --> |Validates and Executes| B I[Ethereum Network] --> |Hosts and Interacts with| B J[WebAuthn Providers] --> |Provides Authentication for| E
The View with diagram provides a more detailed representation of the smart wallet's architecture. It showcases the relationships between the components and the key functionalities they enable. The MultiOwnable
contract manages the wallet owners, the WebAuthnSol
library handles authentication using WebAuthn, the FreshCryptoLib
library performs cryptographic verification, and the MagicSpend
contract facilitates withdrawals. The EntryPoint
contract validates and executes transactions, while the Ethereum Network hosts and interacts with the smart wallet.
The smart wallet allows multiple owners, identified by their Ethereum addresses or WebAuthn public keys, to manage the wallet collectively.
Risks:
Recommendations:
graph LR A[Admin] --> |Deploys and Initializes| B[SmartWallet] A --> |Adds Owners| C[MultiOwnable] A --> |Upgrades Contract| D[UUPSUpgradeable] A --> |Sets Entry Point| E[EntryPoint] A --> |Manages Funds| F[MagicSpend]
The Admin Flow illustrates the actions and responsibilities of the smart wallet administrators. Admins deploy and initialize the SmartWallet
contract, add owners through the MultiOwnable
component, upgrade the contract using the UUPSUpgradeable
pattern, set the EntryPoint
address, and manage funds through the MagicSpend
contract.
Risks:
graph LR A[User] --> |Authenticates with| B[WebAuthn] A --> |Sends Transaction| C[SmartWallet] C --> |Validates Signature| D[WebAuthnSol] D --> |Verifies with| E[FreshCryptoLib] C --> |Executes Transaction| F[Ethereum Network] A --> |Withdraws Funds| G[MagicSpend]
The User Flow showcases the typical user interactions with the Coinbase Smart Wallet. Users authenticate using WebAuthn, send transactions to the SmartWallet
contract, which validates the signature using the WebAuthnSol
library and FreshCryptoLib
for verification. The SmartWallet
then executes the transaction on the Ethereum Network. Users can also withdraw funds through the MagicSpend
contract.
Risks:
MagicSpend
contract if not properly validated.The codebase follows a modular structure, separating concerns into different contracts and libraries. The code is well-commented and follows a consistent naming convention, enhancing readability and maintainability.
However, some areas for improvement include:
The smart wallet integrates external libraries and contracts, such as Solady's Ownable
and UUPSUpgradeable
, WebAuthnSol
, and ERC1271
.
Risks:
Recommendations:
graph LR A[SmartWallet] --> |Calls| B[MultiOwnable] A --> |Calls| C[WebAuthnSol] C --> |Uses| D[FreshCryptoLib] A --> |Calls| E[MagicSpend] F[EntryPoint] --> |Validates and Calls| A A --> |Executes on| G[Ethereum Network]
Illustrates the interactions between the core contracts of the Coinbase Smart Wallet. The SmartWallet
contract acts as the central hub, calling the MultiOwnable
contract for owner management, the WebAuthnSol
library for authentication (which in turn uses FreshCryptoLib
for cryptographic operations), and the MagicSpend
contract for withdrawals. The EntryPoint
contract validates and calls the SmartWallet
, which executes transactions on the Ethereum Network.
Risks:
graph LR A[SmartWallet] --> |Centralization Risk| B[Owner Control] A --> |Systematic Risk| C[Ethereum Network] A --> |Mechanism Review| D[Access Control] A --> |Mechanism Review| E[Signature Validation] A --> |Mechanism Review| F[Withdraw Requests] A --> |Mechanism Review| G[Cross-Chain Replay Protection]
The Contract focuses on the key areas of analysis for the Coinbase Smart Wallet:
The codebase includes various error handling mechanisms, such as custom error types and require statements, to validate input and handle exceptional scenarios.
However, there are areas where error handling and validation can be improved:
The smart wallet grants significant privileges to the owners, including the ability to add/remove owners, upgrade contracts, and execute transactions.
Risks:
Recommendations:
The smart wallet does not have a dedicated admin role, but the owners collectively act as the administrators of the wallet.
Risks:
Recommendations:
The smart wallet supports both Ethereum-based signatures (ERC-1271) and WebAuthn authentication for transaction authorization.
onlyOwner
modifier, to restrict certain actions to authorized owners. Thorough review and testing of these mechanisms are crucial to ensure their effectiveness.
MagicSpend
contract handles withdraw requests, which need to be carefully validated to prevent invalid or malicious withdrawals.Risks:
Recommendations:
The smart wallet includes a mechanism to prevent cross-chain replay attacks by using a unique identifier (REPLAYABLE_NONCE_KEY
) for transactions that are allowed to be replayed across chains.
Risks:
Recommendations:
The smart wallet relies on external systems, such as the Ethereum network and WebAuthn authentication providers, for its functionality.
Risks:
Recommendations:
One of the primary risks associated with the smart wallet is the potential for unauthorized fund movement. Malicious actors may attempt to exploit vulnerabilities in the contract code or gain unauthorized access to owner accounts to transfer funds without permission.
Mitigation:
onlyOwner
modifier, to ensure that only authorized owners can initiate fund transfers.execute
and executeBatch
functions, which are used to execute transactions, are protected by the onlyEntryPointOrOwner
modifier, restricting access to the EntryPoint contract and wallet owners.function execute(address target, uint256 value, bytes calldata data) public payable virtual onlyEntryPointOrOwner { _call(target, value, data); }
Another risk is the possibility of an attacker bricking the smart wallet, rendering it unusable. This could occur if an attacker gains control of the wallet and removes all owners or performs malicious actions that make the wallet inaccessible.
Mitigation:
removeOwnerAtIndex
function checks if removing an owner would leave the wallet without any owners and reverts the transaction if true.function removeOwnerAtIndex(uint256 index) public virtual onlyOwner { bytes memory owner = ownerAtIndex(index); if (owner.length == 0) revert NoOwnerAtIndex(index); // Check if removing the owner would leave no owners if (ownerCount() == 1) revert CannotRemoveLastOwner(); delete _getMultiOwnableStorage().isOwner[owner]; delete _getMultiOwnableStorage().ownerAtIndex[index]; emit RemoveOwner(index, owner); }
The smart wallet supports cross-chain transactions, allowing certain functions to be executed without chain ID validation. This feature introduces the risk of cross-chain replay attacks, where an attacker could replay a transaction on multiple chains, leading to unintended consequences.
Mitigation:
REPLAYABLE_NONCE_KEY
. Transactions that are allowed to be replayed across chains are identified using this unique key.validateUserOp
function checks the REPLAYABLE_NONCE_KEY
and ensures that only authorized functions can be executed without chain ID validation.function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) public payable virtual onlyEntryPoint payPrefund(missingAccountFunds) returns (uint256 validationData) { uint256 key = userOp.nonce >> 64; // 0xbf6ba1fc = bytes4(keccak256("executeWithoutChainIdValidation(bytes)")) if (userOp.callData.length >= 4 && bytes4(userOp.callData[0:4]) == 0xbf6ba1fc) { userOpHash = getUserOpHashWithoutChainId(userOp); if (key != REPLAYABLE_NONCE_KEY) { revert InvalidNonceKey(key); } } else { if (key == REPLAYABLE_NONCE_KEY) { revert InvalidNonceKey(key); } } // Return 0 if the recovered address matches the owner. if (_validateSignature(userOpHash, userOp.signature)) { return 0; } // Else return 1, which is equivalent to: // `(uint256(validAfter) << (160 + 48)) | (uint256(validUntil) << 160) | (success ? 0 : 1)` // where `validUntil` is 0 (indefinite) and `validAfter` is 0. return 1; }
The function starts by extracting the key
from the userOp.nonce
. If the userOp.callData
targets the executeWithoutChainIdValidation
function (identified by the function selector 0xbf6ba1fc
), it checks that the key
matches the REPLAYABLE_NONCE_KEY
. Otherwise, it ensures that the key
is not the REPLAYABLE_NONCE_KEY
.
The main signature validation happens in the _validateSignature
function:
function _validateSignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool) { SignatureWrapper memory sigWrapper = abi.decode(signature, (SignatureWrapper)); bytes memory ownerBytes = ownerAtIndex(sigWrapper.ownerIndex); if (ownerBytes.length == 32) { if (uint256(bytes32(ownerBytes)) > type(uint160).max) { // technically should be impossible given owners can only be added with // addOwnerAddress and addOwnerPublicKey, but we leave incase of future changes. revert InvalidEthereumAddressOwner(ownerBytes); } address owner; assembly ("memory-safe") { owner := mload(add(ownerBytes, 32)) } return SignatureCheckerLib.isValidSignatureNow(owner, message, sigWrapper.signatureData); } if (ownerBytes.length == 64) { (uint256 x, uint256 y) = abi.decode(ownerBytes, (uint256, uint256)); WebAuthn.WebAuthnAuth memory auth = abi.decode(sigWrapper.signatureData, (WebAuthn.WebAuthnAuth)); return WebAuthn.verify({challenge: abi.encode(message), requireUV: false, webAuthnAuth: auth, x: x, y: y}); } revert InvalidOwnerBytesLength(ownerBytes); }
The function decodes the signature as a SignatureWrapper
, which contains the ownerIndex
and signatureData
. It retrieves the ownerBytes
using the ownerAtIndex
function.
If the ownerBytes
length is 32, it treats it as an Ethereum address and validates the signature using SignatureCheckerLib.isValidSignatureNow
.
If the ownerBytes
length is 64, it treats it as a WebAuthn public key (x, y) and validates the signature using WebAuthn.verify
.
Edge cases and considerations:
ownerBytes
of length 32 represents a valid Ethereum address. It includes a check to ensure the address fits within uint160
, but there could be other types of invalid addresses that pass this check.ownerBytes
of length 64 represents a valid WebAuthn public key. It doesn't perform additional validation on the public key format or values.abi.decode
to decode the signature
and signatureData
. If the provided data doesn't match the expected format, it could lead to unexpected behavior or reverts.WebAuthn.verify
function is called with requireUV: false
, meaning it doesn't enforce user verification. This is a design choice, but it's worth considering if user verification should be required for certain operations.1
if the signature validation fails, which is a specific format expected by the EntryPoint. If the EntryPoint changes its format, this function would need to be updated accordingly.To ensure the robustness of the validateUserOp
function, consider the following:
ownerAtIndex
function to ensure it correctly retrieves owner data and handles edge cases like out-of-bounds indices.SignatureCheckerLib
and WebAuthn
libraries are secure and correctly integrated.ownerBytes
and signature
data.The MagicSpend
component allows users to withdraw funds using signed withdraw requests. There is a risk of attackers attempting to withdraw funds using invalid or malicious withdraw requests.
Mitigation:
MagicSpend
contract includes thorough validation checks for withdraw requests, ensuring that only valid and authorized requests are processed.validatePaymasterUserOp
function verifies the signature of the withdraw request and checks the expiry and nonce to prevent replay attacks.function validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256 maxCost) external onlyEntryPoint returns (bytes memory context, uint256 validationData) { WithdrawRequest memory withdrawRequest = abi.decode(userOp.paymasterAndData[20:], (WithdrawRequest)); uint256 withdrawAmount = withdrawRequest.amount; if (withdrawAmount < maxCost) { revert RequestLessThanGasMaxCost(withdrawAmount, maxCost); } if (withdrawRequest.asset != address(0)) { revert UnsupportedPaymasterAsset(withdrawRequest.asset); } _validateRequest(userOp.sender, withdrawRequest); bool sigFailed = !isValidWithdrawSignature(userOp.sender, withdrawRequest); validationData = (sigFailed ? 1 : 0) | (uint256(withdrawRequest.expiry) << 160); // Ensure at validation that the contract has enough balance to cover the requested funds. // NOTE: This check is necessary to enforce that the contract will be able to transfer the remaining funds // when `postOp()` is called back after the `UserOperation` has been executed. 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; context = abi.encode(maxCost, userOp.sender); }
The smart wallet relies on the WebAuthnSol
and FreshCryptoLib
libraries for secure transaction authorization using WebAuthn authentication. False positives or false negatives in the validation process could lead to unauthorized access or denial of legitimate transactions.
Mitigation:
WebAuthnSol
library follows the WebAuthn specification closely, implementing the necessary validation steps to ensure the authenticity of WebAuthn assertions.FreshCryptoLib
library has been audited and tested extensively to ensure the correctness of the cryptographic functions used for signature verification.function verify(bytes memory challenge, bool requireUV, WebAuthnAuth memory webAuthnAuth, uint256 x, uint256 y) internal view returns (bool) { // ... if (webAuthnAuth.authenticatorData[32] & AUTH_DATA_FLAGS_UP != AUTH_DATA_FLAGS_UP) { return false; } if (requireUV && (webAuthnAuth.authenticatorData[32] & AUTH_DATA_FLAGS_UV) != AUTH_DATA_FLAGS_UV) { return false; } // ... }
As the usage of the smart wallet grows, scalability and performance considerations become crucial.
Risks:
Recommendations:
function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4 result) { if (_validateSignature(SignatureCheckerLib.toEthSignedMessageHash(hash), signature)) { // bytes4(keccak256("isValidSignature(bytes32,bytes)")) return 0x1626ba7e; } return 0xffffffff; }
This function is the entry point for ERC-1271 signature validation. It takes a hash
and a signature
as input, converts the hash
to an Ethereum signed message hash using SignatureCheckerLib.toEthSignedMessageHash
, and then calls the internal _validateSignature
function with the modified hash and the signature. If the signature is valid, it returns the ERC-1271 magic value 0x1626ba7e
; otherwise, it returns 0xffffffff
.
function _validateSignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool) { SignatureWrapper memory sigWrapper = abi.decode(signature, (SignatureWrapper)); bytes memory ownerBytes = ownerAtIndex(sigWrapper.ownerIndex); if (ownerBytes.length == 32) { if (uint256(bytes32(ownerBytes)) > type(uint160).max) { // technically should be impossible given owners can only be added with // addOwnerAddress and addOwnerPublicKey, but we leave incase of future changes. revert InvalidEthereumAddressOwner(ownerBytes); } address owner; assembly ("memory-safe") { owner := mload(add(ownerBytes, 32)) } return SignatureCheckerLib.isValidSignatureNow(owner, message, sigWrapper.signatureData); } if (ownerBytes.length == 64) { (uint256 x, uint256 y) = abi.decode(ownerBytes, (uint256, uint256)); WebAuthn.WebAuthnAuth memory auth = abi.decode(sigWrapper.signatureData, (WebAuthn.WebAuthnAuth)); return WebAuthn.verify({challenge: abi.encode(message), requireUV: false, webAuthnAuth: auth, x: x, y: y}); } revert InvalidOwnerBytesLength(ownerBytes); }
This function is responsible for validating the signature based on the signature type (ERC-1271 or WebAuthn). It takes a message
(hash) and a signature
as input. The signature
is decoded as a SignatureWrapper
struct, which contains the ownerIndex
and signatureData
.
If the ownerBytes
length is 32, it treats it as an Ethereum address. It checks if the ownerBytes
can be converted to a valid Ethereum address (uint160
). If the conversion fails, it reverts with an InvalidEthereumAddressOwner
error. Otherwise, it extracts the Ethereum address from ownerBytes
using assembly and validates the signature using SignatureCheckerLib.isValidSignatureNow
.
If the ownerBytes
length is 64, it treats it as a WebAuthn public key. It decodes the ownerBytes
into x
and y
coordinates and decodes the signatureData
as a WebAuthn.WebAuthnAuth
struct. It then calls the WebAuthn.verify
function to validate the signature using the provided message
, x
, y
, and webAuthnAuth
.
If the ownerBytes
length is neither 32 nor 64, it reverts with an InvalidOwnerBytesLength
error.
Edge cases and considerations:
isValidSignature
function assumes that the provided hash
is a valid 32-byte hash. If the hash
is not 32 bytes or is not a valid hash, the behavior may be unexpected._validateSignature
function relies on the ownerAtIndex
function to retrieve the owner bytes based on the provided ownerIndex
. If the ownerIndex
is invalid or the ownerAtIndex
function has vulnerabilities, it could affect the signature validation process._validateSignature
only checks if the ownerBytes
can be converted to a uint160
. There could be other types of invalid addresses that pass this check._validateSignature
relies on the correctness and security of the WebAuthn.verify
function. Any vulnerabilities or weaknesses in the WebAuthn library could impact the signature validation._validateSignature
function uses abi.decode
to decode the signature
and signatureData
. If the provided data doesn't match the expected format, it could lead to unexpected behavior or reverts.To ensure the robustness and security of the signature validation process:
ownerAtIndex
function to ensure it retrieves owner data correctly and securely.uint160
conversion.WebAuthn.verify
function and the WebAuthn library to ensure their correctness and security.signature
and signatureData
using abi.decode
.addOwnerAddress
, addOwnerPublicKey
, and removeOwnerAtIndex
functions to verify proper access control and ensure that an owner cannot accidentally or maliciously remove all owners, making the account inaccessible.function addOwnerAddress(address owner) public virtual onlyOwner { _addOwner(abi.encode(owner)); }
This function allows adding a new owner by providing an Ethereum address. It uses the onlyOwner
modifier, which ensures that only an existing owner can call this function. The provided owner
address is encoded using abi.encode
and then passed to the internal _addOwner
function.
function addOwnerPublicKey(bytes32 x, bytes32 y) public virtual onlyOwner { _addOwner(abi.encode(x, y)); }
This function allows adding a new owner by providing a WebAuthn public key (represented by x
and y
coordinates). Similar to addOwnerAddress
, it uses the onlyOwner
modifier to restrict access to only existing owners. The x
and y
coordinates are encoded using abi.encode
and then passed to the internal _addOwner
function.
function removeOwnerAtIndex(uint256 index) public virtual onlyOwner { bytes memory owner = ownerAtIndex(index); if (owner.length == 0) revert NoOwnerAtIndex(index); delete _getMultiOwnableStorage().isOwner[owner]; delete _getMultiOwnableStorage().ownerAtIndex[index]; emit RemoveOwner(index, owner); }
This function allows removing an owner at a specific index. It also uses the onlyOwner
modifier to ensure only an existing owner can call it. The function retrieves the owner bytes at the given index
using the ownerAtIndex
function. If the owner
bytes are empty (length == 0), it reverts with a NoOwnerAtIndex
error. Otherwise, it deletes the owner from the isOwner
mapping and the ownerAtIndex
mapping, effectively removing the owner from the contract.
Access Control:
onlyOwner
modifier is used in all three functions, ensuring that only existing owners can add or remove owners.onlyOwner
modifier is implemented as follows: https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/MultiOwnable.sol#L77-L80
https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/MultiOwnable.sol#L201-L207modifier onlyOwner() virtual { _checkOwner(); _; } function _checkOwner() internal view virtual { if (isOwnerAddress(msg.sender) || (msg.sender == address(this))) { return; } revert Unauthorized(); }
It checks if the caller is an owner address or the contract itself. If not, it reverts with an Unauthorized
error.
Preventing Removal of All Owners:
removeOwnerAtIndex
function allows removing an owner at a specific index. However, there is no explicit check to prevent removing the last remaining owner.removeOwnerAtIndex
and removes all owners, the contract would become inaccessible since there would be no owners left to perform any operations.removeOwnerAtIndex
function to ensure that at least one owner remains after the removal. For example:
This check would prevent removing the last owner and revert with afunction removeOwnerAtIndex(uint256 index) public virtual onlyOwner { // ... if (_getMultiOwnableStorage().currentOwnerCount == 1) revert CannotRemoveLastOwner(); // ... }
CannotRemoveLastOwner
error.Edge Cases and Considerations:
addOwnerAddress
and addOwnerPublicKey
functions do not check for duplicate owners. If the same owner address or public key is added multiple times, it could lead to inconsistencies and unnecessary storage usage.removeOwnerAtIndex
function relies on the ownerAtIndex
function to retrieve the owner bytes. If the ownerAtIndex
function has vulnerabilities or returns unexpected values, it could impact the removal process.removeOwnerAtIndex
function emits a RemoveOwner
event, but there are no corresponding events for addOwnerAddress
and addOwnerPublicKey
. Consider adding events for consistency and transparency.To further enhance the security and robustness of the owner management functions:
addOwnerAddress
and addOwnerPublicKey
.ownerAtIndex
function is thoroughly tested and audited for correctness and security.addOwnerAddress
and addOwnerPublicKey
to provide transparency and allow monitoring of owner changes.UUPSUpgradeable.upgradeToAndCall
to ensure it is secure and prevents a malicious owner from upgrading to a faulty implementation contract that could brick the account.upgradeToAndCall
function:
The upgradeToAndCall
function is inherited from the UUPSUpgradeable
contract provided by the Solady library. Here's the relevant code snippet from the Solady library:function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy { _authorizeUpgrade(newImplementation); _upgradeToAndCallUUPS(newImplementation, data, true); }
The function takes two parameters:
newImplementation
: The address of the new implementation contract to upgrade to.data
: Additional data to be passed to the new implementation contract during the upgrade process.The function performs the following steps:
_authorizeUpgrade
function to check if the caller is authorized to perform the upgrade._upgradeToAndCallUUPS
function to execute the actual upgrade process and pass the additional data to the new implementation contract._authorizeUpgrade
function:
In the SmartWallet contract, the _authorizeUpgrade
function is overridden to implement the access control for upgrades. Here's the relevant code snippet: https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/CoinbaseSmartWallet.sol#L330function _authorizeUpgrade(address) internal view virtual override(UUPSUpgradeable) onlyOwner {}
The _authorizeUpgrade
function is marked with the onlyOwner
modifier, which ensures that only an owner of the SmartWallet contract can authorize an upgrade. If a non-owner tries to initiate an upgrade, the onlyOwner
modifier will revert the transaction.
The upgradeToAndCall
function itself is secure as it inherits the implementation from the Solady library, which has been audited and is widely used.
The access control for upgrades is properly implemented using the onlyOwner
modifier in the _authorizeUpgrade
function. This ensures that only current owners of the SmartWallet contract can initiate an upgrade.
However, it's important to note that if a malicious owner is present among the current owners, they could potentially initiate an upgrade to a faulty implementation contract that could brick the account.
To mitigate this risk, consider implementing additional safeguards, such as:
_authorizeUpgrade
function does not perform any additional checks on the newImplementation
address. A malicious owner could potentially upgrade to a contract that is not a valid implementation or contains unexpected behavior._authorizeUpgrade
function to verify the validity and integrity of the newImplementation
contract before allowing the upgrade.While the current implementation of the upgrade functionality using UUPSUpgradeable.upgradeToAndCall
provides basic security through the onlyOwner
access control, it is important to consider the risks associated with malicious owners and implement additional safeguards to mitigate those risks.
By implementing multiple owner approvals, timelock, emergency stop mechanisms, and thorough testing and auditing of new implementation contracts, the security of the upgrade process can be significantly enhanced, reducing the chances of a malicious upgrade that could brick the account.
The Coinbase Smart Wallet codebase demonstrates a well-structured and modular design, with a focus on security and extensibility. However, there are potential risks and areas for improvement that should be addressed to enhance the wallet's overall security and reliability.
The Coinbase Smart Wallet implements various security measures and best practices to mitigate potential risks and vulnerabilities. However, it is essential to continuously monitor and audit the system to identify and address any emerging threats. Regular security assessments, code reviews, and testing should be conducted to ensure the ongoing security and reliability of the smart wallet.
By understanding the architecture, risk assessment, and mitigation strategies, developers and users can make informed decisions when interacting with the Coinbase Smart Wallet. The provided code snippets demonstrate the implementation of critical security features and serve as a reference for further analysis and improvement.
Key recommendations include:
By addressing these recommendations and continuously monitoring and updating the smart wallet system, Coinbase can provide a secure and trustworthy solution for users to manage their digital assets.
38 hours
#0 - c4-pre-sort
2024-03-22T21:17:29Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-03-27T10:36:56Z
3docSec marked the issue as grade-b