🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Token Swaps

A token swap is a trade that takes one token and returns another token at the pool ratio. A swapper contract carries out the trade through a pool on the trader behalf.

A swap through the swapper moves both balances

A trader swaps 100 token A through the swapper. The trader gains 100 token B.

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}


contract Swapper {
    ToyToken public tokenA;
    ToyToken public tokenB;
    Pool public pool;
    constructor(address a, address b, address poolAddr) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
        pool = Pool(poolAddr);
    }
    function quoteAForB(uint256 amountIn) public view returns (uint256) {
        return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
    }
    function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenA.approve(address(pool), amountIn);
        amountOut = pool.swapAForB(amountIn);
        require(amountOut >= minOut, "output too low");
        tokenB.transfer(msg.sender, amountOut);
    }
}

A swap below the minimum output is refused

A swap that would return less than the minimum is refused. The pool reserves stay exactly as they were.

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}


contract Swapper {
    ToyToken public tokenA;
    ToyToken public tokenB;
    Pool public pool;
    constructor(address a, address b, address poolAddr) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
        pool = Pool(poolAddr);
    }
    function quoteAForB(uint256 amountIn) public view returns (uint256) {
        return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
    }
    function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenA.approve(address(pool), amountIn);
        amountOut = pool.swapAForB(amountIn);
        require(amountOut >= minOut, "output too low");
        tokenB.transfer(msg.sender, amountOut);
    }
}

A quote changes after a swap runs

A quote predicts the output before any trade. After a swap, the same quote predicts a smaller output.

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}


contract Swapper {
    ToyToken public tokenA;
    ToyToken public tokenB;
    Pool public pool;
    constructor(address a, address b, address poolAddr) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
        pool = Pool(poolAddr);
    }
    function quoteAForB(uint256 amountIn) public view returns (uint256) {
        return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
    }
    function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenA.approve(address(pool), amountIn);
        amountOut = pool.swapAForB(amountIn);
        require(amountOut >= minOut, "output too low");
        tokenB.transfer(msg.sender, amountOut);
    }
}

Example

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}


contract Swapper {
    ToyToken public tokenA;
    ToyToken public tokenB;
    Pool public pool;
    constructor(address a, address b, address poolAddr) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
        pool = Pool(poolAddr);
    }
    function quoteAForB(uint256 amountIn) public view returns (uint256) {
        return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
    }
    function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenA.approve(address(pool), amountIn);
        amountOut = pool.swapAForB(amountIn);
        require(amountOut >= minOut, "output too low");
        tokenB.transfer(msg.sender, amountOut);
    }
}

Key points

  • Swapper: a contract that trades through a pool for a user.
  • Quote: the output a pool ratio predicts before a trade runs.
  • Minimum out: the smallest output a trader accepts for a swap.
  • Slippage: the price moving between a quote and execution.
  • Failed swap: a swap below the minimum output is refused and changes nothing.

In this example, a swapper quotes 80 token B for 50 token A, then runs the swap with a minimum of 70. The trader ends with 750 token A and 680 token B.

Note

this swapper trades toy tokens on a local chain only, with no real funds.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

What does a minimum-out check protect against?