Coinbase Smart Wallet - cryptphi's results

Smart Wallet from Coinbase Wallet

General Information

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

Coinbase

Findings Distribution

Researcher Performance

Rank: 49/51

Findings: 1

Award: $6.95

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

6.9492 USDC - $6.95

Labels

bug
downgraded by judge
grade-b
QA (Quality Assurance)
satisfactory
sufficient quality report
edited-by-warden
:robot:_08_group
duplicate-18
Q-06

External Links

Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/CoinbaseSmartWallet.sol#L180-L187 https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/CoinbaseSmartWallet.sol#L252-L262 https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L102-L110

Vulnerability details

Impact

The removeOwnerAtIndex() in MultiOwnable contract removes an owner from the given index of the MUTLI_OWNABLE_STORAGE_LOCATION storage slot in the Contract. This function has an access control in the modifier onlyOwner to ensure that the caller is a registered owner or the contract itself.

However, through the executeWithoutChainIdValidation() in CoinbaseSmartWallet contract, an owner can remove other owners from the contract.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.23; import {Test, console2, stdError} from "forge-std/Test.sol"; import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import "../mocks/MockMultiOwnable.sol"; import "../../../src/SmartWallet/CoinbaseSmartWallet.sol"; import {MockCoinbaseSmartWallet} from "../mocks/MockCoinbaseSmartWallet.sol"; import {Static} from "./Static.sol"; contract RemoveTest is Test { CoinbaseSmartWallet public account = new MockCoinbaseSmartWallet(); MockMultiOwnable mock = new MockMultiOwnable(); uint256 signerPrivateKey = 0xa11ce; address owner1Address = vm.addr(signerPrivateKey); bytes owner1Bytes = abi.encode(owner1Address); // public key x,y bytes owner2Bytes = abi.encode( 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4, 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437 ); bytes[] owners; IEntryPoint entryPoint = IEntryPoint(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789); address bundler = address(uint160(uint256(keccak256(abi.encodePacked("bundler"))))); // userOp values uint256 userOpNonce; bytes userOpCalldata; function setUp() public virtual { owners.push(owner1Bytes); owners.push(owner2Bytes); vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES); account.initialize(owners); userOpNonce = account.REPLAYABLE_NONCE_KEY() << 64; userOpCalldata = abi.encodeWithSelector(CoinbaseSmartWallet.executeWithoutChainIdValidation.selector); } function testRemovesOwnerThroughEndPoint() public { //owner1 removes owner2 through entryPoint userOpCalldata = abi.encodeWithSelector( CoinbaseSmartWallet.executeWithoutChainIdValidation.selector, abi.encodeWithSelector(MultiOwnable.removeOwnerAtIndex.selector, 1) ); _sendUserOperation(_getUserOpWithSignature()); assertFalse(account.isOwnerBytes(owner2Bytes)); //owner1 removes itself through entryPoint userOpNonce++; userOpCalldata = abi.encodeWithSelector( CoinbaseSmartWallet.executeWithoutChainIdValidation.selector, abi.encodeWithSelector(MultiOwnable.removeOwnerAtIndex.selector, 0) ); _sendUserOperation(_getUserOpWithSignature()); assertFalse(account.isOwnerBytes(owner1Bytes)); } function _sendUserOperation(UserOperation memory userOp) internal { UserOperation[] memory ops = new UserOperation[](1); ops[0] = userOp; entryPoint.handleOps(ops, payable(bundler)); } function _getUserOp() internal view returns (UserOperation memory userOp) { userOp = UserOperation({ sender: address(account), nonce: userOpNonce, initCode: "", callData: userOpCalldata, callGasLimit: uint256(1_000_000), verificationGasLimit: uint256(1_000_000), preVerificationGas: uint256(0), maxFeePerGas: uint256(0), maxPriorityFeePerGas: uint256(0), paymasterAndData: "", signature: "" }); } function _getUserOpWithSignature() internal view returns (UserOperation memory userOp) { userOp = _getUserOp(); userOp.signature = _sign(userOp); } function _sign(UserOperation memory userOp) internal view returns (bytes memory signature) { bytes32 toSign = account.getUserOpHashWithoutChainId(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, toSign); signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v))); } }

Tools Used

Manual review

Add a require check to ensure the owner at the index is the signer of the signature.

Assessed type

Other

#0 - c4-pre-sort

2024-03-21T22:19:51Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-03-21T22:19:59Z

raymondfam marked the issue as duplicate of #18

#2 - raymondfam

2024-03-21T22:20:46Z

See #18.

#3 - c4-pre-sort

2024-03-22T22:32:22Z

raymondfam marked the issue as duplicate of #22

#4 - c4-pre-sort

2024-03-24T14:46:52Z

raymondfam marked the issue as duplicate of #181

#5 - c4-judge

2024-03-27T09:15:31Z

3docSec marked the issue as satisfactory

#6 - c4-judge

2024-03-27T17:52:45Z

3docSec marked the issue as not a duplicate

#7 - c4-judge

2024-03-27T17:53:04Z

3docSec marked the issue as duplicate of #114

#8 - McCoady

2024-03-28T03:32:30Z

The issue raised in this report differs from the one in reports #114 and #88.

This report highlights that an owner is able to remove other owners (and themselves) via use of the EntryPoint contract on one chain.

The report makes no mention of following key parts of the issue:

  • The signatures can be replayed (by anyone) on other chains.
  • The owner at a particular index on chain X is not guaranteed to be the same owner on chain Y.
  • There's no guarantee there are any owners left on a given chain after removeOwnerAtIndex.

I believe this report is highlighting an intentional design choice of the protocol (that owners have 1/1 power to execute transactions, including the addition/removal of other owners).

#9 - 3docSec

2024-03-28T08:16:20Z

Yes @McCoady you are right, this one should dupe #18 because it does not elaborate on the key aspects of #114 .

#10 - c4-judge

2024-03-28T08:16:29Z

3docSec marked the issue as not a duplicate

#11 - c4-judge

2024-03-28T08:16:41Z

3docSec marked the issue as duplicate of #18

#12 - c4-judge

2024-03-28T08:16:53Z

3docSec changed the severity to QA (Quality Assurance)

#13 - c4-judge

2024-03-28T08:16:59Z

3docSec marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter