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

Insecure Approval Pattern in ParaSwapUtils

Summary

The ParaSwapUtils library uses the safeApprove function from OpenZeppelin to set token allowances without first resetting any existing allowance. This pattern may lead to issues if the token contract exhibits non-standard behavior or if a non-zero allowance already exists, potentially causing the approval to fail or be manipulated.

Vulnerability Details

  • Issue:
    The library directly calls:

IERC20(fromToken).safeApprove(approvalAddress, fromAmount);

without checking whether the allowance is already set or resetting it to zero.

  • Context:
    Some ERC20 tokens do not behave as expected when increasing or setting allowances directly if a previous non-zero value exists. This can lead to race conditions or unexpected behavior.

  • Inter‑Contract Considerations:
    As the library functions assume a standard ERC20 behavior, tokens with non-standard implementations could cause the approval step to fail, leaving the swap unexecutable or exposing the contract to potential manipulation.

Impact

  • Approval Failures:
    If the allowance is not zero, the call to safeApprove may revert or leave an unexpected allowance, breaking the swap process.

  • Race Conditions:
    An attacker might exploit timing issues around token approvals if the allowance is manipulated during the external call.

  • Economic Risk:
    Failure to properly approve tokens could lead to stuck funds or failed swaps, impacting the protocol’s operation.

Proof of Concept for Insecure Approval Pattern

Overview

This PoC demonstrates how the absence of an allowance reset in the approval pattern can lead to issues. It simulates a scenario where a token already has a non-zero allowance, and then the swap function attempts to set a new allowance without resetting it.

Actors

  • Attacker/Malicious Token: A token contract that mimics non-standard behavior by rejecting approval changes when a non-zero allowance exists.

  • Legitimate User: The caller attempting to execute the swap.

  • Protocol: The ParaSwapUtils library which attempts the approval and swap.

Working Test Case

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// A dummy ERC20 token that rejects changing non-zero allowances.
contract NonStandardToken is IERC20 {
using SafeERC20 for IERC20;
mapping(address => uint256) public _balances;
mapping(address => mapping(address => uint256)) public _allowances;
uint256 public _totalSupply;
// Standard ERC20 functions (simplified for PoC)
function totalSupply() external view override returns (uint256) { return _totalSupply; }
function balanceOf(address account) external view override returns (uint256) { return _balances[account]; }
function transfer(address recipient, uint256 amount) external override returns (bool) {
_balances[msg.sender] -= amount;
_balances[recipient] += amount;
return true;
}
function allowance(address owner, address spender) external view override returns (uint256) {
return _allowances[owner][spender];
}
// Modified approve that fails if current allowance is non-zero.
function approve(address spender, uint256 amount) external override returns (bool) {
require(_allowances[msg.sender][spender] == 0, "Allowance not zero");
_allowances[msg.sender][spender] = amount;
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) {
_allowances[sender][msg.sender] -= amount;
_balances[sender] -= amount;
_balances[recipient] += amount;
return true;
}
}
// A simplified version of the ParaSwapUtils library swap function for demonstration.
library ParaSwapUtilsForPoC {
using SafeERC20 for IERC20;
function swap(address approvalAddress, address fromToken, uint256 fromAmount) external {
// Direct approval call without resetting to zero.
IERC20(fromToken).safeApprove(approvalAddress, fromAmount);
// In a real scenario, an external call would follow.
}
}
contract TestApprovalPattern {
NonStandardToken public token;
address public dummyApprovalAddress = address(0xdead);
constructor() {
token = new NonStandardToken();
// Mint tokens for this contract.
token._balances[address(this)] = 1000;
token._totalSupply = 1000;
}
// Simulate setting a non-zero allowance and then attempting a swap.
function testInsecureApproval() external returns (string memory) {
// Set an initial allowance manually (simulate previous state).
token.approve(dummyApprovalAddress, 100);
// Now, try to execute swap which calls safeApprove without resetting.
// This should revert due to our non-standard token logic.
ParaSwapUtilsForPoC.swap(dummyApprovalAddress, address(token), 200);
return "Swap executed";
}
}

Exploit Scenario Explanation

  • Setup:
    A NonStandardToken is deployed that disallows changing an allowance if it is already non-zero.

  • Attack Execution:
    The TestApprovalPattern contract first sets a non-zero allowance manually. It then calls the swap function from the library, which attempts to approve a new allowance without resetting.

  • Result:
    The call reverts with the error "Allowance not zero," demonstrating how the insecure approval pattern may fail with non-standard tokens.

Tools Used

Manual Code Review

Recommendations

  • Reset Allowance:
    First set the token allowance to zero before setting it to the desired amount:

IERC20(fromToken).safeApprove(approvalAddress, 0);
IERC20(fromToken).safeApprove(approvalAddress, fromAmount);
  • Alternative Pattern:
    Consider using transferFrom mechanisms if the integration permits, which avoids approval altogether.

  • Token Compatibility Checks:
    Where possible, ensure that the tokens interacted with conform to expected standards.

Updates

Lead Judging Commences

n0kto Lead Judge 5 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.