๐Ÿšฉ Challenge: โš–๏ธ Build a DEX

readme-4

Skills you'll gain
  • Build and understand an Automated Market Maker (AMM)

  • Learn about liquidity pools and impermanent loss

  • Design and build functions for swapping tokens and providing/withdrawing liquidity

Skill level
Intermediate
Time to complete
3 - 10 hours
Completed by
1004 builders

This challenge ships with context-aware AI support. Open it in your preferred AI coding tool and ask questions, request hints, or get explanations at any point along the way.

๐Ÿ’ต Build an exchange that swaps ETH to tokens and tokens to ETH. ๐Ÿ’ฐ This is possible because the smart contract holds reserves of both assets and has a price function based on the ratio of the reserves. Liquidity providers are issued a token that represents their share of the reserves and fees.

๐Ÿงฎ You'll implement a minimal constant-product market maker (like Uniswap v2): the DEX holds reserves of both assets and uses a pricing curve (x * y = k). You'll add trading and liquidity provisioning, then try it out in the frontend UI.

๐ŸŒŸ The final deliverable is an app that lets users swap ETH โ†”๏ธŽ $BAL and provide/withdraw liquidity. Deploy to a public testnet, ship the frontend, and submit on SpeedrunEthereum.com!

๐Ÿ’ฌ Meet other builders working on this challenge and get help in the Challenge Telegram


Checkpoint 0: ๐Ÿ“ฆ Environment ๐Ÿ“š

๐Ÿงฐ Before you begin, you need to install the following tools:

Then download the challenge to your computer and install dependencies by running:

npx create-eth@2.0.4 -e challenge-dex challenge-dex
cd challenge-dex

in the same terminal, start your local network (a blockchain emulator in your computer):

yarn chain

in a second terminal window, ๐Ÿ›ฐ deploy your contract (locally):

cd challenge-dex
yarn deploy

in a third terminal window, start your ๐Ÿ“ฑ frontend:

cd challenge-dex
yarn start

๐Ÿ“ฑ Open http://localhost:3000 to see the app.

๐Ÿ‘ฉโ€๐Ÿ’ป Rerun yarn deploy --reset whenever you want to deploy new contracts to the frontend, update your current contracts with changes, or re-deploy it to get a fresh contract address.


โš ๏ธ We've disabled Cursor auto-suggestions (Tab completions and predictions) via .vscode/settings.json to reduce distractions while you code. AI chat and agent features are still enabled, and we've included AGENTS.md and CLAUDE.md files with project context to help AI assistants understand the codebase.

๐Ÿ”’ Want to disable AI and do everything yourself? (Recommended for deeper learning):

  • Cursor: add * to a .cursorignore file in the root of your project
  • VSCode: set chat.disableAIFeatures to true in .vscode/settings.json file

Checkpoint 1: ๐Ÿ”ญ The Structure ๐Ÿ“บ

๐Ÿงญ Navigate to the Debug Contracts tab, you should see two smart contracts displayed called DEX and Balloons.

  • packages/hardhat/contracts/Balloons.sol: a simple ERC20 that mints the deployer some $BAL
  • packages/hardhat/contracts/DEX.sol: the exchange contract you will implement

Below is what your front-end will look like without the implementation code within your smart contracts. The buttons will likely break because there are no functions tied to them yet! firstLoad

๐Ÿ—‚๏ธ You can find the page's code here: packages/nextjs/app/dex/page.tsx

๐Ÿ” You'll notice the frontend calls several functions that are defined in the DEX contract.

๐ŸŽฏ None of these functions have any logic inside them. That is your mission! Let's implement them in the following checkpoints.


Checkpoint 2: โš–๏ธ Reserves

๐Ÿฆ To start trading, the DEX needs reserves of both ETH and $BAL.

๐Ÿ’ง These reserves will provide liquidity that allows anyone to swap between the assets.

๐Ÿงพ Let's start with declaring our totalLiquidity and the liquidity of each user of our DEX!

๐ŸŒฑ Then you'll implement init(uint256 tokens) to seed the pool the first time.

๐Ÿ—๏ธ Implementing reserves + init()

  • Add totalLiquidity and the liquidity mapping.
  • Define these custom errors in DEX.sol:
    • DexAlreadyInitialized()
    • TokenTransferFailed()
  • init(tokens) should only work once (when totalLiquidity == 0). Revert if it is already initialized.
  • It should set totalLiquidity to the ETH balance of the contract (after receiving msg.value).
  • It should assign all initial liquidity to the initializer.
  • It should pull tokens from the initializer using transferFrom (requires ERC20 approve first).

๐Ÿงช Also implement getLiquidity(address lp) since the frontend and tests use it.

๐Ÿ”Ž Hint

๐Ÿ”‘ Before init() can pull tokens from a user, the user must call approve(spender, amount) on the Balloons contract.

โ„น๏ธ Also good to know:

  • ETH sent with the tx arrives in the contract before your function body runs, so address(this).balance already includes msg.value inside init().
๐ŸŽฏ Solution
/////////////////
/// Errors //////
/////////////////

error DexAlreadyInitialized();
error TokenTransferFailed();

//////////////////////
/// State Variables //
//////////////////////
// ...
uint256 public totalLiquidity;
mapping(address => uint256) public liquidity;
// ...

function init(uint256 tokens) public payable returns (uint256 initialLiquidity) {
    // Pool can only be initialized once.
    if (totalLiquidity != 0) revert DexAlreadyInitialized();

    // ETH arrives before function execution, so balance includes msg.value here.
    initialLiquidity = address(this).balance;
    totalLiquidity = initialLiquidity;
    liquidity[msg.sender] = initialLiquidity;

    if (!token.transferFrom(msg.sender, address(this), tokens)) revert TokenTransferFailed();
    return initialLiquidity;
}
// ...
function getLiquidity(address lp) public view returns (uint256 lpLiquidity) {
    return liquidity[lp];
}

๐Ÿงช Try it out

๐Ÿงฉ Go uncomment the line in packages/hardhat/deploy/00_deploy_dex.ts that sends 10 BAL to your frontend address (and make sure you paste in your actual frontend address).

๐Ÿ” Now redeploy (yarn deploy --reset) and visit http://localhost:3000/dex and use the Balloons contract to call approve() with:

  • spender = DEX address
  • amount = some $BAL (e.g. 5) balloons-dex-tab

๐Ÿค Get over 5 ETH from the faucet and then go to the DEX contract in the Debug tab (http://localhost:3000/debug) and call init() with equal amounts of ETH and $BAL:

  • tokens = 5 (* 10**18)
  • payable value = 5 (* 10**18) multiply-wei

โœ… Go back to the DEX tab http://localhost:3000/dex and verify:

  • the DEX shows ETH + $BAL reserves
  • your liquidity value (๐Ÿ’ฆ๐Ÿ’ฆ) is non-zero CheckLiquAndBalance

๐Ÿงช Testing your progress

โ–ถ๏ธ Run:

yarn test --grep "Checkpoint2"

Checkpoint 3: ๐Ÿค‘ price()

๐Ÿ“ˆ Now that our contract holds reserves of both ETH and BAL tokens, we want to use a simple formula to determine the exchange rate between the two.

โœจ This is where we will begin to understand the magic of AMMs. It utilizes the constant product formula credited to Martin Koppelmann and mentioned by Vitalik in this article which was later adopted by Uniswap.

๐Ÿงฎ Here is the formula:

x * y = k

๐Ÿ“ฆ where x and y are the reserves, so:

(amount of ETH in DEX ) * ( amount of tokens in DEX ) = k

๐Ÿงท The k is called an invariant because it doesn't change during trades. (The k only changes as liquidity is added.) If we plot this formula, we'll get a curve that looks something like:

image

๐Ÿ’ก We are just swapping one asset for another, the โ€œpriceโ€ is basically how much of the resulting output asset you will get if you put in a certain amount of the input asset.

๐Ÿคฏ This is the unlock! A market based on a curve like this will always have liquidity, but keep in mind, as the ratio becomes further unbalanced, you will get less and less of the less-liquid asset from the same trade amount. Again, if the smart contract has too much ETH and not enough $BAL tokens, the price to swap $BAL tokens to ETH should be more desirable.

๐Ÿ”„ When we call init() we passed in ETH and $BAL tokens at a ratio of 1:1. As the reserves of one asset changes, the other asset must also change inversely in order to maintain the constant product formula (invariant k).

๐Ÿ› ๏ธ Implementing the price() function

๐Ÿ› ๏ธ Now, we will edit the DEX.sol smart contract and fill out the price function!

๐Ÿง  The price function should take in the reserves of xReserves, yReserves, and xInput to calculate the yOutput.

Don't forget about trading fees! These fees are important to incentivize liquidity providers. Let's make the trading fee 0.3% and remember that there are no floats or decimals in Solidity, only whole numbers!

We should apply the fee to xInput, and store it in a new variable xInputWithFee. We want the input value to pay the fee immediately, or else we will accidentally tax our yOutput or our DEX's supply k ๐Ÿ˜จ Think about how to apply a 0.3% to our xInput.

Tip: Because there are no decimals in Solidity you can achieve the same outcome by multiplying the input by 997 (99.7% since we are deducting the 0.3% fee) and then dividing the result by 1000.

โœ… Your price(xInput, xReserves, yReserves) function should return the output amount of the y-asset.

๐Ÿ”Ž Hint

๐Ÿงฎ The standard Uniswap-style formula with fee looks like this:

  • xInputWithFee = xInput * 997
  • numerator = xInputWithFee * yReserves
  • denominator = (xReserves * 1000) + xInputWithFee
  • yOutput = numerator / denominator
๐ŸŽฏ Solution
function price(uint256 xInput, uint256 xReserves, uint256 yReserves) public pure returns (uint256 yOutput) {
    uint256 xInputWithFee = xInput * 997;
    uint256 numerator = xInputWithFee * yReserves;
    uint256 denominator = (xReserves * 1000) + xInputWithFee;
    return numerator / denominator;
}

๐Ÿงช Try it out

๐Ÿงฉ Uncomment the last part of packages/hardhat/deploy/00_deploy_dex.ts so that you don't have to add the liquidity manually.

๐Ÿ” Now redeploy and go to http://localhost:3000/dex and type values into the swap inputs. The curve preview should move and show output estimates (including the 0.3% fee).

Let's say we have 1 million ETH and 1 million tokens, if we put this into our price formula and ask it the price of 1000 ETH it will be an almost 1:1 ratio. Try it in the Debug tab:

price-example-1

If we put in 1000 ETH, we will receive 996 tokens. If we're paying a 0.3% fee, it should be 997 if everything was perfect. BUT, there is a tiny bit of slippage as our contract moves away from the original ratio. Let's dig in more to really understand what is going on here. Let's say there is 5 million ETH and only 1 million tokens. Then, we want to put 1000 tokens in. That means we should receive about 5000 ETH:

price-example-2

Finally, let's say the ratio is the same, but we want to swap 100,000 tokens instead of just 1000. We'll notice that the amount of slippage is much bigger. Instead of 498,000 back, we will only get 453,305 because we are making such a big dent in the reserves.

price-example-3

โ—๏ธ The contract automatically adjusts the price as the ratio of reserves shifts away from the equilibrium. It's called an ๐Ÿค– Automated Market Maker (AMM).

๐Ÿงช Testing your progress

โ–ถ๏ธ Run:

yarn test --grep "Checkpoint3"

Checkpoint 4: ๐Ÿค Swapping

๐Ÿ› ๏ธ Now implement the swap functions:

  • ethToToken() swaps ETH โ†’ $BAL
  • tokenToEth(tokenInput) swaps $BAL โ†’ ETH

๐Ÿง  Key idea: you must compute reserves from the contract before applying the input amount in the swap.

๐Ÿ—๏ธ Implementing ethToToken() + tokenToEth()

๐Ÿงพ Define these events in DEX.sol:

event EthToTokenSwap(address swapper, uint256 tokenOutput, uint256 ethInput);
event TokenToEthSwap(address swapper, uint256 tokensInput, uint256 ethOutput);

๐Ÿšซ Start by defining these custom errors in DEX.sol:

error TokenTransferFailed(); // Should already exist
error InvalidEthAmount();
error InvalidTokenAmount();
error InsufficientTokenBalance(uint256 available, uint256 required);
error InsufficientTokenAllowance(uint256 available, uint256 required);
error EthTransferFailed(address to, uint256 amount);

โœ๏ธ Now write the ethToToken() and tokenToEth() using those events and errors as nudges in the right direction. See if you can figure it all out without the hint!

๐Ÿ”Ž Hint

๐Ÿ’ก For ethToToken() the contract's ETH balance already includes msg.value, so the ETH reserve before the swap is:

  • ethReserve = address(this).balance - msg.value

๐Ÿ’ก For tokenToEth(tokenInput), you need an allowance check and should send ETH with call:

  • (bool sent,) = msg.sender.call{value: ethOutput}("");

๐Ÿ“ฃ Don't forget to emit the swap events.

๐ŸŽฏ Solution
function ethToToken() public payable returns (uint256 tokenOutput) {
    if (msg.value == 0) revert InvalidEthAmount();
    uint256 ethReserve = address(this).balance - msg.value;
    uint256 tokenReserve = token.balanceOf(address(this));
    tokenOutput = price(msg.value, ethReserve, tokenReserve);

    if (!token.transfer(msg.sender, tokenOutput)) revert TokenTransferFailed();
    emit EthToTokenSwap(msg.sender, tokenOutput, msg.value);
    return tokenOutput;
}

function tokenToEth(uint256 tokenInput) public returns (uint256 ethOutput) {
    if (tokenInput == 0) revert InvalidTokenAmount();
    uint256 bal = token.balanceOf(msg.sender);
    if (bal < tokenInput) revert InsufficientTokenBalance(bal, tokenInput);
    uint256 allow = token.allowance(msg.sender, address(this));
    if (allow < tokenInput) revert InsufficientTokenAllowance(allow, tokenInput);
    uint256 tokenReserve = token.balanceOf(address(this));
    ethOutput = price(tokenInput, tokenReserve, address(this).balance);
    if (!token.transferFrom(msg.sender, address(this), tokenInput)) revert TokenTransferFailed();
    (bool sent, ) = msg.sender.call{ value: ethOutput }("");
    if (!sent) revert EthTransferFailed(msg.sender, ethOutput);
    emit TokenToEthSwap(msg.sender, tokenInput, ethOutput);
    return ethOutput;
}

๐Ÿงช Try it out

๐ŸŒ Go to http://localhost:3000/dex and try:

  • ethToToken: enter some ETH and click Send
  • tokenToETH: approve the DEX in the Balloons section first, then swap tokens back to ETH
  • Check the Events tab to make sure the swap events are showing up as expected

๐ŸŽ‰ Now you can actually swap back and forth between tokens and ETH. This is amazing! With minimal infrastructure you can make it possible for people to move in and out of any set of tokens using this simple x * y = k formula!

๐Ÿงช Testing your progress

โ–ถ๏ธ Run:

yarn test --grep "Checkpoint4"

Checkpoint 5: ๐ŸŒŠ Liquidity

๐Ÿงฉ So far, only the init() function controls liquidity. To make this more decentralized, lets add make it so anyone can add to the liquidity pool by sending the DEX both ETH and tokens at the correct ratio.

๐Ÿง  The important part is letting anyone add liquidity and allowing them to later remove liquidity while keeping the pool ratio consistent.

  • deposit() takes ETH (msg.value) and pulls the right amount of $BAL from the depositor, minting LPTs to the sender in the right proportion
  • withdraw(amount) burns LPTs and returns ETH + $BAL proportional to pool reserves

๐Ÿ—๏ธ Implementing deposit() + withdraw()

๐Ÿงพ First define these events in DEX.sol:

event LiquidityProvided(address liquidityProvider, uint256 liquidityMinted, uint256 ethInput, uint256 tokensInput);
event LiquidityRemoved(address liquidityRemover, uint256 liquidityWithdrawn, uint256 tokensOutput, uint256 ethOutput);

๐Ÿšซ And then add this custom error:

error InsufficientLiquidity(uint256 available, uint256 required);

โœ๏ธ Now go write the logic for the deposit() and withdraw() functions.

๐Ÿง  Make sure you check that they have enough BAL tokens approved (and in their balance) to provide liquity to both sides so that x * y = k stays true even though k is moving this time.

๐Ÿงฎ On the deposit side, when finding out how many BAL tokens to add, make sure you add an additional wei to the result.

Solidity does integer division, which always rounds down. That means msg.value * tokenReserve / ethReserve can be ever-so-slightly smaller than the โ€œtrueโ€ proportional amount (because the fractional remainder is discarded). If we used the rounded-down value, the LP would deposit a tiny bit too little $BAL for the ETH they're adding, nudging the pool ratio and potentially failing later checks that assume the pool stays properly collateralized. Adding + 1 is a cheap way to effectively โ€œround upโ€ by the minimum unit, keeping deposits safely on the conservative side (at the cost of at most 1 wei of token).

Don't forget to add the new liquidity (represented by the ๐Ÿ’ฆ token) to the users balance AND the total liquidity (effectively, this is k).

๐Ÿ”Ž Hint

๐Ÿ’ก For deposit() you can derive the token deposit required from the existing reserves:

  • tokenDeposit = (msg.value * tokenReserve / ethReserve) + 1

๐Ÿงพ And the liquidity minted:

  • liquidityMinted = msg.value * totalLiquidity / ethReserve
๐ŸŽฏ Solution
    function deposit() public payable returns (uint256 tokensDeposited) {
        if (msg.value == 0) revert InvalidEthAmount();
        uint256 ethReserve = address(this).balance - msg.value;
        uint256 tokenReserve = token.balanceOf(address(this));

        uint256 tokenDeposit = (msg.value * tokenReserve / ethReserve) + 1;

        uint256 bal = token.balanceOf(msg.sender);
        if (bal < tokenDeposit) revert InsufficientTokenBalance(bal, tokenDeposit);
        uint256 allow = token.allowance(msg.sender, address(this));
        if (allow < tokenDeposit) revert InsufficientTokenAllowance(allow, tokenDeposit);

        uint256 liquidityMinted = msg.value * totalLiquidity / ethReserve;
        liquidity[msg.sender] += liquidityMinted;
        totalLiquidity += liquidityMinted;

        if (!token.transferFrom(msg.sender, address(this), tokenDeposit)) revert TokenTransferFailed();
        emit LiquidityProvided(msg.sender, liquidityMinted, msg.value, tokenDeposit);
        return tokenDeposit;
    }

function withdraw(uint256 amount) public returns (uint256 ethAmount, uint256 tokenAmount) {
    uint256 availableLp = liquidity[msg.sender];
    if (availableLp < amount) revert InsufficientLiquidity(availableLp, amount);
        uint256 ethReserve = address(this).balance;
        uint256 tokenReserve = token.balanceOf(address(this));

    uint256 ethWithdrawn = amount * ethReserve / totalLiquidity;
    uint256 tokensWithdrawn = amount * tokenReserve / totalLiquidity;

        liquidity[msg.sender] -= amount;
        totalLiquidity -= amount;

        (bool sent, ) = payable(msg.sender).call{ value: ethWithdrawn }("");
    if (!sent) revert EthTransferFailed(msg.sender, ethWithdrawn);
    if (!token.transfer(msg.sender, tokensWithdrawn)) revert TokenTransferFailed();

    emit LiquidityRemoved(msg.sender, amount, tokensWithdrawn, ethWithdrawn);
    return (ethWithdrawn, tokensWithdrawn);
}

๐Ÿงช Try it out

๐Ÿ” Redeploy, then go to http://localhost:3000/dex and try:

  • Approve the DEX to spend your $BAL
  • Deposit some ETH (and observe that it also pulls tokens)
  • Withdraw some liquidity and verify you receive both ETH and $BAL back
  • Check the Events tab to make sure the events are showing up as expected

๐Ÿงช Testing your progress

โ–ถ๏ธ Run:

yarn test --grep "Checkpoint5"

Checkpoint 6: ๐Ÿ’พ Deploy your contracts! ๐Ÿ›ฐ

๐Ÿ“ก Edit the defaultNetwork in hardhat.config.ts to match the name of one of testnets from the networks object. We recommend to use "sepolia" or "optimismSepolia"

๐Ÿ” You will need to generate a deployer address using yarn generate This creates a mnemonic and saves it locally.

๐Ÿ‘ฉโ€๐Ÿš€ Use yarn account to view your deployer account balances.

โ›ฝ๏ธ You will need to send ETH to your deployer address with your wallet, or get it from a public faucet of your chosen network. You can also request ETH by sending a message with your new deployer address and preferred network in the challenge Telegram. People are usually more than willing to share.

๐Ÿš€ Run yarn deploy to deploy your smart contract to a public network (selected in hardhat.config.ts)

๐Ÿ’ฌ Hint: Instead of editing hardhat.config.ts you can just add a network flag to the deploy command like this: yarn deploy --network sepolia or yarn deploy --network optimismSepolia


Checkpoint 7: ๐Ÿšข Ship your frontend! ๐Ÿš

โœ๏ธ Edit your frontend config in packages/nextjs/scaffold.config.ts to change the targetNetwork to chains.sepolia (or chains.optimismSepolia if you deployed to OP Sepolia)

๐Ÿ’ป View your frontend at http://localhost:3000 and verify you see the correct network.

๐Ÿ“ก When you are ready to ship the frontend app...

๐Ÿ“ฆ Run yarn vercel to package up your frontend and deploy.

You might need to log in to Vercel first by running yarn vercel:login. Once you log in (email, GitHub, etc), the default options should work.

If you want to redeploy to the same production URL you can run yarn vercel --prod. If you omit the --prod flag it will deploy it to a preview/test URL.

Follow the steps to deploy to Vercel. It'll give you a public URL.

๐ŸฆŠ Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default ๐Ÿ”ฅ burner wallets are only available on hardhat . You can enable them on every chain by setting onlyLocalBurnerWallet: false in your frontend config (scaffold.config.ts in packages/nextjs/)

โš™๏ธ Configuration of Third-Party Services for Production-Grade Apps.

By default, ๐Ÿ— Scaffold-ETH 2 provides predefined API keys for popular services such as Alchemy and Etherscan. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services. This is great to complete your Speedrun Ethereum.

For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at:

  • ๐Ÿ”ทALCHEMY_API_KEY variable in packages/hardhat/.env and packages/nextjs/.env.local. You can create API keys from the Alchemy dashboard.
  • ๐Ÿ“ƒETHERSCAN_API_KEY variable in packages/hardhat/.env with your generated API key. You can get your key here.

๐Ÿ’ฌ Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing.


Checkpoint 8: ๐Ÿ“œ Contract Verification

Run the yarn verify --network your_network command to verify your contracts on etherscan ๐Ÿ›ฐ

๐Ÿ‘€ You may see an address for both YourToken and Vendor. You will want the Vendor address.

๐Ÿ‘‰ Search this address on Sepolia Etherscan (or Optimism Sepolia Etherscan if you deployed to OP Sepolia) to get the URL you submit to ๐Ÿƒโ€โ™€๏ธSpeedrunEthereum.com.

Checkpoint 10: Deeper Understanding and Next Steps

๐ŸŽ‰ So you have built a functioning DEX. What an accomplishment! Now you have great context to begin to understand different DEX designs. Go research the latest DEX designs and their improvements.

๐Ÿงฉ Something you will run into quickly are some limitations that your own DEX has.

๐Ÿ“– Because the swap functions don't promise a certain amount of "out" tokens anyone who sees your transaction in the mempool can send a transaction in front of you (e.g. with a higher gas price) and move the liquidity down the curve so that you get fewer tokens. Then immediately after your transaction executes they can have another transaction lined up to swap the tokens back in the other direction. The outcome is that they walk away with the "slippage", the difference between the amount of tokens you were quoted originally and how many you actually ended up with in the end.

Side note: This is called a sandwich attack and is a popular form of MEV (Look it up!).

๐Ÿ’ก The solution is obvious: Just revert the function if the quote price != the amount of "out" tokens. This would work to protect you from sandwich attacks but it introduces another problem...

๐Ÿคผ Two honest people want to make a swap. The amounts don't matter and the direction doesn't matter. They get their quotes and execute it at nearly the same time (both transactions will be included in the same block). But wait, if one of them swaps then the liquidity will have changed when the second person's swap is attempted, causing it to revert because the "out" tokens would differ from the quote they received. Only one person can execute a swap in a single block. What a huge bottleneck!

๐ŸŽฏ A fuller solution is to allow some "slippage" in the swap. This way your transaction will execute as long as the execution price is within a certain percent of the quote price. This way you know that you will receive at least X amount of tokens with each swap you do and others can still swap in either direction before and after you in the same block. Bingo!


๐Ÿƒ Head to your next challenge here. ๐Ÿ’ฌ Problems, questions, comments on the stack? Post them to the ๐Ÿ— scaffold-eth developers chat

๐Ÿš€ Ready to submit your challenge?

You're viewing this challenge as a guest. Want to start building your onchain portfolio?

Connect your wallet and register to unlock the full Speedrun Ethereum experience.