Diagnosing and Fixing Reentrancy in Vyper with @nonreentrant
Diagnose reentrancy in Vyper: identify risky external calls, apply @nonreentrant locks with proper keys, and verify with a simple attacker test. Follow the step‑by‑step guide to secure your smart contracts.
22 Sept 2025, 07:17 UTC

Recognizing the Reentrancy Problem
In Vyper, a smart contract that sends Ether (via send or raw_call) or makes an external call before updating its own storage is exposed to a classic reentrancy attack. The symptoms are simple: balances are drained, accounting becomes inconsistent, or the contract reverts unexpectedly after a malicious call.
Reentrancy in Vyper: What the Language Gives You
- Vyper enforces checked arithmetic, bounds checks, and disallows modifiers.
- It does not automatically lock state during external calls.
- The only supported, explicit lock mechanism is the
@nonreentrant('key')decorator. - Functions that share the same key are mutually exclusive; distinct keys allow unrelated functions to run concurrently.
Diagnostic Table
| Condition | Severity | Escalation |
|---|---|---|
| External call before state update | High | Immediate review |
| External call to untrusted or user‑supplied address | Critical | Security audit |
| External call to trusted contract only | Medium | Check lock coverage |
| Multiple functions share state but use different keys | Low | Consider a common key |
Ordered Checks
- Inventory: List every function that performs
sendorraw_calland note the target address.- Use a static analysis tool or grep for
sendandraw_call. - Record the function name, parameters, and whether the target is a constant or variable.
- Use a static analysis tool or grep for
- Check‑Effects‑Interactions: Verify that each such function follows the checks‑effects‑interactions pattern—state updates before any external call.
- If state is written after the call, flag it for re‑ordering or locking.
- Lock Coverage: Ensure every flagged function is decorated with
@nonreentrant('key').- Confirm that the key used in each function is the same if they touch the same critical state (e.g., a balance mapping).
- Key Granularity: Check that unrelated functions are not forced to share a global lock unless necessary.
- For a protocol that manages multiple independent vaults, use a key derived from the vault ID.
Fixes Tied to Findings
- Reorder Code: Move all state updates to the beginning of the function before any external call.
@external def withdraw(amount: uint256): # Checks assert balances[msg.sender] >= amount # Effects balances[msg.sender] -= amount # Interaction send(msg.sender, amount) - Apply @nonreentrant: Wrap the entire function body.
@nonreentrant('vault') @external def withdraw(amount: uint256): # ... - Use Pull‑Payments: Replace push patterns with a withdraw pattern where the user explicitly calls a withdrawal function.
- This reduces the surface area for reentrancy because no external call is made until the user initiates it.
- Shared Key Strategy: If two functions modify the same mapping, give them the same key.
@nonreentrant('balance_lock') @external def deposit(): # ... @nonreentrant('balance_lock') @external def withdraw(amount: uint256): # ...
Escalation Criteria
- Any contract holding more than
100 ETHwith post‑call state writes should trigger a third‑party audit. - Contracts that use
raw_callto an arbitrary address (variableaddr) require a security review. - Cross‑function reentrancy paths (e.g.,
AcallsBwhich reentersC) must be examined for lock coverage.
Practical Verification
- Deploy a test attacker contract.
- In its fallback, call the target function again using
raw_call. - When
@nonreentrantis applied, the transaction should revert with an out‑of‑gas or lock error. - Compile with the exact Vyper version you plan to use and inspect the storage slot for the lock flag using a debugger or
eth_getStorageAt.
Limitations
- Reentrancy locks do not protect against read‑only reentrancy during
viewcalls. - Overusing a single global lock key can cause denial‑of‑service in high‑throughput contracts.
- Vyper syntax may change; always verify the decorator signature against your compiler version.
Conclusion
By following the ordered checks, applying @nonreentrant with the correct key granularity, and verifying with a simple attacker test, you can confidently eliminate the classic reentrancy vulnerability in Vyper contracts. Remember that the lock is only as strong as the key you choose; choose it to protect the shared state without unnecessarily blocking unrelated operations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.