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: 1/51
Findings: 3
Award: $8,800.83
🌟 Selected for report: 1
🚀 Solo Findings: 0
8743.2065 USDC - $8,743.21
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L102
Impact
Users are able to upgrade their account's owners via either directly onto the contract with a regular transaction or via an ERC-4337 EntryPoint transaction calling executeWithoutChainIdValidation
. If a user chooses to use a combination of these methods it's very likely that the addresses at a particular ownership index differ across chain. Therefore if a user later calls removeOwnerAtIndex
on another chain will end up removing different addresses on different chains. It is unlikely this would be the users intention. The severity of this ranges from minimal (the user can just add the mistakenly removed owner back) or criticial (the user mistakenly removes their only accessible owner on a specific chain, permanently locking the account).
Proof Of Concept
Scenario A: Alice permanently bricks her account on an unused chain:
executeWithoutChainIdValidation
Scenario A: Alice adds owners using both methods and ends up with undesired results
execute
.executeWithoutChainIdValidation
executeWithoutChainIdValidation
to call removeOwnerAtInde
she will be removing different owners on different chains, which is likely not her intention.While more complex scenarios than this might sound bizarre it's important to remember that Alice could be using this smart account for the next N years, only making changes sporadically, and as her ownership mappings across different chains become more out of sync the likelihood of a signifanct error occuring increases.
Recommended Mitigation
As MultiOwnableStorage
uses a mapping to track owner information rather than a conventional array, it might be simpler to do away with the indexes entirely and have a removeOwner(bytes calldata _ownerToRemove)
function. This would avoid the sitations outlined above where when calling removeOwnerAtIndex
removes different owners on different chains. To ensure replayability and avoid having a stuck nonce on chains where _ownerToRemove
is not an owner the function should not revert in the case the owner is not there, but instead return a bool removed
to indicate whether an owner was removed or not.
This would make it significantly less likely that users run into the issues stated above, without having to limit their freedom to make ownership changes manually or via ERC-4337 EntryPoint transactions.
Other
#0 - c4-pre-sort
2024-03-22T16:22:01Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-22T16:22:11Z
raymondfam marked the issue as duplicate of #46
#2 - raymondfam
2024-03-22T16:23:10Z
See #46.
#3 - c4-pre-sort
2024-03-22T23:17:57Z
raymondfam marked the issue as duplicate of #68
#4 - c4-judge
2024-03-27T04:44:46Z
3docSec marked the issue as not a duplicate
#5 - c4-judge
2024-03-27T04:47:44Z
3docSec changed the severity to QA (Quality Assurance)
#6 - c4-judge
2024-03-27T05:41:26Z
This previously downgraded issue has been upgraded by 3docSec
#7 - c4-judge
2024-03-27T05:42:09Z
3docSec marked the issue as duplicate of #88
#8 - c4-judge
2024-03-27T05:43:41Z
3docSec marked the issue as not a duplicate
#9 - c4-judge
2024-03-27T05:45:38Z
3docSec marked the issue as primary issue
#10 - 3docSec
2024-03-27T07:26:14Z
Root cause: removeOwnerAtIndex
can be replayed cross-chain despite the same index may point to a different owner. The issue was confirmed by the sponsor in the #57 thread and addressed in this PR.
#11 - c4-judge
2024-03-27T07:26:33Z
3docSec marked the issue as satisfactory
#12 - c4-judge
2024-03-27T13:46:53Z
3docSec marked the issue as selected for report
#13 - 3docSec
2024-03-27T18:04:35Z
After consulting with a fellow judge, I'm upgrading this one as high, as there is a well-defined attack path that causes the user to lose ownership of their wallet.
#14 - c4-judge
2024-03-27T18:04:56Z
3docSec changed the severity to 3 (High Risk)
#15 - c4-judge
2024-03-28T08:16:52Z
3docSec changed the severity to QA (Quality Assurance)
#16 - McCoady
2024-03-28T08:31:32Z
@3docSec was this downgraded mistakenly when de-duping #170 or for another reason?
#17 - 3docSec
2024-03-28T08:41:54Z
Totally, I will report the issue to the C4 team, this is a technical issue, thanks for flagging @McCoady
#18 - c4-judge
2024-03-28T08:42:23Z
This previously downgraded issue has been upgraded by 3docSec
#19 - thebrittfactor
2024-04-25T13:49:30Z
Note: The sponsor requested to have the title of this finding updated to appropriately reflect the issue. The judge (3docSec) has reviewed and agreed to the change.
Final report title:
Remove owner calls can be replayed to remove a different owner at the same index, leading to severe issues when combined with lack of last owner guard
🌟 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/main/src/SmartWallet/CoinbaseSmartWallet.sol#L104-L106 https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L85
Impact
There are no checks in MultiOwnable::addOwnerAddress
to stop someone adding the zero address as an owner. While typically this would not be a significant issue, in the case of this codebase if address(0)
is an owner CoinbaseSmartWallet::_validateSignature
will always return true regardless of the signature if SignatureWrapper.ownerIndex
is the index of the zero address.
This means that a malicious user would be able to use EntryPoint transactions to cause significant damage to a users account (steal funds, change owners, upgrade the proxy implementation).
This is currently the case in the CoinbaseSmartWallet
proxy implementation contract as address(0)
is mistakenly added as an owner in an attempt to make the proxy implementation inoperable, but instead has the exact same effect. This would escalate the issue severity to critical if it were possible to do something such as upgrade the implementation contract's implementation, putting all CoinbaseSmartWallet
instances that use this implementation contract at risk.
Proof of Concept
The following shows the control flow outlining how any instance where address(0)
is a viable owner will result in the signature being validated:
Recommended Mitigation
Given the severity of the damage should the zero address be made an owner, and that the likelihood of the issue occurring is scaled up by the number of users using a CoinbaseSmartWallet
, the project should implement sufficient address(0)
checks when MultiOwnable::addOwnerAddress
to protect their users from a potentially costly error.
On top of this the CoinbaseSmartWallet
constructor should not pass address(0)
as an owner when calling _initializeOwners
and should instead use another address such as "0x000000000000000000000000000000000000dEaD" top stop users being able to make arbitrary calls via the proxy implementation contract.
Other
#0 - raymondfam
2024-03-22T06:38:57Z
See #90.
#1 - c4-pre-sort
2024-03-22T06:39:04Z
raymondfam marked the issue as insufficient quality report
#2 - c4-pre-sort
2024-03-22T06:39:11Z
raymondfam marked the issue as duplicate of #90
#3 - 3docSec
2024-03-27T05:08:40Z
Interesting finding. If we exclude user errors, it is safe to assume that wallets created by the factory are initialized with non-zero addresses because the factory creates and initializes wallets in the same call.
The only exception to the above is the implementation contract, that has its storage initialized in the constructor. For this one, the only consequence I can imagine is that the implementation wallet can accept UserOperations
from anybody. This does not seem to be a big deal, apart from self-calls to upgradeToAndCall
which are however secured by the onlyProxy
modifier.
The similar finding (this and this) pointed by #90 as having previously been judged valid are much more impactful because they open a viable path for destructing the implementation - this is not true for this codebase.
Leaving this as QA because having address(1)
as the implementation owner is a sound recommendation that would prevent the implementation contract from accepting UserOperations
with malformed signatures.
#4 - c4-judge
2024-03-27T05:09:18Z
3docSec changed the severity to QA (Quality Assurance)
#5 - c4-judge
2024-03-27T09:09:01Z
3docSec marked the issue as grade-a
#6 - wilsoncusack
2024-04-01T18:01:49Z
This is an interesting one, thanks for bring it up. This would have been very concerning if there were a way for the implementation to delegatecall, and an attacker could selfdestructed the contract. I tried poc here, trying to call upgradeToAndCall and you get the error UnauthorizedCallContext
from Solady UUPS.
#7 - wilsoncusack
2024-04-01T18:47:38Z
Looking further, I do not think this finding is correct re Solady behavior https://github.com/Vectorized/solady/blob/main/test/SignatureCheckerLib.t.sol#L68-L70
🌟 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
When undertaking the review of the Coinbase Smart Wallet protocol the following steps were taken:
A total of approximately 30 hours was spent conducting the security review of this codebase over a 7 day period.
The Coinbase Smart Wallet protocol is a combination of codebases that function together to create an ERC-4337 compliant smart account and paymaster contract. Two main distinctions set this protocol apart from other ERC-4337 implementations:
The codebase contains logic to allow cross chain replayable signatures, allowing users to perform admin functions (such as adding/removing owners or upgrading their contracts logic implementation) across all supported chains with a single signature. This is a move to improve a users experience when managing their account on multiple chains.
The codebase supports users being able to give their account multiple different owners. Furthermore the owners of an account can be either EVM addresses or passkey (Secp256r1) public keys.
Entities in the protocol fall primarily into one of three roles:
As a smart contract account protocol the main intended user is the owner(s) of the account. Owners can either be Ethereum address or a Secp256r1 passkey.
Owners can interact with their wallets in the following ways:
This role will be fulfilled by Coinbase and will require they always have enough Ether in the contract to cover the withdrawable
amounts users as signed off to use. If/when ERC20 tokens become supported assets, the same will apply them too.
The transaction batcher is responsible for taking the UserOperations
and putting them on chain. This role may initially be fulfilled by Coinbase themselves but could also be done by any user with access to "mempool" of Coinbase users UserOperations
.
The protocol's code is separated into four distinct parts:
Smart Wallet represents the key logic associated with the ERC-4337 compliant Smart Wallet, the multiple owner functionality and the ability to validate cross chain replayable signatures.
Magic Spend provides the key functionality of the codebases ERC-4337 paymaster implementation, which will allow users to cover their gas fees using their balances on the Coinbase exchange.
WebAuthnSol is a library for verifying WebAuthn assertations. In the context of this codebase it is used to verify the signature of an ERC-4337 EntryPoint transactions where the signer of that transactions is a Secp256r1 passkey.
The FreshCryptoLib library's ecdsa_verify
function is used by the WebAuthnSol library when verifying passkey signatures.
The following gives a general overview of the logic flow when completing an ERC-4337 EntryPoint transaction on a users CoinbaseSmartWallet
where gas is covered by the MagicSpend
paymaster:
The protocol has made the design choice to allow specific function calls to be permittable to be replayed across different EVM chains. This is a choice to improve user experience and allow them to update an account's ownership status or upgrade the account's implementation logic.
However the team should be aware of a number of pitfalls users may run into because of this. Below are a few examples:
removeOwnerAtIndex
as there's no guarantee that the same owner will be at that index on all chains.address(0)
ownersThe protocol team should be aware the Solady implementation of isValidSignatureNow
does not return false in the event that the address passed to it is the zero address. This means that all signatures will be deemed valid if the zero address is an owner. This is an issue for this codebase for two reasons:
CoinbaseSmartWallet
constructor currently sets the proxy implementation's owner to address(0)
The codebase appears to be sufficiently decentralised. The only ownership the protocol has over the contract is that it will be in charge of the Magic Spend
paymaster contract. This seems like a necessary exception to make as they will be required to add/remove funds from the contract as well as move them to the EntryPoint contract, all of which will require some amount of access control.
The primary code complexity from this codebase is mostly unavoidable as it stems from it's integration with ERC-4337. However the codebase handles this integration well. Overall the codebase is concise and avoids ununecessary complication.
The codebase includes complete NATSPEC comments as well as additional inline comments providing necessary context for certain aspects. On top of this the provided documentation explains the system well, however there is some incorrect information such as non-existant functions being listed as being able to skip chain id validation here. Finally the Coinbase/Base team have provided user friendly guides explaining the protocol, in the original announcement article as well as the following video.
The codebase has good test coverage, but has room for improvement. One example of this would be to add a more complete integration testing suite to ensure the contracts are working as intended with both the deployed EntryPoint
instance and interacts as intended when other smart contract accounts are owners and vice versa (Gnosis Safe, ERC6551 token bound accounts). However as the contract are currently being actively used on Base Sepolia testnet, this may make up for some of the missing edge case coverage.
30 hours
#0 - c4-pre-sort
2024-03-22T21:20:09Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-03-27T12:22:49Z
3docSec marked the issue as grade-b