MorpheusAI

MorpheusAI
Foundry
22,500 USDC
View results
Submission Details
Severity: high
Valid

All bridged funds will be lost for users who use account abstraction wallets

Summary

Users who use account abstraction wallets and bridge their stETH on the Arbitrum network using the claim() function will lose their funds. Account abstraction wallets are, by design, unique smart contract instances deployed on individual chains. While users can create account abstraction wallets on different chains, they ultimately have distinct account addresses
Since account abstraction wallets are on the rise, with Safe having almost 6M users (disregarding other Account Abstraction wallet "providers"), this is a very plausible scenario that should be taken into consideration.

Vulnerability Details

When users stake stETH on Mainnet using the stake() function, the funds that they've staked is recorded under msg.sender (the account on Mainnet with which they're interacting with the Distribution.sol contract).

function stake(uint256 poolId_, uint256 amount_) external poolExists(poolId_) poolPublic(poolId_) {
_stake(_msgSender(), poolId_, amount_, _getCurrentPoolRate(poolId_));
}

As we can see msg.sender is passed as the user_ and the pending rewards will be recorded to that user data, meaning the msg.sender:

function _stake(address user_, uint256 poolId_, uint256 amount_, uint256 currentPoolRate_) private {
require(amount_ > 0, "DS: nothing to stake");
Pool storage pool = pools[poolId_];
PoolData storage poolData = poolsData[poolId_];
UserData storage userData = usersData[user_][poolId_];
userData.pendingRewards = _getCurrentUserReward(currentPoolRate_, userData);
// Update pool data
poolData.lastUpdate = uint128(block.timestamp);
poolData.rate = currentPoolRate_;
poolData.totalDeposited += amount_;
// Update user data
userData.lastStake = uint128(block.timestamp);
userData.rate = currentPoolRate_;
userData.deposited += amount_;

All of the rewards that the user has accumulated will be recorded under the userData which in the above case is the msg.sender (the mainnet address which called this function).

function claim(uint256 poolId_, address user_) external payable poolExists(poolId_) {
Pool storage pool = pools[poolId_];
PoolData storage poolData = poolsData[poolId_];
UserData storage userData = usersData[user_][poolId_];
require(block.timestamp > pool.payoutStart + pool.claimLockPeriod, "DS: pool claim is locked");
uint256 currentPoolRate_ = _getCurrentPoolRate(poolId_);
uint256 pendingRewards_ = _getCurrentUserReward(currentPoolRate_, userData);
require(pendingRewards_ > 0, "DS: nothing to claim");
// Update pool data
poolData.lastUpdate = uint128(block.timestamp);
poolData.rate = currentPoolRate_;
// Update user data
userData.rate = currentPoolRate_;
userData.pendingRewards = 0;
// Transfer rewards
L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender());

When a user goes on to claim their rewards on Arbitrum, all of the awards will be lost since they won't have the same address on Arbitrum as the one that they used to interact with the contract on mainnet.

Impact

Users which use Account Abstraction wallets will lose all of their funds when bridging tokens due to AA wallets having different addresses on different chains.

Tools Used

Manual Review

Recommendations

Include a rewardAddress field on the claim function so that users with account abstraction wallets can pass as an arbitrary argument to "redirect" all of their pending rewards to.

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Users that interact through smart contracts, account abstaction or multisig wallets lose all rewards because they are not the owners of the same addresses on L2

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.