Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Invalid

Bypassing the Deposit Cap in depositMargin Due to Missing Update to totalDeposited

Summary

This issue revolves around the depositMargin() function, which performs a check to ensure that the newly deposited amount plus the existing totalDeposited does not exceed the configured depositCap. However, the contract never increments marginCollateralConfiguration.totalDeposited after a successful deposit. Consequently, the system repeatedly verifies against an unchanged (often zero) total, allowing users to make multiple smaller deposits, each deemed individually permissible, until they collectively surpass the intended limit. This logic flaw invalidates the protective mechanism of the depositCap by failing to track the cumulative amount actually deposited.

Vulnerability Details

The core issue lies in the depositMargin() function, which only checks if the new deposit plus the current totalDeposited stays under depositCap without ever incrementing totalDeposited afterward. As a result, repeated deposits that each appear individually valid can be made until the actual sum far exceeds the intended cap. Because totalDeposited remains effectively static (often at zero), the contract’s logic fails to track the accumulated total and thus does not enforce the deposit limit as designed.

Impact

Because the contract never increments totalDeposited, it effectively ignores the enforced cap. This allows users to deposit an amount of collateral that vastly exceeds the configured limit, undermining any protective measures intended to manage risk or cap exposure. Consequently, the protocol’s expectations regarding collateral limits are violated, increasing both operational and financial risks.

Proof of Concept

In depositMargin(), the contract checks if amountX18 + marginCollateralConfiguration.totalDeposited exceeds the depositCap. However, the code does not increment marginCollateralConfiguration.totalDeposited after a successful deposit. As a result, the contract treats each deposit as if no prior deposits had been made, thus allowing multiple successive deposits to cumulatively exceed the global depositCap.

Code Analysis

Below is a condensed view of the relevant portion of depositMargin(), with comments highlighting the issue:

function depositMargin(uint128 tradingAccountId, address collateralType, uint256 amount) public virtual {
// ... Preliminary logic (storage loads, conversions, checks) ...
// Verifies the deposit does not exceed the cap
// _requireEnoughDepositCap compares: amountX18 + totalCollateralDepositedX18 <= depositCapX18
_requireEnoughDepositCap(collateralType, amountX18, depositCapX18, totalCollateralDepositedX18);
// ... ERC20 token transfer logic (IERC20.safeTransferFrom) ...
// (!) Missing increment of marginCollateralConfiguration.totalDeposited
// (the deposited amount is not accumulated here)
// ... Subsequent code (emit deposit event, etc.) ...
}

(!) The root cause is that marginCollateralConfiguration.totalDeposited is never incremented post-deposit. Despite a valid cap check, the system fails to maintain the aggregated deposit total, allowing repeated small deposits to eventually exceed the overall limit.

Explanation

The depositCap becomes ineffective because the contract never updates the real deposit count. By performing multiple smaller deposits, each seemingly within the cap, a user can surpass the intended total deposit limit since totalDeposited remains unchanged (e.g., it stays at zero if not initialized otherwise).

Vulnerable Scenario

  • depositCap is set to 1,000 tokens.

  • totalDeposited starts at 0 and is never updated.

  • A user deposits 500 tokens:

    • The check passes since 500 + 0 <= 1,000.

    • totalDeposited remains 0.

  • The user deposits another 500 tokens:

    • The check passes again (500 + 0 <= 1,000).

  • This process repeats endlessly, surpassing the actual limit of 1,000 tokens.

Because the deposit tracking is never incremented, the contract cannot detect that the combined deposits exceed the limit.

Test and Outcome

The testExceedDepositCapWithMultipleDeposits function deposits 500 USDC twice and then an additional 200 USDC, resulting in a total of 1,200 USDC. Despite exceeding the supposed cap of 1,000 USDC, the test passes without reverting. This confirms that the contract is not updating marginCollateralConfiguration.totalDeposited and is therefore allowing multiple deposits that cumulatively go beyond the set depositCap. The final line [PASS] demonstrates the flaw: if the cap were enforced correctly, the third deposit would revert instead of passing.

  • Add the following test to test/integration/perpetuals/trading-account-branch/depositMargin/depositMargin.t.sol

function testExceedDepositCapWithMultipleDeposits() external {
// Set up the environment. Assume 'usdc' has a configured depositCap of 1,000 tokens (converted to X18 if required).
// Deposit in increments that individually appear below the depositCap.
// Provide the user Naruto with enough tokens to exceed the total depositCap through multiple deposits.
uint256 depositCap = 1000 * 10 ** 6; // Example: 1,000 USDC with 6 decimals
deal({ token: address(usdc), to: users.naruto.account, give: depositCap * 2 });
// Create a new trading account for the user.
changePrank({ msgSender: users.naruto.account });
uint128 userTradingAccountId = perpsEngine.createTradingAccount(bytes(""), false);
// Perform an initial deposit of 500 USDC, which is well below the depositCap of 1,000.
uint256 firstDeposit = 500 * 10 ** 6;
perpsEngine.depositMargin(userTradingAccountId, address(usdc), firstDeposit);
// Follow with a second deposit of 500 USDC, bringing the total to 1,000.
// If the contract does not increment 'marginCollateralConfiguration.totalDeposited', its internal state remains unchanged.
uint256 secondDeposit = 500 * 10 ** 6;
perpsEngine.depositMargin(userTradingAccountId, address(usdc), secondDeposit);
// Make a third deposit of 200 USDC, pushing the real total over 1,000.
// Because 'totalDeposited' is not updated, the contract incorrectly allows it.
uint256 thirdDeposit = 200 * 10 ** 6;
// The call succeed silently, exceeding the depositCap.
// With a proper update mechanism, this should revert with Errors.DepositCap.
perpsEngine.depositMargin(userTradingAccountId, address(usdc), thirdDeposit);
}
[PASS] testExceedDepositCapWithMultipleDeposits() (gas: 562485)

Confirmation

This confirms that depositCap is disregarded due to the missing update of marginCollateralConfiguration.totalDeposited. Repeated deposits easily bypass the intended cap, demonstrating a clear logical flaw.

Tools Used

  • Manual Code Review: A thorough, line-by-line analysis of the depositMargin() function was performed to identify the absence of state updates for totalDeposited and confirm the vulnerability.

Recommendations

The simplest and most direct remedy is to update marginCollateralConfiguration.totalDeposited immediately after each successful deposit. Specifically, add the newly deposited amount to totalDeposited to ensure the contract accurately tracks the total collateral deposited over time. Once implemented, this change will prevent multiple smaller deposits from bypassing the configured depositCap, thereby restoring the intended protection against excessive collateral inflows.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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