Skip to content

Commit

Permalink
Merge pull request #76 from osmosis-labs/reward-payments-1
Browse files Browse the repository at this point in the history
Reward payments 1
  • Loading branch information
ethanfrey authored Jun 28, 2023
2 parents df58e1a + 0f01711 commit 3e74dfd
Show file tree
Hide file tree
Showing 14 changed files with 633 additions and 315 deletions.
14 changes: 7 additions & 7 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# 0.3.0-beta

* IBC specification is added to the documents.
* IBC types and logic added to `mesh-api::ibc`
* `converter` and `external-staking` support ibc
* Handshake and channel creation
* Validator sync protocol (Consumer -> Provider)
* WIP: Staking protocol (Provider -> Consumer)
* TODO: Reward protocol (Consumer -> Provider)
- IBC specification is added to the documents.
- IBC types and logic added to `mesh-api::ibc`
- `converter` and `external-staking` support ibc
- Handshake and channel creation
- Validator sync protocol (Consumer -> Provider)
- WIP: Staking protocol (Provider -> Consumer)
- TODO: Reward protocol (Consumer -> Provider)
103 changes: 85 additions & 18 deletions contracts/consumer/converter/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use cosmwasm_std::{
ensure_eq, to_binary, Addr, Coin, Decimal, Deps, DepsMut, Event, Reply, Response, SubMsg,
SubMsgResponse, WasmMsg,
ensure_eq, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, DepsMut, Event, IbcMsg,
Reply, Response, SubMsg, SubMsgResponse, WasmMsg,
};
use cw2::set_contract_version;
use cw_storage_plus::Item;
use cw_utils::{nonpayable, parse_instantiate_response_data};
use cw_utils::{must_pay, nonpayable, parse_instantiate_response_data};
use mesh_apis::ibc::ConsumerPacket;
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, ReplyCtx};
use sylvia::{contract, schemars};

Expand All @@ -13,6 +14,7 @@ use mesh_apis::price_feed_api;
use mesh_apis::virtual_staking_api;

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

Expand All @@ -38,11 +40,11 @@ impl ConverterContract<'_> {
}
}

// TODO: there is a chicken and egg problem here.
// converter needs fixed address for virtual stake contract
// virtual stake contract needs fixed address for converter
// (Price feed can be made first and then converter second, that is no issue)
/// The caller of the instantiation will be the converter contract
/// We must first instantiate the price feed contract, then the converter contract.
/// The converter will then instantiate a virtual staking contract to work with it,
/// as they both need references to each other. The admin of the virtual staking
/// contract is taken as an explicit argument.
///
/// Discount is applied to foreign tokens after adjusting foreign/native price,
/// such that 0.3 discount means foreign assets have 70% of their value
#[msg(instantiate)]
Expand All @@ -56,12 +58,17 @@ impl ConverterContract<'_> {
admin: Option<String>,
) -> Result<Response, ContractError> {
nonpayable(&ctx.info)?;
// validate args
if discount > Decimal::one() {
return Err(ContractError::InvalidDiscount);
}
if remote_denom.is_empty() {
return Err(ContractError::InvalidDenom(remote_denom));
}
let config = Config {
price_feed: ctx.deps.api.addr_validate(&price_feed)?,
// TODO: better error if discount greater than 1 (this will panic)
price_adjustment: Decimal::one() - discount,
local_denom: ctx.deps.querier.query_bonded_denom()?,
// TODO: validation here? Just that it is non-empty?
remote_denom,
};
self.config.save(ctx.deps.storage, &config)?;
Expand Down Expand Up @@ -229,6 +236,34 @@ impl ConverterContract<'_> {
amount: converted,
})
}

pub(crate) fn transfer_rewards(
&self,
deps: Deps,
recipient: String,
rewards: Coin,
) -> Result<CosmosMsg, ContractError> {
// ensure the address is proper
let recipient = deps.api.addr_validate(&recipient)?;

// ensure this is the reward denom (same as staking denom)
let config = self.config.load(deps.storage)?;
ensure_eq!(
config.local_denom,
rewards.denom,
ContractError::WrongDenom {
sent: rewards.denom,
expected: config.local_denom
}
);

// send the coins
let msg = BankMsg::Send {
to_address: recipient.into(),
amount: vec![rewards],
};
Ok(msg.into())
}
}

#[contract]
Expand All @@ -237,25 +272,57 @@ impl ConverterApi for ConverterContract<'_> {
type Error = ContractError;

/// Rewards tokens (in native staking denom) are sent alongside the message, and should be distributed to all
/// stakers who staked on this validator.
/// stakers who staked on this validator. This is tracked on the provider, so we send an IBC packet there.
#[msg(exec)]
fn distribute_reward(&self, ctx: ExecCtx, validator: String) -> Result<Response, Self::Error> {
let _ = (ctx, validator);
todo!();
fn distribute_reward(
&self,
mut ctx: ExecCtx,
validator: String,
) -> Result<Response, Self::Error> {
let config = self.config.load(ctx.deps.storage)?;
let denom = config.local_denom;
must_pay(&ctx.info, &denom)?;
let rewards = ctx.info.funds.remove(0);

let event = Event::new("distribute_reward")
.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),
};

Ok(Response::new().add_message(msg).add_event(event))
}

/// This is a batch for of distribute_reward, including the payment for multiple validators.
/// This is a batch form of distribute_reward, including the payment for multiple validators.
/// This is more efficient than calling distribute_reward multiple times, but also more complex.
///
/// info.funds sent along with the message should be the sum of all rewards for all validators,
/// in the native staking denom.
#[msg(exec)]
fn distribute_rewards(
&self,
ctx: ExecCtx,
mut ctx: ExecCtx,
payments: Vec<RewardInfo>,
) -> Result<Response, Self::Error> {
let _ = (ctx, payments);
todo!();
// 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);
}
Ok(resp)
}
}
6 changes: 6 additions & 0 deletions contracts/consumer/converter/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ pub enum ContractError {

#[error("Invalid reply id: {0}")]
InvalidReplyId(u64),

#[error("Invalid discount, must be between 0.0 and 1.0")]
InvalidDiscount,

#[error("Invalid denom: {0}")]
InvalidDenom(String),
}
30 changes: 23 additions & 7 deletions contracts/consumer/converter/src/ibc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use cw_storage_plus::Item;

use mesh_apis::ibc::{
ack_success, validate_channel_order, AckWrapper, AddValidator, ConsumerPacket, ProtocolVersion,
ProviderPacket, StakeAck, UnstakeAck, PROTOCOL_NAME,
ProviderPacket, StakeAck, TransferRewardsAck, UnstakeAck, PROTOCOL_NAME,
};

use crate::{contract::ConverterContract, error::ContractError};
Expand All @@ -22,15 +22,24 @@ const SUPPORTED_IBC_PROTOCOL_VERSION: &str = "0.10.0";
const MIN_IBC_PROTOCOL_VERSION: &str = "0.10.0";

// IBC specific state
const IBC_CHANNEL: Item<IbcChannel> = Item::new("ibc_channel");
pub const IBC_CHANNEL: Item<IbcChannel> = Item::new("ibc_channel");

// Let those validator syncs take a day...
const DEFAULT_TIMEOUT: u64 = 24 * 60 * 60;
const DEFAULT_VALIDATOR_TIMEOUT: u64 = 24 * 60 * 60;
// But reward messages should go faster or timeout
const DEFAULT_REWARD_TIMEOUT: u64 = 60 * 60;

fn packet_timeout(env: &Env) -> IbcTimeout {
pub fn packet_timeout_validator(env: &Env) -> IbcTimeout {
// No idea about their blocktime, but 24 hours ahead of our view of the clock
// should be decently in the future.
let timeout = env.block.time.plus_seconds(DEFAULT_TIMEOUT);
let timeout = env.block.time.plus_seconds(DEFAULT_VALIDATOR_TIMEOUT);
IbcTimeout::with_timestamp(timeout)
}

pub fn packet_timeout_rewards(env: &Env) -> IbcTimeout {
// No idea about their blocktime, but 1 hour ahead of our view of the clock
// should be decently in the future.
let timeout = env.block.time.plus_seconds(DEFAULT_REWARD_TIMEOUT);
IbcTimeout::with_timestamp(timeout)
}

Expand Down Expand Up @@ -120,7 +129,7 @@ pub fn ibc_channel_connect(
let msg = IbcMsg::SendPacket {
channel_id: channel.endpoint.channel_id,
data: to_binary(&packet)?,
timeout: packet_timeout(&env),
timeout: packet_timeout_validator(&env),
};

Ok(IbcBasicResponse::new().add_message(msg))
Expand Down Expand Up @@ -175,6 +184,13 @@ pub fn ibc_packet_receive(
.add_events(response.events)
.add_attributes(response.attributes)
}
ProviderPacket::TransferRewards {
rewards, recipient, ..
} => {
let msg = contract.transfer_rewards(deps.as_ref(), recipient, rewards)?;
let ack = ack_success(&TransferRewardsAck {})?;
IbcReceiveResponse::new().set_ack(ack).add_message(msg)
}
};
Ok(res)
}
Expand Down Expand Up @@ -216,7 +232,7 @@ pub fn ibc_packet_timeout(
let msg = IbcMsg::SendPacket {
channel_id: msg.packet.src.channel_id,
data: msg.packet.data,
timeout: packet_timeout(&env),
timeout: packet_timeout_validator(&env),
};
Ok(IbcBasicResponse::new().add_message(msg))
}
96 changes: 85 additions & 11 deletions contracts/consumer/virtual-staking/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ use std::cmp::Ordering;
use std::collections::BTreeMap;

use cosmwasm_std::{
coin, ensure_eq, entry_point, Coin, CosmosMsg, DepsMut, DistributionMsg, Env, Response, SubMsg,
Uint128,
coin, ensure_eq, entry_point, to_binary, Coin, CosmosMsg, CustomQuery, DepsMut,
DistributionMsg, Env, Event, Reply, Response, StdResult, SubMsg, SubMsgResponse, Uint128,
WasmMsg,
};
use cw2::set_contract_version;
use cw_storage_plus::{Item, Map};
use cw_utils::nonpayable;
use mesh_apis::converter_api;
use mesh_bindings::{
TokenQuerier, VirtualStakeCustomMsg, VirtualStakeCustomQuery, VirtualStakeMsg,
};
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx};
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, ReplyCtx};
use sylvia::{contract, schemars};

use mesh_apis::virtual_staking_api::{self, SudoMsg, VirtualStakingApi};
Expand Down Expand Up @@ -89,12 +91,12 @@ impl VirtualStakingContract<'_> {
*/
fn handle_epoch(
&self,
deps: DepsMut<VirtualStakeCustomQuery>,
mut deps: DepsMut<VirtualStakeCustomQuery>,
env: Env,
) -> Result<Response<VirtualStakeCustomMsg>, ContractError> {
// withdraw rewards
let bonded = self.bonded.load(deps.storage)?;
let withdraw = withdraw_reward_msgs(&bonded);
let withdraw = withdraw_reward_msgs(deps.branch(), &bonded);
let resp = Response::new().add_submessages(withdraw);

let bond =
Expand Down Expand Up @@ -131,6 +133,60 @@ impl VirtualStakingContract<'_> {

Ok(resp)
}

#[msg(reply)]
fn reply(&self, ctx: ReplyCtx, reply: Reply) -> Result<Response, ContractError> {
match (reply.id, reply.result.into_result()) {
(REPLY_REWARDS_ID, Ok(result)) => self.reply_rewards(ctx.deps, ctx.env, result),
(REPLY_REWARDS_ID, Err(e)) => {
// We need to pop the REWARD_TARGETS so it doesn't get out of sync
let target = pop_target(ctx.deps)?;
// Ignore errors, so the rest doesn't fail, but report them.
let evt = Event::new("rewards_error")
.add_attribute("error", e)
.add_attribute("target", target);
Ok(Response::new().add_event(evt))
}
(id, _) => Err(ContractError::InvalidReplyId(id)),
}
}

/// This is called on each successful withdrawal
fn reply_rewards(
&self,
mut deps: DepsMut,
env: Env,
_reply: SubMsgResponse,
) -> Result<Response, ContractError> {
// Find the validator to assign the rewards to
let target = pop_target(deps.branch())?;

// Find all the tokens received here (consider it rewards to that validator)
let cfg = self.config.load(deps.storage)?;
let reward = deps
.querier
.query_balance(env.contract.address, cfg.denom)?;
if reward.amount.is_zero() {
return Ok(Response::new());
}

// Send them to the converter, assigned to proper validator
let msg = converter_api::ExecMsg::DistributeReward { validator: target };
let msg = WasmMsg::Execute {
contract_addr: cfg.converter.into_string(),
msg: to_binary(&msg)?,
funds: vec![reward],
};
let resp = Response::new().add_message(msg);
Ok(resp)
}
}

fn pop_target(deps: DepsMut) -> StdResult<String> {
let mut targets = REWARD_TARGETS.load(deps.storage)?;
let target = targets.pop().unwrap();
REWARD_TARGETS.save(deps.storage, &targets)?;
Ok(target)
}

fn calculate_rebalance(
Expand Down Expand Up @@ -168,15 +224,33 @@ fn calculate_rebalance(
msgs
}

// TODO: each submsg should have a callback to this contract to trigger sending the rewards to converter
// This will be done in a future PR
fn withdraw_reward_msgs(bonded: &[(String, Uint128)]) -> Vec<SubMsg<VirtualStakeCustomMsg>> {
const REWARD_TARGETS: Item<Vec<String>> = Item::new("reward_targets");
const REPLY_REWARDS_ID: u64 = 1;

/// Each of these messages will need to get a callback to distribute received rewards to the proper validator
/// To manage that, we store a queue of validators in an Item, one item for each SubMsg, and read them in reply.
/// Look at reply implementation that uses the value set here.
fn withdraw_reward_msgs<T: CustomQuery>(
deps: DepsMut<T>,
bonded: &[(String, Uint128)],
) -> Vec<SubMsg<VirtualStakeCustomMsg>> {
// We need to make a list, so we know where to send the rewards later (reversed, so we can pop off the top)
let targets = bonded
.iter()
.map(|(v, _)| v.clone())
.rev()
.collect::<Vec<_>>();
REWARD_TARGETS.save(deps.storage, &targets).unwrap();

bonded
.iter()
.map(|(validator, _)| {
SubMsg::new(DistributionMsg::WithdrawDelegatorReward {
validator: validator.clone(),
})
SubMsg::reply_always(
DistributionMsg::WithdrawDelegatorReward {
validator: validator.clone(),
},
REPLY_REWARDS_ID,
)
})
.collect()
}
Expand Down
Loading

0 comments on commit 3e74dfd

Please sign in to comment.