Skip to content

BUG: UniswapV2SwapConnectors ignores amountOutMin parameter #83

@liobrasil

Description

@liobrasil

Summary

The UniswapV2SwapConnectors.swapExactTokensForTokens() function receives an amountOutMin parameter but completely ignores it, passing UInt256(0) to the EVM router call. This means users have no protection against price movement between quote and execution.

Severity

MEDIUM - Users can receive less than expected if pool state changes between quote and swap

Note: Flow transactions are atomic - state cannot change mid-transaction. However, this is still a bug because:

  1. Users typically get quotes via scripts before submitting transactions
  2. Pool state can change between the script call and transaction execution (other txns complete first)
  3. The amountOutMin parameter exists and is computed by callers but then discarded
  4. Behavior differs from UniswapV3SwapConnectors which correctly enforces the minimum

Location

File: cadence/contracts/connectors/evm/UniswapV2SwapConnectors.cdc
Line: 244

The Bug

access(self) fun swapExactTokensForTokens(
    exactVaultIn: @{FungibleToken.Vault},
    amountOutMin: UFix64,  // ← Parameter is RECEIVED...
    reverse: Bool
): @{FungibleToken.Vault} {
    ...
    // perform the swap
    res = self.call(to: self.routerAddress,
        signature: "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)",
        args: [
            evmAmountIn,
            UInt256(0),  // ← ...but IGNORED! Hardcoded to zero
            (reverse ? self.addressPath.reverse() : self.addressPath),
            coa.address(),
            UInt256(getCurrentBlock().timestamp)
        ],
        ...
    )

The callers (swap() and swapBack()) properly compute amountOutMin:

// Line 168 - swap()
let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: inVault.balance, reverse: false).outAmount
return <-self.swapExactTokensForTokens(exactVaultIn: <-inVault, amountOutMin: amountOutMin, reverse: false)

// Line 186 - swapBack()  
let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: residual.balance, reverse: true).outAmount
return <-self.swapExactTokensForTokens(exactVaultIn: <-residual, amountOutMin: amountOutMin, reverse: true)

But swapExactTokensForTokens() discards this value entirely.

Impact Scenario

1. User calls script to get quote: 1000 FLOW → 800 USDC (with amountOutMin = 800)
2. User submits transaction with this quote
3. Before user's txn executes, other transactions change pool state
4. User's txn executes: amountOutMin=0 means ANY output accepted
   → User receives 700 USDC instead of transaction reverting
5. User expected their quoted minimum (800) to be enforced, but it wasn't

Comparison with UniswapV3SwapConnectors (Fixed)

The V3 connector correctly uses the parameter (after PR #82):

// UniswapV3SwapConnectors.cdc:518-522
let minOutUint = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
    amountOutMin,
    erc20Address: outToken
)
// ... then passes minOutUint to the router

Suggested Fix

access(self) fun swapExactTokensForTokens(
    exactVaultIn: @{FungibleToken.Vault},
    amountOutMin: UFix64,
    reverse: Bool
): @{FungibleToken.Vault} {
    ...
    // Convert amountOutMin to EVM units
    let outTokenAddress = reverse ? self.addressPath[0] : self.addressPath[self.addressPath.length - 1]
    let minOutUint = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
        amountOutMin,
        erc20Address: outTokenAddress
    )
    
    // perform the swap
    res = self.call(to: self.routerAddress,
        signature: "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)",
        args: [
            evmAmountIn,
            minOutUint,  // ← Use the converted amountOutMin
            (reverse ? self.addressPath.reverse() : self.addressPath),
            coa.address(),
            UInt256(getCurrentBlock().timestamp)
        ],
        ...
    )

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions