onflow / onflow/FlowActions

BUG: UniswapV2SwapConnectors ignores amountOutMin parameter

Open
#83 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Cadence
Stars
9
Forks
3
Avg merge
1h 45m
Merged PRs (30d)
2

Description

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

  • PR #82 fixed the equivalent issue in UniswapV3SwapConnectors
  • This is the V2 counterpart that was missed

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in cadence/contracts/connectors/evm/UniswapV2SwapConnectors.cdc around line 244 and compare the V2 implementation with UniswapV3SwapConnectors.cdc around lines 518-522. Trace how swap() and swapBack() provide amountOutMin. Done means the V2 router call uses the converted minimum-output value instead of a hardcoded zero.

Written by the indexing model from the issue text.

Assessment

Domain
blockchain
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.