Addresses
FXRP is never hardcoded in the code that runs: it is resolved through
the Flare Contract Registry → AssetManagerFXRP →
fAsset(), so a redeployment of the token does not silently
break anything. The address above is what that path returned.
london and not the default.
Solidity 0.8.24 targets cancun by default, which emits
TSTORE and MCOPY; shanghai emits
PUSH0. Flare's EVM is Avalanche-derived, and its support
for those is not something to discover from a failed deployment. The
deployed runtime bytecode was checked for all three and contains
none.
Interface
struct PaymentIntent {
bytes32 invoiceId;
address payer;
address payee;
uint256 amount;
uint256 deadline;
}
struct Signature { uint8 v; bytes32 r; bytes32 s; }
function settle(
PaymentIntent calldata intent,
Signature calldata permitSig, // over FXRP's EIP-712 domain
Signature calldata intentSig // over this contract's domain
) external;
function intentDigest(PaymentIntent calldata intent) external view returns (bytes32);
function token() external view returns (address);
function settled(bytes32) external view returns (bool);
function DOMAIN_SEPARATOR() external view returns (bytes32);
function PAYMENT_INTENT_TYPEHASH() external view returns (bytes32);
event PaymentSettled(
bytes32 indexed invoiceId,
address indexed payer,
address indexed payee,
uint256 requested,
uint256 delivered
);
The intent and both signatures are passed as calldata structs
rather than as flat parameters. That is not stylistic: eleven flat
arguments overflow the stack under legacy codegen, and structs fix it
without turning on via-ir, which would change the codegen
the london target was chosen to control.
Check the digest before you sign
intentDigest() is exposed so an offchain signer can assert
its locally built digest matches the chain's, rather than discovering a
mismatch as an unexplained revert. If the two disagree, every
settle() fails with
IntentNotSignedByPayer and the error names no cause.
settle.ts makes this check before it signs anything.
const local = hashTypedData({
domain: { name: "Scrip", version: "1", chainId: 114, verifyingContract: facilitator },
types: { PaymentIntent: [
{ name: "invoiceId", type: "bytes32" },
{ name: "payer", type: "address" },
{ name: "payee", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "deadline", type: "uint256" },
]},
primaryType: "PaymentIntent",
message: intent,
});
const onchain = await client.readContract({
address: facilitator, abi, functionName: "intentDigest", args: [intent],
});
if (local !== onchain) throw new Error("do not sign - the domains disagree");
Custom errors
Every failure is a named error rather than a bare revert. Put these in your ABI so a revert decodes to a cause instead of a selector.
| Selector | Error | Means |
|---|---|---|
0xaa2fd925 |
Expired |
The deadline passed before the transaction was mined. Re-sign and resubmit. |
0xb196a44a |
AlreadySettled |
That invoice ID was already paid. IDs are single-use. |
0x90f478ba |
IntentNotSignedByPayer |
The intent signature recovered to someone other than the payer - a substituted field, or a domain mismatch. |
0x2a1b2dd8 |
InsufficientAllowance |
The permit did not land and no allowance was already in place. |
0x9a8c96da |
Underdelivered |
The payee received less than the invoice - the token levied a transfer fee. |
0xf0ad0d09 |
MalleableSignature |
The signature's s was in the upper half order. The signer is not normalising to EIP-2. |
0x1f003d0a |
BadSignatureV |
v was neither 27 nor 28. |
0x90b8ec18 |
TransferFailed |
transferFrom reverted or returned false. |
0xe1ee996d |
ZeroPayee |
The payee was the zero address. |
Properties it enforces
- The payee cannot be substituted. Every field a relayer supplies is covered by the intent digest.
- An invoice settles once. The ID is marked before any external call - effects before interactions.
- A replayed permit cannot grief a payment. The permit call is wrapped, and the allowance is checked explicitly afterwards.
-
Signatures are not malleable.
sis constrained to the lower half order, andecrecoverreturning zero is treated as failure rather than as an address. - Underpayment reverts. The payee's balance is read before and after, so a transfer fee cannot silently shortchange the invoice.
-
Signatures cannot cross a fork. The domain separator
is rebuilt if
chainidchanges.
The first three are exercised as attacks, not asserted - see the evidence.