DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: high
Invalid

Compounding Mechanism Bypasses Deposit Limits Leading to Systemic Risk

Summary

Critical vulnerability in PerpetualVault where the compound mechanism bypasses maxDepositAmount checks, allowing positions to grow beyond intended limits and enabling leverage limit bypass.

Vulnerability Details

In PerpetualVault.sol , the runNextAction function allows compounding without validating against maxDepositAmount:

function runNextAction(MarketPrices memory prices, bytes[] memory metadata) external nonReentrant gmxLock {
_onlyKeeper();
// ...
} else if (positionIsClosed == false && _isFundIdle()) {
flow = FLOW.COMPOUND;
if (_isLongOneLeverage(beenLong)) {
_runSwap(metadata, true, prices);
} else {
(uint256 acceptablePrice) = abi.decode(metadata[0], (uint256));
_createIncreasePosition(beenLong, acceptablePrice, prices);
}
}

While normal deposits are restricted:

function deposit(uint256 amount) external nonReentrant payable {
if (amount < minDepositAmount) {
revert Error.InsufficientAmount();
}
if (totalDepositAmount + amount > maxDepositAmount) {
revert Error.ExceedMaxDepositCap();
}

The compounding flow bypasses these checks entirely, allowing:

  1. Position growth beyond maxDepositAmount

  2. Leverage increase beyond intended limits

  3. Systemic risk accumulation

Impact

  1. Vault can grow beyond manageable size

  2. Leverage limits can be bypassed

  3. Protocol's risk management compromised

  4. Potential for catastrophic liquidations

  5. Risk to all vault participants

Proof of Concept

This PoC demonstrates how an attacker can exploit the runNextAction function in PerpetualVault.sol to bypass maxDepositAmount restrictions and grow their position beyond intended limits.

Exploitation Steps:

  1. Initial Deposit: The attacker deposits an amount just below maxDepositAmount (e.g., 99,000 USDC out of 100,000).

  2. Opening a Profitable Position: A trade is executed, generating profits as market conditions change.

  3. Waiting for Profits: The vault accumulates earnings over time.

  4. Compounding Exploit: The attacker repeatedly calls runNextAction, which reinvests profits without checking maxDepositAmount.

  5. Bypassing Leverage Limits: The total position size grows unchecked, leading to excessive risk exposure and potential liquidation cascades.

Outcome:

This vulnerability allows positions to expand beyond safe limits, bypassing deposit and leverage restrictions, which can destabilize the vault and endanger all participants.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
import "../contracts/PerpetualVault.sol";
contract CompoundingExploitTest is Test {
PerpetualVault vault;
address attacker;
function setUp() public {
// Setup vault with 100k USDC max deposit
vault = new PerpetualVault();
vault.initialize(
address(gmx_market),
address(keeper),
address(treasury),
address(gmxProxy),
address(vaultReader),
1000e6, // minDeposit
100000e6, // maxDeposit - 100k USDC
10000 // 1x leverage
);
attacker = address(0x1);
vm.deal(attacker, 100 ether);
}
function testCompoundingExploit() public {
// 1. Make initial deposit just under max
vm.startPrank(attacker);
usdc.approve(address(vault), 99000e6);
vault.deposit(99000e6);
vm.stopPrank();
// 2. Take profitable position to generate returns
vault.run(true, true, _mockProfitableMarket(), new bytes[](0));
// 3. Wait for profits
vm.roll(block.number + 1000);
// 4. Compound repeatedly
for(uint i = 0; i < 5; i++) {
vault.runNextAction(_mockProfitableMarket(), new bytes[](0));
// Check total position size
uint256 totalSize = vault.totalAmount(_mockProfitableMarket());
console.log("Total Size:", totalSize);
// Shows growth beyond maxDepositAmount
}
// Final position size > maxDepositAmount
assertTrue(vault.totalAmount(_mockProfitableMarket()) > 100000e6);
}
function _mockProfitableMarket() internal pure returns (MarketPrices memory) {
// Mock market prices showing profit
return MarketPrices({
indexTokenPrice: PriceProps({
min: 2000e30,
max: 2100e30
}),
longTokenPrice: PriceProps({
min: 2000e30,
max: 2100e30
}),
shortTokenPrice: PriceProps({
min: 1e30,
max: 1e30
})
});
}
}

Tools Used

  • Manual code review

  • Foundry for testing

  • Static analysis with Aderyn

Recommended Mitigation

Add maxDeposit validation to compounding:

function runNextAction(MarketPrices memory prices, bytes[] memory metadata) external nonReentrant gmxLock {
_onlyKeeper();
// ...
} else if (positionIsClosed == false && _isFundIdle()) {
uint256 currentTotal = _totalAmount(prices);
uint256 pendingCompound = getPendingCompound(); // New function to calculate compound amount
if (currentTotal + pendingCompound > maxDepositAmount) {
revert Error.ExceedMaxDepositCap();
}
flow = FLOW.COMPOUND;
// ... rest of function
}
}
Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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