Skip to content

Commit

Permalink
Update vesting contract (#28)
Browse files Browse the repository at this point in the history
* bump dependencies

* error handling; use cw-utils

* global config

* update schema and typescript types

* fix migrate entry point function signature

* bump rust-optimizer to latest

* Refactor migration. Add tests.

---------

Co-authored-by: piobab <[email protected]>
  • Loading branch information
larry0x and piobab authored Aug 31, 2023
1 parent ee816d9 commit 93771fa
Show file tree
Hide file tree
Showing 22 changed files with 553 additions and 347 deletions.
230 changes: 124 additions & 106 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
members = ["contracts/*"]

[workspace.package]
version = "1.2.0"
version = "1.3.0"
authors = ["Larry Engineer <[email protected]>"]
edition = "2021"
rust-version = "1.65"
rust-version = "1.69"
license = "GPL-3.0-or-later"
homepage = "https://marsprotocol.io"
repository = "https://github.com/mars-protocol/periphery"
documentation = "https://github.com/mars-protocol/periphery#readme"

[workspace.dependencies]
cosmwasm-schema = "1.1"
cosmwasm-std = "1.1"
cw2 = "1.0"
cw-storage-plus = "1.0"
cosmwasm-schema = "1.3"
cosmwasm-std = "1.3"
cw2 = "1.1"
cw-storage-plus = "1.1"
cw-utils = "1.0"
serde = "1.0"
thiserror = "1.0"

Expand Down
6 changes: 3 additions & 3 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ alias = "coverage-grcov-lcov"
[tasks.rust-optimizer]
script = """
if [[ $(arch) == "arm64" ]]; then
image="cosmwasm/workspace-optimizer-arm64:0.12.11"
image="cosmwasm/workspace-optimizer-arm64:0.14.0"
else
image="cosmwasm/workspace-optimizer:0.12.11"
image="cosmwasm/workspace-optimizer:0.14.0"
fi
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
${image}
"""
2 changes: 2 additions & 0 deletions contracts/vesting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true, features = ["stargate"] }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
cosmwasm-schema = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions contracts/vesting/examples/schema.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use cosmwasm_schema::write_api;
use mars_vesting::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use mars_vesting::msg::{Config, ExecuteMsg, QueryMsg};

fn main() {
write_api! {
instantiate: InstantiateMsg,
instantiate: Config<String>,
execute: ExecuteMsg,
query: QueryMsg,
}
Expand Down
165 changes: 77 additions & 88 deletions contracts/vesting/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
coins, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order,
Response, StdError, StdResult, Uint128,
coins, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo,
Order, Response, Uint128,
};
use cw2::set_contract_version;
use cw_storage_plus::Bound;
use cw_utils::must_pay;

use crate::{
error::{Error, Result},
helpers::{compute_position_response, compute_withdrawable},
migrations::v1_3_0,
msg::{
ConfigResponse, ExecuteMsg, InstantiateMsg, PositionResponse, QueryMsg, Schedule,
VotingPowerResponse, VEST_DENOM,
Config, ExecuteMsg, Position, PositionResponse, QueryMsg, Schedule, VotingPowerResponse,
},
state::{Position, OWNER, POSITIONS, UNLOCK_SCHEDULE},
state::{CONFIG, POSITIONS},
};

const CONTRACT_NAME: &str = "crates.io:mars-vesting";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const CONTRACT_NAME: &str = "crates.io:mars-vesting";
pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

const MAX_LIMIT: u32 = 30;
const DEFAULT_LIMIT: u32 = 10;
Expand All @@ -31,12 +33,12 @@ pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
cfg: Config<String>,
) -> Result<Response> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

OWNER.save(deps.storage, &deps.api.addr_validate(&msg.owner)?)?;
UNLOCK_SCHEDULE.save(deps.storage, &msg.unlock_schedule)?;
let cfg = cfg.check(deps.api)?;
CONFIG.save(deps.storage, &cfg)?;

Ok(Response::new())
}
Expand All @@ -46,9 +48,12 @@ pub fn instantiate(
//--------------------------------------------------------------------------------------------------

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response> {
let api = deps.api;
match msg {
ExecuteMsg::UpdateConfig {
new_cfg,
} => update_config(deps, info, new_cfg),
ExecuteMsg::CreatePosition {
user,
vest_schedule,
Expand All @@ -57,50 +62,45 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
user,
} => terminate_position(deps, env, info, api.addr_validate(&user)?),
ExecuteMsg::Withdraw {} => withdraw(deps, env.block.time.seconds(), info.sender),
ExecuteMsg::TransferOwnership(new_owner) => {
transfer_ownership(deps, info.sender, api.addr_validate(&new_owner)?)
}
}
}

pub fn update_config(
deps: DepsMut,
info: MessageInfo,
new_cfg: Config<String>,
) -> Result<Response> {
let cfg = CONFIG.load(deps.storage)?;

// only owner can update config
if info.sender != cfg.owner {
return Err(Error::NotOwner);
}

let new_cfg = new_cfg.check(deps.api)?;
CONFIG.save(deps.storage, &new_cfg)?;

Ok(Response::new().add_attribute("action", "mars/vesting/update_config"))
}

pub fn create_position(
deps: DepsMut,
info: MessageInfo,
user_addr: Addr,
vest_schedule: Schedule,
) -> StdResult<Response> {
// only owner can create allocations
let owner_addr = OWNER.load(deps.storage)?;
if info.sender != owner_addr {
return Err(StdError::generic_err("only owner can create allocations"));
}
) -> Result<Response> {
let cfg = CONFIG.load(deps.storage)?;

// must send exactly one coin
if info.funds.len() != 1 {
return Err(StdError::generic_err(format!(
"wrong number of coins: expecting 1, received {}",
info.funds.len()
)));
}

// the coin must be the vesting coin
let coin = &info.funds[0];
if coin.denom != VEST_DENOM {
return Err(StdError::generic_err(format!(
"wrong denom: expecting {}, received {}",
VEST_DENOM, coin.denom
)));
// only owner can create allocations
if info.sender != cfg.owner {
return Err(Error::NotOwner);
}

// the amount must be greater than zero
let total = coin.amount;
if total.is_zero() {
return Err(StdError::generic_err("wrong amount: must be greater than zero"));
}
let total = must_pay(&info, &cfg.denom)?;

POSITIONS.update(deps.storage, &user_addr, |position| {
if position.is_some() {
return Err(StdError::generic_err("user has a vesting position"));
return Err(Error::PositionExists);
}
Ok(Position {
total,
Expand All @@ -123,24 +123,23 @@ pub fn terminate_position(
env: Env,
info: MessageInfo,
user_addr: Addr,
) -> StdResult<Response> {
) -> Result<Response> {
let cfg = CONFIG.load(deps.storage)?;
let current_time = env.block.time.seconds();

// only owner can terminate allocations
let owner_addr = OWNER.load(deps.storage)?;
if info.sender != owner_addr {
return Err(StdError::generic_err("only owner can terminate allocations"));
if info.sender != cfg.owner {
return Err(Error::NotOwner);
}

let unlock_schedule = UNLOCK_SCHEDULE.load(deps.storage)?;
let mut position = POSITIONS.load(deps.storage, &user_addr)?;

let (vested, _, _) = compute_withdrawable(
current_time,
position.total,
position.withdrawn,
&position.vest_schedule,
&unlock_schedule,
&cfg.unlock_schedule,
);

// unvested tokens are to be reclaimed by the owner
Expand All @@ -153,29 +152,29 @@ pub fn terminate_position(

Ok(Response::new()
.add_message(CosmosMsg::Bank(BankMsg::Send {
to_address: owner_addr.into(),
amount: coins(reclaim.u128(), VEST_DENOM),
to_address: cfg.owner.into(),
amount: coins(reclaim.u128(), cfg.denom),
}))
.add_attribute("action", "mars/vesting/terminate_position")
.add_attribute("user", user_addr)
.add_attribute("vested", vested)
.add_attribute("relaimed", reclaim))
}

pub fn withdraw(deps: DepsMut, time: u64, user_addr: Addr) -> StdResult<Response> {
let unlock_schedule = UNLOCK_SCHEDULE.load(deps.storage)?;
pub fn withdraw(deps: DepsMut, time: u64, user_addr: Addr) -> Result<Response> {
let cfg = CONFIG.load(deps.storage)?;
let mut position = POSITIONS.load(deps.storage, &user_addr)?;

let (_, _, withdrawable) = compute_withdrawable(
time,
position.total,
position.withdrawn,
&position.vest_schedule,
&unlock_schedule,
&cfg.unlock_schedule,
);

if withdrawable.is_zero() {
return Err(StdError::generic_err("withdrawable amount is zero"));
return Err(Error::ZeroWithdrawable);
}

position.withdrawn += withdrawable;
Expand All @@ -184,38 +183,20 @@ pub fn withdraw(deps: DepsMut, time: u64, user_addr: Addr) -> StdResult<Response
Ok(Response::new()
.add_message(CosmosMsg::Bank(BankMsg::Send {
to_address: user_addr.to_string(),
amount: coins(withdrawable.u128(), VEST_DENOM),
amount: coins(withdrawable.u128(), cfg.denom),
}))
.add_attribute("action", "mars/vesting/withdraw")
.add_attribute("user", user_addr)
.add_attribute("timestamp", time.to_string())
.add_attribute("withdrawable", withdrawable))
}

pub fn transfer_ownership(
deps: DepsMut,
sender_addr: Addr,
new_owner_addr: Addr,
) -> StdResult<Response> {
let owner_addr = OWNER.load(deps.storage)?;
if sender_addr != owner_addr {
return Err(StdError::generic_err("only owner can transfer ownership"));
}

OWNER.save(deps.storage, &new_owner_addr)?;

Ok(Response::new()
.add_attribute("action", "mars/vesting/transfer_ownership")
.add_attribute("previous_owner", owner_addr)
.add_attribute("new_owner", new_owner_addr))
}

//--------------------------------------------------------------------------------------------------
// Queries
//--------------------------------------------------------------------------------------------------

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<Binary> {
let api = deps.api;
match msg {
QueryMsg::Config {} => to_binary(&query_config(deps)?),
Expand All @@ -234,20 +215,19 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
limit,
} => to_binary(&query_positions(deps, env.block.time.seconds(), start_after, limit)?),
}
.map_err(Into::into)
}

pub fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
Ok(ConfigResponse {
owner: OWNER.load(deps.storage)?.into(),
unlock_schedule: UNLOCK_SCHEDULE.load(deps.storage)?,
})
pub fn query_config(deps: Deps) -> Result<Config<String>> {
let cfg = CONFIG.load(deps.storage)?;
Ok(cfg.into())
}

pub fn query_voting_power(deps: Deps, user_addr: Addr) -> StdResult<VotingPowerResponse> {
pub fn query_voting_power(deps: Deps, user_addr: Addr) -> Result<VotingPowerResponse> {
let voting_power = match POSITIONS.may_load(deps.storage, &user_addr) {
Ok(Some(position)) => position.total - position.withdrawn,
Ok(None) => Uint128::zero(),
Err(err) => return Err(err),
Err(err) => return Err(err.into()),
};

Ok(VotingPowerResponse {
Expand All @@ -256,18 +236,18 @@ pub fn query_voting_power(deps: Deps, user_addr: Addr) -> StdResult<VotingPowerR
})
}

pub fn query_position(deps: Deps, time: u64, user_addr: Addr) -> StdResult<PositionResponse> {
let unlock_schedule = UNLOCK_SCHEDULE.load(deps.storage)?;
pub fn query_position(deps: Deps, time: u64, user_addr: Addr) -> Result<PositionResponse> {
let cfg = CONFIG.load(deps.storage)?;
let position = POSITIONS.load(deps.storage, &user_addr)?;

Ok(compute_position_response(time, user_addr, &position, &unlock_schedule))
Ok(compute_position_response(time, user_addr, &position, &cfg.unlock_schedule))
}

pub fn query_voting_powers(
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<Vec<VotingPowerResponse>> {
) -> Result<Vec<VotingPowerResponse>> {
let addr: Addr;
let start = match &start_after {
Some(addr_str) => {
Expand Down Expand Up @@ -297,8 +277,8 @@ pub fn query_positions(
time: u64,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<Vec<PositionResponse>> {
let unlock_schedule = UNLOCK_SCHEDULE.load(deps.storage)?;
) -> Result<Vec<PositionResponse>> {
let cfg = CONFIG.load(deps.storage)?;

let addr: Addr;
let start = match &start_after {
Expand All @@ -316,7 +296,16 @@ pub fn query_positions(
.take(limit)
.map(|res| {
let (user_addr, position) = res?;
Ok(compute_position_response(time, user_addr, &position, &unlock_schedule))
Ok(compute_position_response(time, user_addr, &position, &cfg.unlock_schedule))
})
.collect()
}

//--------------------------------------------------------------------------------------------------
// Migration
//--------------------------------------------------------------------------------------------------

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(deps: DepsMut, _: Env, _: Empty) -> Result<Response> {
v1_3_0::migrate(deps)
}
Loading

0 comments on commit 93771fa

Please sign in to comment.