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: 49/51
Findings: 1
Award: $6.95
🌟 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
6.9492 USDC - $6.95
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
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.
// 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))); } }
Manual review
Add a require check to ensure the owner at the index is the signer of the signature.
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:
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