templar_common/
vault.rs

1use std::{collections::BTreeSet, num::NonZeroU8};
2
3use near_sdk::{
4    env,
5    json_types::{U128, U64},
6    near, require, AccountId, AccountIdRef, Gas, Promise, PromiseOrValue,
7};
8
9use crate::{
10    asset::{BorrowAsset, FungibleAsset},
11    supply::SupplyPosition,
12};
13
14pub type TimestampNs = u64;
15
16pub const MIN_TIMELOCK_NS: u64 = 0;
17pub const MAX_TIMELOCK_NS: u64 = 30 * 86_400_000_000_000; // 30 days
18pub const MAX_QUEUE_LEN: usize = 64;
19
20pub type ExpectedIdx = u32;
21pub type ActualIdx = u32;
22pub type AllocationWeights = Vec<(AccountId, U128)>;
23pub type AllocationPlan = Vec<(AccountId, u128)>;
24
25/// Parsed from the string parameter `msg` passed by `*_transfer_call` to
26/// `*_on_transfer` calls.
27#[near(serializers = [json])]
28pub enum DepositMsg {
29    /// Add the attached tokens to the sender's vault position.
30    Supply,
31}
32
33/// Concrete configuration for a market.
34#[derive(Clone, Default, Debug)]
35#[near]
36pub struct MarketConfiguration {
37    /// Supply cap for this market (in underlying asset units)
38    pub cap: U128,
39    /// Whether market is enabled for deposits/withdrawals
40    pub enabled: bool,
41    /// Timestamp (ns) after which market can be removed (if pending removal)
42    pub removable_at: TimestampNs,
43}
44
45/// Configuration for the setup of a metavault.
46#[derive(Clone)]
47#[near(serializers = [json, borsh])]
48pub struct VaultConfiguration {
49    /// The account that owns this vault.
50    pub owner: AccountId,
51    /// The account that can submit allocation plans. See [AllocationMode].
52    pub curator: AccountId,
53    /// The account that can set guardianship. See [AllocationMode].
54    pub guardian: AccountId,
55    /// The underlying asset for this vault.
56    pub underlying_token: FungibleAsset<BorrowAsset>,
57    /// The initial timelock for this vault used for modifying the configuration.
58    pub initial_timelock_ns: U64,
59    /// The account that receives fees for this vault.
60    pub fee_recipient: AccountId,
61    /// The skim account that can unorphan any assets erroneously sent to this vault.
62    pub skim_recipient: AccountId,
63    /// The name of the share token.
64    pub name: String,
65    /// The symbol of the share token.
66    pub symbol: String,
67    /// The number of decimals for the share token, usually would be the same as the underlying asset.
68    pub decimals: NonZeroU8,
69    /// Restrictions for this market.
70    pub restrictions: Option<Restrictions>,
71}
72
73/// Restrictions that can be applied to the vault.
74///
75/// It should cover both Whitelist style functionality and Blacklist style functionality.
76/// It should also enable Pausing
77#[near(serializers = [borsh, json])]
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum Restrictions {
80    Paused,
81    BlackList(BTreeSet<AccountId>),
82    WhiteList(BTreeSet<AccountId>),
83}
84
85impl Restrictions {
86    /// Check if the account is restricted, and if so, what is the reason
87    pub fn is_restricted(&self, account_id: &AccountIdRef) -> Option<Restrictions> {
88        match self {
89            Restrictions::Paused => Some(Restrictions::Paused),
90            Restrictions::BlackList(blacklist) => {
91                if blacklist.contains(account_id) {
92                    Some(Restrictions::BlackList(blacklist.clone()))
93                } else {
94                    None
95                }
96            }
97            Restrictions::WhiteList(whitelist) => {
98                if whitelist.contains(account_id) || account_id == env::current_account_id() {
99                    None
100                } else {
101                    Some(Restrictions::WhiteList(whitelist.clone()))
102                }
103            }
104        }
105    }
106}
107
108#[near_sdk::ext_contract(ext_vault)]
109pub trait VaultExt {
110    // Role and admin
111    fn set_curator(account: AccountId);
112    fn set_is_allocator(account: AccountId, allowed: bool);
113    fn submit_guardian(new_g: AccountId);
114    fn accept_guardian();
115    fn revoke_pending_guardian();
116    fn set_skim_recipient(account: AccountId);
117    fn set_fee_recipient(account: AccountId);
118    fn set_performance_fee(fee: U128);
119    fn submit_timelock(new_timelock_ns: U64);
120    fn accept_timelock();
121    fn revoke_pending_timelock();
122
123    // Market config and queues
124    fn submit_cap(market: AccountId, new_cap: U128);
125    fn accept_cap(market: AccountId);
126    fn revoke_pending_cap(market: AccountId);
127    fn submit_market_removal(market: AccountId);
128    fn revoke_pending_market_removal(market: AccountId);
129    fn set_supply_queue(markets: Vec<AccountId>);
130    fn set_withdraw_queue(queue: Vec<AccountId>);
131
132    // User flows
133    fn withdraw(amount: U128, receiver: AccountId) -> PromiseOrValue<()>;
134    fn redeem(shares: U128, receiver: AccountId) -> PromiseOrValue<()>;
135    fn execute_next_withdrawal_request() -> PromiseOrValue<()>;
136    fn skim(token: AccountId) -> Promise;
137    fn allocate(weights: AllocationWeights, amount: Option<U128>) -> PromiseOrValue<()>;
138
139    // Views
140    fn get_configuration() -> VaultConfiguration;
141    fn get_total_assets() -> U128;
142    fn get_total_supply() -> U128;
143    fn get_max_deposit() -> U128;
144    fn convert_to_shares(assets: U128) -> U128;
145    fn convert_to_assets(shares: U128) -> U128;
146    fn preview_deposit(assets: U128) -> U128;
147    fn preview_mint(shares: U128) -> U128;
148    fn preview_withdraw(assets: U128) -> U128;
149    fn preview_redeem(shares: U128) -> U128;
150}
151
152// Add a 20% buffer to a gas estimate
153#[must_use]
154pub const fn buffer(size: u64) -> Gas {
155    Gas::from_tgas((size * 6).div_ceil(5))
156}
157
158// Fetching a position
159const GET_SUPPLY_POSITION: u64 = 4;
160pub const GET_SUPPLY_POSITION_GAS: Gas = Gas::from_tgas(GET_SUPPLY_POSITION);
161
162// Create a withdrawal request
163pub const CREATE_WITHDRAW_REQ_GAS: Gas = buffer(5);
164
165// Balance reads against the underlying NEP-141
166pub const FT_BALANCE_OF_GAS: Gas = Gas::from_tgas(5);
167
168// Execute the next withdrawal request on a market
169const EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ: u64 = 20;
170pub const EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ_GAS: Gas =
171    Gas::from_tgas(EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ);
172
173// Extra gas reserved for post-supply verification callbacks, used in
174// paths where we want a conservative safety margin beyond the base
175// estimate.
176pub const SUPPLY_POST_VERIFY_GAS: Gas = Gas::from_tgas(30);
177
178// Callback gas roots for withdraw/supply orchestration.
179
180// Root budget for callbacks after creating a market-side
181// supply-withdrawal request. Encodes: create request, read supply
182// position and settle withdraw accounting.
183pub const WITHDRAW_CREATE_REQUEST_CALLBACK_GAS: Gas =
184    buffer(EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ + AFTER_EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ);
185
186// Budget for the final "settle" phase of a withdraw execution:
187// reconcile principal and idle_balance, and potentially transition to
188// payout or the next market.
189const AFTER_EXECUTE_NEXT_WITHDRAW: u64 = 5 + 5 + AFTER_SEND_TO_USER;
190pub const WITHDRAW_SETTLE_CALLBACK_GAS: Gas = buffer(AFTER_EXECUTE_NEXT_WITHDRAW);
191
192// Budget for executing the next supply-withdrawal request on a market
193// and fetching the updated supply position before the settle step.
194const AFTER_EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ: u64 =
195    GET_SUPPLY_POSITION + AFTER_EXECUTE_NEXT_WITHDRAW;
196pub const WITHDRAW_EXECUTE_FETCH_POSITION_GAS: Gas = buffer(AFTER_EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ);
197
198const AFTER_SUPPLY_2_READ: u64 = 5;
199pub const SUPPLY_POSITION_READ_CALLBACK_GAS: Gas = buffer(AFTER_SUPPLY_2_READ);
200pub const SUPPLY_AFTER_TRANSFER_CHECK_GAS: Gas = buffer(GET_SUPPLY_POSITION + AFTER_SUPPLY_2_READ);
201
202// NOTE: these are taken after running the contract with the gas report and cieled to next whole TGAS.
203pub const SUPPLY_GAS: Gas = buffer(8);
204pub const ALLOCATE_GAS: Gas = buffer(20);
205pub const WITHDRAW_GAS: Gas = buffer(4);
206pub const EXECUTE_WITHDRAW_GAS: Gas = buffer(9);
207pub const SUBMIT_CAP_GAS: Gas = buffer(3);
208
209const AFTER_SEND_TO_USER: u64 = 5;
210pub const AFTER_SEND_TO_USER_GAS: Gas = Gas::from_tgas(AFTER_SEND_TO_USER);
211
212pub fn require_at_least(needed: Gas) {
213    let gas = env::prepaid_gas();
214    require!(
215        gas >= needed,
216        format!("Insufficient gas: {}, needed: {needed}", gas)
217    );
218}
219
220#[derive(Clone, Debug)]
221#[near]
222pub struct PendingValue<T: core::fmt::Debug> {
223    pub value: T,
224    // Timestamp when this pending value can be finalized
225    pub valid_at_ns: TimestampNs,
226}
227
228impl<T: core::fmt::Debug> PendingValue<T> {
229    pub fn verify(&self) {
230        require!(
231            near_sdk::env::block_timestamp() >= self.valid_at_ns,
232            "Timelock not elapsed yet"
233        );
234    }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
238#[near(serializers = [borsh])]
239/// No operation in-flight. The vault is ready to start a new allocation or withdrawal.
240pub struct IdleState;
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[near(serializers = [borsh])]
244/// Supplying idle underlying to markets according to a plan or queue.
245///
246/// Transitions:
247/// - On completion of allocation: Withdrawing (to satisfy pending user requests) or Idle (if stopped).
248/// - On stop/failure: Idle.
249pub struct AllocatingState {
250    /// Unique operation id used to correlate async callbacks and detect drift.
251    pub op_id: u64,
252    /// Zero-based position within the allocation plan/queue currently being processed.
253    pub index: u32,
254    /// Amount of underlying (in asset units) still to allocate during this operation.
255    pub remaining: u128,
256    /// Plan for allocation.
257    pub plan: Vec<(AccountId, u128)>,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261#[near(serializers = [borsh])]
262/// Collecting liquidity from markets to satisfy a user withdrawal/redeem request.
263///
264/// Transitions:
265/// - Advance within queue: Withdrawing (index increments) while collecting funds.
266/// - When enough is collected to satisfy the request: Payout.
267/// - If the op is stopped or cannot proceed and needs to refund: Idle (escrow_shares refunded).
268pub struct WithdrawingState {
269    /// Unique operation id used to correlate async callbacks and detect drift.
270    pub op_id: u64,
271    /// Zero-based position within the withdraw queue currently being processed.
272    pub index: u32,
273    /// Remaining assets that must still be collected to satisfy the request.
274    pub remaining: u128,
275    /// Assets already collected and held as idle_balance pending payout.
276    pub collected: u128,
277    /// Account that should receive the assets during payout.
278    pub receiver: AccountId,
279    /// The owner whose shares are being redeemed.
280    pub owner: AccountId,
281    /// Shares locked in escrow for this request.
282    /// - Refunded on stop/failure.
283    /// - On payout success, a portion is burned (see burn_shares) and any remainder is refunded.
284    pub escrow_shares: u128,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
288#[near(serializers = [borsh])]
289/// Final step that transfers assets to the receiver and settles the share escrow.
290///
291/// Transitions:
292/// - On success or failure: Idle.
293///
294/// Invariant hooks:
295/// - idle_balance decreases only on payout success by `amount`.
296/// - On success, `burn_shares` are burned from `escrow_shares`; any remainder is refunded.
297/// - On failure, all `escrow_shares` are refunded.
298pub struct PayoutState {
299    /// Unique operation id used to correlate async callbacks and detect drift.
300    pub op_id: u64,
301    /// Receiver of the asset payout.
302    pub receiver: AccountId,
303    /// Amount of assets to transfer out from idle_balance.
304    pub amount: u128,
305    /// The owner whose shares were escrowed for this payout.
306    pub owner: AccountId,
307    /// Total shares currently held in escrow for this operation.
308    pub escrow_shares: u128,
309    /// Portion of `escrow_shares` that will be burned on successful payout.
310    pub burn_shares: u128,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314#[near(serializers = [borsh])]
315/// Operation state machine for asynchronous allocation, withdrawal, and payout flows.
316///
317/// State machine:
318/// - Allocating -> Withdrawing (or Idle via stop)
319/// - Withdrawing -> Withdrawing (advance) | Payout | Idle (refund)
320/// - Payout -> Idle (success or failure)
321///
322/// Invariants:
323/// - idle_balance increases only when funds are received and decreases only on payout success.
324/// - escrow_shares are refunded on stop/failure or partially burned/refunded on payout success.
325pub enum OpState {
326    /// No operation in-flight. The vault is ready to start a new allocation or withdrawal.
327    Idle,
328
329    /// Supplying idle underlying to markets according to a plan or queue.
330    ///
331    /// Transitions:
332    /// - On completion of allocation: Withdrawing (to satisfy pending user requests) or Idle (if stopped).
333    /// - On stop/failure: Idle.
334    Allocating(AllocatingState),
335
336    /// Collecting liquidity from markets to satisfy a user withdrawal/redeem request.
337    ///
338    /// Transitions:
339    /// - Advance within queue: Withdrawing (index increments) while collecting funds.
340    /// - When enough is collected to satisfy the request: Payout.
341    /// - If the op is stopped or cannot proceed and needs to refund: Idle (escrow_shares refunded).
342    Withdrawing(WithdrawingState),
343
344    /// Final step that transfers assets to the receiver and settles the share escrow.
345    ///
346    /// Transitions:
347    /// - On success or failure: Idle.
348    ///
349    /// Invariant hooks:
350    /// - idle_balance decreases only on payout success by `amount`.
351    /// - On success, `burn_shares` are burned from `escrow_shares`; any remainder is refunded.
352    /// - On failure, all `escrow_shares` are refunded.
353    Payout(PayoutState),
354}
355
356impl From<IdleState> for OpState {
357    fn from(_: IdleState) -> Self {
358        OpState::Idle
359    }
360}
361
362impl From<AllocatingState> for OpState {
363    fn from(s: AllocatingState) -> Self {
364        OpState::Allocating(s)
365    }
366}
367
368impl From<WithdrawingState> for OpState {
369    fn from(s: WithdrawingState) -> Self {
370        OpState::Withdrawing(s)
371    }
372}
373
374impl From<PayoutState> for OpState {
375    fn from(s: PayoutState) -> Self {
376        OpState::Payout(s)
377    }
378}
379
380impl AsRef<IdleState> for OpState {
381    fn as_ref(&self) -> &IdleState {
382        match self {
383            OpState::Idle => &IdleState,
384            _ => panic!("OpState::Idle expected"),
385        }
386    }
387}
388
389impl AsRef<AllocatingState> for OpState {
390    fn as_ref(&self) -> &AllocatingState {
391        match self {
392            OpState::Allocating(s) => s,
393            _ => panic!("OpState::Allocating expected"),
394        }
395    }
396}
397
398impl AsRef<WithdrawingState> for OpState {
399    fn as_ref(&self) -> &WithdrawingState {
400        match self {
401            OpState::Withdrawing(s) => s,
402            _ => panic!("OpState::Withdrawing expected"),
403        }
404    }
405}
406
407impl AsRef<PayoutState> for OpState {
408    fn as_ref(&self) -> &PayoutState {
409        match self {
410            OpState::Payout(s) => s,
411            _ => panic!("OpState::Payout expected"),
412        }
413    }
414}
415
416#[derive(Debug, Clone)]
417#[near(serializers = [borsh, json])]
418pub struct Delta {
419    pub market: AccountId,
420    pub amount: U128,
421}
422
423impl Delta {
424    pub fn new<T: Into<U128>>(market: AccountId, amount: T) -> Self {
425        Delta {
426            market,
427            amount: amount.into(),
428        }
429    }
430    pub fn validate(&self) {
431        require!(self.amount.0 > 0, "Delta amount must be greater than zero");
432    }
433}
434
435// + Supply: forward-supply idle assets to a market
436// - Withdraw: ONLY creates a supply-withdrawal request in the market; does not execute it.
437#[derive(Debug, Clone)]
438#[near(serializers = [borsh, json])]
439pub enum AllocationDelta {
440    Supply(Delta),
441    Withdraw(Delta),
442}
443
444impl AsRef<Delta> for AllocationDelta {
445    fn as_ref(&self) -> &Delta {
446        match self {
447            AllocationDelta::Supply(d) | AllocationDelta::Withdraw(d) => d,
448        }
449    }
450}
451
452#[derive(Debug, Clone, Copy)]
453pub struct EscrowSettlement {
454    pub to_burn: u128,
455    pub refund: u128,
456}
457
458impl EscrowSettlement {
459    pub fn new(escrow_shares: u128, burn_shares: u128) -> Self {
460        let to_burn = burn_shares.min(escrow_shares);
461        let refund = escrow_shares.saturating_sub(to_burn);
462
463        Self { to_burn, refund }
464    }
465}
466
467impl From<EscrowSettlement> for (u128, u128) {
468    fn from(tuple: EscrowSettlement) -> Self {
469        (tuple.to_burn, tuple.refund)
470    }
471}
472
473#[derive(Debug)]
474#[near(serializers = [json])]
475pub enum Error {
476    // Invariant: Index drift or stale op_id results in a graceful stop
477    IndexDrifted(ExpectedIdx, ActualIdx),
478    // Invariant: Attempting to work on a market that is missing from the withdraw queue
479    MissingMarket(u32),
480    NotWithdrawing,
481    NotAllocating,
482    MarketTransferFailed,
483    MissingSupplyPosition,
484    PositionReadFailed,
485    BalanceReadFailed,
486    // Insufficient liquidity across all markets to satisfy withdrawal
487    InsufficientLiquidity,
488    ZeroAmount,
489}
490
491impl std::fmt::Display for Error {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        write!(f, "{self:?}")
494    }
495}
496
497#[derive(Clone, Debug)]
498#[near(serializers = [borsh])]
499pub struct PendingWithdrawal {
500    pub owner: AccountId,
501    pub receiver: AccountId,
502    pub escrow_shares: u128,
503    pub expected_assets: u128,
504    pub requested_at: u64,
505}
506
507impl PendingWithdrawal {
508    #[must_use]
509    pub fn encoded_size() -> u64 {
510        storage_bytes_for_account_id()
511            + storage_bytes_for_account_id()
512            + 16  // escrow_shares: u128
513            + 16  // expected_assets: u128
514            + 8 // requested_at: u64
515    }
516}
517
518// Worst case size encoded for AccountId
519#[must_use]
520pub const fn storage_bytes_for_account_id() -> u64 {
521    // 4 bytes for length prefix + worst case size encoded for AccountId
522    4 + AccountId::MAX_LEN as u64
523}
524
525#[derive(Clone, Debug)]
526#[near(serializers = [borsh, json])]
527pub enum IdleBalanceDelta {
528    Increase(U128),
529    Decrease(U128),
530}
531
532impl IdleBalanceDelta {
533    pub fn apply(&self, balance: u128) -> u128 {
534        let new = match self {
535            IdleBalanceDelta::Increase(amount) => balance.saturating_add(amount.0),
536            IdleBalanceDelta::Decrease(amount) => balance.saturating_sub(amount.0),
537        };
538        Event::IdleBalanceUpdated {
539            prev: U128::from(balance),
540            delta: self.clone(),
541        }
542        .emit();
543        new
544    }
545}
546
547#[derive(Debug, Clone)]
548#[near(serializers = [borsh, json])]
549pub enum Reason {
550    NoRoom,
551    ZeroTarget,
552    Other(String),
553}
554
555#[derive(Debug, Clone)]
556#[near(serializers = [borsh, json])]
557pub enum QueueAction {
558    Dequeued,
559    Parked,
560}
561
562#[derive(Debug, Clone)]
563#[near(serializers = [borsh, json])]
564pub enum QueueStatus {
565    NextFound,
566    Empty,
567}
568
569#[derive(Debug, Clone)]
570#[near(serializers = [borsh, json])]
571pub enum WithdrawProgressPhase {
572    ExecutionStarted,
573    SkippedDust,
574    CoveredByIdle,
575    ExecutionRequired,
576}
577
578#[derive(Debug, Clone)]
579#[near(serializers = [borsh, json])]
580pub enum AllocationPositionIssueKind {
581    Missing,
582    ReadFailed,
583}
584
585#[derive(Debug, Clone)]
586#[near(serializers = [borsh, json])]
587pub enum WithdrawalAccountingKind {
588    InflowMismatch,
589    OverpayCredited,
590}
591
592#[derive(Debug, Clone)]
593#[near(serializers = [borsh, json])]
594pub enum PositionReportOutcome {
595    Ok,
596    Missing,
597    ReadFailed,
598}
599
600#[derive(Debug, Clone)]
601#[near(serializers = [borsh, json])]
602pub enum UnbrickPhase {
603    Withdrawing,
604    Payout,
605}
606
607#[near(event_json(standard = "templar-vault"))]
608pub enum Event {
609    #[event_version("1.0.0")]
610    IdleBalanceUpdated { prev: U128, delta: IdleBalanceDelta },
611    #[event_version("1.0.0")]
612    PerformanceFeeAccrued { recipient: AccountId, shares: U128 },
613    #[event_version("1.0.0")]
614    PerformanceFeeMintFailed { error: String },
615    #[event_version("1.0.0")]
616    LockChange { is_locked: bool, market_index: u32 },
617
618    // Allocation
619    #[event_version("1.0.0")]
620    AllocationPlanSet {
621        op_id: U64,
622        total: U128,
623        plan: Vec<(AccountId, U128)>,
624    },
625    #[event_version("1.0.0")]
626    AllocationStarted { op_id: U64, remaining: U128 },
627    #[event_version("1.0.0")]
628    AllocationStepPlan {
629        op_id: U64,
630        index: u32,
631        market: AccountId,
632        target: U128,
633        room: U128,
634        to_supply: U128,
635        remaining_before: U128,
636        planned: bool,
637        reason: Option<Reason>,
638    },
639    #[event_version("1.0.0")]
640    AllocationTransferFailed {
641        op_id: U64,
642        index: u32,
643        market: AccountId,
644        attempted: U128,
645    },
646    #[event_version("1.0.0")]
647    AllocationStepSettled {
648        op_id: U64,
649        index: u32,
650        market: AccountId,
651        before: U128,
652        new_principal: U128,
653        accepted: U128,
654        attempted: U128,
655        refunded: U128,
656        remaining_after: U128,
657    },
658    #[event_version("1.0.0")]
659    AllocationCompleted { op_id: u64 },
660    #[event_version("1.0.0")]
661    AllocationStopped {
662        op_id: U64,
663        index: u32,
664        remaining: U128,
665        reason: Option<Reason>,
666    },
667
668    // Admin and configuration events
669    #[event_version("1.0.0")]
670    CuratorSet { account: AccountId },
671    #[event_version("1.0.0")]
672    GuardianSet { account: AccountId },
673    #[event_version("1.0.0")]
674    AllocatorRoleSet { account: AccountId, allowed: bool },
675    #[event_version("1.0.0")]
676    SkimRecipientSet { account: AccountId },
677    #[event_version("1.0.0")]
678    FeeRecipientSet { account: AccountId },
679    #[event_version("1.0.0")]
680    PerformanceFeeSet { fee: U128 },
681    #[event_version("1.0.0")]
682    TimelockSet { seconds: U64 },
683    #[event_version("1.0.0")]
684    TimelockChangeSubmitted { valid_at_ns: U64 },
685    #[event_version("1.0.0")]
686    PendingTimelockRevoked,
687
688    #[event_version("1.0.0")]
689    Abdicated { method_name: String },
690
691    // Market and queue management
692    #[event_version("1.0.0")]
693    MarketCreated { market: AccountId },
694    #[event_version("1.0.0")]
695    MarketEnabled { market: AccountId },
696    #[event_version("1.0.0")]
697    MarketRemovalSubmitted {
698        market: AccountId,
699        removable_at: U64,
700    },
701    #[event_version("1.0.0")]
702    MarketRemovalRevoked { market: AccountId },
703    #[event_version("1.0.0")]
704    SupplyCapRaiseSubmitted {
705        market: AccountId,
706        new_cap: U128,
707        valid_at_ns: u64,
708    },
709    #[event_version("1.0.0")]
710    SupplyCapRaiseRevoked { market: AccountId },
711    #[event_version("1.0.0")]
712    SupplyCapSet { market: AccountId, new_cap: U128 },
713
714    #[event_version("1.0.0")]
715    WithdrawQueueUpdate { action: QueueAction, id: U64 },
716    #[event_version("1.0.0")]
717    WithdrawQueueStatus {
718        status: QueueStatus,
719        id: Option<U64>,
720    },
721
722    // Rebalance-only withdraw flows
723    #[event_version("1.0.0")]
724    RebalanceWithdrawCompleted { op_id: U64, market: AccountId },
725    #[event_version("1.0.0")]
726    RebalanceWithdrawStopped {
727        op_id: U64,
728        market: AccountId,
729        reason: Option<Reason>,
730    },
731
732    // User flows
733    #[event_version("1.0.0")]
734    RedeemRequested {
735        shares: U128,
736        estimated_assets: U128,
737    },
738    #[event_version("1.0.0")]
739    WithdrawalQueued {
740        id: U64,
741        owner: AccountId,
742        receiver: AccountId,
743        escrow_shares: U128,
744        expected_assets: U128,
745        requested_at: U64,
746    },
747    #[event_version("1.0.0")]
748    WithdrawPreview { shares: U128, receiver: AccountId },
749    #[event_version("1.0.0")]
750    WithdrawProgress {
751        phase: WithdrawProgressPhase,
752        op_id: Option<U64>,
753        id: Option<U64>,
754        market_index: Option<u32>,
755        owner: Option<AccountId>,
756        receiver: Option<AccountId>,
757        escrow_shares: Option<U128>,
758        expected_assets: Option<U128>,
759        requested_at: Option<U64>,
760    },
761    #[event_version("1.0.0")]
762    SupplyWithdrawRequestCreated { market: AccountId, amount: U128 },
763    #[event_version("1.0.0")]
764    WithdrawRequestCreated { market: AccountId, amount: U128 },
765    #[event_version("1.0.0")]
766    // Allocation read/settlement diagnostics
767    #[event_version("1.0.0")]
768    AllocationPositionIssue {
769        op_id: U64,
770        index: u32,
771        market: AccountId,
772        attempted: U128,
773        accepted: U128,
774        kind: AllocationPositionIssueKind,
775    },
776
777    // Withdrawal read diagnostics
778    #[event_version("1.0.0")]
779    CreateWithdrawalFailed {
780        op_id: U64,
781        market: AccountId,
782        index: u32,
783        need: U128,
784    },
785
786    #[event_version("1.0.0")]
787    WithdrawalAccounting {
788        kind: WithdrawalAccountingKind,
789        op_id: U64,
790        market: AccountId,
791        index: u32,
792        delta: Option<U128>,
793        inflow: Option<U128>,
794        extra: Option<U128>,
795    },
796
797    // Payout and stop diagnostics
798    #[event_version("1.0.0")]
799    PayoutUnexpectedState {
800        op_id: U64,
801        receiver: AccountId,
802        amount: U128,
803    },
804    #[event_version("1.0.0")]
805    WithdrawalStopped {
806        op_id: U64,
807        index: u32,
808        remaining: U128,
809        collected: U128,
810        reason: Option<Reason>,
811    },
812    #[event_version("1.0.0")]
813    PayoutStopped {
814        op_id: U64,
815        receiver: AccountId,
816        amount: U128,
817        reason: Option<Reason>,
818    },
819    #[event_version("1.0.0")]
820    OperationStoppedWhileIdle { reason: Option<Reason> },
821    #[event_version("1.0.0")]
822    UnbrickInvoked {
823        phase: UnbrickPhase,
824        op_id: Option<U64>,
825        id: Option<U64>,
826    },
827
828    #[event_version("1.0.0")]
829    WithdrawPositionReport {
830        outcome: PositionReportOutcome,
831        op_id: U64,
832        market: AccountId,
833        index: u32,
834        position: Option<SupplyPosition>,
835        before: Option<U128>,
836    },
837
838    #[event_version("1.0.0")]
839    VaultBalance { amount: U128 },
840}
841
842#[derive(Default)]
843#[near(serializers = [borsh, serde])]
844pub struct Locker {
845    to_lock: Vec<u32>,
846}
847
848impl Locker {
849    pub fn lock(&mut self, i: u32) {
850        if self.is_locked(i) {
851            env::panic_str("Market is locked for index");
852        }
853        Event::LockChange {
854            is_locked: true,
855            market_index: i,
856        }
857        .emit();
858        self.to_lock.push(i);
859    }
860
861    pub fn unlock(&mut self, i: u32) {
862        Event::LockChange {
863            is_locked: false,
864            market_index: i,
865        }
866        .emit();
867        self.to_lock.retain(|&x| x != i);
868    }
869
870    /// Clears the lock status for all markets.
871    /// This method should be used with caution as it will unlock all markets
872    pub fn clear(&mut self) {
873        self.to_lock.clear();
874    }
875
876    pub fn is_locked(&self, i: u32) -> bool {
877        self.to_lock.contains(&i)
878    }
879
880    pub fn is_locked_all(&self) -> bool {
881        !self.to_lock.is_empty()
882    }
883}