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

Incorrect ABI Decoding in ParaSwapUtils Leads to Failed Token Swaps and Potential Loss of Funds

Summary

The ParaSwapUtils.swap() function incorrectly decodes the calldata for Paraswap's simpleSwap() function by using wrong memory offsets to extract fromToken (or fromAmount). This causes the function to approve the wrong token for transfer or approve incorrect amounts, leading to failed swaps or potential loss of funds.

Vulnerability Details

The ParaSwapUtils.swap() function attempts to extract fromToken and fromAmount from the calldata of Paraswap's simpleSwap() function using assembly, but uses incorrect memory offsets:

This what the Paraswap simpleSwap function and its struct param looks like:

function simpleSwap(SimpleData memory data) external {}
struct SimpleData {
address fromToken;
address toToken;
uint256 fromAmount;
uint256 toAmount;
uint256 expectedAmount;
address[] callees;
bytes exchangeData;
uint256[] startIndexes;
uint256[] values;
address payable beneficiary;
address payable partner;
uint256 feePercent;
bytes permit;
uint256 deadline;
bytes16 uuid;
}

The correct ParaSwapUtils::swap implementation:

function swap(address to, bytes memory callData) external {
// ... existing code ...
assembly {
fromToken := mload(add(callData, 68)) // INCORRECT: reads toToken instead
fromAmount := mload(add(callData, 100)) // INCORRECT: wrong offset
}
// ... existing code ...
}

The actual calldata layout for simpleSwap(SimpleData) should be:

[0:4] - Function selector
[4:36] - SimpleData struct offset
[36:68] - fromToken <-- Should read from here
[68:100] - toToken
[100:132] - fromAmount
[132:164] - toAmount
...

The function currently:

  1. Reads toToken (offset 68) instead of fromToken (offset 36)

  2. Uses incorrect spacing between fromToken and fromAmount (32 bytes instead of 64 bytes)

Note
Even if fromToken were correctly extracted, fromAmount would still be incorrectly extracted, leading to similar swap revert or incorrect transfer amount errors. This is because the extraction offsets for these two values do not align with the SimpleData layout. The correct layout requires a 64-byte gap between these values, but the swap function only accounts for 32 bytes (from offset 68 to 100).

Proof of Concept

A simple test contract demonstrating proper ABI encoding for struct parameters:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract AbiEncodingTest {
struct S {
uint256 x;
bytes data;
}
function x(S memory s) external {}
}

Calling with [1, "0xab"] (`x=1, data="0xab") produces calldata showing proper struct encoding:

[0:4] - Function selector: 0x6b76f3c5
[4:36] - Struct offset: 0x0000000000000000000000000000000000000000000000000000000000000020
[36:68] - x value: 0x0000000000000000000000000000000000000000000000000000000000000001
[68:100] - data offset: 0x0000000000000000000000000000000000000000000000000000000000000040
[100:132] - data length: 0x00000000000000000000000000000000000000000000000000000000000000001
[132:164] - data value: 0xab00000000000000000000000000000000000000000000000000000000000000

Impact Details

When the function attempts to swap tokens through Paraswap, it will either approve the wrong token for transfer (if reading toToken as fromToken) or approve an incorrect amount. This will cause all swap transactions to revert, making the swap functionality unusable i.e. DoS of PerpetualVault::deposit and PerpetualVault::withdraw functions.

Tools Used

Manual Review

Recommendations

Update the assembly block in ParaSwapUtils.swap() to use correct offsets:

function swap(address to, bytes memory callData) external {
_validateCallData(to, callData);
address approvalAddress = IAugustusSwapper(to).getTokenTransferProxy();
address fromToken;
uint256 fromAmount;
assembly {
+ fromToken := mload(add(callData, 36)) // FIXED: correct offset for fromToken
fromAmount := mload(add(callData, 100))
}
IERC20(fromToken).safeApprove(approvalAddress, fromAmount);
(bool success, ) = to.call(callData);
require(success, "paraswap call reverted");
}
Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

invalid_ParaswapUtils_incorrect_offset_SimpleSwap

Keepers use megaSwap with this struct: struct MegaSwapSellData { address fromToken; uint256 fromAmount; uint256 toAmount; uint256 expectedAmount; address payable beneficiary; Utils.MegaSwapPath[] path; address payable partner; uint256 feePercent; bytes permit; uint256 deadline; bytes16 uuid; } 32 first bytes of callData bytes array → length of the bytes array. 4 bytes selector, 32 bytes → offset of the struct → 68 bytes before the fromToken.

Appeal created

krisrenzo Submitter
7 months ago
n0kto Lead Judge
6 months ago
n0kto Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

invalid_ParaswapUtils_incorrect_offset_SimpleSwap

Keepers use megaSwap with this struct: struct MegaSwapSellData { address fromToken; uint256 fromAmount; uint256 toAmount; uint256 expectedAmount; address payable beneficiary; Utils.MegaSwapPath[] path; address payable partner; uint256 feePercent; bytes permit; uint256 deadline; bytes16 uuid; } 32 first bytes of callData bytes array → length of the bytes array. 4 bytes selector, 32 bytes → offset of the struct → 68 bytes before the fromToken.

Support

FAQs

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