Skip to content

Commit

Permalink
#154 epic(contracts-lockup): test for all execute calls and lock life…
Browse files Browse the repository at this point in the history
…cycle clarity

epic(contracts-lockup): test for all execute calls and lock lifecycle clarity
  • Loading branch information
Unique-Divine authored Sep 25, 2024
2 parents 8893b69 + 1920652 commit 90bc6e4
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 43 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

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

1 change: 1 addition & 0 deletions contracts/lockup/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ cw20 = { workspace = true }
schemars = { workspace = true }
thiserror = { workspace = true }
serde = { version = "1.0.188", default-features = false, features = ["derive"] }
anyhow = { workspace = true }

[dev-dependencies]
69 changes: 42 additions & 27 deletions contracts/lockup/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::error::ContractError;
use crate::events::{
new_coins_locked_event, new_funds_withdrawn_event,
new_unlock_initiation_event,
event_coins_locked, event_funds_withdrawn, event_unlock_initiated,
};
use crate::msgs::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{locks, Lock, LOCKS_ID, NOT_UNLOCKING_BLOCK_IDENTIFIER};
use crate::state::{
locks, Lock, LockState, LOCKS_ID, NOT_UNLOCKING_BLOCK_IDENTIFIER,
};
use cosmwasm_std::{
to_json_binary, BankMsg, Binary, Deps, DepsMut, Env, Event, MessageInfo,
Order, Response, StdResult,
to_json_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, Event,
MessageInfo, Order, Response, StdResult,
};
use cw_storage_plus::Bound;

Expand Down Expand Up @@ -51,30 +52,36 @@ pub(crate) fn execute_withdraw_funds(
) -> Result<Response, ContractError> {
let locks = locks();

let mut tx_msgs: Vec<CosmosMsg> = Vec::new();

// we update the lock to mark funds have been withdrawn
let lock =
locks.update(deps.storage, id, |lock| -> Result<_, ContractError> {
let mut lock = lock.ok_or(ContractError::NotFound(id))?;

if lock.funds_withdrawn {
return Err(ContractError::FundsAlreadyWithdrawn(id));
}

if lock.end_block < env.block.height {
return Err(ContractError::NotMatured(id));
match lock.state(env.block.height) {
LockState::Matured => {
tx_msgs.push(
BankMsg::Send {
to_address: lock.owner.to_string(),
amount: vec![lock.coin.clone()],
}
.into(),
);
lock.funds_withdrawn = true;
Ok(lock)
}
LockState::FundedPreUnlock | LockState::Unlocking => {
Err(ContractError::NotMatured(id))
}
LockState::Withdrawn => {
Err(ContractError::FundsAlreadyWithdrawn(id))
}
}

lock.funds_withdrawn = true;

Ok(lock)
})?;

Ok(Response::new()
.add_event(new_funds_withdrawn_event(id, &lock.coin))
.add_message(BankMsg::Send {
to_address: lock.owner.to_string(),
amount: vec![lock.coin],
}))
.add_event(event_funds_withdrawn(id, &lock.coin))
.add_messages(tx_msgs))
}

pub(crate) fn execute_initiate_unlock(
Expand All @@ -89,15 +96,23 @@ pub(crate) fn execute_initiate_unlock(
let lock =
locks.update(deps.storage, id, |lock| -> Result<_, ContractError> {
let mut lock = lock.ok_or(ContractError::NotFound(id))?;
if lock.end_block != NOT_UNLOCKING_BLOCK_IDENTIFIER {
return Err(ContractError::AlreadyUnlocking(id));

match lock.state(env.block.height) {
LockState::FundedPreUnlock => {
lock.end_block = env.block.height + lock.duration_blocks;
Ok(lock)
}
LockState::Unlocking | LockState::Matured => {
Err(ContractError::AlreadyUnlocking(id))
}
LockState::Withdrawn => {
Err(ContractError::FundsAlreadyWithdrawn(id))
}
}
lock.end_block = env.block.height + lock.duration_blocks;
Ok(lock)
})?;

// emit unlock initiation event
Ok(Response::new().add_event(new_unlock_initiation_event(
Ok(Response::new().add_event(event_unlock_initiated(
id,
&lock.coin,
lock.end_block,
Expand Down Expand Up @@ -149,7 +164,7 @@ pub(crate) fn execute_lock(
)
.expect("must never fail");

events.push(new_coins_locked_event(id, &coin))
events.push(event_coins_locked(id, &coin))
}

Ok(Response::new().add_events(events))
Expand Down
222 changes: 222 additions & 0 deletions contracts/lockup/src/contract_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//! Tests for the execute calls of contract.rs
use crate::contract::{execute, instantiate};
use crate::error::ContractError;
use crate::msgs::{ExecuteMsg, InstantiateMsg};
use crate::state::{locks, Lock, LockState, NOT_UNLOCKING_BLOCK_IDENTIFIER};

use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage,
};
use cosmwasm_std::{coins, BankMsg, Coin, Env, MessageInfo, OwnedDeps, SubMsg};

const OWNER: &str = "owner";
const USER: &str = "user";
const DENOM: &str = "unibi";

fn setup_contract() -> anyhow::Result<(
OwnedDeps<MockStorage, MockApi, MockQuerier>,
Env,
MessageInfo,
)> {
let mut deps = mock_dependencies();
let env = mock_env();
let info = mock_info(OWNER, &[]);
let msg = InstantiateMsg {};
let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg)?;
assert_eq!(0, res.messages.len());
Ok((deps, env, info))
}

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

#[test]
fn test_execute_lock() -> TestResult {
let (mut deps, env, _info) = setup_contract()?;

// Successful lock
let info = mock_info(USER, &coins(100, DENOM));
let msg = ExecuteMsg::Lock { blocks: 100 };
let res = execute(deps.as_mut(), env.clone(), info, msg)?;
assert_eq!(1, res.events.len());

// Query the lock
let locks = locks();
let lock = locks.load(&deps.storage, 1)?;
assert_eq!(lock.owner, USER);
assert_eq!(lock.coin, Coin::new(100u128, DENOM));
assert_eq!(lock.duration_blocks, 100);
assert_eq!(lock.start_block, env.block.height);
assert_eq!(lock.end_block, NOT_UNLOCKING_BLOCK_IDENTIFIER);
assert!(!lock.funds_withdrawn);

// Attempt to lock with no funds
let info = mock_info(USER, &[]);
let msg = ExecuteMsg::Lock { blocks: 100 };
let err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err();
assert!(matches!(err, ContractError::InvalidCoins(_)));

// Attempt to lock with zero duration
let info = mock_info(USER, &coins(100, DENOM));
let msg = ExecuteMsg::Lock { blocks: 0 };
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::InvalidLockDuration));
Ok(())
}

#[test]
fn test_execute_initiate_unlock() -> TestResult {
let (mut deps, env, _info) = setup_contract()?;

// Create a lock first
let info = mock_info(USER, &coins(100, DENOM));
let msg = ExecuteMsg::Lock { blocks: 100 };
let _ = execute(deps.as_mut(), env.clone(), info, msg)?;

// Successful initiate unlock
let msg = ExecuteMsg::InitiateUnlock { id: 1 };
let info = mock_info(USER, &[]);
let res = execute(deps.as_mut(), env.clone(), info, msg)?;
assert_eq!(1, res.events.len());

// Query the lock
let locks = locks();
let lock = locks.load(&deps.storage, 1)?;
assert_eq!(lock.end_block, env.block.height + 100);

// Attempt to initiate unlock again
let msg = ExecuteMsg::InitiateUnlock { id: 1 };
let info = mock_info(USER, &[]);
let err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err();
assert!(matches!(err, ContractError::AlreadyUnlocking(_)));

// Attempt to initiate unlock for non-existent lock
let msg = ExecuteMsg::InitiateUnlock { id: 99 };
let info = mock_info(USER, &[]);
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::NotFound(_)));
Ok(())
}

#[test]
fn test_execute_withdraw_funds() -> TestResult {
let (mut deps, mut env, _info) = setup_contract()?;

// Create and initiate unlock for a lock
let info = mock_info(USER, &coins(100, DENOM));
let msg = ExecuteMsg::Lock { blocks: 100 };
let _ = execute(deps.as_mut(), env.clone(), info, msg)?;

let msg = ExecuteMsg::InitiateUnlock { id: 1 };
let info = mock_info(USER, &[]);
let _ = execute(deps.as_mut(), env.clone(), info, msg)?;

// Attempt to withdraw before maturity
let msg = ExecuteMsg::WithdrawFunds { id: 1 };
let info = mock_info(USER, &[]);
let err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err();
assert!(matches!(err, ContractError::NotMatured(_)));

// Fast forward to maturity
env.block.height += 101;

// Successful withdraw
let msg = ExecuteMsg::WithdrawFunds { id: 1 };
let info = mock_info(USER, &[]);
let res = execute(deps.as_mut(), env.clone(), info, msg)?;
assert_eq!(1, res.messages.len());
assert_eq!(
res.messages[0],
SubMsg::new(BankMsg::Send {
to_address: USER.to_string(),
amount: vec![Coin::new(100u128, DENOM)]
})
);

// Query the lock
let locks = locks();
let lock = locks.load(&deps.storage, 1)?;
assert!(lock.funds_withdrawn);

// Attempt to withdraw again
let msg = ExecuteMsg::WithdrawFunds { id: 1 };
let info = mock_info(USER, &[]);
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::FundsAlreadyWithdrawn(_)));

Ok(())
}

#[test]
fn test_lock_state() -> TestResult {
let (mut _deps, env, _info) = setup_contract()?;

let lock = Lock {
id: 1,
coin: Coin::new(100u128, DENOM),
owner: USER.to_string(),
duration_blocks: 100,
start_block: env.block.height,
end_block: NOT_UNLOCKING_BLOCK_IDENTIFIER,
funds_withdrawn: false,
};

// Test FundedPreUnlock state
assert_eq!(lock.state(env.block.height), LockState::FundedPreUnlock);

// Test Unlocking state
let mut unlocking_lock = lock.clone();
unlocking_lock.end_block = env.block.height + 50;
assert_eq!(unlocking_lock.state(env.block.height), LockState::Unlocking);

// Test Matured state
let mut matured_lock = lock.clone();
matured_lock.end_block = env.block.height - 1;
assert_eq!(matured_lock.state(env.block.height), LockState::Matured);

// Test Withdrawn state
let mut withdrawn_lock = lock;
withdrawn_lock.funds_withdrawn = true;
assert_eq!(withdrawn_lock.state(env.block.height), LockState::Withdrawn);

Ok(())
}

#[test]
fn test_withdraw_at_exact_maturity() -> TestResult {
let (mut deps, mut env, _info) = setup_contract()?;

// Create and initiate unlock for a lock
let info = mock_info(USER, &coins(100, DENOM));
let msg = ExecuteMsg::Lock { blocks: 100 };
let _ = execute(deps.as_mut(), env.clone(), info, msg)?;

let msg = ExecuteMsg::InitiateUnlock { id: 1 };
let info = mock_info(USER, &[]);
let _ = execute(deps.as_mut(), env.clone(), info, msg)?;

// Fast forward to exact maturity
env.block.height += 100;

// Attempt to withdraw at exact maturity
let msg = ExecuteMsg::WithdrawFunds { id: 1 };
let info = mock_info(USER, &[]);
let res = execute(deps.as_mut(), env.clone(), info, msg)?;

// Check that withdrawal was successful
assert_eq!(1, res.messages.len());
assert_eq!(
res.messages[0],
SubMsg::new(BankMsg::Send {
to_address: USER.to_string(),
amount: vec![Coin::new(100u128, DENOM)]
})
);

// Query the lock to ensure it's marked as withdrawn
let locks = locks();
let lock = locks.load(&deps.storage, 1)?;
assert!(lock.funds_withdrawn);

Ok(())
}
10 changes: 3 additions & 7 deletions contracts/lockup/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@ pub const COINS_LOCKED_EVENT_NAME: &str = "coins_locked";
pub const UNLOCK_INITIATION_EVENT_NAME: &str = "unlock_initiated";
pub const LOCK_FUNDS_WITHDRAWN: &str = "funds_withdrawn";

pub fn new_coins_locked_event(id: u64, coin: &Coin) -> Event {
pub fn event_coins_locked(id: u64, coin: &Coin) -> Event {
Event::new(COINS_LOCKED_EVENT_NAME)
.add_attribute("id", id.to_string())
.add_attribute("coins", coin.to_string())
}

pub fn new_unlock_initiation_event(
id: u64,
coin: &Coin,
unlock_block: u64,
) -> Event {
pub fn event_unlock_initiated(id: u64, coin: &Coin, unlock_block: u64) -> Event {
Event::new(UNLOCK_INITIATION_EVENT_NAME)
.add_attribute("id", id.to_string())
.add_attribute("coins", coin.to_string())
.add_attribute("unlock_block", unlock_block.to_string())
}

pub fn new_funds_withdrawn_event(id: u64, coin: &Coin) -> Event {
pub fn event_funds_withdrawn(id: u64, coin: &Coin) -> Event {
Event::new(LOCK_FUNDS_WITHDRAWN)
.add_attribute("id", id.to_string())
.add_attribute("coins", coin.to_string())
Expand Down
3 changes: 3 additions & 0 deletions contracts/lockup/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ pub mod error;
pub mod events;
pub mod msgs;
pub mod state;

#[cfg(test)]
pub mod contract_test;
Loading

0 comments on commit 90bc6e4

Please sign in to comment.