Appearance
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
| Point | Explanation |
|---|---|
| Reentrancy attack | An external call executed before a state update allows the attacker to repeatedly re-enter the same function |
| CEI pattern | Checks-Effects-Interactions: always update state first, then make external calls |
| Gas management | Reentrancy loops must control iteration count to avoid exceeding the gas limit |