diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index f5f5301c31b..4c883d3c1ac 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: 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/keepers/keepers.go b/app/keepers/keepers.go index 6ae34cc899d..6251262a602 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)) @@ -436,6 +461,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/app/modules.go b/app/modules.go index 008fd1657d3..3421fec2ba9 100644 --- a/app/modules.go +++ b/app/modules.go @@ -2,9 +2,13 @@ package app import ( "github.com/CosmWasm/wasmd/x/wasm" + "github.com/cosmos/cosmos-sdk/client" + capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" ibc "github.com/cosmos/ibc-go/v3/modules/core" ibchost "github.com/cosmos/ibc-go/v3/modules/core/24-host" + ibckeeper "github.com/cosmos/ibc-go/v3/modules/core/keeper" ica "github.com/cosmos/ibc-go/v3/modules/apps/27-interchain-accounts" icatypes "github.com/cosmos/ibc-go/v3/modules/apps/27-interchain-accounts/types" @@ -233,3 +237,20 @@ func (app *OsmosisApp) GetAccountKeeper() simtypes.AccountKeeper { func (app *OsmosisApp) GetBankKeeper() simtypes.BankKeeper { return app.AppKeepers.BankKeeper } + +// Required for ibctesting +func (app *OsmosisApp) GetStakingKeeper() stakingkeeper.Keeper { + return *app.AppKeepers.StakingKeeper // Dereferencing the pointer +} + +func (app *OsmosisApp) GetIBCKeeper() *ibckeeper.Keeper { + return app.AppKeepers.IBCKeeper // This is a *ibckeeper.Keeper +} + +func (app *OsmosisApp) GetScopedIBCKeeper() capabilitykeeper.ScopedKeeper { + return app.AppKeepers.ScopedIBCKeeper +} + +func (app *OsmosisApp) GetTxConfig() client.TxConfig { + return MakeEncodingConfig().TxConfig +} diff --git a/tests/e2e/configurer/chain/chain.go b/tests/e2e/configurer/chain/chain.go index fb39b752d60..21e2f69b9b4 100644 --- a/tests/e2e/configurer/chain/chain.go +++ b/tests/e2e/configurer/chain/chain.go @@ -29,6 +29,8 @@ type Config struct { LatestLockNumber int NodeConfigs []*NodeConfig + LatestCodeId int + t *testing.T containerManager *containers.Manager } @@ -150,7 +152,7 @@ func (c *Config) SendIBC(dstChain *Config, recipient string, token sdk.Coin) { if ibcCoin.Len() == 1 { tokenPre := balancesDstPre.AmountOfNoDenomValidation(ibcCoin[0].Denom) tokenPost := balancesDstPost.AmountOfNoDenomValidation(ibcCoin[0].Denom) - resPre := initialization.OsmoToken.Amount + resPre := token.Amount resPost := tokenPost.Sub(tokenPre) return resPost.Uint64() == resPre.Uint64() } else { diff --git a/tests/e2e/configurer/chain/commands.go b/tests/e2e/configurer/chain/commands.go index 3cd74af4790..ee070b4b754 100644 --- a/tests/e2e/configurer/chain/commands.go +++ b/tests/e2e/configurer/chain/commands.go @@ -1,7 +1,9 @@ package chain import ( + "encoding/json" "fmt" + "os" "regexp" "strconv" "strings" @@ -36,6 +38,79 @@ func (n *NodeConfig) CreatePool(poolFile, from string) uint64 { return poolID } +func (n *NodeConfig) StoreWasmCode(wasmFile, from string) { + n.LogActionF("storing wasm code from file %s", wasmFile) + cmd := []string{"osmosisd", "tx", "wasm", "store", wasmFile, fmt.Sprintf("--from=%s", from), "--gas=auto", "--gas-prices=0.1uosmo", "--gas-adjustment=1.3"} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully stored") +} + +func (n *NodeConfig) InstantiateWasmContract(codeId, initMsg, from string) { + n.LogActionF("instantiating wasm contract %s with %s", codeId, initMsg) + cmd := []string{"osmosisd", "tx", "wasm", "instantiate", codeId, initMsg, fmt.Sprintf("--from=%s", from), "--no-admin", "--label=ratelimit"} + n.LogActionF(strings.Join(cmd, " ")) + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully initialized") +} + +func (n *NodeConfig) WasmExecute(contract, execMsg, from string) { + n.LogActionF("executing %s on wasm contract %s from %s", execMsg, contract, from) + cmd := []string{"osmosisd", "tx", "wasm", "execute", contract, execMsg, fmt.Sprintf("--from=%s", from)} + n.LogActionF(strings.Join(cmd, " ")) + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully executed") +} + +// QueryParams extracts the params for a given subspace and key. This is done generically via json to avoid having to +// specify the QueryParamResponse type (which may not exist for all params). +func (n *NodeConfig) QueryParams(subspace, key string, result any) { + cmd := []string{"osmosisd", "query", "params", "subspace", subspace, key, "--output=json"} + + out, _, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + require.NoError(n.t, err) + + err = json.Unmarshal(out.Bytes(), &result) + require.NoError(n.t, err) +} + +func (n *NodeConfig) SubmitParamChangeProposal(proposalJson, from string) { + n.LogActionF("submitting param change proposal %s", proposalJson) + // ToDo: Is there a better way to do this? + wd, err := os.Getwd() + require.NoError(n.t, err) + localProposalFile := wd + "/scripts/param_change_proposal.json" + f, err := os.Create(localProposalFile) + require.NoError(n.t, err) + _, err = f.WriteString(proposalJson) + require.NoError(n.t, err) + err = f.Close() + require.NoError(n.t, err) + + cmd := []string{"osmosisd", "tx", "gov", "submit-proposal", "param-change", "/osmosis/param_change_proposal.json", fmt.Sprintf("--from=%s", from)} + + _, _, err = n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd) + require.NoError(n.t, err) + + err = os.Remove(localProposalFile) + require.NoError(n.t, err) + + n.LogActionF("successfully submitted param change proposal") +} + +func (n *NodeConfig) FailIBCTransfer(from, recipient, amount string) { + n.LogActionF("IBC sending %s from %s to %s", amount, from, recipient) + + 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") + require.NoError(n.t, err) + + n.LogActionF("Failed to send IBC transfer (as expected)") +} + // SwapExactAmountIn swaps tokenInCoin to get at least tokenOutMinAmountInt of the other token's pool out. // swapRoutePoolIds is the comma separated list of pool ids to swap through. // swapRouteDenoms is the comma separated list of denoms to swap through. diff --git a/tests/e2e/configurer/chain/queries.go b/tests/e2e/configurer/chain/queries.go index 8c3600be641..712d6aebf5e 100644 --- a/tests/e2e/configurer/chain/queries.go +++ b/tests/e2e/configurer/chain/queries.go @@ -9,6 +9,8 @@ import ( "strconv" "time" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" @@ -83,6 +85,31 @@ 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") + require.NoError(n.t, err) + + var supplyResp banktypes.QueryTotalSupplyResponse + if err := util.Cdc.UnmarshalJSON(bz, &supplyResp); err != nil { + return sdk.Coins{}, err + } + return supplyResp.GetSupply(), nil +} + +func (n *NodeConfig) QueryContractsFromId(codeId int) ([]string, error) { + path := fmt.Sprintf("/cosmwasm/wasm/v1/code/%d/contracts", codeId) + bz, err := n.QueryGRPCGateway(path) + + require.NoError(n.t, err) + + var contractsResponse wasmtypes.QueryContractsByCodeResponse + if err := util.Cdc.UnmarshalJSON(bz, &contractsResponse); err != nil { + return nil, err + } + + return contractsResponse.Contracts, nil +} + func (n *NodeConfig) QueryPropTally(proposalNumber int) (sdk.Int, sdk.Int, sdk.Int, sdk.Int, error) { path := fmt.Sprintf("cosmos/gov/v1beta1/proposals/%d/tally", proposalNumber) bz, err := n.QueryGRPCGateway(path) diff --git a/tests/e2e/containers/containers.go b/tests/e2e/containers/containers.go index a402eb2b468..829013a7e15 100644 --- a/tests/e2e/containers/containers.go +++ b/tests/e2e/containers/containers.go @@ -53,13 +53,17 @@ func NewManager(isUpgrade bool, isFork bool, isDebugLogEnabled bool) (docker *Ma return docker, nil } -// ExecTxCmd Runs ExecCmd, with flags for txs added. -// namely adding flags `--chain-id={chain-id} -b=block --yes --keyring-backend=test "--log_format=json"`, -// and searching for `code: 0` +// ExecTxCmd Runs ExecTxCmdWithSuccessString searching for `code: 0` func (m *Manager) ExecTxCmd(t *testing.T, chainId string, containerName string, command []string) (bytes.Buffer, bytes.Buffer, error) { + return m.ExecTxCmdWithSuccessString(t, chainId, containerName, command, "code: 0") +} + +// ExecTxCmdWithSuccessString Runs ExecCmd, with flags for txs added. +// namely adding flags `--chain-id={chain-id} -b=block --yes --keyring-backend=test "--log_format=json"`, +// and searching for `successStr` +func (m *Manager) ExecTxCmdWithSuccessString(t *testing.T, chainId string, containerName string, command []string, successStr string) (bytes.Buffer, bytes.Buffer, error) { allTxArgs := []string{fmt.Sprintf("--chain-id=%s", chainId), "-b=block", "--yes", "--keyring-backend=test", "--log_format=json"} txCommand := append(command, allTxArgs...) - successStr := "code: 0" return m.ExecCmd(t, containerName, txCommand, successStr) } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 6ab20e3bca5..0c90121ff67 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -4,7 +4,12 @@ package e2e import ( + "encoding/json" + "fmt" + paramsutils "github.com/cosmos/cosmos-sdk/x/params/client/utils" + ibcratelimittypes "github.com/osmosis-labs/osmosis/v12/x/ibc-rate-limit/types" + "io" "os" "path/filepath" "strconv" @@ -110,6 +115,126 @@ 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/.cargo/config b/x/ibc-rate-limit/.cargo/config similarity index 60% rename from x/ibc-rate-limit/contracts/rate-limiter/.cargo/config rename to x/ibc-rate-limit/.cargo/config index f31de6c2a75..e57a8822395 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/.cargo/config +++ b/x/ibc-rate-limit/.cargo/config @@ -1,3 +1,5 @@ [alias] wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" +schema = "run --example schema" +optimize = "cw-optimizoor" diff --git a/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml b/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml index e166d606418..4c78fcf37fb 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml +++ b/x/ibc-rate-limit/contracts/rate-limiter/Cargo.toml @@ -15,17 +15,6 @@ exclude = [ [lib] crate-type = ["cdylib", "rlib"] -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] @@ -43,8 +32,9 @@ optimize = """docker run --rm -v "$(pwd)":/code \ """ [dependencies] -cosmwasm-std = "1.0.0" -cosmwasm-storage = "1.0.0" +cosmwasm-std = "1.1.0" +cosmwasm-storage = "1.1.0" +cosmwasm-schema = "1.1.0" cw-storage-plus = "0.13.2" cw2 = "0.13.2" schemars = "0.8.8" @@ -52,5 +42,4 @@ serde = { version = "1.0.137", default-features = false, features = ["derive"] } thiserror = { version = "1.0.31" } [dev-dependencies] -cosmwasm-schema = "1.0.0" cw-multi-test = "0.13.2" diff --git a/x/ibc-rate-limit/contracts/rate-limiter/examples/schema.rs b/x/ibc-rate-limit/contracts/rate-limiter/examples/schema.rs new file mode 100644 index 00000000000..954edd462e1 --- /dev/null +++ b/x/ibc-rate-limit/contracts/rate-limiter/examples/schema.rs @@ -0,0 +1,13 @@ +use cosmwasm_schema::write_api; + +use rate_limiter::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SudoMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + sudo: SudoMsg, + migrate: MigrateMsg, + } +} 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 3eef38eed8b..16bc08802b0 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 @@ -2,7 +2,7 @@ use crate::{contract::*, ContractError}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{from_binary, Addr, Attribute}; +use cosmwasm_std::{from_binary, Addr, Attribute, Uint256}; use crate::helpers::tests::verify_query_response; use crate::msg::{InstantiateMsg, PathMsg, QueryMsg, QuotaMsg, SudoMsg}; @@ -52,8 +52,8 @@ fn consume_allowance() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); @@ -64,8 +64,8 @@ fn consume_allowance() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let err = sudo(deps.as_mut(), mock_env(), msg).unwrap_err(); assert!(matches!(err, ContractError::RateLimitExceded { .. })); @@ -91,14 +91,14 @@ fn symetric_flows_dont_consume_allowance() { let send_msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let recv_msg = SudoMsg::RecvPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), send_msg.clone()).unwrap(); @@ -154,8 +154,8 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 60, + channel_value: 3_000_u32.into(), + funds: 60_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); let Attribute { key, value } = &res.attributes[4]; @@ -166,8 +166,8 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 60, + channel_value: 3_000_u32.into(), + funds: 60_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), msg).unwrap(); @@ -180,8 +180,8 @@ fn asymetric_quotas() { let recv_msg = SudoMsg::RecvPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 30, + channel_value: 3_000_u32.into(), + funds: 30_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), recv_msg).unwrap(); let Attribute { key, value } = &res.attributes[3]; @@ -195,8 +195,8 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 60, + channel_value: 3_000_u32.into(), + funds: 60_u32.into(), }; let err = sudo(deps.as_mut(), mock_env(), msg.clone()).unwrap_err(); assert!(matches!(err, ContractError::RateLimitExceded { .. })); @@ -205,8 +205,8 @@ fn asymetric_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 30, + channel_value: 3_000_u32.into(), + funds: 30_u32.into(), }; let res = sudo(deps.as_mut(), mock_env(), msg.clone()).unwrap(); let Attribute { key, value } = &res.attributes[3]; @@ -246,8 +246,8 @@ fn query_state() { assert_eq!(value[0].quota.max_percentage_send, 10); assert_eq!(value[0].quota.max_percentage_recv, 10); assert_eq!(value[0].quota.duration, RESET_TIME_WEEKLY); - assert_eq!(value[0].flow.inflow, 0); - assert_eq!(value[0].flow.outflow, 0); + assert_eq!(value[0].flow.inflow, Uint256::from(0_u32)); + assert_eq!(value[0].flow.outflow, Uint256::from(0_u32)); assert_eq!( value[0].flow.period_end, env.block.time.plus_seconds(RESET_TIME_WEEKLY) @@ -256,16 +256,16 @@ fn query_state() { let send_msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; sudo(deps.as_mut(), mock_env(), send_msg.clone()).unwrap(); let recv_msg = SudoMsg::RecvPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 30, + channel_value: 3_000_u32.into(), + funds: 30_u32.into(), }; sudo(deps.as_mut(), mock_env(), recv_msg.clone()).unwrap(); @@ -277,8 +277,8 @@ fn query_state() { "weekly", (10, 10), RESET_TIME_WEEKLY, - 30, - 300, + 30_u32.into(), + 300_u32.into(), env.block.time.plus_seconds(RESET_TIME_WEEKLY), ); } @@ -317,8 +317,8 @@ fn bad_quotas() { "bad_quota", (100, 100), 200, - 0, - 0, + 0_u32.into(), + 0_u32.into(), env.block.time.plus_seconds(200), ); } @@ -343,13 +343,13 @@ fn undo_send() { let send_msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let undo_msg = SudoMsg::UndoSend { channel_id: format!("channel"), denom: format!("denom"), - funds: 300, + funds: 300_u32.into(), }; sudo(deps.as_mut(), mock_env(), send_msg.clone()).unwrap(); @@ -357,7 +357,10 @@ fn undo_send() { let trackers = RATE_LIMIT_TRACKERS .load(&deps.storage, ("channel".to_string(), "denom".to_string())) .unwrap(); - assert_eq!(trackers.first().unwrap().flow.outflow, 300); + assert_eq!( + trackers.first().unwrap().flow.outflow, + Uint256::from(300_u32) + ); let period_end = trackers.first().unwrap().flow.period_end; let channel_value = trackers.first().unwrap().quota.channel_value; @@ -366,7 +369,7 @@ fn undo_send() { let trackers = RATE_LIMIT_TRACKERS .load(&deps.storage, ("channel".to_string(), "denom".to_string())) .unwrap(); - assert_eq!(trackers.first().unwrap().flow.outflow, 0); + 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); } diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/execute.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/execute.rs index 4bac4c0d37f..047a2179dd0 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/execute.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/execute.rs @@ -159,8 +159,8 @@ mod tests { "daily", (3, 5), 1600, - 0, - 0, + 0_u32.into(), + 0_u32.into(), env.block.time.plus_seconds(1600), ); @@ -208,8 +208,8 @@ mod tests { "daily", (3, 5), 1600, - 0, - 0, + 0_u32.into(), + 0_u32.into(), env.block.time.plus_seconds(1600), ); @@ -241,8 +241,8 @@ mod tests { "different", (50, 30), 5000, - 0, - 0, + 0_u32.into(), + 0_u32.into(), env.block.time.plus_seconds(5000), ); } diff --git a/x/ibc-rate-limit/contracts/rate-limiter/src/helpers.rs b/x/ibc-rate-limit/contracts/rate-limiter/src/helpers.rs index 6cfd60a65a8..530d3b6cf2d 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/helpers.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/helpers.rs @@ -37,7 +37,7 @@ impl RateLimitingContract { } pub mod tests { - use cosmwasm_std::Timestamp; + use cosmwasm_std::{Timestamp, Uint256}; use crate::state::RateLimit; @@ -46,8 +46,8 @@ pub mod tests { quota_name: &str, send_recv: (u32, u32), duration: u64, - inflow: u128, - outflow: u128, + inflow: Uint256, + outflow: Uint256, period_end: Timestamp, ) { assert_eq!(value.quota.name, quota_name); 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 8807028fcb9..66a145b397d 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 @@ -42,7 +42,7 @@ fn mock_app() -> App { // Instantiate the contract fn proper_instantiate(paths: Vec) -> (App, RateLimitingContract) { let mut app = mock_app(); - let cw_template_id = app.store_code(contract_template()); + let cw_code_id = app.store_code(contract_template()); let msg = InstantiateMsg { gov_module: Addr::unchecked(GOV_ADDR), @@ -52,7 +52,7 @@ fn proper_instantiate(paths: Vec) -> (App, RateLimitingContract) { let cw_rate_limit_contract_addr = app .instantiate_contract( - cw_template_id, + cw_code_id, Addr::unchecked(GOV_ADDR), &msg, &[], @@ -82,8 +82,8 @@ fn expiration() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); let res = app.sudo(cosmos_msg).unwrap(); @@ -105,8 +105,8 @@ fn expiration() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); let _err = app.sudo(cosmos_msg).unwrap_err(); @@ -123,8 +123,8 @@ fn expiration() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -162,8 +162,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); @@ -172,8 +172,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -188,8 +188,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -207,8 +207,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); @@ -224,8 +224,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -240,8 +240,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -257,8 +257,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -272,8 +272,8 @@ fn multiple_quotas() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); @@ -296,8 +296,8 @@ fn channel_value_cached() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 1, + channel_value: 100_u32.into(), + funds: 1_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap(); @@ -306,8 +306,8 @@ fn channel_value_cached() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100, - funds: 3, + channel_value: 100_u32.into(), + funds: 3_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -316,8 +316,8 @@ fn channel_value_cached() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 100000, - funds: 3, + channel_value: 100000_u32.into(), + funds: 3_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); app.sudo(cosmos_msg).unwrap_err(); @@ -336,8 +336,8 @@ fn channel_value_cached() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 10_000, - funds: 100, + channel_value: 10_000_u32.into(), + funds: 100_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -353,8 +353,8 @@ fn channel_value_cached() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 10_000, - funds: 100, + channel_value: 10_000_u32.into(), + funds: 100_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -364,8 +364,8 @@ fn channel_value_cached() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 1, - funds: 75, + channel_value: 1_u32.into(), + funds: 75_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg); @@ -380,8 +380,8 @@ fn add_paths_later() { let msg = SudoMsg::SendPacket { channel_id: format!("channel"), denom: format!("denom"), - channel_value: 3_000, - funds: 300, + channel_value: 3_000_u32.into(), + funds: 300_u32.into(), }; let cosmos_msg = cw_rate_limit_contract.sudo(msg.clone()); let res = app.sudo(cosmos_msg).unwrap(); 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 7ae027efd25..0f1f0c4b061 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/msg.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/msg.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::Addr; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint256}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -44,7 +45,7 @@ impl QuotaMsg { /// Initialize the contract with the address of the IBC module and any existing channels. /// Only the ibc module is allowed to execute actions on this contract -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct InstantiateMsg { pub gov_module: Addr, pub ibc_module: Addr, @@ -53,8 +54,7 @@ pub struct InstantiateMsg { /// The caller (IBC module) is responsible for correctly calculating the funds /// being sent through the channel -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[cw_serde] pub enum ExecuteMsg { AddPath { channel_id: String, @@ -72,34 +72,33 @@ pub enum ExecuteMsg { }, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[cw_serde] +#[derive(QueryResponses)] pub enum QueryMsg { + #[returns(Vec)] GetQuotas { channel_id: String, denom: String }, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[cw_serde] pub enum SudoMsg { SendPacket { channel_id: String, denom: String, - channel_value: u128, - funds: u128, + channel_value: Uint256, + funds: Uint256, }, RecvPacket { channel_id: String, denom: String, - channel_value: u128, - funds: u128, + channel_value: Uint256, + funds: Uint256, }, UndoSend { channel_id: String, denom: String, - funds: u128, + funds: Uint256, }, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[cw_serde] pub enum MigrateMsg {} 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 8dc5f9fa2f6..5237946487d 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/state.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/state.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Timestamp}; +use cosmwasm_std::{Addr, Timestamp, Uint256}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::cmp; @@ -62,16 +62,15 @@ pub enum FlowType { /// This is a design decision to avoid the period calculations and thus reduce gas consumption #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Copy)] pub struct Flow { - // Q: Do we have edge case issues with inflow/outflow being u128, e.g. what if a token has super high precision. - pub inflow: u128, - pub outflow: u128, + pub inflow: Uint256, + pub outflow: Uint256, pub period_end: Timestamp, } impl Flow { pub fn new( - inflow: impl Into, - outflow: impl Into, + inflow: impl Into, + outflow: impl Into, now: Timestamp, duration: u64, ) -> Self { @@ -87,7 +86,7 @@ impl Flow { /// (balance_in, balance_out) where balance_in in is how much has been /// transferred into the flow, and balance_out is how much value transferred /// out. - pub fn balance(&self) -> (u128, u128) { + pub fn balance(&self) -> (Uint256, Uint256) { ( self.inflow.saturating_sub(self.outflow), self.outflow.saturating_sub(self.inflow), @@ -95,7 +94,7 @@ impl Flow { } /// checks if the flow, in the current state, has exceeded a max allowance - pub fn exceeds(&self, direction: &FlowType, max_inflow: u128, max_outflow: u128) -> bool { + pub fn exceeds(&self, direction: &FlowType, max_inflow: Uint256, max_outflow: Uint256) -> bool { let (balance_in, balance_out) = self.balance(); match direction { FlowType::In => balance_in > max_inflow, @@ -113,13 +112,13 @@ impl Flow { /// Expire resets the Flow to start tracking the value transfer from the /// moment this method is called. pub fn expire(&mut self, now: Timestamp, duration: u64) { - self.inflow = 0; - self.outflow = 0; + self.inflow = Uint256::from(0_u32); + self.outflow = Uint256::from(0_u32); self.period_end = now.plus_seconds(duration); } /// Updates the current flow incrementing it by a transfer of value. - pub fn add_flow(&mut self, direction: FlowType, value: u128) { + pub fn add_flow(&mut self, direction: FlowType, value: Uint256) { match direction { FlowType::In => self.inflow = self.inflow.saturating_add(value), FlowType::Out => self.outflow = self.outflow.saturating_add(value), @@ -127,7 +126,7 @@ impl Flow { } /// Updates the current flow reducing it by a transfer of value. - pub fn undo_flow(&mut self, direction: FlowType, value: u128) { + pub fn undo_flow(&mut self, direction: FlowType, value: Uint256) { match direction { FlowType::In => self.inflow = self.inflow.saturating_sub(value), FlowType::Out => self.outflow = self.outflow.saturating_sub(value), @@ -139,7 +138,7 @@ impl Flow { fn apply_transfer( &mut self, direction: &FlowType, - funds: u128, + funds: Uint256, now: Timestamp, quota: &Quota, ) -> bool { @@ -166,7 +165,7 @@ pub struct Quota { pub max_percentage_send: u32, pub max_percentage_recv: u32, pub duration: u64, - pub channel_value: Option, + pub channel_value: Option, } impl Quota { @@ -174,13 +173,13 @@ impl Quota { /// total_value) in each direction based on the total value of the denom in /// the channel. The result tuple represents the max capacity when the /// transfer is in directions: (FlowType::In, FlowType::Out) - pub fn capacity(&self) -> (u128, u128) { + pub fn capacity(&self) -> (Uint256, Uint256) { match self.channel_value { Some(total_value) => ( - total_value * (self.max_percentage_recv as u128) / 100_u128, - total_value * (self.max_percentage_send as u128) / 100_u128, + total_value * Uint256::from(self.max_percentage_recv) / Uint256::from(100_u32), + total_value * Uint256::from(self.max_percentage_send) / Uint256::from(100_u32), ), - None => (0, 0), // This should never happen, but ig the channel value is not set, we disallow any transfer + None => (0_u32.into(), 0_u32.into()), // This should never happen, but ig the channel value is not set, we disallow any transfer } } } @@ -221,8 +220,8 @@ impl RateLimit { &mut self, path: &Path, direction: &FlowType, - funds: u128, - channel_value: u128, + funds: Uint256, + channel_value: Uint256, now: Timestamp, ) -> Result { let expired = self.flow.apply_transfer(direction, funds, now, &self.quota); @@ -292,18 +291,18 @@ pub mod tests { assert!(!flow.is_expired(epoch.plus_seconds(RESET_TIME_WEEKLY))); assert!(flow.is_expired(epoch.plus_seconds(RESET_TIME_WEEKLY).plus_nanos(1))); - assert_eq!(flow.balance(), (0_u128, 0_u128)); - flow.add_flow(FlowType::In, 5); - assert_eq!(flow.balance(), (5_u128, 0_u128)); - flow.add_flow(FlowType::Out, 2); - assert_eq!(flow.balance(), (3_u128, 0_u128)); + assert_eq!(flow.balance(), (0_u32.into(), 0_u32.into())); + flow.add_flow(FlowType::In, 5_u32.into()); + assert_eq!(flow.balance(), (5_u32.into(), 0_u32.into())); + flow.add_flow(FlowType::Out, 2_u32.into()); + assert_eq!(flow.balance(), (3_u32.into(), 0_u32.into())); // Adding flow doesn't affect expiration assert!(!flow.is_expired(epoch.plus_seconds(RESET_TIME_DAILY))); flow.expire(epoch.plus_seconds(RESET_TIME_WEEKLY), RESET_TIME_WEEKLY); - assert_eq!(flow.balance(), (0_u128, 0_u128)); - assert_eq!(flow.inflow, 0_u128); - assert_eq!(flow.outflow, 0_u128); + assert_eq!(flow.balance(), (0_u32.into(), 0_u32.into())); + assert_eq!(flow.inflow, Uint256::from(0_u32)); + assert_eq!(flow.outflow, Uint256::from(0_u32)); assert_eq!(flow.period_end, epoch.plus_seconds(RESET_TIME_WEEKLY * 2)); // Expiration has moved 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 8315a01fcf8..0a8ae8e5161 100644 --- a/x/ibc-rate-limit/contracts/rate-limiter/src/sudo.rs +++ b/x/ibc-rate-limit/contracts/rate-limiter/src/sudo.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{DepsMut, Response, Timestamp}; +use cosmwasm_std::{DepsMut, Response, Timestamp, Uint256}; use crate::{ state::{FlowType, Path, RateLimit, RATE_LIMIT_TRACKERS}, @@ -14,8 +14,8 @@ use crate::{ pub fn try_transfer( deps: DepsMut, path: &Path, - channel_value: u128, - funds: u128, + channel_value: Uint256, + funds: Uint256, direction: FlowType, now: Timestamp, ) -> Result { @@ -96,7 +96,7 @@ 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: u128) -> Result { +pub fn undo_send(deps: DepsMut, path: &Path, funds: Uint256) -> Result { // Sudo call. Only go modules should be allowed to access this let trackers = RATE_LIMIT_TRACKERS.may_load(deps.storage, path.into())?; 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..eb16f899a68 --- /dev/null +++ b/x/ibc-rate-limit/ibc_middleware_test.go @@ -0,0 +1,351 @@ +package ibc_rate_limit_test + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "testing" + "time" + + 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 + +// NewValidMessage generates a new sdk.Msg of type MsgTransfer. +// forward=true means that the message will be a "send" message, while forward=false is for a "receive" message. +// amount represents the amount transferred +func (suite *MiddlewareTestSuite) NewValidMessage(forward bool, amount sdk.Int) sdk.Msg { + var coins sdk.Coin + var port, channel, accountFrom, accountTo string + + if forward { + coins = sdk.NewCoin(sdk.DefaultBondDenom, amount) + port = suite.path.EndpointA.ChannelConfig.PortID + channel = suite.path.EndpointA.ChannelID + accountFrom = suite.chainA.SenderAccount.GetAddress().String() + accountTo = suite.chainB.SenderAccount.GetAddress().String() + } else { + coins = transfertypes.GetTransferCoin( + suite.path.EndpointB.ChannelConfig.PortID, + suite.path.EndpointB.ChannelID, + sdk.DefaultBondDenom, + sdk.NewInt(1), + ) + coins = sdk.NewCoin(sdk.DefaultBondDenom, 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, + coins, + 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.ExecuteReceive(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) ExecuteReceive(msg sdk.Msg) (string, error) { + res, err := suite.chainB.SendMsgsNoCheck(msg) + suite.Require().NoError(err) + + packet, err := ibctesting.ParsePacketFromEvents(res.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()) + return string(ack), err +} + +func (suite *MiddlewareTestSuite) AssertReceive(success bool, msg sdk.Msg) (string, error) { + ack, err := suite.ExecuteReceive(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.chainA.SendMsgsNoCheck(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 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]}] } + `, sdk.DefaultBondDenom, 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.NewValidMessage(true, 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.NewValidMessage(false, one)) +} + +func (suite *MiddlewareTestSuite) fullSendTest() map[string]string { + // Setup contract + suite.chainA.StoreContractCode(&suite.Suite) + quotas := suite.BuildChannelQuota("weekly", 604800, 5, 5) + addr := suite.chainA.InstantiateContract(&suite.Suite, quotas) + suite.chainA.RegisterRateLimitingContract(addr) + + // Setup sender chain's quota + osmosisApp := suite.chainA.GetOsmosisApp() + + // Each user has 10% of the supply + supply := osmosisApp.BankKeeper.GetSupplyWithOffset(suite.chainA.GetContext(), sdk.DefaultBondDenom) + quota := supply.Amount.QuoRaw(20) + half := quota.QuoRaw(2) + + // send 2.5% (quota is 5%) + suite.AssertSend(true, suite.NewValidMessage(true, half)) + + // send 2.5% (quota is 5%) + r, _ := suite.AssertSend(true, suite.NewValidMessage(true, half)) + + // 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, quota) + + // Sending above the quota should fail. + suite.AssertSend(false, suite.NewValidMessage(true, sdk.NewInt(1))) + return attrs +} + +// Test rate limiting on sends +func (suite *MiddlewareTestSuite) TestSendTransferWithRateLimiting() { + suite.fullSendTest() +} + +// 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() + 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 both chains one block + suite.chainA.NextBlock() + suite.chainA.SenderAccount.SetSequence(suite.chainA.SenderAccount.GetSequence() + 1) + suite.chainB.NextBlock() + suite.chainB.SenderAccount.SetSequence(suite.chainB.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.NewValidMessage(true, sdk.NewInt(1))) +} + +// Test rate limiting on receives +func (suite *MiddlewareTestSuite) TestRecvTransferWithRateLimiting() { + // Setup contract + suite.chainA.StoreContractCode(&suite.Suite) + quotas := suite.BuildChannelQuota("weekly", 604800, 5, 5) + addr := suite.chainA.InstantiateContract(&suite.Suite, quotas) + suite.chainA.RegisterRateLimitingContract(addr) + + osmosisApp := suite.chainA.GetOsmosisApp() + + // Setup receiver chain's quota + // Each user has 10% of the supply + supply := osmosisApp.BankKeeper.GetSupplyWithOffset(suite.chainA.GetContext(), sdk.DefaultBondDenom) + quota := supply.Amount.QuoRaw(20) + half := quota.QuoRaw(2) + + // receive 2.5% (quota is 5%) + suite.AssertReceive(true, suite.NewValidMessage(false, half)) + + // receive 2.5% (quota is 5%) + suite.AssertReceive(true, suite.NewValidMessage(false, half)) + + // Sending above the quota should fail. Adding some extra here because the cap is increasing. See test bellow. + suite.AssertReceive(false, suite.NewValidMessage(false, sdk.NewInt(1))) +} + +// 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.NewValidMessage(true, sdk.NewInt(1))) +} + +// Test rate limits are reverted if a "send" fails +func (suite *MiddlewareTestSuite) TestFailedSendTransfer() { + // Setup contract + suite.chainA.StoreContractCode(&suite.Suite) + quotas := suite.BuildChannelQuota("weekly", 604800, 1, 1) + addr := suite.chainA.InstantiateContract(&suite.Suite, quotas) + suite.chainA.RegisterRateLimitingContract(addr) + + // Setup sender chain's quota + osmosisApp := suite.chainA.GetOsmosisApp() + + // Each user has 10% of the supply + supply := osmosisApp.BankKeeper.GetSupplyWithOffset(suite.chainA.GetContext(), sdk.DefaultBondDenom) + quota := supply.Amount.QuoRaw(100) // 1% of the supply + + // 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) + + res, _ := suite.AssertSend(true, msg) + + // Sending again fails as the quota is filled + suite.AssertSend(false, suite.NewValidMessage(true, 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.NewValidMessage(true, sdk.NewInt(1))) +} diff --git a/x/ibc-rate-limit/ibc_module.go b/x/ibc-rate-limit/ibc_module.go index c1df7c9219f..339c67031dd 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 @@ -131,7 +146,7 @@ func (im *IBCModule) OnRecvPacket( 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/rate_limit.go b/x/ibc-rate-limit/rate_limit.go index 665f04b2990..a0fff052875 100644 --- a/x/ibc-rate-limit/rate_limit.go +++ b/x/ibc-rate-limit/rate_limit.go @@ -3,6 +3,8 @@ package ibc_rate_limit import ( "encoding/json" + transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -15,11 +17,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, @@ -128,7 +125,7 @@ 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 diff --git a/x/ibc-rate-limit/schema/rate-limiter.json b/x/ibc-rate-limit/schema/rate-limiter.json new file mode 100644 index 00000000000..7544964bd09 --- /dev/null +++ b/x/ibc-rate-limit/schema/rate-limiter.json @@ -0,0 +1,472 @@ +{ + "contract_name": "rate-limiter", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "Initialize the contract with the address of the IBC module and any existing channels. Only the ibc module is allowed to execute actions on this contract", + "type": "object", + "required": [ + "gov_module", + "ibc_module", + "paths" + ], + "properties": { + "gov_module": { + "$ref": "#/definitions/Addr" + }, + "ibc_module": { + "$ref": "#/definitions/Addr" + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/PathMsg" + } + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "PathMsg": { + "type": "object", + "required": [ + "channel_id", + "denom", + "quotas" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "denom": { + "type": "string" + }, + "quotas": { + "type": "array", + "items": { + "$ref": "#/definitions/QuotaMsg" + } + } + } + }, + "QuotaMsg": { + "type": "object", + "required": [ + "duration", + "name", + "send_recv" + ], + "properties": { + "duration": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "name": { + "type": "string" + }, + "send_recv": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "The caller (IBC module) is responsible for correctly calculating the funds being sent through the channel", + "oneOf": [ + { + "type": "object", + "required": [ + "add_path" + ], + "properties": { + "add_path": { + "type": "object", + "required": [ + "channel_id", + "denom", + "quotas" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "denom": { + "type": "string" + }, + "quotas": { + "type": "array", + "items": { + "$ref": "#/definitions/QuotaMsg" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_path" + ], + "properties": { + "remove_path": { + "type": "object", + "required": [ + "channel_id", + "denom" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "reset_path_quota" + ], + "properties": { + "reset_path_quota": { + "type": "object", + "required": [ + "channel_id", + "denom", + "quota_id" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "denom": { + "type": "string" + }, + "quota_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "QuotaMsg": { + "type": "object", + "required": [ + "duration", + "name", + "send_recv" + ], + "properties": { + "duration": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "name": { + "type": "string" + }, + "send_recv": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "get_quotas" + ], + "properties": { + "get_quotas": { + "type": "object", + "required": [ + "channel_id", + "denom" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "string", + "enum": [] + }, + "sudo": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SudoMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "send_packet" + ], + "properties": { + "send_packet": { + "type": "object", + "required": [ + "channel_id", + "channel_value", + "denom", + "funds" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "channel_value": { + "$ref": "#/definitions/Uint256" + }, + "denom": { + "type": "string" + }, + "funds": { + "$ref": "#/definitions/Uint256" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "recv_packet" + ], + "properties": { + "recv_packet": { + "type": "object", + "required": [ + "channel_id", + "channel_value", + "denom", + "funds" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "channel_value": { + "$ref": "#/definitions/Uint256" + }, + "denom": { + "type": "string" + }, + "funds": { + "$ref": "#/definitions/Uint256" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "undo_send" + ], + "properties": { + "undo_send": { + "type": "object", + "required": [ + "channel_id", + "denom", + "funds" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "denom": { + "type": "string" + }, + "funds": { + "$ref": "#/definitions/Uint256" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + } + } + }, + "responses": { + "get_quotas": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_RateLimit", + "type": "array", + "items": { + "$ref": "#/definitions/RateLimit" + }, + "definitions": { + "Flow": { + "description": "A Flow represents the transfer of value for a denom through an IBC channel during a time window.\n\nIt tracks inflows (transfers into osmosis) and outflows (transfers out of osmosis).\n\nThe period_end represents the last point in time for which this Flow is tracking the value transfer.\n\nPeriods are discrete repeating windows. A period only starts when a contract call to update the Flow (SendPacket/RecvPackt) is made, and not right after the period ends. This means that if no calls happen after a period expires, the next period will begin at the time of the next call and be valid for the specified duration for the quota.\n\nThis is a design decision to avoid the period calculations and thus reduce gas consumption", + "type": "object", + "required": [ + "inflow", + "outflow", + "period_end" + ], + "properties": { + "inflow": { + "$ref": "#/definitions/Uint256" + }, + "outflow": { + "$ref": "#/definitions/Uint256" + }, + "period_end": { + "$ref": "#/definitions/Timestamp" + } + } + }, + "Quota": { + "description": "A Quota is the percentage of the denom's total value that can be transferred through the channel in a given period of time (duration)\n\nPercentages can be different for send and recv\n\nThe name of the quota is expected to be a human-readable representation of the duration (i.e.: \"weekly\", \"daily\", \"every-six-months\", ...)", + "type": "object", + "required": [ + "duration", + "max_percentage_recv", + "max_percentage_send", + "name" + ], + "properties": { + "channel_value": { + "anyOf": [ + { + "$ref": "#/definitions/Uint256" + }, + { + "type": "null" + } + ] + }, + "duration": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "max_percentage_recv": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "max_percentage_send": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "name": { + "type": "string" + } + } + }, + "RateLimit": { + "description": "RateLimit is the main structure tracked for each channel/denom pair. Its quota represents rate limit configuration, and the flow its current state (i.e.: how much value has been transfered in the current period)", + "type": "object", + "required": [ + "flow", + "quota" + ], + "properties": { + "flow": { + "$ref": "#/definitions/Flow" + }, + "quota": { + "$ref": "#/definitions/Quota" + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/x/ibc-rate-limit/testdata/rate_limiter.wasm b/x/ibc-rate-limit/testdata/rate_limiter.wasm index 0341a5b1c9a..caf63c41459 100644 Binary files a/x/ibc-rate-limit/testdata/rate_limiter.wasm and b/x/ibc-rate-limit/testdata/rate_limiter.wasm differ 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") )