Smart_Overflow picoCTF 2026 Solution

Published: March 20, 2026

Description

Art overflow! The contract tracks balances using uint256 math. It should be impossible to get the flag. Contract: here.

Download and read IntOverflowBank.sol.

Note the Solidity version declared at the top of the contract.

bash
cat IntOverflowBank.sol

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Identify the integer overflow in the deposit function
    Observation
    I noticed the contract name is IntOverflowBank and the pragma declares a Solidity version before 0.8.0, which suggested that arithmetic wraps silently and the deposit function's comparison of new_balance to the deposited amount is actually an overflow detector rather than a safety check.
    Read the deposit function. It computes new_balance = old_balance + amount, then checks: if new_balance < amount (meaning overflow occurred) AND flag not revealed, then set revealed = true and emit FlagRevealed. To trigger this: deposit 2^256 - 1 (the maximum uint256 value = 64 hex Fs), which when added to any existing balance causes overflow.
    bash
    cat IntOverflowBank.sol
    What didn't work first

    Tried: Check the Solidity version pragma and assume the contract is safe because it uses uint256 (a 256-bit type that looks too large to overflow).

    uint256 has a maximum value of 2^256 - 1, not infinity. In Solidity before 0.8.0, addition silently wraps around past that boundary instead of reverting. The bit width is irrelevant - any unsigned type can overflow if unchecked arithmetic is used and the compiler does not enforce bounds.

    Tried: Look for an underflow instead of an overflow, expecting the bug to be in a withdrawal or subtraction operation.

    The vulnerability is in the deposit (addition) path, not a withdrawal. The deposit function adds the incoming amount to the existing balance; when the sum exceeds 2^256 - 1 it wraps to a small number. The contract treats that wrap as the trigger condition, so the exploit vector is a deposit, not a withdrawal.

    Learn more

    Integer overflow in unsigned integer arithmetic occurs when an addition produces a result that wraps around past the maximum value for that type. For uint256, adding 1 to 2^256 - 1 wraps to 0. The deposit function deliberately checks for this: if (balances[msg.sender] < _amount) after the addition is a check for overflow (if the new balance is smaller than what was just added, a wrap-around occurred).

    This exact vulnerability class (integer overflow/underflow in Solidity pre-0.8.0) has caused dozens of real-world exploits in deployed DeFi protocols. The BatchOverflow bug in 2018 affected multiple ERC-20 tokens. Solidity 0.8.0 introduced built-in overflow checks, eliminating the class entirely unless developers use unchecked {} blocks.

  2. Step 2
    Deposit 2^256 - 1 to trigger the overflow
    Observation
    I noticed the overflow check fires when new_balance < amount, which is the exact condition produced by adding 0xfff...fff (64 Fs) to any balance, and Foundry's cast send is the standard CLI tool for submitting signed transactions to an EVM RPC endpoint.
    Use Foundry's cast to call the deposit function with amount = 2^256 - 1 (64 hex Fs). This triggers the overflow check in the contract and reveals the flag.
    bash
    # Set up the environment:
    bash
    export RPC_URL=http://<HOST>:<PORT_FROM_INSTANCE>
    bash
    export CONTRACT=0x<BankAddress from instance page>
    bash
    export PRIVATE_KEY=0x<PrivateKey from instance page>
    bash
    # 2^256 - 1 = 64 hex Fs:
    bash
    cast send $CONTRACT "deposit(uint256)" \
      0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
      --private-key $PRIVATE_KEY --rpc-url $RPC_URL \
      --gas-limit 100000 --legacy
    What didn't work first

    Tried: Pass 2^256 - 1 as a decimal integer directly to cast send instead of as a hex value.

    The decimal representation of 2^256 - 1 is 115792089237316195423570985008687907853269984665640564039457584007913129639935, which most shells and cast versions reject or truncate due to argument length limits. The hex form 0xffff...ffff (64 Fs) is unambiguous and universally accepted by cast. Always use hex for large Solidity integers.

    Tried: Omit --legacy and --gas-limit and let cast estimate gas automatically, expecting the transaction to succeed on the test network RPC.

    Some CTF RPC nodes run older or non-standard EVM implementations that reject EIP-1559 type-2 transactions or fail gas estimation for contracts with conditional paths. Without --legacy the node may return a 'duplicate field data' error; without --gas-limit the estimation call may fail because the node cannot predict the overflow branch. Adding both flags forces a type-0 transaction with a manually set gas ceiling that bypasses both issues.

    Learn more

    Foundry's cast tool is a command-line Ethereum toolkit. cast send submits a signed transaction to the blockchain. The --legacy flag uses type-0 transactions instead of EIP-1559 type-2, which resolves "duplicate field data" errors on some test network RPC nodes. The --gas-limit flag overrides gas estimation when the node returns an error.

    The value 0xfff...fff (64 Fs) is 2^256 - 1. When added to the current balance (even 0), the result wraps around past 2^256. The resulting sum is smaller than the amount deposited, which triggers the overflow detection check in the contract.

  3. Step 3
    Claim the flag
    Observation
    I noticed the contract emits a FlagRevealed event rather than storing the flag in a readable state variable, which meant the flag would appear in the cast send transaction receipt logs rather than in a separate view call.
    The flag is emitted as a FlagRevealed event in the deposit transaction receipt. After the cast send succeeds, look for the flag in the transaction output logs. If needed, inspect the transaction directly.
    bash
    # The flag appears in the cast send output above.
    bash
    # If you missed it, inspect the transaction logs:
    bash
    cast tx <TX_HASH> --rpc-url $RPC_URL
    Learn more

    Rather than storing the flag in a retrievable view function, this contract emits a FlagRevealed event when the overflow is triggered. Ethereum events are stored in the transaction receipt logs and are visible in the cast send output. Use cast tx with the transaction hash to inspect the logs if the flag did not appear directly in the terminal output.

    In real-world blockchain security auditing, demonstrating that a vulnerability meets the exploit criteria (e.g., draining funds to zero, bypassing access control) is the standard for proof-of-concept. Audit platforms like Immunefi host blockchain bug bounties worth millions of dollars, and finding integer overflow vulnerabilities in production contracts can earn significant rewards. Understanding these Solidity-specific pitfalls is essential knowledge for anyone working in Web3 security.

Flag

Reveal flag

picoCTF{sm4rt_0v3rfl0w_3x1sts_...}

Deposit 2^256 - 1 (64 hex Fs) to trigger uint256 addition overflow. The deposit function checks if the new balance < the deposit amount (overflow detector) and sets revealed = true when that happens.

Key takeaway

Integer overflow in smart contracts occurs when arithmetic on fixed-width unsigned integers wraps silently past the type boundary, producing a result smaller than either operand. In Solidity before version 0.8.0, all arithmetic wrapped by default with no reversion, and attackers exploited this to mint unlimited tokens or drain funds in dozens of real DeFi protocols. The fix was built into the compiler in 0.8.0, which reverts on overflow unless the developer explicitly opts out with an unchecked block, making the correct version pin the single most impactful mitigation.

How to prevent this

Solidity 0.8.0 made arithmetic checked by default. Pinning to an older pragma is the bug. See the Smart Contract CTF Bugs guide for the full pattern catalog.

  • Use pragma solidity ^0.8.0 or newer. Arithmetic operations now revert on overflow/underflow without needing SafeMath. There is essentially no reason to deploy on older versions.
  • For contracts stuck on legacy compilers, use OpenZeppelin's SafeMath. The diff is small but load-bearing:
    // Vulnerable (pre-0.8.0, no SafeMath)
    require(balances[sender] - amount >= 0); // tautology
    balances[sender] = balances[sender] - amount; // wraps
    
    // Safe (with SafeMath)
    balances[sender] = balances[sender].sub(amount); // reverts on underflow
  • Run Slither and Echidna against every contract before deploy. Both detect classic patterns like unchecked subtraction, signed/unsigned comparisons, and divide-before-multiply. Add Foundry invariant tests asserting totalSupply == sum(balances) after every transaction.

Related reading

Want more picoCTF 2026 writeups?

Useful tools for Blockchain

What to try next