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; pub 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#[near(serializers = [json])]
28pub enum DepositMsg {
29 Supply,
31}
32
33#[derive(Clone, Default, Debug)]
35#[near]
36pub struct MarketConfiguration {
37 pub cap: U128,
39 pub enabled: bool,
41 pub removable_at: TimestampNs,
43}
44
45#[derive(Clone)]
47#[near(serializers = [json, borsh])]
48pub struct VaultConfiguration {
49 pub owner: AccountId,
51 pub curator: AccountId,
53 pub guardian: AccountId,
55 pub underlying_token: FungibleAsset<BorrowAsset>,
57 pub initial_timelock_ns: U64,
59 pub fee_recipient: AccountId,
61 pub skim_recipient: AccountId,
63 pub name: String,
65 pub symbol: String,
67 pub decimals: NonZeroU8,
69 pub restrictions: Option<Restrictions>,
71}
72
73#[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 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 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 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 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 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#[must_use]
154pub const fn buffer(size: u64) -> Gas {
155 Gas::from_tgas((size * 6).div_ceil(5))
156}
157
158const GET_SUPPLY_POSITION: u64 = 4;
160pub const GET_SUPPLY_POSITION_GAS: Gas = Gas::from_tgas(GET_SUPPLY_POSITION);
161
162pub const CREATE_WITHDRAW_REQ_GAS: Gas = buffer(5);
164
165pub const FT_BALANCE_OF_GAS: Gas = Gas::from_tgas(5);
167
168const 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
173pub const SUPPLY_POST_VERIFY_GAS: Gas = Gas::from_tgas(30);
177
178pub const WITHDRAW_CREATE_REQUEST_CALLBACK_GAS: Gas =
184 buffer(EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ + AFTER_EXECUTE_NEXT_SUPPLY_WITHDRAW_REQ);
185
186const AFTER_EXECUTE_NEXT_WITHDRAW: u64 = 5 + 5 + AFTER_SEND_TO_USER;
190pub const WITHDRAW_SETTLE_CALLBACK_GAS: Gas = buffer(AFTER_EXECUTE_NEXT_WITHDRAW);
191
192const 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
202pub 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 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])]
239pub struct IdleState;
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[near(serializers = [borsh])]
244pub struct AllocatingState {
250 pub op_id: u64,
252 pub index: u32,
254 pub remaining: u128,
256 pub plan: Vec<(AccountId, u128)>,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261#[near(serializers = [borsh])]
262pub struct WithdrawingState {
269 pub op_id: u64,
271 pub index: u32,
273 pub remaining: u128,
275 pub collected: u128,
277 pub receiver: AccountId,
279 pub owner: AccountId,
281 pub escrow_shares: u128,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
288#[near(serializers = [borsh])]
289pub struct PayoutState {
299 pub op_id: u64,
301 pub receiver: AccountId,
303 pub amount: u128,
305 pub owner: AccountId,
307 pub escrow_shares: u128,
309 pub burn_shares: u128,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314#[near(serializers = [borsh])]
315pub enum OpState {
326 Idle,
328
329 Allocating(AllocatingState),
335
336 Withdrawing(WithdrawingState),
343
344 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#[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 IndexDrifted(ExpectedIdx, ActualIdx),
478 MissingMarket(u32),
480 NotWithdrawing,
481 NotAllocating,
482 MarketTransferFailed,
483 MissingSupplyPosition,
484 PositionReadFailed,
485 BalanceReadFailed,
486 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 + 16 + 8 }
516}
517
518#[must_use]
520pub const fn storage_bytes_for_account_id() -> u64 {
521 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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}