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    },
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/// Genesis state for the consensus module.
169#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
170pub struct Genesis {
171    pub parameters: Parameters,
172}
173
174/// Interface that can be called from other modules.
175pub trait API {
176    /// Transfer from consensus staking account to runtime account.
177    ///
178    /// # Arguments
179    ///
180    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
181    ///   When called from a deposit transaction, we use the signer nonce.
182    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    /// Transfer from runtime account to consensus staking account.
191    ///
192    /// # Arguments
193    ///
194    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
195    ///   When called from a withdraw transaction, we use the signer nonce.
196    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    /// Delegate from runtime account to consensus staking account.
205    ///
206    /// # Arguments
207    ///
208    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
209    ///   When called from a delegate transaction, we use the signer nonce.
210    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    /// Start the undelegation process of the given number of shares from consensus staking account
220    /// to runtime account.
221    ///
222    /// # Arguments
223    ///
224    /// * `nonce`: A caller-provided sequence number that will help identify the success/fail events.
225    ///   When called from an undelegate transaction, we use the signer nonce.
226    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
240/// Module's address that has the tokens pending withdrawal.
241///
242/// oasis1qr677rv0dcnh7ys4yanlynysvnjtk9gnsyhvm6ln
243pub static ADDRESS_PENDING_WITHDRAWAL: Lazy<Address> =
244    Lazy::new(|| Address::from_module(MODULE_NAME, "pending-withdrawal"));
245
246/// Module's address that has the tokens pending delegation.
247///
248/// oasis1qzcdegtf7aunxr5n5pw7n5xs3u7cmzlz9gwmq49r
249pub 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        // XXX: could check consensus state if allowance for the runtime account
266        // exists, but consensus state could be outdated since last block, so
267        // just try to withdraw.
268
269        // Do withdraw from the consensus account and update the account state if
270        // successful.
271        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        // Transfer out of runtime account and update the account state if successful.
297        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        // Transfer the given amount to the module's withdrawal account to make sure the tokens
317        // remain available until actually withdrawn.
318        <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        // Transfer the given amount to the module's delegation account to make sure the tokens
353        // remain available until actually delegated.
354        <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        // Subtract shares from delegation, making sure there are enough there.
369        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        // Set genesis parameters.
403        Self::set_params(genesis.parameters);
404    }
405
406    /// Deposit in the runtime.
407    #[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        // Check whether deposit is allowed.
413        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    /// Withdraw from the runtime.
426    #[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        // Prefetch withdrawing account balance.
433        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        // Check whether withdraw is allowed.
451        if params.disable_withdraw {
452            return Err(Error::Forbidden);
453        }
454
455        // Signer.
456        if body.to.is_none() {
457            // If no `to` field is specified, i.e. withdrawing to the transaction sender's account,
458            // only allow the consensus-compatible single-Ed25519-key signer type. Otherwise, the
459            // tokens would get stuck in an account that you can't sign for on the consensus layer.
460            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        // Check whether delegate is allowed.
479        if params.disable_delegate {
480            return Err(Error::Forbidden);
481        }
482        // Make sure receipts can only be requested internally (e.g. via subcalls).
483        if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
484            return Err(Error::InvalidArgument);
485        }
486
487        // Signer.
488        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 // Use receipt identifier as the nonce.
492        } else {
493            signer.nonce // Use signer nonce as the nonce.
494        };
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        // Check whether undelegate is allowed.
508        if params.disable_undelegate {
509            return Err(Error::Forbidden);
510        }
511        // Make sure receipts can only be requested internally (e.g. via subcalls).
512        if store_receipt && !CurrentState::with_env(|env| env.is_internal()) {
513            return Err(Error::InvalidArgument);
514        }
515
516        // Signer.
517        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 // Use receipt identifer as the nonce.
521        } else {
522            signer.nonce // Use signer nonce as the nonce.
523        };
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            // Transfer out failed, refund the balance.
637            <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            // Emit withdraw failed event.
645            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        // Burn the withdrawn tokens.
658        <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_WITHDRAWAL, &context.amount)
659            .expect("should have enough balance");
660
661        // Emit withdraw successful event.
662        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            // Transfer in failed, emit deposit failed event.
681            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        // Update runtime state.
694        <C::Runtime as Runtime>::Accounts::mint(context.address, &context.amount).unwrap();
695
696        // Emit deposit successful event.
697        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            // Delegation failed, refund the balance.
716            <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            // Store receipt if requested.
724            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            // Emit delegation failed event.
737            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        // Burn the delegated tokens.
750        <C::Runtime as Runtime>::Accounts::burn(*ADDRESS_PENDING_DELEGATION, &context.amount)
751            .expect("should have enough balance");
752
753        // Record delegation.
754        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        // Store receipt if requested.
763        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        // Emit delegation successful event.
776        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            // Undelegation failed, add shares back.
795            state::add_delegation(context.to, context.from, context.shares).unwrap();
796
797            // Store receipt if requested.
798            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            // Emit undelegation failed event.
811            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        // Queue undelegation processing at the debond end epoch. Further processing will happen in
825        // the end block handler.
826        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 // No receipt needed for UndelegateEnd.
836        };
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        // Store receipt if requested.
848        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        // Emit undelegation started event.
862        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        // Only do work in case the epoch has changed since the last processed block.
880        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            // Determine total amount the runtime got during the reclaim operation.
903            let (total_amount, total_shares) =
904                if let Some(totals) = reclaims.get(&(ud.epoch, ud.from)) {
905                    *totals
906                } else {
907                    // Fetch consensus height corresponding to the given epoch transition. This
908                    // query may be expensive in case the epoch is far back, but the node is
909                    // guaranteed to have it as it was the state after the last normal round
910                    // (otherwise we would have already processed this epoch).
911                    let height = Consensus::height_for_epoch(ctx, ud.epoch)
912                        .expect("failed to determine height for epoch");
913
914                    // Find the relevant reclaim escrow event.
915                    //
916                    // There will always be exactly one matching reclaim escrow event here, because
917                    // debonding delegations get merged at the consensus layer when there are
918                    // multiple reclaims for the same accounts on the same epoch.
919                    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            // Compute proportion of received amount (shares * total_amount / total_shares).
946            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            // Mint the given number of tokens.
956            <C::Runtime as Runtime>::Accounts::mint(ud.to, &amount).unwrap();
957
958            // Store receipt if requested.
959            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            // Emit undelegation done event.
972            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    /// Check invariants.
986    fn check_invariants<C: Context>(ctx: &C) -> Result<(), CoreError> {
987        // Total supply of the designated consensus layer token denomination
988        // should be less than or equal to the balance of the runtime's general
989        // account in the consensus layer.
990
991        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        // Check that the number of shares the runtime has escrowed in consensus is >= what is in
1017        // its internally tracked delegation state.
1018
1019        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}