0005. Bare-ETH acceptance is an opt-in facet under the zero selector
- Status: Accepted
- Date: the facet from #155 (2026-07-20); opt-in per recipe from #295 (2026-10-08); recorded 2026-10-09
Context
diamond-lib v0.3.0 made its Diamond base abstract and dropped its receive(). Some diamonds must take
plain ETH sends (accounts, timelocks, an HTS treasury, a swap executor's native payouts); most never
should, and ETH sent to a diamond that cannot move it is lost. A diamond's fallback looks up msg.sig,
which reads as 0x00000000 for empty calldata.
Options considered
- A
Receivefacet cut underbytes4(0). Only diamonds that cut it accept bare sends. receive()on theLatticeproxy. Every diamond would accept bare ETH, including the ones that cannot spend it.- A payable fallback that accepts anything unrouted. It would also swallow calls to selectors that were never cut.
Decision
Latticedeclares noreceive(), so a diamond without the facet rejects bare ETH.Receiveis a stateless facet whosereceive()accepts ETH and whose ERC-8153 export is the single zero selector0x00000000. It has no init, no interface and no ERC-165 registration, sincereceive()has no selector.- A recipe cuts
Receiveonly when its diamond must accept plain native sends. Forwardingmsg.valuethrough a payable function never needs it.
Consequences
- Only empty calldata reaches
receive(). Calldata of one to four zero bytes routes to the facet but matches no function and reverts. - Routing costs a cold selector lookup and a
delegatecall(about 4,700 gas), so Solidity's.transfer()and.send(), with their 2,300-gas stipend, cannot pay a Lattice diamond. Senders usecall{value: ...}(""). Receiveis the one facet whose export differs from itsforge inspectmethod identifiers, and the parity test carries that exception.- The
LatticeNatSpec still says every recipe cutsReceive. That has been false since #295; the fix waits for a change that may move theLatticeFactoryaddress, whose initcode embedsLattice(0004).
Confirmation
ReceiveTest:test_BareSendAcceptedWithReceiveFacet,test_BareSendRejectedWithoutReceiveFacet,test_ExplicitZeroSelectorCalldataRejected,test_ShortNonEmptyCalldataRejected.ExportSelectorsParityTestrequires the export to be exactly0x00000000.