Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch rewards distribution #125

Merged
merged 25 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
78e9254
converter: batch rewards message
uint Sep 13, 2023
0497287
provider: handle batch reward distribution
uint Sep 15, 2023
22134b3
provider: abstract away creating an app with balances
uint Sep 18, 2023
0ca318c
provider: abstract away activating validators
uint Sep 18, 2023
36df343
provider tests: abstract away staking
uint Sep 18, 2023
9c4d9b6
provider: more test utils
uint Sep 18, 2023
b47b80b
provider: batch reward distribution tests
uint Sep 18, 2023
646bd49
provider: remove invalid validator test
uint Sep 19, 2023
61123f3
style
uint Sep 19, 2023
a56fff0
virtual-staking: batch distribute messages
uint Sep 19, 2023
5abf297
converter: virtual_staking auth
uint Sep 19, 2023
da51bb4
style
uint Sep 19, 2023
32dce0d
converter: test unauthorized scenarios
uint Sep 19, 2023
8c102c4
ext-stk: give test utils their own module
uint Sep 21, 2023
60f8280
virt-stk: fix reply_rewards bugs
uint Sep 25, 2023
49e26ab
virt-stk: reply_rewards - fix edge case
uint Sep 25, 2023
9b3ec02
virt-stk: reply_rewards tests
uint Sep 21, 2023
dca8913
virt-stk: refactor tests
uint Sep 25, 2023
4bbc757
virt-stk: add test for when the last push is 0
uint Sep 25, 2023
9de5fd7
virt-stk: test utils bugfix
uint Sep 25, 2023
55c6219
virt-stk: one more reply_rewards test
uint Sep 25, 2023
da3aeb8
lints
uint Sep 25, 2023
7da61b5
Merge pull request #131 from osmosis-labs/improve-virtual-staking-tes…
uint Sep 27, 2023
a73fca4
virt-stk: Fix repeatedly distributing rewards
uint Sep 27, 2023
e35ce75
virt-stk: improve reply_rewards logic
uint Sep 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 41 additions & 32 deletions contracts/consumer/converter/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use cosmwasm_std::{
ensure_eq, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, DepsMut, Event, IbcMsg,
Reply, Response, SubMsg, SubMsgResponse, Validator, WasmMsg,
ensure_eq, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, DepsMut, Event,
MessageInfo, Reply, Response, SubMsg, SubMsgResponse, Uint128, Validator, WasmMsg,
};
use cw2::set_contract_version;
use cw_storage_plus::Item;
Expand All @@ -14,9 +14,7 @@ use mesh_apis::price_feed_api;
use mesh_apis::virtual_staking_api;

use crate::error::ContractError;
use crate::ibc::{
add_validators_msg, packet_timeout_rewards, tombstone_validators_msg, IBC_CHANNEL,
};
use crate::ibc::{add_validators_msg, make_ibc_packet, tombstone_validators_msg, IBC_CHANNEL};
use crate::msg::ConfigResponse;
use crate::state::Config;

Expand Down Expand Up @@ -266,6 +264,13 @@ impl ConverterContract<'_> {
};
Ok(msg.into())
}

fn ensure_authorized(&self, deps: &DepsMut, info: &MessageInfo) -> Result<(), ContractError> {
let virtual_stake = self.virtual_stake.load(deps.storage)?;
ensure_eq!(info.sender, virtual_stake, ContractError::Unauthorized {});

Ok(())
}
}

#[contract]
Expand All @@ -281,6 +286,8 @@ impl ConverterApi for ConverterContract<'_> {
mut ctx: ExecCtx,
validator: String,
) -> Result<Response, Self::Error> {
self.ensure_authorized(&ctx.deps, &ctx.info)?;

let config = self.config.load(ctx.deps.storage)?;
let denom = config.local_denom;
must_pay(&ctx.info, &denom)?;
Expand All @@ -290,15 +297,7 @@ impl ConverterApi for ConverterContract<'_> {
.add_attribute("validator", &validator)
.add_attribute("amount", rewards.amount.to_string());

// Create a packet for the provider, informing of distribution
let packet = ConsumerPacket::Distribute { validator, rewards };
let channel = IBC_CHANNEL.load(ctx.deps.storage)?;
let msg = IbcMsg::SendPacket {
channel_id: channel.endpoint.channel_id,
data: to_binary(&packet)?,
timeout: packet_timeout_rewards(&ctx.env),
};

let msg = make_ibc_packet(&mut ctx, ConsumerPacket::Distribute { validator, rewards })?;
Ok(Response::new().add_message(msg).add_event(event))
}

Expand All @@ -313,19 +312,34 @@ impl ConverterApi for ConverterContract<'_> {
mut ctx: ExecCtx,
payments: Vec<RewardInfo>,
) -> Result<Response, Self::Error> {
// TODO: Optimize this, when we actually get such calls
let mut resp = Response::new();
for RewardInfo { validator, reward } in payments {
let mut sub_ctx = ctx.branch();
sub_ctx.info.funds[0].amount = reward;
let r = self.distribute_reward(sub_ctx, validator)?;
// put all values on the parent one
resp = resp
.add_submessages(r.messages)
.add_attributes(r.attributes)
.add_events(r.events);
self.ensure_authorized(&ctx.deps, &ctx.info)?;

let config = self.config.load(ctx.deps.storage)?;
let denom = config.local_denom;

let summed_rewards: Uint128 = payments.iter().map(|reward_info| reward_info.reward).sum();
let sent = must_pay(&ctx.info, &denom)?;

if summed_rewards != sent {
return Err(ContractError::DistributeRewardsInvalidAmount {
sum: summed_rewards,
sent,
});
}
Ok(resp)

Ok(Response::new()
.add_events(payments.iter().map(|reward_info| {
Event::new("distribute_reward")
.add_attribute("validator", &reward_info.validator)
.add_attribute("amount", reward_info.reward)
}))
.add_message(make_ibc_packet(
&mut ctx,
ConsumerPacket::DistributeBatch {
rewards: payments,
uint marked this conversation as resolved.
Show resolved Hide resolved
denom,
},
)?))
}

/// Valset updates.
Expand All @@ -339,12 +353,7 @@ impl ConverterApi for ConverterContract<'_> {
additions: Vec<Validator>,
tombstones: Vec<Validator>,
) -> Result<Response, Self::Error> {
let virtual_stake = self.virtual_stake.load(ctx.deps.storage)?;
ensure_eq!(
ctx.info.sender,
virtual_stake,
ContractError::Unauthorized {}
);
self.ensure_authorized(&ctx.deps, &ctx.info)?;

// Send over IBC to the Consumer
let channel = IBC_CHANNEL.load(ctx.deps.storage)?;
Expand Down
5 changes: 4 additions & 1 deletion contracts/consumer/converter/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cosmwasm_std::StdError;
use cosmwasm_std::{StdError, Uint128};
use cw_utils::{ParseReplyError, PaymentError};
use mesh_apis::ibc::VersionError;
use thiserror::Error;
Expand Down Expand Up @@ -37,4 +37,7 @@ pub enum ContractError {

#[error("Invalid denom: {0}")]
InvalidDenom(String),

#[error("Sum of rewards ({sum}) doesn't match funds sent ({sent})")]
DistributeRewardsInvalidAmount { sum: Uint128, sent: Uint128 },
}
13 changes: 13 additions & 0 deletions contracts/consumer/converter/src/ibc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use mesh_apis::ibc::{
ack_success, validate_channel_order, AckWrapper, AddValidator, ConsumerPacket, ProtocolVersion,
ProviderPacket, StakeAck, TransferRewardsAck, UnstakeAck, PROTOCOL_NAME,
};
use sylvia::types::ExecCtx;

use crate::{contract::ConverterContract, error::ContractError};

Expand Down Expand Up @@ -261,3 +262,15 @@ pub fn ibc_packet_timeout(
};
Ok(IbcBasicResponse::new().add_message(msg))
}

pub(crate) fn make_ibc_packet(
ctx: &mut ExecCtx,
packet: ConsumerPacket,
) -> Result<IbcMsg, ContractError> {
let channel = IBC_CHANNEL.load(ctx.deps.storage)?;
Ok(IbcMsg::SendPacket {
channel_id: channel.endpoint.channel_id,
data: to_binary(&packet)?,
timeout: packet_timeout_rewards(&ctx.env),
})
}
186 changes: 185 additions & 1 deletion contracts/consumer/converter/src/multitest.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
mod virtual_staking_mock;

use cosmwasm_std::{coin, Addr, Decimal, StdError, Uint128, Validator};
use cosmwasm_std::{coin, coins, Addr, Decimal, StdError, Uint128, Validator};
use cw_multi_test::App as MtApp;
use mesh_apis::converter_api::RewardInfo;
use sylvia::multitest::App;

use crate::contract;
Expand Down Expand Up @@ -266,3 +267,186 @@ fn valset_update_works() {
})
);
}

#[test]
fn unauthorized() {
let app = App::default();

let SetupResponse { converter, .. } = setup(
&app,
SetupArgs {
owner: "owner",
admin: "admin",
discount: Decimal::percent(10),
native_per_foreign: Decimal::percent(40),
},
);

let err = converter
.converter_api_proxy()
.distribute_rewards(vec![
RewardInfo {
validator: "alice".to_string(),
reward: 33u128.into(),
},
RewardInfo {
validator: "bob".to_string(),
reward: 53u128.into(),
},
])
.call("mallory")
.unwrap_err();

assert_eq!(err, ContractError::Unauthorized);

let err = converter
.converter_api_proxy()
.distribute_reward("validator".to_string())
.call("mallory")
.unwrap_err();

assert_eq!(err, ContractError::Unauthorized);

let err = converter
.converter_api_proxy()
.valset_update(vec![], vec![])
.call("mallory")
.unwrap_err();

assert_eq!(err, ContractError::Unauthorized);
}

#[test]
fn distribute_rewards_invalid_amount_is_rejected() {
let owner = "sunny";
let admin = "theman";
let discount = Decimal::percent(10); // 1 OSMO worth of JUNO should give 0.9 OSMO of stake
let native_per_foreign = Decimal::percent(40); // 1 JUNO is worth 0.4 OSMO

let app = App::default();

let SetupResponse {
price_feed: _,
converter,
virtual_staking,
} = setup(
&app,
SetupArgs {
owner,
admin,
discount,
native_per_foreign,
},
);

app.app_mut().init_modules(|router, _, storage| {
router
.bank
.init_balance(
storage,
&virtual_staking.contract_addr,
coins(99999, "TOKEN"),
)
.unwrap();
});

let err = converter
.converter_api_proxy()
.distribute_rewards(vec![
RewardInfo {
validator: "alice".to_string(),
reward: 33u128.into(),
},
RewardInfo {
validator: "bob".to_string(),
reward: 53u128.into(),
},
])
.with_funds(&[coin(80, "TOKEN")])
.call(virtual_staking.contract_addr.as_str())
.unwrap_err();

assert_eq!(
err,
ContractError::DistributeRewardsInvalidAmount {
sum: 86u128.into(),
sent: 80u128.into()
}
);

let err = converter
.converter_api_proxy()
.distribute_rewards(vec![
RewardInfo {
validator: "alice".to_string(),
reward: 33u128.into(),
},
RewardInfo {
validator: "bob".to_string(),
reward: 53u128.into(),
},
])
.with_funds(&[coin(90, "TOKEN")])
.call(virtual_staking.contract_addr.as_str())
.unwrap_err();

assert_eq!(
err,
ContractError::DistributeRewardsInvalidAmount {
sum: 86u128.into(),
sent: 90u128.into()
}
);
}

#[test]
#[ignore = "unsupported by Sylvia"]
fn distribute_rewards_valid_amount() {
let owner = "sunny";
let admin = "theman";
let discount = Decimal::percent(10); // 1 OSMO worth of JUNO should give 0.9 OSMO of stake
let native_per_foreign = Decimal::percent(40); // 1 JUNO is worth 0.4 OSMO

let app = App::default();

let SetupResponse {
price_feed: _,
converter,
virtual_staking,
} = setup(
&app,
SetupArgs {
owner,
admin,
discount,
native_per_foreign,
},
);

app.app_mut().init_modules(|router, _, storage| {
router
.bank
.init_balance(
storage,
&virtual_staking.contract_addr,
coins(99999, "TOKEN"),
)
.unwrap();
});

converter
.converter_api_proxy()
.distribute_rewards(vec![
RewardInfo {
validator: "alice".to_string(),
reward: 33u128.into(),
},
RewardInfo {
validator: "bob".to_string(),
reward: 53u128.into(),
},
])
.with_funds(&[coin(86, "TOKEN")])
.call(virtual_staking.contract_addr.as_str())
.unwrap();
}
Loading
Loading