diff --git a/contracts/rewards/src/contract.rs b/contracts/rewards/src/contract.rs index 8eaca607d..b97df9dc2 100644 --- a/contracts/rewards/src/contract.rs +++ b/contracts/rewards/src/contract.rs @@ -9,7 +9,7 @@ use itertools::Itertools; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{self, Config, Epoch, ParamsSnapshot, PoolId, CONFIG, PARAMS}; +use crate::state::{self, Config, PoolId, CONFIG}; mod execute; mod migrations; @@ -24,7 +24,7 @@ pub fn migrate( _env: Env, _msg: Empty, ) -> Result { - migrations::v0_4_0::migrate(deps.storage)?; + migrations::v1_0_0::migrate(deps.storage)?; // any version checks should be done before here @@ -36,7 +36,7 @@ pub fn migrate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - env: Env, + _env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> Result { @@ -52,17 +52,6 @@ pub fn instantiate( }, )?; - PARAMS.save( - deps.storage, - &ParamsSnapshot { - params: msg.params, - created_at: Epoch { - epoch_num: 0, - block_height_started: env.block.height, - }, - }, - )?; - Ok(Response::new()) } @@ -135,11 +124,15 @@ pub fn execute( Ok(Response::new().add_messages(msgs)) } - ExecuteMsg::UpdateParams { params } => { - execute::update_params(deps.storage, params, env.block.height)?; + ExecuteMsg::UpdatePoolParams { params, pool_id } => { + execute::update_pool_params(deps.storage, &pool_id, params, env.block.height)?; Ok(Response::new()) } + ExecuteMsg::CreatePool { params, pool_id } => { + execute::create_pool(deps.storage, params, env.block.height, &pool_id)?; + Ok(Response::new()) + } } } @@ -181,7 +174,7 @@ mod tests { let mut deps = mock_dependencies(); #[allow(deprecated)] - migrations::v0_4_0::tests::instantiate_contract(deps.as_mut(), "denom"); + migrations::v1_0_0::tests::instantiate_contract(deps.as_mut(), "denom"); migrate(deps.as_mut(), mock_env(), Empty {}).unwrap(); @@ -224,7 +217,6 @@ mod tests { &InstantiateMsg { governance_address: governance_address.to_string(), rewards_denom: AXL_DENOMINATION.to_string(), - params: initial_params.clone(), }, &[], "Contract", @@ -237,6 +229,17 @@ mod tests { contract: pool_contract.clone(), }; + let res = app.execute_contract( + governance_address.clone(), + contract_address.clone(), + &ExecuteMsg::CreatePool { + params: initial_params.clone(), + pool_id: pool_id.clone(), + }, + &[], + ); + assert!(res.is_ok()); + let rewards = 200; let res = app.execute_contract( user.clone(), @@ -255,8 +258,9 @@ mod tests { let res = app.execute_contract( governance_address, contract_address.clone(), - &ExecuteMsg::UpdateParams { + &ExecuteMsg::UpdatePoolParams { params: updated_params.clone(), + pool_id: pool_id.clone(), }, &[], ); diff --git a/contracts/rewards/src/contract/execute.rs b/contracts/rewards/src/contract/execute.rs index 4fc8994ea..ead843cae 100644 --- a/contracts/rewards/src/contract/execute.rs +++ b/contracts/rewards/src/contract/execute.rs @@ -2,11 +2,13 @@ use std::collections::HashMap; use axelar_wasm_std::{nonempty, FnExt}; use cosmwasm_std::{Addr, OverflowError, OverflowOperation, Storage, Uint128}; -use error_stack::{Report, Result}; +use error_stack::{ensure, Report, Result}; use crate::error::ContractError; use crate::msg::Params; -use crate::state::{self, Epoch, EpochTally, Event, ParamsSnapshot, PoolId, StorageState}; +use crate::state::{ + self, Epoch, EpochTally, Event, ParamsSnapshot, PoolId, RewardsPool, StorageState, +}; const DEFAULT_EPOCHS_TO_PROCESS: u64 = 10; const EPOCH_PAYOUT_DELAY: u64 = 2; @@ -18,7 +20,7 @@ pub fn record_participation( pool_id: PoolId, block_height: u64, ) -> Result<(), ContractError> { - let current_params = state::load_params(storage); + let current_params = state::load_rewards_pool_params(storage, pool_id.clone())?; let cur_epoch = Epoch::current(¤t_params, block_height)?; let event = load_or_store_event(storage, event_id, pool_id.clone(), cur_epoch.epoch_num)?; @@ -59,7 +61,7 @@ pub fn distribute_rewards( epoch_process_limit: Option, ) -> Result, ContractError> { let epoch_process_limit = epoch_process_limit.unwrap_or(DEFAULT_EPOCHS_TO_PROCESS); - let cur_epoch = Epoch::current(&state::load_params(storage), cur_block_height)?; + let cur_epoch = state::current_epoch(storage, &pool_id, cur_block_height)?; let from = state::load_rewards_watermark(storage, pool_id.clone())? .map_or(0, |last_processed| last_processed.saturating_add(1)); @@ -85,7 +87,7 @@ fn process_rewards_for_epochs( to: u64, ) -> Result, ContractError> { let rewards = cumulate_rewards(storage, &pool_id, from, to)?; - state::load_rewards_pool_or_new(storage, pool_id.clone())? + state::load_rewards_pool(storage, pool_id.clone())? .sub_reward(rewards.values().sum())? .then(|pool| state::save_rewards_pool(storage, &pool))?; @@ -114,12 +116,43 @@ fn iterate_epoch_tallies<'a>( }) } -pub fn update_params( +pub fn create_pool( storage: &mut dyn Storage, + params: Params, + block_height: u64, + pool_id: &PoolId, +) -> Result<(), ContractError> { + ensure!( + !state::pool_exists(storage, pool_id)?, + ContractError::RewardsPoolAlreadyExists + ); + + let cur_epoch = Epoch { + epoch_num: 0, + block_height_started: block_height, + }; + + let params_snapshot = ParamsSnapshot { + params, + created_at: cur_epoch, + }; + + let pool = RewardsPool { + id: pool_id.clone(), + balance: Uint128::zero(), + params: params_snapshot, + }; + + state::save_rewards_pool(storage, &pool) +} + +pub fn update_pool_params( + storage: &mut dyn Storage, + pool_id: &PoolId, new_params: Params, block_height: u64, ) -> Result<(), ContractError> { - let cur_epoch = Epoch::current(&state::load_params(storage), block_height)?; + let cur_epoch = state::current_epoch(storage, pool_id, block_height)?; // If the param update reduces the epoch duration such that the current epoch immediately ends, // start a new epoch at this block, incrementing the current epoch number by 1. // This prevents us from jumping forward an arbitrary number of epochs, and maintains consistency for past events. @@ -149,13 +182,13 @@ pub fn update_params( } else { cur_epoch }; - state::save_params( - storage, - &ParamsSnapshot { - params: new_params, - created_at: cur_epoch, - }, - )?; + let new_params_snapshot = ParamsSnapshot { + params: new_params, + created_at: cur_epoch, + }; + + state::update_pool_params(storage, pool_id, &new_params_snapshot)?; + Ok(()) } @@ -164,7 +197,7 @@ pub fn add_rewards( pool_id: PoolId, amount: nonempty::Uint128, ) -> Result<(), ContractError> { - let mut pool = state::load_rewards_pool_or_new(storage, pool_id)?; + let mut pool = state::load_rewards_pool(storage, pool_id)?; pool.balance = pool .balance .checked_add(Uint128::from(amount)) @@ -221,8 +254,19 @@ mod test { let cur_epoch_num = 1u64; let block_height_started = 250u64; let epoch_duration = 100u64; - let mock_deps = setup(cur_epoch_num, block_height_started, epoch_duration); - let current_params = state::load_params(mock_deps.as_ref().storage); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; + let mock_deps = setup( + cur_epoch_num, + block_height_started, + epoch_duration, + pool_id.clone(), + ); + let current_params = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id) + .unwrap() + .params; let new_epoch = Epoch::current(¤t_params, block_height_started).unwrap(); assert_eq!(new_epoch.epoch_num, cur_epoch_num); @@ -245,8 +289,19 @@ mod test { let cur_epoch_num = 1u64; let block_height_started = 250u64; let epoch_duration = 100u64; - let mock_deps = setup(cur_epoch_num, block_height_started, epoch_duration); - let current_params = state::load_params(mock_deps.as_ref().storage); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; + let mock_deps = setup( + cur_epoch_num, + block_height_started, + epoch_duration, + pool_id.clone(), + ); + let current_params = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params; assert!(Epoch::current(¤t_params, block_height_started - 1).is_err()); assert!(Epoch::current(¤t_params, block_height_started - epoch_duration).is_err()); @@ -258,7 +313,16 @@ mod test { let cur_epoch_num = 1u64; let block_height_started = 250u64; let epoch_duration = 100u64; - let mock_deps = setup(cur_epoch_num, block_height_started, epoch_duration); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; + let mock_deps = setup( + cur_epoch_num, + block_height_started, + epoch_duration, + pool_id.clone(), + ); // elements are (height, expected epoch number, expected epoch start) let test_cases = vec![ @@ -285,8 +349,13 @@ mod test { ]; for (height, expected_epoch_num, expected_block_start) in test_cases { - let new_epoch = - Epoch::current(&state::load_params(mock_deps.as_ref().storage), height).unwrap(); + let new_epoch = Epoch::current( + &state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params, + height, + ) + .unwrap(); assert_eq!(new_epoch.epoch_num, expected_epoch_num); assert_eq!(new_epoch.block_height_started, expected_block_start); @@ -300,12 +369,16 @@ mod test { let epoch_block_start = 250u64; let epoch_duration = 100u64; - let mut mock_deps = setup(cur_epoch_num, epoch_block_start, epoch_duration); - let pool_id = PoolId { chain_name: "mock-chain".parse().unwrap(), contract: Addr::unchecked("some contract"), }; + let mut mock_deps = setup( + cur_epoch_num, + epoch_block_start, + epoch_duration, + pool_id.clone(), + ); let mut simulated_participation = HashMap::new(); simulated_participation.insert(Addr::unchecked("verifier_1"), 10); @@ -354,12 +427,16 @@ mod test { let block_height_started = 250u64; let epoch_duration = 100u64; - let mut mock_deps = setup(starting_epoch_num, block_height_started, epoch_duration); - let pool_id = PoolId { chain_name: "mock-chain".parse().unwrap(), contract: Addr::unchecked("some contract"), }; + let mut mock_deps = setup( + starting_epoch_num, + block_height_started, + epoch_duration, + pool_id.clone(), + ); let verifiers = vec![ Addr::unchecked("verifier_1"), @@ -381,7 +458,9 @@ mod test { } let cur_epoch = Epoch::current( - &state::load_params(mock_deps.as_ref().storage), + &state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params, height_at_epoch_end, ) .unwrap(); @@ -414,9 +493,6 @@ mod test { fn record_participation_multiple_contracts() { let cur_epoch_num = 1u64; let block_height_started = 250u64; - let epoch_duration = 100u64; - - let mut mock_deps = setup(cur_epoch_num, block_height_started, epoch_duration); let mut simulated_participation = HashMap::new(); simulated_participation.insert( @@ -450,6 +526,20 @@ mod test { ), ); + let params = Params { + participation_threshold: (1, 2).try_into().unwrap(), + epoch_duration: 100u64.try_into().unwrap(), + rewards_per_epoch: 100u128.try_into().unwrap(), + }; + let mut mock_deps = setup_multiple_pools_with_params( + cur_epoch_num, + block_height_started, + simulated_participation + .iter() + .map(|(_, (pool_id, _))| (pool_id.clone(), params.clone())) + .collect(), + ); + for (verifier, (pool_contract, events_participated)) in &simulated_participation { for i in 0..*events_participated { let event_id = i.to_string().try_into().unwrap(); @@ -482,6 +572,7 @@ mod test { ); } } + /// Test that rewards parameters are updated correctly. In this test we don't change the epoch duration, so /// that computation of the current epoch is unaffected. #[test] @@ -491,12 +582,17 @@ mod test { let initial_rewards_per_epoch = 100u128; let initial_participation_threshold = (1, 2); let epoch_duration = 100u64; + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; let mut mock_deps = setup_with_params( initial_epoch_num, initial_epoch_start, epoch_duration, initial_rewards_per_epoch, initial_participation_threshold, + pool_id.clone(), ); // simulate the below tests running at this block height @@ -511,16 +607,34 @@ mod test { }; // the epoch shouldn't change when the params are updated, since we are not changing the epoch duration - let expected_epoch = - Epoch::current(&state::load_params(mock_deps.as_ref().storage), cur_height).unwrap(); + let expected_epoch = Epoch::current( + &state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params, + cur_height, + ) + .unwrap(); - update_params(mock_deps.as_mut().storage, new_params.clone(), cur_height).unwrap(); - let stored = state::load_params(mock_deps.as_ref().storage); + update_pool_params( + mock_deps.as_mut().storage, + &pool_id, + new_params.clone(), + cur_height, + ) + .unwrap(); + let stored = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params; assert_eq!(stored.params, new_params); // current epoch shouldn't have changed - let cur_epoch = - Epoch::current(&state::load_params(mock_deps.as_ref().storage), cur_height).unwrap(); + let cur_epoch = Epoch::current( + &state::load_rewards_pool(mock_deps.as_ref().storage, pool_id) + .unwrap() + .params, + cur_height, + ) + .unwrap(); assert_eq!(expected_epoch.epoch_num, cur_epoch.epoch_num); assert_eq!( expected_epoch.block_height_started, @@ -537,10 +651,15 @@ mod test { let initial_epoch_num = 1u64; let initial_epoch_start = 250u64; let initial_epoch_duration = 100u64; + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; let mut mock_deps = setup( initial_epoch_num, initial_epoch_start, initial_epoch_duration, + pool_id.clone(), ); // simulate the tests running after 5 epochs have passed @@ -548,7 +667,10 @@ mod test { let cur_height = initial_epoch_start + initial_epoch_duration * epochs_elapsed + 10; // add 10 here just to be a little past the epoch boundary // epoch shouldn't change if we are extending the duration - let initial_params_snapshot = state::load_params(mock_deps.as_ref().storage); + let initial_params_snapshot = + state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params; let epoch_prior_to_update = Epoch::current(&initial_params_snapshot, cur_height).unwrap(); let new_epoch_duration = initial_epoch_duration * 2; @@ -557,9 +679,17 @@ mod test { ..initial_params_snapshot.params // keep everything besides epoch duration the same }; - update_params(mock_deps.as_mut().storage, new_params.clone(), cur_height).unwrap(); + update_pool_params( + mock_deps.as_mut().storage, + &pool_id.clone(), + new_params.clone(), + cur_height, + ) + .unwrap(); - let updated_params_snapshot = state::load_params(mock_deps.as_ref().storage); + let updated_params_snapshot = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id) + .unwrap() + .params; // current epoch shouldn't change let epoch = Epoch::current(&updated_params_snapshot, cur_height).unwrap(); @@ -590,10 +720,15 @@ mod test { let initial_epoch_num = 1u64; let initial_epoch_start = 256u64; let initial_epoch_duration = 100u64; + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; let mut mock_deps = setup( initial_epoch_num, initial_epoch_start, initial_epoch_duration, + pool_id.clone(), ); // simulate the tests running after 10 epochs have passed @@ -602,7 +737,10 @@ mod test { let new_epoch_duration = initial_epoch_duration / 2; - let initial_params_snapshot = state::load_params(mock_deps.as_ref().storage); + let initial_params_snapshot = + state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params; let epoch_prior_to_update = Epoch::current(&initial_params_snapshot, cur_height).unwrap(); // we are shortening the epoch, but not so much it causes the epoch number to change. We want to remain in the same epoch assert!(cur_height - epoch_prior_to_update.block_height_started < new_epoch_duration); @@ -611,9 +749,18 @@ mod test { epoch_duration: new_epoch_duration.try_into().unwrap(), ..initial_params_snapshot.params }; - update_params(mock_deps.as_mut().storage, new_params.clone(), cur_height).unwrap(); + update_pool_params( + mock_deps.as_mut().storage, + &pool_id, + new_params.clone(), + cur_height, + ) + .unwrap(); - let updated_params_snapshot = state::load_params(mock_deps.as_ref().storage); + let updated_params_snapshot = + state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params; // current epoch shouldn't have changed let epoch = Epoch::current(&updated_params_snapshot, cur_height).unwrap(); @@ -636,10 +783,15 @@ mod test { let initial_epoch_num = 1u64; let initial_epoch_start = 250u64; let initial_epoch_duration = 100u64; + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; let mut mock_deps = setup( initial_epoch_num, initial_epoch_start, initial_epoch_duration, + pool_id.clone(), ); // simulate running the test after 100 epochs have elapsed @@ -650,16 +802,28 @@ mod test { let cur_height = initial_epoch_start + initial_epoch_duration * epochs_elapsed + new_epoch_duration * 2; - let initial_params_snapshot = state::load_params(mock_deps.as_ref().storage); + let initial_params_snapshot = + state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params; let epoch_prior_to_update = Epoch::current(&initial_params_snapshot, cur_height).unwrap(); let new_params = Params { epoch_duration: 10.try_into().unwrap(), ..initial_params_snapshot.params }; - update_params(mock_deps.as_mut().storage, new_params.clone(), cur_height).unwrap(); + update_pool_params( + mock_deps.as_mut().storage, + &pool_id.clone(), + new_params.clone(), + cur_height, + ) + .unwrap(); - let updated_params_snapshot = state::load_params(mock_deps.as_ref().storage); + let updated_params_snapshot = + state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()) + .unwrap() + .params; // should be in new epoch now let epoch = Epoch::current(&updated_params_snapshot, cur_height).unwrap(); @@ -679,15 +843,19 @@ mod test { let cur_epoch_num = 1u64; let block_height_started = 250u64; let epoch_duration = 100u64; - - let mut mock_deps = setup(cur_epoch_num, block_height_started, epoch_duration); - let pool_id = PoolId { chain_name: "mock-chain".parse().unwrap(), contract: Addr::unchecked("some contract"), }; - let pool = - state::load_rewards_pool_or_new(mock_deps.as_ref().storage, pool_id.clone()).unwrap(); + + let mut mock_deps = setup( + cur_epoch_num, + block_height_started, + epoch_duration, + pool_id.clone(), + ); + + let pool = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()).unwrap(); assert!(pool.balance.is_zero()); let initial_amount = Uint128::from(100u128); @@ -698,8 +866,7 @@ mod test { ) .unwrap(); - let pool = - state::load_rewards_pool_or_new(mock_deps.as_ref().storage, pool_id.clone()).unwrap(); + let pool = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id.clone()).unwrap(); assert_eq!(pool.balance, initial_amount); let added_amount = Uint128::from(500u128); @@ -710,7 +877,7 @@ mod test { ) .unwrap(); - let pool = state::load_rewards_pool_or_new(mock_deps.as_ref().storage, pool_id).unwrap(); + let pool = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id).unwrap(); assert_eq!(pool.balance, initial_amount + added_amount); } @@ -720,8 +887,17 @@ mod test { let cur_epoch_num = 1u64; let block_height_started = 250u64; let epoch_duration = 100u64; + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("some contract"), + }; - let mut mock_deps = setup(cur_epoch_num, block_height_started, epoch_duration); + let mut mock_deps = setup( + cur_epoch_num, + block_height_started, + epoch_duration, + pool_id.clone(), + ); // a vector of (contract, rewards amounts) pairs let test_data = vec![ (Addr::unchecked("contract_1"), vec![100, 200, 50]), @@ -736,6 +912,19 @@ mod test { chain_name: chain_name.clone(), contract: pool_contract.clone(), }; + let participation_threshold = (1, 2); + let rewards_per_epoch = 100u128; + create_pool( + mock_deps.as_mut().storage, + Params { + epoch_duration: epoch_duration.try_into().unwrap(), + rewards_per_epoch: rewards_per_epoch.try_into().unwrap(), + participation_threshold: participation_threshold.try_into().unwrap(), + }, + block_height_started, + &pool_id, + ) + .unwrap(); for amount in rewards { add_rewards( @@ -753,8 +942,7 @@ mod test { contract: pool_contract.clone(), }; - let pool = - state::load_rewards_pool_or_new(mock_deps.as_ref().storage, pool_id).unwrap(); + let pool = state::load_rewards_pool(mock_deps.as_ref().storage, pool_id).unwrap(); assert_eq!( pool.balance, cosmwasm_std::Uint128::from(rewards.iter().sum::()) @@ -762,6 +950,276 @@ mod test { } } + /// Tests that pools can have different reward amounts + #[test] + fn multiple_pools_different_rewards_amount() { + let cur_epoch_num = 1u64; + let block_height_started = 250u64; + let epoch_duration = 100u64; + + let mut simulated_participation = HashMap::new(); + simulated_participation.insert( + Addr::unchecked("verifier-1"), + ( + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + 3, + ), + ); + simulated_participation.insert( + Addr::unchecked("verifier-2"), + ( + PoolId { + chain_name: "mock-chain-2".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + 4, + ), + ); + simulated_participation.insert( + Addr::unchecked("verifier-3"), + ( + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract-3"), + }, + 2, + ), + ); + let base_params = Params { + participation_threshold: (1, 2).try_into().unwrap(), + epoch_duration: 100u64.try_into().unwrap(), + rewards_per_epoch: 100u128.try_into().unwrap(), // this is overwritten below + }; + let rewards_per_epoch = vec![50u128, 100u128, 200u128]; + let pool_params: Vec<(PoolId, Params)> = simulated_participation + .values() + .map(|(pool_id, _)| pool_id.clone()) + .zip(rewards_per_epoch.into_iter().map(|r| Params { + rewards_per_epoch: r.try_into().unwrap(), + ..base_params.clone() + })) + .collect(); + + let mut mock_deps = setup_multiple_pools_with_params( + cur_epoch_num, + block_height_started, + pool_params.clone(), + ); + + for (verifier, (pool_contract, events_participated)) in &simulated_participation { + for i in 0..*events_participated { + let event_id = i.to_string().try_into().unwrap(); + record_participation( + mock_deps.as_mut().storage, + event_id, + verifier.clone(), + pool_contract.clone(), + block_height_started, + ) + .unwrap(); + } + } + + for (pool_id, params) in pool_params { + let rewards_to_add = params.rewards_per_epoch; + let _ = add_rewards( + mock_deps.as_mut().storage, + pool_id.clone(), + Uint128::from(rewards_to_add).try_into().unwrap(), + ); + + let rewards_claimed = distribute_rewards( + mock_deps.as_mut().storage, + pool_id, + block_height_started + epoch_duration * 2, + None, + ) + .unwrap(); + assert_eq!( + rewards_claimed.values().sum::(), + Uint128::from(params.rewards_per_epoch) + ); + } + } + + /// Tests that pools can have different participation thresholds + #[test] + fn multiple_pools_different_threshold() { + let cur_epoch_num = 1u64; + let block_height_started = 250u64; + let epoch_duration = 100u64; + let pools = vec![ + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + PoolId { + chain_name: "mock-chain-2".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + ]; + + let verifiers = [Addr::unchecked("verifier-1"), Addr::unchecked("verifier-2")]; + + // simulate two verifiers each participating in two pools + // the first verifier participates in 2 events, and the second in 3 events (out of a total of 3 events) + let simulated_participation = vec![ + (verifiers[0].clone(), (pools[0].clone(), 2)), + (verifiers[0].clone(), (pools[1].clone(), 2)), + (verifiers[1].clone(), (pools[0].clone(), 3)), + (verifiers[1].clone(), (pools[1].clone(), 3)), + ]; + let base_params = Params { + participation_threshold: (1, 2).try_into().unwrap(), // this is overwritten below + epoch_duration: 100u64.try_into().unwrap(), + rewards_per_epoch: 100u128.try_into().unwrap(), + }; + // the first pool has a 2/3 threshold, the second 3/4 threshold + let participation_thresholds = vec![(2, 3), (3, 4)]; + let pool_params: Vec<(PoolId, Params)> = pools + .clone() + .into_iter() + .zip(participation_thresholds.into_iter().map(|p| Params { + participation_threshold: p.try_into().unwrap(), + ..base_params.clone() + })) + .collect(); + + let mut mock_deps = setup_multiple_pools_with_params( + cur_epoch_num, + block_height_started, + pool_params.clone(), + ); + + for (verifier, (pool_contract, events_participated)) in &simulated_participation { + for i in 0..*events_participated { + let event_id = i.to_string().try_into().unwrap(); + record_participation( + mock_deps.as_mut().storage, + event_id, + verifier.clone(), + pool_contract.clone(), + block_height_started, + ) + .unwrap(); + } + } + + for (pool_id, params) in pool_params { + let rewards_to_add = params.rewards_per_epoch; + let _ = add_rewards(mock_deps.as_mut().storage, pool_id.clone(), rewards_to_add); + + let rewards_claimed = distribute_rewards( + mock_deps.as_mut().storage, + pool_id.clone(), + block_height_started + epoch_duration * 2, + None, + ) + .unwrap(); + + if pool_id == pools[0] { + // the first pool has a 2/3 threshold, which both verifiers meet + assert_eq!( + rewards_claimed, + HashMap::from_iter(verifiers.iter().map(|v| ( + v.clone(), + Uint128::from(Uint128::from(rewards_to_add).u128() / 2) + ))) + ); + } else { + // the second pool has 3/4 threshold, which only the second verifier meets + assert_eq!( + rewards_claimed, + HashMap::from([(verifiers[1].clone(), Uint128::from(rewards_to_add))]) + ); + } + } + } + + /// Tests that pools can have different epoch lengths + #[test] + fn multiple_pools_different_epoch_length() { + let cur_epoch_num = 1u64; + let block_height_started = 250u64; + let base_epoch_duration = 100u64; + let pools = vec![ + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + PoolId { + chain_name: "mock-chain-2".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + ]; + + let verifier = Addr::unchecked("verifier-1"); + + // simulate one verifier participating in two events in each pool + let simulated_participation = vec![ + (verifier.clone(), (pools[0].clone(), 2)), + (verifier.clone(), (pools[1].clone(), 2)), + ]; + + let base_params = Params { + participation_threshold: (1, 2).try_into().unwrap(), + epoch_duration: 100u64.try_into().unwrap(), // this is overwritten below + rewards_per_epoch: 100u128.try_into().unwrap(), + }; + // one pool has twice the epoch duration as the other + let epoch_durations = vec![base_epoch_duration, base_epoch_duration * 2]; + let pool_params: Vec<(PoolId, Params)> = pools + .clone() + .into_iter() + .zip(epoch_durations.into_iter().map(|e| Params { + epoch_duration: e.try_into().unwrap(), + ..base_params.clone() + })) + .collect(); + + let mut mock_deps = setup_multiple_pools_with_params( + cur_epoch_num, + block_height_started, + pool_params.clone(), + ); + + for (verifier, (pool_contract, events_participated)) in &simulated_participation { + for i in 0..*events_participated { + let event_id = i.to_string().try_into().unwrap(); + record_participation( + mock_deps.as_mut().storage, + event_id, + verifier.clone(), + pool_contract.clone(), + block_height_started, + ) + .unwrap(); + } + } + + for (pool_id, params) in pool_params { + let rewards_to_add = params.rewards_per_epoch; + add_rewards(mock_deps.as_mut().storage, pool_id.clone(), rewards_to_add).unwrap(); + + let rewards = distribute_rewards( + mock_deps.as_mut().storage, + pool_id.clone(), + block_height_started + base_epoch_duration * EPOCH_PAYOUT_DELAY, // this is long enough for the first pool to pay out, but not the second + None, + ) + .unwrap(); + + if pool_id == pools[0] { + assert_eq!(rewards.len(), 1); + } else { + assert_eq!(rewards.len(), 0); + } + } + } + /// Tests that rewards are distributed correctly based on participation #[test] fn successfully_distribute_rewards() { @@ -770,6 +1228,10 @@ mod test { let epoch_duration = 1000u64; let rewards_per_epoch = 100u128; let participation_threshold = (2, 3); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("pool_contract"), + }; let mut mock_deps = setup_with_params( cur_epoch_num, @@ -777,6 +1239,7 @@ mod test { epoch_duration, rewards_per_epoch, participation_threshold, + pool_id.clone(), ); let verifier1 = Addr::unchecked("verifier1"); let verifier2 = Addr::unchecked("verifier2"); @@ -812,11 +1275,6 @@ mod test { (verifier4.clone(), rewards_per_epoch / 4), ]); - let pool_id = PoolId { - chain_name: "mock-chain".parse().unwrap(), - contract: Addr::unchecked("pool_contract"), - }; - for (verifier, events_participated) in verifier_participation_per_epoch.clone() { for (epoch, events) in events_participated.iter().enumerate().take(epoch_count) { for event in events { @@ -832,8 +1290,8 @@ mod test { } } - // we add 2 epochs worth of rewards. There were 2 epochs of participation, but only 2 epochs where rewards should be given out - // These tests we are accounting correctly, and only removing from the pool when we actually give out rewards + // we add 2 epochs worth of rewards. There were 4 epochs of participation, but only 2 epochs where rewards should be given out + // 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 _ = add_rewards( mock_deps.as_mut().storage, @@ -870,6 +1328,10 @@ mod test { let epoch_duration = 1000u64; let rewards_per_epoch = 100u128; let participation_threshold = (1, 2); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("pool_contract"), + }; let mut mock_deps = setup_with_params( cur_epoch_num, @@ -877,12 +1339,9 @@ mod test { epoch_duration, rewards_per_epoch, participation_threshold, + pool_id.clone(), ); let verifier = Addr::unchecked("verifier"); - let pool_id = PoolId { - chain_name: "mock-chain".parse().unwrap(), - contract: Addr::unchecked("pool_contract"), - }; for height in block_height_started..block_height_started + epoch_duration * 9 { let event_id = height.to_string() + "event"; @@ -949,6 +1408,10 @@ mod test { let epoch_duration = 1000u64; let rewards_per_epoch = 100u128; let participation_threshold = (8, 10); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("pool_contract"), + }; let mut mock_deps = setup_with_params( cur_epoch_num, @@ -956,6 +1419,7 @@ mod test { epoch_duration, rewards_per_epoch, participation_threshold, + pool_id.clone(), ); let verifier = Addr::unchecked("verifier"); let pool_id = PoolId { @@ -1028,6 +1492,10 @@ mod test { let epoch_duration = 1000u64; let rewards_per_epoch = 100u128; let participation_threshold = (8, 10); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("pool_contract"), + }; let mut mock_deps = setup_with_params( cur_epoch_num, @@ -1035,6 +1503,7 @@ mod test { epoch_duration, rewards_per_epoch, participation_threshold, + pool_id.clone(), ); let verifier = Addr::unchecked("verifier"); let pool_id = PoolId { @@ -1095,6 +1564,10 @@ mod test { let epoch_duration = 1000u64; let rewards_per_epoch = 100u128; let participation_threshold = (8, 10); + let pool_id = PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("pool_contract"), + }; let mut mock_deps = setup_with_params( cur_epoch_num, @@ -1102,12 +1575,9 @@ mod test { epoch_duration, rewards_per_epoch, participation_threshold, + pool_id.clone(), ); let verifier = Addr::unchecked("verifier"); - let pool_id = PoolId { - chain_name: "mock-chain".parse().unwrap(), - contract: Addr::unchecked("pool_contract"), - }; let _ = record_participation( mock_deps.as_mut().storage, @@ -1144,14 +1614,108 @@ mod test { assert_eq!(err.current_context(), &ContractError::NoRewardsToDistribute); } + #[test] + fn cannot_record_participation_before_pool_is_created() { + let cur_epoch_num = 1u64; + let block_height_started = 250u64; + let mut mock_deps = + setup_multiple_pools_with_params(cur_epoch_num, block_height_started, vec![]); + + assert!(record_participation( + mock_deps.as_mut().storage, + "some-event".parse().unwrap(), + Addr::unchecked("verifier"), + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract") + }, + block_height_started + ) + .is_err()); + } + + #[test] + fn cannot_add_rewards_before_pool_is_created() { + let cur_epoch_num = 1u64; + let block_height_started = 250u64; + let mut mock_deps = + setup_multiple_pools_with_params(cur_epoch_num, block_height_started, vec![]); + assert!(add_rewards( + mock_deps.as_mut().storage, + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract") + }, + 100u128.try_into().unwrap(), + ) + .is_err()); + } + + #[test] + fn cannot_distribute_rewards_before_pool_is_created() { + let cur_epoch_num = 1u64; + let block_height_started = 250u64; + let mut mock_deps = + setup_multiple_pools_with_params(cur_epoch_num, block_height_started, vec![]); + assert!(distribute_rewards( + mock_deps.as_mut().storage, + PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract") + }, + block_height_started, + None + ) + .is_err()); + } + type MockDeps = OwnedDeps; + fn setup_multiple_pools_with_params( + cur_epoch_num: u64, + block_height_started: u64, + pools: Vec<(PoolId, Params)>, + ) -> MockDeps { + let current_epoch = Epoch { + epoch_num: cur_epoch_num, + block_height_started, + }; + + let mut deps = mock_dependencies(); + let storage = deps.as_mut().storage; + for (pool_id, params) in pools { + let params_snapshot = ParamsSnapshot { + params, + created_at: current_epoch.clone(), + }; + + state::save_rewards_pool( + storage, + &RewardsPool { + id: pool_id, + params: params_snapshot, + balance: Uint128::zero(), + }, + ) + .unwrap(); + } + + let config = Config { + rewards_denom: "AXL".to_string(), + }; + + CONFIG.save(storage, &config).unwrap(); + + deps + } + fn setup_with_params( cur_epoch_num: u64, block_height_started: u64, epoch_duration: u64, rewards_per_epoch: u128, participation_threshold: (u64, u64), + pool_id: PoolId, ) -> MockDeps { let rewards_per_epoch: nonempty::Uint128 = cosmwasm_std::Uint128::from(rewards_per_epoch) .try_into() @@ -1172,8 +1736,15 @@ mod test { let mut deps = mock_dependencies(); let storage = deps.as_mut().storage; - - state::save_params(storage, ¶ms_snapshot).unwrap(); + state::save_rewards_pool( + storage, + &RewardsPool { + id: pool_id, + params: params_snapshot, + balance: Uint128::zero(), + }, + ) + .unwrap(); let config = Config { rewards_denom: "AXL".to_string(), @@ -1184,7 +1755,12 @@ mod test { deps } - fn setup(cur_epoch_num: u64, block_height_started: u64, epoch_duration: u64) -> MockDeps { + fn setup( + cur_epoch_num: u64, + block_height_started: u64, + epoch_duration: u64, + pool_id: PoolId, + ) -> MockDeps { let participation_threshold = (1, 2); let rewards_per_epoch = 100u128; setup_with_params( @@ -1193,6 +1769,7 @@ mod test { epoch_duration, rewards_per_epoch, participation_threshold, + pool_id, ) } } diff --git a/contracts/rewards/src/contract/migrations/mod.rs b/contracts/rewards/src/contract/migrations/mod.rs index a73cc4eb4..1d185e9d7 100644 --- a/contracts/rewards/src/contract/migrations/mod.rs +++ b/contracts/rewards/src/contract/migrations/mod.rs @@ -1 +1 @@ -pub mod v0_4_0; +pub mod v1_0_0; diff --git a/contracts/rewards/src/contract/migrations/v0_4_0.rs b/contracts/rewards/src/contract/migrations/v0_4_0.rs deleted file mode 100644 index 115da34ae..000000000 --- a/contracts/rewards/src/contract/migrations/v0_4_0.rs +++ /dev/null @@ -1,153 +0,0 @@ -#![allow(deprecated)] - -use axelar_wasm_std::error::ContractError; -use axelar_wasm_std::permission_control; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Storage}; -use cw_storage_plus::Item; -use router_api::error::Error; - -use crate::contract::CONTRACT_NAME; -use crate::state; - -const BASE_VERSION: &str = "0.4.0"; - -pub fn migrate(storage: &mut dyn Storage) -> Result<(), ContractError> { - cw2::assert_contract_version(storage, CONTRACT_NAME, BASE_VERSION)?; - - set_generalized_permission_control(storage)?; - Ok(()) -} - -fn set_generalized_permission_control(storage: &mut dyn Storage) -> Result<(), Error> { - let old_config = CONFIG.load(storage)?; - - permission_control::set_governance(storage, &old_config.governance).map_err(Error::from)?; - - let new_config = &state::Config { - rewards_denom: old_config.rewards_denom, - }; - state::CONFIG.save(storage, new_config)?; - Ok(()) -} - -#[cw_serde] -#[deprecated(since = "0.4.0", note = "only used during migration")] -pub struct Config { - pub governance: Addr, - pub rewards_denom: String, -} - -#[deprecated(since = "0.4.0", note = "only used during migration")] -pub const CONFIG: Item = Item::new("config"); - -#[cfg(test)] -pub mod tests { - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; - - use crate::contract::migrations::v0_4_0; - use crate::contract::{execute, CONTRACT_NAME}; - use crate::msg::{ExecuteMsg, InstantiateMsg, Params}; - use crate::state; - use crate::state::{Epoch, ParamsSnapshot, PARAMS}; - - #[deprecated(since = "0.4.0", note = "only used during migration tests")] - fn instantiate( - deps: DepsMut, - env: Env, - _info: MessageInfo, - msg: InstantiateMsg, - ) -> Result { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, v0_4_0::BASE_VERSION)?; - - let governance = deps.api.addr_validate(&msg.governance_address)?; - - v0_4_0::CONFIG.save( - deps.storage, - &v0_4_0::Config { - governance, - rewards_denom: msg.rewards_denom, - }, - )?; - - PARAMS.save( - deps.storage, - &ParamsSnapshot { - params: msg.params, - created_at: Epoch { - epoch_num: 0, - block_height_started: env.block.height, - }, - }, - )?; - - Ok(Response::new()) - } - - #[test] - fn migrate_checks_contract_version() { - let mut deps = mock_dependencies(); - instantiate_contract(deps.as_mut(), "denom"); - cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, "something wrong").unwrap(); - - assert!(v0_4_0::migrate(deps.as_mut().storage).is_err()); - - cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, v0_4_0::BASE_VERSION) - .unwrap(); - - assert!(v0_4_0::migrate(deps.as_mut().storage).is_ok()); - } - - #[test] - fn migrate_config() { - let mut deps = mock_dependencies(); - let denom = "denom".to_string(); - instantiate_contract(deps.as_mut(), &denom); - - v0_4_0::migrate(&mut deps.storage).unwrap(); - - let new_config = state::CONFIG.load(&deps.storage).unwrap(); - assert_eq!(denom, new_config.rewards_denom); - } - - #[test] - fn migrate_governance_permission() { - let mut deps = mock_dependencies(); - - instantiate_contract(deps.as_mut(), "denom"); - - v0_4_0::migrate(&mut deps.storage).unwrap(); - - let msg = ExecuteMsg::UpdateParams { - params: Params { - epoch_duration: 100u64.try_into().unwrap(), - rewards_per_epoch: 1000u128.try_into().unwrap(), - participation_threshold: (1, 2).try_into().unwrap(), - }, - }; - assert!(execute( - deps.as_mut(), - mock_env(), - mock_info("anyone", &[]), - msg.clone(), - ) - .is_err()); - - assert!(execute(deps.as_mut(), mock_env(), mock_info("governance", &[]), msg).is_ok()); - } - - #[deprecated(since = "0.4.0", note = "only used during migration tests")] - pub fn instantiate_contract(deps: DepsMut, denom: impl Into) { - let msg = InstantiateMsg { - governance_address: "governance".to_string(), - rewards_denom: denom.into(), - params: Params { - epoch_duration: 100u64.try_into().unwrap(), - rewards_per_epoch: 1000u128.try_into().unwrap(), - participation_threshold: (1, 2).try_into().unwrap(), - }, - }; - instantiate(deps, mock_env(), mock_info("anyone", &[]), msg).unwrap(); - } -} diff --git a/contracts/rewards/src/contract/migrations/v1_0_0.rs b/contracts/rewards/src/contract/migrations/v1_0_0.rs new file mode 100644 index 000000000..3db5b8312 --- /dev/null +++ b/contracts/rewards/src/contract/migrations/v1_0_0.rs @@ -0,0 +1,175 @@ +#![allow(deprecated)] + +use axelar_wasm_std::error::ContractError; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Order, Storage, Uint128}; +use cw_storage_plus::{Item, Map}; + +use crate::contract::CONTRACT_NAME; +use crate::state::{self, ParamsSnapshot, PoolId}; + +const BASE_VERSION: &str = "1.0.0"; + +pub fn migrate(storage: &mut dyn Storage) -> Result<(), ContractError> { + cw2::assert_contract_version(storage, CONTRACT_NAME, BASE_VERSION)?; + + migrate_params(storage)?; + Ok(()) +} + +fn migrate_params(storage: &mut dyn Storage) -> Result<(), ContractError> { + let params = PARAMS.load(storage)?; + let pools = get_all_pools(storage)?; + + for pool in pools { + state::save_rewards_pool( + storage, + &state::RewardsPool { + params: params.to_owned(), + id: pool.id, + balance: pool.balance, + }, + )?; + } + PARAMS.remove(storage); + + Ok(()) +} + +const POOLS: Map = Map::new("pools"); + +fn get_all_pools(storage: &mut dyn Storage) -> Result, ContractError> { + POOLS + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|(_, pool)| pool).map_err(|err| err.into())) + .collect::, _>>() +} + +#[deprecated(since = "1.0.0", note = "only used during migration")] +const PARAMS: Item = Item::new("params"); + +#[cw_serde] +#[deprecated(since = "1.0.0", note = "only used during migration")] +struct RewardsPool { + pub id: PoolId, + pub balance: Uint128, +} + +#[cfg(test)] +pub mod tests { + use axelar_wasm_std::permission_control; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; + + use super::{RewardsPool, PARAMS, POOLS}; + use crate::contract::migrations::v1_0_0; + use crate::contract::CONTRACT_NAME; + use crate::msg::{InstantiateMsg, Params}; + use crate::state::{self, Config, Epoch, ParamsSnapshot, PoolId, CONFIG}; + + #[test] + fn migrate_rewards_pools() { + let mut deps = mock_dependencies(); + instantiate_contract(deps.as_mut(), "denom"); + + let test_pools = vec![ + RewardsPool { + id: PoolId { + chain_name: "mock-chain".parse().unwrap(), + contract: Addr::unchecked("contract-1"), + }, + balance: Uint128::from(250u128), + }, + RewardsPool { + id: PoolId { + chain_name: "mock-chain-2".parse().unwrap(), + contract: Addr::unchecked("contract-2"), + }, + balance: Uint128::from(100u128), + }, + ]; + + for pool in &test_pools { + POOLS + .save(deps.as_mut().storage, pool.id.to_owned(), pool) + .unwrap(); + } + let params = PARAMS.load(deps.as_mut().storage).unwrap(); + + v1_0_0::migrate(deps.as_mut().storage).unwrap(); + + for pool in &test_pools { + let new_pool = + state::load_rewards_pool(deps.as_mut().storage, pool.id.to_owned()).unwrap(); + assert_eq!( + new_pool, + state::RewardsPool { + id: pool.id.to_owned(), + balance: pool.balance, + params: params.clone() + } + ); + } + } + + #[test] + fn migrate_checks_contract_version() { + let mut deps = mock_dependencies(); + instantiate_contract(deps.as_mut(), "denom"); + cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, "something wrong").unwrap(); + + assert!(v1_0_0::migrate(deps.as_mut().storage).is_err()); + + cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, v1_0_0::BASE_VERSION) + .unwrap(); + + assert!(v1_0_0::migrate(deps.as_mut().storage).is_ok()); + } + + #[deprecated(since = "0.4.0", note = "only used during migration tests")] + pub fn instantiate_contract(deps: DepsMut, denom: impl Into) { + let msg = InstantiateMsg { + governance_address: "governance".to_string(), + rewards_denom: denom.into(), + }; + instantiate(deps, mock_env(), mock_info("anyone", &[]), msg).unwrap(); + } + + #[deprecated(since = "0.4.0", note = "only used during migration tests")] + fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, + ) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, v1_0_0::BASE_VERSION)?; + + let governance = deps.api.addr_validate(&msg.governance_address)?; + permission_control::set_governance(deps.storage, &governance)?; + + CONFIG.save( + deps.storage, + &Config { + rewards_denom: msg.rewards_denom, + }, + )?; + + let params = Params { + epoch_duration: 100u64.try_into().unwrap(), + rewards_per_epoch: 1000u128.try_into().unwrap(), + participation_threshold: (1, 2).try_into().unwrap(), + }; + PARAMS.save( + deps.storage, + &ParamsSnapshot { + params, + created_at: Epoch { + epoch_num: 0, + block_height_started: env.block.height, + }, + }, + )?; + + Ok(Response::new()) + } +} diff --git a/contracts/rewards/src/contract/query.rs b/contracts/rewards/src/contract/query.rs index 9da88e320..998770ce2 100644 --- a/contracts/rewards/src/contract/query.rs +++ b/contracts/rewards/src/contract/query.rs @@ -11,7 +11,7 @@ pub fn rewards_pool( block_height: u64, ) -> Result { let pool = state::load_rewards_pool(storage, pool_id.clone())?; - let current_params = state::load_params(storage); + let current_params = pool.params; let cur_epoch = Epoch::current(¤t_params, block_height)?; // the params could have been updated since the tally was created. Therefore we use the params from the @@ -42,7 +42,7 @@ pub fn participation( let epoch_num = match epoch_num { Some(num) => num, None => { - let current_params = state::load_params(storage); + let current_params = state::load_rewards_pool_params(storage, pool_id.clone())?; Epoch::current(¤t_params, block_height)?.epoch_num } }; @@ -94,9 +94,9 @@ mod tests { let rewards_pool = RewardsPool { id: pool_id.clone(), balance: initial_balance, + params: params_snapshot.clone(), }; - state::save_params(storage, ¶ms_snapshot).unwrap(); state::save_rewards_pool(storage, &rewards_pool).unwrap(); (params_snapshot, pool_id) diff --git a/contracts/rewards/src/error.rs b/contracts/rewards/src/error.rs index 169222898..11aaf4de3 100644 --- a/contracts/rewards/src/error.rs +++ b/contracts/rewards/src/error.rs @@ -1,9 +1,12 @@ use axelar_wasm_std::IntoContractError; -use cosmwasm_std::OverflowError; +use cosmwasm_std::{OverflowError, StdError}; use thiserror::Error; #[derive(Error, Debug, PartialEq, IntoContractError)] pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + #[error("error saving params")] SaveParams, @@ -16,6 +19,9 @@ pub enum ContractError { #[error("error saving rewards pool")] SaveRewardsPool, + #[error("error updating rewards pool")] + UpdateRewardsPool, + #[error("error saving rewards watermark")] SaveRewardsWatermark, @@ -31,6 +37,9 @@ pub enum ContractError { #[error("rewards pool not found")] RewardsPoolNotFound, + #[error("rewards pool already exists")] + RewardsPoolAlreadyExists, + #[error("error loading rewards watermark")] LoadRewardsWatermark, diff --git a/contracts/rewards/src/msg.rs b/contracts/rewards/src/msg.rs index 64d46a317..be31741f3 100644 --- a/contracts/rewards/src/msg.rs +++ b/contracts/rewards/src/msg.rs @@ -12,7 +12,6 @@ use crate::state::{Epoch, PoolId}; pub struct InstantiateMsg { pub governance_address: String, pub rewards_denom: String, - pub params: Params, } #[cw_serde] @@ -35,6 +34,7 @@ pub struct Params { #[derive(EnsurePermissions)] pub enum ExecuteMsg { /// Log a specific verifier as participating in a specific event. Verifier weights are ignored + /// This call will error if the pool does not yet exist. /// /// TODO: For batched voting, treating the entire batch as a single event can be problematic. /// A verifier may vote correctly for 9 out of 10 messages in a batch, but the verifier's participation @@ -50,6 +50,7 @@ pub enum ExecuteMsg { }, /// 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 verifier + /// This call will error if the pool does not yet exist. #[permission(Any)] DistributeRewards { pool_id: PoolId, @@ -57,14 +58,20 @@ pub enum ExecuteMsg { epoch_count: Option, }, - /// Start a new reward pool for the given contract if none exists. Otherwise, add tokens to an existing reward pool. + /// Add tokens to an existing rewards pool. /// Any attached funds with a denom matching the rewards denom are added to the pool. + /// This call will error if the pool does not yet exist. #[permission(Any)] AddRewards { pool_id: PoolId }, - /// Overwrites the currently stored params. Callable only by governance. + /// Overwrites the currently stored params for the specified pool. Callable only by governance. + /// This call will error if the pool does not yet exist. + #[permission(Governance)] + UpdatePoolParams { params: Params, pool_id: PoolId }, + + /// Creates a rewards pool with the specified pool ID and parameters. Callable only by governance. #[permission(Governance)] - UpdateParams { params: Params }, + CreatePool { params: Params, pool_id: PoolId }, } #[cw_serde] diff --git a/contracts/rewards/src/state.rs b/contracts/rewards/src/state.rs index 400774dd5..159898b26 100644 --- a/contracts/rewards/src/state.rs +++ b/contracts/rewards/src/state.rs @@ -11,6 +11,21 @@ use router_api::ChainName; use crate::error::ContractError; use crate::msg::Params; +/// 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, pool id) pair to an Event +const EVENTS: Map<(String, PoolId), Event> = Map::new("events"); + +/// Maps the id to the rewards pool for given chain and contract +const POOLS: Map = Map::new("pools"); + +/// 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"); + #[cw_serde] pub struct Config { pub rewards_denom: String, @@ -230,16 +245,10 @@ impl Epoch { pub struct RewardsPool { pub id: PoolId, pub balance: Uint128, + pub params: ParamsSnapshot, } impl RewardsPool { - pub fn new(id: PoolId) -> Self { - RewardsPool { - id, - balance: Uint128::zero(), - } - } - pub fn sub_reward(mut self, reward: Uint128) -> Result { self.balance = self .balance @@ -250,32 +259,10 @@ impl RewardsPool { } } -/// Current rewards parameters, along with when the params were updated -pub const PARAMS: Item = Item::new("params"); - -/// 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, pool id) pair to an Event -const EVENTS: Map<(String, PoolId), Event> = Map::new("events"); - -/// Maps the id to the rewards pool for given chain and contract -const POOLS: Map = Map::new("pools"); - -/// 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"); - pub fn load_config(storage: &dyn Storage) -> Config { CONFIG.load(storage).expect("couldn't load config") } -pub fn load_params(storage: &dyn Storage) -> ParamsSnapshot { - PARAMS.load(storage).expect("params should exist") -} - pub fn load_rewards_watermark( storage: &dyn Storage, pool_id: PoolId, @@ -314,29 +301,21 @@ pub fn may_load_rewards_pool( .change_context(ContractError::LoadRewardsPool) } -pub fn load_rewards_pool_or_new( +pub fn load_rewards_pool( storage: &dyn Storage, pool_id: PoolId, ) -> Result { - may_load_rewards_pool(storage, pool_id.clone()) - .map(|pool| pool.unwrap_or(RewardsPool::new(pool_id))) + may_load_rewards_pool(storage, pool_id.clone())? + .ok_or(ContractError::RewardsPoolNotFound.into()) } -pub fn load_rewards_pool( +pub fn load_rewards_pool_params( storage: &dyn Storage, pool_id: PoolId, -) -> Result { +) -> Result { may_load_rewards_pool(storage, pool_id.clone())? .ok_or(ContractError::RewardsPoolNotFound.into()) -} - -pub fn save_params( - storage: &mut dyn Storage, - params: &ParamsSnapshot, -) -> Result<(), ContractError> { - PARAMS - .save(storage, params) - .change_context(ContractError::SaveParams) + .map(|pool| pool.params) } pub fn save_rewards_watermark( @@ -382,6 +361,41 @@ pub fn save_rewards_pool( .change_context(ContractError::SaveRewardsPool) } +pub fn update_pool_params( + storage: &mut dyn Storage, + pool_id: &PoolId, + updated_params: &ParamsSnapshot, +) -> Result { + POOLS + .update(storage, pool_id.clone(), |pool| match pool { + None => Err(ContractError::RewardsPoolNotFound), + Some(pool) => Ok(RewardsPool { + id: pool_id.to_owned(), + balance: pool.balance, + params: updated_params.to_owned(), + }), + }) + .change_context(ContractError::UpdateRewardsPool) +} + +pub fn pool_exists(storage: &mut dyn Storage, pool_id: &PoolId) -> Result { + POOLS + .may_load(storage, pool_id.to_owned()) + .change_context(ContractError::LoadRewardsPool) + .map(|pool| pool.is_some()) +} + +pub fn current_epoch( + storage: &mut dyn Storage, + pool_id: &PoolId, + cur_block_height: u64, +) -> Result { + Epoch::current( + &load_rewards_pool_params(storage, pool_id.to_owned())?, + cur_block_height, + ) +} + pub enum StorageState { Existing(T), New(T), @@ -477,12 +491,24 @@ mod test { #[test] fn sub_reward_from_pool() { + let params = ParamsSnapshot { + params: Params { + participation_threshold: (Uint64::new(1), Uint64::new(2)).try_into().unwrap(), + epoch_duration: 100u64.try_into().unwrap(), + rewards_per_epoch: Uint128::from(1000u128).try_into().unwrap(), + }, + created_at: Epoch { + epoch_num: 1, + block_height_started: 1, + }, + }; let pool = RewardsPool { id: PoolId { chain_name: "mock-chain".parse().unwrap(), contract: Addr::unchecked("pool_contract"), }, balance: Uint128::from(100u128), + params, }; let new_pool = pool.sub_reward(Uint128::from(50u128)).unwrap(); assert_eq!(new_pool.balance, Uint128::from(50u128)); @@ -494,41 +520,6 @@ mod test { )); } - #[test] - fn save_and_load_params() { - let mut mock_deps = mock_dependencies(); - let params = ParamsSnapshot { - params: Params { - participation_threshold: (Uint64::new(1), Uint64::new(2)).try_into().unwrap(), - epoch_duration: 100u64.try_into().unwrap(), - rewards_per_epoch: Uint128::from(1000u128).try_into().unwrap(), - }, - created_at: Epoch { - epoch_num: 1, - block_height_started: 1, - }, - }; - // save an initial params, then load it - assert!(save_params(mock_deps.as_mut().storage, ¶ms).is_ok()); - let loaded = load_params(mock_deps.as_ref().storage); - assert_eq!(loaded, params); - - // now store a new params, and check that it was updated - let new_params = ParamsSnapshot { - params: Params { - epoch_duration: 200u64.try_into().unwrap(), - ..params.params - }, - created_at: Epoch { - epoch_num: 2, - block_height_started: 101, - }, - }; - assert!(save_params(mock_deps.as_mut().storage, &new_params).is_ok()); - let loaded = load_params(mock_deps.as_mut().storage); - assert_eq!(loaded, new_params); - } - #[test] fn save_and_load_rewards_watermark() { let mut mock_deps = mock_dependencies(); @@ -711,30 +702,31 @@ mod test { #[test] fn save_and_load_rewards_pool() { + let params = ParamsSnapshot { + params: Params { + participation_threshold: (Uint64::new(1), Uint64::new(2)).try_into().unwrap(), + epoch_duration: 100u64.try_into().unwrap(), + rewards_per_epoch: Uint128::from(1000u128).try_into().unwrap(), + }, + created_at: Epoch { + epoch_num: 1, + block_height_started: 1, + }, + }; let mut mock_deps = mock_dependencies(); let chain_name: ChainName = "mock-chain".parse().unwrap(); - let pool = RewardsPool::new(PoolId::new( - chain_name.clone(), - Addr::unchecked("some contract"), - )); + let pool = RewardsPool { + id: PoolId::new(chain_name.clone(), Addr::unchecked("some contract")), + params, + balance: Uint128::zero(), + }; let res = save_rewards_pool(mock_deps.as_mut().storage, &pool); assert!(res.is_ok()); - let loaded = load_rewards_pool_or_new(mock_deps.as_ref().storage, pool.id.clone()); + let loaded = load_rewards_pool(mock_deps.as_ref().storage, pool.id.clone()); assert!(loaded.is_ok()); assert_eq!(loaded.unwrap(), pool); - - // return new pool when pool is not found - let loaded = load_rewards_pool_or_new( - mock_deps.as_ref().storage, - 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/integration-tests/src/rewards_contract.rs b/integration-tests/src/rewards_contract.rs index 0e0118739..fb8572b1b 100644 --- a/integration-tests/src/rewards_contract.rs +++ b/integration-tests/src/rewards_contract.rs @@ -9,12 +9,7 @@ pub struct RewardsContract { } impl RewardsContract { - pub fn instantiate_contract( - app: &mut App, - governance: Addr, - rewards_denom: String, - params: rewards::msg::Params, - ) -> Self { + pub fn instantiate_contract(app: &mut App, governance: Addr, rewards_denom: String) -> Self { let code = ContractWrapper::new( rewards::contract::execute, rewards::contract::instantiate, @@ -29,7 +24,6 @@ impl RewardsContract { &rewards::msg::InstantiateMsg { governance_address: governance.to_string(), rewards_denom, - params, }, &[], "rewards", diff --git a/integration-tests/tests/test_utils/mod.rs b/integration-tests/tests/test_utils/mod.rs index 3b4a976be..ace32837b 100644 --- a/integration-tests/tests/test_utils/mod.rs +++ b/integration-tests/tests/test_utils/mod.rs @@ -375,7 +375,6 @@ pub fn setup_protocol(service_name: nonempty::String) -> Protocol { &mut app, governance_address.clone(), AXL_DENOMINATION.to_string(), - rewards_params.clone(), ); let multisig = MultisigContract::instantiate_contract( @@ -696,6 +695,38 @@ pub fn setup_chain( ); assert!(response.is_ok()); + let rewards_params = rewards::msg::Params { + epoch_duration: nonempty::Uint64::try_from(10u64).unwrap(), + rewards_per_epoch: Uint128::from(100u128).try_into().unwrap(), + participation_threshold: (1, 2).try_into().unwrap(), + }; + + let response = protocol.rewards.execute( + &mut protocol.app, + protocol.governance_address.clone(), + &rewards::msg::ExecuteMsg::CreatePool { + pool_id: PoolId { + chain_name: chain_name.clone(), + contract: voting_verifier.contract_addr.clone(), + }, + params: rewards_params.clone(), + }, + ); + assert!(response.is_ok()); + + let response = protocol.rewards.execute( + &mut protocol.app, + protocol.governance_address.clone(), + &rewards::msg::ExecuteMsg::CreatePool { + pool_id: PoolId { + chain_name: chain_name.clone(), + contract: protocol.multisig.contract_addr.clone(), + }, + params: rewards_params, + }, + ); + assert!(response.is_ok()); + let response = protocol.rewards.execute_with_funds( &mut protocol.app, protocol.genesis_address.clone(),