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 },
147
148 #[sdk_event(code = 4)]
149 UndelegateStart {
150 from: Address,
151 nonce: u64,
152 to: Address,
153 shares: u128,
154 debond_end_time: EpochTime,
155 #[cbor(optional)]
156 error: Option<types::ConsensusError>,
157 },
158
159 #[sdk_event(code = 5)]
160 UndelegateDone {
161 from: Address,
162 to: Address,
163 shares: u128,
164 amount: token::BaseUnits,
165 },
166}
167
168#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
170pub struct Genesis {
171 pub parameters: Parameters,
172}
173
174pub trait API {
176 fn deposit<C: Context>(
183 ctx: &C,
184 from: Address,
185 nonce: u64,
186 to: Address,
187 amount: token::BaseUnits,
188 ) -> Result<(), Error>;
189
190 fn withdraw<C: Context>(
197 ctx: &C,
198 from: Address,
199 nonce: u64,
200 to: Address,
201 amount: token::BaseUnits,
202 ) -> Result<(), Error>;
203
204 fn delegate<C: Context>(
211 ctx: &C,
212 from: Address,
213 nonce: u64,
214 to: Address,
215 amount: token::BaseUnits,
216 receipt: bool,
217 ) -> Result<(), Error>;
218
219 fn undelegate<C: Context>(
227 ctx: &C,
228 from: Address,
229 nonce: u64,
230 to: Address,
231 shares: u128,
232 receipt: bool,
233 ) -> Result<(), Error>;
234}
235
236pub struct Module<Consensus: modules::consensus::API> {
237 _consensus: std::marker::PhantomData<Consensus>,
238}
239
240pub static ADDRESS_PENDING_WITHDRAWAL: Lazy<Address> =
244 Lazy::new(|| Address::from_module(MODULE_NAME, "pending-withdrawal"));
245
246pub static ADDRESS_PENDING_DELEGATION: Lazy<Address> =
250 Lazy::new(|| Address::from_module(MODULE_NAME, "pending-delegation"));
251
252const CONSENSUS_TRANSFER_HANDLER: &str = "consensus.TransferFromRuntime";
253const CONSENSUS_WITHDRAW_HANDLER: &str = "consensus.WithdrawIntoRuntime";
254const CONSENSUS_DELEGATE_HANDLER: &str = "consensus.Delegate";
255const CONSENSUS_UNDELEGATE_HANDLER: &str = "consensus.Undelegate";
256
257impl<Consensus: modules::consensus::API> API for Module<Consensus> {
258 fn deposit<C: Context>(
259 ctx: &C,
260 from: Address,
261 nonce: u64,
262 to: Address,
263 amount: token::BaseUnits,
264 ) -> Result<(), Error> {
265 Consensus::withdraw(
272 ctx,
273 from,
274 &amount,
275 MessageEventHookInvocation::new(
276 CONSENSUS_WITHDRAW_HANDLER.to_string(),
277 types::ConsensusWithdrawContext {
278 from,
279 nonce,
280 address: to,
281 amount: amount.clone(),
282 },
283 ),
284 )?;
285
286 Ok(())
287 }
288
289 fn withdraw<C: Context>(
290 ctx: &C,
291 from: Address,
292 nonce: u64,
293 to: Address,
294 amount: token::BaseUnits,
295 ) -> Result<(), Error> {
296 Consensus::transfer(
298 ctx,
299 to,
300 &amount,
301 MessageEventHookInvocation::new(
302 CONSENSUS_TRANSFER_HANDLER.to_string(),
303 types::ConsensusTransferContext {
304 to,
305 nonce,
306 address: from,
307 amount: amount.clone(),
308 },
309 ),
310 )?;
311
312 if CurrentState::with_env(|env| env.is_check_only()) {
313 return Ok(());
314 }
315
316 <C::Runtime as Runtime>::Accounts::transfer(from, *ADDRESS_PENDING_WITHDRAWAL, &amount)
319 .map_err(|_| Error::InsufficientBalance)?;
320
321 Ok(())
322 }
323
324 fn delegate<C: Context>(
325 ctx: &C,
326 from: Address,
327 nonce: u64,
328 to: Address,
329 amount: token::BaseUnits,
330 receipt: bool,
331 ) -> Result<(), Error> {
332 Consensus::escrow(
333 ctx,
334 to,
335 &amount,
336 MessageEventHookInvocation::new(
337 CONSENSUS_DELEGATE_HANDLER.to_string(),
338 types::ConsensusDelegateContext {
339 from,
340 nonce,
341 to,
342 amount: amount.clone(),
343 receipt,
344 },
345 ),
346 )?;
347
348 if CurrentState::with_env(|env| env.is_check_only()) {
349 return Ok(());
350 }
351
352 <C::Runtime as Runtime>::Accounts::transfer(from, *ADDRESS_PENDING_DELEGATION, &amount)
355 .map_err(|_| Error::InsufficientBalance)?;
356
357 Ok(())
358 }
359
360 fn undelegate<C: Context>(
361 ctx: &C,
362 from: Address,
363 nonce: u64,
364 to: Address,
365 shares: u128,
366 receipt: bool,
367 ) -> Result<(), Error> {
368 state::sub_delegation(to, from, shares)?;
370
371 Consensus::reclaim_escrow(
372 ctx,
373 from,
374 shares,
375 MessageEventHookInvocation::new(
376 CONSENSUS_UNDELEGATE_HANDLER.to_string(),
377 types::ConsensusUndelegateContext {
378 from,
379 nonce,
380 to,
381 shares,
382 receipt,
383 },
384 ),
385 )?;
386
387 Ok(())
388 }
389}
390
391#[sdk_derive(Module)]
392impl<Consensus: modules::consensus::API> Module<Consensus> {
393 const NAME: &'static str = MODULE_NAME;
394 const VERSION: u32 = 1;
395 type Error = Error;
396 type Event = Event;
397 type Parameters = Parameters;
398 type Genesis = Genesis;
399
400 #[migration(init)]
401 pub fn init(genesis: Genesis) {
402 Self::set_params(genesis.parameters);
404 }
405
406 #[handler(call = "consensus.Deposit")]
408 fn tx_deposit<C: Context>(ctx: &C, body: types::Deposit) -> Result<(), Error> {
409 let params = Self::params();
410 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_deposit)?;
411
412 if params.disable_deposit {
414 return Err(Error::Forbidden);
415 }
416
417 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
418 Consensus::ensure_compatible_tx_signer()?;
419
420 let address = signer.address_spec.address();
421 let nonce = signer.nonce;
422 Self::deposit(ctx, address, nonce, body.to.unwrap_or(address), body.amount)
423 }
424
425 #[handler(prefetch = "consensus.Withdraw")]
427 fn prefetch_withdraw(
428 add_prefix: &mut dyn FnMut(Prefix),
429 _body: cbor::Value,
430 auth_info: &AuthInfo,
431 ) -> Result<(), error::RuntimeError> {
432 let addr = auth_info.signer_info[0].address_spec.address();
434 add_prefix(Prefix::from(
435 [
436 modules::accounts::Module::NAME.as_bytes(),
437 modules::accounts::state::BALANCES,
438 addr.as_ref(),
439 ]
440 .concat(),
441 ));
442 Ok(())
443 }
444
445 #[handler(call = "consensus.Withdraw")]
446 fn tx_withdraw<C: Context>(ctx: &C, body: types::Withdraw) -> Result<(), Error> {
447 let params = Self::params();
448 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_withdraw)?;
449
450 if params.disable_withdraw {
452 return Err(Error::Forbidden);
453 }
454
455 if body.to.is_none() {
457 Consensus::ensure_compatible_tx_signer()?;
461 }
462
463 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
464 let address = signer.address_spec.address();
465 let nonce = signer.nonce;
466 Self::withdraw(ctx, address, nonce, body.to.unwrap_or(address), body.amount)
467 }
468
469 #[handler(call = "consensus.Delegate")]
470 fn tx_delegate<C: Context>(ctx: &C, body: types::Delegate) -> Result<(), Error> {
471 let params = Self::params();
472 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_delegate)?;
473 let store_receipt = body.receipt > 0;
474 if store_receipt {
475 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.store_receipt)?;
476 }
477
478 if params.disable_delegate {
480 return Err(Error::Forbidden);
481 }
482 if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
484 return Err(Error::InvalidArgument);
485 }
486
487 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
489 let from = signer.address_spec.address();
490 let nonce = if store_receipt {
491 body.receipt } else {
493 signer.nonce };
495 Self::delegate(ctx, from, nonce, body.to, body.amount, store_receipt)
496 }
497
498 #[handler(call = "consensus.Undelegate")]
499 fn tx_undelegate<C: Context>(ctx: &C, body: types::Undelegate) -> Result<(), Error> {
500 let params = Self::params();
501 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.tx_undelegate)?;
502 let store_receipt = body.receipt > 0;
503 if store_receipt {
504 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.store_receipt)?;
505 }
506
507 if params.disable_undelegate {
509 return Err(Error::Forbidden);
510 }
511 if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
513 return Err(Error::InvalidArgument);
514 }
515
516 let signer = CurrentState::with_env(|env| env.tx_auth_info().signer_info[0].clone());
518 let to = signer.address_spec.address();
519 let nonce = if store_receipt {
520 body.receipt } else {
522 signer.nonce };
524 Self::undelegate(ctx, body.from, nonce, to, body.shares, store_receipt)
525 }
526
527 #[handler(call = "consensus.TakeReceipt", internal)]
528 fn internal_take_receipt<C: Context>(
529 _ctx: &C,
530 body: types::TakeReceipt,
531 ) -> Result<Option<types::Receipt>, Error> {
532 let params = Self::params();
533 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.take_receipt)?;
534
535 if !body.kind.is_valid() {
536 return Err(Error::InvalidArgument);
537 }
538
539 Ok(state::take_receipt(
540 CurrentState::with_env(|env| env.tx_caller_address()),
541 body.kind,
542 body.id,
543 ))
544 }
545
546 #[handler(query = "consensus.Balance")]
547 fn query_balance<C: Context>(
548 _ctx: &C,
549 args: types::BalanceQuery,
550 ) -> Result<types::AccountBalance, Error> {
551 let denomination = Consensus::consensus_denomination()?;
552 let balances = <C::Runtime as Runtime>::Accounts::get_balances(args.address)
553 .map_err(|_| Error::InvalidArgument)?;
554 let balance = balances
555 .balances
556 .get(&denomination)
557 .copied()
558 .unwrap_or_default();
559 Ok(types::AccountBalance { balance })
560 }
561
562 #[handler(query = "consensus.Account")]
563 fn query_consensus_account<C: Context>(
564 ctx: &C,
565 args: types::ConsensusAccountQuery,
566 ) -> Result<ConsensusAccount, Error> {
567 Consensus::account(ctx, args.address).map_err(|_| Error::InvalidArgument)
568 }
569
570 #[handler(query = "consensus.Delegation")]
571 fn query_delegation<C: Context>(
572 _ctx: &C,
573 args: types::DelegationQuery,
574 ) -> Result<types::DelegationInfo, Error> {
575 state::get_delegation(args.from, args.to)
576 }
577
578 #[handler(call = "consensus.Delegation", internal)]
579 fn internal_delegation<C: Context>(
580 _ctx: &C,
581 args: types::DelegationQuery,
582 ) -> Result<types::DelegationInfo, Error> {
583 let params = Self::params();
584 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.delegation)?;
585
586 state::get_delegation(args.from, args.to)
587 }
588
589 #[handler(query = "consensus.Delegations")]
590 fn query_delegations<C: Context>(
591 _ctx: &C,
592 args: types::DelegationsQuery,
593 ) -> Result<Vec<types::ExtendedDelegationInfo>, Error> {
594 state::get_delegations(args.from)
595 }
596
597 #[handler(query = "consensus.Undelegations")]
598 fn query_undelegations<C: Context>(
599 _ctx: &C,
600 args: types::UndelegationsQuery,
601 ) -> Result<Vec<types::UndelegationInfo>, Error> {
602 state::get_undelegations(args.to)
603 }
604
605 #[handler(call = "consensus.SharesToTokens", internal)]
606 fn internal_shares_to_tokens<C: Context>(
607 ctx: &C,
608 args: types::SharesToTokens,
609 ) -> Result<u128, Error> {
610 let params = Self::params();
611 <C::Runtime as Runtime>::Core::use_tx_gas(params.gas_costs.shares_to_tokens)?;
612
613 let account = Consensus::account(ctx, args.address)?;
614 let pool = match args.pool {
615 types::SharePool::Active => &account.escrow.active,
616 types::SharePool::Debonding => &account.escrow.debonding,
617 _ => return Err(Error::InvalidArgument),
618 };
619
620 args.shares
621 .checked_mul(u128::try_from(pool.balance.clone()).map_err(|_| Error::InvalidArgument)?)
622 .ok_or(Error::InvalidArgument)?
623 .checked_div(
624 u128::try_from(pool.total_shares.clone()).map_err(|_| Error::InvalidArgument)?,
625 )
626 .ok_or(Error::InvalidArgument)
627 }
628
629 #[handler(message_result = CONSENSUS_TRANSFER_HANDLER)]
630 fn message_result_transfer<C: Context>(
631 ctx: &C,
632 me: MessageEvent,
633 context: types::ConsensusTransferContext,
634 ) {
635 if !me.is_success() {
636 <C::Runtime as Runtime>::Accounts::transfer(
638 *ADDRESS_PENDING_WITHDRAWAL,
639 context.address,
640 &context.amount,
641 )
642 .expect("should have enough balance");
643
644 CurrentState::with(|state| {
646 state.emit_event(Event::Withdraw {
647 from: context.address,
648 nonce: context.nonce,
649 to: context.to,
650 amount: context.amount.clone(),
651 error: Some(me.into()),
652 });
653 });
654 return;
655 }
656
657 <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_WITHDRAWAL, &context.amount)
659 .expect("should have enough balance");
660
661 CurrentState::with(|state| {
663 state.emit_event(Event::Withdraw {
664 from: context.address,
665 nonce: context.nonce,
666 to: context.to,
667 amount: context.amount.clone(),
668 error: None,
669 });
670 });
671 }
672
673 #[handler(message_result = CONSENSUS_WITHDRAW_HANDLER)]
674 fn message_result_withdraw<C: Context>(
675 ctx: &C,
676 me: MessageEvent,
677 context: types::ConsensusWithdrawContext,
678 ) {
679 if !me.is_success() {
680 CurrentState::with(|state| {
682 state.emit_event(Event::Deposit {
683 from: context.from,
684 nonce: context.nonce,
685 to: context.address,
686 amount: context.amount.clone(),
687 error: Some(me.into()),
688 });
689 });
690 return;
691 }
692
693 <C::Runtime as Runtime>::Accounts::mint(context.address, &context.amount).unwrap();
695
696 CurrentState::with(|state| {
698 state.emit_event(Event::Deposit {
699 from: context.from,
700 nonce: context.nonce,
701 to: context.address,
702 amount: context.amount.clone(),
703 error: None,
704 });
705 });
706 }
707
708 #[handler(message_result = CONSENSUS_DELEGATE_HANDLER)]
709 fn message_result_delegate<C: Context>(
710 ctx: &C,
711 me: MessageEvent,
712 context: types::ConsensusDelegateContext,
713 ) {
714 if !me.is_success() {
715 <C::Runtime as Runtime>::Accounts::transfer(
717 *ADDRESS_PENDING_DELEGATION,
718 context.from,
719 &context.amount,
720 )
721 .expect("should have enough balance");
722
723 if context.receipt {
725 state::set_receipt(
726 context.from,
727 types::ReceiptKind::Delegate,
728 context.nonce,
729 types::Receipt {
730 error: Some(me.clone().into()),
731 ..Default::default()
732 },
733 );
734 }
735
736 CurrentState::with(|state| {
738 state.emit_event(Event::Delegate {
739 from: context.from,
740 nonce: context.nonce,
741 to: context.to,
742 amount: context.amount,
743 error: Some(me.into()),
744 });
745 });
746 return;
747 }
748
749 <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_DELEGATION, &context.amount)
751 .expect("should have enough balance");
752
753 let result = me
755 .result
756 .expect("event from consensus should have a result");
757 let result: AddEscrowResult = cbor::from_value(result).unwrap();
758 let shares = result.new_shares.try_into().unwrap();
759
760 state::add_delegation(context.from, context.to, shares).unwrap();
761
762 if context.receipt {
764 state::set_receipt(
765 context.from,
766 types::ReceiptKind::Delegate,
767 context.nonce,
768 types::Receipt {
769 shares,
770 ..Default::default()
771 },
772 );
773 }
774
775 CurrentState::with(|state| {
777 state.emit_event(Event::Delegate {
778 from: context.from,
779 nonce: context.nonce,
780 to: context.to,
781 amount: context.amount,
782 error: None,
783 });
784 });
785 }
786
787 #[handler(message_result = CONSENSUS_UNDELEGATE_HANDLER)]
788 fn message_result_undelegate<C: Context>(
789 ctx: &C,
790 me: MessageEvent,
791 context: types::ConsensusUndelegateContext,
792 ) {
793 if !me.is_success() {
794 state::add_delegation(context.to, context.from, context.shares).unwrap();
796
797 if context.receipt {
799 state::set_receipt(
800 context.to,
801 types::ReceiptKind::UndelegateStart,
802 context.nonce,
803 types::Receipt {
804 error: Some(me.clone().into()),
805 ..Default::default()
806 },
807 );
808 }
809
810 CurrentState::with(|state| {
812 state.emit_event(Event::UndelegateStart {
813 from: context.from,
814 nonce: context.nonce,
815 to: context.to,
816 shares: context.shares,
817 debond_end_time: EPOCH_INVALID,
818 error: Some(me.into()),
819 });
820 });
821 return;
822 }
823
824 let result = me
827 .result
828 .expect("event from consensus should have a result");
829 let result: ReclaimEscrowResult = cbor::from_value(result).unwrap();
830 let debonding_shares = result.debonding_shares.try_into().unwrap();
831
832 let receipt = if context.receipt {
833 context.nonce
834 } else {
835 0 };
837
838 let done_receipt = state::add_undelegation(
839 context.from,
840 context.to,
841 result.debond_end_time,
842 debonding_shares,
843 receipt,
844 )
845 .unwrap();
846
847 if context.receipt {
849 state::set_receipt(
850 context.to,
851 types::ReceiptKind::UndelegateStart,
852 context.nonce,
853 types::Receipt {
854 epoch: result.debond_end_time,
855 receipt: done_receipt,
856 ..Default::default()
857 },
858 );
859 }
860
861 CurrentState::with(|state| {
863 state.emit_event(Event::UndelegateStart {
864 from: context.from,
865 nonce: context.nonce,
866 to: context.to,
867 shares: context.shares,
868 debond_end_time: result.debond_end_time,
869 error: None,
870 });
871 });
872 }
873}
874
875impl<Consensus: modules::consensus::API> module::TransactionHandler for Module<Consensus> {}
876
877impl<Consensus: modules::consensus::API> module::BlockHandler for Module<Consensus> {
878 fn end_block<C: Context>(ctx: &C) {
879 if !<C::Runtime as Runtime>::Core::has_epoch_changed() {
881 return;
882 }
883
884 let logger = ctx.get_logger("consensus_accounts");
885 slog::debug!(logger, "epoch changed, processing queued undelegations";
886 "epoch" => ctx.epoch(),
887 );
888
889 let mut reclaims: lru::LruCache<(EpochTime, Address), (u128, u128)> =
890 lru::LruCache::new(NonZeroUsize::new(128).unwrap());
891
892 let own_address = Address::from_runtime_id(ctx.runtime_id());
893 let denomination = Consensus::consensus_denomination().unwrap();
894 let qd = state::get_queued_undelegations(ctx.epoch()).unwrap();
895 for ud in qd {
896 let udi = state::take_undelegation(&ud).unwrap();
897
898 slog::debug!(logger, "processing undelegation";
899 "shares" => udi.shares,
900 );
901
902 let (total_amount, total_shares) =
904 if let Some(totals) = reclaims.get(&(ud.epoch, ud.from)) {
905 *totals
906 } else {
907 let height = Consensus::height_for_epoch(ctx, ud.epoch)
912 .expect("failed to determine height for epoch");
913
914 let totals = ctx
920 .history()
921 .consensus_events_at(height, EventKind::Staking)
922 .expect("failed to fetch historic events")
923 .iter()
924 .find_map(|ev| match ev {
925 consensus::Event::Staking(staking::Event {
926 escrow:
927 Some(staking::EscrowEvent::Reclaim {
928 owner,
929 escrow,
930 amount,
931 shares,
932 }),
933 ..
934 }) if owner == &own_address.into() && escrow == &ud.from.into() => {
935 Some((amount.try_into().unwrap(), shares.try_into().unwrap()))
936 }
937 _ => None,
938 })
939 .expect("reclaim event should have been emitted");
940
941 reclaims.put((ud.epoch, ud.from), totals);
942 totals
943 };
944
945 let amount = udi
947 .shares
948 .checked_mul(total_amount)
949 .expect("shares * total_amount should not overflow")
950 .checked_div(total_shares)
951 .expect("total_shares should not be zero");
952 let raw_amount = Consensus::amount_from_consensus(ctx, amount).unwrap();
953 let amount = token::BaseUnits::new(raw_amount, denomination.clone());
954
955 <C::Runtime as Runtime>::Accounts::mint(ud.to, &amount).unwrap();
957
958 if udi.receipt > 0 {
960 state::set_receipt(
961 ud.to,
962 types::ReceiptKind::UndelegateDone,
963 udi.receipt,
964 types::Receipt {
965 amount: raw_amount,
966 ..Default::default()
967 },
968 );
969 }
970
971 CurrentState::with(|state| {
973 state.emit_event(Event::UndelegateDone {
974 from: ud.from,
975 to: ud.to,
976 shares: udi.shares,
977 amount,
978 });
979 });
980 }
981 }
982}
983
984impl<Consensus: modules::consensus::API> module::InvariantHandler for Module<Consensus> {
985 fn check_invariants<C: Context>(ctx: &C) -> Result<(), CoreError> {
987 let den = Consensus::consensus_denomination().unwrap();
992 #[allow(clippy::or_fun_call)]
993 let ts = <C::Runtime as Runtime>::Accounts::get_total_supplies().or(Err(
994 CoreError::InvariantViolation("unable to get total supplies".to_string()),
995 ))?;
996
997 let rt_addr = Address::from_runtime_id(ctx.runtime_id());
998 let rt_acct = Consensus::account(ctx, rt_addr).unwrap_or_default();
999 let rt_ga_balance = rt_acct.general.balance;
1000 let rt_ga_balance: u128 = rt_ga_balance.try_into().unwrap_or(u128::MAX);
1001
1002 let rt_ga_balance = Consensus::amount_from_consensus(ctx, rt_ga_balance).map_err(|_| {
1003 CoreError::InvariantViolation(
1004 "runtime's consensus balance is not representable".to_string(),
1005 )
1006 })?;
1007
1008 if let Some(total_supply) = ts.get(&den) {
1009 if total_supply > &rt_ga_balance {
1010 return Err(CoreError::InvariantViolation(
1011 format!("total supply ({total_supply}) is greater than runtime's general account balance ({rt_ga_balance})"),
1012 ));
1013 }
1014 }
1015
1016 let delegations = state::get_delegations_by_destination()
1020 .map_err(|_| CoreError::InvariantViolation("unable to get delegations".to_string()))?;
1021
1022 for (to, shares) in delegations {
1023 let cons_shares = Consensus::delegation(ctx, rt_addr, to)
1024 .map_err(|err| {
1025 CoreError::InvariantViolation(format!(
1026 "unable to fetch consensus delegation {rt_addr} -> {to}: {err}"
1027 ))
1028 })?
1029 .shares;
1030
1031 if cons_shares < shares.into() {
1032 return Err(CoreError::InvariantViolation(format!(
1033 "runtime does not have enough shares delegated to {to} (expected: {shares} got: {cons_shares}"
1034 )));
1035 }
1036 }
1037
1038 Ok(())
1039 }
1040}