diff --git a/app/app.go b/app/app.go index b5d3f67ef1b..54d78c60e65 100644 --- a/app/app.go +++ b/app/app.go @@ -176,6 +176,10 @@ func NewOsmosisApp( wasmDir := filepath.Join(homePath, "wasm") wasmConfig, err := wasm.ReadWasmConfig(appOpts) + + // Uncomment this for debugging contracts. In the future this could be made into a param passed by the tests + //wasmConfig.ContractDebugMode = true + if err != nil { panic(fmt.Sprintf("error while reading wasm config: %s", err)) } diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 1090638c445..5f56a1e6472 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -351,7 +351,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( // The last arguments can contain custom message handlers, and custom query handlers, // if we want to allow any custom callbacks - supportedFeatures := "iterator,staking,stargate,osmosis" + supportedFeatures := "iterator,staking,stargate,osmosis,cosmwasm_1_1" wasmOpts = append(owasm.RegisterCustomPlugins(appKeepers.GAMMKeeper, appKeepers.BankKeeper, appKeepers.TwapKeeper, appKeepers.TokenFactoryKeeper), wasmOpts...) wasmOpts = append(owasm.RegisterStargateQueries(*bApp.GRPCQueryRouter(), appCodec), wasmOpts...) diff --git a/tests/e2e/configurer/chain/commands.go b/tests/e2e/configurer/chain/commands.go index 335d4924eca..ee070b4b754 100644 --- a/tests/e2e/configurer/chain/commands.go +++ b/tests/e2e/configurer/chain/commands.go @@ -105,7 +105,7 @@ func (n *NodeConfig) FailIBCTransfer(from, recipient, amount string) { cmd := []string{"osmosisd", "tx", "ibc-transfer", "transfer", "transfer", "channel-0", recipient, amount, fmt.Sprintf("--from=%s", from)} - _, _, err := n.containerManager.ExecTxCmdWithSuccessString(n.t, n.chainId, n.Name, cmd, "Rate Limit exceeded") + _, _, err := n.containerManager.ExecTxCmdWithSuccessString(n.t, n.chainId, n.Name, cmd, "rate limit exceeded") require.NoError(n.t, err) n.LogActionF("Failed to send IBC transfer (as expected)") diff --git a/tests/e2e/configurer/chain/queries.go b/tests/e2e/configurer/chain/queries.go index f40782b1a9e..ddea89da905 100644 --- a/tests/e2e/configurer/chain/queries.go +++ b/tests/e2e/configurer/chain/queries.go @@ -85,15 +85,16 @@ func (n *NodeConfig) QueryBalances(address string) (sdk.Coins, error) { return balancesResp.GetBalances(), nil } -func (n *NodeConfig) QueryTotalSupply() (sdk.Coins, error) { - bz, err := n.QueryGRPCGateway("cosmos/bank/v1beta1/supply") +func (n *NodeConfig) QuerySupplyOf(denom string) (sdk.Int, error) { + path := fmt.Sprintf("cosmos/bank/v1beta1/supply/%s", denom) + bz, err := n.QueryGRPCGateway(path) require.NoError(n.t, err) - var supplyResp banktypes.QueryTotalSupplyResponse + var supplyResp banktypes.QuerySupplyOfResponse if err := util.Cdc.UnmarshalJSON(bz, &supplyResp); err != nil { - return sdk.Coins{}, err + return sdk.NewInt(0), err } - return supplyResp.GetSupply(), nil + return supplyResp.Amount.Amount, nil } func (n *NodeConfig) QueryContractsFromId(codeId int) ([]string, error) { diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 47840e8d91c..d46973e3abb 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -142,9 +142,8 @@ func (s *IntegrationTestSuite) TestIBCTokenTransferRateLimiting() { node, err := chainA.GetDefaultNode() s.NoError(err) - supply, err := node.QueryTotalSupply() + osmoSupply, err := node.QuerySupplyOf("uosmo") s.NoError(err) - osmoSupply := supply.AmountOf("uosmo") // balance, err := node.QueryBalances(chainA.NodeConfigs[1].PublicAddress) // s.NoError(err) diff --git a/x/ibc-rate-limit/bytecode/rate_limiter.wasm b/x/ibc-rate-limit/bytecode/rate_limiter.wasm index 3af5be772c7..f51ce90951a 100644 Binary files a/x/ibc-rate-limit/bytecode/rate_limiter.wasm and b/x/ibc-rate-limit/bytecode/rate_limiter.wasm differ diff --git a/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml b/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml index 4c78fcf37fb..9a82ff8d95a 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml +++ b/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml @@ -32,14 +32,20 @@ optimize = """docker run --rm -v "$(pwd)":/code \ """ [dependencies] -cosmwasm-std = "1.1.0" -cosmwasm-storage = "1.1.0" -cosmwasm-schema = "1.1.0" -cw-storage-plus = "0.13.2" +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" cw2 = "0.13.2" schemars = "0.8.8" serde = { version = "1.0.137", default-features = false, features = ["derive"] } thiserror = { version = "1.0.31" } +prost = {version = "0.11.2", default-features = false, features = ["prost-derive"]} +osmosis-std-derive = {version = "0.12.0"} +osmosis-std = "0.12.0" +sha2 = "0.10.6" +hex = "0.4.3" [dev-dependencies] cw-multi-test = "0.13.2" +serde-json-wasm = "0.4.1" diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/contract.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/contract.rs index 95458d4c464..ae04d88968e 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/contract.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/contract.rs @@ -5,7 +5,7 @@ use cw2::set_contract_version; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SudoMsg}; -use crate::state::{FlowType, Path, GOVMODULE, IBCMODULE}; +use crate::state::{FlowType, GOVMODULE, IBCMODULE}; use crate::{execute, query, sudo}; // version info for migration info @@ -66,36 +66,33 @@ pub fn execute( pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result { match msg { SudoMsg::SendPacket { - channel_id, - channel_value, - funds, - denom, - } => sudo::try_transfer( + packet, + local_denom, + channel_value_hint, + } => sudo::process_packet( deps, - &Path::new(&channel_id, &denom), - channel_value, - funds, + packet, FlowType::Out, env.block.time, + local_denom, + channel_value_hint, ), SudoMsg::RecvPacket { - channel_id, - channel_value, - funds, - denom, - } => sudo::try_transfer( + packet, + local_denom, + channel_value_hint, + } => sudo::process_packet( deps, - &Path::new(&channel_id, &denom), - channel_value, - funds, + packet, FlowType::In, env.block.time, + local_denom, + channel_value_hint, ), SudoMsg::UndoSend { - channel_id, - denom, - funds, - } => sudo::undo_send(deps, &Path::new(&channel_id, &denom), funds), + packet, + local_denom, + } => sudo::undo_send(deps, packet, local_denom), } } diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/contract_tests.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/contract_tests.rs index fa5b99e49da..0b62f21df44 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/contract_tests.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/contract_tests.rs @@ -1,6 +1,7 @@ #![cfg(test)] -use crate::{contract::*, ContractError}; +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}; @@ -41,7 +42,7 @@ fn consume_allowance() { gov_module: Addr::unchecked(GOV_ADDR), ibc_module: Addr::unchecked(IBC_ADDR), paths: vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![quota], }], @@ -49,24 +50,24 @@ fn consume_allowance() { let info = mock_info(GOV_ADDR, &vec![]); let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), - denom: format!("denom"), + denom: format!("denom") , channel_value: 3_300_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); let Attribute { key, value } = &res.attributes[4]; assert_eq!(key, "weekly_used_out"); assert_eq!(value, "300"); - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_300_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let err = sudo(deps.as_mut(), mock_env(), msg).unwrap_err(); assert!(matches!(err, ContractError::RateLimitExceded { .. })); } @@ -80,7 +81,7 @@ fn symetric_flows_dont_consume_allowance() { gov_module: Addr::unchecked(GOV_ADDR), ibc_module: Addr::unchecked(IBC_ADDR), paths: vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![quota], }], @@ -88,18 +89,18 @@ fn symetric_flows_dont_consume_allowance() { let info = mock_info(GOV_ADDR, &vec![]); let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - let send_msg = SudoMsg::SendPacket { + let send_msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_300_u32.into(), - funds: 300_u32.into(), - }; - let recv_msg = SudoMsg::RecvPacket { + funds: 300_u32.into() + ); + let recv_msg = test_msg_recv!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_000_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let res = sudo(deps.as_mut(), mock_env(), send_msg.clone()).unwrap(); let Attribute { key, value } = &res.attributes[3]; @@ -142,7 +143,7 @@ fn asymetric_quotas() { gov_module: Addr::unchecked(GOV_ADDR), ibc_module: Addr::unchecked(IBC_ADDR), paths: vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![quota], }], @@ -151,24 +152,24 @@ fn asymetric_quotas() { let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); // Sending 2% - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_060_u32.into(), - funds: 60_u32.into(), - }; + funds: 60_u32.into() + ); let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); let Attribute { key, value } = &res.attributes[4]; assert_eq!(key, "weekly_used_out"); assert_eq!(value, "60"); // Sending 2% more. Allowed, as sending has a 4% allowance - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_060_u32.into(), - funds: 60_u32.into(), - }; + funds: 60_u32.into() + ); let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); println!("{res:?}"); @@ -177,12 +178,12 @@ fn asymetric_quotas() { assert_eq!(value, "120"); // Receiving 1% should still work. 4% *sent* through the path, but we can still receive. - let recv_msg = SudoMsg::RecvPacket { + let recv_msg = test_msg_recv!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_000_u32.into(), - funds: 30_u32.into(), - }; + funds: 30_u32.into() + ); let res = sudo(deps.as_mut(), mock_env(), recv_msg).unwrap(); let Attribute { key, value } = &res.attributes[3]; assert_eq!(key, "weekly_used_in"); @@ -192,22 +193,22 @@ fn asymetric_quotas() { assert_eq!(value, "90"); // Sending 2%. Should fail. In balance, we've sent 4% and received 1%, so only 1% left to send. - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_060_u32.into(), - funds: 60_u32.into(), - }; + funds: 60_u32.into() + ); let err = sudo(deps.as_mut(), mock_env(), msg.clone()).unwrap_err(); assert!(matches!(err, ContractError::RateLimitExceded { .. })); // Sending 1%: Allowed; because sending has a 4% allowance. We've sent 4% already, but received 1%, so there's send cappacity again - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_060_u32.into(), - funds: 30_u32.into(), - }; + funds: 30_u32.into() + ); let res = sudo(deps.as_mut(), mock_env(), msg.clone()).unwrap(); let Attribute { key, value } = &res.attributes[3]; assert_eq!(key, "weekly_used_in"); @@ -226,7 +227,7 @@ fn query_state() { gov_module: Addr::unchecked(GOV_ADDR), ibc_module: Addr::unchecked(IBC_ADDR), paths: vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![quota], }], @@ -236,7 +237,7 @@ fn query_state() { let _res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); let query_msg = QueryMsg::GetQuotas { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), }; @@ -253,20 +254,20 @@ fn query_state() { env.block.time.plus_seconds(RESET_TIME_WEEKLY) ); - let send_msg = SudoMsg::SendPacket { + let send_msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_300_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); sudo(deps.as_mut(), mock_env(), send_msg.clone()).unwrap(); - let recv_msg = SudoMsg::RecvPacket { + let recv_msg = test_msg_recv!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_000_u32.into(), - funds: 30_u32.into(), - }; + funds: 30_u32.into() + ); sudo(deps.as_mut(), mock_env(), recv_msg.clone()).unwrap(); // Query @@ -291,7 +292,7 @@ fn bad_quotas() { gov_module: Addr::unchecked(GOV_ADDR), ibc_module: Addr::unchecked(IBC_ADDR), paths: vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![QuotaMsg { name: "bad_quota".to_string(), @@ -307,7 +308,7 @@ fn bad_quotas() { // If a quota is higher than 100%, we set it to 100% let query_msg = QueryMsg::GetQuotas { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), }; let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); @@ -332,7 +333,7 @@ fn undo_send() { gov_module: Addr::unchecked(GOV_ADDR), ibc_module: Addr::unchecked(IBC_ADDR), paths: vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![quota], }], @@ -340,22 +341,21 @@ fn undo_send() { let info = mock_info(GOV_ADDR, &vec![]); let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - let send_msg = SudoMsg::SendPacket { + let send_msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_300_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let undo_msg = SudoMsg::UndoSend { - channel_id: format!("channel"), - denom: format!("denom"), - funds: 300_u32.into(), + packet: Packet::mock(format!("channel"), format!("denom"), 300_u32.into()), + local_denom: Some(format!("denom")), }; sudo(deps.as_mut(), mock_env(), send_msg.clone()).unwrap(); let trackers = RATE_LIMIT_TRACKERS - .load(&deps.storage, ("channel".to_string(), "denom".to_string())) + .load(&deps.storage, ("any".to_string(), "denom".to_string())) .unwrap(); assert_eq!( trackers.first().unwrap().flow.outflow, @@ -367,9 +367,16 @@ fn undo_send() { sudo(deps.as_mut(), mock_env(), undo_msg.clone()).unwrap(); let trackers = RATE_LIMIT_TRACKERS - .load(&deps.storage, ("channel".to_string(), "denom".to_string())) + .load(&deps.storage, ("any".to_string(), "denom".to_string())) .unwrap(); assert_eq!(trackers.first().unwrap().flow.outflow, Uint256::from(0_u32)); assert_eq!(trackers.first().unwrap().flow.period_end, period_end); assert_eq!(trackers.first().unwrap().quota.channel_value, channel_value); } + +#[test] +fn test_basic_message() { + let json = r#"{"send_packet":{"packet":{"sequence":2,"source_port":"transfer","source_channel":"channel-0","destination_port":"transfer","destination_channel":"channel-0","data":{"denom":"stake","amount":"125000000000011250","sender":"osmo1dwtagd6xzl4eutwtyv6mewra627lkg3n3w26h6","receiver":"osmo1yvjkt8lnpxucjmspaj5ss4aa8562gx0a3rks8s"},"timeout_height":{"revision_height":100}}}}"#; + let parsed: SudoMsg = serde_json_wasm::from_str(json).unwrap(); + println!("{parsed:?}"); +} diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/integration_tests.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/integration_tests.rs index 363552e386d..fb9c711eaa3 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/integration_tests.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/integration_tests.rs @@ -1,10 +1,10 @@ #![cfg(test)] -use crate::{helpers::RateLimitingContract, msg::ExecuteMsg}; +use crate::{helpers::RateLimitingContract, msg::ExecuteMsg, test_msg_send}; use cosmwasm_std::{Addr, Coin, Empty, Uint128}; use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor}; use crate::{ - msg::{InstantiateMsg, PathMsg, QuotaMsg, SudoMsg}, + msg::{InstantiateMsg, PathMsg, QuotaMsg}, state::tests::{RESET_TIME_DAILY, RESET_TIME_MONTHLY, RESET_TIME_WEEKLY}, }; @@ -73,18 +73,18 @@ fn expiration() { let quota = QuotaMsg::new("weekly", RESET_TIME_WEEKLY, 10, 10); let (mut app, cw_rate_limit_contract) = proper_instantiate(vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![quota], }]); // Using all the allowance - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_000_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); let res = app.sudo(cosmos_msg).unwrap(); @@ -102,12 +102,12 @@ fn expiration() { assert_eq!(value, "300"); // Another packet is rate limited - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_000_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); let _err = app.sudo(cosmos_msg).unwrap_err(); @@ -120,12 +120,12 @@ fn expiration() { }); // Sending the packet should work now - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_000_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); let res = app.sudo(cosmos_msg).unwrap(); @@ -153,28 +153,28 @@ fn multiple_quotas() { ]; let (mut app, cw_rate_limit_contract) = proper_instantiate(vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas, }]); // Sending 1% to use the daily allowance - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); // Another packet is rate limited - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -185,12 +185,12 @@ fn multiple_quotas() { }); // Sending the packet should work now - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); @@ -204,12 +204,12 @@ fn multiple_quotas() { }); // Sending the packet should work now - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); } @@ -221,12 +221,12 @@ fn multiple_quotas() { }); // We now have exceeded the weekly limit! Even if the daily limit allows us, the weekly doesn't - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -237,12 +237,12 @@ fn multiple_quotas() { }); // We can still can't send because the weekly and monthly limits are the same - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -254,12 +254,12 @@ fn multiple_quotas() { }); // We can still can't send because the monthly limit hasn't passed - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -269,12 +269,12 @@ fn multiple_quotas() { b.time = b.time.plus_seconds((RESET_TIME_WEEKLY * 2) + 1) // Two weeks }); - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 101_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); } @@ -287,38 +287,38 @@ fn channel_value_cached() { ]; let (mut app, cw_rate_limit_contract) = proper_instantiate(vec![PathMsg { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas, }]); // Sending 1% (half of the daily allowance) - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 100_u32.into(), - funds: 1_u32.into(), - }; + funds: 1_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); // Sending 3% is now rate limited - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 100_u32.into(), - funds: 3_u32.into(), - }; + funds: 3_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); // Even if the channel value increases, the percentage is calculated based on the value at period start - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 100000_u32.into(), - funds: 3_u32.into(), - }; + funds: 3_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -333,12 +333,12 @@ fn channel_value_cached() { // Sending 1% of a new value (10_000) passes the daily check, cause it // has expired, but not the weekly check (The value for last week is // sitll 100, as only 1 day has passed) - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 10_000_u32.into(), - funds: 100_u32.into(), - }; + funds: 100_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -350,23 +350,23 @@ fn channel_value_cached() { }); // Sending 1% of a new value should work and set the value for the day at 10_000 - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 10_000_u32.into(), - funds: 100_u32.into(), - }; + funds: 100_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); // If the value magically decreasses. We can still send up to 100 more (1% of 10k) - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 1_u32.into(), - funds: 75_u32.into(), - }; + funds: 75_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); @@ -377,21 +377,22 @@ fn add_paths_later() { let (mut app, cw_rate_limit_contract) = proper_instantiate(vec![]); // All sends are allowed - let msg = SudoMsg::SendPacket { + let msg = test_msg_send!( channel_id: format!("channel"), denom: format!("denom"), channel_value: 3_000_u32.into(), - funds: 300_u32.into(), - }; + funds: 300_u32.into() + ); let cosmos_msg = cw_rate_limit_contract.sudo(msg.clone()); let res = app.sudo(cosmos_msg).unwrap(); + let Attribute { key, value } = &res.custom_attrs(1)[3]; assert_eq!(key, "quota"); assert_eq!(value, "none"); // Add a weekly limit of 1% let management_msg = ExecuteMsg::AddPath { - channel_id: format!("channel"), + channel_id: format!("any"), denom: format!("denom"), quotas: vec![QuotaMsg::new("weekly", RESET_TIME_WEEKLY, 1, 1)], }; diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/lib.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/lib.rs index 68e25ce20e3..6fcd1c32cec 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/lib.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/lib.rs @@ -6,6 +6,8 @@ mod error; pub mod msg; mod state; +pub mod packet; + // Functions mod execute; mod query; diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/msg.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/msg.rs index 0f1f0c4b061..05c3f9629f3 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/msg.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/msg.rs @@ -3,6 +3,8 @@ use cosmwasm_std::{Addr, Uint256}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::packet::Packet; + // PathMsg contains a channel_id and denom to represent a unique identifier within ibc-go, and a list of rate limit quotas #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct PathMsg { @@ -82,21 +84,18 @@ pub enum QueryMsg { #[cw_serde] pub enum SudoMsg { SendPacket { - channel_id: String, - denom: String, - channel_value: Uint256, - funds: Uint256, + packet: Packet, + local_denom: Option, + channel_value_hint: Option, }, RecvPacket { - channel_id: String, - denom: String, - channel_value: Uint256, - funds: Uint256, + packet: Packet, + local_denom: Option, + channel_value_hint: Option, }, UndoSend { - channel_id: String, - denom: String, - funds: Uint256, + packet: Packet, + local_denom: Option, }, } diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs index 6bc5b8cfed1..894fdc577c9 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs @@ -1,7 +1,11 @@ -use cosmwasm_std::{Addr, Deps, Timestamp}; +use crate::state::FlowType; +use cosmwasm_std::{Addr, Deps, StdError, Timestamp, Uint256}; +use osmosis_std_derive::CosmwasmExt; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct Height { /// Previously known as "epoch" revision_number: Option, @@ -10,15 +14,17 @@ pub struct Height { revision_height: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +// IBC transfer data +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct FungibleTokenData { - denom: String, - amount: u128, + pub denom: String, + amount: Uint256, sender: Addr, receiver: Addr, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +// An IBC packet +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct Packet { pub sequence: u64, pub source_port: String, @@ -30,35 +36,128 @@ pub struct Packet { pub timeout_timestamp: Option, } +// SupplyOf query message definition. +// osmosis-std doesn't currently support the SupplyOf query, so I'm defining it localy so it can be used to obtain the channel value +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/cosmos.bank.v1beta1.QuerySupplyOfRequest")] +#[proto_query( + path = "/cosmos.bank.v1beta1.Query/SupplyOf", + response_type = QuerySupplyOfResponse +)] +pub struct QuerySupplyOfRequest { + #[prost(string, tag = "1")] + pub denom: ::prost::alloc::string::String, +} + +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/cosmos.bank.v1beta1.QuerySupplyOf")] +pub struct QuerySupplyOfResponse { + #[prost(message, optional, tag = "1")] + pub amount: ::core::option::Option, +} +// End of SupplyOf query message definition + +use std::str::FromStr; // Needed to parse the coin's String as Uint256 + impl Packet { - pub fn channel_value(&self, _deps: Deps) -> u128 { - // let balance = deps.querier.query_all_balances("address", self.data.denom); - // deps.querier.sup - return 125000000000011250 * 2; + pub fn mock(channel_id: String, denom: String, funds: Uint256) -> Packet { + Packet { + sequence: 0, + source_port: "transfer".to_string(), + source_channel: channel_id.clone(), + destination_port: "transfer".to_string(), + destination_channel: channel_id, + data: crate::packet::FungibleTokenData { + denom, + amount: funds, + sender: Addr::unchecked("sender"), + receiver: Addr::unchecked("receiver"), + }, + timeout_height: crate::packet::Height { + revision_number: None, + revision_height: None, + }, + timeout_timestamp: None, + } } - pub fn get_funds(&self) -> u128 { - return self.data.amount; + pub fn channel_value(&self, deps: Deps) -> Result { + let res = QuerySupplyOfRequest { + denom: self.local_denom(), + } + .query(&deps.querier)?; + Uint256::from_str(&res.amount.unwrap_or_default().amount) } - fn local_channel(&self) -> String { - // Pick the appropriate channel depending on whether this is a send or a recv - return self.destination_channel.clone(); + pub fn get_funds(&self) -> Uint256 { + self.data.amount } - fn local_demom(&self) -> String { - // This should actually convert the denom from the packet to the osmosis denom, but for now, just returning this - return self.data.denom.clone(); + fn local_channel(&self, direction: &FlowType) -> String { + // Pick the appropriate channel depending on whether this is a send or a recv + match direction { + FlowType::In => self.destination_channel.clone(), + FlowType::Out => self.source_channel.clone(), + } } - pub fn path_data(&self) -> (String, String) { - let denom = self.local_demom(); - let channel = if denom.starts_with("ibc/") { - self.local_channel() - } else { - "any".to_string() // native tokens are rate limited globally - }; + fn local_denom(&self) -> String { + if !self.data.denom.starts_with("transfer/") { + // For native tokens we just use what's on the packet + return self.data.denom.clone(); + } + // For non-native tokens, we need to generate the IBCDenom + let mut hasher = Sha256::new(); + hasher.update(self.data.denom.as_bytes()); + let result = hasher.finalize(); + let hash = hex::encode(result); + format!("ibc/{}", hash.to_uppercase()) + } - return (channel, denom); + pub fn path_data(&self, direction: &FlowType) -> (String, String) { + (self.local_channel(direction), self.local_denom()) } } + +// Helpers + +// Create a new packet for testing +#[macro_export] +macro_rules! test_msg_send { + (channel_id: $channel_id:expr, denom: $denom:expr, channel_value: $channel_value:expr, funds: $funds:expr) => { + $crate::msg::SudoMsg::SendPacket { + packet: $crate::packet::Packet::mock($channel_id, $denom, $funds), + local_denom: Some($denom), + channel_value_hint: Some($channel_value), + } + }; +} + +#[macro_export] +macro_rules! test_msg_recv { + (channel_id: $channel_id:expr, denom: $denom:expr, channel_value: $channel_value:expr, funds: $funds:expr) => { + $crate::msg::SudoMsg::RecvPacket { + packet: $crate::packet::Packet::mock($channel_id, $denom, $funds), + local_denom: Some($denom), + channel_value_hint: Some($channel_value), + } + }; +} diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/sudo.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/sudo.rs index 0a8ae8e5161..378b8430870 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/sudo.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/sudo.rs @@ -1,10 +1,41 @@ use cosmwasm_std::{DepsMut, Response, Timestamp, Uint256}; use crate::{ + packet::Packet, state::{FlowType, Path, RateLimit, RATE_LIMIT_TRACKERS}, ContractError, }; +// This function will process a packet and extract the paths information, funds, +// and channel value from it. This is will have to interact with the chain via grpc queries to properly +// obtain this information. +// +// For backwards compatibility, we're teporarily letting the chain override the +// denom and channel value, but these should go away in favour of the contract +// extracting these from the packet +pub fn process_packet( + deps: DepsMut, + packet: Packet, + direction: FlowType, + now: Timestamp, + local_denom: Option, + channel_value_hint: Option, +) -> Result { + let (channel_id, extracted_denom) = packet.path_data(&direction); + let denom = match local_denom { + Some(denom) => denom, + None => extracted_denom, + }; + + let path = &Path::new(&channel_id, &denom); + let funds = packet.get_funds(); + let channel_value = match channel_value_hint { + Some(channel_value) => channel_value, + None => packet.channel_value(deps.as_ref())?, + }; + try_transfer(deps, path, channel_value, funds, direction, now) +} + /// This function checks the rate limit and, if successful, stores the updated data about the value /// that has been transfered through the channel for a specific denom. /// If the period for a RateLimit has ended, the Flow information is reset. @@ -20,15 +51,20 @@ pub fn try_transfer( now: Timestamp, ) -> Result { // Sudo call. Only go modules should be allowed to access this - let trackers = RATE_LIMIT_TRACKERS.may_load(deps.storage, path.into())?; - let configured = match trackers { - None => false, - Some(ref x) if x.is_empty() => false, - _ => true, - }; + // Fetch potential trackers for "any" channel of the required token + let any_path = Path::new("any", path.denom.clone()); + let mut any_trackers = RATE_LIMIT_TRACKERS + .may_load(deps.storage, any_path.clone().into())? + .unwrap_or_default(); + // Fetch trackers for the requested path + let mut trackers = RATE_LIMIT_TRACKERS + .may_load(deps.storage, path.into())? + .unwrap_or_default(); + + let not_configured = trackers.is_empty() && any_trackers.is_empty(); - if !configured { + if not_configured { // No Quota configured for the current path. Allowing all messages. return Ok(Response::new() .add_attribute("method", "try_transfer") @@ -37,16 +73,20 @@ pub fn try_transfer( .add_attribute("quota", "none")); } - let mut rate_limits = trackers.unwrap(); - // If any of the RateLimits fails, allow_transfer() will return // ContractError::RateLimitExceded, which we'll propagate out - let results: Vec = rate_limits + let results: Vec = trackers + .iter_mut() + .map(|limit| limit.allow_transfer(path, &direction, funds, channel_value, now)) + .collect::>()?; + + let any_results: Vec = any_trackers .iter_mut() .map(|limit| limit.allow_transfer(path, &direction, funds, channel_value, now)) .collect::>()?; RATE_LIMIT_TRACKERS.save(deps.storage, path.into(), &results)?; + RATE_LIMIT_TRACKERS.save(deps.storage, any_path.into(), &any_results)?; let response = Response::new() .add_attribute("method", "try_transfer") @@ -55,7 +95,11 @@ pub fn try_transfer( // Adds the attributes for each path to the response. In prod, the // addtribute add_rate_limit_attributes is a noop - results.iter().fold(Ok(response), |acc, result| { + let response: Result = + any_results.iter().fold(Ok(response), |acc, result| { + Ok(add_rate_limit_attributes(acc?, result)) + }); + results.iter().fold(Ok(response?), |acc, result| { Ok(add_rate_limit_attributes(acc?, result)) }) } @@ -96,17 +140,31 @@ fn add_rate_limit_attributes(response: Response, result: &RateLimit) -> Response // This function manually injects an inflow. This is used when reverting a // packet that failed ack or timed-out. -pub fn undo_send(deps: DepsMut, path: &Path, funds: Uint256) -> Result { +pub fn undo_send( + deps: DepsMut, + packet: Packet, + local_denom: Option, +) -> Result { // Sudo call. Only go modules should be allowed to access this - let trackers = RATE_LIMIT_TRACKERS.may_load(deps.storage, path.into())?; - - let configured = match trackers { - None => false, - Some(ref x) if x.is_empty() => false, - _ => true, + let (channel_id, extracted_denom) = packet.path_data(&FlowType::Out); // Sends have direction out. + let denom = match local_denom { + Some(denom) => denom, + None => extracted_denom, }; + let path = &Path::new(&channel_id, &denom); + let any_path = Path::new("any", &denom); + let funds = packet.get_funds(); + + let mut any_trackers = RATE_LIMIT_TRACKERS + .may_load(deps.storage, any_path.clone().into())? + .unwrap_or_default(); + let mut trackers = RATE_LIMIT_TRACKERS + .may_load(deps.storage, path.into())? + .unwrap_or_default(); + + let not_configured = trackers.is_empty() && any_trackers.is_empty(); - if !configured { + if not_configured { // No Quota configured for the current path. Allowing all messages. return Ok(Response::new() .add_attribute("method", "try_transfer") @@ -115,10 +173,15 @@ pub fn undo_send(deps: DepsMut, path: &Path, funds: Uint256) -> Result = rate_limits + let results: Vec = trackers + .iter_mut() + .map(|limit| { + limit.flow.undo_flow(FlowType::Out, funds); + limit.to_owned() + }) + .collect(); + let any_results: Vec = any_trackers .iter_mut() .map(|limit| { limit.flow.undo_flow(FlowType::Out, funds); @@ -127,9 +190,11 @@ pub fn undo_send(deps: DepsMut, path: &Path, funds: Uint256) -> Result ibc/xxx - // send native: denom -> denom - // recv (B)non-native: denom - // recv (B)native: transfer/channel-0/denom - // - if strings.HasPrefix(denom, "transfer/") { - denomTrace := transfertypes.ParseDenomTrace(denom) - return denomTrace.IBCDenom() - } else { - return denom - } -} - -func CalculateChannelValue(ctx sdk.Context, denom string, bankKeeper bankkeeper.Keeper, channelKeeper channelkeeper.Keeper) sdk.Int { - // For non-native (ibc) tokens, return the supply if the token in osmosis - if strings.HasPrefix(denom, "ibc/") { - return bankKeeper.GetSupplyWithOffset(ctx, denom).Amount - } - - return bankKeeper.GetSupplyWithOffset(ctx, denom).Amount - - // ToDo: The commented-out code bellow is what we want to happen, but we're temporarily - // using the whole supply for efficiency until there's a solution for - // https://github.com/cosmos/ibc-go/issues/2664 - - // For native tokens, obtain the balance held in escrow for all potential channels - //channels := channelKeeper.GetAllChannels(ctx) - //balance := sdk.NewInt(0) - //for _, channel := range channels { - // escrowAddress := transfertypes.GetEscrowAddress("transfer", channel.ChannelId) - // balance = balance.Add(bankKeeper.GetBalance(ctx, escrowAddress, denom).Amount) - // - //} - //return balance -}