diff --git a/README.md b/README.md index 51e8db6..67d8b67 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,9 @@ At the end of each execution, `revenue_token` balance is read and is deposited t - code-id `3147` - address `kujira158ydy6qlfq7khtnj5lj9a5dy25ep8hece4d0lqngzxqrwuz6dctsdl5eqx` + +### Mainnet + +- code-id `282` +- `kujira1xajlwpfpjnvwehurrj2w7d8ru6sm4579vzfp44c6fd5sj86u6tvqdk6mjn`: USK target +- `kujira1x97ay4eq7uv7hh59ytdxm3lsz567yz9wrwn74gq7dsspuapqvjtq03tajj`: KUJI target diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt index 4333759..20bd600 100644 --- a/artifacts/checksums.txt +++ b/artifacts/checksums.txt @@ -1 +1 @@ -3e32932f38991ab7fb572aaa0aea4792b6d72ff034b7a9927dfdcd790e8b940b kujira_revenue_converter.wasm +30e7100ba9fe0599593ca45bf9b42efbd0b317ee11e1fa702dfda797d1a2e01a kujira_revenue_converter.wasm diff --git a/artifacts/kujira_revenue_converter.wasm b/artifacts/kujira_revenue_converter.wasm index 5162872..081d5c3 100644 Binary files a/artifacts/kujira_revenue_converter.wasm and b/artifacts/kujira_revenue_converter.wasm differ diff --git a/src/contract.rs b/src/contract.rs index 0848c92..8c93f6f 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_json_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Event, MessageInfo, - Reply, Response, StdResult, SubMsg, + QuerierWrapper, Reply, Response, StdResult, Storage, SubMsg, }; use kujira::Denom; @@ -18,7 +18,10 @@ const CONTRACT_NAME: &str = "crates.io:kujira-revenue-converter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: ()) -> Result { +pub fn migrate(deps: DepsMut, _env: Env, msg: InstantiateMsg) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let config = Config::from(msg); + config.save(deps.storage)?; Ok(Response::default()) } @@ -82,25 +85,38 @@ pub fn execute( if info.sender != config.executor { return Err(ContractError::Unauthorized {}); } - - if let Some((action, msg)) = get_action_msg(deps, &env.contract.address)? { - let event = - Event::new("revenue/run").add_attribute("denom", action.denom.to_string()); - return Ok(Response::default() - .add_event(event) - .add_submessage(SubMsg::reply_always(msg, 0))); + let action_msg = get_action_msg(deps.storage, deps.querier, &env.contract.address)?; + + match action_msg { + Some((action, msg)) => { + let event = + Event::new("revenue/run").add_attribute("denom", action.denom.to_string()); + Ok(Response::default() + .add_event(event) + .add_submessage(SubMsg::reply_always(msg, 0))) + } + // If there's no compatible action, skip to the reply + None => { + let mut sends: Vec = vec![]; + for target in config.target_denoms.clone() { + distribute_denom(deps.as_ref(), &env, &config, &mut sends, target)?; + } + + Ok(Response::default().add_messages(sends)) + } } - Ok(Response::default()) } } } -fn get_action_msg(deps: DepsMut, contract: &Addr) -> StdResult> { +fn get_action_msg( + storage: &mut dyn Storage, + querier: QuerierWrapper, + contract: &Addr, +) -> StdResult> { // Fetch the next action in the iterator - if let Some(action) = Action::next(deps.storage)? { - let balance = deps - .querier - .query_balance(contract, action.denom.to_string())?; + if let Some(action) = Action::next(storage)? { + let balance = querier.query_balance(contract, action.denom.to_string())?; return match action.execute(balance)? { None => Ok(None), Some(msg) => Ok(Some((action, msg))), @@ -111,38 +127,17 @@ fn get_action_msg(deps: DepsMut, contract: &Addr) -> StdResult Result { - let config = Config::load(deps.storage)?; - let balance = deps - .querier - .query_balance(env.contract.address, config.target_denom.to_string())?; - let send = if balance.amount.is_zero() { - vec![] - } else { - let total_weight = config.target_addresses.iter().fold(0, |a, e| e.1 + a); - let mut sends = vec![]; - let mut remaining = balance.amount; - let mut targets = config.target_addresses.iter().peekable(); + execute_reply(deps.as_ref(), env) +} - while let Some((addr, weight)) = targets.next() { - let amount = if targets.peek().is_none() { - remaining - } else { - balance - .amount - .mul_floor(Decimal::from_ratio(*weight, total_weight)) - }; - if amount.is_zero() { - continue; - } - remaining -= amount; - sends.push(config.target_denom.send(&addr, &balance.amount)); - } +pub fn execute_reply(deps: Deps, env: Env) -> Result { + let config = Config::load(deps.storage)?; + let mut sends: Vec = vec![]; + for target in config.target_denoms.clone() { + distribute_denom(deps, &env, &config, &mut sends, target)?; + } - sends - }; - Ok(Response::default() - .add_messages(send) - .add_event(Event::new("revenue/reply").add_attribute("send", balance.to_string()))) + Ok(Response::default().add_messages(sends)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -161,14 +156,48 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } } +fn distribute_denom( + deps: Deps, + env: &Env, + config: &Config, + sends: &mut Vec, + denom: Denom, +) -> StdResult<()> { + let balance = deps + .querier + .query_balance(env.contract.address.clone(), denom.to_string())?; + + let total_weight = config.target_addresses.iter().fold(0, |a, e| e.1 + a); + if !balance.amount.is_zero() { + let mut remaining = balance.amount; + let mut targets = config.target_addresses.iter().peekable(); + + while let Some((addr, weight)) = targets.next() { + let amount = if targets.peek().is_none() { + remaining + } else { + let ratio = Decimal::from_ratio(*weight, total_weight); + balance.amount.mul_floor(ratio) + }; + + if amount.is_zero() { + continue; + } + remaining -= amount; + sends.push(denom.send(&addr, &amount)) + } + }; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; use cosmwasm_std::{ - coin, from_json, + coin, coins, from_json, testing::{mock_dependencies, mock_dependencies_with_balances, mock_env, mock_info}, - Uint128, + BankMsg, ReplyOn, Uint128, }; use kujira::fee_address; @@ -178,7 +207,7 @@ mod tests { let info = mock_info("owner", &vec![]); let msg = InstantiateMsg { owner: Addr::unchecked("owner"), - target_denom: Denom::from("ukuji"), + target_denoms: vec![Denom::from("ukuji"), Denom::from("another")], target_addresses: vec![(fee_address(), 1)], executor: Addr::unchecked("executor"), }; @@ -186,7 +215,10 @@ mod tests { let config: ConfigResponse = from_json(query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap()).unwrap(); assert_eq!(config.owner, Addr::unchecked("owner")); - assert_eq!(config.target_denom, Denom::from("ukuji")); + assert_eq!( + config.target_denoms, + vec![Denom::from("ukuji"), Denom::from("another")], + ); let status: StatusResponse = from_json(query(deps.as_ref(), mock_env(), QueryMsg::Status {}).unwrap()).unwrap(); assert_eq!(status.last, None); @@ -200,7 +232,7 @@ mod tests { let info = mock_info("owner", &vec![]); let msg = InstantiateMsg { owner: Addr::unchecked("owner"), - target_denom: Denom::from("ukuji"), + target_denoms: vec![Denom::from("ukuji"), Denom::from("another")], target_addresses: vec![(fee_address(), 1)], executor: Addr::unchecked("executor"), }; @@ -308,7 +340,7 @@ mod tests { let info = mock_info("contract-0", &vec![]); let msg = InstantiateMsg { owner: Addr::unchecked("owner"), - target_denom: Denom::from("ukuji"), + target_denoms: vec![Denom::from("ukuji"), Denom::from("another")], target_addresses: vec![(fee_address(), 1)], executor: Addr::unchecked("executor"), }; @@ -422,4 +454,70 @@ mod tests { ) .unwrap(); } + + #[test] + fn distribution() { + let mut deps = mock_dependencies_with_balances(&[( + "cosmos2contract", + &[coin(1000u128, "ukuji"), coin(2000u128, "another")], + )]); + let info = mock_info("contract-0", &vec![]); + let msg = InstantiateMsg { + owner: Addr::unchecked("owner"), + target_denoms: vec![Denom::from("ukuji"), Denom::from("another")], + target_addresses: vec![(fee_address(), 1), (Addr::unchecked("another"), 3)], + executor: Addr::unchecked("executor"), + }; + instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + // Dummy action to make sure it cranks the reply + set_action(deps.as_mut(), "token-a", "contract-a", Uint128::MAX); + + // Make sure that execution ends when there are no actions + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("executor", &vec![]), + ExecuteMsg::Run {}, + ) + .unwrap(); + + assert!(res.messages.contains(&SubMsg { + id: 0, + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: "kujira17xpfvakm2amg962yls6f84z3kell8c5lp3pcxh".to_string(), + amount: coins(250, "ukuji"), + },), + gas_limit: None, + reply_on: ReplyOn::Never, + })); + assert!(res.messages.contains(&SubMsg { + id: 0, + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: "another".to_string(), + amount: coins(750, "ukuji"), + },), + gas_limit: None, + reply_on: ReplyOn::Never, + })); + + assert!(res.messages.contains(&SubMsg { + id: 0, + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: "kujira17xpfvakm2amg962yls6f84z3kell8c5lp3pcxh".to_string(), + amount: coins(500, "another"), + },), + gas_limit: None, + reply_on: ReplyOn::Never, + })); + + assert!(res.messages.contains(&SubMsg { + id: 0, + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: "another".to_string(), + amount: coins(1500, "another"), + },), + gas_limit: None, + reply_on: ReplyOn::Never, + })); + } } diff --git a/src/msg.rs b/src/msg.rs index bfa5696..c39c136 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -8,7 +8,7 @@ use crate::state::Action; pub struct InstantiateMsg { pub owner: Addr, pub executor: Addr, - pub target_denom: Denom, + pub target_denoms: Vec, pub target_addresses: Vec<(Addr, u8)>, } @@ -36,7 +36,7 @@ pub enum QueryMsg { pub struct ConfigResponse { pub owner: Addr, pub executor: Addr, - pub target_denom: Denom, + pub target_denoms: Vec, pub target_addresses: Vec<(Addr, u8)>, } diff --git a/src/state.rs b/src/state.rs index aef64bb..49120b2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -20,8 +20,8 @@ pub struct Config { /// The address permitted to execute the crank pub executor: Addr, - /// The denom that is transferred to the fee_collector at the end of every execution - pub target_denom: Denom, + /// The denoms that are transferred to the fee_collector at the end of every execution + pub target_denoms: Vec, /// The final destinations that `target_denom` is sent to (address, weight) pub target_addresses: Vec<(Addr, u8)>, @@ -42,7 +42,7 @@ impl From for Config { Self { owner: value.owner, executor: value.executor, - target_denom: value.target_denom, + target_denoms: value.target_denoms, target_addresses: value.target_addresses, } } @@ -53,7 +53,7 @@ impl From for ConfigResponse { Self { owner: value.owner, executor: value.executor, - target_denom: value.target_denom, + target_denoms: value.target_denoms, target_addresses: value.target_addresses, } }