diff --git a/Cargo.lock b/Cargo.lock index 4fb776b9..84c71d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,7 @@ dependencies = [ name = "btc-finality" version = "0.10.0" dependencies = [ + "anybuf", "anyhow", "assert_matches", "babylon-apis", diff --git a/Cargo.toml b/Cargo.toml index 6e67aabe..069e1402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ cosmwasm-std = { version = "2.1.4", default-features = false, features = [ ] } cw2 = "2.0.0" cw-controllers = "2.0.0" -cw-multi-test = "2.0.1" +cw-multi-test = { version = "2.0.1", features = [ "staking", "cosmwasm_1_1", "cosmwasm_2_0" ] } cw-storage-plus = "2.0.0" cw-utils = "2.0.0" derivative = "2" diff --git a/contracts/btc-finality/Cargo.toml b/contracts/btc-finality/Cargo.toml index 1055e1f6..45e700ee 100644 --- a/contracts/btc-finality/Cargo.toml +++ b/contracts/btc-finality/Cargo.toml @@ -31,14 +31,16 @@ full-validation = [ "btc-staking/full-validation" ] [dependencies] babylon-apis = { path = "../../packages/apis" } babylon-bindings = { path = "../../packages/bindings" } -babylon-contract = { path = "../babylon", features = [ "library" ] } babylon-merkle = { path = "../../packages/merkle" } babylon-proto = { path = "../../packages/proto" } babylon-btcstaking = { path = "../../packages/btcstaking" } babylon-bitcoin = { path = "../../packages/bitcoin" } -btc-staking = { path = "../btc-staking", features = [ "library" ] } eots = { path = "../../packages/eots" } +babylon-contract = { path = "../babylon", features = [ "library" ] } +btc-staking = { path = "../btc-staking", features = [ "library" ] } + +anybuf = { workspace = true } bitcoin = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } diff --git a/contracts/btc-finality/src/contract.rs b/contracts/btc-finality/src/contract.rs index edd3d5ba..045e7913 100644 --- a/contracts/btc-finality/src/contract.rs +++ b/contracts/btc-finality/src/contract.rs @@ -30,7 +30,13 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result, ContractError> { nonpayable(&info)?; + let denom = deps.querier.query_bonded_denom()?; + + // Query blocks per year from the chain's mint module + let blocks_per_year = get_blocks_per_year(&mut deps)?; let config = Config { + denom, + blocks_per_year, babylon: info.sender, staking: Addr::unchecked("UNSET"), // To be set later, through `UpdateStaking` }; @@ -47,6 +53,33 @@ pub fn instantiate( Ok(Response::new().add_attribute("action", "instantiate")) } +/// Queries the chain's blocks per year using the mint Params Grpc query +fn get_blocks_per_year(deps: &mut DepsMut) -> Result { + let blocks_per_year; + #[cfg(any(test, all(feature = "library", not(target_arch = "wasm32"))))] + { + let _ = deps; + blocks_per_year = 60 * 60 * 24 * 365 / 6; // Default / hardcoded value for tests + } + #[cfg(not(any(test, all(feature = "library", not(target_arch = "wasm32")))))] + { + let res = deps.querier.query_grpc( + "/cosmos.mint.v1beta1.Query/Params".into(), + cosmwasm_std::Binary::new("".into()), + )?; + // Deserialize protobuf + let res_decoded = anybuf::Bufany::deserialize(&res).unwrap(); + // See https://github.com/cosmos/cosmos-sdk/blob/8bfcf554275c1efbb42666cc8510d2da139b67fa/proto/cosmos/mint/v1beta1/query.proto#L35-L36 + let res_params = res_decoded.message(1).unwrap(); + // See https://github.com/cosmos/cosmos-sdk/blob/8bfcf554275c1efbb42666cc8510d2da139b67fa/proto/cosmos/mint/v1beta1/mint.proto#L60-L61 + // to see from where the field number comes from + blocks_per_year = res_params + .uint64(6) + .ok_or(ContractError::MissingBlocksPerYear {})?; + } + Ok(blocks_per_year) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(_deps: DepsMut, _env: Env, _reply: Reply) -> StdResult { Ok(Response::default()) @@ -212,7 +245,10 @@ fn handle_end_block( let ev = finality::index_block(deps, env.block.height, &hex::decode(app_hash_hex)?)?; res = res.add_event(ev); // Tally all non-finalised blocks - let events = finality::tally_blocks(deps, activated_height, env.block.height)?; + let (msg, events) = finality::tally_blocks(deps, activated_height, env.block.height)?; + if let Some(msg) = msg { + res = res.add_message(msg); + } res = res.add_events(events); } Ok(res) diff --git a/contracts/btc-finality/src/error.rs b/contracts/btc-finality/src/error.rs index b96481aa..25e14128 100644 --- a/contracts/btc-finality/src/error.rs +++ b/contracts/btc-finality/src/error.rs @@ -94,4 +94,8 @@ pub enum ContractError { SecretKeyExtractionError(String), #[error("Hash length error: {0}")] WrongHashLength(String), + #[error("Blocks per year could not be queried from the mint module")] + MissingBlocksPerYear {}, + #[error("Division by zero")] + DivideByZero, } diff --git a/contracts/btc-finality/src/finality.rs b/contracts/btc-finality/src/finality.rs index 41a64fdd..13f6cce6 100644 --- a/contracts/btc-finality/src/finality.rs +++ b/contracts/btc-finality/src/finality.rs @@ -1,12 +1,6 @@ -use k256::ecdsa::signature::Verifier; -use k256::schnorr::{Signature, VerifyingKey}; -use k256::sha2::{Digest, Sha256}; -use std::cmp::max; -use std::collections::HashSet; - use crate::contract::encode_smart_query; use crate::error::ContractError; -use crate::state::config::{CONFIG, PARAMS}; +use crate::state::config::{Config, CONFIG, PARAMS}; use crate::state::finality::{BLOCKS, EVIDENCES, FP_SET, NEXT_HEIGHT, SIGNATURES, TOTAL_POWER}; use crate::state::public_randomness::{ get_last_pub_rand_commit, get_pub_rand_commit_for_height, PUB_RAND_COMMITS, PUB_RAND_VALUES, @@ -18,9 +12,15 @@ use babylon_merkle::Proof; use btc_staking::msg::{FinalityProviderInfo, FinalityProvidersByPowerResponse}; use cosmwasm_std::Order::Ascending; use cosmwasm_std::{ - to_json_binary, Addr, DepsMut, Env, Event, QuerierWrapper, Response, StdResult, Storage, - WasmMsg, + to_json_binary, Addr, Coin, Decimal, DepsMut, Env, Event, QuerierWrapper, Response, StdResult, + Storage, WasmMsg, }; +use k256::ecdsa::signature::Verifier; +use k256::schnorr::{Signature, VerifyingKey}; +use k256::sha2::{Digest, Sha256}; +use std::cmp::max; +use std::collections::HashSet; +use std::ops::Mul; pub fn handle_public_randomness_commit( deps: DepsMut, @@ -422,7 +422,7 @@ pub fn tally_blocks( deps: &mut DepsMut, activated_height: u64, height: u64, -) -> Result, ContractError> { +) -> Result<(Option, Vec), ContractError> { // Start finalising blocks since max(activated_height, next_height) let next_height = NEXT_HEIGHT.may_load(deps.storage)?.unwrap_or(0); let start_height = max(activated_height, next_height); @@ -437,6 +437,7 @@ pub fn tally_blocks( // After this for loop, the blocks since the earliest activated height are either finalised or // non-finalisable let mut events = vec![]; + let mut finalized_blocks = 0; for h in start_height..=height { let mut indexed_block = BLOCKS.load(deps.storage, h)?; // Get the finality provider set of this block @@ -452,6 +453,7 @@ pub fn tally_blocks( if tally(&fp_set, &voter_btc_pks) { // If this block gets >2/3 votes, finalise it let ev = finalize_block(deps.storage, &mut indexed_block, &voter_btc_pks)?; + finalized_blocks += 1; events.push(ev); } else { // If not, then this block and all subsequent blocks should not be finalised. @@ -480,7 +482,21 @@ pub fn tally_blocks( } } } - Ok(events) + + // Compute block rewards for finalized blocks + let msg = if finalized_blocks > 0 { + let cfg = CONFIG.load(deps.storage)?; + let rewards = compute_block_rewards(deps, &cfg, finalized_blocks)?; + // Assemble mint message + let mint_msg = BabylonMsg::MintRewards { + amount: rewards, + recipient: cfg.staking.into(), + }; + Some(mint_msg) + } else { + None + }; + Ok((msg, events)) } /// `tally` checks whether a block with the given finality provider set and votes reaches a quorum @@ -521,6 +537,31 @@ fn finalize_block( Ok(ev) } +/// `compute_block_rewards` computes the block rewards for the finality providers +fn compute_block_rewards( + deps: &mut DepsMut, + cfg: &Config, + finalized_blocks: u64, +) -> Result { + // Get the total supply (standard bank query) + let total_supply = deps.querier.query_supply(cfg.denom.clone())?; + + // Get the finality inflation rate (params) + let finality_inflation_rate = PARAMS.load(deps.storage)?.finality_inflation_rate; + + // Compute the block rewards for the finalized blocks + let inv_blocks_per_year = Decimal::from_ratio(1u128, cfg.blocks_per_year); + let block_rewards = finality_inflation_rate + .mul(Decimal::from_ratio(total_supply.amount, 1u128)) + .mul(inv_blocks_per_year) + .mul(Decimal::from_ratio(finalized_blocks, 1u128)); + + Ok(Coin { + denom: cfg.denom.clone(), + amount: block_rewards.to_uint_floor(), + }) +} + const QUERY_LIMIT: Option = Some(30); /// `compute_active_finality_providers` sorts all finality providers, counts the total voting diff --git a/contracts/btc-finality/src/multitest.rs b/contracts/btc-finality/src/multitest.rs index d83a441d..f83f4bec 100644 --- a/contracts/btc-finality/src/multitest.rs +++ b/contracts/btc-finality/src/multitest.rs @@ -41,7 +41,7 @@ mod finality { use babylon_apis::finality_api::IndexedBlock; use test_utils::get_public_randomness_commitment; - use cosmwasm_std::Event; + use cosmwasm_std::{coin, Event}; use test_utils::{ create_new_finality_provider, get_add_finality_sig, get_derived_btc_delegation, get_pub_rand_value, @@ -78,8 +78,12 @@ mod finality { let proof = add_finality_signature.proof.unwrap(); let initial_height = pub_rand.start_height; + let initial_funds = &[coin(1_000_000, "TOKEN")]; - let mut suite = SuiteBuilder::new().with_height(initial_height).build(); + let mut suite = SuiteBuilder::new() + .with_height(initial_height) + .with_funds(initial_funds) + .build(); // Register one FP // NOTE: the test data ensures that pub rand commit / finality sig are @@ -166,8 +170,12 @@ mod finality { let proof = add_finality_signature.proof.unwrap(); let initial_height = pub_rand.start_height; + let initial_funds = &[coin(1_000_000_000_000, "TOKEN")]; - let mut suite = SuiteBuilder::new().with_height(initial_height).build(); + let mut suite = SuiteBuilder::new() + .with_funds(initial_funds) + .with_height(initial_height) + .build(); // signed by the 1st FP let new_fp = create_new_finality_provider(1); @@ -260,6 +268,7 @@ mod finality { mod slashing { use babylon_apis::finality_api::IndexedBlock; + use cosmwasm_std::coin; use test_utils::{ create_new_finality_provider, get_add_finality_sig, get_add_finality_sig_2, get_derived_btc_delegation, get_pub_rand_value, @@ -278,8 +287,12 @@ mod slashing { let proof = add_finality_signature.proof.unwrap(); let initial_height = pub_rand.start_height; + let initial_funds = &[coin(10_000_000_000_000, "TOKEN")]; - let mut suite = SuiteBuilder::new().with_height(initial_height).build(); + let mut suite = SuiteBuilder::new() + .with_funds(initial_funds) + .with_height(initial_height) + .build(); // Register one FP // NOTE: the test data ensures that pub rand commit / finality sig are diff --git a/contracts/btc-finality/src/multitest/suite.rs b/contracts/btc-finality/src/multitest/suite.rs index 49ba1108..ca0fcb30 100644 --- a/contracts/btc-finality/src/multitest/suite.rs +++ b/contracts/btc-finality/src/multitest/suite.rs @@ -2,7 +2,7 @@ use anyhow::Result as AnyResult; use derivative::Derivative; use hex::ToHex; -use cosmwasm_std::{to_json_binary, Addr}; +use cosmwasm_std::{to_json_binary, Addr, Coin}; use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; @@ -52,6 +52,7 @@ fn contract_babylon() -> Box> { #[derivative(Default = "new")] pub struct SuiteBuilder { height: Option, + init_funds: Vec, } impl SuiteBuilder { @@ -60,6 +61,11 @@ impl SuiteBuilder { self } + pub fn with_funds(mut self, funds: &[Coin]) -> Self { + self.init_funds = funds.to_vec(); + self + } + #[track_caller] pub fn build(self) -> Suite { let owner = Addr::unchecked("owner"); @@ -68,8 +74,10 @@ impl SuiteBuilder { let _block_info = app.block_info(); - app.init_modules(|_router, _api, _storage| -> AnyResult<()> { Ok(()) }) - .unwrap(); + app.init_modules(|router, _api, storage| -> AnyResult<()> { + router.bank.init_balance(storage, &owner, self.init_funds) + }) + .unwrap(); let btc_staking_code_id = app.store_code_with_creator(owner.clone(), contract_btc_staking()); diff --git a/contracts/btc-finality/src/state/config.rs b/contracts/btc-finality/src/state/config.rs index 77c18b6b..60a2f342 100644 --- a/contracts/btc-finality/src/state/config.rs +++ b/contracts/btc-finality/src/state/config.rs @@ -1,7 +1,7 @@ use derivative::Derivative; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, Decimal}; use cw_controllers::Admin; use cw_storage_plus::Item; @@ -15,6 +15,8 @@ pub(crate) const ADMIN: Admin = Admin::new("admin"); // TODO: Add / enable config entries as needed #[cw_serde] pub struct Config { + pub denom: String, + pub blocks_per_year: u64, pub babylon: Addr, pub staking: Addr, } @@ -32,4 +34,7 @@ pub struct Params { /// should commit #[derivative(Default(value = "1"))] pub min_pub_rand: u64, + /// `finality_inflation_rate` is the inflation rate for finality providers' block rewards + #[derivative(Default(value = "Decimal::permille(35)"))] // 3.5 % by default + pub finality_inflation_rate: Decimal, } diff --git a/packages/bindings-test/src/multitest.rs b/packages/bindings-test/src/multitest.rs index 731cca46..64653de2 100644 --- a/packages/bindings-test/src/multitest.rs +++ b/packages/bindings-test/src/multitest.rs @@ -88,6 +88,10 @@ impl Module for BabylonModule { // FIXME? We don't do anything here Ok(AppResponse::default()) } + BabylonMsg::MintRewards { .. } => { + // FIXME? We don't do anything here + Ok(AppResponse::default()) + } } } diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 10b0f3a9..c05c963b 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -4,7 +4,7 @@ //! - FinalizedHeader: reporting a BTC-finalised header. use cosmwasm_schema::cw_serde; -use cosmwasm_std::{CosmosMsg, Empty}; +use cosmwasm_std::{Coin, CosmosMsg, Empty}; /// BabylonMsg is the message that the Babylon contract can send to the Cosmos zone. /// The Cosmos zone has to integrate https://github.com/babylonlabs-io/wasmbinding for @@ -17,6 +17,11 @@ pub enum BabylonMsg { height: i64, time: i64, // NOTE: UNIX timestamp is in i64 }, + /// MintRewards mints the requested block rewards for the finality providers. + /// It can only be sent from the finality contract. + /// The rewards are minted to the staking contract address, so that they + /// can be distributed across the active finality provider set + MintRewards { amount: Coin, recipient: String }, } pub type BabylonSudoMsg = Empty;