/Ethernaut-with-Hardhat

Suite for solving Ethernaut CTF challenges

Primary LanguageSolidity

About this repo

Suite to solve Ethernaut challenges.

Prior to using this Ethernaut solving suite:

  1. Create and fill a .env file with the data specified in .env.example on the root folder
  2. Run yarn on this folder.
  • If you wanna skip Etherscan verifications, don't fill out the ETHERSCAN_API_KEY variable.

File locations

  • contracts/ folder: Original and attacker contracts are inside the same file
  • deploy/ folder: Deploy scripts for attacks that require deploying another contract
  • scripts/ folder: Scripts that run the entire attack from start to finish (pwn)
  • .env: Environment variables. Located on the root folder of the project.

For any troubles, visit the Troubleshooting section.

Index

  1. Fallback
  2. Fallout
  3. CoinFlip
  4. Telephone
  5. Token
  6. Delegation
  7. Force
  8. Vault
  9. King
  10. Reentrance
  11. Elevator
  12. Privacy
  13. Gatekeeper One
  14. Gatekeeper Two
  15. Naught Coin
  16. Preservation
  17. Recovery
  18. Magic Number
  19. Alien Codex
  20. Denial
  21. Shop
  22. Dex
  23. Dex Two
  24. Puzzle Wallet
  25. Motorbike
  26. DoubleEntryPoint
  27. Good Samaritan

01 - Fallback

Lesson on calling the receive function.

  1. Change the address in scripts/fallback.js to your instance address
  2. Run yarn hardhat run scripts/fallback.js

02 - Fallout

Lesson on... dangers of not using constructor keyword? Paying attention to typos?

  1. Change the address in scripts/fallout.js to your instance address
  2. Run yarn hardhat run scripts/fallout.js

03 - Coinflip

Lesson on exploiting blockhash "randomness".

  1. Change the address in contracts/CoinFlip.sol to your instance address
  2. Run yarn hardhat run scripts/coinflip.js

Note: If one of the "guesses" fails, comment out the "deploy" section of the script and uncomment the "use existing contract" one, then populate it with your attacker contract's address. Or just go to Etherscan and call the "attack" write function every some seconds, your call.

04 - Telephone

Lesson on using a contract to attack another contract.

  1. Change the address in contracts/Telephone.sol to your instance address
  2. Run yarn hardhat run scripts/telephone.js

05 - Token

Lesson on underflow and overflow.

  1. Change the address in scripts/token.js to your instance address
  2. Run yarn hardhat run scripts/token.js

06 - Delegation

Lesson on delegatecall and calling through fallback function

  1. Change the address in scripts/delegation.js to your instance address
  2. Run yarn hardhat run scripts/delegation.js

07 - Force

Lesson on self-destruct.

  1. Change the address in contracts/Force.sol to your instance address
  2. Run yarn hardhat run scripts/force.js

08 - Vault

Lesson on private variables and storage slots.

  1. Change the address in scripts/vault.js to your instance address
  2. Run yarn hardhat run scripts/vault.js

09 - King

Lesson on payable keyword in constructor and disabling receive function

  1. Change the address in deploy/09-deploy-king.js to your instance address
  2. Run yarn hardhat run scripts/king.js

10 - Reentrance

Lesson on reentrancy attacks.

  1. Change the address in contracts/Reentrance.sol to your instance address
  2. Run yarn hardhat run scripts/reentrance.js

11 - Elevator

Lesson on interfaces (and them not having the view keyword...) and using a boolean toggle.

  1. Change the address in contracts/Elevator.sol to your instance address
  2. Run yarn hardhat run scripts/elevator.js

12 - Privacy

Lesson on size and ordering of storage slots.

  1. Change the address in scripts/privacy.js to your instance address
  2. Run yarn hardhat run scripts/privacy.js

13 - Gatekeeper One

Lesson on typecasting, data types, and gas calculation

  1. Change the address in contracts/GatekeeperOne.sol to your instance address
  2. Run yarn hardhat run scripts/gatekeeperOne.js

About

First condition is just calling from another contract. Done.

Getting the right gas

Copied solution from this guy that doesn't explain very well how it's done. My interpretation is that it just multiplies 3 times 8191 to get enough gas in any network conditions, then adds up from 150 up to 270 to cover the whole range the transaction could cost.

About getting the right bytes8 key

Condition 1:

  • Hexadecimal uses 4 bits per digit (0-9+a-f)
  • And it uses two digits per byte
  • If 1 byte consists in 2 digits, bytes8 has 16 digits

0xXXXXXXXXXXXXXXXX

  • 1 byte = 8 bits
  • 8 bytes = 64 bits

bytes8 has the same amount of digits as uint64

First requirement converts one argument from uint64 to uint32 -> uint32(uin64(_gateKey)),

That means it takes this part of the password: 0x XXXXXXXX "XXXXXXXX"

and it has to equal the same argument from uint64 to uint16 on the other side, -> uint16(uint64(_gateKey))

0x XXXXXXXX XXXX "XXXX"

When comparing to a uint32, the uint16 will fill the bytes it doesn't have with zeros, that's why:

0x XXXXXXXX 0000 XXXX

We already have 4 digits of the password!

Password: 0xXXXXXXXX0000XXXX

Condition 2:

Last 4 bytes (8 digits, or uint32) must not be equal to uint64 of key uint32(uint64(_gateKey)) != uint64(_gateKey)

0x "00000000" 0000XXXX != 0x XXXXXXXX XXXXXXXX

This means that first 4 bytes (8 digits) must have at least one different digit than 0 0x "XXXXXXXX" 0000XXXX

Password: 0xXXXXXXXX0000XXXX

Condition 3:

First requirement converts one argument from uint64 to uint32 | uint32(uin64(_gateKey)), and must equal first two bytes (4 digits, or uint16) of tx.origin (our address!) || uint16(tx.origin)

0x XXXXXXXX 0000 "XXXX"

That last "XXXX" MUST be the last 4 digits of our wallet address.

Password: 0xXXXXXXXX0000XXXX

Attacker contract is using the Masking bits off function with the & operator on your wallet address.

  • 10010101 <- This

  • 11110000 <- AND this

  • 10010000 <- Equals this (only both 1 result in 1)

  • X value with 0 equals 0

  • X value with 1 equals X.

14 - Gatekeeper Two

Lesson on contract size before initializing and the xor operator.

  1. Change the address in deploy/14-deploy-gatekeepertwo.js to your instance address
  2. Run yarn hardhat run scripts/gatekeeperTwo.js

About

Condition 2:

To call from a contract of size 0, we must call the enter function from the constructor, because the contract's size will be 0 until it is initialized.

Here's the reference inside the code

assembly { x := extcodesize(caller()) }

Condition 3:

^ stands for xor bitwise operator, this is an exclusive or operator.

This means that if: a ^ b == c, then a ^ c == b

  • & : and (x, y) bitwise and of x and y; where 1010 & 1111 == 1010 (true false true false)
  • | : or (x, y) bitwise or of x and y; where 1010 | 1111 == 1111 (true true true true)
  • ^ : xor (x, y) bitwise xor of x and y; where 1010 ^ 1010 == 0101 (false true false true)
  • ~ : not (x) bitwise not of x; where ~1010 == 0101 (false true false true)

15 - Naught Coin

Lesson on ERC20 standard, and approving to transferFrom any address (including our own).

  1. Change the address in scripts/naughtCoin.js to your instance address
  2. Run yarn hardhat run scripts/naughtCoin.js

16 - Preservation

Lesson combining knowledge of Privacy and Delegation challenges.

  1. Change the address in scripts/preservation.js to your instance address
  2. Run yarn hardhat run scripts/preservation.js

17 - Recovery

Lesson on contract creation addresses.

  1. Change the address in scripts/recovery.js to your instance address
  2. Run yarn hardhat run scripts/recovery.js

Getting contract creation address

To figure out a contract address, it takes the rightmost 20 bytes (160 bits) of the keccak hash of the RLP encoding of the structure containing only the sender of the contract creation transaction and the nonce of that transaction.(page 10 of Ethereum Yelowpaper)

address = rightmost_20_bytes(keccak(RLP(sender address, nonce)))

RLP = Recursive Length Prefix

In the attack script, we're using ethers.utils.SolidityKeccak256

ethers.utils.solidityKeccak256( types , values ) ⇒ string< DataHexString< 32 > > source Returns the KECCAK256 of the non-standard encoded values packed according to their respective type in types.

As a reference: abi.encodePacked in Solidity gives the same output as ethers.utils.solidityPack Here we encode and hash directly into keccak256 with a single command with solidityKeccak256.

The RLP encoding of a 20-byte address is: 0xd6, 0x94 . And for all integers less than 0x7f, its encoding is just its own byte value. So the RLP of 1 is 0x01.

That's how we end with this line:

const simpleTokenAdd = await ethers.utils.solidityKeccak256(
    ["bytes1", "bytes1", "address", "bytes1"],
    ["0xd6", "0x94", recoveryInstanceAddress, "0x01"]
  )

And then just take the last 40 digits (20 bytes) -because each byte is 2 digits-, and append them to "0x" to form the contract's address:

const simpleTokenAddress =
"0x" + simpleTokenAdd.substring(simpleTokenAdd.length - 40, simpleTokenAdd.length)

18 - Magic Number

Lesson on Opcode, Bytecode, Contract Creation vs. Runtime.

  1. Change the address in scripts/magicNumber.js to your instance address
  2. Run yarn hardhat run scripts/magicNumber.js

About

Creation vs. Runtime

Goal is to run two sets of OPCODES. One for creation (initialization) and another for runtime (10 OPCODES or less). Once initialized, runtime is stored for future computations.

Solidity Code (high level) > Opcodes > Byte code (EVM language)

Runtime code

To RETURN 42, we need:

  1. First we need to PUSH the value (v)
  2. Then we need to store it in memory with MSTORE, which needs position (p) first and then size (s)
  3. Then we RETURN the value (v)

Hex to Decimal converter

Using the converter, we can see that 42 value in Hex is 0x2a

EVM Opcodes

PUSH is 0x60 MSTORE is 0x52 RETURN is 0xf3

Bytecode - Description

  1. 602a - PUSH the value (v) to MSTORE. In this case, we push 42 (0x2a).
  2. 6050 - PUSH the position (p) (0x50) to MSTORE
  3. 52 - Store value (v=0x2a) at position (p=0x50) in memory.
  4. 6020 - PUSH the size (s) of v to the stack. In this case, we push 32 (0x20) for 32 bytes.
  5. 6050 - PUSH the position (p) of the size (s) (slot in which value was stored). In this case, position was 0x50.
  6. f3 - RETURN value, v=0x24 (42) of size s=0x20 (32 bytes)

Concatenated, it reads: 602a60505260206050f3

This is 10 bytes, as it has 20 digits when concatenated.

Solidity Bytecode and Opcode Basics

"In the EVM, there are 3 places to store data. Firstly, in the stack. We’ve just used the “PUSH” opcode to store data there as per the example above. Secondly in the memory (RAM) where we use the “MSTORE” opcode and lastly, in the disk storage where we use “SSTORE” to store the data. The gas required to store data to storage is the most expensive and storing data to stack is the cheapest."

Creation (initialization) code

The CODECOPY opcode can be used to copy the runtime opcodes. It takes three arguments: the destination position of copied code in memory, current position of runtime opcode in the bytecode and size of the code in bytes.

Value 10 (size of our code in bytes) in hex is 0x0a

We don't know the position of runtime opcode in the final bytecode (since initialization opcode comes before runtime opcode), so we omit by setting position as unknown with --.

After CODECOPY, we need to specify the values for RETURN.

  1. 600a - PUSH size of runtime opcode to stack. Size (s) param for COPYCODE.
  2. 60-- - PUSH -- (unknown) to the stack. Position (p) param for COPYCODE.
  3. 6000 - PUSH 0x00 (chosen destination in memory) in stack. Destination (d) param for COPYCODE.
  4. 39 - CODECOPY of size (s) at position (p) to destination (d) in memory.
  5. 600a - PUSH 0x0a (size of our runtime opcode) in stack. Size (s) param for RETURN.
  6. 6000 - PUSH 0x00 (location of value in memory) in stack. Position (p) param for RETURN.
  7. f3 - RETURN value (v) of size (s) at position (p)

So the initialization opcode is 600460--600039600a6000f3 which is 12 bytes in total.

Which means that runtime opcodes start at index 12 or position 0x0c

Therefore, initialization opcode must be 6004600c600039600a6000f3

Final opcode

Concatenate initialization opcode with runtime opcode:

600a600c600039600a6000f3 + 602a60505260206050f3
600a600c600039600a6000f3602a60505260206050f3

And now we can create the contract by sending a transaction to the zero address (0x0) with some data as it will be interpreted as Contract Creation by the EVM.

On scripts/magicnumber.js, I appendedd 0x at the beginning of the final opcode to make it readable as hex to be able to send the transaction.

More in-depth guides: Watch how to solve it - Read how to solve it

19 - Alien Codex

Lesson on Check-Effect-Interact and layout of dynamically-sized arrays in storage

  1. Change the address in scripts/alienCodex.js to your instance address
  2. Run yarn hardhat run scripts/alienCodex.js

About

First step is to make contact. Then we retract, so we substract 1 from position 0. Then we gotta figure out where is the owner. Then we gotta figure out the format to enter our address.

Check-Effect-Interact

Retract function doesn't respect the Check-Effect-Interact pattern, it does the effect right away. If something doesn't respect the CEI pattern, there's a chance there's a security vulnerability there.

codex.length-- is gonna remove the last element from the array.

We're doing this Effect without doing first a Check. This allows the opportunity to do an underflow on the array. If we substract 1 from the position 0, we're going to go to the last element of the array, and we're gonna start from there, which will allow to put anything we want wherever we want in the storage of the contract.

Layout of State Variables in Storage

State variables of contracts are stored in storage in a compact way such that multiple values sometimes use the same storage slot. Except for dynamically-sized arrays and mappings (see below), data is stored contiguously item after item starting with the first state variable, which is stored in slot 0. Read more in SolidityLang.org

Dynamically sized arrays use keccak-256 to find the starting position (always full stack slot - 32 bytes).

Mapping

  • p = position (storing the array length)

  • k = value (value corresponding to the mapping)

  • keccak256(k.p)

To find this information, we hash them together by concatenating then hashing them

  1. p = web3.utils.keccak256(web3.abi.encodeParameters(["uint256"], [1]))

Position: We're gonna hash with keccak256 the parameters (["uint256"], [1]) This is gonna be slot 1 inside AlienCodex. Remember slot 0 has the owner address and contact boolean set to true.

  1. i = BigInt(2 ** 256) - BigInt(p)

We substact to go to the slot 0, not inside the contract storage, but inside the array storage, because that's where the owner is. Converting hashed value to BigInt, we're able to substact back to Slot 0 of the codex.

  1. Content = "0x" + "0".repeat(24) + playeraddress.slice(2)

playeraddress.slice(2) <- This takes out the 0x at the beginning of playeraddress

Padding our address with 0's to meet the expected 32 bytes length.

Exploiting a flaw in the ABI specs. Doesn't validate that the length of the array matches the length of the payload. (e.g 0'd out.)

  1. contract.revise(i, content)

Place our content into the owner array index location

Watch how to solve it

20 - Denial

Lesson on different ways of sending value and assert method on ^0.6.0 (doesn't revert the transaction)

  1. Change the address in scripts/denial.js to your instance address
  2. Run yarn hardhat run scripts/denial.js

About sending value

send and transfer:

  • Require an address to be marked as payable to send them ETH.
  • They cost 2300 gas to prevent reentrancy vulnerability.

call

  • Doesn't require an address to be marked as payable to send them ETH.
  • Can be manually specified how much gas to use.
function depositUsingTransfer(address payable _to) public payable {
    _to.transfer(msg.value); // 2300 gas, throws error
}

function depositUsingSend(address payable _to) public payable {
    bool sent = _to.send(msg.value); // 2300 gas, returns bool that can be used to throw error
    require(sent, "Error! Ether not sent!");
}

Call returns bool and the return (if any) of calling the specified function. Gas can also be specified: _to.call{gas: 20000, value: ...}

function depositUsingCall(address _to) public payable {

    (bool sent, /* bytes memory data */) = _to.call{value: msg.value}("");
    require(sent, "Error! Ether not sent!");
}

About assert

There are 3 ways of throwing an error: assert, revert and require

For this version of the solidity compiler (^0.6.0), revert and require undo any state changes that have occurred until that error was reached. assert did not refund or return any of the gas that was sent to it.

require(condition, "Error explanation") revert("an error has occurred") <- Usually nested inside some logic assert(condition)

21 - Shop

Lesson on interfaces and using ternary operators to bypass repeated check with a boolean toggle in between

  1. Change the address in scripts/shop.js to your instance address
  2. Run yarn hardhat run scripts/shop.js

22 - Dex

Lesson on DEX mechanisms and Solidity's inhability to comprehend floating point numbers

  1. Change the address in scripts/shop.js to your instance address
  2. Run yarn hardhat run scripts/shop.js

About

In Solidity, 3 / 2 = 1, because it can't comprehend floating point numbers such as 1.5.

We start like this:

Then we swap all of our Token 1 for Token 2

At this point the exchange rate changed. Now 20 token2 would be 20 * 110 / 90 = 24,444

Since result is an integer, we get 24 token2. Swap again to get another price readjustment.

Each swap gets us more of token1 or token2 than we had before, because of the inaccuracy of price calculation in the get_swap_price function. Keep swapping:

Now we got enough tokens to drain the 110 token1! 65 tokens would get us 158! 65 * 110 / 45 = 158

But as there aren't enough tokens, we need to calculate that number down. 110 * 45 / 110 = 45

We just need to swap 45 tokens in that last transaction :)

23 - Dex Two

Lesson on ERC20 standard, DEX oracle manipulation, and using transferFrom to bypass restrictions.

  1. Change the address in scripts/dexTwo.js to your instance address
  2. Run yarn hardhat run scripts/dexTwo.js

About

Read how to solve it

24 - Puzzle Wallet

Lesson on delegatecall and proxy contracts.

Reccomended reads:

About

When an EOA (Externally Owned Account) does a call to a contract that does a regular call to another contract, the contract is taken as sender and owner of value for the second contract.

But with delegatecall, EOA is taken as sender and owner of value for the second contract. (i.e: Logic contract (2nd) for a Proxy contract (1st))

For the attack, we're using Multicall to Multicall twice the deposit function.

Watch how to solve it - Shorter video

25 - Motorbike

Lesson on proxy contracts, Initializable contracts, getStorageAt, delegatecall and selfdestruct.

  1. Change the address in scripts/motorbike.js to your instance address
  2. Run yarn hardhat run scripts/motorbike.js

Useful reads:

About

  1. Gain access to Engine (Address is inside the IMPLEMENTATION_SLOT location)
  2. "Initialize" to make ourselves the "upgrader"
  3. Deploy a attacker contract with self destruct
  4. Call "upgradeToAndCall" to attacker contract
  5. Self destruct attacker contract

Watch how to solve it - Another video - Read how to solve it

26 - Double Entry Point

You can read about it below

Watch how to solve it - Read how to solve it - Another read - A GitHub repo!

27 - Good Samaritan

Read how to do it:

Troubleshooting

General errors

  • Check level indications to see if you set the parameters right for your instance.
  • Did you save after changing address to your instance?
  • Delete "artifacts" and "cache" folder and run the command again.

"nonce too low" / Pending transaction stuck:

If you get this error when submitting level instance, it's because you used some nonces to send the transactions that attack the level. In your MetaMask, go to Settings > Advanced > Reset Account

Don't have Goerli ETH

Get some here: Chainlink Faucet - Alchemy Faucet

Don't have yarn

  • Just enter npm install --global yarn on your console.
  • Don't have NPM either? Go get the LTS champ!
  • Don't know how to use it? Search how to :)

Don't know how to fill .env file

(optional) Use different network than Goerli:

  1. Add your network (i.e: mumbai) as DefaultNetwork on hardhat.config.js
  2. Add it's parameters in network section same as goerli is added.

(optional) Want to use Mumbai