Skip to content

Commit

Permalink
test(token-vesting): strong error types, explicit checks in tests, an…
Browse files Browse the repository at this point in the history
…d deregister tests

test(token-vesting): strong error types, explicit checks in tests, and deregister tests

- Closes #82

--- 

- ci: Exclude prost types from nibiru-std/src/proto/buf from coverage + fmt
- feat(nibiru-std): implement NibiruStargateQuery as an extension of prost::Nme
- test(nibiru-std): verify that CosmosMsg::Stargate types error when converted to QueryRequest::Stargate
- test(token-vesting): add tests for execute deregister
- test(token-vesting): clean up linear vesting tests
- test(token-vesting): use strongly-typed, idiomatic errors
- test(token-vesting): use typed errors + Result types in contract.rs
- test(token-vesting): use explicit error comparisons in tests + Result types in testing.rs
  • Loading branch information
Unique-Divine authored Oct 24, 2023
2 parents 41ccaec + 3a4a97f commit ac45814
Show file tree
Hide file tree
Showing 14 changed files with 541 additions and 305 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified artifacts/bindings_perp.wasm
Binary file not shown.
10 changes: 5 additions & 5 deletions artifacts/checksums.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
37e1b6a8583c5158568cebedbced902936d2f4f397f9ba20fe8e3f49a312fb8b bindings_perp.wasm
d0d3df98c4af178c66630777092df45462289530b731e2ddc6b1c2300d1d657e controller.wasm
65548e00849045afabb51ec80f357dc1306198ffbfd548d03660ef40fba29017 bindings_perp.wasm
67191fdbcaaaae07c19cb3fca86a7273fae46c1015a0c52b1526b5329e559d81 controller.wasm
ec6cc298bf32cad7f5f426002fdcee602b133f24f6f7d65905c5aad0e3b777c9 incentives.wasm
459f3d2dd6240ef36cba48d3e102407639caffa7bca9c3b924df483b4fd40b6a lockup.wasm
663f737a0f25ec93eb6dab83f8872e3a7a289cde2a3ec4c49d1bf6127b992ec8 nibi_stargate.wasm
d2dfc9ed4a689a8b23b020421c7e3fc0a90fd985c1291a44659073cc9021bb38 nibi_stargate.wasm
bbb9c32c863ff78366ac5bae241967b206a6ad463e0154e7081309ab36904dce pricefeed.wasm
76dc1fc5e9b7fd975bde8f30d364b185733402ffe64fd841866bdf85634dcd3a shifter.wasm
a2f0262bbb39bcc16a353976154de22325e53f68f264ba61549f39d78dde2291 token_vesting.wasm
8014645275544b90c2ae33002e30c6cce6859dcd1193a48c599bedf4fb1015e3 shifter.wasm
0e4db7a116f27db4973e46db4a9cdd60abde1fb3e0dfeb7b1d5d9197c788229b token_vesting.wasm
10 changes: 5 additions & 5 deletions artifacts/checksums_intermediate.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
1d228461eaa095ec54360c920a851ed142703721f1e5596e740068f19492a868 target/wasm32-unknown-unknown/release/bindings_perp.wasm
da3bdc0569f318d367eea17f39517e9f39d75b37c164b00bed22a4050b4e088c target/wasm32-unknown-unknown/release/controller.wasm
e03f22cf0c8fe52eda23249aafdc7afc2545b227918d9480835b160187083ad3 target/wasm32-unknown-unknown/release/incentives.wasm
9a088c48bd29274d7a0a95359d3749c236dc323d31e94ed0aba0c6c7826930e5 target/wasm32-unknown-unknown/release/lockup.wasm
8c6960f4852bfde4b9ed427a1708f2d0039d439aeb7c1697ece7059a0b998989 target/wasm32-unknown-unknown/release/pricefeed.wasm
a82731d918951ed592e25fb1bd4afba60e0b2be2346d99dda1c52118730ce678 target/wasm32-unknown-unknown/release/shifter.wasm
9318d75912302fedb0f775bb2fe75a60c494e7535f0e08e8e4eb0442cc5ef49a target/wasm32-unknown-unknown/release/token_vesting.wasm
02098f31f1a0dac2f01832c40ab3fc85883533fce16bb8aef966dbb201200b83 target/wasm32-unknown-unknown/release/nibi_stargate.wasm
f95b4a566d319f6cf39de88d168c19134bdff3864916c48c71a9a7f2d4474ee2 target/wasm32-unknown-unknown/release/bindings_perp.wasm
516fc6e8ca410041834e807cfed96478b51805d5c112bdd520bfd32e0f55eb4b target/wasm32-unknown-unknown/release/controller.wasm
ae4e6405de49a97cde63c7d8abb88a77b8dcb8211759dc22ecab61b8e98f8519 target/wasm32-unknown-unknown/release/nibi_stargate.wasm
4a9bc648eaa78b691e286108dcf57111b1325c8df4c59366532b19cf80bac782 target/wasm32-unknown-unknown/release/shifter.wasm
b457a863837b7f4e93a322e9cc90887bd6b6d51f88efd0be50d9d52ee5a8c2d0 target/wasm32-unknown-unknown/release/token_vesting.wasm
Binary file modified artifacts/controller.wasm
Binary file not shown.
Binary file modified artifacts/nibi_stargate.wasm
Binary file not shown.
Binary file modified artifacts/shifter.wasm
Binary file not shown.
Binary file modified artifacts/token_vesting.wasm
Binary file not shown.
5 changes: 4 additions & 1 deletion contracts/token-vesting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ thiserror = { version = "1.0.49" }
cw-storage-plus = "1.1.0"
schemars = "0.8.15"
serde = { version = "1.0.188", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }

[dev-dependencies]
anyhow = { workspace = true }
181 changes: 168 additions & 13 deletions contracts/token-vesting/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde_json::to_string;
use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, Denom};
use cw_storage_plus::Bound;

use crate::errors::ContractError;
use crate::msg::{
Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, VestingAccountResponse,
VestingData, VestingSchedule,
Expand All @@ -33,9 +34,11 @@ pub fn execute(
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg),
ExecuteMsg::Receive(msg) => {
receive_cw20(deps, env, info, msg).map_err(ContractError::from)
}
ExecuteMsg::RegisterVestingAccount {
master_address,
address,
Expand All @@ -45,7 +48,8 @@ pub fn execute(
if info.funds.len() != 1 {
return Err(StdError::generic_err(
"must deposit only one type of token",
));
)
.into());
}

let deposit_coin = info.funds[0].clone();
Expand Down Expand Up @@ -87,12 +91,12 @@ fn register_vesting_account(
deposit_denom: Denom,
deposit_amount: Uint128,
vesting_schedule: VestingSchedule,
) -> StdResult<Response> {
) -> Result<Response, ContractError> {
let denom_key = denom_to_key(deposit_denom.clone());

// vesting_account existence check
if VESTING_ACCOUNTS.has(storage, (address.as_str(), &denom_key)) {
return Err(StdError::generic_err("already exists"));
return Err(StdError::generic_err("already exists").into());
}

// validate vesting schedule
Expand Down Expand Up @@ -131,7 +135,7 @@ fn deregister_vesting_account(
denom: Denom,
vested_token_recipient: Option<String>,
left_vesting_token_recipient: Option<String>,
) -> StdResult<Response> {
) -> Result<Response, ContractError> {
let denom_key = denom_to_key(denom.clone());
let sender = info.sender;

Expand All @@ -141,17 +145,17 @@ fn deregister_vesting_account(
let account = VESTING_ACCOUNTS
.may_load(deps.storage, (address.as_str(), &denom_key))?;
if account.is_none() {
return Err(StdError::generic_err(format!(
return Err(ContractError::Std(StdError::generic_err(format!(
"vesting entry is not found for denom {:?}",
to_string(&denom).unwrap(),
)));
))));
}

let account = account.unwrap();
if account.master_address.is_none()
|| account.master_address.unwrap() != sender
{
return Err(StdError::generic_err("unauthorized"));
return Err(StdError::generic_err("unauthorized").into());
}

// remove vesting account
Expand Down Expand Up @@ -207,7 +211,7 @@ fn claim(
info: MessageInfo,
denoms: Vec<Denom>,
recipient: Option<String>,
) -> StdResult<Response> {
) -> Result<Response, ContractError> {
let sender = info.sender;
let recipient = recipient.unwrap_or_else(|| sender.to_string());

Expand All @@ -223,7 +227,8 @@ fn claim(
return Err(StdError::generic_err(format!(
"vesting entry is not found for denom {}",
to_string(&denom).unwrap(),
)));
))
.into());
}

let mut account = account.unwrap();
Expand Down Expand Up @@ -301,7 +306,7 @@ pub fn receive_cw20(
env: Env,
info: MessageInfo,
cw20_msg: Cw20ReceiveMsg,
) -> StdResult<Response> {
) -> Result<Response, ContractError> {
let amount = cw20_msg.amount;
let _sender = cw20_msg.sender;
let contract = info.sender;
Expand All @@ -320,7 +325,7 @@ pub fn receive_cw20(
amount,
vesting_schedule,
),
Err(_) => Err(StdError::generic_err("invalid cw20 hook message")),
Err(_) => Err(StdError::generic_err("invalid cw20 hook message").into()),
}
}

Expand Down Expand Up @@ -387,3 +392,153 @@ fn vesting_account(

Ok(VestingAccountResponse { address, vestings })
}

#[cfg(test)]
pub mod tests {

use super::*;
use anyhow::anyhow;
use cosmwasm_std::{
coin,
testing::{self, MockApi, MockQuerier, MockStorage},
Empty, OwnedDeps, Uint64,
};

pub type TestResult = Result<(), anyhow::Error>;

pub fn mock_env_with_time(block_time: u64) -> Env {
let mut env = testing::mock_env();
env.block.time = Timestamp::from_seconds(block_time);
env
}

/// Convenience function for instantiating the contract at and setting up
/// the env to have the given block time.
pub fn setup_with_block_time(
block_time: u64,
) -> anyhow::Result<(OwnedDeps<MockStorage, MockApi, MockQuerier, Empty>, Env)>
{
let mut deps = testing::mock_dependencies();
let env = mock_env_with_time(block_time);
instantiate(
deps.as_mut(),
env.clone(),
testing::mock_info("admin-sender", &[]),
InstantiateMsg {},
)?;
Ok((deps, env))
}

#[test]
fn deregister_err_nonexistent_vesting_account() -> TestResult {
let (mut deps, _env) = setup_with_block_time(0)?;

let msg = ExecuteMsg::DeregisterVestingAccount {
address: "nonexistent".to_string(),
denom: Denom::Native("token".to_string()),
vested_token_recipient: None,
left_vesting_token_recipient: None,
};

let res = execute(
deps.as_mut(),
testing::mock_env(),
testing::mock_info("admin-sender", &[]),
msg,
);

match res {
Ok(_) => Err(anyhow!("Unexpected result: {:#?}", res)),
Err(ContractError::Std(StdError::GenericErr { msg, .. })) => {
assert!(msg.contains("vesting entry is not found for denom"));
Ok(())
}
Err(err) => Err(anyhow!("Unexpected error: {:#?}", err)),
}
}

#[test]
fn deregister_err_unauthorized_vesting_account() -> TestResult {
// Set up the environment with a block time before the vesting start time
let (mut deps, env) = setup_with_block_time(50)?;

let register_msg = ExecuteMsg::RegisterVestingAccount {
master_address: Some("addr0002".to_string()),
address: "addr0001".to_string(),
vesting_schedule: VestingSchedule::LinearVesting {
start_time: Uint64::new(100),
end_time: Uint64::new(110),
vesting_amount: Uint128::new(1000000u128),
},
};

execute(
deps.as_mut(),
env.clone(), // Use the custom environment with the adjusted block time
testing::mock_info("admin-sender", &[coin(1000000, "token")]),
register_msg,
)?;

// Try to deregister with unauthorized sender
let msg = ExecuteMsg::DeregisterVestingAccount {
address: "addr0001".to_string(),
denom: Denom::Native("token".to_string()),
vested_token_recipient: None,
left_vesting_token_recipient: None,
};

let res = execute(
deps.as_mut(),
env, // Use the custom environment with the adjusted block time
testing::mock_info("addr0003", &[]),
msg,
);
match res {
Err(ContractError::Std(StdError::GenericErr { msg, .. }))
if msg == "unauthorized" => {}
_ => return Err(anyhow!("Unexpected result: {:?}", res)),
}

Ok(())
}

#[test]
fn deregister_successful() -> TestResult {
// Set up the environment with a block time before the vesting start time
let (mut deps, env) = setup_with_block_time(50)?;

let register_msg = ExecuteMsg::RegisterVestingAccount {
master_address: Some("addr0002".to_string()),
address: "addr0001".to_string(),
vesting_schedule: VestingSchedule::LinearVesting {
start_time: Uint64::new(100),
end_time: Uint64::new(110),
vesting_amount: Uint128::new(1000000u128),
},
};

execute(
deps.as_mut(),
env.clone(), // Use the custom environment with the adjusted block time
testing::mock_info("admin-sender", &[coin(1000000, "token")]),
register_msg,
)?;

// Deregister with the master address
let msg = ExecuteMsg::DeregisterVestingAccount {
address: "addr0001".to_string(),
denom: Denom::Native("token".to_string()),
vested_token_recipient: None,
left_vesting_token_recipient: None,
};

let _res = execute(
deps.as_mut(),
env, // Use the custom environment with the adjusted block time
testing::mock_info("addr0002", &[]),
msg,
)?;

Ok(())
}
}
54 changes: 54 additions & 0 deletions contracts/token-vesting/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error(transparent)]
Std(#[from] cosmwasm_std::StdError),

#[error(transparent)]
Vesting(#[from] VestingError),

#[error(transparent)]
Cliff(#[from] CliffError),

#[error(transparent)]
Overflow(#[from] cosmwasm_std::OverflowError),
}

#[derive(thiserror::Error, Debug, PartialEq)]
pub enum CliffError {
#[error("cliff_amount is zero but should be greater than 0")]
ZeroAmount,

#[error("cliff_time ({cliff_time}) should be greater than block_time ({block_time})")]
InvalidTime { cliff_time: u64, block_time: u64 },

#[error("cliff_amount ({cliff_amount}) should be less than or equal to vesting_amount ({vesting_amount})")]
ExcessiveAmount {
cliff_amount: u128,
vesting_amount: u128,
},
}

#[derive(thiserror::Error, Debug, PartialEq)]
pub enum VestingError {
#[error("vesting_amount is zero but should be greater than 0")]
ZeroVestingAmount,

#[error(
"end_time ({end_time}) should be greater than start_time ({start_time})"
)]
InvalidTimeRange { start_time: u64, end_time: u64 },

#[error("start_time ({start_time}) should be greater than block_time ({block_time})")]
StartBeforeBlockTime { start_time: u64, block_time: u64 },

#[error(transparent)]
Cliff(#[from] CliffError),

#[error("vesting_amount ({vesting_amount}) should be equal to deposit_amount ({deposit_amount})")]
MismatchedVestingAndDepositAmount {
vesting_amount: u128,
deposit_amount: u128,
},
}
1 change: 1 addition & 0 deletions contracts/token-vesting/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod contract;
pub mod errors;
pub mod msg;
pub mod state;

Expand Down
Loading

0 comments on commit ac45814

Please sign in to comment.