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.
cat IntOverflowBank.solSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Identify the integer overflow in the deposit functionObservationI 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.bashcat IntOverflowBank.solWhat 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 to2^256 - 1wraps 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.Step 2
Deposit 2^256 - 1 to trigger the overflowObservationI 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:bashexport RPC_URL=http://<HOST>:<PORT_FROM_INSTANCE>bashexport CONTRACT=0x<BankAddress from instance page>bashexport PRIVATE_KEY=0x<PrivateKey from instance page>bash# 2^256 - 1 = 64 hex Fs:bashcast send $CONTRACT "deposit(uint256)" \ 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \ --private-key $PRIVATE_KEY --rpc-url $RPC_URL \ --gas-limit 100000 --legacyWhat 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 sendsubmits a signed transaction to the blockchain. The--legacyflag uses type-0 transactions instead of EIP-1559 type-2, which resolves "duplicate field data" errors on some test network RPC nodes. The--gas-limitflag overrides gas estimation when the node returns an error.The value
0xfff...fff(64 Fs) is2^256 - 1. When added to the current balance (even 0), the result wraps around past2^256. The resulting sum is smaller than the amount deposited, which triggers the overflow detection check in the contract.Step 3
Claim the flagObservationI 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:bashcast tx <TX_HASH> --rpc-url $RPC_URLLearn more
Rather than storing the flag in a retrievable
viewfunction, this contract emits aFlagRevealedevent when the overflow is triggered. Ethereum events are stored in the transaction receipt logs and are visible in thecast sendoutput. Usecast txwith 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
How to prevent this
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.0or 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.