Token swaps

How on-chain token exchange works — from the user's approve() call to the final transferFrom().

The full swap flow

A token swap through an AMM like those powering Doodledapp.com involves a specific sequence of calls. Understanding this flow is essential for building frontends and for auditing swap contracts.

solidity
// Step 1: User approves the router to spend their tokenIn
token.approve(address(router), amountIn);

// Step 2: User calls the router's swap function
router.swapExactTokensForTokens(
    amountIn,        // exact amount in
    minAmountOut,    // minimum acceptable output (slippage protection)
    path,            // [tokenA, tokenB] or [tokenA, WETH, tokenB] for multi-hop
    msg.sender,      // recipient
    deadline         // transaction must execute before this timestamp
);

// Step 3: Inside the router
// - Pulls tokenIn from user via transferFrom
// - Calls pair.swap() with computed output amount
// - Pair transfers tokenOut to recipient

Exact input vs. exact output swaps

solidity
// Exact input: specify how much you're spending, get at least minOut
function swapExactTokensForTokens(
    uint256 amountIn,
    uint256 amountOutMin,
    address[] calldata path,
    address to,
    uint256 deadline
) external returns (uint256[] memory amounts);

// Exact output: specify how much you want, spend at most maxIn
function swapTokensForExactTokens(
    uint256 amountOut,
    uint256 amountInMax,
    address[] calldata path,
    address to,
    uint256 deadline
) external returns (uint256[] memory amounts);

Multi-hop swaps

Not every token pair has a direct pool. Routers can chain through multiple pools to find a path.

solidity
// Swap TOKEN_A → WETH → TOKEN_B
address[] memory path = new address[](3);
path[0] = TOKEN_A;
path[1] = WETH;
path[2] = TOKEN_B;

router.swapExactTokensForTokens(amountIn, minOut, path, recipient, deadline);

Calling a swap from another contract

Protocols routinely call swap functions programmatically — for example, auto-compounding yield, liquidating collateral, or rebalancing portfolios.

solidity
import '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol';

contract AutoCompound {
    IUniswapV2Router02 public immutable router;

    constructor(address _router) {
        router = IUniswapV2Router02(_router);
    }

    function swapRewards(
        address rewardToken,
        address targetToken,
        uint256 amount
    ) internal {
        IERC20(rewardToken).approve(address(router), amount);

        address[] memory path = new address[](2);
        path[0] = rewardToken;
        path[1] = targetToken;

        router.swapExactTokensForTokens(
            amount,
            0,              // accept any output (use slippage protection in production!)
            path,
            address(this),
            block.timestamp
        );
    }
}

Slippage and deadlines in production

Always set a meaningful minAmountOut and deadline in production. Zero slippage protection invites sandwich attacks. An expired deadline prevents a transaction from being included in a block far later than expected (when market conditions may have changed dramatically).

solidity
// 0.5% slippage tolerance
uint256 amountOutMin = (expectedAmountOut * 995) / 1000;

// Must execute within 20 minutes
uint256 deadline = block.timestamp + 20 minutes;
←   Liquidity poolsDeploying to testnet   →