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: 4/51
Findings: 2
Award: $2,629.91
π Selected for report: 1
π Solo Findings: 0
2622.962 USDC - $2,622.96
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); }
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.
Are there any griefing attacks that could cause this paymaster to be banned by bundlers?
I consider that this have to be another vulnerability but i decided to putted here because the main problem is the same.
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); }
Manual,Foundry
Consider add another signer for the withdraw function different from the validatePaymasterUserOp signer.
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
π 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
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); } }
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:
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
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.
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); } }
This can be problematic if the owners of the wallet was already changed.
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.
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