Skip to content
LogoLogo

Receive and fallback

A call names an entrypoint with its first four bytes. receive runs for a call with no calldata, such as a plain ether transfer, and fallback for calldata that matches no entrypoint. Without them, both revert.

receive.lithe
(* Plain ether, with no calldata, runs `receive`. Calldata that matches no entrypoint runs
   `fallback`. Without them, both revert. *)
 
import "std/evm";
 
event Received of record {
  indexed from : address;
  amount : wei;
}
 
event Unknown of record {
  indexed selector : bytes4;
  data : bytes;
}
 
error NotOwner;
 
storage is record {
  owner : address;
  received : wei;
}
 
deploy(const owner : address) is {
  storage.owner := owner;
}
 
payable receive is {
  storage.received := storage.received + Evm.value();
  emit Received { from = Evm.sender(); amount = Evm.value(); };
}
 
(* Not `payable`: a call with value that no entrypoint takes is refused. *)
fallback is {
  emit Unknown { selector = Evm.selector(); data = Evm.calldata(); };
}
 
external nonreentrant function withdraw(const amount : wei)
  raises NotOwner
is {
  assert(Evm.sender() = storage.owner, NotOwner);
  defer Evm.transfer(storage.owner, amount);
}
 
external view function received() : wei is
  storage.received
  • receive is always payable: taking ether is what it is for.
  • This fallback is not payable, so a call with value that no entrypoint takes is still refused.
  • Evm.selector() and Evm.calldata() say what the unknown call was.

A proxy

A fallback can pass a call on whole. delegates to runs the target's code on this contract's storage, and returns or reverts with whatever it did:

proxy.lithe
(* A proxy. Every call it has no entrypoint for runs the implementation's code on the
   proxy's storage, and returns or reverts with whatever that code did. The proxy's own
   state sits in a named domain, clear of the implementation's slots. *)
 
import "std/evm";
 
error NotAdmin;
 
storage proxy at "example.proxy" is record {
  implementation : address;
  admin : address;
}
 
deploy(const implementation : address; const admin : address) is {
  proxy.implementation := implementation;
  proxy.admin := admin;
}
 
external function upgradeProxy(const implementation : address)
  raises NotAdmin
is {
  assert(Evm.sender() = proxy.admin, NotAdmin);
  proxy.implementation := implementation;
}
 
payable fallback delegates to proxy.implementation;
  • upgradeProxy has a selector of its own, so the proxy runs it. Every other call goes to the implementation.
  • The implementation's code writes the proxy's storage, so the proxy keeps its own fields in a named domain, where the implementation's default record never reaches.
  • payable lets the implementation's payable functions take ether through the proxy.
  • fallback forwards to target; calls the target instead, which runs on its own storage.

For a contract whose own code is upgradeable, use upgradeable: it adds the guard, migrations, and layout checks. See receive and fallback for the rules.