Calling other contracts, and reentrancy
The moment your contract calls another one, it can be re-entered before it finishes. Order your writes accordingly.
Contracts compose through abi.call_contract:
result = abi.call_contract(other, b"balance_of", [addr], 0, read_only=True)
and its non-throwing sibling abi.try_call_contract, which reports failure instead of reverting your whole transaction. abi.call_depth() tells you how deep you are in a chain of calls.
In this academy sandbox, inter-contract calls are disabled — call_contract reverts with *"inter-contract calls are not enabled"*, because the hook that routes calls between contracts lives in the chain's execution engine rather than the standalone VM. So this lesson is a walkthrough rather than an exercise. The pattern below is what matters, and it is the same on every chain that has ever lost money to this bug.
Reentrancy. When you call another contract, you hand it control. It can call you back *before your first call has finished*, while your storage is still mid-update. Consider a withdrawal that pays first and bookkeeps second:
def withdraw(amount):
abi.require(_balance(caller) >= amount, b"insufficient")
abi.call_contract(caller, b"receive", [], amount) # they get control HERE
_set_balance(caller, _balance(caller) - amount) # …this has not run yet
Their receive calls withdraw again. The balance check still reads the old, undecremented balance, so it passes. And again. The contract is drained in a single transaction, and every individual step looked valid.
Checks → Effects → Interactions. Order the function so that by the time you hand over control, your state already reflects what happened:
def withdraw(amount):
# 1. CHECKS — validate everything first
have = _balance(caller)
abi.require(have >= amount, b"insufficient")
# 2. EFFECTS — write state BEFORE calling out
_set_balance(caller, have - amount)
# 3. INTERACTIONS — now it is safe to hand over control
abi.call_contract(caller, b"receive", [], amount)
Re-entered now, the balance check reads the already decremented value and fails. The bug is gone — not patched, structurally absent.
Two habits that compound with this one:
- Assume every external call is hostile, including ones to contracts you wrote. Addresses can be upgraded, and
callermay be a contract you have never seen. - A reentrancy guard is a seatbelt, not a substitute. A stored
lockedflag is worth adding, but if your ordering is wrong the guard is the only thing between you and an empty contract. Get the order right first.
Claim your 10 ANM
Finish this lesson and claim 10 ANM, once per address. Paid from the Animica treasury in batches — allow a few minutes.
Stuck? Ask
Answered by Animica's own free inference network. It is donated GPU capacity, so give it 20-30 seconds.