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

Unsafe Token Approval Pattern in ParaSwap Integration Could Block Token Swaps

Summary

A critical vulnerability exists in ParaSwapUtils.sol where unsafe token approval handling leads to permanent failure of swap operations with certain ERC20 tokens. The issue can cause funds to become stuck and disrupt core vault operations due to improper approval management.

Vulnerability Details

The vulnerability exists in ParaSwapUtils.sol's swap function where token approvals are handled incorrectly:

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); // @audit-issue
(bool success, ) = to.call(callData);
require(success, "paraswap call reverted");
}

The issue arises because:

  1. Direct safeApprove calls are made without first resetting existing approvals

  2. Several major ERC20 tokens (e.g., USDT, BNB) revert when modifying non-zero approvals

  3. No approval reset mechanism exists in the codebase

  4. Failed approvals permanently block swap functionality

  5. No recovery mechanism is implemented

Impact

  1. Permanent Operation Failure

    • Once a token has a non-zero approval, subsequent operations fail

    • No built-in recovery mechanism exists

    • Requires contract upgrade to fix

  2. Core Functionality Disruption

    • Vault operations requiring swaps become blocked

    • Position management can fail

    • User deposits/withdrawals may be affected

  3. Fund Lock Risk

    • Users may be unable to execute planned operations

    • Positions could become temporarily immovable

    • Emergency withdrawals could be impacted

Proof of Concept

The following Proof of Concept demonstrates the vulnerability:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
import "../../contracts/libraries/ParaSwapUtils.sol";
import "../../contracts/PerpetualVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ParaSwapApprovalExploitTest is Test {
// Mock USDT-like token with strict approval checks
contract USDTMock is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
function mint(address to, uint256 amount) external {
_balances[to] += amount;
}
function approve(address spender, uint256 amount) external returns (bool) {
require(
(_allowances[msg.sender][spender] == 0) || (amount == 0),
"USDT: Cannot modify existing approval"
);
_allowances[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(_allowances[from][msg.sender] >= amount, "Insufficient allowance");
require(_balances[from] >= amount, "Insufficient balance");
_allowances[from][msg.sender] -= amount;
_balances[from] -= amount;
_balances[to] += amount;
return true;
}
function allowance(address owner, address spender) external view returns (uint256) {
return _allowances[owner][spender];
}
}
ParaSwapUtils public swapUtils;
USDTMock public usdt;
PerpetualVault public vault;
address public constant AUGUSTUS_SWAPPER = 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57;
address alice = address(0x1);
address bob = address(0x2);
function setUp() public {
swapUtils = new ParaSwapUtils();
usdt = new USDTMock();
vault = new PerpetualVault();
usdt.mint(alice, 10000e6);
usdt.mint(bob, 10000e6);
}
function testVaultImpact() public {
// Initialize vault
vault.initialize(
address(0x123), // market
address(this), // keeper
address(0x456), // treasury
address(0x789), // gmxProxy
address(0x321), // vaultReader
1000e6, // minDeposit
100000e6, // maxDeposit
10000 // leverage
);
// Simulate first swap operation
bytes memory swapData = _createSwapCalldata(
address(usdt),
1000e6,
address(vault)
);
// First swap succeeds
swapUtils.swap(AUGUSTUS_SWAPPER, swapData);
// Second swap fails due to existing approval
vm.expectRevert("USDT: Cannot modify existing approval");
swapUtils.swap(AUGUSTUS_SWAPPER, swapData);
// Vault operations are now blocked
assertTrue(
usdt.allowance(address(swapUtils), AUGUSTUS_SWAPPER) > 0,
"Non-zero approval persists"
);
}
function _createSwapCalldata(
address token,
uint256 amount,
address recipient
) internal pure returns (bytes memory) {
return abi.encodePacked(
bytes4(0x12345678),
token,
amount,
recipient
);
}
}

The PoC demonstrates:

  1. Initial swap operation succeeds

  2. Subsequent swaps fail due to approval mechanics

  3. Vault operations become blocked

  4. No recovery mechanism exists

  5. System-wide impact of the issue

Tools Used

  • Manual code review

  • Foundry testing framework

  • Static analysis (Aderyn)

Recommended Mitigation

Implement proper approval management:

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 token = IERC20(fromToken);
// Reset approval first
token.safeApprove(approvalAddress, 0);
// Set new approval
token.safeApprove(approvalAddress, fromAmount);
(bool success, ) = to.call(callData);
require(success, "paraswap call reverted");
}

Alternative options:

  1. Use OpenZeppelin's newer forceApprove

  2. Implement approval increase/decrease pattern

  3. Add recovery mechanism for stuck approvals

The vulnerability is considered High severity because:

  1. Impacts core vault functionality

  2. Can lead to stuck funds

  3. Affects common tokens like USDT

  4. No existing workaround

  5. Requires contract upgrade to fix

Updates

Lead Judging Commences

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

invalid_safeApprove_no_reset

USDT or other unusual ERC20 tokens: out of scope. For the other reports: No proof that the allowance won't be consumed by the receiver.

Support

FAQs

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