Repository navigation
Expand file tree
/
Copy pathlib.rs
More file actions
7359 lines (6522 loc) · 315 KB
/
Copy pathlib.rs
File metadata and controls
7359 lines (6522 loc) · 315 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#![no_std]
//! Vortex Protocol — Cross-Chain Intent Settlement
//!
//! Users submit swap intents (e.g. "swap 1 ETH on Ethereum for ~3500 USDC on Stellar").
//! Solvers compete to fill these intents off-chain, then settle on-chain via this contract.
//! Settlement is guaranteed by a solver bond; failing to fill within the deadline slashes the bond.
use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, panic_with_error, token, xdr::ToXdr,
Address, Bytes, BytesN, Env, IntoVal, String, Symbol, Vec,
};
/// Cross-contract client for the `ProofRegistry` contract (issue #190).
/// Used only on the `fill_intent(..., require_proof = true)` path.
use vortex_proof_registry::ProofRegistryClient;
#[cfg(test)]
mod test;
#[cfg(test)]
mod proptest_bond;
#[cfg(test)]
mod bench;
// Issue #415: Pure arithmetic helpers (Env-free, Kani-provable).
pub mod math;
// Issue #415: Kani model-checker harnesses. Only compiled under `kani`.
#[cfg(kani)]
mod kani_proofs;
// ─── Protocol Constants (Canonical Block) – Issue #341 ───────────────────────
// All protocol parameters consolidated here (previously scattered with duplicates).
// Pick one value for each parameter; new variants take the next free numbers.
// ─── Timing Parameters ─────────────────────────────────────────────────────────
/// Default intent lifetime (issue #63). Overridable via set_config().
const INTENT_EXPIRY: u64 = 1800; // 30 minutes
/// Time a solver has to fill after accepting (issue #63). Overridable via set_config().
const FILL_WINDOW: u64 = 300; // 5 minutes
/// Dispute window: user's time to contest a fill (issue #188, docs/dispute-resolution-design.md).
const DISPUTE_WINDOW: u64 = 3_600; // 1 hour
/// Arbiter's time to resolve a dispute (issue #188). After expiry, release_fill becomes permissionless.
const ARBITER_WINDOW: u64 = 86_400; // 24 hours
/// Anti-griefing bond a user must post when opening a dispute (#188).
/// Minimum solver bond (50 USDC).
const MIN_BOND: i128 = 50 * 10_000_000;
/// Protocol fee in basis points (0.05%).
const PROTOCOL_FEE_BPS: i128 = 5;
/// Anti-griefing bond a user must pay when opening a dispute (#188).
const DISPUTE_BOND: i128 = 1 * 10_000_000; // 1 USDC
/// Issue #187 — a solver may hold bonds in at most this many distinct
/// approved tokens. Bounds the work done by `deregister_solver` (which must
/// refund every token) and the storage cost of the per-token bond entries.
const MAX_BOND_TOKENS: u32 = 8;
/// Upper bound on the number of intent IDs `list_open_intents` returns per
/// call (issue #249), bounding the resource cost of paginated reads.
const MAX_PAGE_SIZE: u32 = 100;
/// Number of intent IDs stored per UserIntents bucket (issue #153).
/// Append-only; pruning via `close_intent` leaves holes that pagination skips.
const USER_INTENT_BUCKET_SIZE: u32 = 64;
/// Number of addresses stored per page in the paged persistent sets
/// (SolverList, AllowedDstTokenList) (issue #373).
const PAGED_SET_PAGE_SIZE: u32 = 64;
/// Default retention period (in seconds) for terminal intents before
/// `close_intent` may delete them (issue #371). 30 days.
const DEFAULT_INTENT_RETENTION_SECS: u64 = 2_592_000;
/// Time a solver has to fill after accepting (issue #63). Overridable via set_config().
const FILL_WINDOW: u64 = 300; // 5 minutes
/// Dispute window: user's time to contest a fill (issue #188, docs/dispute-resolution-design.md).
const DISPUTE_WINDOW: u64 = 3_600; // 1 hour
/// After being slashed a solver must wait this many seconds before they can
/// accept new intents. Used by `accept_intent`'s cooldown guard and by
/// `get_slash_cooldown_remaining` (issue #256).
/// `get_slash_cooldown_remaining` (issue #256), which both derive from the
/// same `slash_cooldown_remaining` helper so they can never disagree.
const SLASH_COOLDOWN: u64 = 3_600; // 1 hour
/// Minimum gap the same user must leave between `cancel_intent` calls.
const CANCEL_COOLDOWN: u64 = 60; // 1 minute
/// Arbiter's time to resolve a dispute (issue #188). After expiry, release_fill becomes permissionless.
const ARBITER_WINDOW: u64 = 86_400; // 24 hours
/// Seconds a solver must wait after slash before accepting new intents (issue #256).
const SLASH_COOLDOWN: u64 = 3_600; // 1 hour
/// Minimum gap between successive cancel_intent() calls by the same user (issue #341).
/// Deters cancel-spam griefing while allowing correction of mistakes.
const CANCEL_COOLDOWN: u64 = 60; // 1 minute (NOT 1 hour; validated at #341)
/// #358: Minimum gap the same user must leave between `amend_intent` calls.
/// Separate from CANCEL_COOLDOWN to allow frequent amendments for liquidity.
const AMENDMENT_COOLDOWN: u64 = 60; // 1 minute
const SLASH_COOLDOWN: u64 = 3_600; // 1 hour
/// Delay enforced between proposing and executing a sensitive admin change
/// (admin transfer, fee recipient handover, dst_token allowlist changes).
/// Gives users and solvers a window to notice and react before the change
/// takes effect (#115).
const ADMIN_TIMELOCK_DELAY: u64 = 172_800; // 48 hours
// ─── Default protocol parameters (#202) ──────────────────────────────────────
const DEFAULT_MIN_BOND: i128 = MIN_BOND; // 50 USDC
const DEFAULT_FILL_WINDOW: u64 = FILL_WINDOW; // 300 s
const DEFAULT_INTENT_EXPIRY: u64 = INTENT_EXPIRY; // 1800 s
const DEFAULT_PROTOCOL_FEE_BPS: i128 = PROTOCOL_FEE_BPS; // 5 bps (0.05%)
const DEFAULT_MAX_ACTIVE_INTENTS_PER_SOLVER: u32 = 10;
// ─── `set_config` bounds (#202) ──────────────────────────────────────────────
const MAX_PROTOCOL_FEE_BPS: i128 = 1_000; // 10% — hard ceiling on the protocol fee
const MIN_FILL_WINDOW_SECS: u64 = 60; // a solver needs at least a minute to deliver a fill
const MIN_INTENT_EXPIRY_SECS: u64 = 300; // an intent must stay live for at least five minutes
const MIN_BOND_FLOOR: i128 = 10_000_000; // 1 USDC (7 decimals) — absolute floor for `min_bond`
// ─── Cooldowns (#202) ────────────────────────────────────────────────────────
/// Minimum gap the same user must leave between `cancel_intent` calls.
const CANCEL_COOLDOWN: u64 = 60; // 1 minute
// ─── Batch + extension limits (#202) ─────────────────────────────────────────
/// Upper bound on the number of items any `batch_*` entrypoint processes in a single call.
const MAX_BATCH_SIZE: u32 = 20;
/// Longest additional time `request_extension` can add to an Accepted intent's deadline.
const MAX_EXTENSION_DURATION: u64 = 300; // 5 minutes
// ─── Competitive bid window (#191) ───────────────────────────────────────────
const BID_WINDOW: u64 = 600; // 10 minutes
// ─── Storage-migration schema version (#194) ─────────────────────────────────
const MIGRATION_VERSION: u32 = 1;
// ─── Basis-points denominator ────────────────────────────────────────────────
const BPS_DENOMINATOR: i128 = 10_000;
// ─── Amount sanity bounds ─────────────────────────────────────────────────────
/// Upper bound for src_amount and min_dst_amount (10^30).
pub const MAX_AMOUNT: i128 = 1_000_000_000_000_000_000_000_000_000_000i128;
/// Ceiling on whole dst_token units for the decimals-aware guard (#252).
const MAX_WHOLE_UNITS: i128 = 1_000_000_000_000i128; // 1 trillion
// ─── TTL constants ────────────────────────────────────────────────────────────
const DAY_IN_LEDGERS: u32 = 17280; // ~5s per ledger
const PERSISTENT_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 14;
const PERSISTENT_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30;
const INSTANCE_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30;
const INSTANCE_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 60;
// ─── Referral (#281) ─────────────────────────────────────────────────────────
const MAX_REFERRAL_SHARE_BPS: i128 = 5_000; // 50% max referral share
// ─── Validation limits ───────────────────────────────────────────────────────
const MAX_CHAIN_LEN: u32 = 64;
const MAX_TOKEN_LEN: u32 = 128;
const MAX_FILL_HISTORY: u32 = 10;
// ─── Backstop compensation caps (#369) ───────────────────────────────────────
/// Maximum compensation per intent: 10% of min_dst_amount.
const MAX_BACKSTOP_INTENT_BPS: i128 = 1_000;
/// Maximum compensation per user per 24-hour epoch.
const MAX_BACKSTOP_USER_EPOCH_CLAIM: i128 = 5 * 50 * 10_000_000; // 5 × 50 USDC
/// Epoch length for per-user backstop claim caps.
const BACKSTOP_EPOCH_SECS: u64 = 86_400; // 24 hours
/// Minimum gap between successive cancel_intent() calls by the same user (issue #341).
/// Deters cancel-spam griefing while allowing correction of mistakes.
const CANCEL_COOLDOWN: u64 = 60; // 1 minute (NOT 1 hour; validated at #341)
/// #358: Minimum gap the same user must leave between `amend_intent` calls.
/// Separate from CANCEL_COOLDOWN to allow frequent amendments for liquidity.
const AMENDMENT_COOLDOWN: u64 = 60; // 1 minute
/// Storage-migration schema version (issue #194). Bumped on breaking upgrades.
const MIGRATION_VERSION: u32 = 1;
// ─── Batch + extension limits (#202) ─────────────────────────────────────────
/// Admin timelock for sensitive changes: transfer_admin, set_fee_recipient, dst allowlist (issue #115).
const ADMIN_TIMELOCK_DELAY: u64 = 172_800; // 48 hours
// ─── Economic Parameters ───────────────────────────────────────────────────────
/// Minimum solver bond amount in stroops (50 USDC). Overridable via set_config().
const MIN_BOND: i128 = 50 * 10_000_000; // 50 USDC
/// Minimum delay between `propose_rescue` and `execute_rescue`. Matches
/// `ADMIN_TIMELOCK_DELAY` so the community has the same 48-hour window to
/// notice and react to a pending rescue as to any other admin-key action.
/// A rescue proposal also emits a `rescue_proposed` event immediately so
/// off-chain monitors have advance notice.
const RESCUE_TIMELOCK_DELAY: u64 = ADMIN_TIMELOCK_DELAY; // 48 hours
// ── Defaults seeded into `ProtocolConfig` by `initialize`, and the fallback
// `load_config` returns for contracts deployed before the configurable-params
// feature existed. They mirror the historical compile-time constants above.
const DEFAULT_MIN_BOND: i128 = MIN_BOND;
const DEFAULT_FILL_WINDOW: u64 = FILL_WINDOW;
const DEFAULT_INTENT_EXPIRY: u64 = INTENT_EXPIRY;
const DEFAULT_PROTOCOL_FEE_BPS: i128 = PROTOCOL_FEE_BPS;
/// Protocol fee in basis points (issue #36). Overridable via set_config().
const PROTOCOL_FEE_BPS: i128 = 5; // 0.05%
/// Longest additional time `request_extension` can add to an Accepted intent's
/// deadline. One extension is allowed per intent; this is the same order of
/// magnitude as `FILL_WINDOW` so a single extension can at most roughly double
/// the solver's delivery window.
const MAX_EXTENSION_DURATION: u64 = 300; // 5 minutes
/// Baseline slash rate in basis points (issue #193). Slashed amount is now proportional to intent value.
const SLASH_BPS: i128 = 1_000; // 10% – cap on the slash relative to intent value
/// Anti-griefing bond required from user to open a dispute (issue #233).
const DISPUTE_BOND: i128 = 1 * 10_000_000; // 1 USDC
// ─── Hard Limits ───────────────────────────────────────────────────────────────
/// Absolute max on any amount (src_amount, min_dst_amount) to prevent overflow (issue #341).
/// 10^30 stroops ≈ 1 trillion 18-decimal tokens. Rejects implausibly large inputs.
pub const MAX_AMOUNT: i128 = 1_000_000_000_000_000_000_000_000_000_000i128; // 10^30
/// Max batch size for any batch_* entrypoint (issue #277, validated at #341).
/// 20 items keeps transaction cost bounded within Soroban limits.
const MAX_BATCH_SIZE: u32 = 20;
/// Max extension duration: one `request_extension()` call can add this much time (issue #341).
const MAX_EXTENSION_DURATION: u64 = 300; // 5 minutes (NOT 10 min; 5 min matches FILL_WINDOW magnitude)
// ─── Storage-migration schema version (#194). Bumped whenever a `migrate()`
// body is added for a new release; `initialize` stamps fresh deploys with the
// current value and `migrate` refuses to run once the contract is already at
// it, so a migration can never be applied twice.
const MIGRATION_VERSION: u32 = 1;
// Basis-points denominator, shared by the protocol fee and the #192 discount
// schedule (`discount_bps` is a fraction of the fee, not of the fill).
const BPS_DENOMINATOR: i128 = 10_000;
// Upper sanity bound for src_amount and min_dst_amount.
//
// Largest realistic token amounts use 18-decimal ETH units.
// 1e12 tokens × 1e18 units/token = 1e30, well within i128 range (~1.7e38),
// but downstream arithmetic (fee = amount * 5 / 10_000) multiplies first and
// then divides. To guarantee `amount * PROTOCOL_FEE_BPS` never overflows i128,
// the bound is i128::MAX / PROTOCOL_FEE_BPS ≈ 3.4e37. We choose a round,
// economically implausible threshold: 10^30 (one trillion 18-decimal tokens).
// That is a comfortable safety margin while rejecting only fat-fingered inputs.
pub const MAX_AMOUNT: i128 = 1_000_000_000_000_000_000_000_000_000_000i128; // 10^30
/// Upper bound on the number of items any `batch_*` entrypoint processes in a
/// single call. Keeps the worst-case resource cost (and therefore fee) of one
/// transaction bounded regardless of caller input.
const MAX_BATCH_SIZE: u32 = 20;
// Soroban archives ledger entries that go too long without being touched.
// Persistent Intent/Solver records get their TTL bumped on every write so
// they don't need to be manually restored before later calls can read them.
const DAY_IN_LEDGERS: u32 = 17_280; // ~5s per ledger
const PERSISTENT_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 14;
const PERSISTENT_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30;
// The contract instance entry (Admin/FeeRecipient/BondToken/TotalIntents/
// TotalVolume, plus the
// Soroban archives ledger entries that go too long without being touched.
// Persistent Intent/Solver records get their TTL bumped on every write so
// they don't need to be manually restored before later calls can read them.
const DAY_IN_LEDGERS: u32 = 17_280; // ~5s per ledger
const PERSISTENT_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 14;
const PERSISTENT_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30;
// The contract instance entry (Admin/FeeRecipient/BondToken/TotalIntents/
// TotalVolume, plus the contract's own code) is a single ledger entry and
// needs the same treatment, or the whole contract becomes unreachable.
const INSTANCE_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30;
const INSTANCE_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 60;
// ─── Solver-registry tier perks (#197) ──────────────────────────────────────
//
// Index = tier number (0 Unranked … 4 Platinum). These MUST stay in lock-step
// with `solver_registry`'s tier table and `docs/solver-registry-design.md`
// §3/§6/§7. They are held here, rather than fetched per call, so
// `accept_intent` / `slash_solver` make at most one cross-contract call each
// (just `get_tier`) on their hot paths. A change to these values is a
// protocol-parameter change.
/// Max tokens a solver may hold bonds in (issue #187).
const MAX_BOND_TOKENS: u32 = 8;
/// Max src_chain/dst_token route entries per solver (issue #255).
const MAX_ROUTE_ENTRIES: u32 = 20;
/// Max intent IDs returned by paginated list_open_intents (issue #249).
const MAX_PAGE_SIZE: u32 = 100;
// ─── Config Bounds (enforced by set_config) ────────────────────────────────────
/// Hard cap on protocol fee when set_config is called (issue #202).
const MAX_PROTOCOL_FEE_BPS: i128 = 1_000; // 10%
/// Minimum fill window allowed by set_config (issue #202).
const MIN_FILL_WINDOW_SECS: u64 = 60; // 1 minute
/// Minimum intent expiry allowed by set_config (issue #202).
const MIN_INTENT_EXPIRY_SECS: u64 = 300; // 5 minutes (must exceed FILL_WINDOW)
/// Floor for minimum bond when set_config is called (issue #202).
const MIN_BOND_FLOOR: i128 = 10_000_000; // 1 USDC
// ─── Default Config ────────────────────────────────────────────────────────────
// Seeds ProtocolConfig at initialize() and fallback for pre-upgrade contracts.
const DEFAULT_MIN_BOND: i128 = MIN_BOND;
const DEFAULT_FILL_WINDOW: u64 = FILL_WINDOW;
const DEFAULT_INTENT_EXPIRY: u64 = INTENT_EXPIRY;
const DEFAULT_PROTOCOL_FEE_BPS: i128 = PROTOCOL_FEE_BPS;
// ─── Basis Points ──────────────────────────────────────────────────────────────
/// Denominator for basis-point arithmetic (fee, discount, slash).
const BPS_DENOMINATOR: i128 = 10_000;
// ─── Ledger Entry TTL ──────────────────────────────────────────────────────────
/// Soroban ledger entry eviction constants (issue #267 onwards).
const DAY_IN_LEDGERS: u32 = 17280; // ~5s per ledger
const PERSISTENT_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 14; // Re-extend persistent at 14 days old
const PERSISTENT_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // Extend persistent to 30-day TTL
const INSTANCE_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30; // Re-extend instance at 30 days old
const INSTANCE_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 60; // Extend instance to 60-day TTL
// ─── Solver Tier Perks (issue #197) ────────────────────────────────────────────
// Index = tier (0=Unranked, 1=Bronze, ..., 4=Platinum). MUST lock-step with solver_registry.
/// Fill-window bonus per tier in basis points (10_000 = 100% bonus).
const TIER_FILL_WINDOW_BONUS_BPS: [u64; 5] = [0, 1_000, 2_000, 3_000, 5_000];
/// Slash rate per tier in basis points of bond (floor is 500 bps = 5%).
const TIER_SLASH_BPS: [i128; 5] = [1_000, 1_000, 800, 600, 500];
/// Minimum slash rate across all tiers (Platinum).
const MIN_SLASH_BPS: i128 = 500;
// ─── Hot-path invocation context (issue #375) ────────────────────────────────
/// Lazily loads the values that every state-changing hot-path function needs
/// (config, fee_recipient, open_intents counter, total_volume counter) exactly
/// once per contract invocation, and writes back dirty counters in a single
/// pass before any external token transfer (preserving CEI order).
///
/// **Reentrancy invariant:** cached values must never be trusted across an
/// external call (token transfer, cross-contract call) that could mutate the
/// same storage entries. All storage writes via `commit()` are flushed
/// *before* any external call executes. After `commit()` this context must
/// not be used to write further counter updates in the same invocation.
pub(crate) struct InvocationCtx {
pub config: ProtocolConfig,
pub fee_recipient: Address,
pub open_intents: u64,
pub total_volume: i128,
open_intents_dirty: bool,
total_volume_dirty: bool,
}
impl InvocationCtx {
/// Load all hot-path values once from instance storage.
pub fn load(env: &Env) -> Self {
let config: ProtocolConfig = env
.storage()
.instance()
.get(&DataKey::Config)
.unwrap_or(ProtocolConfig {
min_bond: DEFAULT_MIN_BOND,
fill_window: DEFAULT_FILL_WINDOW,
intent_expiry: DEFAULT_INTENT_EXPIRY,
protocol_fee_bps: DEFAULT_PROTOCOL_FEE_BPS,
max_active_intents_per_solver: DEFAULT_MAX_ACTIVE_INTENTS_PER_SOLVER,
});
let fee_recipient: Address = env
.storage()
.instance()
.get(&DataKey::FeeRecipient)
.unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized));
let open_intents: u64 = env
.storage()
.instance()
.get(&DataKey::OpenIntents)
.unwrap_or(0);
let total_volume: i128 = env
.storage()
.instance()
.get(&DataKey::TotalVolume)
.unwrap_or(0);
InvocationCtx {
config,
fee_recipient,
open_intents,
total_volume,
open_intents_dirty: false,
total_volume_dirty: false,
}
}
/// Decrement open_intents counter (marks dirty for write-back).
pub fn decrement_open_intents(&mut self) {
self.open_intents = self.open_intents.saturating_sub(1);
self.open_intents_dirty = true;
}
/// Increment open_intents counter (marks dirty for write-back).
pub fn increment_open_intents(&mut self) {
self.open_intents += 1;
self.open_intents_dirty = true;
}
/// Add to the total volume counter (marks dirty for write-back).
pub fn add_volume(&mut self, amount: i128) {
self.total_volume += amount;
self.total_volume_dirty = true;
}
/// Write back only dirty counters. Call this before any external token
/// transfer so the storage is consistent if a reentrant call occurs.
pub fn commit(&self, env: &Env) {
if self.open_intents_dirty {
env.storage()
.instance()
.set(&DataKey::OpenIntents, &self.open_intents);
}
if self.total_volume_dirty {
env.storage()
.instance()
.set(&DataKey::TotalVolume, &self.total_volume);
}
}
}
// ─── Storage Keys ─────────────────────────────────────────────────────────────
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
/// **Instance storage.** The admin `Address` that may call privileged
/// functions (`pause`, `unpause`, `propose_fee_recipient`,
/// `propose_admin_transfer`, `propose_add_dst_token`, etc.). Written
/// once by `initialize` and rotated by `accept_admin_transfer`. Lives as
/// long as the contract instance.
Admin,
/// **Instance storage.** The `Address` that receives protocol fees
/// (collected in `fill_intent`) and slashed bond amounts (collected in
/// `slash_solver`). Written by `initialize` and updated by
/// `set_fee_recipient`. Lives as long as the contract instance.
FeeRecipient,
/// Proposed-but-not-yet-accepted new fee recipient plus the ledger
/// timestamp at which `accept_fee_recipient` may execute it (issue #30,
/// timelock added by #115): `(Address, u64)`.
PendingFeeRecipient,
/// **Instance storage.** Proposed-but-not-yet-accepted new admin plus the
/// ledger timestamp at which `accept_admin_transfer` may execute it
/// (#115/#116): `(Address, u64)`. Cleared once the handover completes.
PendingAdmin,
/// **Instance storage.** The stored `ProtocolConfig` (min bond, fill
/// window, intent expiry, protocol fee bps). Seeded by `initialize` and
/// replaced atomically by `set_config`. `load_config` falls back to the
/// `DEFAULT_*` constants when this key is absent (pre-upgrade safety).
Config,
BondToken, // USDC address for bonds
Intent(BytesN<32>), // intent_id -> IntentRecord
/// **Persistent storage.** Bounded on-chain fill-history log for a given
/// intent (issue #244): `Vec<(solver, amount, timestamp)>`, oldest first,
/// capped at `MAX_FILL_HISTORY` entries with FIFO eviction of the oldest
/// entry once the cap is reached. Appended to by `fill_intent`.
IntentFillHistory(BytesN<32>),
Solver(Address), // address -> SolverRecord
/// **Instance storage.** All currently-registered solver addresses
/// (`Vec<Address>`), kept in sync by `register_solver` (append if absent)
/// and `deregister_solver` (remove). Backs the paginated `list_solvers`
/// view (#198) so integrators and dashboards can enumerate solvers without
/// replaying every `solver_registered` / `solver_deregistered` event.
/// Mirror of the `AllowedDstTokenList` pattern used for the dst_token
/// allowlist (#117).
///
/// **Trade-off (#198):** like `OpenIntents`, this counter-style structure
/// lives in the instance entry that is already loaded on every call, so
/// `register_solver` / `deregister_solver` pay only one extra Vec
/// read+write — negligible next to the persistent `SolverRecord` I/O they
/// already do, and neither is a hot path (unlike `accept_intent` /
/// `fill_intent`, which never touch this key). The cost that *does* scale
/// is the size of this single entry: it grows O(n) with the
/// registered-solver count, and the instance entry is deserialized on
/// every contract call. That is comfortably fine into the low thousands of
/// solvers; well beyond that, the enumeration should move to a chunked or
/// paged persistent layout so the per-call instance load stays flat. The
/// alternative — no on-chain enumeration — forces every integrator to
/// replay the full `solver_registered` / `solver_deregistered` event
/// history, which is O(events) and needs an archival node.
SolverList,
TotalIntents,
/// **Instance storage.** Count of intents currently in `Open` or
/// `PartiallyFilled` state (`u64`). Incremented by `submit_intent` and
/// by `slash_solver` (which re-opens the intent). Decremented by
/// `accept_intent`, `cancel_intent`, `expire_intent`, and `fill_intent`
/// (only on a full fill that closes the intent).
///
/// Trade-off (#109): maintaining this counter on-chain costs one extra
/// instance-storage read+write on every state-changing call but gives
/// dashboards an O(1) open-intent count without replaying events. The
/// alternative — leaving the computation entirely to indexers — is cheaper
/// on-chain but forces every dashboard to run a full event replay. Given
/// that the counter sits in instance storage (one ledger entry, already
/// loaded on every call) the marginal cost is a single integer increment/
/// decrement, which is negligible compared to the persistent-storage reads
/// for `IntentRecord` and `SolverRecord`.
OpenIntents,
/// **Instance storage.** Cumulative `dst_token` volume (`i128`) across
/// all successfully filled intents. Incremented by `fill_intent`.
TotalVolume,
/// **Instance storage.** Cumulative protocol fee revenue (`i128`)
/// collected across all fills (issue #248). Incremented by `fill_intent`
/// with the same `fee` value transferred to `FeeRecipient`, so it can
/// never drift from real transferred amounts. Absent until the first
/// fill after this field was introduced; `unwrap_or(0)` handles that.
TotalFeesCollected,
/// **Instance storage.** Count of currently registered solvers (`u32`).
/// Incremented by `register_solver` on first registration, decremented
/// by `deregister_solver`.
TotalSolvers,
/// **Instance storage.** Boolean flag (`true` = paused). Set by
/// `pause()` and cleared by `unpause()`. When `true`,
/// `submit_intent`, `accept_intent`, and `fill_intent` reject all
/// calls. Absent until first `pause()` call (defaults to `false`).
Paused,
/// **Instance storage.** Presence-flag (value `true`) indicating that
/// `token` is on the allowed-destination list. Added by
/// `add_allowed_dst_token` and removed by `remove_allowed_dst_token`.
/// Only checked by `submit_intent` when `DstAllowlistEnabled` is `true`.
AllowedDstToken(Address),
/// **Instance storage.** Boolean toggle (`true` = enforced). Set via
/// `set_dst_allowlist_enabled`. When `false` (the default), the
/// `AllowedDstToken` list is populated but not enforced by
/// `submit_intent`, letting an admin pre-populate the list before
/// switching enforcement on.
DstAllowlistEnabled,
/// **Instance storage.** Enumerable mirror of the `AllowedDstToken`
/// presence flags (`Vec<Address>`), maintained by
/// `add_to_dst_token_list` / `remove_from_dst_token_list`. Backs
/// `list_allowed_dst_tokens` (#117).
AllowedDstTokenList,
/// **Persistent storage.** Per-`dst_token` bond multiplier (`i128`, where
/// `10` = 1.0×). Set by `set_min_bond_multiplier`; consulted by
/// `get_adjusted_min_bond` in `accept_intent`. Absent ⇒ 1.0×.
MinBondMultiplier(Address),
// ── Issue #153: Bucketed UserIntents ────────────────────────────────────
/// **Persistent storage.** A fixed-size bucket of up to `USER_INTENT_BUCKET_SIZE`
/// intent IDs for `user`, indexed by `bucket_idx`. Buckets are append-only;
/// closed intents leave `None` holes that pagination skips. Backed by
/// `UserIntentCount` for total-count tracking.
UserIntents(Address, u32),
/// **Persistent storage.** Total number of intent IDs (including holes from
/// pruned intents) ever appended to `user`'s buckets. Divided by
/// `USER_INTENT_BUCKET_SIZE` gives the current bucket index; the remainder
/// is the slot within that bucket.
UserIntentCount(Address),
/// **Persistent storage.** Ledger timestamp of a user's most recent
/// `cancel_intent` (`u64`). Enforces `CANCEL_COOLDOWN` between cancels.
CancelCooldown(Address),
/// **Persistent storage.** Presence flag (`true`) recording that an intent
/// has already used its single permitted `request_extension`.
ExtensionGranted(BytesN<32>),
UserNonce(Address), // per-user submit counter to widen intent_id preimage
AllowedSrcChain(String), // src_chain name -> present if allowed
SrcChainAllowlistEnabled,
/// **Instance storage.** Pending `propose_add_dst_token` proposal: maps a
/// candidate `dst_token` to the ledger timestamp (`u64`) at which
/// `execute_add_dst_token` may apply it (#118).
PendingDstTokenAdd(Address),
/// **Instance storage.** Pending `propose_remove_dst_token` proposal: maps
/// a `dst_token` to the ledger timestamp (`u64`) at which
/// `execute_remove_dst_token` may apply it (#118).
PendingDstTokenRemove(Address),
/// **Instance storage.** The `Address` authorized to call `pause` in
/// addition to `Admin` (issue #120). Lets an operator hand a hot key to
/// an incident-response process without exposing the admin key that
/// also controls fee routing and admin transfer. Absent until the admin
/// calls `set_pauser`, in which case `pause` remains admin-only.
/// `unpause` is intentionally *not* reachable via this role (narrow
/// unpause access) -- resuming the protocol always needs the full
/// admin's judgment.
Pauser,
/// **Instance storage.** `Address` of the deployed `ProofRegistry`
/// contract (issue #190, docs/124 §4.1). Absent until the admin calls
/// `set_proof_registry`. Only read by `fill_intent` when it is invoked
/// with `require_proof = true`; a `require_proof = false` fill never
/// touches this key, so proof-gating is fully opt-in and defaults off
/// exactly like `DstAllowlistEnabled`.
ProofRegistry,
// ── Multi-bond-token support (#187) ─────────────────────────────────────
/// Per-(solver, bond_token) bond balance (i128).
SolverBond(Address, Address),
/// Allowed bond tokens set (presence flag `true`).
AllowedBondToken(Address),
/// Minimum bond amount required for a non-default bond token (i128).
MinBond(Address),
// ── Per-solver pending intents list (#230) ───────────────────────────────
/// `Vec<BytesN<32>>` of intent IDs currently accepted by this solver.
SolverIntents(Address),
// ── Total bonded across all solvers (#231) ───────────────────────────────
TotalBonded,
// ── Token-level stats (#248) ─────────────────────────────────────────────
TokenVolume(Address),
TokenFees(Address),
// ── Fee discount schedule (#192) ─────────────────────────────────────────
/// `Vec<(i128, i128)>` — (min_volume, discount_bps) pairs, ascending.
FeeDiscountTiers,
// ── Open-intent enumeration list (#249) ──────────────────────────────────
OpenIntentList,
// ── Solver registry link (#197) ───────────────────────────────────────────
SolverRegistry,
// ── Solver routes (#255) ─────────────────────────────────────────────────
SolverRoutes(Address),
// ── Reputation snapshot (#272) ───────────────────────────────────────────
SolverReputation(Address),
// ── Contract upgrade (#194) ──────────────────────────────────────────────
PendingUpgrade,
MigrationVersion,
// ── Bid window (#191) ────────────────────────────────────────────────────
BidWindowEnabled,
BestBid(BytesN<32>),
// ── Dispute / arbiter (#188) ─────────────────────────────────────────────
Arbiter,
// ── Backstop pool (#280) ─────────────────────────────────────────────────
/// Current backstop pool balance (i128), funded by slash proceeds.
BackstopPool,
/// Presence flag recording that a user already claimed for this intent.
BackstopClaimed(BytesN<32>),
// ── #367 Exposure tracking ────────────────────────────────────────────────
/// Per-solver accepted notional exposure (i128).
SolverExposure(Address),
// ── #368 Oracle ───────────────────────────────────────────────────────────
/// Address of the oracle adapter contract.
OracleAddr,
/// Pending oracle proposal: (Address, u64 eta).
PendingOracle,
// ── #369 Per-user epoch claims ────────────────────────────────────────────
/// Per-user epoch claim amount (i128). Key: (user, epoch_number).
UserEpochClaim(Address, u64),
// ── #370 Backstop vault ───────────────────────────────────────────────────
/// Address of the deployed backstop_vault contract.
BackstopVault,
/// bps of fees/slashes routed to the vault.
BackstopVaultShareBps,
/// **Persistent storage.** Pending `propose_rescue` record for `token`:
/// stores the target address, the amount, the ledger timestamp at which
/// `execute_rescue` may apply it, and the ledger sequence at which the
/// proposal was created (for the same-ledger conflict guard).
/// Cleared by `execute_rescue` or a future `cancel_rescue` call.
PendingRescue(Address),
/// **Instance storage.** Per-token running liability counter (`i128`) for
/// all protocol obligations in `token` NOT already covered by `TotalBonded`
/// or `BackstopPool`:
/// - bond token: open user dispute bonds currently held in escrow
/// (incremented by `dispute_fill` / `open_dispute`, decremented by
/// `resolve_dispute` or the arbiter-timeout path in `release_fill`).
/// - dst tokens: fill amounts currently held in contract escrow
/// (incremented by `begin_fill`, decremented by `release_fill` and
/// `resolve_dispute`).
///
/// Used by `check_solvency` for an O(1) liability read without scanning
/// every intent. `unwrap_or(0)` is the correct fallback when absent.
Liabilities(Address),
/// **Instance storage.** Optional policy contract address (issue #378).
/// When set, `submit_intent` calls `check_intent(user, dst_token, amount)
/// -> bool` before proceeding. Absent by default — zero overhead when not
/// configured.
PolicyContract,
/// **Instance storage.** Pending policy contract proposal: `(Option<Address>, u64)`
/// where the u64 is the ETA timestamp after which `execute_set_policy` may run.
PendingPolicy,
/// Issue #362: Deposit escrow for a given intent. Maps intent_id -> (user, amount, token).
/// Refunded on Filled/Cancelled, forfeited on Expired, paid to expire_intent caller on Expired.
IntentDeposit(BytesN<32>),
/// Issue #361: Consumed nonces for gasless intents (anti-replay). Maps public_key + nonce -> true.
ConsumedNonce(BytesN<32>, u64),
/// Issue #361: Per-public-key nonce counter for submit_intent_signed (prevents replay).
/// Maps public_key -> next nonce to accept.
PublicKeyNonce(BytesN<32>),
// ── Issue #373: Paged persistent SolverList ─────────────────────────────
/// **Persistent storage.** Page `page_idx` of the registered-solver
/// enumerable list (`Vec<Address>`), each page holding up to
/// `PAGED_SET_PAGE_SIZE` entries. Replaces the instance-storage
/// `SolverList` so the per-call instance size stays flat.
SolverListPage(u32),
/// **Persistent storage.** Total count of entries across all solver-list
/// pages (including swap-remove holes). Used to compute the current page
/// index and to size pagination loops.
SolverListCount,
/// **Persistent storage.** Index of `solver` in the flat SolverList
/// (page * PAGE_SIZE + slot_within_page), enabling O(1) swap-remove.
SolverListIdx(Address),
// ── Issue #373: Paged persistent AllowedDstTokenList ────────────────────
/// **Persistent storage.** Page `page_idx` of the allowed-dst-token
/// enumerable list, each page holding up to `PAGED_SET_PAGE_SIZE` entries.
DstTokenListPage(u32),
/// **Persistent storage.** Total count of entries across all dst-token-list pages.
DstTokenListCount,
/// **Persistent storage.** Index of `token` in the flat DstTokenList,
/// enabling O(1) swap-remove.
DstTokenListIdx(Address),
// ── Issue #374: O(1) SolverIntents index ────────────────────────────────
/// **Persistent storage.** Position of `intent_id` within
/// `SolverIntents(solver)`, stored as a `u32`. Enables O(1) swap-remove
/// without a linear scan.
SolverIntentIdx(Address, BytesN<32>),
// ── Issue #371: Intent pruning ───────────────────────────────────────────
/// **Persistent storage.** Presence flag (`true`) marking that
/// `intent_id` has been closed by `close_intent` and its record deleted.
/// Kept as a tombstone so `IntentAlreadyExists` still fires on id reuse,
/// preventing a pruned-and-resubmitted intent from appearing as new.
IntentTombstone(BytesN<32>),
/// **Instance storage.** Configurable retention period (seconds) after an
/// intent reaches a terminal state before `close_intent` may delete it.
/// Defaults to `DEFAULT_INTENT_RETENTION_SECS` (30 days) when absent.
IntentRetention,
// ── Pre-existing variants used throughout the contract ───────────────────
/// **Persistent storage.** Active intent IDs currently accepted by `solver`.
SolverIntents(Address),
/// **Persistent storage.** Per-solver cumulative volume in a specific token.
SolverBond(Address, Address),
/// **Persistent storage.** Reputation snapshot preserved across deregister.
SolverReputation(Address),
/// **Persistent storage.** Declared route preferences for a solver.
SolverRoutes(Address),
/// **Persistent storage.** A pending solver bid for a Bidding-state intent.
SolverBid(Address, BytesN<32>),
/// **Instance storage.** Total bonded amount across all solvers.
TotalBonded,
/// **Instance storage.** Schema version for the run-once migrate() hook.
MigrationVersion,
/// **Instance storage.** Pending wasm-upgrade proposal `(hash, eta)`.
PendingUpgrade,
/// **Instance storage.** Whether bid-window mode is active.
BidWindowEnabled,
/// **Instance storage.** Explicit arbiter address for dispute resolution.
Arbiter,
/// **Instance storage.** Cumulative volume by dst_token.
TokenVolume(Address),
/// **Instance storage.** Cumulative fees by dst_token.
TokenFees(Address),
/// **Instance storage.** Per-token minimum bond override (non-default tokens).
MinBond(Address),
/// **Instance storage.** Allowed non-default bond token presence flag.
AllowedBondToken(Address),
/// **Instance storage.** Address of the optional solver registry contract.
SolverRegistry,
/// **Persistent storage.** The leading bid for a Bidding-state intent.
BestBid(BytesN<32>),
/// **Persistent storage.** Presence flag: user has claimed backstop for intent.
BackstopClaimed(BytesN<32>),
/// **Instance storage.** Total backstop pool balance.
BackstopPool,
/// **Instance storage.** Backstop diversion rate in bps.
BackstopBps,
/// **Instance storage.** Maximum fraction of pool one intent can claim.
BackstopClaimBps,
/// **Instance storage.** Optional referral share in bps (issue #281).
ReferralShareBps,
/// **Persistent storage.** Timestamp of a user's most recent
/// `amend_intent` call (`u64`). Enforces `AMENDMENT_COOLDOWN` between amendments.
/// (#358)
AmendmentCooldown(Address),
/// **Persistent storage.** Outbound (reverse) intent record locked in escrow.
/// Maps outbound_intent_id -> OutboundIntentRecord. (#355)
OutboundIntent(BytesN<32>),
/// **Persistent storage.** All outbound intent ids submitted by a given user.
/// Vec<BytesN<32>>, appended by `submit_outbound_intent`. (#355)
UserOutboundIntents(Address),
/// Issue #363: Solver operator (session) key grants: (solver, operator) → Operator struct
SolverOperator(Address, Address),
/// Issue #364: Delegated staking shares: (solver, delegator) → share balance
DelegatedStake(Address, Address),
/// Issue #364: Total delegated shares for a solver (for share accounting)
SolverDelegatedShares(Address),
/// Issue #365: Commit-reveal sealed bids: (intent_id, solver) → CommittedBid
CommittedBid(BytesN<32>, Address),
/// Issue #365: Bid reveal window configuration per intent
BidRevealWindow(BytesN<32>),
/// Issue #366: Configuration flag for batch mode (atomic vs best-effort)
BatchModeConfig,
}
// ─── Data Structs ─────────────────────────────────────────────────────────────
/// Issue #408 — full configuration bundle for a single source chain.
///
/// `propose_chain_onboarding` queues this struct under a timelock;
/// `execute_chain_onboarding` applies it atomically to both
/// `intent_settlement` (src-chain allowlist) and `proof_registry`
/// (authorized emitter).
///
/// **Fields:**
/// * `name` — canonical lowercase string used in `submit_intent`'s
/// `src_chain` field (e.g. `"ethereum"`).
/// * `wormhole_id` — Wormhole chain ID (e.g. 2 for Ethereum).
/// * `axelar_name` — Axelar source-chain identifier string (e.g. `"Ethereum"`).
/// * `emitter` — 32-byte Wormhole emitter address for this chain.
/// * `axelar_source`— Axelar source-contract address string.
/// * `token_format` — human-readable description of the `src_token` format
/// (e.g. `"0x-prefixed 40-char hex"`); stored for reference,
/// not validated on-chain.
#[contracttype]
#[derive(Clone)]
pub struct ChainConfig {
pub name: String,
pub wormhole_id: u32,
pub axelar_name: String,
pub emitter: BytesN<32>,
pub axelar_source: String,
pub token_format: String,
}
/// Admin-configurable protocol parameters. Stored as a single instance-storage
/// entry so all values are read/written atomically.
#[contracttype]
#[derive(Clone)]
pub struct ProtocolConfig {
/// Minimum solver bond in bond_token's smallest unit.
pub min_bond: i128,
/// Seconds a solver has to fill after accepting an intent.
pub fill_window: u64,
/// Default intent lifetime in seconds (used when submit_intent deadline is None).
pub intent_expiry: u64,
/// Protocol fee in basis points charged on each fill (0.01% per bps).
pub protocol_fee_bps: i128,
/// Maximum number of intents a single solver may accept simultaneously (issue #230).
pub max_active_intents_per_solver: u32,
/// Maximum slash cycles before an intent is abandoned (#241).
pub max_slash_cycles: u32,
/// Referral share in bps (0 = disabled, max 5000 = 50%) (#281).
pub referral_share_bps: i128,
/// Bps of fees/slashes routed to the backstop vault (#370, 0 = disabled).
pub backstop_vault_share_bps: i128,
/// Exposure coverage multiplier (#367): accepted_notional <= bond * coverage_multiplier.
/// Stored as a plain integer (e.g. 10 means 10×).
pub coverage_multiplier: i128,
/// Issue #362: Optional deposit token (None = deposits disabled).
pub submission_deposit_token: Option<Address>,
/// Issue #362: Deposit amount in smallest units (refunded on fill/cancel, forfeited on expiry).
pub submission_deposit_amount: i128,
/// #281: Percentage of the protocol fee paid to the intent referrer, in bps.
/// 10_000 bps = 100% (entire fee to referrer), 0 = none. Capped at 10_000.
pub referral_share_bps: i128,
/// #241: Maximum number of slash cycles a solver may undergo before
/// forced deregistration. 0 = unlimited (default). Enforced by `slash_solver`.
pub max_slash_cycles: u32,
}
/// #357: Optional post-fill hook that invokes a recipient contract
#[contracttype]
#[derive(Clone)]
pub struct Hook {
pub contract: Address, // recipient contract to invoke
pub symbol: Symbol, // function to call
pub data: Bytes, // encoded arguments
pub fail_policy: HookFailPolicy,
}
#[contracttype]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum HookFailPolicy {
Revert, // failing hook reverts the fill
Isolate, // failing hook emits event but doesn't revert
}
/// A user's cross-chain swap intent
#[contracttype]
#[derive(Clone)]
pub struct IntentRecord {
pub intent_id: BytesN<32>,
pub user: Address,
/// #356: Optional recipient for the output (defaults to user if None)
pub recipient: Option<Address>,
/// Source chain details (off-chain reference)
pub src_chain: String, // "ethereum" | "base" | "polygon" etc.
pub src_token: String, // token address on source chain
pub src_amount: i128, // amount in source token's smallest unit
/// Destination (always Stellar)
pub dst_token: Address, // SAC/SEP-41 token on Stellar
pub min_dst_amount: i128, // minimum acceptable output per fill (#348: per src_portion)
pub solver: Option<Address>, // assigned solver
pub state: IntentState,
/// #357: Optional post-fill hook to invoke on the recipient contract
pub hook: Option<Hook>,
pub created_at: u64,
/// #347: user's original deadline; separate from fill-window deadline which gets reset
pub user_deadline: u64,
pub deadline: u64, // effective deadline (fill window or user deadline)
pub filled_at: Option<u64>,
pub fill_amount: Option<i128>, // cumulative dst tokens received across all fills
/// Issue #187: the approved bond token that backs this intent's fill
/// guarantee. Set to the solver's chosen token in `accept_intent` /
/// `settle_bids` and consulted by `slash_solver` so the slash is taken
/// from — and paid out in — the same token the solver actually bonded.
/// Defaults to the legacy `DataKey::BondToken` for intents that never
/// reach an `Accepted`-family state.
pub bond_token: Address,
/// Issue #188: end of the escrow/dispute window, set by `begin_fill`.
/// `None` for intents that took the legacy one-shot `fill_intent` path.
pub dispute_deadline: Option<u64>,
/// Issue #188: timestamp `dispute_fill` was called, if a dispute is open.
pub dispute_raised_at: Option<u64>,
/// Issue #188: the arbiter's decision once `resolve_dispute` has run.
pub resolution: Option<DisputeResolution>,