Description
Welcome! 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 - versions before 0.8.0 do not check for arithmetic overflow.
cat IntOverflowBank.solSolution
Walk me through it- Step 1Identify the integer underflowThe bug is the
>=check itself.require(balances[msg.sender] - _amount >= 0)looks like a range guard but is a tautology: uint256 cannot go negative, so the subtraction wraps before the comparison runs. The check is structurally impossible to fail.bashcat IntOverflowBank.solLearn more
Integer underflow in unsigned integer arithmetic occurs when a subtraction produces a result below zero, which wraps around to the maximum value for that type. For
uint256, subtracting 1 from 0 yields2^256 - 1 = 115792089237316195423570985008687907853269984665640564039457584007913129639935- an astronomically large number. This is not a bug in the CPU; it is defined behavior in unsigned arithmetic, inherited from how binary subtraction works in hardware.The vulnerability is in the guard condition:
require(balances[msg.sender] - _amount >= 0)is supposed to prevent withdrawing more than your balance. But since both operands areuint256, the subtraction underflows to a huge positive number before the>= 0comparison is evaluated. The comparison is always true - the require never reverts. The developer intended a signed integer check (int256), but unsigned types cannot represent negative numbers, making this check semantically meaningless.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 and allowed attackers to mint astronomical token quantities by exploiting unsigned integer overflow in transfer batch operations. Solidity 0.8.0 (released December 2020) introduced built-in overflow checks that revert the transaction on overflow/underflow, eliminating the class entirely - unless developers explicitly use
unchecked {}blocks to opt out for gas efficiency. - Step 2Underflow: transfer 1 from a zero balanceCall the transfer function with an amount of 1 when your balance is 0. The uint256 underflows to the maximum value (2^256-1), giving you an astronomically large balance and triggering the win condition.bash
# Set up the environment first:bashexport RPC_URL=http://<HOST>:<PORT_FROM_INSTANCE>bashexport CONTRACT=0xYourDeployedContractAddressbashexport YOUR_ADDR=0xYourWalletAddressbashexport PRIVATE_KEY=0xYourPrivateKeybash# Check your initial balance (should be 0):bashcast call $CONTRACT "balances(address)" $YOUR_ADDR --rpc-url $RPC_URLbash# Trigger the underflow by transferring 1 token from a 0-balance account:bashcast send $CONTRACT "transfer(address,uint256)" 0x0000000000000000000000000000000000000001 1 \ --private-key $PRIVATE_KEY --rpc-url $RPC_URLbash# Verify your balance is now max uint256:bashcast call $CONTRACT "balances(address)" $YOUR_ADDR --rpc-url $RPC_URLLearn more
Foundry's cast tool is a command-line Ethereum Swiss Army knife.
cast callmakes a read-only call (free, no gas, no state change) whilecast sendsubmits a signed transaction that modifies blockchain state (costs gas, requires a private key). The ABI signature format"transfer(address,uint256)"specifies the function name and parameter types, which Foundry uses to ABI-encode the calldata correctly.The recipient address
0x0000...0001(the "burn" or null address with value 1) is used as a dummy recipient because we do not care about the transfer destination - the goal is just to trigger the underflow in the sender's balance mapping. The Solidity storage mappingbalances[msg.sender]is updated bybalances[msg.sender] -= _amount, which underflows to2^256 - 1.The win condition check varies by challenge - some contracts expose
isSolved(), others check directly in the flag function. This pattern of "prove a condition to get the flag" mirrors real-world bug bounties where demonstrating proof-of-concept (showing you can drain the contract or bypass the access control) is required to claim the reward. - Step 3Claim the flagWith your balance now at max uint256, the contract's win condition is met. Call the flag retrieval function.bash
cast call $CONTRACT "isSolved()(bool)" --rpc-url $RPC_URLbashcast call $CONTRACT "getFlag()(string)" --rpc-url $RPC_URLLearn more
Smart contract CTF challenges commonly follow a pattern where the flag is stored on-chain and gated behind a condition. The
getFlag()function returns the flag string only whenisSolved()returns true. Since both areviewfunctions (read-only, no state change), they can be called withcast callwithout spending gas.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
picoCTF{sm4rt_0v3rfl0w_...}
Silent underflow: `require(balances[sender] - amount >= 0)` always passes on uint256. Transferring 1 from a zero balance wraps to ~10^77 (specifically 2^256 - 1 = 115792089237316195423570985008687907853269984665640564039457584007913129639935), instantly satisfying the win condition.
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.