[HIGH-1] Position Health Check Gap
Location
PerpetualVault.sol -> willPositionCollateralBeInsufficient() function
Description
The vault lacks continuous health check mechanisms between GMX position updates, creating a vulnerability window where positions can become unsafe without detection. This issue exists because:
Health checks only occur during explicit keeper actions
No automatic monitoring between updates
GMX price movements can make positions unsafe between checks
Impact
Positions can become liquidatable without timely intervention
Increased risk of forced liquidations
Potential losses due to delayed health checks
Proof of Concept
contract HealthCheckGapTest is Test {
PerpetualVault public vault;
MockPriceOracle public priceOracle;
address user = address(0x1);
function setUp() public {
vault = new PerpetualVault();
priceOracle = new MockPriceOracle();
vault.setPriceOracle(address(priceOracle));
vm.deal(user, 100 ether);
vm.prank(user);
vault.deposit{value: 10 ether}();
}
function testHealthCheckGap() public {
vm.prank(user);
bytes32 positionId = vault.createPosition({
size: 20 ether,
collateral: 10 ether,
isLong: true
});
vm.warp(block.timestamp + 1 hours);
priceOracle.setPrice(initialPrice * 60 / 100);
assertTrue(vault.isPositionUnsafe(positionId));
assertFalse(vault.lastHealthCheck(positionId) > block.timestamp - 1 hours);
vm.prank(user);
vault.increasePosition(positionId, 1 ether);
}
}
Recommendation
Implement continuous health monitoring:
contract PerpetualVault {
uint256 public constant HEALTH_CHECK_INTERVAL = 5 minutes;
mapping(bytes32 => uint256) public lastHealthCheck;
modifier requireHealthCheck(bytes32 positionId) {
if (block.timestamp >= lastHealthCheck[positionId] + HEALTH_CHECK_INTERVAL) {
_performHealthCheck(positionId);
}
_;
}
function _performHealthCheck(bytes32 positionId) internal {
Position storage position = positions[positionId];
require(!_isPositionUnsafe(position), "Position unsafe");
lastHealthCheck[positionId] = block.timestamp;
}
function increasePosition(bytes32 positionId, uint256 amount)
external
requireHealthCheck(positionId)
{
}
}
Implement a keeper network for regular health checks
Add incentives for external health check triggers
Implement automatic position reduction when approaching unsafe levels
[HIGH-2] Token Balance Accounting Gap in Multi-Token Outputs
Location
PerpetualVault.sol -> _totalAmount() function
Description
The contract fails to properly account for multiple token outputs during position decreases, particularly when GMX decrease position swaps fail. This leads to incorrect withdrawal amounts and corrupted accounting states.
Impact
Incorrect withdrawal amounts leading to direct fund losses
Corrupted accounting states affecting all subsequent operations
Potential for exploitation through manipulated token outputs
Proof of Concept
contract TokenBalanceAccountingTest is Test {
PerpetualVault public vault;
MockERC20 public tokenA;
MockERC20 public tokenB;
function setUp() public {
vault = new PerpetualVault();
tokenA = new MockERC20("Token A", "TKNA");
tokenB = new MockERC20("Token B", "TKNB");
tokenA.mint(address(vault), 1000e18);
tokenB.mint(address(vault), 1000e18);
}
function testMultiTokenAccountingFailure() public {
address user = address(0x1);
vm.prank(user);
vault.deposit(tokenA, 100e18);
bytes32 positionId = vault.createPosition({
token: address(tokenA),
size: 50e18,
isLong: true
});
vm.mockCall(
address(gmx),
abi.encodeWithSelector(IGmx.decreasePosition.selector),
abi.encode(
tokenA, 25e18,
tokenB, 25e18
)
);
vm.prank(user);
vault.closePosition(positionId);
assertEq(vault.userBalance(user, tokenA), 75e18);
assertEq(vault.userBalance(user, tokenB), 0);
}
}
Recommendation
Implement proper multi-token return handling:
contract PerpetualVault {
struct TokenReturn {
address token;
uint256 amount;
}
function _handlePositionClose(bytes32 positionId) internal returns (TokenReturn[] memory) {
Position storage position = positions[positionId];
TokenReturn[] memory returns = new TokenReturn[](2);
(address token1, uint256 amount1) = _closePositionPrimary(position);
returns[0] = TokenReturn(token1, amount1);
(address token2, uint256 amount2) = _closePositionSecondary(position);
if (amount2 > 0) {
returns[1] = TokenReturn(token2, amount2);
}
for (uint256 i = 0; i < returns.length; i++) {
if (returns[i].amount > 0) {
_updateUserBalance(
position.owner,
returns[i].token,
returns[i].amount
);
}
}
return returns;
}
}
Add comprehensive token accounting tests
Implement balance reconciliation mechanisms
Add event emissions for all token movements