Errors and reverts¶
Fourier has one error mechanism: REVERT. There are no try/catch constructs. A reverting sub-call returns 0 from call_b / delegatecall_b / staticcall_b, and the parent contract decides whether to propagate.
require¶
cond is any expression that produces a non-zero value to pass. msg_bytes is a bytes value (typically constructed with pack_sel(...) or returned by a helper).
Bytecode:
<cond>
PUSHLABEL ok
JUMPI
; --- no msg ---
PUSH 0
PUSH 0
REVERT
; --- with msg ---
<msg_ptr>
DUP 1
MLOAD ; length at ptr
SWAP 1
PUSH 32
ADD ; data offset = ptr + 32
REVERT
ok:
REVERT semantics¶
The REVERT opcode in the VM (vm/machine.py):
if op == Op.REVERT:
offset, length = self._spop(f), self._spop(f)
self._mem_expand(f, offset, length)
raise Revert(bytes(f.memory[offset:offset + length]))
A Revert propagates up to the call boundary that caught a frame snapshot:
except Revert as r:
self.world.restore(snapshot)
return ExecutionResult(
success=False,
gas_used=..., # whatever was consumed so far
return_data=r.return_data,
error="REVERT",
)
What this means:
- All state changes inside the reverted frame are rolled back. Storage writes, balance transfers, and emitted logs are dropped.
- The frame's gas consumed up to the REVERT is still spent. The remaining gas is refunded to the caller, but the work done before the REVERT is not.
- The frame's return data is preserved. The caller can inspect it via
MLOAD(RETURN_AT)after acall_bthat returned 0.
Hard errors vs explicit reverts¶
Two failure modes are distinct (vm/errors.py):
| Error type | Source | State rollback | Gas refund |
|---|---|---|---|
Revert | REVERT opcode (explicit, from require or hand-written) | Yes | Yes (unused gas refunded) |
VMError and subclasses (OutOfGas, StackOverflow, StackUnderflow, InvalidJump, InvalidOpcode, CallDepthExceeded) | Bytecode hit an illegal state | Yes | No — all gas is consumed |
A sub-call hitting either failure path:
- Sets parent's stack-top to
0(the success word fromCALL). - Restores the world state to the pre-call snapshot.
- For
Revert: parent can read the revert payload via memory atRETURN_AT. - For
VMError: parent gets empty return data, no refund.
Sub-call return-value pattern¶
If target reverted, ok == 0, require reverts the parent contract as well, and the whole tx unwinds. If you want to handle the sub-call failure (e.g. log it and continue), skip the require:
let ok: uint = call_b(target, cd, 0, 50000);
if ok == 0 {
emit CallFailed(target);
// continue without reverting
}
Inside a STATICCALL frame¶
STATICCALL (or staticcall_b) executes the target's code with state mutation forbidden. Inside such a frame:
SSTOREraisesVMError("SSTORE forbidden in STATICCALL frame").LOGraisesVMError("LOG forbidden in STATICCALL frame").- A nested
CALLwithvalue > 0raisesVMError("CALL with value forbidden in STATICCALL frame").
These are hard errors — the caller sees 0 from staticcall_b and loses all the gas spent in the static call.
Dispatcher revert¶
If the calldata's first byte does not match any pub fn selector, the dispatcher emits an unconditional PUSH 0 / PUSH 0 / REVERT. The calling tx fails with empty return data.
No errors-as-types¶
Fourier has no Result, no Option, no error-typed return. Failure modes are communicated either through a bool / uint return value checked by the caller, or through a hard revert that aborts the tx. The norm in the stdlib is to return 1 for success and revert on any precondition failure.
Revert payload conventions¶
The bytes payload to require(cond, msg) is opaque — there is no defined string-encoding convention in v1. Common patterns:
- Encode an error code as a single 32-byte word:
- Build a tagged bytes value with
pack_sel(error_code, context_word).
Off-chain consumers receive the payload in the transaction receipt's return_data field; they decide what to do with it.