Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC - Rolling Time Periods #3

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
919 changes: 919 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[workspace]
resolver = "2"
members = [
"x/ibc-rate-limit/contracts/rate-limiter"
]
7 changes: 7 additions & 0 deletions x/ibc-rate-limit/Cargo.lock

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

Binary file modified x/ibc-rate-limit/bytecode/rate_limiter.wasm
Binary file not shown.
4 changes: 3 additions & 1 deletion x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ optimize = """docker run --rm -v "$(pwd)":/code \
cosmwasm-std = { version = "1.1.5", features = ["stargate", "cosmwasm_1_1"]}
cosmwasm-schema = "1.1.5"
cosmwasm-storage = "1.1.5"
cw-storage-plus = "0.16.0"
cw-storage-plus = {version = "0.16.0", features = ["iterator"]}
cw2 = "0.13.2"
schemars = "0.8.8"
serde = { version = "1.0.137", default-features = false, features = ["derive"] }
Expand All @@ -45,6 +45,8 @@ osmosis-std-derive = {version = "0.12.0"}
osmosis-std = "0.12.0"
sha2 = "0.10.6"
hex = "0.4.3"
semver = "1"


[dev-dependencies]
cw-multi-test = "0.13.2"
Expand Down
25 changes: 19 additions & 6 deletions x/ibc-rate-limit/contracts/rate-limiter/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw2::set_contract_version;
use cosmwasm_std::{Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdError, StdResult};
use cw2::{get_contract_version, set_contract_version, ContractVersion};

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SudoMsg};
use crate::state::{FlowType, GOVMODULE, IBCMODULE};
use crate::{execute, query, sudo};

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:rate-limiter";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) const CONTRACT_NAME: &str = "crates.io:rate-limiter";

pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");



#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
Expand All @@ -19,7 +22,13 @@ pub fn instantiate(
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
// for testing purposes always set version to 0.1.0
#[cfg(test)]
set_contract_version(deps.storage, CONTRACT_NAME, "0.1.0")?;

#[cfg(not(test))]
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

IBCMODULE.save(deps.storage, &msg.ibc_module)?;
GOVMODULE.save(deps.storage, &msg.gov_module)?;

Expand Down Expand Up @@ -90,6 +99,10 @@ pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractE
channel_value_mock,
),
SudoMsg::UndoSend { packet } => sudo::undo_send(deps, packet),
SudoMsg::RolloverRules => {
crate::sudo::rollover_expired_rate_limits(deps, env)?;
Ok(Response::default())
},
}
}

Expand All @@ -101,6 +114,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
unimplemented!()
pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
crate::migrations::migrate_internal(deps, env, msg)
}
225 changes: 219 additions & 6 deletions x/ibc-rate-limit/contracts/rate-limiter/src/contract_tests.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,80 @@
#![cfg(test)]
use std::collections::HashMap;

use crate::helpers::{expired_rate_limits};
use crate::sudo::rollover_expired_rate_limits;
use crate::msg::MigrateMsg;

use crate::packet::Packet;
use crate::{contract::*, test_msg_recv, test_msg_send, ContractError};
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_binary, Addr, Attribute, Uint256};

use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockStorage, MockQuerier};
use cosmwasm_std::{from_binary, Addr, Attribute, Env, Uint256, Querier, OwnedDeps, MemoryStorage};
use cw_multi_test::{App, AppBuilder, BankKeeper, ContractWrapper, Executor};
use cosmwasm_std::Timestamp;
use crate::helpers::tests::verify_query_response;
use crate::msg::{InstantiateMsg, PathMsg, QueryMsg, QuotaMsg, SudoMsg};
use crate::state::tests::RESET_TIME_WEEKLY;
use crate::state::tests::{RESET_TIME_WEEKLY, RESET_TIME_DAILY, RESET_TIME_MONTHLY};
use crate::state::{RateLimit, GOVMODULE, IBCMODULE, RATE_LIMIT_TRACKERS};

const IBC_ADDR: &str = "IBC_MODULE";
const GOV_ADDR: &str = "GOV_MODULE";

pub const SECONDS_PER_DAY: u64 = 86400;
pub const SECONDS_PER_HOUR: u64 = 3600;

pub(crate) struct TestEnv {
pub env: Env,
pub deps: OwnedDeps<MemoryStorage, MockApi, MockQuerier>
}
fn new_test_env(paths: &[PathMsg]) -> TestEnv {

let mut deps: OwnedDeps<MemoryStorage, MockApi, MockQuerier> = mock_dependencies();
let env = mock_env();
let msg = InstantiateMsg {
gov_module: Addr::unchecked(GOV_ADDR),
ibc_module: Addr::unchecked(IBC_ADDR),
paths: paths.to_vec(),
};
let info = mock_info(GOV_ADDR, &vec![]);
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();

TestEnv {
deps,
env,
}
}

impl TestEnv {
pub fn plus_hours(&mut self, hours: u64) {
self.env.block.time = self.env.block.time.plus_seconds( hours * SECONDS_PER_HOUR);
}
pub fn plus_days(&mut self, days: u64) {
self.env.block.time = self.env.block.time.plus_seconds(days * SECONDS_PER_DAY);
}
}

// performs a very basic migration test, ensuring that standard migration logic works
#[test]
fn test_basic_migration() {
let test_env = new_test_env(&[PathMsg {
channel_id: format!("any"),
denom: format!("denom"),
quotas: vec![QuotaMsg::new("weekly", RESET_TIME_WEEKLY, 10, 10)],
}]);



for key in RATE_LIMIT_TRACKERS.keys(&test_env.deps.storage, None, None, cosmwasm_std::Order::Ascending) {
match key {
Ok((k, v)) => {
println!("got key {}, {}", k, v);
}
Err(err) => {
println!("got error {err:#?}");
}
}
}
}

#[test] // Tests we ccan instantiate the contract and that the owners are set correctly
fn proper_instantiation() {
let mut deps = mock_dependencies();
Expand Down Expand Up @@ -219,7 +281,7 @@ fn asymetric_quotas() {

#[test] // Tests we can get the current state of the trackers
fn query_state() {
let mut deps = mock_dependencies();
let mut deps: OwnedDeps<MemoryStorage, MockApi, MockQuerier> = mock_dependencies();

let quota = QuotaMsg::new("weekly", RESET_TIME_WEEKLY, 10, 10);
let msg = InstantiateMsg {
Expand Down Expand Up @@ -397,3 +459,154 @@ fn test_tokenfactory_message() {
let _parsed: SudoMsg = serde_json_wasm::from_str(json).unwrap();
//println!("{parsed:?}");
}


#[test]
fn test_expired_rate_limits() {
let mut test_env = new_test_env(&[PathMsg {
channel_id: format!("any"),
denom: format!("denom"),
quotas: vec![
QuotaMsg::new("daily", RESET_TIME_DAILY, 1, 1),
QuotaMsg::new("weekly", RESET_TIME_WEEKLY, 5, 5),
QuotaMsg::new("monthly", RESET_TIME_MONTHLY, 5, 5),
],
}]);
// no rules should be expired
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits.len(), 0);

// advance timestamp by half day
test_env.plus_hours(12);

// still no rules should be expired
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits.len(), 0);

// advance timestamp by 13 hours
test_env.plus_hours(13);

// only 1 rule should be expired
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits[0].1.len(), 1);
assert_eq!(expired_limits[0].1[0].quota.name, "daily");

// advance timestamp by 6 days
test_env.plus_days(6);

// weekly + daily rules should be expired
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits[0].1.len(), 2);
// as long as the ordering of the `range(..)` function is the same
// this test shouldn't fail
assert_eq!(expired_limits[0].1[0].quota.name, "daily");
assert_eq!(expired_limits[0].1[1].quota.name, "weekly");
// advance timestamp by 24 days for a total of 31 days passed
test_env.plus_days(24);

// daily, weekly, monthly rules should be expired
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits[0].1.len(), 3);
assert_eq!(expired_limits[0].1[0].quota.name, "daily");
assert_eq!(expired_limits[0].1[1].quota.name, "weekly");
assert_eq!(expired_limits[0].1[2].quota.name, "monthly");
}
#[test]
fn test_rollover_expired_rate_limits() {
let mut test_env = new_test_env(&[PathMsg {
channel_id: format!("any"),
denom: format!("denom"),
quotas: vec![
QuotaMsg::new("daily", RESET_TIME_DAILY, 1, 1),
QuotaMsg::new("weekly", RESET_TIME_WEEKLY, 5, 5),
QuotaMsg::new("monthly", RESET_TIME_MONTHLY, 5, 5),
],
}]);

// shorthand for returning all rules
fn get_rules(test_env: &TestEnv) -> HashMap<String, RateLimit> {
let rules = RATE_LIMIT_TRACKERS.range(&test_env.deps.storage, None, None, cosmwasm_std::Order::Ascending).flatten().collect::<Vec<_>>();
let mut indexed_rules: HashMap<String, RateLimit> = HashMap::new();
rules.into_iter().for_each(|(_, rules)| {
rules.into_iter().for_each(|rule| {indexed_rules.insert(rule.quota.name.clone(), rule);});
});
indexed_rules
}

// store a copy of the unchanged rules
let original_rules = get_rules(&test_env);
// ensure the helper function indexes rules as expected
assert!(original_rules.contains_key("daily"));
assert!(original_rules.contains_key("weekly"));
assert!(original_rules.contains_key("monthly"));

// no rules should be expired
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits.len(), 0);

// advance timestamp by a day
test_env.plus_hours(25);

// only 1 rule should be expired
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits[0].1.len(), 1);
assert_eq!(expired_limits[0].1[0].quota.name, "daily");

// trigger expiration of daily rate limits
rollover_expired_rate_limits(test_env.deps.as_mut(), test_env.env.clone()).unwrap();

// store a copy of rules after the daily limit has changed
let daily_rules_changed = get_rules(&test_env);
// ensure the daily period is different
assert!(daily_rules_changed.get("daily").unwrap().flow.period_end > original_rules.get("daily").unwrap().flow.period_end);
// ensure weekly and monthly rules are the same
assert!(daily_rules_changed.get("weekly").unwrap().flow.period_end == original_rules.get("weekly").unwrap().flow.period_end);
assert!(daily_rules_changed.get("monthly").unwrap().flow.period_end == original_rules.get("monthly").unwrap().flow.period_end);

// advance timestamp by half day, no rules should be changed
test_env.plus_hours(12);

// there should be no expired rate limits
let expired_limits = expired_rate_limits(test_env.deps.as_ref(), test_env.env.block.time);
assert_eq!(expired_limits.len(), 0);

// advance timestamp by another half day
test_env.plus_hours(13);

// daily rule should change again
rollover_expired_rate_limits(test_env.deps.as_mut(), test_env.env.clone()).unwrap();

let daily_rules_changed2 = get_rules(&test_env);
// ensure the daily period is different
assert!(daily_rules_changed2.get("daily").unwrap().flow.period_end > daily_rules_changed.get("daily").unwrap().flow.period_end);
// ensure weekly and monthly rules are the same
assert!(daily_rules_changed2.get("weekly").unwrap().flow.period_end == original_rules.get("weekly").unwrap().flow.period_end);
assert!(daily_rules_changed2.get("monthly").unwrap().flow.period_end == original_rules.get("monthly").unwrap().flow.period_end);

// advance timestamp by 6 days
test_env.plus_days(6);

// daily rule + weekly rules should change
rollover_expired_rate_limits(test_env.deps.as_mut(), test_env.env.clone()).unwrap();

let weekly_rules_changed = get_rules(&test_env);
// ensure the daily period is different
assert!(weekly_rules_changed.get("daily").unwrap().flow.period_end > daily_rules_changed2.get("daily").unwrap().flow.period_end);
// ensure weekly is different
assert!(weekly_rules_changed.get("weekly").unwrap().flow.period_end > daily_rules_changed2.get("weekly").unwrap().flow.period_end);
// ensure monthly is unchanged
assert!(weekly_rules_changed.get("monthly").unwrap().flow.period_end == original_rules.get("monthly").unwrap().flow.period_end);

// advance timestamp by 24 days
test_env.plus_days(24);

// all rules should now rollover
rollover_expired_rate_limits(test_env.deps.as_mut(), test_env.env.clone()).unwrap();

let monthly_rules_changed = get_rules(&test_env);
// ensure all three periods have reset
assert!(monthly_rules_changed.get("daily").unwrap().flow.period_end > weekly_rules_changed.get("daily").unwrap().flow.period_end);
assert!(monthly_rules_changed.get("weekly").unwrap().flow.period_end > weekly_rules_changed.get("weekly").unwrap().flow.period_end);
assert!(monthly_rules_changed.get("monthly").unwrap().flow.period_end > weekly_rules_changed.get("monthly").unwrap().flow.period_end);

}
9 changes: 9 additions & 0 deletions x/ibc-rate-limit/contracts/rate-limiter/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@ pub enum ContractError {
channel_id: String,
denom: String,
},
#[error("semver parse error {0}")]
SemVer(String)
}

impl From<semver::Error> for ContractError {
fn from(err: semver::Error) -> Self {
Self::SemVer(err.to_string())
}
}

Loading