1use std::{collections::BTreeSet, convert::TryInto, num::NonZeroUsize};
6
7use once_cell::sync::Lazy;
8use thiserror::Error;
9
10use oasis_core_runtime::{
11 consensus::{
12 self,
13 beacon::{EpochTime, EPOCH_INVALID},
14 staking::{self, Account as ConsensusAccount, AddEscrowResult, ReclaimEscrowResult},
15 },
16 types::EventKind,
17};
18use oasis_runtime_sdk_macros::{handler, sdk_derive};
19
20use crate::{
21 context::Context,
22 error, migration, module,
23 module::Module as _,
24 modules,
25 modules::{
26 accounts::API as _,
27 core::{Error as CoreError, API as _},
28 },
29 runtime::Runtime,
30 state::CurrentState,
31 storage::Prefix,
32 types::{
33 address::Address,
34 message::{MessageEvent, MessageEventHookInvocation},
35 token,
36 transaction::AuthInfo,
37 },
38};
39
40pub mod state;
41#[cfg(test)]
42mod test;
43pub mod types;
44
45const MODULE_NAME: &str = "consensus_accounts";
47
48#[derive(Error, Debug, oasis_runtime_sdk_macros::Error)]
49pub enum Error {
50 #[error("invalid argument")]
51 #[sdk_error(code = 1)]
52 InvalidArgument,
53
54 #[error("invalid denomination")]
55 #[sdk_error(code = 2)]
56 InvalidDenomination,
57
58 #[error("insufficient balance")]
59 #[sdk_error(code = 3)]
60 InsufficientBalance,
61
62 #[error("forbidden by policy")]
63 #[sdk_error(code = 4)]
64 Forbidden,
65
66 #[error("consensus: {0}")]
67 #[sdk_error(transparent)]
68 Consensus(#[from] modules::consensus::Error),
69
70 #[error("core: {0}")]
71 #[sdk_error(transparent)]
72 Core(#[from] modules::core::Error),
73}
74
75#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
77pub struct GasCosts {
78 pub tx_deposit: u64,
79 pub tx_withdraw: u64,
80 pub tx_delegate: u64,
81 pub tx_undelegate: u64,
82
83 pub store_receipt: u64,
85 pub take_receipt: u64,
87
88 pub delegation: u64,
90
91 pub shares_to_tokens: u64,
93}
94
95#[derive(Clone, Default, Debug, cbor::Encode, cbor::Decode)]
97pub struct Parameters {
98 pub gas_costs: GasCosts,
99
100 pub disable_delegate: bool,
102 pub disable_undelegate: bool,
104 pub disable_deposit: bool,
106 pub disable_withdraw: bool,
108}
109
110impl module::Parameters for Parameters {
111 type Error = ();
112}
113
114#[derive(Debug, cbor::Encode, oasis_runtime_sdk_macros::Event)]
116#[cbor(untagged)]
117pub enum Event {
118 #[sdk_event(code = 1)]
119 Deposit {
120 from: Address,
121 nonce: u64,
122 to: Address,
123 amount: token::BaseUnits,
124 #[cbor(optional)]
125 error: Option<types::ConsensusError>,
126 },
127
128 #[sdk_event(code = 2)]
129 Withdraw {
130 from: Address,
131 nonce: u64,
132 to: Address,
133 amount: token::BaseUnits,
134 #[cbor(optional)]
135 error: Option<types::ConsensusError>,
136 },
137
138 #[sdk_event(code = 3)]
139 Delegate {
140 from: Address,
141 nonce: u64,
142 to: Address,
143 amount: token::BaseUnits,
144 #[cbor(optional)]
145 error: Option<types::ConsensusError>,
146 #[cbor(optional)]
148 shares: Option<u128>,
149 },
150
151 #[sdk_event(code = 4)]
152 UndelegateStart {
153 from: Address,
154 nonce: u64,
155 to: Address,
156 shares: u128,
157 debond_end_time: EpochTime,
158 #[cbor(optional)]
159 error: Option<types::ConsensusError>,
160 },
161
162 #[sdk_event(code = 5)]
163 UndelegateDone {
164 from: Address,
165 to: Address,
166 shares: u128,
167 amount: token::BaseUnits,
168 #[cbor(optional)]
170 epoch: Option<EpochTime>,
171 },
172}
173
174#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
176pub struct Genesis {
177 pub parameters: Parameters,
178}
179
180pub trait API {
182 fn deposit<C: Context>(
189 ctx: &C,
190 from: Address,
191 nonce: u64,
192 to: Address,
193 amount: token::BaseUnits,
194 ) -> Result<(), Error>;
195
196 fn withdraw<C: Context>(
203 ctx: &C,
204 from: Address,
205 nonce: u64,
206 to: Address,
207 amount: token::BaseUnits,
208 ) -> Result<(), Error>;
209
210 fn delegate<C: Context>(
217 ctx: &C,
218 from: Address,
219 nonce: u64,
220 to: Address,
221 amount: token::BaseUnits,
222 receipt: bool,
223 ) -> Result<(), Error>;
224
225 fn undelegate<C: Context>(
233 ctx: &C,
234 from: Address,
235 nonce: u64,
236 to: Address,
237 shares: u128,
238 receipt: bool,
239 ) -> Result<(), Error>;
240}
241
242pub struct Module<Consensus: modules::consensus::API> {
243 _consensus: std::marker::PhantomData<Consensus>,
244}
245
246pub static ADDRESS_PENDING_WITHDRAWAL: Lazy<Address> =
250 Lazy::new(|| Address::from_module(MODULE_NAME, "pending-withdrawal"));
251
252pub static ADDRESS_PENDING_DELEGATION: Lazy<Address> =
256 Lazy::new(|| Address::from_module(MODULE_NAME, "pending-delegation"));
257
258const CONSENSUS_TRANSFER_HANDLER: &str = "consensus.TransferFromRuntime";
259const CONSENSUS_WITHDRAW_HANDLER: &str = "consensus.WithdrawIntoRuntime";
260const CONSENSUS_DELEGATE_HANDLER: &str = "consensus.Delegate";
261const CONSENSUS_UNDELEGATE_HANDLER: &str = "consensus.Undelegate";
262
263impl<Consensus: modules::consensus::API> API for Module<Consensus> {
264 fn deposit<C: Context>(
265 ctx: &C,
266 from: Address,
267 nonce: u64,
268 to: Address,
269 amount: token::BaseUnits,
270 ) -> Result<(), Error> {
271 Consensus::withdraw(
278 ctx,
279 from,
280 &amount,
281 MessageEventHookInvocation::new(
282 CONSENSUS_WITHDRAW_HANDLER.to_string(),
283 types::ConsensusWithdrawContext {
284 from,
285 nonce,
286 address: to,
287 amount: amount.clone(),
288 },
289 ),
290 )?;
291
292 Ok(())
293 }
294
295 fn withdraw<C: Context>(
296 ctx: &C,
297 from: Address,
298 nonce: u64,
299 to: Address,
300 amount: token::BaseUnits,
301 ) -> Result<(), Error> {
302 Consensus::transfer(
304 ctx,
305 to,
306 &amount,
307 MessageEventHookInvocation::new(
308 CONSENSUS_TRANSFER_HANDLER.to_string(),
309 types::ConsensusTransferContext {
310 to,
311 nonce,
312 address: from,
313 amount: amount.clone(),
314 },
315 ),
316 )?;
317
318 if CurrentState::with_env(|env| env.is_check_only()) {
319 return Ok(());
320 }
321
322 <C::Runtime as Runtime>::Accounts::transfer(from, *ADDRESS_PENDING_WITHDRAWAL, &amount)
325 .map_err(|_| Error::InsufficientBalance)?;
326
327 Ok(())
328 }
329
330 fn delegate<C: Context>(
331 ctx: &C,
332 from: Address,
333 nonce: u64,
334 to: Address,
335 amount: token::BaseUnits,
336 receipt: bool,
337 ) -> Result<(), Error> {
338 Consensus::escrow(
339 ctx,
340 to,
341 &amount,
342 MessageEventHookInvocation::new(
343 CONSENSUS_DELEGATE_HANDLER.to_string(),
344 types::ConsensusDelegateContext {
345 from,
346 nonce,
347 to,
348 amount: amount.clone(),
349 receipt,
350 },
351 ),
352 )?;
353
354 if CurrentState::with_env(|env| env.is_check_only()) {
355 return Ok(());
356 }
357
358 <C::Runtime as Runtime>::Accounts::transfer(from, *ADDRESS_PENDING_DELEGATION, &amount)
361 .map_err(|_| Error::InsufficientBalance)?;
362
363 Ok(())
364 }
365
366 fn undelegate<C: Context>(
367 ctx: &C,
368 from: Address,
369 nonce: u64,
370 to: Address,
371 shares: u128,
372 receipt: bool,
373 ) -> Result<(), Error> {
374 state::sub_delegation(to, from, shares)?;
376
377 Consensus::reclaim_escrow(
378 ctx,
379 from,
380 shares,
381 MessageEventHookInvocation::new(
382 CONSENSUS_UNDELEGATE_HANDLER.to_string(),
383 types::ConsensusUndelegateContext {
384 from,
385 nonce,
386 to,
387 shares,
388 receipt,
389 },
390 ),
391 )?;
392
393 Ok(())
394 }
395}
396
397#[sdk_derive(Module)]
398impl<Consensus: modules::consensus::API> Module<Consensus> {
399 const NAME: &'static str = MODULE_NAME;
400 const VERSION: u32 = 1;
401 type Error = Error;
402 type Event = Event;
403 type Parameters = Parameters;
404 type Genesis = Genesis;
405
406 #[migration(init)]
407 pub fn init(genesis: Genesis) {
408 Self::set_params(genesis.parameters);
410 }
411
412 #[handler(call = "consensus.Deposit")]
414 fn tx_deposit<C: Context>(ctx: &C, body: types::Deposit) -> Result<(), Error> {
415 let params = Self::params();
416 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_deposit)?;
417
418 if params.disable_deposit {
420 return Err(Error::Forbidden);
421 }
422
423 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
424 Consensus::ensure_compatible_tx_signer()?;
425
426 let address = signer.address_spec.address();
427 let nonce = signer.nonce;
428 Self::deposit(ctx, address, nonce, body.to.unwrap_or(address), body.amount)
429 }
430
431 #[handler(prefetch = "consensus.Withdraw")]
433 fn prefetch_withdraw(
434 add_prefix: &mut dyn FnMut(Prefix),
435 _body: cbor::Value,
436 auth_info: &AuthInfo,
437 ) -> Result<(), error::RuntimeError> {
438 let addr = auth_info.signer_info[0].address_spec.address();
440 add_prefix(Prefix::from(
441 [
442 modules::accounts::Module::NAME.as_bytes(),
443 modules::accounts::state::BALANCES,
444 addr.as_ref(),
445 ]
446 .concat(),
447 ));
448 Ok(())
449 }
450
451 #[handler(call = "consensus.Withdraw")]
452 fn tx_withdraw<C: Context>(ctx: &C, body: types::Withdraw) -> Result<(), Error> {
453 let params = Self::params();
454 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_withdraw)?;
455
456 if params.disable_withdraw {
458 return Err(Error::Forbidden);
459 }
460
461 if body.to.is_none() {
463 Consensus::ensure_compatible_tx_signer()?;
467 }
468
469 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
470 let address = signer.address_spec.address();
471 let nonce = signer.nonce;
472 Self::withdraw(ctx, address, nonce, body.to.unwrap_or(address), body.amount)
473 }
474
475 #[handler(call = "consensus.Delegate")]
476 fn tx_delegate<C: Context>(ctx: &C, body: types::Delegate) -> Result<(), Error> {
477 let params = Self::params();
478 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_delegate)?;
479 let store_receipt = body.receipt > 0;
480 if store_receipt {
481 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.store_receipt)?;
482 }
483
484 if params.disable_delegate {
486 return Err(Error::Forbidden);
487 }
488 if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
490 return Err(Error::InvalidArgument);
491 }
492
493 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
495 let from = signer.address_spec.address();
496 let nonce = if store_receipt {
497 body.receipt } else {
499 signer.nonce };
501 Self::delegate(ctx, from, nonce, body.to, body.amount, store_receipt)
502 }
503
504 #[handler(call = "consensus.Undelegate")]
505 fn tx_undelegate<C: Context>(ctx: &C, body: types::Undelegate) -> Result<(), Error> {
506 let params = Self::params();
507 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_undelegate)?;
508 let store_receipt = body.receipt > 0;
509 if store_receipt {
510 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.store_receipt)?;
511 }
512
513 if params.disable_undelegate {
515 return Err(Error::Forbidden);
516 }
517 if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
519 return Err(Error::InvalidArgument);
520 }
521
522 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
524 let to = signer.address_spec.address();
525 let nonce = if store_receipt {
526 body.receipt } else {
528 signer.nonce };
530 Self::undelegate(ctx, body.from, nonce, to, body.shares, store_receipt)
531 }
532
533 #[handler(call = "consensus.TakeReceipt", internal)]
534 fn internal_take_receipt<C: Context>(
535 _ctx: &C,
536 body: types::TakeReceipt,
537 ) -> Result<Option<types::Receipt>, Error> {
538 let params = Self::params();
539 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.take_receipt)?;
540
541 if !body.kind.is_valid() {
542 return Err(Error::InvalidArgument);
543 }
544
545 Ok(state::take_receipt(
546 CurrentState::with_env(|env| env.tx_caller_address()),
547 body.kind,
548 body.id,
549 ))
550 }
551
552 #[handler(query = "consensus.Balance")]
553 fn query_balance<C: Context>(
554 _ctx: &C,
555 args: types::BalanceQuery,
556 ) -> Result<types::AccountBalance, Error> {
557 let denomination = Consensus::consensus_denomination()?;
558 let balances = <C::Runtime as Runtime>::Accounts::get_balances(args.address)
559 .map_err(|_| Error::InvalidArgument)?;
560 let balance = balances
561 .balances
562 .get(&denomination)
563 .copied()
564 .unwrap_or_default();
565 Ok(types::AccountBalance { balance })
566 }
567
568 #[handler(query = "consensus.Account")]
569 fn query_consensus_account<C: Context>(
570 ctx: &C,
571 args: types::ConsensusAccountQuery,
572 ) -> Result<ConsensusAccount, Error> {
573 Consensus::account(ctx, args.address).map_err(|_| Error::InvalidArgument)
574 }
575
576 #[handler(query = "consensus.Delegation")]
577 fn query_delegation<C: Context>(
578 _ctx: &C,
579 args: types::DelegationQuery,
580 ) -> Result<types::DelegationInfo, Error> {
581 state::get_delegation(args.from, args.to)
582 }
583
584 #[handler(call = "consensus.Delegation", internal)]
585 fn internal_delegation<C: Context>(
586 _ctx: &C,
587 args: types::DelegationQuery,
588 ) -> Result<types::DelegationInfo, Error> {
589 let params = Self::params();
590 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.delegation)?;
591
592 state::get_delegation(args.from, args.to)
593 }
594
595 #[handler(query = "consensus.Delegations")]
596 fn query_delegations<C: Context>(
597 _ctx: &C,
598 args: types::DelegationsQuery,
599 ) -> Result<Vec<types::ExtendedDelegationInfo>, Error> {
600 state::get_delegations(args.from)
601 }
602
603 #[handler(query = "consensus.AllDelegations", expensive)]
604 fn query_all_delegations<C: Context>(
605 _ctx: &C,
606 _args: (),
607 ) -> Result<Vec<types::CompleteDelegationInfo>, Error> {
608 state::get_all_delegations()
609 }
610
611 #[handler(query = "consensus.Undelegations")]
612 fn query_undelegations<C: Context>(
613 _ctx: &C,
614 args: types::UndelegationsQuery,
615 ) -> Result<Vec<types::UndelegationInfo>, Error> {
616 state::get_undelegations(args.to)
617 }
618
619 #[handler(query = "consensus.AllUndelegations", expensive)]
620 fn query_all_undelegations<C: Context>(
621 _ctx: &C,
622 _args: (),
623 ) -> Result<Vec<types::CompleteUndelegationInfo>, Error> {
624 state::get_all_undelegations()
625 }
626
627 #[handler(call = "consensus.SharesToTokens", internal)]
628 fn internal_shares_to_tokens<C: Context>(
629 ctx: &C,
630 args: types::SharesToTokens,
631 ) -> Result<u128, Error> {
632 let params = Self::params();
633 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.shares_to_tokens)?;
634
635 let account = Consensus::account(ctx, args.address)?;
636 let pool = match args.pool {
637 types::SharePool::Active => &account.escrow.active,
638 types::SharePool::Debonding => &account.escrow.debonding,
639 _ => return Err(Error::InvalidArgument),
640 };
641
642 args.shares
643 .checked_mul(u128::try_from(pool.balance.clone()).map_err(|_| Error::InvalidArgument)?)
644 .ok_or(Error::InvalidArgument)?
645 .checked_div(
646 u128::try_from(pool.total_shares.clone()).map_err(|_| Error::InvalidArgument)?,
647 )
648 .ok_or(Error::InvalidArgument)
649 }
650
651 #[handler(message_result = CONSENSUS_TRANSFER_HANDLER)]
652 fn message_result_transfer<C: Context>(
653 ctx: &C,
654 me: MessageEvent,
655 context: types::ConsensusTransferContext,
656 ) {
657 if !me.is_success() {
658 <C::Runtime as Runtime>::Accounts::transfer(
660 *ADDRESS_PENDING_WITHDRAWAL,
661 context.address,
662 &context.amount,
663 )
664 .expect("should have enough balance");
665
666 CurrentState::with(|state| {
668 state.emit_event(Event::Withdraw {
669 from: context.address,
670 nonce: context.nonce,
671 to: context.to,
672 amount: context.amount.clone(),
673 error: Some(me.into()),
674 });
675 });
676 return;
677 }
678
679 <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_WITHDRAWAL, &context.amount)
681 .expect("should have enough balance");
682
683 CurrentState::with(|state| {
685 state.emit_event(Event::Withdraw {
686 from: context.address,
687 nonce: context.nonce,
688 to: context.to,
689 amount: context.amount.clone(),
690 error: None,
691 });
692 });
693 }
694
695 #[handler(message_result = CONSENSUS_WITHDRAW_HANDLER)]
696 fn message_result_withdraw<C: Context>(
697 ctx: &C,
698 me: MessageEvent,
699 context: types::ConsensusWithdrawContext,
700 ) {
701 if !me.is_success() {
702 CurrentState::with(|state| {
704 state.emit_event(Event::Deposit {
705 from: context.from,
706 nonce: context.nonce,
707 to: context.address,
708 amount: context.amount.clone(),
709 error: Some(me.into()),
710 });
711 });
712 return;
713 }
714
715 <C::Runtime as Runtime>::Accounts::mint(context.address, &context.amount).unwrap();
717
718 CurrentState::with(|state| {
720 state.emit_event(Event::Deposit {
721 from: context.from,
722 nonce: context.nonce,
723 to: context.address,
724 amount: context.amount.clone(),
725 error: None,
726 });
727 });
728 }
729
730 #[handler(message_result = CONSENSUS_DELEGATE_HANDLER)]
731 fn message_result_delegate<C: Context>(
732 ctx: &C,
733 me: MessageEvent,
734 context: types::ConsensusDelegateContext,
735 ) {
736 if !me.is_success() {
737 <C::Runtime as Runtime>::Accounts::transfer(
739 *ADDRESS_PENDING_DELEGATION,
740 context.from,
741 &context.amount,
742 )
743 .expect("should have enough balance");
744
745 if context.receipt {
747 state::set_receipt(
748 context.from,
749 types::ReceiptKind::Delegate,
750 context.nonce,
751 types::Receipt {
752 error: Some(me.clone().into()),
753 ..Default::default()
754 },
755 );
756 }
757
758 CurrentState::with(|state| {
760 state.emit_event(Event::Delegate {
761 from: context.from,
762 nonce: context.nonce,
763 to: context.to,
764 amount: context.amount,
765 shares: None,
766 error: Some(me.into()),
767 });
768 });
769 return;
770 }
771
772 <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_DELEGATION, &context.amount)
774 .expect("should have enough balance");
775
776 let result = me
778 .result
779 .expect("event from consensus should have a result");
780 let result: AddEscrowResult = cbor::from_value(result).unwrap();
781 let shares = result.new_shares.try_into().unwrap();
782
783 state::add_delegation(context.from, context.to, shares).unwrap();
784
785 if context.receipt {
787 state::set_receipt(
788 context.from,
789 types::ReceiptKind::Delegate,
790 context.nonce,
791 types::Receipt {
792 shares,
793 ..Default::default()
794 },
795 );
796 }
797
798 CurrentState::with(|state| {
800 state.emit_event(Event::Delegate {
801 from: context.from,
802 nonce: context.nonce,
803 to: context.to,
804 amount: context.amount,
805 shares: Some(shares),
806 error: None,
807 });
808 });
809 }
810
811 #[handler(message_result = CONSENSUS_UNDELEGATE_HANDLER)]
812 fn message_result_undelegate<C: Context>(
813 ctx: &C,
814 me: MessageEvent,
815 context: types::ConsensusUndelegateContext,
816 ) {
817 if !me.is_success() {
818 state::add_delegation(context.to, context.from, context.shares).unwrap();
820
821 if context.receipt {
823 state::set_receipt(
824 context.to,
825 types::ReceiptKind::UndelegateStart,
826 context.nonce,
827 types::Receipt {
828 error: Some(me.clone().into()),
829 ..Default::default()
830 },
831 );
832 }
833
834 CurrentState::with(|state| {
836 state.emit_event(Event::UndelegateStart {
837 from: context.from,
838 nonce: context.nonce,
839 to: context.to,
840 shares: context.shares,
841 debond_end_time: EPOCH_INVALID,
842 error: Some(me.into()),
843 });
844 });
845 return;
846 }
847
848 let result = me
851 .result
852 .expect("event from consensus should have a result");
853 let result: ReclaimEscrowResult = cbor::from_value(result).unwrap();
854 let debonding_shares = result.debonding_shares.try_into().unwrap();
855
856 let receipt = if context.receipt {
857 context.nonce
858 } else {
859 0 };
861
862 let done_receipt = state::add_undelegation(
863 context.from,
864 context.to,
865 result.debond_end_time,
866 debonding_shares,
867 receipt,
868 )
869 .unwrap();
870
871 if context.receipt {
873 state::set_receipt(
874 context.to,
875 types::ReceiptKind::UndelegateStart,
876 context.nonce,
877 types::Receipt {
878 epoch: result.debond_end_time,
879 receipt: done_receipt,
880 ..Default::default()
881 },
882 );
883 }
884
885 CurrentState::with(|state| {
887 state.emit_event(Event::UndelegateStart {
888 from: context.from,
889 nonce: context.nonce,
890 to: context.to,
891 shares: context.shares,
892 debond_end_time: result.debond_end_time,
893 error: None,
894 });
895 });
896 }
897}
898
899impl<Consensus: modules::consensus::API> module::TransactionHandler for Module<Consensus> {}
900
901impl<Consensus: modules::consensus::API> module::BlockHandler for Module<Consensus> {
902 fn end_block<C: Context>(ctx: &C) {
903 if !<C::Runtime as Runtime>::Core::has_epoch_changed() {
905 return;
906 }
907
908 let logger = ctx.get_logger("consensus_accounts");
909 slog::debug!(logger, "epoch changed, processing queued undelegations";
910 "epoch" => ctx.epoch(),
911 );
912
913 let mut reclaims: lru::LruCache<(EpochTime, Address), (u128, u128)> =
914 lru::LruCache::new(NonZeroUsize::new(128).unwrap());
915
916 let own_address = Address::from_runtime_id(ctx.runtime_id());
917 let denomination = Consensus::consensus_denomination().unwrap();
918 let qd = state::get_queued_undelegations(ctx.epoch()).unwrap();
919 for ud in qd {
920 let udi = state::take_undelegation(&ud).unwrap();
921
922 slog::debug!(logger, "processing undelegation";
923 "shares" => udi.shares,
924 );
925
926 let (total_amount, total_shares) =
928 if let Some(totals) = reclaims.get(&(ud.epoch, ud.from)) {
929 *totals
930 } else {
931 let height = Consensus::height_for_epoch(ctx, ud.epoch)
936 .expect("failed to determine height for epoch");
937
938 let totals = ctx
944 .history()
945 .consensus_events_at(height, EventKind::Staking)
946 .expect("failed to fetch historic events")
947 .iter()
948 .find_map(|ev| match ev {
949 consensus::Event::Staking(staking::Event {
950 escrow:
951 Some(staking::EscrowEvent::Reclaim {
952 owner,
953 escrow,
954 amount,
955 shares,
956 }),
957 ..
958 }) if owner == &own_address.into() && escrow == &ud.from.into() => {
959 Some((amount.try_into().unwrap(), shares.try_into().unwrap()))
960 }
961 _ => None,
962 })
963 .expect("reclaim event should have been emitted");
964
965 reclaims.put((ud.epoch, ud.from), totals);
966 totals
967 };
968
969 let amount = udi
971 .shares
972 .checked_mul(total_amount)
973 .expect("shares * total_amount should not overflow")
974 .checked_div(total_shares)
975 .expect("total_shares should not be zero");
976 let raw_amount = Consensus::amount_from_consensus(ctx, amount).unwrap();
977 let amount = token::BaseUnits::new(raw_amount, denomination.clone());
978
979 <C::Runtime as Runtime>::Accounts::mint(ud.to, &amount).unwrap();
981
982 if udi.receipt > 0 {
984 state::set_receipt(
985 ud.to,
986 types::ReceiptKind::UndelegateDone,
987 udi.receipt,
988 types::Receipt {
989 amount: raw_amount,
990 ..Default::default()
991 },
992 );
993 }
994
995 CurrentState::with(|state| {
997 state.emit_event(Event::UndelegateDone {
998 from: ud.from,
999 to: ud.to,
1000 shares: udi.shares,
1001 amount,
1002 epoch: Some(ud.epoch),
1003 });
1004 });
1005 }
1006 }
1007}
1008
1009impl<Consensus: modules::consensus::API> module::InvariantHandler for Module<Consensus> {
1010 fn check_invariants<C: Context>(ctx: &C) -> Result<(), CoreError> {
1012 let den = Consensus::consensus_denomination().unwrap();
1017 #[allow(clippy::or_fun_call)]
1018 let ts = <C::Runtime as Runtime>::Accounts::get_total_supplies().or(Err(
1019 CoreError::InvariantViolation("unable to get total supplies".to_string()),
1020 ))?;
1021
1022 let rt_addr = Address::from_runtime_id(ctx.runtime_id());
1023 let rt_acct = Consensus::account(ctx, rt_addr).unwrap_or_default();
1024 let rt_ga_balance = rt_acct.general.balance;
1025 let rt_ga_balance: u128 = rt_ga_balance.try_into().unwrap_or(u128::MAX);
1026
1027 let rt_ga_balance = Consensus::amount_from_consensus(ctx, rt_ga_balance).map_err(|_| {
1028 CoreError::InvariantViolation(
1029 "runtime's consensus balance is not representable".to_string(),
1030 )
1031 })?;
1032
1033 if let Some(total_supply) = ts.get(&den) {
1034 if total_supply > &rt_ga_balance {
1035 return Err(CoreError::InvariantViolation(
1036 format!("total supply ({total_supply}) is greater than runtime's general account balance ({rt_ga_balance})"),
1037 ));
1038 }
1039 }
1040
1041 let delegations = state::get_delegations_by_destination()
1045 .map_err(|_| CoreError::InvariantViolation("unable to get delegations".to_string()))?;
1046
1047 for (to, shares) in delegations {
1048 let cons_shares = Consensus::delegation(ctx, rt_addr, to)
1049 .map_err(|err| {
1050 CoreError::InvariantViolation(format!(
1051 "unable to fetch consensus delegation {rt_addr} -> {to}: {err}"
1052 ))
1053 })?
1054 .shares;
1055
1056 if cons_shares < shares.into() {
1057 return Err(CoreError::InvariantViolation(format!(
1058 "runtime does not have enough shares delegated to {to} (expected: {shares} got: {cons_shares}"
1059 )));
1060 }
1061 }
1062
1063 Ok(())
1064 }
1065}