Skip to content

Commit

Permalink
runtime-sdk: Add support for (un)delegation receipts
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Sep 23, 2023
1 parent a7a879a commit 7b21437
Show file tree
Hide file tree
Showing 16 changed files with 645 additions and 12 deletions.
15 changes: 15 additions & 0 deletions runtime-sdk/src/modules/consensus/mod.rs
Expand Up @@ -45,13 +45,20 @@ const MODULE_NAME: &str = "consensus";
pub struct Parameters {
pub consensus_denomination: token::Denomination,
pub consensus_scaling_factor: u64,

/// Minimum amount that is allowed to be delegated. This should be greater than or equal to what
/// is configured in the consensus layer as the consensus layer will do its own checks.
///
/// The amount is in consensus units.
pub min_delegate_amount: u128,
}

impl Default for Parameters {
fn default() -> Self {
Self {
consensus_denomination: token::Denomination::from_str("TEST").unwrap(),
consensus_scaling_factor: 1,
min_delegate_amount: 0,
}
}
}
Expand Down Expand Up @@ -119,6 +126,10 @@ pub enum Error {
#[sdk_error(code = 5)]
AmountNotRepresentable,

#[error("amount is lower than the minimum delegation amount")]
#[sdk_error(code = 6)]
UnderMinDelegationAmount,

#[error("history: {0}")]
#[sdk_error(transparent)]
History(#[from] history::Error),
Expand Down Expand Up @@ -278,6 +289,10 @@ impl API for Module {
Self::ensure_consensus_denomination(ctx, amount.denomination())?;
let amount = Self::amount_to_consensus(ctx, amount.amount())?;

if amount < Self::params().min_delegate_amount {
return Err(Error::UnderMinDelegationAmount);
}

ctx.emit_message(
Message::Staking(Versioned::new(
0,
Expand Down
42 changes: 41 additions & 1 deletion runtime-sdk/src/modules/consensus/test.rs
Expand Up @@ -19,7 +19,7 @@ use crate::{
},
};

use super::{Genesis, Parameters, API as _};
use super::{Error, Genesis, Parameters, API as _};

#[test]
fn test_api_transfer_invalid_denomination() {
Expand Down Expand Up @@ -276,6 +276,43 @@ fn test_api_escrow() {
});
}

#[test]
fn test_api_escrow_min_delegate_amount() {
let mut mock = mock::Mock::default();
let mut ctx = mock.create_ctx();

Consensus::set_params(Parameters {
min_delegate_amount: 10,
..Default::default()
});

ctx.with_tx(mock::transaction().into(), |mut tx_ctx, _call| {
let hook_name = "test_event_handler";
let amount = BaseUnits::new(5, Denomination::from_str("TEST").unwrap());
let result = Consensus::escrow(
&mut tx_ctx,
keys::alice::address(),
&amount,
MessageEventHookInvocation::new(hook_name.to_string(), 0),
);

assert!(matches!(result, Err(Error::UnderMinDelegationAmount)));
});

ctx.with_tx(mock::transaction().into(), |mut tx_ctx, _call| {
let hook_name = "test_event_handler";
let amount = BaseUnits::new(15, Denomination::from_str("TEST").unwrap());
let result = Consensus::escrow(
&mut tx_ctx,
keys::alice::address(),
&amount,
MessageEventHookInvocation::new(hook_name.to_string(), 0),
);

assert!(result.is_ok());
});
}

#[test]
fn test_api_escrow_scaling() {
let mut mock = mock::Mock::default();
Expand Down Expand Up @@ -430,6 +467,7 @@ fn test_query_parameters() {
let params = Parameters {
consensus_denomination: Denomination::NATIVE,
consensus_scaling_factor: 1_000,
min_delegate_amount: 10,
};
Consensus::set_params(params.clone());

Expand All @@ -445,6 +483,7 @@ fn test_init_bad_scaling_factor_1() {
consensus_denomination: Denomination::NATIVE,
// Zero scaling factor is invalid.
consensus_scaling_factor: 0,
min_delegate_amount: 0,
},
});
}
Expand All @@ -457,6 +496,7 @@ fn test_init_bad_scaling_factor_2() {
consensus_denomination: Denomination::NATIVE,
// Scaling factor that is not a power of 10 is invalid.
consensus_scaling_factor: 1230,
min_delegate_amount: 0,
},
});
}
141 changes: 134 additions & 7 deletions runtime-sdk/src/modules/consensus_accounts/mod.rs
Expand Up @@ -75,6 +75,11 @@ pub struct GasCosts {
pub tx_withdraw: u64,
pub tx_delegate: u64,
pub tx_undelegate: u64,

/// Cost of storing a delegation/undelegation receipt.
pub store_receipt: u64,
/// Cost of taking a delegation/undelegation receipt.
pub take_receipt: u64,
}

/// Parameters for the consensus module.
Expand Down Expand Up @@ -198,6 +203,7 @@ pub trait API {
nonce: u64,
to: Address,
amount: token::BaseUnits,
receipt: bool,
) -> Result<(), Error>;

/// Start the undelegation process of the given number of shares from consensus staking account
Expand All @@ -213,6 +219,7 @@ pub trait API {
nonce: u64,
to: Address,
shares: u128,
receipt: bool,
) -> Result<(), Error>;
}

Expand Down Expand Up @@ -309,6 +316,7 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API> API
nonce: u64,
to: Address,
amount: token::BaseUnits,
receipt: bool,
) -> Result<(), Error> {
Consensus::escrow(
ctx,
Expand All @@ -321,6 +329,7 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API> API
nonce,
to,
amount: amount.clone(),
receipt,

Check warning on line 332 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L332

Added line #L332 was not covered by tests
},
),
)?;
Expand All @@ -343,6 +352,7 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API> API
nonce: u64,
to: Address,
shares: u128,
receipt: bool,
) -> Result<(), Error> {
// Subtract shares from delegation, making sure there are enough there.
state::sub_delegation(to, from, shares)?;
Expand All @@ -358,6 +368,7 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API> API
nonce,
to,
shares,
receipt,

Check warning on line 371 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L371

Added line #L371 was not covered by tests
},
),
)?;
Expand Down Expand Up @@ -450,34 +461,77 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API>
fn tx_delegate<C: TxContext>(ctx: &mut C, body: types::Delegate) -> Result<(), Error> {
let params = Self::params();
<C::Runtime as Runtime>::Core::use_tx_gas(ctx, params.gas_costs.tx_delegate)?;
let store_receipt = body.receipt > 0;
if store_receipt {
<C::Runtime as Runtime>::Core::use_tx_gas(ctx, params.gas_costs.store_receipt)?;
}

// Check whether delegate is allowed.
if params.disable_delegate {
return Err(Error::Forbidden);
}
// Make sure receipts can only be requested internally (e.g. via subcalls).
if store_receipt && !ctx.is_internal() {
return Err(Error::InvalidArgument);
}

// Signer.
let signer = &ctx.tx_auth_info().signer_info[0];
let from = signer.address_spec.address();
let nonce = signer.nonce;
Self::delegate(ctx, from, nonce, body.to, body.amount)
let nonce = if store_receipt {
body.receipt // Use receipt identifier as the nonce.

Check warning on line 482 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L482

Added line #L482 was not covered by tests
} else {
signer.nonce // Use signer nonce as the nonce.
};
Self::delegate(ctx, from, nonce, body.to, body.amount, store_receipt)
}

#[handler(call = "consensus.Undelegate")]
fn tx_undelegate<C: TxContext>(ctx: &mut C, body: types::Undelegate) -> Result<(), Error> {
let params = Self::params();
<C::Runtime as Runtime>::Core::use_tx_gas(ctx, params.gas_costs.tx_undelegate)?;
let store_receipt = body.receipt > 0;
if store_receipt {
<C::Runtime as Runtime>::Core::use_tx_gas(ctx, params.gas_costs.store_receipt)?;
}

// Check whether undelegate is allowed.
if params.disable_undelegate {
return Err(Error::Forbidden);
}
// Make sure receipts can only be requested internally (e.g. via subcalls).
if store_receipt && !ctx.is_internal() {
return Err(Error::InvalidArgument);
}

// Signer.
let signer = &ctx.tx_auth_info().signer_info[0];
let to = signer.address_spec.address();
let nonce = signer.nonce;
Self::undelegate(ctx, body.from, nonce, to, body.shares)
let nonce = if store_receipt {
body.receipt // Use receipt identifer as the nonce.

Check warning on line 511 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L511

Added line #L511 was not covered by tests
} else {
signer.nonce // Use signer nonce as the nonce.
};
Self::undelegate(ctx, body.from, nonce, to, body.shares, store_receipt)
}

#[handler(call = "consensus.TakeReceipt", internal)]
fn internal_take_receipt<C: TxContext>(

Check warning on line 519 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L518-L519

Added lines #L518 - L519 were not covered by tests
ctx: &mut C,
body: types::TakeReceipt,
) -> Result<Option<types::Receipt>, Error> {
let params = Self::params();
<C::Runtime as Runtime>::Core::use_tx_gas(ctx, params.gas_costs.take_receipt)?;

Check warning on line 524 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L523-L524

Added lines #L523 - L524 were not covered by tests

if !body.kind.is_valid() {
return Err(Error::InvalidArgument);

Check warning on line 527 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L526-L527

Added lines #L526 - L527 were not covered by tests
}

Ok(state::take_receipt(
ctx.tx_caller_address(),
body.kind,
body.id,

Check warning on line 533 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L530-L533

Added lines #L530 - L533 were not covered by tests
))
}

#[handler(query = "consensus.Balance")]
Expand Down Expand Up @@ -615,6 +669,19 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API>
)
.expect("should have enough balance");

// Store receipt if requested.
if context.receipt {
state::set_receipt(
context.from,
types::ReceiptKind::Delegate,
context.nonce,
types::Receipt {
error: Some(me.clone().into()),
..Default::default()

Check warning on line 680 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L675-L680

Added lines #L675 - L680 were not covered by tests
},
);
}

// Emit delegation failed event.
ctx.emit_event(Event::Delegate {
from: context.from,
Expand All @@ -639,6 +706,19 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API>

state::add_delegation(context.from, context.to, shares).unwrap();

// Store receipt if requested.
if context.receipt {
state::set_receipt(
context.from,
types::ReceiptKind::Delegate,
context.nonce,
types::Receipt {
shares,
..Default::default()

Check warning on line 717 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L712-L717

Added lines #L712 - L717 were not covered by tests
},
);
}

// Emit delegation successful event.
ctx.emit_event(Event::Delegate {
from: context.from,
Expand All @@ -659,6 +739,19 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API>
// Undelegation failed, add shares back.
state::add_delegation(context.to, context.from, context.shares).unwrap();

// Store receipt if requested.
if context.receipt {
state::set_receipt(
context.to,
types::ReceiptKind::UndelegateStart,
context.nonce,
types::Receipt {
error: Some(me.clone().into()),
..Default::default()

Check warning on line 750 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L745-L750

Added lines #L745 - L750 were not covered by tests
},
);
}

// Emit undelegation failed event.
ctx.emit_event(Event::UndelegateStart {
from: context.from,
Expand All @@ -679,14 +772,34 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API>
let result: ReclaimEscrowResult = cbor::from_value(result).unwrap();
let debonding_shares = result.debonding_shares.try_into().unwrap();

let receipt = if context.receipt {
context.nonce

Check warning on line 776 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L776

Added line #L776 was not covered by tests
} else {
0 // No receipt needed for UndelegateEnd.
};

state::add_undelegation(
context.from,
context.to,
result.debond_end_time,
debonding_shares,
receipt,
)
.unwrap();

// Store receipt if requested.
if context.receipt {
state::set_receipt(
context.to,
types::ReceiptKind::UndelegateStart,
context.nonce,
types::Receipt {
epoch: result.debond_end_time,
..Default::default()

Check warning on line 798 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L793-L798

Added lines #L793 - L798 were not covered by tests
},
);
}

// Emit undelegation started event.
ctx.emit_event(Event::UndelegateStart {
from: context.from,
Expand Down Expand Up @@ -781,12 +894,26 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API> modul
.expect("shares * total_amount should not overflow")
.checked_div(total_shares)
.expect("total_shares should not be zero");
let amount = Consensus::amount_from_consensus(ctx, amount).unwrap();
let amount = token::BaseUnits::new(amount, denomination.clone());
let raw_amount = Consensus::amount_from_consensus(ctx, amount).unwrap();
let amount = token::BaseUnits::new(raw_amount, denomination.clone());

// Mint the given number of tokens.
Accounts::mint(ctx, ud.to, &amount).unwrap();

// Store receipt if requested.
if udi.receipt > 0 {
state::set_receipt(
ud.to,
types::ReceiptKind::UndelegateDone,
udi.receipt,
types::Receipt {
amount: raw_amount,
epoch: ud.epoch,
..Default::default()

Check warning on line 912 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L906-L912

Added lines #L906 - L912 were not covered by tests
},
);
}

// Emit undelegation done event.
ctx.emit_event(Event::UndelegateDone {
from: ud.from,
Expand Down Expand Up @@ -827,7 +954,7 @@ impl<Accounts: modules::accounts::API, Consensus: modules::consensus::API> modul
if let Some(total_supply) = ts.get(&den) {
if total_supply > &rt_ga_balance {
return Err(CoreError::InvariantViolation(
"total supply is greater than runtime's general account balance".to_string(),
format!("total supply ({total_supply}) is greater than runtime's general account balance ({rt_ga_balance})"),

Check warning on line 957 in runtime-sdk/src/modules/consensus_accounts/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus_accounts/mod.rs#L957

Added line #L957 was not covered by tests
));
}
}
Expand Down

0 comments on commit 7b21437

Please sign in to comment.