From fa25019980de4fac4dab9e83feac6b80a4c914a9 Mon Sep 17 00:00:00 2001 From: leovct Date: Mon, 30 Sep 2024 14:25:54 +0200 Subject: [PATCH] chore: add 3 new ethernaut challenges --- doc/EthernautCTF.md | 6 +- src/EthernautCTF/DexTwo.sol | 73 ++++++++++++++++ src/EthernautCTF/DoubleEntry.sol | 142 +++++++++++++++++++++++++++++++ src/EthernautCTF/Motorbike.sol | 121 ++++++++++++++++++++++++++ 4 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 src/EthernautCTF/DexTwo.sol create mode 100644 src/EthernautCTF/DoubleEntry.sol create mode 100644 src/EthernautCTF/Motorbike.sol diff --git a/doc/EthernautCTF.md b/doc/EthernautCTF.md index 8db7d6d..dc50a45 100644 --- a/doc/EthernautCTF.md +++ b/doc/EthernautCTF.md @@ -24,10 +24,10 @@ | 20 | [Denial](../src/EthernautCTF/Denial.sol) | ✅ | [DenialExploit](../test/EthernautCTF/DenialExploit.t.sol) | - Always set the amount of gas when using a low-level call. It will prevent the external contract to consume all the gas.
- Check the return value of low-level calls, especially when the address is controlled by someone else. | | 21 | [Shop](../src/EthernautCTF/Shop.sol) | ✅ | [ShopExploit](../test/EthernautCTF/ShopExploit.t.sol) | - When calling an external contract, always check the returned value before using it!
- This challenge is very similar to challenge 11. | | 22 | [Dex](../src/EthernautCTF/Dex.sol) | ✅ | [DexExploit](../test/EthernautCTF/DexExploit.t.sol) | The contract uses a division operation to compute the swap amount which can be exploited because of a precision loss. Indeed, Solidity does not support floating points. | -| 23 | DexTwo | ❌ | | | +| 23 | [DexTwo](../src/EthernautCTF/DexTwo.sol) | ❌ | | | | 24 | PuzzleWallet | ❌ | | | -| 25 | Motorbike | ❌ | | | -| 26 | DoubleEntry | ❌ | | | +| 25 | [Motorbike](../src/EthernautCTF/Motorbike.sol) | ❌ | | | +| 26 | [DoubleEntry](../src/EthernautCTF/DoubleEntry.sol) | ❌ | | | | 27 | GoodSamaritan | ❌ | | | | 28 | [GatekeeperThree](../src/EthernautCTF/GatekeeperThree.sol) | ❌ | | | | 29 | [Switch](../src/EthernautCTF/Switch.sol) | ❌ | | | diff --git a/src/EthernautCTF/DexTwo.sol b/src/EthernautCTF/DexTwo.sol new file mode 100644 index 0000000..90db722 --- /dev/null +++ b/src/EthernautCTF/DexTwo.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import '@openzeppelin-08/token/ERC20/IERC20.sol'; +import '@openzeppelin-08/token/ERC20/ERC20.sol'; +import '@openzeppelin-08/access/Ownable.sol'; + +contract DexTwo is Ownable { + address public token1; + address public token2; + + constructor() Ownable(msg.sender) {} + + function setTokens(address _token1, address _token2) public onlyOwner { + token1 = _token1; + token2 = _token2; + } + + function add_liquidity( + address token_address, + uint256 amount + ) public onlyOwner { + IERC20(token_address).transferFrom(msg.sender, address(this), amount); + } + + function swap(address from, address to, uint256 amount) public { + require(IERC20(from).balanceOf(msg.sender) >= amount, 'Not enough to swap'); + uint256 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, + uint256 amount + ) public view returns (uint256) { + return ((amount * IERC20(to).balanceOf(address(this))) / + IERC20(from).balanceOf(address(this))); + } + + function approve(address spender, uint256 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 (uint256) { + return IERC20(token).balanceOf(account); + } +} + +contract SwappableTokenTwo 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); + } +} diff --git a/src/EthernautCTF/DoubleEntry.sol b/src/EthernautCTF/DoubleEntry.sol new file mode 100644 index 0000000..d4b96f2 --- /dev/null +++ b/src/EthernautCTF/DoubleEntry.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import '@openzeppelin-08/access/Ownable.sol'; +import '@openzeppelin-08/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(msg.sender) { + 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(msg.sender) +{ + 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; + } +} diff --git a/src/EthernautCTF/Motorbike.sol b/src/EthernautCTF/Motorbike.sol new file mode 100644 index 0000000..e94243a --- /dev/null +++ b/src/EthernautCTF/Motorbike.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT + +pragma solidity <0.7.0; + +import '@openzeppelin-06/utils/Address.sol'; +import '@openzeppelin-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; + } +}