Coinbase Smart Wallet - Jorgect'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: 4/51

Findings: 2

Award: $2,629.91

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: Jorgect

Also found by: cheatc0d3

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
satisfactory
selected for report
sponsor acknowledged
sufficient quality report
:robot:_39_group
M-02

Awards

2622.962 USDC - $2,622.96

External Links

Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L181

Vulnerability details

The paymaster is a extension of the eip-4337, normally the paymaster is welling to pay a user transaction if the account can return the amount of gas at the final of the transaction.

In the context of the coinbase smart wallet the paymaster is the contract call magicSpend.sol, This contract expose the normal function need it to be a paymaster:

function validatePaymasterUserOp (PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) external returns (bytes memory context, uint256 validationData); function postOp (PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) external;

The magic spend is also implementing the entry point deposit, unlock and withdraw functions as required.

Addionally of this the magicSpend is implementing a withdraw functions for users:

file:https://src/MagicSpend/MagicSpend.sol#L181 function withdraw(WithdrawRequest memory withdrawRequest) external { _validateRequest(msg.sender, withdrawRequest); if (!isValidWithdrawSignature(msg.sender, withdrawRequest)) { revert InvalidSignature(); } if (block.timestamp > withdrawRequest.expiry) { revert Expired(); } // reserve funds for gas, will credit user with difference in post op _withdraw(withdrawRequest.asset, msg.sender, withdrawRequest.amount); }

[Link]

The problem is that validatePaymasterUserOp is consuming the same signature of the withdraw function, so user can request a transaction through the paymaster, then front runt this transaction calling the withdraw function in the magicSpend (as you notice this transaction is not being processed through the bundler so user can get this withdraw transaction first if he send the correct amount of gas to be included first) making the validatePaymasterUserOp revert because the nonce was already consumed.

This is behavior is so problematic see the impact.

Impact

Are there any griefing attacks that could cause this paymaster to be banned by bundlers?

  1. Paymaster can be banned by bundlers because user can trigger revert transactions which is one of the reason because bundlers can banned paymasters.

I consider that this have to be another vulnerability but i decided to putted here because the main problem is the same.

  1. Paymaster can be drained if user front run the signature gave to pay a operation, withdrawing directly this funds in the withdraw function, user can do this repeated times until the Paymaster is completed drained.

Proof of Concept

Run this test in file:/test/MagicSpend/Withdraw.t.sol :

function test_frontRuning() public { //@audit POC HIGH 1 MagicSpend.WithdrawRequest memory WithdrawRequest = MagicSpend.WithdrawRequest({ asset: asset, amount: amount, nonce: nonce, expiry: expiry, signature: _getSignature() }); UserOperation memory userOp = UserOperation({ sender: withdrawer, nonce: 0, initCode: "", callData: "", callGasLimit: 49152, verificationGasLimit: 378989, preVerificationGas: 273196043, maxFeePerGas: 1000304, maxPriorityFeePerGas: 1000000, paymasterAndData: abi.encodePacked(address(magic), abi.encode(WithdrawRequest)), signature: "" }); magic.withdraw(WithdrawRequest); //Front runing the validatePaymasterUserOp operation bytes32 userOpHash = sha256("hi"); uint256 maxCost = amount - 10; vm.expectRevert(); magic.validatePaymasterUserOp(userOp, userOpHash, maxCost); }

Tools Used

Manual,Foundry

Consider add another signer for the withdraw function different from the validatePaymasterUserOp signer.

Assessed type

Other

#0 - c4-pre-sort

2024-03-22T03:13:53Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-03-22T03:14:12Z

raymondfam marked the issue as primary issue

#2 - c4-pre-sort

2024-03-22T03:16:38Z

raymondfam marked the issue as sufficient quality report

#3 - raymondfam

2024-03-22T03:19:01Z

Will let the sponsor verify the runnable POC.

#4 - wilsoncusack

2024-03-26T15:24:55Z

To be clear, the same signature cannot be used twice. The front run is interesting: requires a user to submit a userOp with the withdraw signature in paymasterAndData, and then call from their SCW (via another user op or direct call from an EOA owner) and directly call withdraw. There's a race condition here, but it could indeed hurt the paymaster's reputation if pulled off.

#5 - c4-sponsor

2024-03-26T15:24:59Z

wilsoncusack marked the issue as disagree with severity

#6 - c4-sponsor

2024-03-26T15:25:01Z

wilsoncusack (sponsor) acknowledged

#7 - 3docSec

2024-03-27T08:14:22Z

Because the front-run signature would DoS the second one, tokens would be spent only once (invalidating point 2. of the impact), so it looks more like a grieving attack on the MS reputation with no tokens at risk => medium seems more appropriate.

#8 - c4-judge

2024-03-27T08:14:33Z

3docSec changed the severity to 2 (Med Risk)

#9 - c4-judge

2024-03-27T08:14:55Z

3docSec marked the issue as satisfactory

#10 - c4-judge

2024-03-27T13:45:37Z

3docSec marked the issue as selected for report

Awards

6.9492 USDC - $6.95

Labels

bug
downgraded by judge
grade-b
QA (Quality Assurance)
sufficient quality report
:robot:_46_group
Q-23

External Links

Lines of code

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

Vulnerability details

When user wants create a wallet he has to call the createAccount function in the factory, this functions is taking the owner by a salt:

function createAccount(bytes[] calldata owners, uint256 nonce) public payable virtual returns (CoinbaseSmartWallet account) { if (owners.length == 0) { revert OwnerRequired(); } (bool alreadyDeployed, address accountAddress) = LibClone.createDeterministicERC1967(msg.value, implementation, _getSalt(owners, nonce)); <----- account = CoinbaseSmartWallet(payable(accountAddress)); if (alreadyDeployed == false) { account.initialize(owners); } }

[Link]

Note that user have to deploy the wallet with the same owner and nonces if they want the same address for the wallet in others chain.

The problem is that if a user changed some owners (addOwnerAddress,removeOwnerAtIndex) whether the signature have chain id or not, the user need to pass the first owners to deploy the wallet in other chains.

Consider the next scenario:

  1. user create a Coinbase wallet in all the chain availables with two owners.
  2. One of this owners wallet get compromised, so the compromised owner get removed from the Coinbase wallet.
  3. A new L2 get added in the protocol.
  4. User want to deploy his wallet in the new chain but he notice that he need to pass the compromised owner wallet to create the wallet with the same address.

At this point the compromised owner wallet can do bad thing due he is an owner in other chain of the wallet, bad thing like stole money from the wallet, delete owners, etc

Impact

User may not be capable to create another wallet in other chain because he need the all owner of the wallet, one of this owners can be a compromised wallet who can steal funds.

Proof of Concept

As you can see below, the salt for a new wallet has to be the first owners of the wallet to get the same address:

function createAccount(bytes[] calldata owners, uint256 nonce) public payable virtual returns (CoinbaseSmartWallet account) { if (owners.length == 0) { revert OwnerRequired(); } (bool alreadyDeployed, address accountAddress) = LibClone.createDeterministicERC1967(msg.value, implementation, _getSalt(owners, nonce)); <----- account = CoinbaseSmartWallet(payable(accountAddress)); if (alreadyDeployed == false) { account.initialize(owners); } }

[Link]

This can be problematic if the owners of the wallet was already changed.

Tools Used

Manual.

The most straightforward solution if change the the way that the salt is got it instead of owner this can be some other string passed by the user.

Assessed type

Other

#0 - c4-pre-sort

2024-03-22T03:34:36Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-03-22T03:34:40Z

raymondfam marked the issue as primary issue

#2 - raymondfam

2024-03-22T03:35:59Z

Seems valid barring a secured and smooth SCW deployment on various chains for the same address.

#3 - c4-pre-sort

2024-03-22T23:18:05Z

raymondfam marked the issue as duplicate of #68

#4 - c4-judge

2024-03-27T04:46:42Z

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 - 3docSec

2024-03-27T04:51:08Z

In case of loss of trust in one of the original owners, users can (and should!) create a wallet with a different address in the new chain. The code in scope makes no assumption that a given user must have the same wallet on multiple chains, so the impact is purely UX.

The user privileging the UX of having the same address over the security of barring a disgraced owner would be a user mistake.

#7 - c4-judge

2024-03-27T04:51:18Z

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