L11: Building DeFi

Deploy multiple interacting contracts on Remix — an ERC-20 token and a minimal DEX — and observe how AMM mechanics work by tracking reserve changes during swaps.

Tool Overview

What you will use in this lesson

  • Remix IDE — deploy and interact with multiple contracts from one interface
  • Etherscan Sepolia — read emitted events, verify transactions, and inspect contract state
  • MetaMask — switch accounts to simulate different users (LP, trader, owner)
Before you start: Complete L10 Activity 3 to get Sepolia test ETH, and have MetaMask connected to Sepolia. You will need at least 0.05 SepoliaETH to cover gas for multiple deployments.

Guided Activities

1

Deploy Two ERC-20 Tokens

Tool: Remix IDE
  1. Open Remix and create a new file TokenA.sol. Paste a minimal ERC-20:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract TokenA { string public name = "Token A"; string public symbol = "TKA"; uint8 public decimals = 18; 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); event Approval(address indexed owner, address indexed spender, uint256 value); constructor(uint256 _supply) { totalSupply = _supply * 10**18; balanceOf[msg.sender] = totalSupply; emit Transfer(address(0), msg.sender, totalSupply); } function transfer(address to, uint256 amount) public returns (bool) { 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; emit Approval(msg.sender, spender, amount); return true; } function transferFrom(address from, address to, uint256 amount) public returns (bool) { allowance[from][msg.sender] -= amount; balanceOf[from] -= amount; balanceOf[to] += amount; emit Transfer(from, to, amount); return true; } }
  1. Compile TokenA.sol. Deploy it with constructor argument 1000000 (1 million tokens). Record the deployed address as Token A address.
  2. Create TokenB.sol — copy the same contract, change name to "Token B" and symbol to "TKB". Deploy with 1000000 tokens. Record as Token B address.
  3. Verify both appear at sepolia.etherscan.io by searching each address.
Reflection question: Both tokens start with identical code. What makes one "Token A" and the other "Token B" at the protocol level? Is the name on-chain authoritative, or just cosmetic?
2

Deploy a Minimal AMM DEX

Tool: Remix IDE
  1. Create SimpleDEX.sol and paste the following constant-product AMM:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IERC20 { function transferFrom(address, address, uint256) external returns (bool); function transfer(address, uint256) external returns (bool); function balanceOf(address) external view returns (uint256); } contract SimpleDEX { IERC20 public tokenA; IERC20 public tokenB; uint256 public reserveA; uint256 public reserveB; event LiquidityAdded(address indexed provider, uint256 amountA, uint256 amountB); event Swap(address indexed trader, address tokenIn, uint256 amountIn, uint256 amountOut); constructor(address _tokenA, address _tokenB) { tokenA = IERC20(_tokenA); tokenB = IERC20(_tokenB); } function addLiquidity(uint256 amountA, uint256 amountB) external { tokenA.transferFrom(msg.sender, address(this), amountA); tokenB.transferFrom(msg.sender, address(this), amountB); reserveA += amountA; reserveB += amountB; emit LiquidityAdded(msg.sender, amountA, amountB); } function swapAforB(uint256 amountAIn) external returns (uint256 amountBOut) { // x * y = k => amountOut = reserveB - k / (reserveA + amountIn) uint256 k = reserveA * reserveB; tokenA.transferFrom(msg.sender, address(this), amountAIn); reserveA += amountAIn; amountBOut = reserveB - (k / reserveA); reserveB -= amountBOut; tokenB.transfer(msg.sender, amountBOut); emit Swap(msg.sender, address(tokenA), amountAIn, amountBOut); } function swapBforA(uint256 amountBIn) external returns (uint256 amountAOut) { uint256 k = reserveA * reserveB; tokenB.transferFrom(msg.sender, address(this), amountBIn); reserveB += amountBIn; amountAOut = reserveA - (k / reserveB); reserveA -= amountAOut; tokenA.transfer(msg.sender, amountAOut); emit Swap(msg.sender, address(tokenB), amountBIn, amountAOut); } }
  1. Deploy SimpleDEX with the two token addresses as constructor arguments: paste Token A address, then Token B address.
  2. Record the deployed DEX address.
Reflection question: The DEX contract uses the formula k = reserveA * reserveB. What is the mathematical invariant this enforces? What happens to the price of Token B as you buy more of it?
3

The approve + transferFrom Flow

Tool: Remix Deploy Tab
  1. To add liquidity, the DEX needs permission to pull your tokens. This requires a two-step ERC-20 approval pattern.
  2. In Remix, select the deployed Token A contract. Call approve with:
    spender = DEX contract address, amount = 100000000000000000000000 (100,000 tokens with 18 decimals).
  3. Do the same for Token B — approve the DEX address for 100,000 TKB.
  4. Now select the SimpleDEX contract. Call addLiquidity with amountA = 50000000000000000000000 and amountB = 50000000000000000000000 (50,000 of each).
  5. Call reserveA and reserveB — both should now show 50,000 tokens worth of reserves.
Reflection question: Why does the ERC-20 standard use a two-step approve then transferFrom instead of a single call? What attack does this prevent?
4

Execute a Swap and Observe Reserve Changes

Tool: Remix + Etherscan Sepolia
  1. Before swapping, note the current values of reserveA and reserveB by calling them on the DEX. Calculate k = reserveA * reserveB.
  2. Approve the DEX to spend 1,000 Token A: call approve on Token A with amount = 1000000000000000000000.
  3. On the DEX, call swapAforB with amountAIn = 1000000000000000000000.
  4. After the swap, call reserveA and reserveB again. Observe: reserveA increased by 1,000 and reserveB decreased. Calculate the new k — is it exactly the same?
  5. Go to sepolia.etherscan.io, search the DEX address, click the Events tab, and find the Swap event. Read the amountIn and amountOut fields in the event log.
Reflection question: You swapped 1,000 Token A but received slightly less than 1,000 Token B. Why? What determines exactly how much Token B you receive? How would a larger pool (higher reserves) affect your output?
5

Debugging Cross-Contract Calls

Tool: Remix Debugger
  1. Deliberately trigger a failing transaction: try calling swapAforB with more tokens than you approved (e.g., approve 100, try to swap 1000). Remix will show a red error in the console.
  2. Click the Debug button that appears next to the failed transaction in the Remix console. This opens the Remix Debugger.
  3. Step through the transaction with the arrow buttons. Watch the call stack — you will see the DEX calling transferFrom on Token A, which reverts because the allowance is too low.
  4. Common cross-contract errors to look for:
    • Insufficient allowance — approve step was skipped or amount was too low
    • Wrong address — pasting the wrong contract address as an argument
    • Integer overflow on division — if reserves are very small, AMM math can underflow
    • Re-entrancy — malicious token contracts calling back into the DEX during transfer
  5. Fix the allowance and retry the swap successfully. Confirm the Remix console shows a green transaction receipt.
Reflection question: The Remix debugger shows every opcode execution. In production, Ethereum nodes execute the same opcodes on the real chain. How does this transparency help auditors find vulnerabilities — and how does it also help attackers?
Home Lessons Quizzes Glossary © Joerg Osterrieder 2025-2026. All rights reserved.