Reentrancy
What deferral prevents
The classic reentrancy bug is a callee re-entering while the caller's state is half-updated:
function withdraw() external {
uint256 amount = balances[msg.sender];
msg.sender.call{value: amount}(""); // re-enters here...
balances[msg.sender] = 0; // ...before this runs
}In Lithe, this cannot be written. A defer always runs after the transition commits, so the callee sees balances[sender] = 0 whatever order the source uses.
What deferral does not prevent
After your transition commits, a deferred callee can still call back into your contract. It can call any external function, including the one that is running. A second call might start before your later deferred calls or continuations have run. That is sometimes harmless and sometimes a protocol bug, so the compiler reports it.
Reentrancy warnings
For every entrypoint that defers calls, the compiler lists the stateful entrypoints that could re-enter while those calls run:
interface Hook is {
function cancel();
}
storage is record {
status : uint;
extra : uint;
}
external function settle(const hook : Hook) is {
storage.status := 1;
defer hook.cancel();
}
external function cancel() is {
storage.extra := 2;
}vault.lithe:12:10: warning: `settle` performs deferred external operations and may
be re-entered through `cancel`
consider `nonreentrant` if the protocol requires serialized
external-effect executionnonreentrant
external nonreentrant function takes the contract's reentrancy lock:
interface Hook is {
function cancel();
}
storage is record {
status : uint;
}
external nonreentrant function settle(const hook : Hook) is {
storage.status := 1;
defer hook.cancel();
}
external nonreentrant function cancel() is {
storage.status := 2;
}
external view function status() : uint is
storage.statusThe rules:
- There is one lock per contract. All
nonreentrantfunctions share it. - A
nonreentrantfunction reverts if the lock is held, and holds it for its whole run, including its deferred calls and continuations. - Any function that defers calls,
nonreentrantor not, holds the lock while its deferred calls run. A callee therefore cannot enter anonreentrantfunction through it. viewandpurefunctions are never locked. They cannot change state, so they are always safe to call back into.- The lock lives in transient storage, so it is cheap and clears itself when the transaction ends.
lithe footprint shows each entrypoint's status in its last column:
settle writes: status defers: hook.cancel during defers: rejected
cancel writes: status during defers: rejected
status reads: status during defers: callablenonreentrant applies only to stateful external functions. Named or separate locks are not supported. When you need a finer-grained lock, build it from transient storage.

