From 4f0080019a8019919a425178ade715fc404f4456 Mon Sep 17 00:00:00 2001 From: haiyizxx Date: Wed, 14 Feb 2024 10:45:53 -0500 Subject: [PATCH] fix(minor-rewards)!: handle signing rewards for different chains (#253) * fix(minor-rewards)!: handle signing rewards for different chains The rewards contract can not distinguish signing events for different chains. All signing events are added to a single tally, since all signing events are handled by one multisig contract. This pr associates rewards with a (chain name, contract address) pair. * address comments * lint and fix test --- Cargo.lock | 1 + contracts/multisig/src/contract.rs | 65 ++++- contracts/multisig/src/contract/execute.rs | 25 +- contracts/multisig/src/signing.rs | 29 +- contracts/rewards/Cargo.toml | 1 + contracts/rewards/src/contract.rs | 54 ++-- contracts/rewards/src/contract/execute.rs | 304 ++++++++++++--------- contracts/rewards/src/lib.rs | 2 +- contracts/rewards/src/msg.rs | 12 +- contracts/rewards/src/state.rs | 258 +++++++++++------ contracts/voting-verifier/src/execute.rs | 1 + integration-tests/tests/message_routing.rs | 11 +- integration-tests/tests/test_utils/mod.rs | 54 ++-- 13 files changed, 525 insertions(+), 292 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e036046ad..536cda712 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6445,6 +6445,7 @@ version = "0.1.0" dependencies = [ "axelar-wasm-std", "axelar-wasm-std-derive", + "connection-router", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", diff --git a/contracts/multisig/src/contract.rs b/contracts/multisig/src/contract.rs index c343de0d7..4cd0608ec 100644 --- a/contracts/multisig/src/contract.rs +++ b/contracts/multisig/src/contract.rs @@ -108,6 +108,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { #[cfg(test)] mod tests { + use serde_json::from_str; use std::vec; use crate::{ @@ -121,14 +122,13 @@ mod tests { }; use super::*; + use connection_router::state::ChainName; use cosmwasm_std::{ from_binary, testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, Addr, Empty, OwnedDeps, Uint256, WasmMsg, }; - use serde_json::from_str; - const INSTANTIATOR: &str = "inst"; const PROVER: &str = "prover"; const REWARDS_CONTRACT: &str = "rewards"; @@ -183,6 +183,7 @@ mod tests { deps: DepsMut, sender: &str, worker_set_id: &str, + chain_name: ChainName, ) -> Result { let info = mock_info(sender, &[]); let env = mock_env(); @@ -191,7 +192,7 @@ mod tests { let msg = ExecuteMsg::StartSigningSession { worker_set_id: worker_set_id.to_string(), msg: message.clone(), - chain_name: "Ethereum".to_string().try_into().unwrap(), + chain_name, sig_verifier: None, }; execute(deps, env, info, msg) @@ -369,7 +370,12 @@ mod tests { .into_iter() .enumerate() { - let res = do_start_signing_session(deps.as_mut(), PROVER, &subkey); + let res = do_start_signing_session( + deps.as_mut(), + PROVER, + &subkey, + "mock-chain".parse().unwrap(), + ); assert!(res.is_ok()); @@ -423,7 +429,12 @@ mod tests { let sender = "someone else"; for worker_set_id in [ecdsa_subkey, ed25519_subkey] { - let res = do_start_signing_session(deps.as_mut(), sender, &worker_set_id); + let res = do_start_signing_session( + deps.as_mut(), + sender, + &worker_set_id, + "mock-chain".parse().unwrap(), + ); assert_eq!( res.unwrap_err().to_string(), @@ -438,16 +449,20 @@ mod tests { let (mut deps, ecdsa_subkey, ed25519_subkey) = setup(); do_authorize_caller(deps.as_mut(), Addr::unchecked(PROVER)).unwrap(); + let chain_name: ChainName = "mock-chain".parse().unwrap(); + for (key_type, worker_set_id, signers, session_id) in signature_test_data(&ecdsa_subkey, &ed25519_subkey) { - do_start_signing_session(deps.as_mut(), PROVER, worker_set_id).unwrap(); + do_start_signing_session(deps.as_mut(), PROVER, worker_set_id, chain_name.clone()) + .unwrap(); let signer = signers.get(0).unwrap().to_owned(); let expected_rewards_msg = WasmMsg::Execute { contract_addr: REWARDS_CONTRACT.to_string(), msg: to_binary(&rewards::msg::ExecuteMsg::RecordParticipation { + chain_name: chain_name.clone(), event_id: session_id.to_string().try_into().unwrap(), worker_address: signer.address.clone().into(), }) @@ -505,7 +520,8 @@ mod tests { for (key_type, subkey, signers, session_id) in signature_test_data(&ecdsa_subkey, &ed25519_subkey) { - do_start_signing_session(deps.as_mut(), PROVER, subkey).unwrap(); + do_start_signing_session(deps.as_mut(), PROVER, subkey, "mock-chain".parse().unwrap()) + .unwrap(); let signer = signers.get(0).unwrap().to_owned(); do_sign(deps.as_mut(), mock_env(), session_id, &signer).unwrap(); @@ -554,10 +570,12 @@ mod tests { let (mut deps, ecdsa_subkey, ed25519_subkey) = setup(); do_authorize_caller(deps.as_mut(), Addr::unchecked(PROVER)).unwrap(); + let chain_name: ChainName = "mock-chain".parse().unwrap(); + for (_key_type, subkey, signers, session_id) in signature_test_data(&ecdsa_subkey, &ed25519_subkey) { - do_start_signing_session(deps.as_mut(), PROVER, subkey).unwrap(); + do_start_signing_session(deps.as_mut(), PROVER, subkey, chain_name.clone()).unwrap(); let signer = signers.get(0).unwrap().to_owned(); do_sign(deps.as_mut(), mock_env(), session_id, &signer).unwrap(); @@ -572,6 +590,7 @@ mod tests { let expected_rewards_msg = WasmMsg::Execute { contract_addr: REWARDS_CONTRACT.to_string(), msg: to_binary(&rewards::msg::ExecuteMsg::RecordParticipation { + chain_name: chain_name.clone(), event_id: session_id.to_string().try_into().unwrap(), worker_address: signer.address.clone().into(), }) @@ -603,7 +622,8 @@ mod tests { for (_key_type, subkey, signers, session_id) in signature_test_data(&ecdsa_subkey, &ed25519_subkey) { - do_start_signing_session(deps.as_mut(), PROVER, subkey).unwrap(); + do_start_signing_session(deps.as_mut(), PROVER, subkey, "mock-chain".parse().unwrap()) + .unwrap(); let signer = signers.get(0).unwrap().to_owned(); do_sign(deps.as_mut(), mock_env(), session_id, &signer).unwrap(); @@ -621,7 +641,7 @@ mod tests { assert_eq!( res.unwrap_err().to_string(), axelar_wasm_std::ContractError::from(ContractError::SigningSessionClosed { - session_id: session_id + session_id }) .to_string() ) @@ -632,7 +652,13 @@ mod tests { fn submit_signature_wrong_session_id() { let (mut deps, ecdsa_subkey, _) = setup(); do_authorize_caller(deps.as_mut(), Addr::unchecked(PROVER)).unwrap(); - do_start_signing_session(deps.as_mut(), PROVER, &ecdsa_subkey).unwrap(); + do_start_signing_session( + deps.as_mut(), + PROVER, + &ecdsa_subkey, + "mock-chain".parse().unwrap(), + ) + .unwrap(); let invalid_session_id = Uint64::zero(); let signer = ecdsa_test_data::signers().get(0).unwrap().to_owned(); @@ -655,7 +681,8 @@ mod tests { for (_key_type, subkey, signers, session_id) in signature_test_data(&ecdsa_subkey, &ed25519_subkey) { - do_start_signing_session(deps.as_mut(), PROVER, subkey).unwrap(); + do_start_signing_session(deps.as_mut(), PROVER, subkey, "mock-chain".parse().unwrap()) + .unwrap(); do_sign( deps.as_mut(), @@ -942,7 +969,12 @@ mod tests { do_authorize_caller(deps.as_mut(), Addr::unchecked(PROVER)).unwrap(); for worker_set_id in [ecdsa_subkey.clone(), ed25519_subkey.clone()] { - let res = do_start_signing_session(deps.as_mut(), PROVER, &worker_set_id); + let res = do_start_signing_session( + deps.as_mut(), + PROVER, + &worker_set_id, + "mock-chain".parse().unwrap(), + ); assert!(res.is_ok()); } @@ -950,7 +982,12 @@ mod tests { // unauthorize do_unauthorize_caller(deps.as_mut(), Addr::unchecked(PROVER)).unwrap(); for worker_set_id in [ecdsa_subkey, ed25519_subkey] { - let res = do_start_signing_session(deps.as_mut(), PROVER, &worker_set_id); + let res = do_start_signing_session( + deps.as_mut(), + PROVER, + &worker_set_id, + "mock-chain".parse().unwrap(), + ); assert_eq!( res.unwrap_err().to_string(), diff --git a/contracts/multisig/src/contract/execute.rs b/contracts/multisig/src/contract/execute.rs index 5991e7ddf..1dbc0cee2 100644 --- a/contracts/multisig/src/contract/execute.rs +++ b/contracts/multisig/src/contract/execute.rs @@ -34,8 +34,13 @@ pub fn start_signing_session( let expires_at = env.block.height + config.block_expiry; - let signing_session = - SigningSession::new(session_id, worker_set_id.clone(), msg.clone(), expires_at); + let signing_session = SigningSession::new( + session_id, + worker_set_id.clone(), + chain_name.clone(), + msg.clone(), + expires_at, + ); SIGNING_SESSIONS.save(deps.storage, session_id.into(), &signing_session)?; @@ -95,8 +100,7 @@ pub fn submit_signature( let state_changed = old_state != session.state; signing_response( - session_id, - session.state, + session, state_changed, info.sender, signature, @@ -175,8 +179,7 @@ pub fn require_governance(deps: &DepsMut, sender: Addr) -> Result<(), ContractEr } fn signing_response( - session_id: Uint64, - session_state: MultisigState, + session: SigningSession, state_changed: bool, signer: Addr, signature: Signature, @@ -185,7 +188,9 @@ fn signing_response( let rewards_msg = WasmMsg::Execute { contract_addr: rewards_contract, msg: to_binary(&rewards::msg::ExecuteMsg::RecordParticipation { - event_id: session_id + chain_name: session.chain_name, + event_id: session + .id .to_string() .try_into() .expect("couldn't convert session_id to nonempty string"), @@ -195,7 +200,7 @@ fn signing_response( }; let event = Event::SignatureSubmitted { - session_id, + session_id: session.id, participant: signer, signature, }; @@ -204,12 +209,12 @@ fn signing_response( .add_message(rewards_msg) .add_event(event.into()); - if let MultisigState::Completed { completed_at } = session_state { + if let MultisigState::Completed { completed_at } = session.state { if state_changed { // only send event if state changed response = response.add_event( Event::SigningCompleted { - session_id, + session_id: session.id, completed_at, } .into(), diff --git a/contracts/multisig/src/signing.rs b/contracts/multisig/src/signing.rs index cc2063cf6..65b83d608 100644 --- a/contracts/multisig/src/signing.rs +++ b/contracts/multisig/src/signing.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use connection_router::state::ChainName; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint256, Uint64}; @@ -14,16 +15,24 @@ use crate::{ pub struct SigningSession { pub id: Uint64, pub worker_set_id: String, + pub chain_name: ChainName, pub msg: MsgToSign, pub state: MultisigState, pub expires_at: u64, } impl SigningSession { - pub fn new(session_id: Uint64, worker_set_id: String, msg: MsgToSign, expires_at: u64) -> Self { + pub fn new( + session_id: Uint64, + worker_set_id: String, + chain_name: ChainName, + msg: MsgToSign, + expires_at: u64, + ) -> Self { Self { id: session_id, worker_set_id, + chain_name, msg, state: MultisigState::Pending, expires_at, @@ -113,8 +122,13 @@ mod tests { let message: MsgToSign = ecdsa_test_data::message().try_into().unwrap(); let expires_at = 12345; - let session = - SigningSession::new(Uint64::one(), worker_set_id, message.clone(), expires_at); + let session = SigningSession::new( + Uint64::one(), + worker_set_id, + "mock-chain".parse().unwrap(), + message.clone(), + expires_at, + ); let signatures: HashMap = signers .iter() @@ -146,8 +160,13 @@ mod tests { let message: MsgToSign = ed25519_test_data::message().try_into().unwrap(); let expires_at = 12345; - let session = - SigningSession::new(Uint64::one(), worker_set_id, message.clone(), expires_at); + let session = SigningSession::new( + Uint64::one(), + worker_set_id, + "mock-chain".parse().unwrap(), + message.clone(), + expires_at, + ); let signatures: HashMap = signers .iter() diff --git a/contracts/rewards/Cargo.toml b/contracts/rewards/Cargo.toml index e922f5888..b5a42d959 100644 --- a/contracts/rewards/Cargo.toml +++ b/contracts/rewards/Cargo.toml @@ -35,6 +35,7 @@ optimize = """docker run --rm -v "$(pwd)":/code \ [dependencies] axelar-wasm-std = { workspace = true } axelar-wasm-std-derive = { workspace = true } +connection-router = { workspace = true, features = ["library"] } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/rewards/src/contract.rs b/contracts/rewards/src/contract.rs index 211a6b7e4..86fcc3ef1 100644 --- a/contracts/rewards/src/contract.rs +++ b/contracts/rewards/src/contract.rs @@ -1,16 +1,17 @@ -use crate::{ - contract::execute::Contract, - error::ContractError, - msg::{ExecuteMsg, InstantiateMsg}, - state::{Config, Epoch, StoredParams, CONFIG, PARAMS}, -}; +use itertools::Itertools; + use axelar_wasm_std::nonempty; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, MessageInfo, Response}; use error_stack::ResultExt; -use itertools::Itertools; +use crate::{ + contract::execute::Contract, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg}, + state::{Config, Epoch, PoolId, StoredParams, CONFIG, PARAMS}, +}; mod execute; @@ -54,18 +55,24 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::RecordParticipation { + chain_name, event_id, worker_address, } => { let worker_address = deps.api.addr_validate(&worker_address)?; + let pool_id = PoolId { + chain_name, + contract: info.sender.clone(), + }; Contract::new(deps) - .record_participation(event_id, worker_address, info.sender, env.block.height) + .record_participation(event_id, worker_address, pool_id, env.block.height) .map_err(axelar_wasm_std::ContractError::from)?; Ok(Response::new()) } - ExecuteMsg::AddRewards { contract_address } => { - let contract_address = deps.api.addr_validate(&contract_address)?; + ExecuteMsg::AddRewards { pool_id } => { + deps.api.addr_validate(pool_id.contract.as_str())?; + let mut contract = Contract::new(deps); let amount = info .funds @@ -76,20 +83,21 @@ pub fn execute( .amount; contract.add_rewards( - contract_address, + pool_id, nonempty::Uint128::try_from(amount).change_context(ContractError::ZeroRewards)?, )?; Ok(Response::new()) } ExecuteMsg::DistributeRewards { - contract_address, + pool_id, epoch_count, } => { - let contract_address = deps.api.addr_validate(&contract_address)?; + deps.api.addr_validate(pool_id.contract.as_str())?; + let mut contract = Contract::new(deps); let rewards = contract - .distribute_rewards(contract_address, env.block.height, epoch_count) + .distribute_rewards(pool_id, env.block.height, epoch_count) .map_err(axelar_wasm_std::ContractError::from)?; let msgs = rewards @@ -115,22 +123,26 @@ pub fn execute( #[cfg(test)] mod tests { + use connection_router::state::ChainName; use cosmwasm_std::{coins, Addr, Binary, BlockInfo, Deps, Env, StdResult, Uint128}; use cw_multi_test::{App, ContractWrapper, Executor}; use crate::msg::{ExecuteMsg, InstantiateMsg, Params, QueryMsg}; + use crate::state::PoolId; use super::{execute, instantiate}; /// Tests that the contract entry points (instantiate and execute) work as expected. /// Instantiates the contract and calls each of the 4 ExecuteMsg variants. - /// Adds rewards to the contract, updates the rewards params, records some participation + /// Adds rewards to the pool, updates the rewards params, records some participation /// events and then distributes the rewards. #[test] fn test_rewards_flow() { + let chain_name: ChainName = "mock-chain".parse().unwrap(); let user = Addr::unchecked("user"); let worker = Addr::unchecked("worker"); let worker_contract = Addr::unchecked("worker contract"); + const AXL_DENOMINATION: &str = "uaxl"; let mut app = App::new(|router, _, storage| { router @@ -174,7 +186,10 @@ mod tests { user.clone(), contract_address.clone(), &ExecuteMsg::AddRewards { - contract_address: worker_contract.to_string(), + pool_id: PoolId { + chain_name: chain_name.clone(), + contract: worker_contract.clone(), + }, }, &coins(200, AXL_DENOMINATION), ); @@ -197,6 +212,7 @@ mod tests { worker_contract.clone(), contract_address.clone(), &ExecuteMsg::RecordParticipation { + chain_name: chain_name.clone(), event_id: "some event".to_string().try_into().unwrap(), worker_address: worker.to_string(), }, @@ -208,6 +224,7 @@ mod tests { worker_contract.clone(), contract_address.clone(), &ExecuteMsg::RecordParticipation { + chain_name: chain_name.clone(), event_id: "some other event".to_string().try_into().unwrap(), worker_address: worker.to_string(), }, @@ -226,7 +243,10 @@ mod tests { user, contract_address.clone(), &ExecuteMsg::DistributeRewards { - contract_address: worker_contract.to_string(), + pool_id: PoolId { + chain_name: chain_name.clone(), + contract: worker_contract.clone(), + }, epoch_count: None, }, &[], diff --git a/contracts/rewards/src/contract/execute.rs b/contracts/rewards/src/contract/execute.rs index d89d0ed44..848ab652d 100644 --- a/contracts/rewards/src/contract/execute.rs +++ b/contracts/rewards/src/contract/execute.rs @@ -1,13 +1,15 @@ +use std::collections::HashMap; + use axelar_wasm_std::{nonempty, FnExt}; use cosmwasm_std::{Addr, DepsMut, Uint128}; use error_stack::Result; -use std::collections::HashMap; use crate::{ error::ContractError, msg::Params, state::{ - Config, Epoch, EpochTally, Event, RewardsStore, StorageState, Store, StoredParams, CONFIG, + Config, Epoch, EpochTally, Event, PoolId, RewardsStore, StorageState, Store, StoredParams, + CONFIG, }, }; @@ -71,18 +73,17 @@ where &mut self, event_id: nonempty::String, worker: Addr, - target_contract: Addr, + pool_id: PoolId, block_height: u64, ) -> Result<(), ContractError> { let cur_epoch = self.current_epoch(block_height)?; - let event = - self.load_or_store_event(event_id, target_contract.clone(), cur_epoch.epoch_num)?; + let event = self.load_or_store_event(event_id, pool_id.clone(), cur_epoch.epoch_num)?; self.store - .load_epoch_tally(target_contract.clone(), event.epoch_num)? + .load_epoch_tally(pool_id.clone(), event.epoch_num)? .unwrap_or(EpochTally::new( - target_contract, + pool_id, cur_epoch, self.store.load_params().params, )) @@ -98,16 +99,16 @@ where fn load_or_store_event( &mut self, event_id: nonempty::String, - target_contract: Addr, + pool_id: PoolId, cur_epoch_num: u64, ) -> Result, ContractError> { let event = self .store - .load_event(event_id.to_string(), target_contract.clone())?; + .load_event(event_id.to_string(), pool_id.clone())?; match event { None => { - let event = Event::new(event_id, target_contract, cur_epoch_num); + let event = Event::new(event_id, pool_id, cur_epoch_num); self.store.save_event(&event)?; Ok(StorageState::New(event)) } @@ -117,7 +118,7 @@ where pub fn distribute_rewards( &mut self, - target_contract: Addr, + pool_id: PoolId, cur_block_height: u64, epoch_process_limit: Option, ) -> Result, ContractError> { @@ -126,7 +127,7 @@ where let from = self .store - .load_rewards_watermark(target_contract.clone())? + .load_rewards_watermark(pool_id.clone())? .map_or(0, |last_processed| last_processed + 1); let to = std::cmp::min( @@ -138,46 +139,41 @@ where return Err(ContractError::NoRewardsToDistribute.into()); } - let rewards = self.process_rewards_for_epochs(target_contract.clone(), from, to)?; - self.store.save_rewards_watermark(target_contract, to)?; + let rewards = self.process_rewards_for_epochs(pool_id.clone(), from, to)?; + self.store.save_rewards_watermark(pool_id, to)?; Ok(rewards) } fn process_rewards_for_epochs( &mut self, - target_contract: Addr, + pool_id: PoolId, from: u64, to: u64, ) -> Result, ContractError> { - let rewards = self.cumulate_rewards(&target_contract, from, to); + let rewards = self.cumulate_rewards(&pool_id, from, to); self.store - .load_rewards_pool(target_contract.clone())? + .load_rewards_pool(pool_id.clone())? .sub_reward(rewards.values().sum())? .then(|pool| self.store.save_rewards_pool(&pool))?; Ok(rewards) } - fn cumulate_rewards( - &mut self, - target_contract: &Addr, - from: u64, - to: u64, - ) -> HashMap { - self.iterate_epoch_tallies(target_contract, from, to) + fn cumulate_rewards(&mut self, pool_id: &PoolId, from: u64, to: u64) -> HashMap { + self.iterate_epoch_tallies(pool_id, from, to) .map(|tally| tally.rewards_by_worker()) .fold(HashMap::new(), merge_rewards) } fn iterate_epoch_tallies<'a>( &'a mut self, - target_contract: &'a Addr, + pool_id: &'a PoolId, from: u64, to: u64, ) -> impl Iterator + 'a { (from..=to).filter_map(|epoch_num| { self.store - .load_epoch_tally(target_contract.clone(), epoch_num) + .load_epoch_tally(pool_id.clone(), epoch_num) .unwrap_or_default() }) } @@ -215,10 +211,10 @@ where pub fn add_rewards( &mut self, - contract: Addr, + pool_id: PoolId, amount: nonempty::Uint128, ) -> Result<(), ContractError> { - let mut pool = self.store.load_rewards_pool(contract.clone())?; + let mut pool = self.store.load_rewards_pool(pool_id)?; pool.balance += Uint128::from(amount); self.store.save_rewards_pool(&pool)?; @@ -252,12 +248,16 @@ mod test { }; use axelar_wasm_std::nonempty; + use connection_router::state::ChainName; use cosmwasm_std::{Addr, Uint128, Uint64}; use crate::{ error::ContractError, msg::Params, - state::{self, Config, Epoch, EpochTally, Event, RewardsPool, Store, StoredParams}, + state::{ + self, Config, Epoch, EpochTally, Event, PoolId, RewardsPool, Store, StoredParams, + TallyId, + }, }; use super::Contract; @@ -338,7 +338,7 @@ mod test { } } - /// Tests that multiple participation events for the same contract within a given epoch are recorded correctly + /// Tests that multiple participation events for the same pool within a given epoch are recorded correctly #[test] fn record_participation_multiple_events() { let cur_epoch_num = 1u64; @@ -347,7 +347,10 @@ mod test { let mut contract = setup(cur_epoch_num, epoch_block_start, epoch_duration); - let worker_contract = Addr::unchecked("some contract"); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; let mut simulated_participation = HashMap::new(); simulated_participation.insert(Addr::unchecked("worker_1"), 10); @@ -362,12 +365,7 @@ mod test { if i < *part_count { let event_id = i.to_string().try_into().unwrap(); contract - .record_participation( - event_id, - worker.clone(), - worker_contract.clone(), - cur_height, - ) + .record_participation(event_id, worker.clone(), pool_id.clone(), cur_height) .unwrap(); } } @@ -376,7 +374,7 @@ mod test { let tally = contract .store - .load_epoch_tally(worker_contract, cur_epoch_num) + .load_epoch_tally(pool_id, cur_epoch_num) .unwrap(); assert!(tally.is_some()); @@ -400,7 +398,10 @@ mod test { let mut contract = setup(starting_epoch_num, block_height_started, epoch_duration); - let worker_contract = Addr::unchecked("some contract"); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; let workers = vec![ Addr::unchecked("worker_1"), @@ -415,7 +416,7 @@ mod test { .record_participation( "some event".to_string().try_into().unwrap(), workers.clone(), - worker_contract.clone(), + pool_id.clone(), height_at_epoch_end + i as u64, ) .unwrap(); @@ -426,7 +427,7 @@ mod test { let tally = contract .store - .load_epoch_tally(worker_contract.clone(), starting_epoch_num) + .load_epoch_tally(pool_id.clone(), starting_epoch_num) .unwrap(); assert!(tally.is_some()); @@ -440,12 +441,12 @@ mod test { let tally = contract .store - .load_epoch_tally(worker_contract, starting_epoch_num + 1) + .load_epoch_tally(pool_id, starting_epoch_num + 1) .unwrap(); assert!(tally.is_none()); } - /// Tests that participation events for different contracts are recorded correctly + /// Tests that participation events for different pools are recorded correctly #[test] fn record_participation_multiple_contracts() { let cur_epoch_num = 1u64; @@ -456,16 +457,34 @@ mod test { let mut simulated_participation = HashMap::new(); simulated_participation.insert( - Addr::unchecked("worker_1"), - (Addr::unchecked("contract_1"), 3), + Addr::unchecked("worker-1"), + ( + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + 3, + ), ); simulated_participation.insert( - Addr::unchecked("worker_2"), - (Addr::unchecked("contract_2"), 4), + Addr::unchecked("worker-2"), + ( + PoolId { + chain_name: "mock-chain-2".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + 4, + ), ); simulated_participation.insert( - Addr::unchecked("worker_3"), - (Addr::unchecked("contract_3"), 2), + Addr::unchecked("worker-3"), + ( + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract-3"), + }, + 2, + ), ); for (worker, (worker_contract, events_participated)) in &simulated_participation { @@ -727,7 +746,7 @@ mod test { assert_eq!(epoch.block_height_started, cur_height + new_epoch_duration); } - /// Tests that rewards are added correctly to a single contract + /// Tests that rewards are added correctly to a single pool #[test] fn added_rewards_should_be_reflected_in_rewards_pool() { let cur_epoch_num = 1u64; @@ -735,34 +754,32 @@ mod test { let epoch_duration = 100u64; let mut contract = setup(cur_epoch_num, block_height_started, epoch_duration); - let worker_contract = Addr::unchecked("some contract"); - let pool = contract - .store - .load_rewards_pool(worker_contract.clone()) - .unwrap(); + + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; + let pool = contract.store.load_rewards_pool(pool_id.clone()).unwrap(); assert!(pool.balance.is_zero()); let initial_amount = Uint128::from(100u128); contract - .add_rewards(worker_contract.clone(), initial_amount.try_into().unwrap()) + .add_rewards(pool_id.clone(), initial_amount.try_into().unwrap()) .unwrap(); - let pool = contract - .store - .load_rewards_pool(worker_contract.clone()) - .unwrap(); + let pool = contract.store.load_rewards_pool(pool_id.clone()).unwrap(); assert_eq!(pool.balance, initial_amount); let added_amount = Uint128::from(500u128); contract - .add_rewards(worker_contract.clone(), added_amount.try_into().unwrap()) + .add_rewards(pool_id.clone(), added_amount.try_into().unwrap()) .unwrap(); - let pool = contract.store.load_rewards_pool(worker_contract).unwrap(); + let pool = contract.store.load_rewards_pool(pool_id).unwrap(); assert_eq!(pool.balance, initial_amount + added_amount); } - /// Tests that rewards are added correctly with multiple contracts + /// Tests that rewards are added correctly with multiple pools #[test] fn added_rewards_for_multiple_contracts_should_be_reflected_in_multiple_pools() { let cur_epoch_num = 1u64; @@ -777,11 +794,18 @@ mod test { (Addr::unchecked("contract_3"), vec![1000, 500, 2000]), ]; + let chain_name: ChainName = "mock-chain".parse().unwrap(); + for (worker_contract, rewards) in &test_data { + let pool_id = PoolId { + chain_name: chain_name.clone(), + contract: worker_contract.clone(), + }; + for amount in rewards { contract .add_rewards( - worker_contract.clone(), + pool_id.clone(), cosmwasm_std::Uint128::from(*amount).try_into().unwrap(), ) .unwrap(); @@ -789,7 +813,12 @@ mod test { } for (worker_contract, rewards) in test_data { - let pool = contract.store.load_rewards_pool(worker_contract).unwrap(); + let pool_id = PoolId { + chain_name: chain_name.clone(), + contract: worker_contract.clone(), + }; + + let pool = contract.store.load_rewards_pool(pool_id).unwrap(); assert_eq!( pool.balance, cosmwasm_std::Uint128::from(rewards.iter().sum::()) @@ -846,7 +875,11 @@ mod test { ), (worker4.clone(), rewards_per_epoch / 4), ]); - let contract_addr = Addr::unchecked("worker_contract"); + + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("worker_contract"), + }; for (worker, events_participated) in worker_participation_per_epoch.clone() { for epoch in 0..epoch_count { @@ -855,7 +888,7 @@ mod test { let _ = contract.record_participation( event_id.clone().try_into().unwrap(), worker.clone(), - contract_addr.clone(), + pool_id.clone(), block_height_started + epoch as u64 * epoch_duration, ); } @@ -866,13 +899,13 @@ mod test { // This tests we are accounting correctly, and only removing from the pool when we actually give out rewards let rewards_added = 2 * rewards_per_epoch; let _ = contract.add_rewards( - contract_addr.clone(), + pool_id.clone(), Uint128::from(rewards_added).try_into().unwrap(), ); let rewards_claimed = contract .distribute_rewards( - contract_addr, + pool_id, block_height_started + epoch_duration * (epoch_count + 2) as u64, None, ) @@ -902,21 +935,24 @@ mod test { participation_threshold, ); let worker = Addr::unchecked("worker"); - let contract_addr = Addr::unchecked("worker_contract"); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("worker_contract"), + }; for height in block_height_started..block_height_started + epoch_duration * 9 { let event_id = height.to_string() + "event"; let _ = contract.record_participation( event_id.try_into().unwrap(), worker.clone(), - contract_addr.clone(), + pool_id.clone(), height, ); } let rewards_added = 1000u128; let _ = contract.add_rewards( - contract_addr.clone(), + pool_id.clone(), Uint128::from(rewards_added).try_into().unwrap(), ); @@ -927,7 +963,7 @@ mod test { // distribute 5 epochs worth of rewards let epochs_to_process = 5; let rewards_claimed = contract - .distribute_rewards(contract_addr.clone(), cur_height, Some(epochs_to_process)) + .distribute_rewards(pool_id.clone(), cur_height, Some(epochs_to_process)) .unwrap(); assert_eq!(rewards_claimed.len(), 1); assert!(rewards_claimed.contains_key(&worker)); @@ -938,7 +974,7 @@ mod test { // distribute the remaining epochs worth of rewards let rewards_claimed = contract - .distribute_rewards(contract_addr.clone(), cur_height, None) + .distribute_rewards(pool_id.clone(), cur_height, None) .unwrap(); assert_eq!(rewards_claimed.len(), 1); assert!(rewards_claimed.contains_key(&worker)); @@ -968,41 +1004,40 @@ mod test { participation_threshold, ); let worker = Addr::unchecked("worker"); - let contract_addr = Addr::unchecked("worker_contract"); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("worker_contract"), + }; let _ = contract.record_participation( "event".try_into().unwrap(), worker.clone(), - contract_addr.clone(), + pool_id.clone(), block_height_started, ); let rewards_added = 1000u128; let _ = contract.add_rewards( - contract_addr.clone(), + pool_id.clone(), Uint128::from(rewards_added).try_into().unwrap(), ); // too early, still in the same epoch let err = contract - .distribute_rewards(contract_addr.clone(), block_height_started, None) + .distribute_rewards(pool_id.clone(), block_height_started, None) .unwrap_err(); assert_eq!(err.current_context(), &ContractError::NoRewardsToDistribute); // next epoch, but still too early to claim rewards let err = contract - .distribute_rewards( - contract_addr.clone(), - block_height_started + epoch_duration, - None, - ) + .distribute_rewards(pool_id.clone(), block_height_started + epoch_duration, None) .unwrap_err(); assert_eq!(err.current_context(), &ContractError::NoRewardsToDistribute); // can claim now, two epochs after participation let rewards_claimed = contract .distribute_rewards( - contract_addr.clone(), + pool_id.clone(), block_height_started + epoch_duration * 2, None, ) @@ -1011,11 +1046,7 @@ mod test { // should error if we try again let err = contract - .distribute_rewards( - contract_addr, - block_height_started + epoch_duration * 2, - None, - ) + .distribute_rewards(pool_id, block_height_started + epoch_duration * 2, None) .unwrap_err(); assert_eq!(err.current_context(), &ContractError::NoRewardsToDistribute); } @@ -1038,25 +1069,28 @@ mod test { participation_threshold, ); let worker = Addr::unchecked("worker"); - let contract_addr = Addr::unchecked("worker_contract"); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("worker_contract"), + }; let _ = contract.record_participation( "event".try_into().unwrap(), worker.clone(), - contract_addr.clone(), + pool_id.clone(), block_height_started, ); // rewards per epoch is 100, we only add 10 let rewards_added = 10u128; let _ = contract.add_rewards( - contract_addr.clone(), + pool_id.clone(), Uint128::from(rewards_added).try_into().unwrap(), ); let err = contract .distribute_rewards( - contract_addr.clone(), + pool_id.clone(), block_height_started + epoch_duration * 2, None, ) @@ -1068,15 +1102,12 @@ mod test { // add some more rewards let rewards_added = 90u128; let _ = contract.add_rewards( - contract_addr.clone(), + pool_id.clone(), Uint128::from(rewards_added).try_into().unwrap(), ); - let result = contract.distribute_rewards( - contract_addr, - block_height_started + epoch_duration * 2, - None, - ); + let result = + contract.distribute_rewards(pool_id, block_height_started + epoch_duration * 2, None); assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 1); } @@ -1098,24 +1129,27 @@ mod test { participation_threshold, ); let worker = Addr::unchecked("worker"); - let contract_addr = Addr::unchecked("worker_contract"); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("worker_contract"), + }; let _ = contract.record_participation( "event".try_into().unwrap(), worker.clone(), - contract_addr.clone(), + pool_id.clone(), block_height_started, ); let rewards_added = 1000u128; let _ = contract.add_rewards( - contract_addr.clone(), + pool_id.clone(), Uint128::from(rewards_added).try_into().unwrap(), ); let rewards_claimed = contract .distribute_rewards( - contract_addr.clone(), + pool_id.clone(), block_height_started + epoch_duration * 2, None, ) @@ -1124,21 +1158,17 @@ mod test { // try to claim again, shouldn't get an error let err = contract - .distribute_rewards( - contract_addr, - block_height_started + epoch_duration * 2, - None, - ) + .distribute_rewards(pool_id, block_height_started + epoch_duration * 2, None) .unwrap_err(); assert_eq!(err.current_context(), &ContractError::NoRewardsToDistribute); } fn create_contract( params_store: Arc>, - events_store: Arc>>, - tally_store: Arc>>, - rewards_store: Arc>>, - watermark_store: Arc>>, + events_store: Arc>>, + tally_store: Arc>>, + rewards_store: Arc>>, + watermark_store: Arc>>, ) -> Contract { let mut store = state::MockStore::new(); let params_store_cloned = params_store.clone(); @@ -1151,14 +1181,14 @@ mod test { Ok(()) }); let events_store_cloned = events_store.clone(); - store.expect_load_event().returning(move |id, contract| { + store.expect_load_event().returning(move |id, pool_id| { let events_store = events_store_cloned.read().unwrap(); - Ok(events_store.get(&(id, contract)).cloned()) + Ok(events_store.get(&(id, pool_id)).cloned()) }); store.expect_save_event().returning(move |event| { let mut events_store = events_store.write().unwrap(); events_store.insert( - (event.event_id.clone().into(), event.contract.clone()), + (event.event_id.clone().into(), event.pool_id.clone()), event.clone(), ); Ok(()) @@ -1166,33 +1196,35 @@ mod test { let tally_store_cloned = tally_store.clone(); store .expect_load_epoch_tally() - .returning(move |contract, epoch_num| { + .returning(move |pool_id, epoch_num| { let tally_store = tally_store_cloned.read().unwrap(); - Ok(tally_store.get(&(contract, epoch_num)).cloned()) + let tally_id = TallyId { + pool_id: pool_id.clone(), + epoch_num, + }; + Ok(tally_store.get(&tally_id).cloned()) }); store.expect_save_epoch_tally().returning(move |tally| { let mut tally_store = tally_store.write().unwrap(); - tally_store.insert( - (tally.contract.clone(), tally.epoch.epoch_num.clone()), - tally.clone(), - ); + let tally_id = TallyId { + pool_id: tally.pool_id.clone(), + epoch_num: tally.epoch.epoch_num, + }; + tally_store.insert(tally_id, tally.clone()); Ok(()) }); let rewards_store_cloned = rewards_store.clone(); - store.expect_load_rewards_pool().returning(move |contract| { + store.expect_load_rewards_pool().returning(move |pool_id| { let rewards_store = rewards_store_cloned.read().unwrap(); - Ok(rewards_store - .get(&contract) - .cloned() - .unwrap_or(RewardsPool { - contract, - balance: Uint128::zero(), - })) + Ok(rewards_store.get(&pool_id).cloned().unwrap_or(RewardsPool { + id: pool_id, + balance: Uint128::zero(), + })) }); store.expect_save_rewards_pool().returning(move |pool| { let mut rewards_store = rewards_store.write().unwrap(); - rewards_store.insert(pool.contract.clone(), pool.clone()); + rewards_store.insert(pool.id.clone(), pool.clone()); Ok(()) }); @@ -1205,9 +1237,9 @@ mod test { }); store .expect_save_rewards_watermark() - .returning(move |contract, epoch_num| { + .returning(move |pool_id, epoch_num| { let mut watermark_store = watermark_store.write().unwrap(); - watermark_store.insert(contract, epoch_num); + watermark_store.insert(pool_id, epoch_num); Ok(()) }); Contract { @@ -1221,10 +1253,10 @@ mod test { fn setup_with_stores( params_store: Arc>, - events_store: Arc>>, - tally_store: Arc>>, - rewards_store: Arc>>, - watermark_store: Arc>>, + events_store: Arc>>, + tally_store: Arc>>, + rewards_store: Arc>>, + watermark_store: Arc>>, ) -> Contract { create_contract( params_store, diff --git a/contracts/rewards/src/lib.rs b/contracts/rewards/src/lib.rs index f6466fdf3..a5abdbb0f 100644 --- a/contracts/rewards/src/lib.rs +++ b/contracts/rewards/src/lib.rs @@ -1,4 +1,4 @@ pub mod contract; pub mod error; pub mod msg; -mod state; +pub mod state; diff --git a/contracts/rewards/src/msg.rs b/contracts/rewards/src/msg.rs index 05c223d01..3e4b73ce5 100644 --- a/contracts/rewards/src/msg.rs +++ b/contracts/rewards/src/msg.rs @@ -1,6 +1,9 @@ use axelar_wasm_std::{nonempty, Threshold}; +use connection_router::state::ChainName; use cosmwasm_schema::{cw_serde, QueryResponses}; +use crate::state::PoolId; + #[cw_serde] pub struct InstantiateMsg { pub governance_address: String, @@ -35,24 +38,21 @@ pub enum ExecuteMsg { /// A possible solution to this is to add a weight to each event, where the voting verifier specifies the number /// of messages in a batch as well as the number of messages a particular worker actually participated in. RecordParticipation { + chain_name: ChainName, event_id: nonempty::String, worker_address: String, }, /// Distribute rewards up to epoch T - 2 (i.e. if we are currently in epoch 10, distribute all undistributed rewards for epochs 0-8) and send the required number of tokens to each worker DistributeRewards { - /// Address of contract for which to process rewards. For example, address of a voting verifier instance. - contract_address: String, + pool_id: PoolId, /// Maximum number of historical epochs for which to distribute rewards, starting with the oldest. epoch_count: Option, }, /// Start a new reward pool for the given contract if none exists. Otherwise, add tokens to an existing reward pool. /// Any attached funds with a denom matching the rewards denom are added to the pool. - AddRewards { - /// Address of contract for which to reward participation. For example, address of a voting verifier instance. - contract_address: String, - }, + AddRewards { pool_id: PoolId }, /// Overwrites the currently stored params. Callable only by governance. UpdateParams { params: Params }, diff --git a/contracts/rewards/src/state.rs b/contracts/rewards/src/state.rs index 59b8a8137..a7d0f1b24 100644 --- a/contracts/rewards/src/state.rs +++ b/contracts/rewards/src/state.rs @@ -2,9 +2,10 @@ use std::collections::HashMap; use std::ops::Deref; use axelar_wasm_std::{nonempty, Threshold}; +use connection_router::state::ChainName; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Storage, Uint128}; -use cw_storage_plus::{Item, Map}; +use cosmwasm_std::{Addr, StdResult, Storage, Uint128}; +use cw_storage_plus::{Item, Key, KeyDeserialize, Map, Prefixer, PrimaryKey}; use error_stack::{Result, ResultExt}; use mockall::automock; @@ -23,9 +24,77 @@ pub struct StoredParams { pub last_updated: Epoch, } +/// PoolId a unique identifier for a rewards pool #[cw_serde] -pub struct EpochTally { +#[derive(Eq, Hash)] +pub struct PoolId { + pub chain_name: ChainName, pub contract: Addr, +} + +impl PrimaryKey<'_> for PoolId { + type Prefix = ChainName; + type SubPrefix = (); + type Suffix = Addr; + type SuperSuffix = (ChainName, Addr); + + fn key(&self) -> Vec { + let mut keys = self.chain_name.key(); + keys.extend(self.contract.key()); + keys + } +} + +impl KeyDeserialize for PoolId { + type Output = Self; + + fn from_vec(value: Vec) -> StdResult { + let (chain_name, contract) = <(ChainName, Addr)>::from_vec(value)?; + Ok(PoolId { + chain_name, + contract, + }) + } +} + +impl<'a> Prefixer<'a> for PoolId { + fn prefix(&self) -> Vec { + self.key() + } +} + +#[cw_serde] +#[derive(Eq, Hash)] +pub struct TallyId { + pub pool_id: PoolId, + pub epoch_num: u64, +} + +impl PrimaryKey<'_> for TallyId { + type Prefix = PoolId; + type SubPrefix = (); + type Suffix = (); + type SuperSuffix = (PoolId, u64); + + fn key(&self) -> Vec { + let mut keys = self.pool_id.key(); + keys.extend(self.epoch_num.key()); + keys + } +} + +impl KeyDeserialize for TallyId { + type Output = Self; + + fn from_vec(value: Vec) -> StdResult { + let (pool_id, epoch_num) = <(PoolId, u64)>::from_vec(value)?; + Ok(TallyId { pool_id, epoch_num }) + } +} + +#[cw_serde] +pub struct EpochTally { + pub pool_id: PoolId, pub event_count: u64, pub participation: HashMap, // maps a worker address to participation count. Can't use Addr as key else deserialization will fail pub epoch: Epoch, @@ -33,9 +102,9 @@ pub struct EpochTally { } impl EpochTally { - pub fn new(contract: Addr, epoch: Epoch, params: Params) -> Self { + pub fn new(pool_id: PoolId, epoch: Epoch, params: Params) -> Self { EpochTally { - contract, + pool_id, event_count: 0, participation: HashMap::new(), epoch, @@ -89,15 +158,15 @@ impl EpochTally { #[cw_serde] pub struct Event { pub event_id: nonempty::String, - pub contract: Addr, + pub pool_id: PoolId, pub epoch_num: u64, } impl Event { - pub fn new(event_id: nonempty::String, contract: Addr, epoch_num: u64) -> Self { + pub fn new(event_id: nonempty::String, pool_id: PoolId, epoch_num: u64) -> Self { Self { event_id, - contract, + pool_id, epoch_num, } } @@ -111,15 +180,18 @@ pub struct Epoch { #[cw_serde] pub struct RewardsPool { - pub contract: Addr, + pub id: PoolId, pub balance: Uint128, } impl RewardsPool { #[allow(dead_code)] - pub fn new(contract: Addr) -> Self { + pub fn new(chain_name: ChainName, contract: Addr) -> Self { RewardsPool { - contract, + id: PoolId { + chain_name, + contract, + }, balance: Uint128::zero(), } } @@ -138,23 +210,24 @@ impl RewardsPool { pub trait Store { fn load_params(&self) -> StoredParams; - fn load_rewards_watermark(&self, contract: Addr) -> Result, ContractError>; + fn load_rewards_watermark(&self, pool_id: PoolId) -> Result, ContractError>; - fn load_event(&self, event_id: String, contract: Addr) -> Result, ContractError>; + fn load_event(&self, event_id: String, pool_id: PoolId) + -> Result, ContractError>; fn load_epoch_tally( &self, - contract: Addr, + pool_id: PoolId, epoch_num: u64, ) -> Result, ContractError>; - fn load_rewards_pool(&self, contract: Addr) -> Result; + fn load_rewards_pool(&self, pool_id: PoolId) -> Result; fn save_params(&mut self, params: &StoredParams) -> Result<(), ContractError>; fn save_rewards_watermark( &mut self, - contract: Addr, + pool_id: PoolId, epoch_num: u64, ) -> Result<(), ContractError>; @@ -168,18 +241,18 @@ pub trait Store { /// Current rewards parameters, along with when the params were updated pub const PARAMS: Item = Item::new("params"); -/// Maps a (contract address, epoch number) pair to a tally for that epoch and contract -const TALLIES: Map<(Addr, u64), EpochTally> = Map::new("tallies"); +/// Maps a (pool id, epoch number) pair to a tally for that epoch and rewards pool +const TALLIES: Map = Map::new("tallies"); -/// Maps an (event id, contract address) pair to an Event -const EVENTS: Map<(String, Addr), Event> = Map::new("events"); +/// Maps an (event id, pool id) pair to an Event +const EVENTS: Map<(String, PoolId), Event> = Map::new("events"); -/// Maps a contract address to the rewards pool for that contract -const POOLS: Map = Map::new("pools"); +/// Maps the id to the rewards pool for given chain and contract +const POOLS: Map = Map::new("pools"); -/// Maps a contract address to the epoch number of the most recent epoch for which rewards were distributed. All epochs prior -/// have had rewards distributed already and all epochs after have not yet had rewards distributed for this contract -const WATERMARKS: Map = Map::new("rewards_watermarks"); +/// Maps a rewards pool to the epoch number of the most recent epoch for which rewards were distributed. All epochs prior +/// have had rewards distributed already and all epochs after have not yet had rewards distributed for this pool +const WATERMARKS: Map = Map::new("rewards_watermarks"); pub const CONFIG: Item = Item::new("config"); @@ -192,35 +265,39 @@ impl Store for RewardsStore<'_> { PARAMS.load(self.storage).expect("params should exist") } - fn load_rewards_watermark(&self, contract: Addr) -> Result, ContractError> { + fn load_rewards_watermark(&self, pool_id: PoolId) -> Result, ContractError> { WATERMARKS - .may_load(self.storage, contract) + .may_load(self.storage, pool_id) .change_context(ContractError::LoadRewardsWatermark) } - fn load_event(&self, event_id: String, contract: Addr) -> Result, ContractError> { + fn load_event( + &self, + event_id: String, + pool_id: PoolId, + ) -> Result, ContractError> { EVENTS - .may_load(self.storage, (event_id, contract)) + .may_load(self.storage, (event_id, pool_id)) .change_context(ContractError::LoadEvent) } fn load_epoch_tally( &self, - contract: Addr, + pool_id: PoolId, epoch_num: u64, ) -> Result, ContractError> { TALLIES - .may_load(self.storage, (contract, epoch_num)) + .may_load(self.storage, TallyId { pool_id, epoch_num }) .change_context(ContractError::LoadEpochTally) } - fn load_rewards_pool(&self, contract: Addr) -> Result { + fn load_rewards_pool(&self, pool_id: PoolId) -> Result { POOLS - .may_load(self.storage, contract.clone()) + .may_load(self.storage, pool_id.clone()) .change_context(ContractError::LoadRewardsPool) .map(|pool| { pool.unwrap_or(RewardsPool { - contract, + id: pool_id, balance: Uint128::zero(), }) }) @@ -234,11 +311,11 @@ impl Store for RewardsStore<'_> { fn save_rewards_watermark( &mut self, - contract: Addr, + pool_id: PoolId, epoch_num: u64, ) -> Result<(), ContractError> { WATERMARKS - .save(self.storage, contract, &epoch_num) + .save(self.storage, pool_id, &epoch_num) .change_context(ContractError::SaveRewardsWatermark) } @@ -246,25 +323,26 @@ impl Store for RewardsStore<'_> { EVENTS .save( self.storage, - (event.event_id.clone().into(), event.contract.clone()), + (event.event_id.clone().into(), event.pool_id.clone()), event, ) .change_context(ContractError::SaveEvent) } fn save_epoch_tally(&mut self, tally: &EpochTally) -> Result<(), ContractError> { + let tally_id = TallyId { + pool_id: tally.pool_id.clone(), + epoch_num: tally.epoch.epoch_num, + }; + TALLIES - .save( - self.storage, - (tally.contract.clone(), tally.epoch.epoch_num), - tally, - ) + .save(self.storage, tally_id, tally) .change_context(ContractError::SaveEpochTally) } fn save_rewards_pool(&mut self, pool: &RewardsPool) -> Result<(), ContractError> { POOLS - .save(self.storage, pool.contract.clone(), pool) + .save(self.storage, pool.id.clone(), pool) .change_context(ContractError::SaveRewardsPool) } } @@ -287,9 +365,10 @@ impl Deref for StorageState { #[cfg(test)] mod test { - use super::{Epoch, EpochTally, Event, RewardsPool, RewardsStore, Store}; + use super::{Epoch, EpochTally, Event, PoolId, RewardsPool, RewardsStore, Store}; use crate::error::ContractError; use crate::{msg::Params, state::StoredParams}; + use connection_router::state::ChainName; use cosmwasm_std::{testing::mock_dependencies, Addr, Uint128, Uint64}; use std::collections::HashMap; @@ -305,7 +384,10 @@ mod test { rewards_per_epoch: Uint128::new(1000).try_into().unwrap(), participation_threshold: (1, 2).try_into().unwrap(), }, - contract: Addr::unchecked("worker contract"), + pool_id: PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("worker contract"), + }, event_count: 101u64, participation: HashMap::from([ ("worker1".into(), 75u64), @@ -357,7 +439,10 @@ mod test { #[test] fn sub_reward_from_pool() { let pool = RewardsPool { - contract: Addr::unchecked("worker contract"), + id: PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("worker contract"), + }, balance: Uint128::from(100u128), }; let new_pool = pool.sub_reward(Uint128::from(50u128)).unwrap(); @@ -418,43 +503,49 @@ mod test { epoch_num: 10, block_height_started: 1000, }; - let contract = Addr::unchecked("some contract"); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; // should be empty at first - let loaded = store.load_rewards_watermark(contract.clone()); + let loaded = store.load_rewards_watermark(pool_id.clone()); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); // save the first water mark - let res = store.save_rewards_watermark(contract.clone(), epoch.epoch_num); + let res = store.save_rewards_watermark(pool_id.clone(), epoch.epoch_num); assert!(res.is_ok()); - let loaded = store.load_rewards_watermark(contract.clone()); + let loaded = store.load_rewards_watermark(pool_id.clone()); assert!(loaded.is_ok()); assert!(loaded.as_ref().unwrap().is_some()); assert_eq!(loaded.unwrap().unwrap(), epoch.epoch_num); // now store a new watermark, should overwrite - let res = store.save_rewards_watermark(contract.clone(), epoch.epoch_num + 1); + let res = store.save_rewards_watermark(pool_id.clone(), epoch.epoch_num + 1); assert!(res.is_ok()); - let loaded = store.load_rewards_watermark(contract); + let loaded = store.load_rewards_watermark(pool_id); assert!(loaded.is_ok()); assert!(loaded.as_ref().unwrap().is_some()); assert_eq!(loaded.unwrap().unwrap(), epoch.epoch_num + 1); // check different contract - let diff_contract = Addr::unchecked("some other contract"); + let diff_pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some other contract"), + }; // should be empty at first - let loaded = store.load_rewards_watermark(diff_contract.clone()); + let loaded = store.load_rewards_watermark(diff_pool_id.clone()); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); // save the first water mark for this contract - let res = store.save_rewards_watermark(diff_contract.clone(), epoch.epoch_num + 7); + let res = store.save_rewards_watermark(diff_pool_id.clone(), epoch.epoch_num + 7); assert!(res.is_ok()); - let loaded = store.load_rewards_watermark(diff_contract.clone()); + let loaded = store.load_rewards_watermark(diff_pool_id.clone()); assert!(loaded.is_ok()); assert!(loaded.as_ref().unwrap().is_some()); assert_eq!(loaded.unwrap().unwrap(), epoch.epoch_num + 7); @@ -468,7 +559,10 @@ mod test { }; let event = Event { - contract: Addr::unchecked("some contract"), + pool_id: PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }, event_id: "some event".try_into().unwrap(), epoch_num: 2, }; @@ -477,29 +571,27 @@ mod test { assert!(res.is_ok()); // check that we load the event that we just saved - let loaded = store.load_event(event.event_id.clone().into(), event.contract.clone()); + let loaded = store.load_event(event.event_id.clone().into(), event.pool_id.clone()); assert!(loaded.is_ok()); assert!(loaded.as_ref().unwrap().is_some()); assert_eq!(loaded.unwrap().unwrap(), event); // different event id and contract address should return none - let loaded = store.load_event( - "some other event".into(), - Addr::unchecked("different contract"), - ); + let diff_pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("different contract"), + }; + let loaded = store.load_event("some other event".into(), diff_pool_id.clone()); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); // same event id but different contract address, should still return none - let loaded = store.load_event( - event.event_id.clone().into(), - Addr::unchecked("different contract"), - ); + let loaded = store.load_event(event.event_id.clone().into(), diff_pool_id); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); // different event id, but same contract address, should still return none - let loaded = store.load_event("some other event".into(), event.contract); + let loaded = store.load_event("some other event".into(), event.pool_id); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); } @@ -512,14 +604,17 @@ mod test { }; let epoch_num = 10; - let contract = Addr::unchecked("some contract"); let rewards_rate = Uint128::from(100u128).try_into().unwrap(); let epoch = Epoch { epoch_num, block_height_started: 1, }; + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; let mut tally = EpochTally::new( - contract.clone(), + pool_id.clone(), epoch, Params { epoch_duration: 100u64.try_into().unwrap(), @@ -534,23 +629,29 @@ mod test { assert!(res.is_ok()); // check that we load the tally that we just saved - let loaded = store.load_epoch_tally(contract.clone(), epoch_num); + let loaded = store.load_epoch_tally(pool_id.clone(), epoch_num); assert!(loaded.is_ok()); assert!(loaded.as_ref().unwrap().is_some()); assert_eq!(loaded.unwrap().unwrap(), tally); // different contract but same epoch should return none - let loaded = store.load_epoch_tally(Addr::unchecked("different contract"), epoch_num); + let loaded = store.load_epoch_tally( + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("different contract"), + }, + epoch_num, + ); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); // different epoch but same contract should return none - let loaded = store.load_epoch_tally(contract.clone(), epoch_num + 1); + let loaded = store.load_epoch_tally(pool_id.clone(), epoch_num + 1); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); // different epoch and different contract should return none - let loaded = store.load_epoch_tally(contract.clone(), epoch_num + 1); + let loaded = store.load_epoch_tally(pool_id.clone(), epoch_num + 1); assert!(loaded.is_ok()); assert!(loaded.unwrap().is_none()); } @@ -562,17 +663,20 @@ mod test { storage: &mut mock_deps.storage, }; - let contract = Addr::unchecked("some contract"); - let pool = RewardsPool::new(contract.clone()); + let chain_name: ChainName = "mock-chain".parse().unwrap(); + let pool = RewardsPool::new(chain_name.clone(), Addr::unchecked("some contract")); let res = store.save_rewards_pool(&pool); assert!(res.is_ok()); - let loaded = store.load_rewards_pool(contract); + let loaded = store.load_rewards_pool(pool.id.clone()); assert!(loaded.is_ok()); assert_eq!(loaded.unwrap(), pool); - let loaded = store.load_rewards_pool(Addr::unchecked("a different contract")); + let loaded = store.load_rewards_pool(PoolId { + chain_name: chain_name.clone(), + contract: Addr::unchecked("a different contract"), + }); assert!(loaded.is_ok()); assert!(loaded.as_ref().unwrap().balance.is_zero()); } diff --git a/contracts/voting-verifier/src/execute.rs b/contracts/voting-verifier/src/execute.rs index f88837762..c6a05798b 100644 --- a/contracts/voting-verifier/src/execute.rs +++ b/contracts/voting-verifier/src/execute.rs @@ -201,6 +201,7 @@ pub fn end_poll(deps: DepsMut, env: Env, poll_id: PollId) -> Result multisig::worker_set::WorkerSet { +pub fn get_worker_set(app: &mut App, multisig_prover_address: &Addr) -> WorkerSet { let query_response = app.wrap().query_wasm_smart( multisig_prover_address, &multisig_prover::msg::QueryMsg::GetWorkerSet, @@ -273,12 +271,20 @@ pub fn advance_at_least_to_height(app: &mut App, desired_height: u64) { } } -pub fn distribute_rewards(app: &mut App, rewards_address: &Addr, contract_address: &Addr) { +pub fn distribute_rewards( + app: &mut App, + rewards_address: &Addr, + chain_name: &ChainName, + contract_address: &Addr, +) { let response = app.execute_contract( Addr::unchecked("relayer"), rewards_address.clone(), &rewards::msg::ExecuteMsg::DistributeRewards { - contract_address: contract_address.to_string(), + pool_id: PoolId { + chain_name: chain_name.clone(), + contract: contract_address.clone(), + }, epoch_count: None, }, &[], @@ -347,16 +353,6 @@ pub fn setup_protocol(service_name: nonempty::String) -> Protocol { governance_account: governance_address.to_string(), }, ); - // voting rewards are added when setting up individual chains - let response = app.execute_contract( - genesis.clone(), - rewards_address.clone(), - &rewards::msg::ExecuteMsg::AddRewards { - contract_address: multisig_address.to_string(), - }, - &coins(1000, AXL_DENOMINATION), - ); - assert!(response.is_ok()); Protocol { genesis_address: genesis, @@ -710,8 +706,8 @@ pub fn setup_chain(protocol: &mut Protocol, chain_name: ChainName) -> Chain { service_name: protocol.service_name.to_string(), chain_name: chain_name.to_string(), worker_set_diff_threshold: 0, - encoder: multisig_prover::encoding::Encoder::Abi, - key_type: multisig::key::KeyType::Ecdsa, + encoder: Encoder::Abi, + key_type: KeyType::Ecdsa, }, ); let response = protocol.app.execute_contract( @@ -746,7 +742,23 @@ pub fn setup_chain(protocol: &mut Protocol, chain_name: ChainName) -> Chain { protocol.genesis_address.clone(), protocol.rewards_address.clone(), &rewards::msg::ExecuteMsg::AddRewards { - contract_address: voting_verifier_address.to_string(), + pool_id: PoolId { + chain_name: chain_name.clone(), + contract: voting_verifier_address.clone(), + }, + }, + &coins(1000, AXL_DENOMINATION), + ); + assert!(response.is_ok()); + + let response = protocol.app.execute_contract( + protocol.genesis_address.clone(), + protocol.rewards_address.clone(), + &rewards::msg::ExecuteMsg::AddRewards { + pool_id: PoolId { + chain_name: chain_name.clone(), + contract: protocol.multisig_address.clone(), + }, }, &coins(1000, AXL_DENOMINATION), );