All Articles

Tokens missent to the 1inch Aggregation Router? Forget about them

One of the most notorious missteps in the blockchain space is accidentally transferring tokens to the wrong address. Sometimes users send their funds directly to a contract instead of calling it properly. And the more widely used the contract, the more often this happens.

Among Ethereum’s top gas guzzlers are 1inch Aggregation Routers. And yes, you guessed it — people keep sending tokens there by mistake. Sometimes it’s $20K USDT, sometimes $5K USDT.

Luckily, 1inch Aggregation Routers include a function designed to recover accidentally sent funds:

/**
 * @notice Retrieves funds accidentally sent directly to the contract address
 * @param token ERC20 token to retrieve
 * @param amount amount to retrieve
 */
function rescueFunds(IERC20 token, uint256 amount) external onlyOwner {
    token.uniTransfer(payable(msg.sender), amount);
}

Seems safe, right? But what if we told you anyone can rescue those funds?

Case in spotlight: $500K in WBTC lost to a callback trick

While digging into interactions with Ethereum’s popular gas guzzlers, we found a user who sent 4.2 WBTC to 1inch Aggregation Router V6 in November 2024.

Were they recovered? Well… not by the owner. In fact, a savvy searcher pocketed the funds for themselves — and did it pretty easily by creating a contract that called this method:

/**
 * @notice Called by Curve pool during swap operation initiated by _curfe.
 * @dev This can be called by anyone assuming no tokens are stored on this contract between transactions.
 * @param inCoin Address of the token to be exchanged.
 * @param dx Amount of tokens to be exchanged.
 */
function curveSwapCallback(
    address /* sender */,
    address /* receiver */,
    address inCoin,
    uint256 dx,
    uint256 /* dy */
) external {
    IERC20(inCoin).safeTransfer(msg.sender, dx);
}

Done. Funds “rescued”. Not to mention, one can skip the contract altogether and call the function directly.

Okay, but that’s about V6, right?

Correct. This callback exists in Aggregation Router V6. Then you might think older routers like v4 and v5 must still be holding funds until the owner runs rescueFunds. Not always.

Here’s the catch: in other routers, including Uniswap’s SwapRouter, pool addresses are calculated or fetched dynamically during the swap. But in 1inch Aggregation Routers, pool addresses are passed externally.

That design opens the door for the method that accepts a chain of pools as a parameter:

/// @param amount Amount of source tokens to swap
/// @param minReturn Minimal allowed returnAmount to make transaction commit
/// @param pools Pools chain used for swaps. Pools src and dst tokens should match.
function uniswapV3Swap(
    uint256 amount,
    uint256 minReturn,
    uint256[] calldata pools
) external payable returns(uint256 returnAmount);

During a swap, UniswapV3 requires you to send your liquidity via uniswapV3SwapCallback. For the first pool, 1inch uses msg.sender’s funds, and the returnAmount comes from the UniswapV3 pool.

Normally you can’t control this. Unless you create your own “pool”:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;


contract CustomPool {
    function swap(
        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external returns (int256 amount0, int256 amount1) {
        amount0 = -amountSpecified;
        amount1 = -amountSpecified;
    }
}

This fake pool just echoes back the value it received. Put it first in the chain, and it returns whatever you need without calling uniswapV3SwapCallback. After that, the next pool can be called normally, and voilà — the mistakenly sent tokens are “recovered” and swapped.

Is this a bug? Not really, since 1inch even acknowledged this in v6. So it’s more of a tradeoff for gas efficiency than a vulnerability. But it’s definitely something worth knowing.

A little stat drop

We found users accidentally sent about $520K in tokens (adjusted prices) to routers v4, v5, and v6. It seems all of it was “rescued” by opportunistic users who knew how to drain funds using fake pools or trigger the callback function. 

Here’s a detailed breakdown:

Router V4

Total: $1,836.23

TokenAmount (USD)Contract
WETH$1,481.730xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
USDT$182.640xdac17f958d2ee523a2206206994597c13d831ec7
USDC$121.160xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
MATIC$50.700x7d1afa7b718fb893db30a3abc0cfc608aacfebb0
Router V5

Total: $38,953.55

TokenAmount (USD)Contract
USDT$30,301.680xdac17f958d2ee523a2206206994597c13d831ec7
USDC$6,215.330xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
PEPE$2,078.440x6982508145454ce325ddbe47a25d4ec3d2311933
VOLT$115.870x7f792db54b0e580cdc755178443f0430cf799aca
PAXG$107.740x45804880de22913dafe09f4980848ece6ecbaf78
CRO$78.570xa0b73e1ff0b80914ab6fe0444e65848c4c34450b
SFI$55.740xb753428af26e81097e7fd17f40c88aaa3e04902c
Router V6

Total: $479,273.73

TokenAmount (USD)Contract
WBTC$444,960.240x2260fac5e5542a773aa44fbcfedf7c193bc2c599
DOLA$19,991.460x865377367054516e17014ccded1e7d814edc9ce4
USDT$5,431.150xdac17f958d2ee523a2206206994597c13d831ec7
USDC$4,180.020xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
T$2,156.030xcdf7028ceab81fa0c6971208e83fa7872994bee
PEPE$1,450.390x6982508145454ce325ddbe47a25d4ec3d2311933
WETH$742.630xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
RBX$361.810x3ba925fdeae6b46d0bb4d424d829982cb2f7309e

If you’d like us to perform this level of analysis for a security audit of your own contract — get in touch!

Start work

One click to start work with us

Same Articles

All Articles