All pages

Developer reference

Contracts

The public surface of Trimmy: the Rule struct, the enums, every entry point with its selector, the custom errors, and the test configuration.

Working with an agent? Every page here has a markdown twin, the whole site is one file at llms-full.txt, and trimmy-mcp lets it read live rules and compose a payment it cannot send.

Testnet. Trimmy runs on Flare Coston2. Nothing described here has run on Flare mainnet, and no page on this site describes real money.

Source: Trimmy.sol, Quote.sol, Interfaces.sol. Built with solc 0.8.36, via_ir, optimizer_runs = 1_000_000, evm_version = cancun.

Deployment

Read from the live contract on 2026-08-11. Coston2 only.

Trimmy 0x19F81AAB43f7a26B0659754b70179aDcAF43ef7C
TrimmyConfidentialTrigger 0x02EA709e2278EACDbA00D4A88caA604E3b35293b
REGISTRY (the only literal) 0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019
tokenAt(0) FXRP 0x0b6A3645c240605887a5532109323A3E12273dc7, feed XRP/USD, 6 dp
tokenAt(1) WC2FLR 0xC67DCE33D7A8efA5FfEB961899C73fe01bCe9273, feed FLR/USD, 18 dp
venueAt(0) SwapRouter 0xe2B3aE21461c4ad3415210630EA210e9F53CCEBc, SWAP_ROUTER_V3, fee tier 3000
venueAt(1) TESTearnXRP 0x9E63a5D282F2fBb7DcE822B98e363b2719D28319, QUEUED_VAULT
maxFeedAge() 64
protocolFeeRecipient() 0x38d58d1BEA8FF21fd8397494f17F64A99bcF8E83

Constants: MAX_SLIPPAGE_BIPS 50, MAX_PROTOCOL_FEE_BIPS 50, MIN_SCHEDULE_INTERVAL 60, LATCH_DECAY_WINDOW 1 hour, MAX_RULE_LIFETIME 365 days.

Enums

Verb: SWAP 0, DEPOSIT_VAULT 1, EXIT_VAULT 2. Trigger: PRICE_BELOW 0, PRICE_ABOVE 1, SCHEDULE 2, PRIVATE 3. VenueKind: SWAP_ROUTER_V3 0, QUEUED_VAULT 1.

Rule

Packed into eight slots. Field order is load-bearing for that packing.

address account;          // the personal account; == msg.sender at arm()
uint32  epoch;            // must equal epochOf[account] or the rule is dead
uint8   sellTokenId;      // index into the immutable token allowlist
uint8   buyTokenId;
Verb    verb;
uint8   venueId;          // index into the immutable venue allowlist
Trigger trigger;
bool    active;
uint128 totalSellAmount;  // lifetime budget, sell-token base units
uint128 partSellAmount;   // per-execution size
uint128 spent;            // measured deltas in, not amounts requested
uint128 minOutAbsolute;   // user floor, independent of the oracle
uint128 triggerValue;     // relative price, or interval seconds for SCHEDULE
uint128 latchedPrice;     // 0 until first fire, then decays back to live
uint64  nextEligibleAt;
uint64  expiry;
uint64  claimableAt;      // EXIT_VAULT
uint64  claimPeriod;      // EXIT_VAULT: the vault's day-index bucket
uint128 keeperFeeFlat;    // buy-token units, per execution
uint128 keeperFeeBudget;  // lifetime cap
uint128 keeperFeePaid;
uint128 pendingShares;    // EXIT_VAULT: queued, awaiting claim
uint16  slippageBips;
uint16  protocolFeeBips;
uint128 pendingAssets;    // EXIT_VAULT: assets owed for pendingShares
uint64  latchedAt;        // when latchedPrice was set

triggerValue and latchedPrice are relative prices: buy-token base units per one whole sell token. For FXRP to WC2FLR the exponent is (18 + 8) - (6 + 6) = +14, so the value is around 1.7e20, which is 9.3x past uint64. They were uint64 in an earlier build and the latch saturated; a rule armed at 50 bips of slippage settled at roughly 10.7% of oracle fair value while reporting that it had honoured its floor.

RuleParams is deliberately not the storage struct. A caller supplies intent only: sellTokenId, buyTokenId, verb, venueId, trigger, totalSellAmount, partSellAmount, minOutAbsolute, triggerValue, expiry, slippageBips, protocolFeeBips, keeperFeeFlat, keeperFeeBudget. Everything else is derived.

Entry points

Every selector below was computed with cast sig and confirmed present in the deployed runtime at 0x19F81AAB....

function selector who may call
arm((uint8,uint8,uint8,uint8,uint8,uint128,uint128,uint128,uint128,uint64,uint16,uint16,uint128,uint128)) 0xcc0c55f4 anyone; binds msg.sender as rule.account
execute(uint256) payable 0xfe0d94c1 anyone
claim(uint256) 0x379607f5 anyone
cancel(uint256) 0x40e58ee5 rule.account or its guardian
cancelAll(address) 0x97e8f717 that account or its guardian
setGuardian(address) 0x8a0dac4a the account itself
currentPrice(uint256) payable 0x7a3c4c17 anyone; not a view, getFeedById is payable
ruleAt(uint256) 0x63a6fef6 view; reverts NoRule past the end
ruleCount() 0xf6bcf633 view; rule ids are array indices
tokenAt(uint8) 0xbc13f2a4 view; reverts UnknownToken
venueAt(uint8) 0x5ad2dfd6 view; reverts UnknownVenue

Remaining views: tokenCount() 0x9f181b5e, venueCount() 0x8d640d4b, ftsoV2() 0x25e89883, epochOf(address) 0x582805d9, guardianOf(address) 0xacd52e8d, confidentialTrigger() 0xbcb7f3ca.

arm() was 0xc33d4cc3 before triggerValue widened to uint128. Widening the field changed the selector, so an off-chain encoder pinned to the old one silently builds calldata nothing answers.

arm() refuses, rather than accepts and warns: unknown token or venue id, zero or oversized part, slippage above 50 bips, protocol fee above 50 bips, an expiry in the past or beyond 365 days, a SCHEDULE interval under 60 seconds, a non-zero triggerValue on a PRIVATE rule, a zero triggerValue on a price rule, a venue whose kind does not match the verb, a vault whose asset() does not match the sell token (or, for EXIT_VAULT, a sell token that is not the share token), and a fee budget that cannot fund ceil(total / part) executions (doubled for EXIT_VAULT, which pays per execute and again per claim).

execute() loads the rule, checks active, expiry, epoch, eligibility and exhaustion, reads both FTSO legs fresh and age-checks them, evaluates the trigger, latches the first price it fires at, pulls the measured delta with safeTransferFrom, derives the floor from the decayed latch rather than from any venue quote, calls the venue, measures what actually arrived, and pays. PRIVATE rules call confidentialTrigger.consumeVerdict(ruleId, spent), with spent as the nonce so one verdict covers one part.

claim() completes a queued vault exit. It deliberately bypasses the liveness checks: the shares are already burned, so a cancelled or stale-epoch rule must still be able to collect, and in that case fees are zeroed.

Errors

Deploy: EmptyAllowlist, ZeroAddress, TokenDecimalsMismatch(address,uint8,uint8). Arming: UnknownToken(uint8), UnknownVenue(uint8), VenueKindMismatch(uint8,Verb), VaultAssetMismatch(address,address,address), SameToken, ZeroAmount, PartExceedsTotal, SlippageTooHigh(uint16,uint16), ProtocolFeeTooHigh(uint16,uint16), BadExpiry, ScheduleTooFast(uint64,uint64), FeeBudgetCannotFundExecutions(uint128,uint256), PrivateRuleMustNotPublishThreshold. Execution: NoRule(uint256), RuleInactive(uint256), RuleExpired(uint256), StaleEpoch(uint32,uint32), NotYetEligible(uint64), TriggerNotMet(uint256,uint128), Exhausted, FloorBreached(uint256,uint256), InsufficientFeeValue(uint256,uint256), PrivateTriggersDisabled. Vault exit and authority: RedemptionAlreadyPending(uint64), NotYetClaimable(uint64), NothingPending, WrongVerb, NotAuthorised. From Quote: FeedValueZero, ExponentOutOfRange(uint256), FeedStale(uint64,uint256,uint64), FeedFromTheFuture(uint64,uint256). A feed more than MAX_CLOCK_SKEW (10 s) ahead of block.timestamp is refused, because the latch would carry a poisoned price for the rule's life.

Tests

forge test --no-match-path "test/research/*" runs 92 tests:

suite tests
Trimmy.t.sol 25
Quote.t.sol 15
ConfidentialTrigger.t.sol 11
VerdictBinding.t.sol 8
M1Verify.t.sol 7
PrivateTrigger.t.sol 7
TrimmyInvariant.t.sol 7
LatchRegression.t.sol 5
M3Regression.t.sol 4
TrimmyCoverage.t.sol 2
QuoteDifferential.t.sol 1

Fuzz: runs = 10_000, max_test_rejects = 500_000. Invariant: runs = 512, depth = 64, fail_on_revert = false, with one handler as the sole targetContract. The deep profile raises these to 500,000 fuzz runs and 4,096 invariant runs at depth 256. The seven invariants: Trimmy retains neither sell nor buy token, proceeds are fully accounted as user plus keeper plus protocol, spent never exceeds the budget, keeperFeePaid never exceeds keeperFeeBudget, an exhausted rule is never left active, and an executed rule always has a latched price. Handler reach into execute is proven separately in TrimmyCoverage.t.sol rather than in afterInvariant, because a coverage assertion is not shrink-stable. The adversarial suites under test/research/ are excluded from that count and from the deny = "warnings" gate; they are evidence, not production code.

Limits

This deployment is on Coston2 testnet. Nothing has run on Flare mainnet, so no figure here is a mainnet figure. venueAt(0) routes into an FXRP/WC2FLR pool we deployed and seeded ourselves, and it is thin. PRIVATE rules depend on TrimmyConfidentialTrigger and on one enclave staying up, so the enclave operator can censor them by declining to act; the other three triggers are permissionless and re-derive every bound on chain. The contract has no owner, no pause and no rescue, so a wrong constructor argument cannot be corrected, only abandoned.

Source on GitHub Read this page as markdown: /developers/contracts/index.md