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

Token Balance Accounting Gap in Multi-Token Outputs

[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 {
// 1. Setup initial state
address user = address(0x1);
vm.prank(user);
vault.deposit(tokenA, 100e18);
// 2. Create position
bytes32 positionId = vault.createPosition({
token: address(tokenA),
size: 50e18,
isLong: true
});
// 3. Simulate failed swap returning both tokens
vm.mockCall(
address(gmx),
abi.encodeWithSelector(IGmx.decreasePosition.selector),
abi.encode(
tokenA, 25e18, // Primary token return
tokenB, 25e18 // Secondary token return
)
);
// 4. Close position
vm.prank(user);
vault.closePosition(positionId);
// 5. Verify accounting mismatch
assertEq(vault.userBalance(user, tokenA), 75e18); // Should be 100e18
assertEq(vault.userBalance(user, tokenB), 0); // Should be 25e18
}
}

Recommendation

  1. 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);
// Handle primary token
(address token1, uint256 amount1) = _closePositionPrimary(position);
returns[0] = TokenReturn(token1, amount1);
// Handle secondary token if swap fails
(address token2, uint256 amount2) = _closePositionSecondary(position);
if (amount2 > 0) {
returns[1] = TokenReturn(token2, amount2);
}
// Update balances for all returned tokens
for (uint256 i = 0; i < returns.length; i++) {
if (returns[i].amount > 0) {
_updateUserBalance(
position.owner,
returns[i].token,
returns[i].amount
);
}
}
return returns;
}
}
  1. Add comprehensive token accounting tests

  2. Implement balance reconciliation mechanisms

  3. Add event emissions for all token movements

Updates

Lead Judging Commences

n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

invalid_decreasePositionOrder_ouput_two_tokens_not_handled

Guardian’s audit H-05.

Support

FAQs

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