Skip to main content

Smart_Overflow picoCTF 2026 Solution

The contract's uint256 deposit arithmetic wraps, so seed a balance then deposit 2^256 minus 1 to unlock the flag.

Published: March 20, 2026Updated: September 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 1Identify the integer overflow in the deposit function
    Observation
    The contract is called IntOverflowBank and its pragma predates Solidity 0.8.0, so arithmetic wraps silently. The comparison of the new balance against the deposited amount is an overflow detector, not a safety check.
    Read the deposit function. It computes balances[msg.sender] = balances[msg.sender] + amount, then checks: if !revealed AND the new balance < amount (meaning the addition wrapped), set revealed = true and emit FlagRevealed. Note what that requires: old_balance + amount must reach 2^256. Depositing 2^256 - 1 into an empty balance gives exactly 2^256 - 1, which is not less than the amount, so nothing happens. You need a non-zero balance first, so deposit 1, then deposit 2^256 - 1 (64 hex Fs): the sum is 2^256, which wraps to 0, and 0 < amount fires the check.
    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).

    A uint256 tops out at 2^256 - 1; it is not unbounded. Before Solidity 0.8.0, addition wrapped past that silently instead of reverting. The width does not matter: any unsigned type overflows under unchecked arithmetic.

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

    The bug is on the deposit path, in the addition. Deposit adds the incoming amount to the balance, and past the maximum the sum wraps to something small. The contract treats that wrap as its trigger, so the vector is a deposit.

    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 2Seed a balance, then deposit 2^256 - 1 to trigger the overflow
    Observation
    That check fires when the new balance comes out smaller than the amount, which needs the sum to reach 2^256. Seed the balance with 1, then add the maximum uint256. Send both with Foundry's cast.
    Use Foundry's cast twice: deposit 1 to make the balance non-zero, then deposit 2^256 - 1 (64 hex Fs). The second addition wraps to 0, which trips 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
    # 1. Seed the balance so the next addition can wrap:
    bash
    cast send $CONTRACT "deposit(uint256)" 1 \
      --private-key $PRIVATE_KEY --rpc-url $RPC_URL \
      --gas-limit 100000 --legacy
    bash
    # 2. Add 2^256 - 1 = 64 hex Fs, so the sum reaches 2^256 and wraps to 0:
    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.

    Written out in decimal, 2^256 - 1 is 78 digits long, and most shells and cast versions reject or truncate an argument that size. The hex form, 64 Fs, is unambiguous and always accepted. 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 nodes run older EVM implementations that reject EIP-1559 transactions or fail gas estimation on contracts with conditional branches. Without --legacy you may get a duplicate-field error; without a gas limit the estimation fails because the node cannot predict the overflow branch. Both flags together force a type-0 transaction with a fixed ceiling.

    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. Added to a balance of 0 it stays 2^256 - 1 and nothing wraps, which is why the first deposit matters: from a balance of 1 the sum is exactly 2^256, which wraps to 0. The stored balance is then smaller than the amount deposited, which is the condition the contract tests.

  3. Step 3Claim the flag
    Observation
    The overflow sets revealed = true, which both emits FlagRevealed and unlocks the getFlag() view. Read the receipt log, or just call getFlag().
    The flag is emitted as a FlagRevealed event in the second deposit's receipt, and the same transaction sets revealed = true, which lifts the require() in getFlag(). The simplest route is a plain cast call to getFlag(); the event log is there as a cross-check.
    bash
    # Simplest: revealed is now true, so the view function returns the flag
    bash
    cast call $CONTRACT "getFlag()(string)" --rpc-url $RPC_URL
    bash
    # Cross-check against the emitted event (receipt, not tx, carries the logs):
    bash
    cast receipt <TX_HASH> --rpc-url $RPC_URL
    Learn more

    The contract does both: it emits a FlagRevealed event and flips revealed, which is the flag guarding getFlag(). Events live in the transaction receipt logs, so use cast receipt rather than cast tx (which prints the transaction fields and no logs) when you want to read the emitted string. The ABI-encoded string in the log data decodes to the same value getFlag() returns.

    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.

Interactive tools
  • Number Base ConverterConvert numbers between binary, octal, decimal, and hexadecimal instantly. Enter any value and see all four bases update in real time.
  • Bit Shift CalculatorPerform left/right bit shifts and see the result across binary, octal, decimal, and hex.
  • Binary CalculatorPerform binary arithmetic (add, subtract, multiply, divide) with copyable outputs in every base.

Flag

Reveal flag

picoCTF{Sm4r7_OverFL0ws_ExI5t_...}

Deposit 1, then deposit 2^256 - 1 (64 hex Fs) so the sum reaches 2^256 and wraps to 0. The deposit function checks whether the new balance is less than the deposit amount (an overflow detector), sets revealed = true, emits FlagRevealed, and unlocks getFlag().

Key takeaway

Integer overflow in a contract is arithmetic on fixed-width unsigned integers wrapping past the type boundary and producing a result smaller than either operand. Before Solidity 0.8.0 everything wrapped by default, and attackers used it to mint unlimited tokens and drain funds across dozens of DeFi protocols. The compiler now reverts on overflow unless a developer explicitly opts out, which makes the version pin the single highest-impact 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

Useful tools for Blockchain

Where to go next