From 6bb53849b919cbd0656a0375ca1feacc8742e966 Mon Sep 17 00:00:00 2001 From: leovct Date: Thu, 10 Oct 2024 11:07:20 +0200 Subject: [PATCH] feat: add ethernaut lvl 27 --- docs/ethernaut-ctf/EthernautCTF.md | 2 +- src/EthernautCTF/GoodSamaritan.sol | 106 +++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/EthernautCTF/GoodSamaritan.sol diff --git a/docs/ethernaut-ctf/EthernautCTF.md b/docs/ethernaut-ctf/EthernautCTF.md index 95a5715..6105e9c 100644 --- a/docs/ethernaut-ctf/EthernautCTF.md +++ b/docs/ethernaut-ctf/EthernautCTF.md @@ -30,7 +30,7 @@ | 24 | [PuzzleWallet](../../src/EthernautCTF/PuzzleWallet.sol) | ✅ | [PuzzleWalletExploit](../../test/EthernautCTF/PuzzleWalletExploit.t.sol) | When writing a Proxy contract, and more generally any contract that uses `delegatecall`, always make sure that the sensible storage values are not colliding with other values. The storage layout should be identical, for those values, on both the proxy and the implementation contracts. | | 25 | [Motorbike](../../src/EthernautCTF/Motorbike.sol) | ❌ | | | | 26 | [DoubleEntry](../../src/EthernautCTF/DoubleEntry.sol) | ✅ | [DoubleEntryExploit](../../test/EthernautCTF/DoubleEntryExploit.t.sol) | - When delegating calls from a deprecated token to another token (or any other contract), avoid sending new tokens in place of old tokens.
- This level is made of a lot of different contracts, you can find an architecture diagram [below](#level-26). | -| 27 | GoodSamaritan | ❌ | | | +| 27 | [GoodSamaritan](../../src/EthernautCTF/GoodSamaritan.sol) | ❌ | | | | 28 | [GatekeeperThree](../../src/EthernautCTF/GatekeeperThree.sol) | ✅ | [GatekeeperThreeExploit](../../test/EthernautCTF/GatekeeperThreeExploit.t.sol) | - Make sure the `constructor` method is spelled properly.
- Do not use `tx.origin` to check the origin of the caller.
- Private variables are not private on a public blockchain.
- It's easy to implement a contract that do not accepts ether. | | 29 | [Switch](../../src/EthernautCTF/Switch.sol) | ❌ | | | | 30 | [HigherOrder](../../src/EthernautCTF/HigherOrder.sol) | ✅ | [HigherOrderExploit](../../test/EthernautCTF/HigherOrderExploit.t.sol) | Reading function parameters (or any other value) using `calldataload` is dangerous because it will always return a 32-byte value and not the expected type of the variable. | diff --git a/src/EthernautCTF/GoodSamaritan.sol b/src/EthernautCTF/GoodSamaritan.sol new file mode 100644 index 0000000..1b9da36 --- /dev/null +++ b/src/EthernautCTF/GoodSamaritan.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import '@openzeppelin-07/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; +}