Skip to content
On this page

S1C5: Give Me My Points!

1. Problem

The contract has a points system: claimPoints() gives +1 point each time, and upgradeLevel() consumes points to level up. Certain conditions (points x level) must be met before the flag can be minted. The contract uses a single points variable.

2. Root Cause

The claimPoints() function makes an external call (sending ETH) before updating the points state. This allows an attacker to re-enter claimPoints() via the receive() callback and accumulate points repeatedly before the state is updated.

3. Solution

Deploy an attack contract that exploits the reentrancy vulnerability:

solidity
contract S1C5Solution {
    function solve() external {
        challenge.claimPoints(); // Trigger the reentrancy loop
        // points have accumulated to a sufficient amount
        challenge.upgradeLevel();
        challenge.mintFlag();
    }

    receive() external payable {
        if (points < TARGET) {
            challenge.claimPoints(); // Re-enter
        }
    }
}

4. Pitfalls Encountered

Reentrancy iteration control: The number of reentrancy iterations in receive() must be precisely controlled to avoid running out of gas.

5. Why the Pitfall Exists

Each reentrant call consumes gas. If the loop iterates too many times, it may exceed the block gas limit. A counter must be added to the loop and stopped once the target is reached.

6. How to Resolve

Use a counter variable in the attack contract, check it inside receive(), and stop re-entering once the target count is reached.

7. Key Takeaways

PointExplanation
Reentrancy attackAn external call executed before a state update allows the attacker to repeatedly re-enter the same function
CEI patternChecks-Effects-Interactions: always update state first, then make external calls
Gas managementReentrancy loops must control iteration count to avoid exceeding the gas limit

Built with AiAda