Summary
The _doDexSwap function in the PerpetualVault contract does not handle partial swap completions, leading to potential accounting errors and mixed collateral holdings.
Vulnerability Details
Description:
The _doDexSwap function performs a DEX swap using ParaSwap. However, it does not check for partial completions of the swap. If the swap only partially completes due to insufficient liquidity, the protocol assumes full conversion but holds mixed collateral. This can lead to incorrect share calculations and potential accounting errors.
Root Cause:
The root cause of this issue is the lack of a check for partial swap completions in the _doDexSwap function. The function assumes that the swap will fully complete and does not handle cases where only a partial amount is swapped.
Proof of Concept:
function test_PartialSwapFailure() public {
address alice = makeAddr("alice");
uint256 depositAmount = 1e10;
depositFixture(alice, depositAmount);
IERC20 collateralToken = PerpetualVault(vault).collateralToken();
uint256 initialVaultBalance = collateralToken.balanceOf(vault);
bytes[] memory swapData = new bytes[](1);
address[] memory gmxPath = new address[](1);
gmxPath[0] = address(0x70d95587d40A2caf56bd97485aB3Eec10Bee6336);
uint256 minOutputAmount = depositAmount / 2;
swapData[0] = abi.encode(PROTOCOL.DEX, abi.encode(gmxPath, depositAmount, minOutputAmount));
address keeper = PerpetualVault(vault).keeper();
vm.prank(keeper);
PerpetualVault(vault).run(true, true, mockData.getMarketPrices(), swapData);
GmxOrderExecuted(true);
uint256 outputAmount = collateralToken.balanceOf(vault);
assertLt(outputAmount, depositAmount, "Swap did not partially complete");
uint256 remainingCollateral = collateralToken.balanceOf(vault);
assertGt(remainingCollateral, 0, "No remaining collateral");
uint256 userShares = PerpetualVault(vault).getUserShares(alice);
assertEq(userShares, depositAmount * 1e8, "Incorrect share calculations");
}
Impact
Protocol assumes full conversion but holds mixed collateral: The protocol may hold a mix of the input and output tokens if the swap only partially completes.
Accounting Error: Share calculations use the full amount assumption, leading to incorrect share allocations and potential financial discrepancies.
Tools Used
Manual
Recommendations
To mitigate this issue, implement a check for partial swap completions in the _doDexSwap function. If the swap only partially completes, handle the remaining collateral appropriately.
Example of improved _doDexSwap function:
function _doDexSwap(bytes memory data, bool isCollateralToIndex) internal returns (uint256 outputAmount) {
(address to, uint256 amount, bytes memory callData) = abi.decode(data, (address, uint256, bytes));
IERC20 inputToken;
IERC20 outputToken;
if (isCollateralToIndex) {
inputToken = collateralToken;
outputToken = IERC20(indexToken);
} else {
inputToken = IERC20(indexToken);
outputToken = collateralToken;
}
uint256 balBefore = outputToken.balanceOf(address(this));
ParaSwapUtils.swap(to, callData);
outputAmount = IERC20(outputToken).balanceOf(address(this)) - balBefore;
if (outputAmount < amount) {
uint256 remainingAmount = amount - outputAmount;
inputToken.safeTransfer(address(this), remainingAmount);
}
emit DexSwap(address(inputToken), amount, address(outputToken), outputAmount, isCollateralToIndex);
}