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

Inconsistent swap handling in the `PerpetualVault`

Summary

Inconsistent swap handling in the PerpetualVault it lacks uniformity in how swaps (e.g., DEX swaps or GMX swaps) are managed and validated. This inconsistency can lead to failed swaps, incorrect state updates, or loss of funds. The contract interacts with external protocols like Paraswap (DEX) and GMX for swaps, but the handling of these swaps is not always robust or consistent.

Vulnerability Details

function _runSwap(bytes[] memory metadata, bool isCollateralToIndex, MarketPrices memory prices) internal returns (bool completed) {
if (metadata.length == 0) {
revert Error.InvalidData();
}
if (metadata.length == 2) {
(PROTOCOL _protocol, bytes memory data) = abi.decode(metadata[0], (PROTOCOL, bytes));
if (_protocol != PROTOCOL.DEX) {
revert Error.InvalidData();
}
swapProgressData.swapped = swapProgressData.swapped + _doDexSwap(data, isCollateralToIndex);
(_protocol, data) = abi.decode(metadata[1], (PROTOCOL, bytes));
if (_protocol != PROTOCOL.GMX) {
revert Error.InvalidData();
}
_doGmxSwap(data, isCollateralToIndex);
return false;
} else {
if (metadata.length != 1) {
revert Error.InvalidData();
}
(PROTOCOL _protocol, bytes memory data) = abi.decode(metadata[0], (PROTOCOL, bytes));
if (_protocol == PROTOCOL.DEX) {
uint256 outputAmount = _doDexSwap(data, isCollateralToIndex);
// Update global state
if (flow == FLOW.DEPOSIT) {
_mint(counter, outputAmount + swapProgressData.swapped, true, prices);
} else if (flow == FLOW.WITHDRAW) {
_handleReturn(outputAmount + swapProgressData.swapped, false, true);
} else {
_updateState(!isCollateralToIndex, isCollateralToIndex);
}
return true;
} else {
_doGmxSwap(data, isCollateralToIndex);
return false;
}
}
}

_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;
emit DexSwap(address(inputToken), amount, address(outputToken), outputAmount, isCollateralToIndex);
}

Impact

  • If outputAmount is zero, the contract may mint shares for zero token

  • A user may receive shares for a deposit that was not actually processed.

  • If the contract mints shares for zero tokens, an attacker could exploit this to drain funds.

Tools Used

Manual Code Review

Recommendations

Ensure the output amount meets the expected minimum before proceeding with state updates.

function _doDexSwap(bytes memory data, bool isCollateralToIndex) internal returns (uint256 outputAmount) {
(address to, uint256 amount, bytes memory callData, uint256 minOutputAmount) = abi.decode(data, (address, uint256, bytes, uint256));
IERC20 inputToken = isCollateralToIndex ? collateralToken : indexToken;
IERC20 outputToken = isCollateralToIndex ? indexToken : collateralToken;
uint256 balBefore = outputToken.balanceOf(address(this));
bool success = ParaSwapUtils.swap(to, callData);
require(success, "Swap failed");
outputAmount = outputToken.balanceOf(address(this)) - balBefore;
require(outputAmount >= minOutputAmount, "Slippage too high");
emit DexSwap(address(inputToken), amount, address(outputToken), outputAmount, isCollateralToIndex);
}

Use require to handle swap failures consistently.

function _runSwap(bytes[] memory metadata, bool isCollateralToIndex) internal returns (bool completed) {
if (metadata.length == 0) revert("Invalid data");
(PROTOCOL _protocol, bytes memory data) = abi.decode(metadata[0], (PROTOCOL, bytes));
if (_protocol == PROTOCOL.DEX) {
uint256 outputAmount = _doDexSwap(data, isCollateralToIndex);
require(outputAmount > 0, "Swap failed");
// Update state only if swap succeeds
if (flow == FLOW.DEPOSIT) {
_mint(counter, outputAmount + swapProgressData.swapped);
} else if (flow == FLOW.WITHDRAW) {
_handleReturn(outputAmount + swapProgressData.swapped);
} else {
_updateState(!isCollateralToIndex, isCollateralToIndex);
}
return true;
} else {
revert("Unsupported protocol");
}
}
Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Too generic
Assigned finding tags:

Informational or Gas

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Too generic
Assigned finding tags:

Informational or Gas

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Support

FAQs

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