Skip to content
LogoLogo

Reentrancy

Deferral means a callee never sees half-written state. It can still call back in before your later deferred calls finish. The compiler warns about those paths, and nonreentrant closes them.

reentrancy.lithe
(* Deferred calls run after local work, so a callee never sees half-written state.
   It can still call back in before later deferred calls run. The compiler warns
   about every such path; `nonreentrant` closes it with one transient lock shared
   by all nonreentrant functions, held until the deferred calls finish. *)
 
interface Hook is {
  function notify(amount : uint);
}
 
storage is record {
  hook : Hook;
  total : uint;
}
 
external function setHook(const hook : Hook) is {
  storage.hook := hook;
}
 
external nonreentrant function add(const amount : uint) is {
  storage.total := storage.total + amount;
  defer storage.hook.notify(amount);
}
 
(* While `add`'s hook runs, calling `reset` reverts. *)
external nonreentrant function reset() is {
  storage.total := 0;
}
 
(* Views are always callable during deferred calls. *)
external view function total() : uint is
  storage.total

lithe footprint shows which entrypoints are locked while deferred calls run:

setHook   writes: hook                                during defers: callable
add        writes: total      defers: hook.notify      during defers: rejected
reset      writes: total                               during defers: rejected
total      reads: total                                during defers: callable