From 2a71be2681e1bb48cc5ca3e7ac0deff4df248118 Mon Sep 17 00:00:00 2001 From: Nicolas Lara Date: Mon, 31 Oct 2022 05:40:38 +0100 Subject: [PATCH] Rate limit - Cleaner tests (#3183) * improved testing framework * can test both send and recv for success and failure * cleanner testing framework * added contract instantiation * working wasm integration * added params for contract config * extracted param registration * active rate limiting * calculating channel value * cleaner tests * fix issue with epochs * fixed tests * testing rate limit reset * linting * added receive middleware * added test for non-configured channel * make format * Revert "make format" This reverts commit 9ffdc37c3d473e3c640e8fd85872f3a2f178c857. * only applying format to ibc-rate-limit * applying fmt to app.go * added gov_module and changed no-quota default to "allow all" * added asymetric quotas * moved getters to modules.go * initial work to support multiple quotas * added multiple quotas * small fixes * reordered imports * added management messages * reorganized management messages and experimenting with e2e testing * commenting out test configuration test for now * added query * added flow unit test * cleanup * added AddChannel tests * format * test values are properly stored * testing remove channel * some more rate limiting tests * moved tests about test setup to the right place * fixed params * merged main * running gofumpt * added ibc-rate-limiting contract * added ibc-rate-limit middleware * added chain integration and tests * reverted change to match merged branch in main (#2341 instead of #2274) * added cosmwasm workflow * added a migrate message * added some doc comments to the state * added doc comments * fixed dependency after merging https://github.com/osmosis-labs/cosmos-sdk/pull/312 * added migration msg * added workflow * experimenting with better workflow * added missing $ * using env * Update x/ibc-rate-limit/contracts/rate-limiter/src/msg.rs Co-authored-by: Dev Ojha * using stable for clippy * removed gitkeep * using the minimal profile for clippy * experimenting with cache * removed target from lints * cleaner matrix? * COmments & questions * debugging * more debugging * debug faster * quick cache debug * typo * quick workflow check * working tests with optimization * testing artifacts * split the wasm target into its own step * artifacts without slash * full working tests * clippy fixes * workflow without test data checks and clippy fixes * renamed CHANNEL_FLOWS * renaming and code clenaup * more renames * renames and code fixes * reordered imports * cargo fmt * added danom tracking * cleanup * refactoring * changes to the expiration logic so that balances are calculated based on the direction (as suggested by @ValarDragon) * slightly slower but considerably cleanner * cleanup attributes and removed redundancy when not testing * update to edition 2021 * added comments explaining the tests * removed .beaker * unified gitignore * removed second gitignore * better doc comments * spelling * spelling * added channel value cache * updated the middlware to use the new contract interface * update middleware to match new contract interface * added missing updates * updated dependencies * added missing helpers * go.mod changes shouldn't be in this branch * Revert "go.mod changes shouldn't be in this branch" This reverts commit f8b972a5ea2f2c2b8007fa31c3db5d4450d8ab56. * moved send and receive to sudo * reorganizing * calling the contract via sudo * lint * removed gitkeep * using sudo instead of execute * cleaned up and updated contract and integration * updated x86 test wasm file * fixed bad print * storing and instantiating the contract * setting up E2E tests for ibc rate limits * fixed proposal. Now just have to get the math right and cleanup * Using the supply for the channel value * experimenting with e2e tests * passing the contract keeper instead of instantiating it each time * changes from code review * added contract from main and changes to the middleware from code review * using the correct bank supply method * debugging issues with e2e tests. Everything works after one interaction from the cli, but not before * updated dependency to match latest sdk form (now that the changes to this repo have been applied) * working E2E test for rate limiting * added e2e tests and changes from code review * removed debug logs * remove debug logs * updated test to also use GetSupplyWithOffset * using correct GetSupplyWithOffset method * using correct GetSupplyWithOffset method * lint * removed e2e from this branch as it's not doing anything without the integration * lint * tests fail on CI because of "inactive" proposal. Is the deposit the issue? * remove rate limiting after the test so it doesn't interfeer with the other tests * using standard proposals instead of expedited * added packet reverts on unsuccessful acks and timeouts * lint * ran gofumpt * lint * added undo to the contract * integrating undo * updated contract with x86 wasm file * added undo for sent packages when they are rejected bia timeout or a bad ack * added a readme * markdown lint * added readme * abstracted params * better params and param tests * using a helper function instead of returning from the test * updated contract to allow for undo * added undo, readme, and cleanup based on reviews * updated wasm file with x86 version * using string params in e2e tests * updated to v12 * removed unnecessary keeper * only exposing what's needed * refactoring * updated types * added shell history to gitignore * adding only one wasm file to the codebase * remove test for same wasm files. No longer needed. * refactor based on code review * removed integration tests as they won't pass without integration * added params unit test * reorganized tests * reorganizing tests * refactoring * added address length limit * added tests and fixed lack of return * remove tests from bad merge * remove from bad merge again * comment * test helpers for cosmwasm contracts * added helpers for ibctesting * comments * removed unnecessary txConfig * fixed typos * clearer comment * using second helper function for ExportGenesis * added new wasm file * updated the contract to cosmwasm 1.1 and Uint256 for amounts * Fixed send with ibc assets. Better tests and error messages * updated contract with x86 version * gofumpt * fixed clippy errors * using the escrowed value as the channel value for native tokens * gofumpt * update fail string * initial experiments with moving the calculations into the contract * initial experiments with using the packet inside the contract * improved tests. Experiments with packet in the contract. * original contract * cleaner tests * more test cleanup * cleaner tests * cleanup * align values from the tests and the contract * fixed amounts for receive and cleanup code * removed redundant wrapping logic * adaped failed send test to the new testing abstractions * only manipulate time on chain A * remove commented out block * changed lints to stable so they change less often * gofumpt * update channel value tests * added x86 version of the contract for ci * remove lint type that doesn't exist on stable Co-authored-by: Dev Ojha Co-authored-by: Dev Ojha --- .github/workflows/contracts.yml | 12 +- .gitignore | 4 + app/apptesting/events.go | 6 + app/keepers/keepers.go | 32 +- tests/e2e/configurer/chain/commands.go | 2 +- tests/e2e/e2e_test.go | 123 +++++ .../rate-limiter/src/contract_tests.rs | 18 +- .../contracts/rate-limiter/src/error.rs | 8 +- .../rate-limiter/src/integration_tests.rs | 22 +- .../contracts/rate-limiter/src/packet.rs | 64 +++ .../contracts/rate-limiter/src/state.rs | 59 ++- x/ibc-rate-limit/ibc_middleware_test.go | 462 ++++++++++++++++++ x/ibc-rate-limit/ibc_module.go | 24 +- x/ibc-rate-limit/ics4_wrapper.go | 15 +- x/ibc-rate-limit/rate_limit.go | 44 +- x/ibc-rate-limit/testdata/rate_limiter.wasm | Bin 199435 -> 203516 bytes x/ibc-rate-limit/testutil/chain.go | 96 ++++ x/ibc-rate-limit/testutil/wasm.go | 70 +++ x/ibc-rate-limit/types/errors.go | 3 +- 19 files changed, 1012 insertions(+), 52 deletions(-) create mode 100644 x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs create mode 100644 x/ibc-rate-limit/ibc_middleware_test.go create mode 100644 x/ibc-rate-limit/testutil/chain.go create mode 100644 x/ibc-rate-limit/testutil/wasm.go diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index f5f5301c31b..50dc9addb06 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -82,11 +82,11 @@ jobs: path: ${{ matrix.contract.workdir }}${{ matrix.contract.build }} retention-days: 1 -# - name: Check Test Data -# working-directory: ${{ matrix.contract.workdir }} -# if: ${{ matrix.contract.output != null }} -# run: > -# diff ${{ matrix.contract.output }} ${{ matrix.contract.build }} + - name: Check Test Data + working-directory: ${{ matrix.contract.workdir }} + if: ${{ matrix.contract.output != null }} + run: > + diff ${{ matrix.contract.output }} ${{ matrix.contract.build }} lints: @@ -107,7 +107,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly + toolchain: stable override: true components: rustfmt, clippy diff --git a/.gitignore b/.gitignore index 713ac5a1033..ff99e2886b4 100644 --- a/.gitignore +++ b/.gitignore @@ -230,3 +230,7 @@ Cargo.lock .beaker blocks.db **/blocks.db* + +# Ignore e2e test artifacts (which clould leak information if commited) +.ash_history +.bash_history \ No newline at end of file diff --git a/app/apptesting/events.go b/app/apptesting/events.go index 7d0a4d4dfdd..cdfae502886 100644 --- a/app/apptesting/events.go +++ b/app/apptesting/events.go @@ -21,11 +21,17 @@ func (s *KeeperTestHelper) AssertEventEmitted(ctx sdk.Context, eventTypeExpected func (s *KeeperTestHelper) FindEvent(events []sdk.Event, name string) sdk.Event { index := slices.IndexFunc(events, func(e sdk.Event) bool { return e.Type == name }) + if index == -1 { + return sdk.Event{} + } return events[index] } func (s *KeeperTestHelper) ExtractAttributes(event sdk.Event) map[string]string { attrs := make(map[string]string) + if event.Attributes == nil { + return attrs + } for _, a := range event.Attributes { attrs[string(a.Key)] = string(a.Value) } diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 7804dc34024..83c53e3f2cd 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -2,6 +2,7 @@ package keepers import ( "github.com/CosmWasm/wasmd/x/wasm" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -32,6 +33,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/upgrade" upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + ibcratelimit "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit" + ibcratelimittypes "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit/types" icahost "github.com/cosmos/ibc-go/v3/modules/apps/27-interchain-accounts/host" icahostkeeper "github.com/cosmos/ibc-go/v3/modules/apps/27-interchain-accounts/host/keeper" @@ -110,10 +113,13 @@ type AppKeepers struct { SuperfluidKeeper *superfluidkeeper.Keeper GovKeeper *govkeeper.Keeper WasmKeeper *wasm.Keeper + ContractKeeper *wasmkeeper.PermissionedKeeper TokenFactoryKeeper *tokenfactorykeeper.Keeper + // IBC modules // transfer module - TransferModule transfer.AppModule + TransferModule transfer.AppModule + RateLimitingICS4Wrapper *ibcratelimit.ICS4Wrapper // keys to access the substores keys map[string]*sdk.KVStoreKey @@ -195,12 +201,24 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.ScopedIBCKeeper, ) + // ChannelKeeper wrapper for rate limiting SendPacket(). The wasmKeeper needs to be added after it's created + rateLimitingParams := appKeepers.GetSubspace(ibcratelimittypes.ModuleName) + rateLimitingParams = rateLimitingParams.WithKeyTable(ibcratelimittypes.ParamKeyTable()) + rateLimitingICS4Wrapper := ibcratelimit.NewICS4Middleware( + appKeepers.IBCKeeper.ChannelKeeper, + appKeepers.AccountKeeper, + nil, + appKeepers.BankKeeper, + rateLimitingParams, + ) + appKeepers.RateLimitingICS4Wrapper = &rateLimitingICS4Wrapper + // Create Transfer Keepers transferKeeper := ibctransferkeeper.NewKeeper( appCodec, appKeepers.keys[ibctransfertypes.StoreKey], appKeepers.GetSubspace(ibctransfertypes.ModuleName), - appKeepers.IBCKeeper.ChannelKeeper, + appKeepers.RateLimitingICS4Wrapper, // The ICS4Wrapper is replaced by the rateLimitingICS4Wrapper instead of the channel appKeepers.IBCKeeper.ChannelKeeper, &appKeepers.IBCKeeper.PortKeeper, appKeepers.AccountKeeper, @@ -211,6 +229,9 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.TransferModule = transfer.NewAppModule(*appKeepers.TransferKeeper) transferIBCModule := transfer.NewIBCModule(*appKeepers.TransferKeeper) + // RateLimiting IBC Middleware + rateLimitingTransferModule := ibcratelimit.NewIBCModule(transferIBCModule, appKeepers.RateLimitingICS4Wrapper) + icaHostKeeper := icahostkeeper.NewKeeper( appCodec, appKeepers.keys[icahosttypes.StoreKey], appKeepers.GetSubspace(icahosttypes.SubModuleName), @@ -226,7 +247,8 @@ func (appKeepers *AppKeepers) InitNormalKeepers( // Create static IBC router, add transfer route, then set and seal it ibcRouter := porttypes.NewRouter() ibcRouter.AddRoute(icahosttypes.SubModuleName, icaHostIBCModule). - AddRoute(ibctransfertypes.ModuleName, transferIBCModule) + // The transferIBC module is replaced by rateLimitingTransferModule + AddRoute(ibctransfertypes.ModuleName, &rateLimitingTransferModule) // Note: the sealing is done after creating wasmd and wiring that up // create evidence keeper with router @@ -343,6 +365,9 @@ func (appKeepers *AppKeepers) InitNormalKeepers( wasmOpts..., ) appKeepers.WasmKeeper = &wasmKeeper + // Update the ICS4Wrapper with the proper contractKeeper + appKeepers.ContractKeeper = wasmkeeper.NewDefaultPermissionKeeper(appKeepers.WasmKeeper) + appKeepers.RateLimitingICS4Wrapper.ContractKeeper = appKeepers.ContractKeeper // wire up x/wasm to IBC ibcRouter.AddRoute(wasm.ModuleName, wasm.NewIBCHandler(appKeepers.WasmKeeper, appKeepers.IBCKeeper.ChannelKeeper)) @@ -437,6 +462,7 @@ func (appKeepers *AppKeepers) initParamsKeeper(appCodec codec.BinaryCodec, legac paramsKeeper.Subspace(wasm.ModuleName) paramsKeeper.Subspace(tokenfactorytypes.ModuleName) paramsKeeper.Subspace(twaptypes.ModuleName) + paramsKeeper.Subspace(ibcratelimittypes.ModuleName) return paramsKeeper } diff --git a/tests/e2e/configurer/chain/commands.go b/tests/e2e/configurer/chain/commands.go index ee070b4b754..335d4924eca 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/e2e_test.go b/tests/e2e/e2e_test.go index 5c9b97e0ae8..ec98a290877 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -1,12 +1,17 @@ package e2e import ( + "encoding/json" "fmt" + "io" "os" "path/filepath" "strconv" "time" + paramsutils "github.com/cosmos/cosmos-sdk/x/params/client/utils" + ibcratelimittypes "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit/types" + sdk "github.com/cosmos/cosmos-sdk/types" coretypes "github.com/tendermint/tendermint/rpc/core/types" @@ -107,6 +112,124 @@ func (s *IntegrationTestSuite) TestSuperfluidVoting() { ) } +// Copy a file from A to B with io.Copy +func copyFile(a, b string) error { + source, err := os.Open(a) + if err != nil { + return err + } + defer source.Close() + destination, err := os.Create(b) + if err != nil { + return err + } + defer destination.Close() + _, err = io.Copy(destination, source) + if err != nil { + return err + } + return nil +} + +func (s *IntegrationTestSuite) TestIBCTokenTransferRateLimiting() { + if s.skipIBC { + s.T().Skip("Skipping IBC tests") + } + chainA := s.configurer.GetChainConfig(0) + chainB := s.configurer.GetChainConfig(1) + + node, err := chainA.GetDefaultNode() + s.NoError(err) + + supply, err := node.QueryTotalSupply() + s.NoError(err) + osmoSupply := supply.AmountOf("uosmo") + + // balance, err := node.QueryBalances(chainA.NodeConfigs[1].PublicAddress) + // s.NoError(err) + + f, err := osmoSupply.ToDec().Float64() + s.NoError(err) + + over := f * 0.02 + + // Sending >1% + chainA.SendIBC(chainB, chainB.NodeConfigs[0].PublicAddress, sdk.NewInt64Coin(initialization.OsmoDenom, int64(over))) + + // copy the contract from x/rate-limit/testdata/ + wd, err := os.Getwd() + s.NoError(err) + // co up two levels + projectDir := filepath.Dir(filepath.Dir(wd)) + fmt.Println(wd, projectDir) + err = copyFile(projectDir+"/x/ibc-rate-limit/testdata/rate_limiter.wasm", wd+"/scripts/rate_limiter.wasm") + s.NoError(err) + node.StoreWasmCode("rate_limiter.wasm", initialization.ValidatorWalletName) + chainA.LatestCodeId += 1 + node.InstantiateWasmContract( + strconv.Itoa(chainA.LatestCodeId), + fmt.Sprintf(`{"gov_module": "%s", "ibc_module": "%s", "paths": [{"channel_id": "channel-0", "denom": "%s", "quotas": [{"name":"testQuota", "duration": 86400, "send_recv": [1, 1]}] } ] }`, node.PublicAddress, node.PublicAddress, initialization.OsmoToken.Denom), + initialization.ValidatorWalletName) + + // Using code_id 1 because this is the only contract right now. This may need to change if more contracts are added + contracts, err := node.QueryContractsFromId(chainA.LatestCodeId) + s.NoError(err) + s.Require().Len(contracts, 1, "Wrong number of contracts for the rate limiter") + + proposal := paramsutils.ParamChangeProposalJSON{ + Title: "Param Change", + Description: "Changing the rate limit contract param", + Changes: paramsutils.ParamChangesJSON{ + paramsutils.ParamChangeJSON{ + Subspace: ibcratelimittypes.ModuleName, + Key: "contract", + Value: []byte(fmt.Sprintf(`"%s"`, contracts[0])), + }, + }, + Deposit: "625000000uosmo", + } + proposalJson, err := json.Marshal(proposal) + s.NoError(err) + + node.SubmitParamChangeProposal(string(proposalJson), initialization.ValidatorWalletName) + chainA.LatestProposalNumber += 1 + + for _, n := range chainA.NodeConfigs { + n.VoteYesProposal(initialization.ValidatorWalletName, chainA.LatestProposalNumber) + } + + // The value is returned as a string, so we have to unmarshal twice + type Params struct { + Key string `json:"key"` + Subspace string `json:"subspace"` + Value string `json:"value"` + } + + s.Eventually( + func() bool { + var params Params + node.QueryParams(ibcratelimittypes.ModuleName, "contract", ¶ms) + var val string + err := json.Unmarshal([]byte(params.Value), &val) + if err != nil { + return false + } + return val != "" + }, + 1*time.Minute, + 10*time.Millisecond, + "Osmosis node failed to retrieve params", + ) + + // Sending <1%. Should work + chainA.SendIBC(chainB, chainB.NodeConfigs[0].PublicAddress, sdk.NewInt64Coin(initialization.OsmoDenom, 1)) + // Sending >1%. Should fail + node.FailIBCTransfer(initialization.ValidatorWalletName, chainB.NodeConfigs[0].PublicAddress, fmt.Sprintf("%duosmo", int(over))) + + // Removing the rate limit so it doesn't affect other tests + node.WasmExecute(contracts[0], `{"remove_path": {"channel_id": "channel-0", "denom": "uosmo"}}`, initialization.ValidatorWalletName) +} + // TestAddToExistingLockPostUpgrade ensures addToExistingLock works for locks created preupgrade. func (s *IntegrationTestSuite) TestAddToExistingLockPostUpgrade() { if s.skipUpgrade { 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 16bc08802b0..fa5b99e49da 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 @@ -52,7 +52,7 @@ fn consume_allowance() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); @@ -64,7 +64,7 @@ fn consume_allowance() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; let err = sudo(deps.as_mut(), mock_env(), msg).unwrap_err(); @@ -91,7 +91,7 @@ fn symetric_flows_dont_consume_allowance() { let send_msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; let recv_msg = SudoMsg::RecvPacket { @@ -154,7 +154,7 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_060_u32.into(), funds: 60_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); @@ -166,7 +166,7 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_060_u32.into(), funds: 60_u32.into(), }; @@ -195,7 +195,7 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_060_u32.into(), funds: 60_u32.into(), }; let err = sudo(deps.as_mut(), mock_env(), msg.clone()).unwrap_err(); @@ -205,7 +205,7 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_060_u32.into(), funds: 30_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), msg.clone()).unwrap(); @@ -256,7 +256,7 @@ fn query_state() { let send_msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; sudo(deps.as_mut(), mock_env(), send_msg.clone()).unwrap(); @@ -343,7 +343,7 @@ fn undo_send() { let send_msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; let undo_msg = SudoMsg::UndoSend { diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/error.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/error.rs index dc40f708d1c..367180baf59 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/error.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{StdError, Timestamp}; +use cosmwasm_std::{StdError, Timestamp, Uint256}; use thiserror::Error; #[derive(Error, Debug)] @@ -9,10 +9,14 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, - #[error("IBC Rate Limit exceded for channel {channel:?} and denom {denom:?}. Try again after {reset:?}")] + #[error("IBC Rate Limit exceeded for {channel}/{denom}. Tried to transfer {amount} which exceeds capacity on the '{quota_name}' quota ({used}/{max}). Try again after {reset:?}")] RateLimitExceded { channel: String, denom: String, + amount: Uint256, + quota_name: String, + used: Uint256, + max: Uint256, reset: Timestamp, }, 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 66a145b397d..d5d76acb0e8 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 @@ -82,7 +82,7 @@ fn expiration() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -105,7 +105,7 @@ fn expiration() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -123,7 +123,7 @@ fn expiration() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000_u32.into(), + channel_value: 3_300_u32.into(), funds: 300_u32.into(), }; @@ -162,7 +162,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -172,7 +172,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -188,7 +188,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; @@ -207,7 +207,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -224,7 +224,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -240,7 +240,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -257,7 +257,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -272,7 +272,7 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100_u32.into(), + channel_value: 101_u32.into(), funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs new file mode 100644 index 00000000000..6bc5b8cfed1 --- /dev/null +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/packet.rs @@ -0,0 +1,64 @@ +use cosmwasm_std::{Addr, Deps, Timestamp}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Height { + /// Previously known as "epoch" + revision_number: Option, + + /// The height of a block + revision_height: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct FungibleTokenData { + denom: String, + amount: u128, + sender: Addr, + receiver: Addr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Packet { + pub sequence: u64, + pub source_port: String, + pub source_channel: String, + pub destination_port: String, + pub destination_channel: String, + pub data: FungibleTokenData, + pub timeout_height: Height, + pub timeout_timestamp: Option, +} + +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 get_funds(&self) -> u128 { + return self.data.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(); + } + + 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(); + } + + 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 + }; + + return (channel, denom); + } +} diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/state.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/state.rs index 5237946487d..e28fc1004b7 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/state.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/state.rs @@ -102,6 +102,15 @@ impl Flow { } } + /// returns the balance in a direction. This is used for displaying cleaner errors + pub fn balance_on(&self, direction: &FlowType) -> Uint256 { + let (balance_in, balance_out) = self.balance(); + match direction { + FlowType::In => balance_in, + FlowType::Out => balance_out, + } + } + /// If now is greater than the period_end, the Flow is considered expired. pub fn is_expired(&self, now: Timestamp) -> bool { self.period_end < now @@ -182,6 +191,15 @@ impl Quota { None => (0_u32.into(), 0_u32.into()), // This should never happen, but ig the channel value is not set, we disallow any transfer } } + + /// returns the capacity in a direction. This is used for displaying cleaner errors + pub fn capacity_on(&self, direction: &FlowType) -> Uint256 { + let (max_in, max_out) = self.capacity(); + match direction { + FlowType::In => max_in, + FlowType::Out => max_out, + } + } } impl From<&QuotaMsg> for Quota { @@ -209,6 +227,29 @@ pub struct RateLimit { pub flow: Flow, } +// The channel value on send depends on the amount on escrow. The ibc transfer +// module modifies the escrow amount by "funds" on sends before calling the +// contract. This function takes that into account so that the channel value +// that we track matches the channel value at the moment when the ibc +// transaction started executing +fn calculate_channel_value( + channel_value: Uint256, + denom: &str, + funds: Uint256, + direction: &FlowType, +) -> Uint256 { + match direction { + FlowType::Out => { + if denom.contains("ibc") { + channel_value + funds // Non-Native tokens get removed from the supply on send. Add that amount back + } else { + channel_value - funds // Native tokens increase escrow amount on send. Remove that amount here + } + } + FlowType::In => channel_value, + } +} + impl RateLimit { /// Checks if a transfer is allowed and updates the data structures /// accordingly. @@ -224,10 +265,22 @@ impl RateLimit { channel_value: Uint256, now: Timestamp, ) -> Result { + // Flow used before this transaction is applied. + // This is used to make error messages more informative + let initial_flow = self.flow.balance_on(direction); + + // Apply the transfer. From here on, we will updated the flow with the new transfer + // and check if it exceeds the quota at the current time + let expired = self.flow.apply_transfer(direction, funds, now, &self.quota); // Cache the channel value if it has never been set or it has expired. if self.quota.channel_value.is_none() || expired { - self.quota.channel_value = Some(channel_value) + self.quota.channel_value = Some(calculate_channel_value( + channel_value, + &path.denom, + funds, + direction, + )) } let (max_in, max_out) = self.quota.capacity(); @@ -236,6 +289,10 @@ impl RateLimit { true => Err(ContractError::RateLimitExceded { channel: path.channel.to_string(), denom: path.denom.to_string(), + amount: funds, + quota_name: self.quota.name.to_string(), + used: initial_flow, + max: self.quota.capacity_on(direction), reset: self.flow.period_end, }), false => Ok(RateLimit { diff --git a/x/ibc-rate-limit/ibc_middleware_test.go b/x/ibc-rate-limit/ibc_middleware_test.go new file mode 100644 index 00000000000..497916d8b1a --- /dev/null +++ b/x/ibc-rate-limit/ibc_middleware_test.go @@ -0,0 +1,462 @@ +package ibc_rate_limit_test + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "testing" + "time" + + ibc_rate_limit "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + ibctesting "github.com/cosmos/ibc-go/v3/testing" + "github.com/osmosis-labs/osmosis/v12/app" + "github.com/osmosis-labs/osmosis/v12/app/apptesting" + "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit/testutil" + "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit/types" + "github.com/stretchr/testify/suite" +) + +type MiddlewareTestSuite struct { + apptesting.KeeperTestHelper + + coordinator *ibctesting.Coordinator + + // testing chains used for convenience and readability + chainA *osmosisibctesting.TestChain + chainB *osmosisibctesting.TestChain + path *ibctesting.Path +} + +// Setup +func TestMiddlewareTestSuite(t *testing.T) { + suite.Run(t, new(MiddlewareTestSuite)) +} + +func SetupTestingApp() (ibctesting.TestingApp, map[string]json.RawMessage) { + osmosisApp := app.Setup(false) + return osmosisApp, app.NewDefaultGenesisState() +} + +func NewTransferPath(chainA, chainB *osmosisibctesting.TestChain) *ibctesting.Path { + path := ibctesting.NewPath(chainA.TestChain, chainB.TestChain) + path.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort + path.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort + path.EndpointA.ChannelConfig.Version = transfertypes.Version + path.EndpointB.ChannelConfig.Version = transfertypes.Version + return path +} + +func (suite *MiddlewareTestSuite) SetupTest() { + suite.Setup() + ibctesting.DefaultTestingAppInit = SetupTestingApp + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 2) + suite.chainA = &osmosisibctesting.TestChain{ + TestChain: suite.coordinator.GetChain(ibctesting.GetChainID(1)), + } + // Remove epochs to prevent minting + suite.chainA.MoveEpochsToTheFuture() + suite.chainB = &osmosisibctesting.TestChain{ + TestChain: suite.coordinator.GetChain(ibctesting.GetChainID(2)), + } + suite.path = NewTransferPath(suite.chainA, suite.chainB) + suite.coordinator.Setup(suite.path) +} + +// Helpers +func (suite *MiddlewareTestSuite) MessageFromAToB(denom string, amount sdk.Int) sdk.Msg { + coin := sdk.NewCoin(denom, amount) + port := suite.path.EndpointA.ChannelConfig.PortID + channel := suite.path.EndpointA.ChannelID + accountFrom := suite.chainA.SenderAccount.GetAddress().String() + accountTo := suite.chainB.SenderAccount.GetAddress().String() + timeoutHeight := clienttypes.NewHeight(0, 100) + return transfertypes.NewMsgTransfer( + port, + channel, + coin, + accountFrom, + accountTo, + timeoutHeight, + 0, + ) +} + +func (suite *MiddlewareTestSuite) MessageFromBToA(denom string, amount sdk.Int) sdk.Msg { + coin := sdk.NewCoin(denom, amount) + port := suite.path.EndpointB.ChannelConfig.PortID + channel := suite.path.EndpointB.ChannelID + accountFrom := suite.chainB.SenderAccount.GetAddress().String() + accountTo := suite.chainA.SenderAccount.GetAddress().String() + timeoutHeight := clienttypes.NewHeight(0, 100) + return transfertypes.NewMsgTransfer( + port, + channel, + coin, + accountFrom, + accountTo, + timeoutHeight, + 0, + ) +} + +// Tests that a receiver address longer than 4096 is not accepted +func (suite *MiddlewareTestSuite) TestInvalidReceiver() { + msg := transfertypes.NewMsgTransfer( + suite.path.EndpointB.ChannelConfig.PortID, + suite.path.EndpointB.ChannelID, + sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1)), + suite.chainB.SenderAccount.GetAddress().String(), + strings.Repeat("x", 4097), + clienttypes.NewHeight(0, 100), + 0, + ) + _, ack, _ := suite.FullSendBToA(msg) + suite.Require().Contains(string(ack), "error", + "acknowledgment is not an error") + suite.Require().Contains(string(ack), sdkerrors.ErrInvalidAddress.Error(), + "acknowledgment error is not of the right type") +} + +func (suite *MiddlewareTestSuite) FullSendBToA(msg sdk.Msg) (*sdk.Result, string, error) { + sendResult, err := suite.chainB.SendMsgsNoCheck(msg) + suite.Require().NoError(err) + + packet, err := ibctesting.ParsePacketFromEvents(sendResult.GetEvents()) + suite.Require().NoError(err) + + err = suite.path.EndpointA.UpdateClient() + suite.Require().NoError(err) + + res, err := suite.path.EndpointA.RecvPacketWithResult(packet) + suite.Require().NoError(err) + + ack, err := ibctesting.ParseAckFromEvents(res.GetEvents()) + + err = suite.path.EndpointA.UpdateClient() + suite.Require().NoError(err) + err = suite.path.EndpointB.UpdateClient() + suite.Require().NoError(err) + + return sendResult, string(ack), err +} + +func (suite *MiddlewareTestSuite) FullSendAToB(msg sdk.Msg) (*sdk.Result, string, error) { + sendResult, err := suite.chainA.SendMsgsNoCheck(msg) + if err != nil { + return nil, "", err + } + + packet, err := ibctesting.ParsePacketFromEvents(sendResult.GetEvents()) + if err != nil { + return nil, "", err + } + + err = suite.path.EndpointB.UpdateClient() + if err != nil { + return nil, "", err + } + + res, err := suite.path.EndpointB.RecvPacketWithResult(packet) + if err != nil { + return nil, "", err + } + + ack, err := ibctesting.ParseAckFromEvents(res.GetEvents()) + if err != nil { + return nil, "", err + } + + err = suite.path.EndpointA.UpdateClient() + if err != nil { + return nil, "", err + } + err = suite.path.EndpointB.UpdateClient() + if err != nil { + return nil, "", err + } + + return sendResult, string(ack), nil +} + +func (suite *MiddlewareTestSuite) AssertReceive(success bool, msg sdk.Msg) (string, error) { + _, ack, err := suite.FullSendBToA(msg) + if success { + suite.Require().NoError(err) + suite.Require().NotContains(string(ack), "error", + "acknowledgment is an error") + } else { + suite.Require().Contains(string(ack), "error", + "acknowledgment is not an error") + suite.Require().Contains(string(ack), types.ErrRateLimitExceeded.Error(), + "acknowledgment error is not of the right type") + } + return ack, err +} + +func (suite *MiddlewareTestSuite) AssertSend(success bool, msg sdk.Msg) (*sdk.Result, error) { + r, _, err := suite.FullSendAToB(msg) + if success { + suite.Require().NoError(err, "IBC send failed. Expected success. %s", err) + } else { + suite.Require().Error(err, "IBC send succeeded. Expected failure") + suite.ErrorContains(err, types.ErrRateLimitExceeded.Error(), "Bad error type") + } + return r, err +} + +func (suite *MiddlewareTestSuite) BuildChannelQuota(name, denom string, duration, send_precentage, recv_percentage uint32) string { + return fmt.Sprintf(` + {"channel_id": "channel-0", "denom": "%s", "quotas": [{"name":"%s", "duration": %d, "send_recv":[%d, %d]}] } + `, denom, name, duration, send_precentage, recv_percentage) +} + +// Tests + +// Test that Sending IBC messages works when the middleware isn't configured +func (suite *MiddlewareTestSuite) TestSendTransferNoContract() { + one := sdk.NewInt(1) + suite.AssertSend(true, suite.MessageFromAToB(sdk.DefaultBondDenom, one)) +} + +// Test that Receiving IBC messages works when the middleware isn't configured +func (suite *MiddlewareTestSuite) TestReceiveTransferNoContract() { + one := sdk.NewInt(1) + suite.AssertReceive(true, suite.MessageFromBToA(sdk.DefaultBondDenom, one)) +} + +func (suite *MiddlewareTestSuite) initializeEscrow() (totalEscrow, expectedSed sdk.Int) { + osmosisApp := suite.chainA.GetOsmosisApp() + supply := osmosisApp.BankKeeper.GetSupplyWithOffset(suite.chainA.GetContext(), sdk.DefaultBondDenom) + + // Move some funds from chainA to chainB so that there is something in escrow + // Each user has 10% of the supply, so we send most of the funds from one user to chainA + transferAmount := supply.Amount.QuoRaw(20) + + // When sending, the amount we're sending goes into escrow before we enter the middleware and thus + // it's used as part of the channel value in the rate limiting contract + // To account for that, we subtract the amount we'll send first (2.5% of transferAmount) here + sendAmount := transferAmount.QuoRaw(40) + + // Send from A to B + _, _, err := suite.FullSendAToB(suite.MessageFromAToB(sdk.DefaultBondDenom, transferAmount.Sub(sendAmount))) + suite.Require().NoError(err) + // Send from A to B + _, _, err = suite.FullSendBToA(suite.MessageFromBToA(sdk.DefaultBondDenom, transferAmount.Sub(sendAmount))) + suite.Require().NoError(err) + + return transferAmount, sendAmount +} + +func (suite *MiddlewareTestSuite) fullSendTest(native bool) map[string]string { + quotaPercentage := 5 + suite.initializeEscrow() + // Get the denom and amount to send + denom := sdk.DefaultBondDenom + if !native { + denomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", "channel-0", denom)) + denom = denomTrace.IBCDenom() + } + + osmosisApp := suite.chainA.GetOsmosisApp() + + // This is the first one. Inside the tests. It works as expected. + channelValue := ibc_rate_limit.CalculateChannelValue(suite.chainA.GetContext(), denom, "transfer", "channel-0", osmosisApp.BankKeeper) + + // The amount to be sent is send 2.5% (quota is 5%) + quota := channelValue.QuoRaw(int64(100 / quotaPercentage)) + sendAmount := quota.QuoRaw(2) + + fmt.Printf("Testing send rate limiting for denom=%s, channelValue=%s, quota=%s, sendAmount=%s\n", denom, channelValue, quota, sendAmount) + + // Setup contract + suite.chainA.StoreContractCode(&suite.Suite) + quotas := suite.BuildChannelQuota("weekly", denom, 604800, 5, 5) + fmt.Println(quotas) + addr := suite.chainA.InstantiateContract(&suite.Suite, quotas) + suite.chainA.RegisterRateLimitingContract(addr) + + // TODO: Remove native from MessafeFrom calls + // send 2.5% (quota is 5%) + fmt.Println("trying to send ", sendAmount) + suite.AssertSend(true, suite.MessageFromAToB(denom, sendAmount)) + + // send 2.5% (quota is 5%) + fmt.Println("trying to send ", sendAmount) + r, _ := suite.AssertSend(true, suite.MessageFromAToB(denom, sendAmount)) + + // Calculate remaining allowance in the quota + attrs := suite.ExtractAttributes(suite.FindEvent(r.GetEvents(), "wasm")) + + used, ok := sdk.NewIntFromString(attrs["weekly_used_out"]) + suite.Require().True(ok) + + suite.Require().Equal(used, sendAmount.MulRaw(2)) + + // Sending above the quota should fail. We use 2 instead of 1 here to avoid rounding issues + suite.AssertSend(false, suite.MessageFromAToB(denom, sdk.NewInt(2))) + return attrs +} + +// Test rate limiting on sends +func (suite *MiddlewareTestSuite) TestSendTransferWithRateLimitingNative() { + suite.fullSendTest(true) +} + +// Test rate limiting on sends +func (suite *MiddlewareTestSuite) TestSendTransferWithRateLimitingNonNative() { + suite.fullSendTest(false) +} + +// Test rate limits are reset when the specified time period has passed +func (suite *MiddlewareTestSuite) TestSendTransferReset() { + // Same test as above, but the quotas get reset after time passes + attrs := suite.fullSendTest(true) + parts := strings.Split(attrs["weekly_period_end"], ".") // Splitting timestamp into secs and nanos + secs, err := strconv.ParseInt(parts[0], 10, 64) + suite.Require().NoError(err) + nanos, err := strconv.ParseInt(parts[1], 10, 64) + suite.Require().NoError(err) + resetTime := time.Unix(secs, nanos) + + // Move chainA forward one block + suite.chainA.NextBlock() + suite.chainA.SenderAccount.SetSequence(suite.chainA.SenderAccount.GetSequence() + 1) + + // Reset time + one second + oneSecAfterReset := resetTime.Add(time.Second) + suite.coordinator.IncrementTimeBy(oneSecAfterReset.Sub(suite.coordinator.CurrentTime)) + + // Sending should succeed again + suite.AssertSend(true, suite.MessageFromAToB(sdk.DefaultBondDenom, sdk.NewInt(1))) +} + +// Test rate limiting on receives +func (suite *MiddlewareTestSuite) fullRecvTest(native bool) { + quotaPercentage := 5 + suite.initializeEscrow() + // Get the denom and amount to send + denom := sdk.DefaultBondDenom + if !native { + denomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", "channel-0", denom)) + denom = denomTrace.IBCDenom() + } + + osmosisApp := suite.chainA.GetOsmosisApp() + + // This is the first one. Inside the tests. It works as expected. + channelValue := ibc_rate_limit.CalculateChannelValue(suite.chainA.GetContext(), denom, "transfer", "channel-0", osmosisApp.BankKeeper) + + // The amount to be sent is send 2.5% (quota is 5%) + quota := channelValue.QuoRaw(int64(100 / quotaPercentage)) + sendAmount := quota.QuoRaw(2) + + fmt.Printf("Testing recv rate limiting for denom=%s, channelValue=%s, quota=%s, sendAmount=%s\n", denom, channelValue, quota, sendAmount) + + // Setup contract + suite.chainA.StoreContractCode(&suite.Suite) + quotas := suite.BuildChannelQuota("weekly", denom, 604800, 5, 5) + addr := suite.chainA.InstantiateContract(&suite.Suite, quotas) + suite.chainA.RegisterRateLimitingContract(addr) + + // receive 2.5% (quota is 5%) + suite.AssertReceive(true, suite.MessageFromBToA(denom, sendAmount)) + + // receive 2.5% (quota is 5%) + suite.AssertReceive(true, suite.MessageFromBToA(denom, sendAmount)) + + // Sending above the quota should fail. We send 2 instead of 1 to account for rounding errors + suite.AssertReceive(false, suite.MessageFromBToA(denom, sdk.NewInt(2))) +} + +func (suite *MiddlewareTestSuite) TestRecvTransferWithRateLimitingNative() { + suite.fullRecvTest(true) +} + +func (suite *MiddlewareTestSuite) TestRecvTransferWithRateLimitingNonNative() { + suite.fullRecvTest(false) +} + +// Test no rate limiting occurs when the contract is set, but not quotas are condifured for the path +func (suite *MiddlewareTestSuite) TestSendTransferNoQuota() { + // Setup contract + suite.chainA.StoreContractCode(&suite.Suite) + addr := suite.chainA.InstantiateContract(&suite.Suite, ``) + suite.chainA.RegisterRateLimitingContract(addr) + + // send 1 token. + // If the contract doesn't have a quota for the current channel, all transfers are allowed + suite.AssertSend(true, suite.MessageFromAToB(sdk.DefaultBondDenom, sdk.NewInt(1))) +} + +// Test rate limits are reverted if a "send" fails +func (suite *MiddlewareTestSuite) TestFailedSendTransfer() { + suite.initializeEscrow() + // Setup contract + suite.chainA.StoreContractCode(&suite.Suite) + quotas := suite.BuildChannelQuota("weekly", sdk.DefaultBondDenom, 604800, 1, 1) + addr := suite.chainA.InstantiateContract(&suite.Suite, quotas) + suite.chainA.RegisterRateLimitingContract(addr) + + // Get the escrowed amount + osmosisApp := suite.chainA.GetOsmosisApp() + escrowAddress := transfertypes.GetEscrowAddress("transfer", "channel-0") + escrowed := osmosisApp.BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, sdk.DefaultBondDenom) + + quota := escrowed.Amount.QuoRaw(100) // 1% of the escrowed amount + + // Use the whole quota + coins := sdk.NewCoin(sdk.DefaultBondDenom, quota) + port := suite.path.EndpointA.ChannelConfig.PortID + channel := suite.path.EndpointA.ChannelID + accountFrom := suite.chainA.SenderAccount.GetAddress().String() + timeoutHeight := clienttypes.NewHeight(0, 100) + msg := transfertypes.NewMsgTransfer(port, channel, coins, accountFrom, "INVALID", timeoutHeight, 0) + + // Sending the message manually because AssertSend updates both clients. We need to update the clients manually + // for this test so that the failure to receive on chain B happens after the second packet is sent from chain A. + // That way we validate that chain A is blocking as expected, but the flow is reverted after the receive failure is + // acknowledged on chain A + res, err := suite.chainA.SendMsgsNoCheck(msg) + suite.Require().NoError(err) + + // Sending again fails as the quota is filled + suite.AssertSend(false, suite.MessageFromAToB(sdk.DefaultBondDenom, quota)) + + // Move forward one block + suite.chainA.NextBlock() + suite.chainA.SenderAccount.SetSequence(suite.chainA.SenderAccount.GetSequence() + 1) + suite.chainA.Coordinator.IncrementTime() + + // Update both clients + err = suite.path.EndpointA.UpdateClient() + suite.Require().NoError(err) + err = suite.path.EndpointB.UpdateClient() + suite.Require().NoError(err) + + // Execute the acknowledgement from chain B in chain A + + // extract the sent packet + packet, err := ibctesting.ParsePacketFromEvents(res.GetEvents()) + suite.Require().NoError(err) + + // recv in chain b + res, err = suite.path.EndpointB.RecvPacketWithResult(packet) + + // get the ack from the chain b's response + ack, err := ibctesting.ParseAckFromEvents(res.GetEvents()) + suite.Require().NoError(err) + + // manually relay it to chain a + err = suite.path.EndpointA.AcknowledgePacket(packet, ack) + suite.Require().NoError(err) + + // We should be able to send again because the packet that exceeded the quota failed and has been reverted + suite.AssertSend(true, suite.MessageFromAToB(sdk.DefaultBondDenom, sdk.NewInt(1))) +} diff --git a/x/ibc-rate-limit/ibc_module.go b/x/ibc-rate-limit/ibc_module.go index c1df7c9219f..433826dddac 100644 --- a/x/ibc-rate-limit/ibc_module.go +++ b/x/ibc-rate-limit/ibc_module.go @@ -103,12 +103,27 @@ func (im *IBCModule) OnChanCloseConfirm( return im.app.OnChanCloseConfirm(ctx, portID, channelID) } +func ValidateReceiverAddress(packet channeltypes.Packet) error { + var packetData transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(packet.GetData(), &packetData); err != nil { + return err + } + if len(packetData.Receiver) >= 4096 { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "IBC Receiver address too long. Max supported length is %d", 4096) + } + return nil +} + // OnRecvPacket implements the IBCModule interface func (im *IBCModule) OnRecvPacket( ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, ) exported.Acknowledgement { + if err := ValidateReceiverAddress(packet); err != nil { + return channeltypes.NewErrorAcknowledgement(err.Error()) + } + contract := im.ics4Middleware.GetParams(ctx) if contract == "" { // The contract has not been configured. Continue as usual @@ -116,9 +131,10 @@ func (im *IBCModule) OnRecvPacket( } amount, denom, err := GetFundsFromPacket(packet) if err != nil { - return channeltypes.NewErrorAcknowledgement("bad packet") + return channeltypes.NewErrorAcknowledgement("bad packet in rate limit's OnRecvPacket") } - channelValue := im.ics4Middleware.CalculateChannelValue(ctx, denom) + + channelValue := im.ics4Middleware.CalculateChannelValue(ctx, denom, packet) err = CheckAndUpdateRateLimits( ctx, @@ -127,11 +143,11 @@ func (im *IBCModule) OnRecvPacket( contract, channelValue, packet.GetDestChannel(), - denom, + denom, // We always use the packet's denom here, as we want the limits to be the same on both directions amount, ) if err != nil { - return channeltypes.NewErrorAcknowledgement(types.RateLimitExceededMsg) + return channeltypes.NewErrorAcknowledgement(types.ErrRateLimitExceeded.Error()) } // if this returns an Acknowledgement that isn't successful, all state changes are discarded diff --git a/x/ibc-rate-limit/ics4_wrapper.go b/x/ibc-rate-limit/ics4_wrapper.go index bdf7e935aaf..453de40a4fc 100644 --- a/x/ibc-rate-limit/ics4_wrapper.go +++ b/x/ibc-rate-limit/ics4_wrapper.go @@ -53,9 +53,11 @@ func (i *ICS4Wrapper) SendPacket(ctx sdk.Context, chanCap *capabilitytypes.Capab amount, denom, err := GetFundsFromPacket(packet) if err != nil { - return sdkerrors.Wrap(err, "Rate limited SendPacket") + return sdkerrors.Wrap(err, "Rate limit SendPacket") } - channelValue := i.CalculateChannelValue(ctx, denom) + + channelValue := i.CalculateChannelValue(ctx, denom, packet) + err = CheckAndUpdateRateLimits( ctx, i.ContractKeeper, @@ -63,11 +65,11 @@ func (i *ICS4Wrapper) SendPacket(ctx sdk.Context, chanCap *capabilitytypes.Capab contract, channelValue, packet.GetSourceChannel(), - denom, + denom, // We always use the packet's denom here, as we want the limits to be the same on both directions amount, ) if err != nil { - return sdkerrors.Wrap(err, "Rate limited SendPacket") + return sdkerrors.Wrap(err, "bad packet in rate limit's SendPacket") } return i.channel.SendPacket(ctx, chanCap, packet) @@ -84,6 +86,7 @@ func (i *ICS4Wrapper) GetParams(ctx sdk.Context) (contract string) { // CalculateChannelValue The value of an IBC channel. This is calculated using the denom supplied by the sender. // if the denom is not correct, the transfer should fail somewhere else on the call chain -func (i *ICS4Wrapper) CalculateChannelValue(ctx sdk.Context, denom string) sdk.Int { - return i.bankKeeper.GetSupplyWithOffset(ctx, denom).Amount +func (i *ICS4Wrapper) CalculateChannelValue(ctx sdk.Context, denom string, packet exported.PacketI) sdk.Int { + // The logic is etracted into a function here so that it can be used within the tests + return CalculateChannelValue(ctx, denom, packet.GetSourcePort(), packet.GetSourceChannel(), i.bankKeeper) } diff --git a/x/ibc-rate-limit/rate_limit.go b/x/ibc-rate-limit/rate_limit.go index 665f04b2990..5c91e5ffeed 100644 --- a/x/ibc-rate-limit/rate_limit.go +++ b/x/ibc-rate-limit/rate_limit.go @@ -2,10 +2,13 @@ package ibc_rate_limit import ( "encoding/json" + "strings" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" "github.com/cosmos/ibc-go/v3/modules/core/exported" "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit/types" ) @@ -15,11 +18,6 @@ var ( msgRecv = "recv_packet" ) -type PacketData struct { - Denom string `json:"denom"` - Amount string `json:"amount"` -} - func CheckAndUpdateRateLimits(ctx sdk.Context, contractKeeper *wasmkeeper.PermissionedKeeper, msgType, contract string, channelValue sdk.Int, sourceChannel, denom string, @@ -42,6 +40,7 @@ func CheckAndUpdateRateLimits(ctx sdk.Context, contractKeeper *wasmkeeper.Permis } _, err = contractKeeper.Sudo(ctx, contractAddr, sendPacketMsg) + if err != nil { return sdkerrors.Wrap(types.ErrRateLimitExceeded, err.Error()) } @@ -128,10 +127,41 @@ func BuildWasmExecMsg(msgType, sourceChannel, denom string, channelValue sdk.Int } func GetFundsFromPacket(packet exported.PacketI) (string, string, error) { - var packetData PacketData + var packetData transfertypes.FungibleTokenPacketData err := json.Unmarshal(packet.GetData(), &packetData) if err != nil { return "", "", err } - return packetData.Amount, packetData.Denom, nil + return packetData.Amount, GetLocalDenom(packetData.Denom), nil +} + +func GetLocalDenom(denom string) string { + // Expected denoms in the following cases: + // + // send non-native: transfer/channel-0/denom -> 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, port, channel string, bankKeeper bankkeeper.Keeper) sdk.Int { + if strings.HasPrefix(denom, "ibc/") { + return bankKeeper.GetSupplyWithOffset(ctx, denom).Amount + } + + if channel == "any" { + // ToDo: Get all channels and sum the escrow addr value over all the channels + escrowAddress := transfertypes.GetEscrowAddress(port, channel) + return bankKeeper.GetBalance(ctx, escrowAddress, denom).Amount + } else { + escrowAddress := transfertypes.GetEscrowAddress(port, channel) + return bankKeeper.GetBalance(ctx, escrowAddress, denom).Amount + } } diff --git a/x/ibc-rate-limit/testdata/rate_limiter.wasm b/x/ibc-rate-limit/testdata/rate_limiter.wasm index caf63c41459ca8361691cf27ae6c738682946e40..e19651209c4009fd4bd73bae23f49c69424e91d3 100644 GIT binary patch delta 74291 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 UUUjuw0~#;H?=JlM;|KcvFIw7UWB>pF diff --git a/x/ibc-rate-limit/testutil/chain.go b/x/ibc-rate-limit/testutil/chain.go new file mode 100644 index 00000000000..3ab9c26f0e2 --- /dev/null +++ b/x/ibc-rate-limit/testutil/chain.go @@ -0,0 +1,96 @@ +package osmosisibctesting + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + ibctesting "github.com/cosmos/ibc-go/v3/testing" + "github.com/cosmos/ibc-go/v3/testing/simapp/helpers" + "github.com/osmosis-labs/osmosis/v12/app" + abci "github.com/tendermint/tendermint/abci/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" +) + +type TestChain struct { + *ibctesting.TestChain +} + +// SendMsgsNoCheck overrides ibctesting.TestChain.SendMsgs so that it doesn't check for errors. That should be handled by the caller +func (chain *TestChain) SendMsgsNoCheck(msgs ...sdk.Msg) (*sdk.Result, error) { + // ensure the chain has the latest time + chain.Coordinator.UpdateTimeForChain(chain.TestChain) + + _, r, err := SignAndDeliver( + chain.TxConfig, + chain.App.GetBaseApp(), + chain.GetContext().BlockHeader(), + msgs, + chain.ChainID, + []uint64{chain.SenderAccount.GetAccountNumber()}, + []uint64{chain.SenderAccount.GetSequence()}, + chain.SenderPrivKey, + ) + if err != nil { + return nil, err + } + + // SignAndDeliver calls app.Commit() + chain.NextBlock() + + // increment sequence for successful transaction execution + err = chain.SenderAccount.SetSequence(chain.SenderAccount.GetSequence() + 1) + if err != nil { + return nil, err + } + + chain.Coordinator.IncrementTime() + + return r, nil +} + +// SignAndDeliver signs and delivers a transaction without asserting the results. This overrides the function +// from ibctesting +func SignAndDeliver( + txCfg client.TxConfig, app *baseapp.BaseApp, header tmproto.Header, msgs []sdk.Msg, + chainID string, accNums, accSeqs []uint64, priv ...cryptotypes.PrivKey, +) (sdk.GasInfo, *sdk.Result, error) { + tx, _ := helpers.GenTx( + txCfg, + msgs, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + helpers.DefaultGenTxGas, + chainID, + accNums, + accSeqs, + priv..., + ) + + // Simulate a sending a transaction and committing a block + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + gInfo, res, err := app.Deliver(txCfg.TxEncoder(), tx) + + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() + + return gInfo, res, err +} + +// Move epochs to the future to avoid issues with minting +func (chain *TestChain) MoveEpochsToTheFuture() { + epochsKeeper := chain.GetOsmosisApp().EpochsKeeper + ctx := chain.GetContext() + for _, epoch := range epochsKeeper.AllEpochInfos(ctx) { + epoch.StartTime = ctx.BlockTime().Add(time.Hour * 24 * 30) + epochsKeeper.DeleteEpochInfo(chain.GetContext(), epoch.Identifier) + _ = epochsKeeper.AddEpochInfo(ctx, epoch) + } +} + +// GetOsmosisApp returns the current chain's app as an OsmosisApp +func (chain *TestChain) GetOsmosisApp() *app.OsmosisApp { + v, _ := chain.App.(*app.OsmosisApp) + return v +} diff --git a/x/ibc-rate-limit/testutil/wasm.go b/x/ibc-rate-limit/testutil/wasm.go new file mode 100644 index 00000000000..2beabb9c02a --- /dev/null +++ b/x/ibc-rate-limit/testutil/wasm.go @@ -0,0 +1,70 @@ +package osmosisibctesting + +import ( + "fmt" + "io/ioutil" + + "github.com/stretchr/testify/require" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" + "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit/types" + "github.com/stretchr/testify/suite" +) + +func (chain *TestChain) StoreContractCode(suite *suite.Suite) { + osmosisApp := chain.GetOsmosisApp() + + govKeeper := osmosisApp.GovKeeper + wasmCode, err := ioutil.ReadFile("./testdata/rate_limiter.wasm") + suite.Require().NoError(err) + + addr := osmosisApp.AccountKeeper.GetModuleAddress(govtypes.ModuleName) + src := wasmtypes.StoreCodeProposalFixture(func(p *wasmtypes.StoreCodeProposal) { + p.RunAs = addr.String() + p.WASMByteCode = wasmCode + }) + + // when stored + storedProposal, err := govKeeper.SubmitProposal(chain.GetContext(), src, false) + suite.Require().NoError(err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(chain.GetContext(), storedProposal.GetContent()) + suite.Require().NoError(err) +} + +func (chain *TestChain) InstantiateContract(suite *suite.Suite, quotas string) sdk.AccAddress { + osmosisApp := chain.GetOsmosisApp() + transferModule := osmosisApp.AccountKeeper.GetModuleAddress(transfertypes.ModuleName) + govModule := osmosisApp.AccountKeeper.GetModuleAddress(govtypes.ModuleName) + + initMsgBz := []byte(fmt.Sprintf(`{ + "gov_module": "%s", + "ibc_module":"%s", + "paths": [%s] + }`, + govModule, transferModule, quotas)) + + contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(osmosisApp.WasmKeeper) + codeID := uint64(1) + creator := osmosisApp.AccountKeeper.GetModuleAddress(govtypes.ModuleName) + addr, _, err := contractKeeper.Instantiate(chain.GetContext(), codeID, creator, creator, initMsgBz, "rate limiting contract", nil) + suite.Require().NoError(err) + return addr +} + +func (chain *TestChain) RegisterRateLimitingContract(addr []byte) { + addrStr, err := sdk.Bech32ifyAddressBytes("osmo", addr) + require.NoError(chain.T, err) + params, err := types.NewParams(addrStr) + require.NoError(chain.T, err) + osmosisApp := chain.GetOsmosisApp() + paramSpace, ok := osmosisApp.AppKeepers.ParamsKeeper.GetSubspace(types.ModuleName) + require.True(chain.T, ok) + paramSpace.SetParamSet(chain.GetContext(), ¶ms) +} diff --git a/x/ibc-rate-limit/types/errors.go b/x/ibc-rate-limit/types/errors.go index 67d81abeb79..5394ce11e3d 100644 --- a/x/ibc-rate-limit/types/errors.go +++ b/x/ibc-rate-limit/types/errors.go @@ -5,8 +5,7 @@ import ( ) var ( - RateLimitExceededMsg = "rate limit exceeded" - ErrRateLimitExceeded = sdkerrors.Register(ModuleName, 2, RateLimitExceededMsg) + ErrRateLimitExceeded = sdkerrors.Register(ModuleName, 2, "rate limit exceeded") ErrBadMessage = sdkerrors.Register(ModuleName, 3, "bad message") ErrContractError = sdkerrors.Register(ModuleName, 4, "contract error") )