oasis_runtime_sdk/modules/consensus_accounts/
mod.rs

1//! Consensus accounts module.
2//!
3//! This module allows consensus transfers in and out of the runtime account,
4//! while keeping track of amount deposited per account.
5use 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
45/// Unique module name.
46const 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/// Gas costs.
76#[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    /// Cost of storing a delegation/undelegation receipt.
84    pub store_receipt: u64,
85    /// Cost of taking a delegation/undelegation receipt.
86    pub take_receipt: u64,
87
88    /// Cost of getting delegation info through a subcall.
89    pub delegation: u64,
90
91    /// Cost of converting the number of delegated shares to tokens at current rate.
92    pub shares_to_tokens: u64,
93}
94
95/// Parameters for the consensus module.
96#[derive(Clone, Default, Debug, cbor::Encode, cbor::Decode)]
97pub struct Parameters {
98    pub gas_costs: GasCosts,
99
100    /// Whether delegate functionality should be disabled.
101    pub disable_delegate: bool,
102    /// Whether undelegate functionality should be disabled.
103    pub disable_undelegate: bool,
104    /// Whether deposit functionality should be disabled.
105    pub disable_deposit: bool,
106    /// Whether withdraw functionality should be disabled.
107    pub disable_withdraw: bool,
108}
109
110impl module::Parameters for Parameters {
111    type Error = ();
112}
113
114/// Events emitted by the consensus accounts module.
115#[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        // Added in runtime-sdk v0.15.0.
147        #[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        // Added in runtime-sdk v0.15.0.
169        #[cbor(optional)]
170        epoch: Option<EpochTime>,
171    },
172}
173
174/// Genesis state for the consensus module.
175#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
176pub struct Genesis {
177    pub parameters: Parameters,
178}
179
180/// Interface that can be called from other modules.
181pub trait API {
182    /// Transfer from consensus staking account to runtime account.
183    ///
184    /// # Arguments
185    ///
186    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
187    ///   When called from a deposit transaction, we use the signer nonce.
188    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    /// Transfer from runtime account to consensus staking account.
197    ///
198    /// # Arguments
199    ///
200    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
201    ///   When called from a withdraw transaction, we use the signer nonce.
202    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    /// Delegate from runtime account to consensus staking account.
211    ///
212    /// # Arguments
213    ///
214    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
215    ///   When called from a delegate transaction, we use the signer nonce.
216    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    /// Start the undelegation process of the given number of shares from consensus staking account
226    /// to runtime account.
227    ///
228    /// # Arguments
229    ///
230    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
231    ///   When called from an undelegate transaction, we use the signer nonce.
232    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
246/// Module's address that has the tokens pending withdrawal.
247///
248/// oasis1qr677rv0dcnh7ys4yanlynysvnjtk9gnsyhvm6ln
249pub static ADDRESS_PENDING_WITHDRAWAL: Lazy<Address> =
250    Lazy::new(|| Address::from_module(MODULE_NAME, "pending-withdrawal"));
251
252/// Module's address that has the tokens pending delegation.
253///
254/// oasis1qzcdegtf7aunxr5n5pw7n5xs3u7cmzlz9gwmq49r
255pub 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        // XXX: could check consensus state if allowance for the runtime account
272        // exists, but consensus state could be outdated since last block, so
273        // just try to withdraw.
274
275        // Do withdraw from the consensus account and update the account state if
276        // successful.
277        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        // Transfer out of runtime account and update the account state if successful.
303        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        // Transfer the given amount to the module's withdrawal account to make sure the tokens
323        // remain available until actually withdrawn.
324        <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        // Transfer the given amount to the module's delegation account to make sure the tokens
359        // remain available until actually delegated.
360        <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        // Subtract shares from delegation, making sure there are enough there.
375        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        // Set genesis parameters.
409        Self::set_params(genesis.parameters);
410    }
411
412    /// Deposit in the runtime.
413    #[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        // Check whether deposit is allowed.
419        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    /// Withdraw from the runtime.
432    #[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        // Prefetch withdrawing account balance.
439        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        // Check whether withdraw is allowed.
457        if params.disable_withdraw {
458            return Err(Error::Forbidden);
459        }
460
461        // Signer.
462        if body.to.is_none() {
463            // If no `to` field is specified, i.e. withdrawing to the transaction sender's account,
464            // only allow the consensus-compatible single-Ed25519-key signer type. Otherwise, the
465            // tokens would get stuck in an account that you can't sign for on the consensus layer.
466            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        // Check whether delegate is allowed.
485        if params.disable_delegate {
486            return Err(Error::Forbidden);
487        }
488        // Make sure receipts can only be requested internally (e.g. via subcalls).
489        if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
490            return Err(Error::InvalidArgument);
491        }
492
493        // Signer.
494        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 // Use receipt identifier as the nonce.
498        } else {
499            signer.nonce // Use signer nonce as the nonce.
500        };
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        // Check whether undelegate is allowed.
514        if params.disable_undelegate {
515            return Err(Error::Forbidden);
516        }
517        // Make sure receipts can only be requested internally (e.g. via subcalls).
518        if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
519            return Err(Error::InvalidArgument);
520        }
521
522        // Signer.
523        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 // Use receipt identifer as the nonce.
527        } else {
528            signer.nonce // Use signer nonce as the nonce.
529        };
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            // Transfer out failed, refund the balance.
659            <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            // Emit withdraw failed event.
667            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        // Burn the withdrawn tokens.
680        <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_WITHDRAWAL, &context.amount)
681            .expect("should have enough balance");
682
683        // Emit withdraw successful event.
684        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            // Transfer in failed, emit deposit failed event.
703            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        // Update runtime state.
716        <C::Runtime as Runtime>::Accounts::mint(context.address, &context.amount).unwrap();
717
718        // Emit deposit successful event.
719        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            // Delegation failed, refund the balance.
738            <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            // Store receipt if requested.
746            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            // Emit delegation failed event.
759            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        // Burn the delegated tokens.
773        <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_DELEGATION, &context.amount)
774            .expect("should have enough balance");
775
776        // Record delegation.
777        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        // Store receipt if requested.
786        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        // Emit delegation successful event.
799        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            // Undelegation failed, add shares back.
819            state::add_delegation(context.to, context.from, context.shares).unwrap();
820
821            // Store receipt if requested.
822            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            // Emit undelegation failed event.
835            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        // Queue undelegation processing at the debond end epoch. Further processing will happen in
849        // the end block handler.
850        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 // No receipt needed for UndelegateEnd.
860        };
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        // Store receipt if requested.
872        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        // Emit undelegation started event.
886        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        // Only do work in case the epoch has changed since the last processed block.
904        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            // Determine total amount the runtime got during the reclaim operation.
927            let (total_amount, total_shares) =
928                if let Some(totals) = reclaims.get(&(ud.epoch, ud.from)) {
929                    *totals
930                } else {
931                    // Fetch consensus height corresponding to the given epoch transition. This
932                    // query may be expensive in case the epoch is far back, but the node is
933                    // guaranteed to have it as it was the state after the last normal round
934                    // (otherwise we would have already processed this epoch).
935                    let height = Consensus::height_for_epoch(ctx, ud.epoch)
936                        .expect("failed to determine height for epoch");
937
938                    // Find the relevant reclaim escrow event.
939                    //
940                    // There will always be exactly one matching reclaim escrow event here, because
941                    // debonding delegations get merged at the consensus layer when there are
942                    // multiple reclaims for the same accounts on the same epoch.
943                    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            // Compute proportion of received amount (shares * total_amount / total_shares).
970            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            // Mint the given number of tokens.
980            <C::Runtime as Runtime>::Accounts::mint(ud.to, &amount).unwrap();
981
982            // Store receipt if requested.
983            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            // Emit undelegation done event.
996            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    /// Check invariants.
1011    fn check_invariants<C: Context>(ctx: &C) -> Result<(), CoreError> {
1012        // Total supply of the designated consensus layer token denomination
1013        // should be less than or equal to the balance of the runtime's general
1014        // account in the consensus layer.
1015
1016        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        // Check that the number of shares the runtime has escrowed in consensus is >= what is in
1042        // its internally tracked delegation state.
1043
1044        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}