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:
- Users typically get quotes via scripts before submitting transactions
- Pool state can change between the script call and transaction execution (other txns complete first)
- The
amountOutMin parameter exists and is computed by callers but then discarded
- 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
Summary
The
UniswapV2SwapConnectors.swapExactTokensForTokens()function receives anamountOutMinparameter but completely ignores it, passingUInt256(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
Location
File:
cadence/contracts/connectors/evm/UniswapV2SwapConnectors.cdcLine: 244
The Bug
The callers (
swap()andswapBack()) properly computeamountOutMin:But
swapExactTokensForTokens()discards this value entirely.Impact Scenario
Comparison with UniswapV3SwapConnectors (Fixed)
The V3 connector correctly uses the parameter (after PR #82):
Suggested Fix
Related