-
Notifications
You must be signed in to change notification settings - Fork 122
Expand file tree
/
Copy pathlib.rs
More file actions
4684 lines (4130 loc) · 176 KB
/
Copy pathlib.rs
File metadata and controls
4684 lines (4130 loc) · 176 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
//! # Trivela Rewards Contract
//!
//! On-chain points and rewards for the Trivela campaign platform.
//! Tracks user balances and allows claiming rewards.
//!
//! Events:
//! - `credit`: topics `(credit, user)`, data `amount: u64`
//! - `claim`: topics `(claim, user)`, data `amount: u64`
//! - `transfer`: topics `(transfer, from, to)`, data `amount: u64`
//! - `paused`: topics `(paused,)`, data `is_paused: bool`
//! - `pscredit`: topics `(pscredit,)`, data `is_paused: bool` (credit-class pause, #1019)
//! - `psclaim`: topics `(psclaim,)`, data `is_paused: bool` (claim-class pause, #1019)
//! - `psredeem`: topics `(psredeem,)`, data `is_paused: bool` (redeem-class pause, #1019)
//! - `max_credit_per_call`: topics `(mxcredit,)`, data `max_amount: u64`
//! - `campaign_multiplier`: topics `(multset, campaign_id)`, data `multiplier_bps: u32`
//! - `rate_limit_set`: topics `(ratlset,)`, data `(max_calls: u32, window_ledgers: u32)`
//! - `snapshot`: topics `(snapshot, snapshot_id)`, data `ledger: u32`
//! - `vested_credit`: topics `(vcredit, user)`, data `(vest_id: u64, total: u64)`
//! - `vested_claim`: topics `(vclaim, user)`, data `(vest_id: u64, amount: u64)`
//! - `redeem`: topics `(redeem, user)`, data `(points_burned: u64, asset_amount: i128)`
//! - `ref_config`: topics `(refcfg,)`, data `(rate_bps: u32, per_referrer_cap: u64)`
//! - `ref_bonus`: topics `(refbonus, referrer, referee)`, data `(bonus: u64, qualifying_amount: u64)`
//! - `pruned`: topics `(pruned, kind)`, data `count: u32`
//!
//! ## Storage pruning
//!
//! Multisig nonce records are not bumped indefinitely on Soroban;
//! [`RewardsContract::prune_used_nonces`] lets anyone reclaim storage for
//! nonces past their TTL, in capped batches. [`RewardsContract::storage_stats`]
//! reports current usage for monitoring.
//!
//! ## Co-admin multisig
//!
//! `set_paused` is a critical operation: once a threshold is configured via
//! `set_multisig_threshold`, it requires at least that many valid co-admin
//! signatures (registered via `add_co_admin`) over `(op, nonce, args_hash)`,
//! verified with ed25519. The nonce is consumed on use regardless of how many
//! signers participated.
#![no_std]
use soroban_sdk::{
contract, contracterror, contractimpl, contractmeta, contracttype, symbol_short, Address,
Bytes, BytesN, Env, Symbol, Vec,
};
pub mod groth16;
#[cfg(test)]
mod poseidon;
#[cfg(test)]
mod merkle;
#[cfg(test)]
mod poseidon_merkle_tests;
#[cfg(test)]
mod poseidon_vs_sha256_bench;
#[cfg(test)]
mod airdrop_test;
#[contracterror]
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
Overflow = 1,
InsufficientBalance = 2,
Unauthorized = 3,
ContractPaused = 4,
CreditLimitExceeded = 5,
UnsupportedMigration = 6,
InvalidMultiplier = 7,
RateLimitExceeded = 8,
VestingNotFound = 9,
NoPendingAdmin = 10,
InsufficientReserve = 11,
InvalidRedemptionRate = 12,
InvalidAdminNonce = 13,
/// A referrer and referee cannot be the same address.
SelfReferral = 14,
/// The referee was previously rewarded as a referee of this referrer (cycle).
CircularReferral = 15,
/// This referee has already triggered a referral bonus (one per referee).
ReferralAlreadyRewarded = 16,
/// Paying this bonus would exceed the configured per-referrer cap.
ReferralCapExceeded = 17,
/// Referral rewards have not been configured (bonus rate is zero).
ReferralNotConfigured = 18,
/// The supplied referral configuration is invalid.
InvalidReferralConfig = 19,
/// The computed referral bonus rounded down to zero.
ZeroReferralBonus = 20,
// ── SEP-41 Token errors (issue #530) ──────────────────────────────────────
/// SEP-41 token mode is not enabled.
TokenModeNotEnabled = 21,
/// SEP-41: allowance not sufficient for transfer_from.
AllowanceExceeded = 22,
/// SEP-41: approval expiration ledger has passed.
ApprovalExpired = 23,
/// SEP-41: invalid expiration ledger (must be > current ledger).
InvalidExpiration = 24,
InvalidThreshold = 25,
InsufficientSignatures = 26,
NonceReused = 27,
DuplicateSigner = 28,
UnknownSigner = 29,
/// Operation amount must be greater than zero (issue #1020).
ZeroAmount = 30,
/// Transfer source and destination cannot be the same address (issue #1020).
SelfTransfer = 31,
/// No clawback proposal found for the given id.
ClawbackNotFound = 32,
/// The timelock delay for this clawback has not yet elapsed.
ClawbackTimelocked = 33,
/// Clawback amount exceeds the target's current unclaimed balance.
ClawbackOverspend = 34,
/// Only the configured guardian (admin) may cancel a clawback proposal.
ClawbackGuardianOnly = 35,
// ── Multi-sig errors (issue #733) ─────────────────────────────────────────
/// Multi-sig configuration has not been initialised.
MultiSigNotConfigured = 36,
/// The caller is not in the authorised signer set.
NotASigner = 37,
/// This signer has already approved this proposal.
AlreadyApproved = 38,
/// The referenced proposal does not exist.
ProposalNotFound = 39,
/// The proposal has passed its expiry ledger.
ProposalExpired = 40,
/// The proposal does not yet have enough approvals to execute.
InsufficientApprovals = 41,
// ── Governance errors (issue #735) ────────────────────────────────────────
/// Governance quorum or delay has not been configured.
GovernanceNotConfigured = 42,
/// A governance proposal for this key is already pending.
ProposalAlreadyPending = 43,
/// The time-lock delay has not yet elapsed.
TimeLockActive = 44,
/// The governance proposal has been cancelled or never existed.
ProposalCancelled = 45,
// ── Airdrop errors (issue #845) ──────────────────────────────────────────────
/// Merkle airdrop root has not been set.
AirdropRootNotSet = 46,
/// The supplied merkle proof is invalid or does not match the root.
AirdropInvalidProof = 47,
/// The nullifier has already been used to claim from this airdrop.
AirdropNullifierUsed = 48,
// ── Distribution mode errors (issue #871) ─────────────────────────────────────
/// Invalid distribution mode specified.
InvalidDistributionMode = 49,
/// Below minimum claim amount.
BelowMinClaim = 50,
/// Invalid boost curve configuration.
InvalidBoostCurve = 51,
/// Zero boost multiplier not allowed.
ZeroBoostMultiplier = 52,
/// Invalid lock schedule configuration.
InvalidLockSchedule = 53,
// ── Issue #900: Minimum claim threshold ──────────────────────────────────
/// Claim amount is below the configured minimum threshold.
BelowMinClaim = 49,
// ── Issue #903: Campaign supply cap ───────────────────────────────────────
/// Credit would exceed the campaign's configured total supply cap.
CampaignSupplyCapExceeded = 50,
/// Campaign supply cap configuration is invalid.
InvalidSupplyCap = 51,
// ── Issue #898: Multi-level referral tree ────────────────────────────────
/// Invalid referral depth configuration (must be > 0 and <= MAX_REFERRAL_DEPTH).
InvalidReferralDepth = 52,
/// Referral tier configuration is invalid.
InvalidReferralTierConfig = 53,
// ── Staking/Boost errors ──────────────────────────────────────────────────
/// Invalid boost curve configuration.
InvalidBoostCurve = 54,
/// Lock schedule configuration is invalid.
InvalidLockSchedule = 55,
/// Boost multiplier cannot be zero.
ZeroBoostMultiplier = 56,
// ── Issue #895: Operator delegation errors ────────────────────────────────
/// Operator budget has been fully consumed.
OperatorBudgetExceeded = 57,
/// Operator delegation not found or has been revoked.
OperatorDelegationNotFound = 58,
/// Invalid operator delegation configuration.
InvalidOperatorDelegation = 59,
// ── Issue #896: Multi-asset redemption errors ─────────────────────────────
/// Redemption asset not found or not configured.
RedemptionAssetNotFound = 60,
/// Redemption asset is disabled.
RedemptionAssetDisabled = 61,
/// Invalid asset configuration.
InvalidAssetConfig = 62,
// ── Issue #899: Claim cooldown errors ─────────────────────────────────────
/// Claim is still within the cooldown period.
ClaimCooldownActive = 63,
/// Invalid cooldown configuration.
InvalidCooldownConfig = 64,
}
// ── Issue #895: Operator delegation types ─────────────────────────────────────
/// Operator delegation configuration stored per (operator, campaign_id).
#[contracttype]
#[derive(Clone, Debug)]
pub struct OperatorDelegation {
pub operator: Address,
pub campaign_id: u64,
pub budget_total: u64,
pub budget_used: u64,
pub granted_at: u32,
pub revoked: bool,
}
// ── Issue #896: Multi-asset redemption types ──────────────────────────────────
/// Per-asset redemption configuration.
#[contracttype]
#[derive(Clone, Debug)]
pub struct RedemptionAssetConfig {
pub asset_address: Address,
pub rate_bps: u64, // Points per asset unit (basis points)
pub reserve_balance: i128,
pub enabled: bool,
}
// ── Issue #899: Claim cooldown types ──────────────────────────────────────────
/// Per-campaign claim cooldown configuration.
#[contracttype]
#[derive(Clone, Debug)]
pub struct ClaimCooldown {
pub cooldown_ledgers: u32, // Minimum ledgers between claims
pub enabled: bool,
}
/// Vesting schedule record stored per user per vest_id.
#[contracttype]
#[derive(Clone, Debug)]
pub struct VestingRecord {
pub total: u64,
pub start_ledger: u32,
pub end_ledger: u32,
pub claimed: u64,
}
// ── Staking types ──────────────────────────────────────────────────────────
/// Distribution mode for campaign rewards (issue #871).
#[contracttype]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum DistributionMode {
/// Linear: rewards proportional to actions (default)
Linear = 0,
/// Quadratic: rewards = isqrt(actions) to reduce whale dominance
Quadratic = 1,
}
/// Individual staking position for a user.
#[contracttype]
#[derive(Clone, Debug)]
pub struct StakingPosition {
/// Amount of points staked
pub amount: u64,
/// Ledger when position was created (stake timestamp)
pub staked_at: u32,
/// Ledger when position unlocks (0 = no lock)
pub unlocks_at: u32,
/// Applied boost multiplier in basis points (e.g., 11000 = 1.1x)
pub boost_multiplier_bps: u32,
/// Amount already claimed from this position
pub claimed: u64,
}
/// Lock schedule configuration defining duration options and their boosts.
#[contracttype]
#[derive(Clone, Debug)]
pub struct LockSchedule {
/// Lock duration in ledgers (e.g., 17280 = ~1 day at 5s/ledger)
pub duration_ledgers: u32,
/// Boost multiplier in basis points (e.g., 11000 = 1.1x)
pub boost_multiplier_bps: u32,
}
/// Boost curve configuration for calculating boost based on lock duration.
#[contracttype]
#[derive(Clone, Debug)]
pub struct BoostCurve {
/// Base boost multiplier for minimum lock (basis points)
pub base_multiplier_bps: u32,
/// Maximum boost multiplier (basis points)
pub max_multiplier_bps: u32,
/// Ledger duration for maximum boost
pub max_duration_ledgers: u32,
/// Curve type (0 = linear, 1 = logarithmic, 2 = exponential)
pub curve_type: u8,
}
// ── Multi-sig types (issue #733) ─────────────────────────────────────────────
/// Multi-sig configuration: M-of-N threshold over a signer set.
#[contracttype]
#[derive(Clone, Debug)]
pub struct MultiSigConfig {
/// Minimum number of approvals required to execute a privileged operation.
pub threshold: u32,
/// Ordered list of authorized signers.
pub signers: Vec<Address>,
}
/// An in-flight privileged operation proposal waiting for threshold approvals.
#[contracttype]
#[derive(Clone, Debug)]
pub struct PrivilegedProposal {
/// Unique proposal identifier.
pub proposal_id: u64,
/// Symbolic op code (e.g. `withdraw_reserve`, `upgrade`, `set_rate`).
pub op: Symbol,
/// Serialised op arguments (application-defined payload).
pub payload: Vec<Symbol>,
/// Ledger after which this proposal expires.
pub expires_at_ledger: u32,
/// Set of signers that have approved so far.
pub approvals: Vec<Address>,
}
// ── Governance types (issue #735) ─────────────────────────────────────────────
/// An in-flight on-chain governance proposal for a single parameter change.
#[contracttype]
#[derive(Clone, Debug)]
pub struct ParamProposal {
/// Unique proposal identifier.
pub proposal_id: u64,
/// Storage key for the parameter being changed.
pub param_key: Symbol,
/// New value encoded as a 64-bit word (caller's encoding convention).
pub new_value: u64,
/// Ledger at or after which the proposal may be executed.
pub execute_after_ledger: u32,
/// Ledger after which the proposal expires without execution.
pub expires_at_ledger: u32,
/// Set of addresses that voted in favour.
pub votes_for: Vec<Address>,
/// Quorum required for execution (number of approving votes).
pub quorum: u32,
/// Whether the proposal has been executed.
pub executed: bool,
}
contractmeta!(
key = "Description",
val = "Trivela campaign rewards and points"
);
// ── Instance-storage TTL (issue #279) ────────────────────────────────────────
//
// `extend_ttl(threshold, extend_to)` is called on every state-mutating entry
// point. On mainnet each ledger closes in ~5 seconds, so the prior
// `extend_ttl(50, 100)` literals expired instance storage roughly 8 minutes
// after the last mutation, which would erase admin, balances, and metadata
// in production.
//
// Mainnet defaults aim for the contract to remain live for ~30 days after the
// most recent write, with extension triggered well before that window closes:
// - `TTL_THRESHOLD` ≈ 100,000 ledgers (~6 days minimum life remaining)
// - `TTL_EXTEND_TO` ≈ 518,400 ledgers (~30 days target lifetime)
//
// Tests use a `cfg(test)` override so suites don't spend the full ledger
// budget on TTL bookkeeping. See `docs/TTL_STRATEGY.md` for the full rationale.
#[cfg(not(test))]
pub const TTL_THRESHOLD: u32 = 100_000;
#[cfg(not(test))]
pub const TTL_EXTEND_TO: u32 = 518_400;
#[cfg(test)]
pub const TTL_THRESHOLD: u32 = 50;
#[cfg(test)]
pub const TTL_EXTEND_TO: u32 = 100;
const ADMIN: Symbol = symbol_short!("admin");
const BALANCE: Symbol = symbol_short!("balance");
// Total outstanding points in circulation — incremented by credit, decremented
// by claim and redeem. Conservation invariant: total_supply = Σ user balances
// (issue #1021).
const TOTAL_SUPPLY: Symbol = symbol_short!("tsupply");
const CLAIMED: Symbol = symbol_short!("claimed");
const METADATA: Symbol = symbol_short!("metadata");
const PAUSED: Symbol = symbol_short!("paused");
// Per-function pause flags (#1019)
const PAUSE_CREDIT: Symbol = symbol_short!("pscredit");
const PAUSE_CLAIM: Symbol = symbol_short!("psclaim");
const PAUSE_REDEEM: Symbol = symbol_short!("psredeem");
const CREDIT_EVENT: Symbol = symbol_short!("credit");
const CLAIM_EVENT: Symbol = symbol_short!("claim");
const TRANSFER_EVENT: Symbol = symbol_short!("transfer");
const PAUSED_EVENT: Symbol = symbol_short!("paused");
// Per-function pause events (#1019)
const PAUSE_CREDIT_EVENT: Symbol = symbol_short!("pscredit");
const PAUSE_CLAIM_EVENT: Symbol = symbol_short!("psclaim");
const PAUSE_REDEEM_EVENT: Symbol = symbol_short!("psredeem");
const MAX_CREDIT_EVENT: Symbol = symbol_short!("mxcredit");
const CAMPAIGN_MULTIPLIER_EVENT: Symbol = symbol_short!("multset");
const MAX_CREDIT_PER_CALL: Symbol = symbol_short!("mxcredit");
/// Minimum claim amount (issue #321). 0 means no minimum.
const MIN_CLAIM: Symbol = symbol_short!("min_clm");
const MIN_CLAIM_EVENT: Symbol = symbol_short!("minclmst");
const SCHEMA_VERSION: Symbol = symbol_short!("schema_v");
const CURRENT_SCHEMA_VERSION: u32 = 1;
const CAMPAIGN_MULTIPLIER: Symbol = symbol_short!("mult");
const TIERS: Symbol = symbol_short!("tiers");
const BPS_DENOMINATOR: u128 = 10_000;
const PRUNED_EVENT: Symbol = symbol_short!("pruned");
// ── multisig nonce storage (#451 / #454) ────────────────────────────────────
const NONCE_USED: Symbol = symbol_short!("msnonce");
const NONCE_REGISTRY: Symbol = symbol_short!("nreg");
const NONCE_CURSOR: Symbol = symbol_short!("ncursor");
/// Multisig nonces older than this many ledgers are eligible for pruning.
const NONCE_TTL_LEDGERS: u32 = 10_000;
// ── co-admin multisig (#454) ────────────────────────────────────────────────
const CO_ADMINS: Symbol = symbol_short!("coadmin");
const MULTISIG_THRESHOLD: Symbol = symbol_short!("msthresh");
const OP_SET_PAUSED: u32 = 1;
// Rate limiting constants (issue #324)
const RATE_LIM_MAX: Symbol = symbol_short!("ratlmax");
const RATE_LIM_WIN: Symbol = symbol_short!("ratlwin");
const RATE: Symbol = symbol_short!("rate");
const RATE_LIM_SET_EVENT: Symbol = symbol_short!("ratlset");
// Snapshot constants (issue #325)
const SNAPSHOT: Symbol = symbol_short!("snap");
const SNAP_LIST: Symbol = symbol_short!("snaplist");
const SNAPSHOT_EVENT: Symbol = symbol_short!("snapshot");
// Vesting constants (issue #326)
const VEST: Symbol = symbol_short!("vest");
const VEST_CTR: Symbol = symbol_short!("vestctr");
const VEST_IDS: Symbol = symbol_short!("vestids");
const VESTED_CREDIT_EVENT: Symbol = symbol_short!("vcredit");
const VESTED_CLAIM_EVENT: Symbol = symbol_short!("vclaim");
// Redemption constants (issue #450)
const REDEMPTION_ASSET: Symbol = symbol_short!("red_asst");
const REDEMPTION_RATE: Symbol = symbol_short!("red_rate");
const REDEMPTION_RESERVE: Symbol = symbol_short!("red_rsrv");
const REDEEM_EVENT: Symbol = symbol_short!("redeem");
// Admin nonce — incremented on each admin operation to prevent replay attacks.
const ADMIN_NONCE: Symbol = symbol_short!("anonce");
// ── 2-step admin transfer (issue #281) ───────────────────────────────────────
// `PENDING_ADMIN` holds an in-flight proposed admin; the new admin must call
// `accept_admin()` themselves to complete the rotation, eliminating the
// "wrong address, key now lost" failure mode of a one-step transfer.
const PENDING_ADMIN: Symbol = symbol_short!("padmin");
const ADMIN_PROPOSED_EVENT: Symbol = symbol_short!("aproposed");
const ADMIN_ACCEPTED_EVENT: Symbol = symbol_short!("aaccepted");
// ── On-chain referral rewards (issue #656 / #603) ────────────────────────────
// The referral *graph* (who referred whom) is attributed by the campaign
// contract; this contract owns the *payout* and its anti-abuse invariants:
// self/circular blocking, one-bonus-per-referee uniqueness (the sybil gate),
// and a configurable per-referrer cap. Referral state lives in instance storage
// alongside balances, matching the existing crediting model.
const REF_RATE: Symbol = symbol_short!("refrate"); // u32 bonus rate, basis points
const REF_CAP: Symbol = symbol_short!("refcap"); // u64 cumulative cap per referrer (0 = uncapped)
const REF_PAID: Symbol = symbol_short!("refpaid"); // (REF_PAID, referee) -> referrer Address
const REF_TOTAL: Symbol = symbol_short!("reftotal"); // (REF_TOTAL, referrer) -> u64 cumulative bonus
const REF_COUNT: Symbol = symbol_short!("refcount"); // (REF_COUNT, referrer) -> u64 referrals rewarded
const REF_CONFIG_EVENT: Symbol = symbol_short!("refcfg");
const REF_BONUS_EVENT: Symbol = symbol_short!("refbonus");
// Upper bound on the configurable rate (1000%) to guard against fat-finger
// configuration and keep `qualifying_amount * rate_bps` comfortably in range.
const MAX_REFERRAL_RATE_BPS: u32 = 100_000;
// ── Multi-level referral tree (issue #898) ───────────────────────────────────
/// Maximum referral tree depth (prevents unbounded loops and gas attacks).
const MAX_REFERRAL_TREE_DEPTH: u32 = 10;
/// Configured referral tree depth: (REF_DEPTH) -> u32
const REF_DEPTH: Symbol = symbol_short!("refdepth");
/// Per-level rate configuration: (REF_TIER_RATE, level: u32) -> rate_bps: u32
const REF_TIER_RATE: Symbol = symbol_short!("reftrate");
/// Multi-level referral reward event: topics (ref_mlvl, referrer, referee, level), data (bonus, qualifying_amount)
const REF_MULTILEVEL_EVENT: Symbol = symbol_short!("refmlvl");
/// Referral chain storage (mirrored from campaign contract): (REFERRAL, referee) -> referrer
const REFERRAL: Symbol = symbol_short!("referral");
// ── Campaign supply cap (issue #903) ─────────────────────────────────────────
/// Per-campaign total supply cap: (CAMPAIGN_CAP, campaign_id) -> u64 (0 = uncapped)
const CAMPAIGN_CAP: Symbol = symbol_short!("campcap");
/// Per-campaign issued total: (CAMPAIGN_ISSUED, campaign_id) -> u64
const CAMPAIGN_ISSUED: Symbol = symbol_short!("campiss");
/// Campaign supply cap set event: topics (campcap, campaign_id), data (cap: u64)
const CAMPAIGN_CAP_EVENT: Symbol = symbol_short!("campcap");
// ── Issue #895: Operator delegation constants ────────────────────────────────
/// Operator delegation storage: (OP_DELEGATION, operator, campaign_id) -> OperatorDelegation
const OP_DELEGATION: Symbol = symbol_short!("opdlgt");
/// Operator registry for enumeration: (OP_REGISTRY, campaign_id) -> Vec<Address>
const OP_REGISTRY: Symbol = symbol_short!("opreg");
/// Grant operator event: topics (op_grant, operator, campaign_id), data (budget: u64)
const OP_GRANT_EVENT: Symbol = symbol_short!("opgrant");
/// Revoke operator event: topics (op_revoke, operator, campaign_id), data ()
const OP_REVOKE_EVENT: Symbol = symbol_short!("oprevoke");
/// Operator credit event: topics (op_credit, operator, user), data (amount: u64)
const OP_CREDIT_EVENT: Symbol = symbol_short!("opcredit");
// ── Issue #896: Multi-asset redemption constants ──────────────────────────────
/// List of redemption asset addresses: REDEMPTION_ASSETS -> Vec<Address>
const REDEMPTION_ASSETS: Symbol = symbol_short!("rd_assts");
/// Per-asset config: (ASSET_CONFIG, Address) -> RedemptionAssetConfig
const ASSET_CONFIG: Symbol = symbol_short!("ast_cfg");
/// Asset added event: topics (ast_add,), data (asset: Address, rate: u64)
const ASSET_ADD_EVENT: Symbol = symbol_short!("ast_add");
/// Asset updated event: topics (ast_upd,), data (asset: Address, rate: u64)
const ASSET_UPDATE_EVENT: Symbol = symbol_short!("ast_upd");
/// Asset removed event: topics (ast_rem,), data (asset: Address)
const ASSET_REMOVE_EVENT: Symbol = symbol_short!("ast_rem");
/// Multi-asset redeem event: topics (redeem_ma, user, asset), data (points: u64, amount: i128)
const REDEEM_MULTIASSET_EVENT: Symbol = symbol_short!("rd_ma");
// ── Issue #899: Claim cooldown constants ──────────────────────────────────────
/// Cooldown config: (CLAIM_COOLDOWN, campaign_id) -> ClaimCooldown
const CLAIM_COOLDOWN: Symbol = symbol_short!("clm_cool");
/// Last claim ledger: (LAST_CLAIM, user, campaign_id) -> u32
const LAST_CLAIM: Symbol = symbol_short!("lst_clm");
/// Cooldown set event: topics (cool_set, campaign_id), data (ledgers: u32)
const COOLDOWN_SET_EVENT: Symbol = symbol_short!("cool_set");
// ── Multi-sig constants (issue #733) ─────────────────────────────────────────
const MULTISIG_CFG: Symbol = symbol_short!("mscfg");
const MULTISIG_PROP: Symbol = symbol_short!("msprop");
const MULTISIG_CTR: Symbol = symbol_short!("msctr");
const PRIV_PROP_EVENT: Symbol = symbol_short!("privprop");
const PRIV_APPR_EVENT: Symbol = symbol_short!("privappr");
const PRIV_EXEC_EVENT: Symbol = symbol_short!("privexec");
// ── Governance constants (issue #735) ─────────────────────────────────────────
const GOV_PROP: Symbol = symbol_short!("govprop");
const GOV_CTR: Symbol = symbol_short!("govctr");
const GOV_PROPOSE_EVENT: Symbol = symbol_short!("govprp");
const GOV_VOTE_EVENT: Symbol = symbol_short!("govvote");
const GOV_EXECUTE_EVENT: Symbol = symbol_short!("govexec");
const GOV_CANCEL_EVENT: Symbol = symbol_short!("govcanc");
// ── Emergency timelock constants (issue #838) ────────────────────────────────
/// Persistent map key prefix for queued timelock entries, keyed by
/// `(TIMELOCK_ENTRY, op_hash)` -> `eta_ledger: u32`.
const TIMELOCK_ENTRY: Symbol = symbol_short!("tlentry");
/// Instance key for the admin-configurable minimum delay, in ledgers,
/// between queuing and executing a timelocked op. Defaults to
/// `DEFAULT_TIMELOCK_DELAY` if never configured.
const TIMELOCK_DELAY: Symbol = symbol_short!("tldelay");
const TIMELOCK_QUEUE_EVENT: Symbol = symbol_short!("tlqueue");
const TIMELOCK_EXEC_EVENT: Symbol = symbol_short!("tlexec");
const TIMELOCK_CANCEL_EVENT: Symbol = symbol_short!("tlcanc");
/// Fallback delay (in ledgers, ~5s each) when no delay has been configured —
/// roughly 24 hours.
const DEFAULT_TIMELOCK_DELAY: u32 = 17_280;
// ── Staking constants ──────────────────────────────────────────────────────
/// Individual staking position key: (STAKE, user, stake_id) -> StakingPosition
const STAKE: Symbol = symbol_short!("stake");
/// Staking position counter key: (STAKE_CTR, user) -> u64
const STAKE_CTR: Symbol = symbol_short!("stakectr");
/// Staking position IDs key: (STAKE_IDS, user) -> Vec<u64>
const STAKE_IDS: Symbol = symbol_short!("stakeids");
/// Active lock schedules key: LOCK_SCHEDULES -> Vec<LockSchedule>
const LOCK_SCHEDULES: Symbol = symbol_short!("locksched");
/// Boost curve configuration key: BOOST_CURVE -> BoostCurve
const BOOST_CURVE: Symbol = symbol_short!("boostcrv");
/// Minimum stake amount key: MIN_STAKE -> u64
const MIN_STAKE: Symbol = symbol_short!("minstake");
/// Staking paused flag key: PAUSE_STAKE -> bool
const PAUSE_STAKE: Symbol = symbol_short!("psstake");
/// Stake event: topics (STAKE_EVENT, user), data (stake_id: u64, amount: u64, unlocks_at: u32, boost_multiplier_bps: u32)
const STAKE_EVENT: Symbol = symbol_short!("stake");
/// Unstake event: topics (UNSTAKE_EVENT, user), data (stake_id: u64, amount: u64, claimed: u64)
const UNSTAKE_EVENT: Symbol = symbol_short!("unstake");
/// Boost update event: topics (BOOST_UPDATE_EVENT, user), data (stake_id: u64, new_boost_bps: u32)
const BOOST_UPDATE_EVENT: Symbol = symbol_short!("boostupd");
/// Lock schedule update event: topics (LOCK_SCHEDULE_EVENT,), data ()
const LOCK_SCHEDULE_EVENT: Symbol = symbol_short!("locksched");
/// Boost curve update event: topics (BOOST_CURVE_EVENT,), data ()
const BOOST_CURVE_EVENT: Symbol = symbol_short!("boostcrve");
// ── SEP-41 Token Interface (issue #530) ─────────────────────────────────────
// Optional token-backed mode where reward points are SEP-41-compliant tokens.
// When token_mode is enabled, the contract exposes standard token functions.
const TOKEN_MODE: Symbol = symbol_short!("tokmode");
const TOKEN_DECIMALS: Symbol = symbol_short!("tokdec");
const TOKEN_NAME: Symbol = symbol_short!("tokname");
const TOKEN_SYMBOL: Symbol = symbol_short!("toksym");
const ALLOWANCE: Symbol = symbol_short!("allow");
// SEP-41 Events
const SEP41_TRANSFER_EVENT: Symbol = symbol_short!("transfer");
const SEP41_APPROVE_EVENT: Symbol = symbol_short!("approve");
const SEP41_BURN_EVENT: Symbol = symbol_short!("burn");
// ── Timelocked clawback (issue #729) ─────────────────────────────────────────
//
// A clawback proposal reserves `amount` from a target's unclaimed balance
// and queues an admin-initiated credit removal. The guardian (admin) may
// cancel within the timelock window. After the delay elapses, anyone can
// execute. Only unclaimed points may be clawed back (issued but not yet
// redeemed), so the credit→balance ledger conservation invariant holds.
//
// Storage layout:
// (CLAWBACK_PROPOSAL, u32) -> ClawbackProposal (persistent)
// CLAWBACK_NONCE -> u32 (instance — monotonic counter)
const CLAWBACK_PROPOSAL: Symbol = symbol_short!("clwbprop");
const CLAWBACK_NONCE: Symbol = symbol_short!("clwbnonce");
/// Minimum ledgers that must pass before a clawback can be executed.
/// At ~5 s/ledger this is roughly 7 days on mainnet.
const CLAWBACK_TIMELOCK_LEDGERS: u32 = 120_960;
const CLAWBACK_PROPOSE_EVENT: Symbol = symbol_short!("clwbprop");
const CLAWBACK_CANCEL_EVENT: Symbol = symbol_short!("clwbcanc");
const CLAWBACK_EXECUTE_EVENT: Symbol = symbol_short!("clwbexec");
// ── Merkle Airdrop (issue #845) ──────────────────────────────────────────────
// ZK airdrop claims from a Merkle-committed allowlist of (secret, amount) pairs.
// Users prove membership without revealing their position in the tree.
// Nullifiers prevent double-claiming while maintaining privacy.
const AIRDROP_ROOT: Symbol = symbol_short!("airdrop");
const AIRDROP_NULLIFIERS: Symbol = symbol_short!("nulli");
const AIRDROP_CLAIMED_EVENT: Symbol = symbol_short!("airreclm");
// ── Distribution mode constants (issue #871) ──────────────────────────────────
// Quadratic/anti-whale distribution mode for campaigns.
// Storage key: (DIST_MODE, campaign_id) -> u8 (DistributionMode enum)
const DIST_MODE: Symbol = symbol_short!("distmode");
const DIST_MODE_SET_EVENT: Symbol = symbol_short!("distmset");
/// Proposal record stored under `(CLAWBACK_PROPOSAL, id)`.
#[contracttype]
#[derive(Clone, Debug)]
pub struct ClawbackProposal {
pub target: Address,
pub amount: u64,
/// Ledger sequence number when the proposal was created.
pub proposed_at: u32,
/// True once cancelled so stale proposals don't appear as pending.
pub cancelled: bool,
/// True once executed so replay is impossible.
pub executed: bool,
}
#[contract]
pub struct RewardsContract;
fn require_admin(env: &Env, admin: &Address) -> Result<(), Error> {
admin.require_auth();
let stored_admin: Address = env
.storage()
.instance()
.get(&ADMIN)
.ok_or(Error::Unauthorized)?;
if &stored_admin != admin {
return Err(Error::Unauthorized);
}
Ok(())
}
fn require_admin_with_nonce(env: &Env, admin: &Address, nonce: i128) -> Result<(), Error> {
admin.require_auth();
let stored_admin: Address = env
.storage()
.instance()
.get(&ADMIN)
.ok_or(Error::Unauthorized)?;
if &stored_admin != admin {
return Err(Error::Unauthorized);
}
let current: i128 = env.storage().instance().get(&ADMIN_NONCE).unwrap_or(0);
if nonce != current {
return Err(Error::InvalidAdminNonce);
}
env.storage().instance().set(&ADMIN_NONCE, &(current + 1));
Ok(())
}
fn ensure_not_paused(env: &Env) -> Result<(), Error> {
let paused: bool = env.storage().instance().get(&PAUSED).unwrap_or(false);
if paused {
return Err(Error::ContractPaused);
}
Ok(())
}
fn ensure_credit_not_paused(env: &Env) -> Result<(), Error> {
ensure_not_paused(env)?;
let paused: bool = env.storage().instance().get(&PAUSE_CREDIT).unwrap_or(false);
if paused {
return Err(Error::ContractPaused);
}
Ok(())
}
fn ensure_claim_not_paused(env: &Env) -> Result<(), Error> {
ensure_not_paused(env)?;
let paused: bool = env.storage().instance().get(&PAUSE_CLAIM).unwrap_or(false);
if paused {
return Err(Error::ContractPaused);
}
Ok(())
}
fn ensure_redeem_not_paused(env: &Env) -> Result<(), Error> {
ensure_not_paused(env)?;
let paused: bool = env.storage().instance().get(&PAUSE_REDEEM).unwrap_or(false);
if paused {
return Err(Error::ContractPaused);
}
Ok(())
}
fn ensure_stake_not_paused(env: &Env) -> Result<(), Error> {
ensure_not_paused(env)?;
let paused: bool = env.storage().instance().get(&PAUSE_STAKE).unwrap_or(false);
if paused {
return Err(Error::ContractPaused);
}
Ok(())
}
/// Check caller's rate limit and increment their count for the current window.
/// `n_calls` is how many calls to count (1 for credit, N for batch_credit).
fn check_and_increment_rate(env: &Env, caller: &Address, n_calls: u32) -> Result<(), Error> {
let max_calls: u32 = env.storage().instance().get(&RATE_LIM_MAX).unwrap_or(0);
if max_calls == 0 {
return Ok(());
}
let window_ledgers: u32 = env.storage().instance().get(&RATE_LIM_WIN).unwrap_or(1);
let current_ledger = env.ledger().sequence();
let window_start = current_ledger.checked_div(window_ledgers).unwrap_or(0);
let rate_key = (RATE, caller.clone(), window_start);
let count: u32 = env.storage().instance().get(&rate_key).unwrap_or(0);
if count.saturating_add(n_calls) > max_calls {
return Err(Error::RateLimitExceeded);
}
env.storage().instance().set(&rate_key, &(count + n_calls));
Ok(())
}
/// Compute unlocked amount for a vesting record at `now` (current ledger sequence).
fn compute_unlocked(now: u32, record: &VestingRecord) -> u64 {
if now <= record.start_ledger {
return 0;
}
if now >= record.end_ledger {
return record.total;
}
let elapsed = (now - record.start_ledger) as u128;
let duration = (record.end_ledger - record.start_ledger) as u128;
let total = record.total as u128;
let unlocked = total * elapsed / duration;
(unlocked.min(record.total as u128)) as u64
}
/// Calculate boost multiplier for a given lock duration using configured boost curve.
/// Returns boost multiplier in basis points (e.g., 11000 = 1.1x).
fn calculate_boost_multiplier(env: &Env, duration_ledgers: u32) -> Result<u32, Error> {
let curve: Option<BoostCurve> = env.storage().instance().get(&BOOST_CURVE);
// If no boost curve configured, return 1.0x (10000 bps)
let curve = match curve {
Some(c) => c,
None => return Ok(10_000), // Default 1.0x multiplier
};
// Validate curve configuration
if curve.base_multiplier_bps == 0 || curve.max_multiplier_bps == 0 {
return Err(Error::InvalidBoostCurve);
}
if curve.max_duration_ledgers == 0 {
return Err(Error::InvalidBoostCurve);
}
if curve.base_multiplier_bps > curve.max_multiplier_bps {
return Err(Error::InvalidBoostCurve);
}
// Cap duration at maximum
let duration = duration_ledgers.min(curve.max_duration_ledgers);
match curve.curve_type {
// Linear interpolation: boost = base + (max - base) * (duration / max_duration)
0 => {
let base = curve.base_multiplier_bps as u128;
let max = curve.max_multiplier_bps as u128;
let duration_ratio = (duration as u128 * BPS_DENOMINATOR) / (curve.max_duration_ledgers as u128);
let boost_increase = (max - base) * duration_ratio / BPS_DENOMINATOR;
let result = base + boost_increase;
if result > u32::MAX as u128 {
return Err(Error::Overflow);
}
Ok(result as u32)
}
// Logarithmic: boost = base + (max - base) * log2(1 + duration/max_duration) / log2(2)
1 => {
// Simplified logarithmic scaling for no_std environment
let base = curve.base_multiplier_bps as u128;
let max = curve.max_multiplier_bps as u128;
let duration_ratio = (duration as u128 * BPS_DENOMINATOR) / (curve.max_duration_ledgers as u128);
// Approximate log2(1 + x) using fixed-point math
// For small x: log2(1 + x) ≈ x * 28963 / 2^16 (Pade approximation)
let log_approx = duration_ratio.saturating_mul(28963) / 65536;
let boost_increase = (max - base) * log_approx / BPS_DENOMINATOR;
let result = base + boost_increase;
if result > u32::MAX as u128 {
return Err(Error::Overflow);
}
Ok(result as u32)
}
// Exponential: boost = base * (max/base)^(duration/max_duration)
2 => {
let base = curve.base_multiplier_bps as u128;
let max = curve.max_multiplier_bps as u128;
let duration_ratio = (duration as u128 * BPS_DENOMINATOR) / (curve.max_duration_ledgers as u128);
// Calculate growth rate (r = (max/base) - 1)
let growth_rate_bps = max.saturating_mul(BPS_DENOMINATOR)
.checked_div(base)
.ok_or(Error::Overflow)?
.saturating_sub(BPS_DENOMINATOR);
// Use binomial approximation for small exponents: (1 + r)^x ≈ 1 + r*x + r²*x*(x-1)/2
// Convert to fixed-point arithmetic
let x = duration_ratio; // x in basis points (0 to 10,000)
// First term: 1 + r*x
let term1 = BPS_DENOMINATOR + (growth_rate_bps * x) / BPS_DENOMINATOR;
// Second term: r²*x*(x-1)/2 (more accurate for larger x)
let x_minus_one = if x > 0 { x - 1 } else { 0 };
let r_squared = (growth_rate_bps * growth_rate_bps) / BPS_DENOMINATOR;
let term2_numerator = r_squared * x * x_minus_one;
let term2 = term2_numerator / (2 * BPS_DENOMINATOR * BPS_DENOMINATOR);
let boost_factor = term1 + term2;
let result = (base * boost_factor) / BPS_DENOMINATOR;
if result > u32::MAX as u128 {
return Err(Error::Overflow);
}
// Ensure result is within bounds
let clamped_result = result.min(max as u128).max(base as u128);
Ok(clamped_result as u32)
}
// Step function: boost increases in discrete steps at specific duration thresholds
3 => {
// For step functions, we need predefined schedules
// Fall back to linear if no schedules defined
let schedules: Option<Vec<LockSchedule>> = env.storage().instance().get(&LOCK_SCHEDULES);
match schedules {
Some(schedules) => {
// Find the highest boost for durations <= requested duration
let mut best_boost = curve.base_multiplier_bps;
for schedule in schedules.iter() {
if schedule.duration_ledgers <= duration_ledgers {
if schedule.boost_multiplier_bps > best_boost {
best_boost = schedule.boost_multiplier_bps;
}
}
}
Ok(best_boost)
}
None => {
// No schedules defined, fall back to linear
let base = curve.base_multiplier_bps as u128;
let max = curve.max_multiplier_bps as u128;
let duration_ratio = (duration as u128 * BPS_DENOMINATOR) / (curve.max_duration_ledgers as u128);
let boost_increase = (max - base) * duration_ratio / BPS_DENOMINATOR;
let result = base + boost_increase;
if result > u32::MAX as u128 {
return Err(Error::Overflow);
}
Ok(result as u32)
}
}
}
_ => Err(Error::InvalidBoostCurve),
}
}
/// Get boost multiplier for a specific lock schedule.
/// Returns boost multiplier in basis points or default 1.0x if schedule not found.
fn get_lock_schedule_boost(env: &Env, duration_ledgers: u32) -> Result<u32, Error> {
let schedules: Option<Vec<LockSchedule>> = env.storage().instance().get(&LOCK_SCHEDULES);
match schedules {
Some(schedules) => {
// Find matching schedule
for schedule in schedules.iter() {
if schedule.duration_ledgers == duration_ledgers {
if schedule.boost_multiplier_bps == 0 {
return Err(Error::ZeroBoostMultiplier);
}
return Ok(schedule.boost_multiplier_bps);
}
}
// No matching schedule, use boost curve
calculate_boost_multiplier(env, duration_ledgers)
}
None => {
// No schedules configured, use boost curve
calculate_boost_multiplier(env, duration_ledgers)
}
}
}
/// Build the signed payload for a multisig operation: `sha256(op || nonce || args_hash)`.
/// `op` is a stable per-function discriminant used in place of the function
/// name string (Symbol byte access is not available in `no_std`).
fn multisig_message(env: &Env, op: u32, nonce: u64, args_hash: &BytesN<32>) -> Bytes {
let mut buf = [0u8; 44];
buf[0..4].copy_from_slice(&op.to_be_bytes());
buf[4..12].copy_from_slice(&nonce.to_be_bytes());
buf[12..44].copy_from_slice(&args_hash.to_array());
Bytes::from_slice(env, &buf)
}
/// Verify at least `required` distinct co-admin signatures over
/// `(op, nonce, args_hash)`, then consume `nonce` for replay protection.
/// The nonce is consumed regardless of how many signers submitted.
fn verify_multisig(
env: &Env,
op: u32,
args_hash: BytesN<32>,
nonce: u64,
signatures: &Vec<(Address, BytesN<64>)>,
) -> Result<(), Error> {
let required: u32 = env
.storage()
.instance()
.get(&MULTISIG_THRESHOLD)
.unwrap_or(0);
if required == 0 {
return Ok(());
}
let nonce_key = (NONCE_USED, nonce);
if env.storage().instance().get::<_, u32>(&nonce_key).is_some() {
return Err(Error::NonceReused);
}
let co_admins: Vec<(Address, BytesN<32>)> = env
.storage()
.instance()
.get(&CO_ADMINS)
.unwrap_or(Vec::new(env));
let message = multisig_message(env, op, nonce, &args_hash);
let mut seen: Vec<Address> = Vec::new(env);
for (signer, sig) in signatures.iter() {
if seen.iter().any(|s| s == signer) {
return Err(Error::DuplicateSigner);
}
let pubkey = co_admins
.iter()
.find_map(|(addr, key)| if addr == signer { Some(key) } else { None })
.ok_or(Error::UnknownSigner)?;
env.crypto().ed25519_verify(&pubkey, &message, &sig);
seen.push_back(signer.clone());
}
if seen.len() < required {
return Err(Error::InsufficientSignatures);
}
env.storage()
.instance()
.set(&nonce_key, &env.ledger().sequence());
let mut registry: Vec<u64> = env
.storage()
.instance()
.get(&NONCE_REGISTRY)
.unwrap_or(Vec::new(env));
registry.push_back(nonce);
env.storage().instance().set(&NONCE_REGISTRY, ®istry);
Ok(())
}
/// Integer square root for quadratic distribution (issue #871).
/// Uses Newton's method for efficient computation in no_std environments.
fn isqrt(n: u64) -> u64 {
if n == 0 {
return 0;
}