Submitted by rare_one
https://github.com/code-423n4/2025-04-cabal/blob/5b5f92ab4f95e5f9f405bbfa252860472d164705/sources/cabal.move#L632C5-#L661
The liquid staking protocols deposit_init_for_xinit function, which allows users to deposit INIT tokens to receive xINIT, is vulnerable to transaction failures when multiple users deposit concurrently in the same block. The function withdraws INIT tokens and delegates them to a validator via pool_router::add_stake, which triggers lock_staking::delegate. This, in turn, invokes reentry_check to prevent multiple delegations in the same block. 
If a second user attempts to deposit in the same block as another, their transaction fails with error code 196618 (EREENTER), as reentry_check detects that the StakingAccount was already modified in the current block. This vulnerability disrupts users ability to participate in the protocol, particularly during periods of high transaction activity.
The reentry_check function in lock_staking.move enforces a strict one-delegation-per-block rule for a given StakingAccount:
This function checks if staking_account.last_height equals the current block height and aborts with EREENTER if true. If with_update is true, it updates last_height to the current height, marking the block as processed.
In cabal.move, the deposit_init_for_xinit function processes user deposits independently:
When multiple users call deposit_init_for_xinit in the same block:
The functions lack of coordination for concurrent deposits results in multiple lock_staking::delegate calls, triggering the reentrancy failure. This vulnerability is evident in production scenarios where users deposit INIT during high network activity, such as during market events or protocol launches.
IMPACTS:
Denial-of-Service (DoS) for Users: Users attempting to deposit INIT in a block with multiple deposits will face transaction failures, losing gas fees and being unable to receive xINIT. This disrupts their ability to participate in liquid staking, particularly during peak usage periods.
Financial Loss: Failed transactions result in gas fee losses for users, which can accumulate significantly in high-traffic scenarios, deterring participation.
Implement a batching mechanism to aggregate all user INIT deposits within a block and process them as a single delegation, ensuring only one call to lock_staking::delegate per block and bypassing the reentry_check restriction.
Initialize the protocol using initialize to set up the xINIT pool.
Simulate two users depositing INIT in the same block using mockdepositinitforxinit.
Observe the EREENTER error (code 196618) from reentry_check for the second deposit.
// User 1 transaction (submitted in block 100)
public entry fun user1deposit(account: &signer) {
depositinitforxinit(account, 500000000);
}
// User 2 transaction (submitted in block 100)
public entry fun user2deposit(account: &signer) {
depositinitforxinit(account, 200000000);
}
Setup:
Deploy the protocol and initialize it.
Fund User 1 (@0x1) with 500,000,000 INIT and User 2 (@0x2) with 200,000,000 INIT.
Set block height to 100.
User 1 submits user1deposit in block 100, calling `depositinitforxinit, withdrawing 500,000,000 INIT, delegating viapoolrouter::addstake(triggeringlock_staking::delegate`), and minting approximately 500,000,000 xINIT (adjusted for pool size).
User 2 submits user2deposit in block 100, calling `depositinitforxinit, butpoolrouter::addstaketriggerslockstaking::delegateandreentrycheck. Sincestakingaccount.lastheight` equals 100 (from User 1s deposit), the transaction aborts with EREENTER (code 196618).
Result:
User 1: Receives ~500,000,000 xINIT.
User 2: Transaction fails, loses gas fees, receives no xINIT.
This test demonstrates the issue
and the result
