Skip to content

Commit

Permalink
Added place limit logic
Browse files Browse the repository at this point in the history
  • Loading branch information
crnbarr93 committed Mar 14, 2024
1 parent 4cabdea commit eb414e8
Show file tree
Hide file tree
Showing 2 changed files with 350 additions and 11 deletions.
40 changes: 29 additions & 11 deletions contracts/sumtree-orderbook/src/order.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::constants::{MAX_TICK, MIN_TICK};
use crate::error::ContractError;
use crate::state::{new_order_id, orders, ORDERBOOKS};
use crate::types::{LimitOrder, OrderDirection};
use cosmwasm_std::{ensure, ensure_eq, DepsMut, Env, MessageInfo, Response, Uint128};
use crate::state::{new_order_id, orders, ORDERBOOKS, TICK_STATE};
use crate::types::{LimitOrder, OrderDirection, TickState};
use cosmwasm_std::{
ensure, ensure_eq, Decimal256, DepsMut, Env, MessageInfo, Response, Uint128, Uint256,
};
use cw_utils::{must_pay, nonpayable};

#[allow(clippy::manual_range_contains)]
Expand Down Expand Up @@ -78,11 +80,30 @@ pub fn place_limit(
orders().save(deps.storage, &(book_id, tick_id, order_id), &limit_order)?;
}

// TODO: Update Tick State
let quantity_fullfilled = quantity.checked_sub(limit_order.quantity)?;

// Update tick liquidity
// TICK_STATE.update(deps.storage, &(book_id, tick_id), |state| {
// let curr_state = state.unwrap_or_default();
// })?;
TICK_STATE.update(deps.storage, &(book_id, tick_id), |state| {
let mut curr_state = state.unwrap_or_default();
// Increment total available liquidity by remaining quantity in order post fill
curr_state.total_amount_of_liquidity = curr_state
.total_amount_of_liquidity
.checked_add(Decimal256::from_ratio(
limit_order.quantity.u128(),
Uint256::one(),
))
.unwrap();
// Increment amount swapped by amount fulfilled for order
curr_state.effective_total_amount_swapped = curr_state
.effective_total_amount_swapped
.checked_add(Decimal256::from_ratio(quantity_fullfilled, Uint256::one()))?;
// TODO: Unsure of what this value is for?
curr_state.cumulative_total_limits = curr_state
.cumulative_total_limits
.checked_add(Decimal256::one())?;

Ok::<TickState, ContractError>(curr_state)
})?;

Ok(response
.add_attribute("method", "placeLimit")
Expand All @@ -92,10 +113,7 @@ pub fn place_limit(
.add_attribute("order_id", order_id.to_string())
.add_attribute("order_direction", format!("{order_direction:?}"))
.add_attribute("quantity", quantity.to_string())
.add_attribute(
"quantity_fulfilled",
quantity.checked_sub(limit_order.quantity)?,
))
.add_attribute("quantity_fulfilled", quantity_fullfilled))
}

pub fn cancel_limit(
Expand Down
321 changes: 321 additions & 0 deletions contracts/sumtree-orderbook/src/tests/test_order.rs
Original file line number Diff line number Diff line change
@@ -1 +1,322 @@
use crate::{
constants::{MAX_TICK, MIN_TICK},
error::ContractError,
order::*,
orderbook::*,
state::*,
types::OrderDirection,
};
use cosmwasm_std::{coin, Addr, Uint128, Uint256};
use cosmwasm_std::{
testing::{mock_dependencies_with_balances, mock_env, mock_info},
Decimal256,
};

#[allow(clippy::uninlined_format_args)]
fn format_test_name(name: &str) -> String {
format!("\n\nTest case failed: {}\n", name)
}

struct PlaceLimitTestCase {
name: &'static str,
book_id: u64,
tick_id: i64,
quantity: Uint128,
sent: Uint128,
order_direction: OrderDirection,
expected_error: Option<ContractError>,
}

#[test]
fn test_place_limit() {
let valid_book_id = 0;
let invalid_book_id = valid_book_id + 1;
let test_cases = vec![
PlaceLimitTestCase {
name: "valid order with positive tick id",
book_id: valid_book_id,
tick_id: 10,
quantity: Uint128::new(100),
sent: Uint128::new(100),
order_direction: OrderDirection::Ask,
expected_error: None,
},
PlaceLimitTestCase {
name: "valid order with zero tick id",
book_id: valid_book_id,
tick_id: 0,
quantity: Uint128::new(34321),
sent: Uint128::new(34321),
order_direction: OrderDirection::Bid,
expected_error: None,
},
PlaceLimitTestCase {
name: "valid order with negative tick id",
book_id: valid_book_id,
tick_id: -5,
quantity: Uint128::new(100),
sent: Uint128::new(100),
order_direction: OrderDirection::Bid,
expected_error: None,
},
PlaceLimitTestCase {
name: "valid order with large quantity",
book_id: valid_book_id,
tick_id: 3,
quantity: Uint128::new(34321),
sent: Uint128::new(34321),
order_direction: OrderDirection::Ask,
expected_error: None,
},
PlaceLimitTestCase {
name: "invalid book id",
book_id: invalid_book_id,
tick_id: 1,
quantity: Uint128::new(100),
sent: Uint128::new(100),
order_direction: OrderDirection::Ask,
expected_error: Some(ContractError::InvalidBookId {
book_id: invalid_book_id,
}),
},
PlaceLimitTestCase {
name: "invalid tick id (max)",
book_id: valid_book_id,
tick_id: MAX_TICK + 1,
quantity: Uint128::new(100),
sent: Uint128::new(100),
order_direction: OrderDirection::Ask,
expected_error: Some(ContractError::InvalidTickId {
tick_id: MAX_TICK + 1,
}),
},
PlaceLimitTestCase {
name: "invalid tick id (min)",
book_id: valid_book_id,
tick_id: MIN_TICK - 1,
quantity: Uint128::new(100),
sent: Uint128::new(100),
order_direction: OrderDirection::Ask,
expected_error: Some(ContractError::InvalidTickId {
tick_id: MIN_TICK - 1,
}),
},
PlaceLimitTestCase {
name: "invalid quantity",
book_id: valid_book_id,
tick_id: 1,
quantity: Uint128::zero(),
sent: Uint128::new(1000),
order_direction: OrderDirection::Ask,
expected_error: Some(ContractError::InvalidQuantity {
quantity: Uint128::zero(),
}),
},
PlaceLimitTestCase {
name: "insufficient funds",
book_id: valid_book_id,
tick_id: 1,
quantity: Uint128::new(1000),
sent: Uint128::new(500),
order_direction: OrderDirection::Ask,
expected_error: Some(ContractError::InsufficientFunds {
sent: Uint128::new(500),
required: Uint128::new(1000),
}),
},
PlaceLimitTestCase {
name: "excessive funds",
book_id: valid_book_id,
tick_id: 1,
quantity: Uint128::new(100),
sent: Uint128::new(500),
order_direction: OrderDirection::Ask,
expected_error: Some(ContractError::InsufficientFunds {
sent: Uint128::new(500),
required: Uint128::new(100),
}),
},
];

for test in test_cases {
// --- Setup ---

// Create a mock environment and info
let coin_vec = vec![coin(
test.sent.u128(),
if test.order_direction == OrderDirection::Ask {
"base"
} else {
"quote"
},
)];
let balances = [("creator", coin_vec.as_slice())];
let mut deps = mock_dependencies_with_balances(&balances);
let env = mock_env();
let info = mock_info("creator", &coin_vec);

// Create an orderbook to operate on
let quote_denom = "quote".to_string();
let base_denom = "base".to_string();
let _create_response = create_orderbook(
deps.as_mut(),
env.clone(),
info.clone(),
quote_denom,
base_denom,
)
.unwrap();

// --- System under test ---

let response = place_limit(
deps.as_mut(),
env.clone(),
info.clone(),
test.book_id,
test.tick_id,
test.order_direction,
test.quantity,
);

// --- Assertions ---

// Error case assertions if applicable
if let Some(expected_error) = &test.expected_error {
assert_eq!(
response.unwrap_err(),
*expected_error,
"{}",
format_test_name(test.name)
);

// Verify that the order was not put in state
let order_result = orders()
.may_load(&deps.storage, &(test.book_id, test.tick_id, 0))
.unwrap();
assert!(order_result.is_none(), "{}", format_test_name(test.name));

// Verifiy liquidity was not updated
let state = TICK_STATE
.load(&deps.storage, &(test.book_id, test.tick_id))
.unwrap_or_default();
assert!(
state.total_amount_of_liquidity.is_zero(),
"{}",
format_test_name(test.name)
);
continue;
}

// Assert no error and retrieve response contents
let response = response.unwrap();

// Assertions on the response for a valid order
assert_eq!(
response.attributes[0],
("method", "placeLimit"),
"{}",
format_test_name(test.name)
);
assert_eq!(
response.attributes[1],
("owner", "creator"),
"{}",
format_test_name(test.name)
);
assert_eq!(
response.attributes[2],
("book_id", test.book_id.to_string()),
"{}",
format_test_name(test.name)
);
assert_eq!(
response.attributes[3],
("tick_id", test.tick_id.to_string()),
"{}",
format_test_name(test.name)
);
assert_eq!(
response.attributes[6],
("quantity", test.quantity.to_string()),
"{}",
format_test_name(test.name)
);
assert_eq!(
response.attributes[7],
("quantity_fulfilled", "0"),
"{}",
format_test_name(test.name)
);

// Retrieve the order from storage to verify it was saved correctly
let expected_order_id = 0;
let order = orders()
.load(
&deps.storage,
&(test.book_id, test.tick_id, expected_order_id),
)
.unwrap();

// Verify the order's fields
assert_eq!(
order.book_id,
test.book_id,
"{}",
format_test_name(test.name)
);
assert_eq!(
order.tick_id,
test.tick_id,
"{}",
format_test_name(test.name)
);
assert_eq!(
order.order_id,
expected_order_id,
"{}",
format_test_name(test.name)
);
assert_eq!(
order.order_direction,
test.order_direction,
"{}",
format_test_name(test.name)
);
assert_eq!(
order.owner,
Addr::unchecked("creator"),
"{}",
format_test_name(test.name)
);
assert_eq!(
order.quantity,
test.quantity,
"{}",
format_test_name(test.name)
);

// Validate liquidity updated as intended
let state = TICK_STATE
.load(&deps.storage, &(test.book_id, test.tick_id))
.unwrap();
assert_eq!(
state.total_amount_of_liquidity,
Decimal256::from_ratio(test.quantity, Uint256::one()),
"{}",
format_test_name(test.name)
);
assert_eq!(
state.effective_total_amount_swapped,
Decimal256::zero(),
"{}",
format_test_name(test.name)
);
assert_eq!(
state.cumulative_total_limits,
Decimal256::one(),
"{}",
format_test_name(test.name)
);
}
}

0 comments on commit eb414e8

Please sign in to comment.