Repository: osmosis-labs/osmosis
Pull request #3190 (2022-10-31 07:38:15)

Progress on IBC rate limit spec
## What is the purpose of the change

Improve on the spec for IBC rate limit. [Rendered](https://github.com/osmosis-labs/osmosis/blob/dev/ibc-rate-limit-spec/x/ibc-rate-limit/README.md)

## Brief Changelog

- Improve spec
- Rename `testdata` folder to `bytecode`

## Testing and Verifying

This change is a trivial rework / code cleanup without any test coverage.



Patch:
diff --git a/.github/workflows/auto-update-upgrade.yml b/.github/workflows/auto-update-upgrade.yml
new file mode 100644
index 000000000..aee6de935
--- /dev/null
+++ b/.github/workflows/auto-update-upgrade.yml
@@ -0,0 +1,38 @@
+# When new major release is created this workflow will be triggered and will do 3 things:
+# 1) it will create a directory with an empty upgrade handler in app/upgrades folder
+# 2) will increase an E2E_UPGRADE_VERSION variable in Makefile
+# 3) create a pull request with these changes to main 
+
+name: On Release Auto Upgrade
+
+on:
+  release: 
+    types: [published]
+
+jobs:
+  post_release:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2.3.4
+
+      - name: Run version script
+        run: bash ./scripts/check_release.sh ${{ github.event.release.tag_name }}
+
+      - name: Run post release script
+        if: env.MAJOR == 1    # 1 means vX of existing upgrade handler is smaller than A in tag vA.B.C 
+        run: bash ./scripts/empty_upgrade_handler_gen.sh 
+
+      - name: Create PR
+        if: env.MAJOR == 1
+        uses: peter-evans/create-pull-request@v4
+        with:
+          base: ${{ github.event.repository.default_branch }}
+          body: |
+            Update report
+            - Created a new empty upgrade handler
+            - Increased E2E_UPGRADE_VERSION in Makefile by 1
+          labels: |
+            T:auto
+            C:e2e
+            C:app-wiring
\ No newline at end of file
diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml
index 50dc9addb..630318811 100644
--- a/.github/workflows/contracts.yml
+++ b/.github/workflows/contracts.yml
@@ -12,11 +12,11 @@ on:
 
 jobs:
   test:
-    name: Test Suite
+    name: Test Cosmwasm Contracts
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        contract: [{workdir: ./x/ibc-rate-limit/, output: testdata/rate_limiter.wasm, build: artifacts/rate_limiter-x86_64.wasm, name: rate_limiter}]
+        contract: [{workdir: ./x/ibc-rate-limit/, output: bytecode/rate_limiter.wasm, build: artifacts/rate_limiter-x86_64.wasm, name: rate_limiter}]
 
     steps:
       - name: Checkout sources
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 4baa5ff16..2debe754e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -54,9 +54,4 @@ jobs:
           # within `super-linter`.
           fetch-depth: 0
       - name: Run documentation linter
-        uses: github/super-linter@v4
-        env:
-          VALIDATE_ALL_CODEBASE: false
-          VALIDATE_MARKDOWN: true
-          DEFAULT_BRANCH: main
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: make mdlint
diff --git a/.markdownlint.yml b/.markdownlint.yml
index 926e66256..cbe0c3cee 100644
--- a/.markdownlint.yml
+++ b/.markdownlint.yml
@@ -12,8 +12,6 @@ MD042: true
 MD048: true
 MD051: true
 # MD004: false
-# # Can't disable MD007 :/
-# MD007: false
 # MD009: false
 # MD010:
 #   code_blocks: false
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..9f235a7f4
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,30 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "E2E: (make test-e2e-short)",
+            "type": "go",
+            "request": "launch",
+            "mode": "test",
+            "program": "${workspaceFolder}/tests/e2e",
+            "args": [
+                "-test.timeout",
+                "30m",
+                "-test.run",
+                "IntegrationTestSuite",
+                "-test.v"
+            ],
+            "buildFlags": "-tags e2e",
+            "env": {
+                "OSMOSIS_E2E": "True",
+                "OSMOSIS_E2E_SKIP_IBC": "true",
+                "OSMOSIS_E2E_SKIP_UPGRADE": "true",
+                "OSMOSIS_E2E_SKIP_CLEANUP": "true",
+                "OSMOSIS_E2E_SKIP_STATE_SYNC": "true",
+                "OSMOSIS_E2E_UPGRADE_VERSION": "v13",
+                "OSMOSIS_E2E_DEBUG_LOG": "true",
+            },
+            "preLaunchTask": "e2e-setup"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 000000000..ab5652200
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,12 @@
+{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "e2e-setup",
+            "type": "shell",
+            "command": "make e2e-setup"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/app/app.go b/app/app.go
index 747d77735..b5d3f67ef 100644
--- a/app/app.go
+++ b/app/app.go
@@ -267,6 +267,8 @@ func NewOsmosisApp(
 			app.IBCKeeper,
 		),
 	)
+	// Uncomment to enable postHandlers:
+	// app.SetPostHandler(NewTxPostHandler())
 	app.SetEndBlocker(app.EndBlocker)
 
 	// Register snapshot extensions to enable state-sync for wasm.
diff --git a/app/tx_post_handler.go b/app/tx_post_handler.go
new file mode 100644
index 000000000..2141a3d84
--- /dev/null
+++ b/app/tx_post_handler.go
@@ -0,0 +1,9 @@
+package app
+
+import (
+	sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+func NewTxPostHandler() sdk.AnteHandler {
+	panic("not implemented")
+}
diff --git a/osmomath/decimal.go b/osmomath/decimal.go
index 381729988..2bb9e0134 100644
--- a/osmomath/decimal.go
+++ b/osmomath/decimal.go
@@ -43,6 +43,13 @@ var (
 	oneInt               = big.NewInt(1)
 	tenInt               = big.NewInt(10)
 
+	// log_2(e)
+	// From: https://www.wolframalpha.com/input?i=log_2%28e%29+with+37+digits
+	logOfEbase2 = MustNewDecFromStr("1.442695040888963407359924681001892137")
+
+	// log_2(1.0001)
+	// From: https://www.wolframalpha.com/input?i=log_2%281.0001%29+to+33+digits
+	tickLogOf2 = MustNewDecFromStr("0.000144262291094554178391070900057480")
 	// initialized in init() since requires
 	// precision to be defined.
 	twoBigDec BigDec
@@ -917,3 +924,38 @@ func (x BigDec) LogBase2() BigDec {
 
 	return y
 }
+
+// Natural logarithm of x.
+// Formula: ln(x) = log_2(x) / log_2(e)
+func (x BigDec) Ln() BigDec {
+	log2x := x.LogBase2()
+
+	y := log2x.Quo(logOfEbase2)
+
+	return y
+}
+
+// log_1.0001(x) "tick" base logarithm
+// Formula: log_1.0001(b) = log_2(b) / log_2(1.0001)
+func (x BigDec) TickLog() BigDec {
+	log2x := x.LogBase2()
+
+	y := log2x.Quo(tickLogOf2)
+
+	return y
+}
+
+// log_a(x) custom base logarithm
+// Formula: log_a(b) = log_2(b) / log_2(a)
+func (x BigDec) CustomBaseLog(base BigDec) BigDec {
+	if base.LTE(ZeroDec()) || base.Equal(OneDec()) {
+		panic(fmt.Sprintf("log is not defined at base <= 0 or base == 1, base given (%s)", base))
+	}
+
+	log2x_argument := x.LogBase2()
+	log2x_base := base.LogBase2()
+
+	y := log2x_argument.Quo(log2x_base)
+
+	return y
+}
diff --git a/scripts/check_release.sh b/scripts/check_release.sh
new file mode 100644
index 000000000..befaeaf8e
--- /dev/null
+++ b/scripts/check_release.sh
@@ -0,0 +1,21 @@
+# this script checks if existing upgrade handler's version is not smaller than current release 
+
+#!/bin/bash
+
+VERSION=$1
+latest_version=0
+for f in app/upgrades/*; do 
+    s_f=(${f//// })
+    version=${s_f[2]}
+    num_version=${version//[!0-9]/}
+    if [[ $num_version -gt $latest_version ]]; then
+        latest_version=$num_version
+    fi
+done
+
+VERSION=${VERSION[@]:1}
+VERSION_MAJOR=(${VERSION//./ })
+VERSION_MAJOR=${VERSION_MAJOR[0]}
+if [[ $VERSION_MAJOR -gt $latest_version ]]; then
+    echo "MAJOR=1" >> $GITHUB_ENV
+fi
\ No newline at end of file
diff --git a/scripts/empty_upgrade_handler_gen.sh b/scripts/empty_upgrade_handler_gen.sh
new file mode 100755
index 000000000..1ca78fcbc
--- /dev/null
+++ b/scripts/empty_upgrade_handler_gen.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+
+# 1) this script creates an empty directory in app/upgrades called "vX" where X is a previous version + 1 with an empty upgrade handler.
+# 2) increases E2E_UPGRADE_VERSION in makefile by 1
+# 3) adds new version to app.go
+
+# Also insures that all the imports make use of a current module version from go mod:
+# (see:    module=$(go mod edit -json | jq ".Module.Path")      in this script)
+# Github workflow which calls this script can be found here: osmosis/.github/workflows/auto-update-upgrade.yml
+
+ latest_version=0
+ for f in app/upgrades/*; do 
+     s_f=(${f//// })
+     version=${s_f[2]}
+     num_version=${version//[!0-9]/}
+     if [[ $num_version -gt $latest_version ]]; then
+        LATEST_FILE=$f
+        latest_version=$num_version
+     fi
+ done
+ version_create=$((latest_version+1))
+ new_file=./app/upgrades/v${version_create}
+
+ mkdir $new_file
+ CONSTANTS_FILE=$new_file/constants.go
+ UPGRADES_FILE=$new_file/upgrades.go
+ touch $CONSTANTS_FILE
+ touch $UPGRADES_FILE
+ 
+ module=$(go mod edit -json | jq ".Module.Path")
+ module=${module%?}
+ path=${module%???}
+
+ bracks='"'
+ # set packages
+ echo -e "package v${version_create}\n" >> $CONSTANTS_FILE
+ echo -e "package v${version_create}\n" >> $UPGRADES_FILE
+
+ # imports
+ echo "import (" >> $CONSTANTS_FILE
+ echo "import (" >> $UPGRADES_FILE
+
+ # set imports for constants.go
+ echo -e "\t$module/app/upgrades$bracks\n" >> $CONSTANTS_FILE
+ echo -e '\tstore "github.com/cosmos/cosmos-sdk/store/types"' >> $CONSTANTS_FILE
+
+ # set imports for upgrades.go
+ echo -e '\tsdk "github.com/cosmos/cosmos-sdk/types"' >> $UPGRADES_FILE
+ echo -e '\t"github.com/cosmos/cosmos-sdk/types/module"' >> $UPGRADES_FILE
+ echo -e '\tupgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"\n' >> $UPGRADES_FILE
+ echo -e "\t$module/app/keepers$bracks" >> $UPGRADES_FILE
+ echo -e "\t$module/app/upgrades$bracks" >> $UPGRADES_FILE
+
+ # close import
+ echo ")" >> $UPGRADES_FILE
+ echo -e ")\n" >> $CONSTANTS_FILE
+ 
+ # constants.go logic
+ echo "// UpgradeName defines the on-chain upgrade name for the Osmosis v$version_create upgrade." >> $CONSTANTS_FILE
+ echo "const UpgradeName = ${bracks}v$version_create$bracks" >> $CONSTANTS_FILE
+ echo "
+var Upgrade = upgrades.Upgrade{
+	UpgradeName:          UpgradeName,
+	CreateUpgradeHandler: CreateUpgradeHandler,
+	StoreUpgrades:        store.StoreUpgrades{},
+}" >> $CONSTANTS_FILE
+ 
+ # upgrades.go logic
+ echo "
+func CreateUpgradeHandler(
+	mm *module.Manager,
+	configurator module.Configurator,
+	bpm upgrades.BaseAppParamManager,
+	keepers *keepers.AppKeepers,
+) upgradetypes.UpgradeHandler {
+	return func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
+		return mm.RunMigrations(ctx, configurator, fromVM)
+	}
+}" >> $UPGRADES_FILE
+
+  # change app/app.go file
+ app_file=./app/app.go
+ UPGRADES_LINE=$(grep -F upgrades.Upgrade{ $app_file)
+ UPGRADES_LINE="${UPGRADES_LINE%?}, v${version_create}.Upgrade}"
+ sed -i "s|.*upgrades.Upgrade{.*|$UPGRADES_LINE|" $app_file 
+
+ PREV_IMPORT="v$latest_version $module/app/upgrades/v$latest_version$bracks"
+ NEW_IMPORT="v$version_create $module/app/upgrades/v$version_create$bracks"
+ sed -i "s|.*$PREV_IMPORT.*|\t$PREV_IMPORT\n\t$NEW_IMPORT|" $app_file
+
+ # change e2e version in makefile
+ sed -i "s/E2E_UPGRADE_VERSION := ${bracks}v$latest_version$bracks/E2E_UPGRADE_VERSION := ${bracks}v$version_create$bracks/" ./Makefile
\ No newline at end of file
diff --git a/x/ibc-rate-limit/README.md b/x/ibc-rate-limit/README.md
index f9a91cb8c..22a41d3d7 100644
--- a/x/ibc-rate-limit/README.md
+++ b/x/ibc-rate-limit/README.md
@@ -1,26 +1,119 @@
-# # IBC Rate Limit
+# IBC Rate Limit
 
-The ``IBC Rate Limit`` middleware implements an [IBC Middleware](https://github.com/cosmos/ibc-go/blob/f57170b1d4dd202a3c6c1c61dcf302b6a9546405/docs/ibc/middleware/develop.md) 
-that wraps a [transfer](https://ibc.cosmos.network/main/apps/transfer/overview.html) app to regulate how much value can
-flow in and out of the chain for a specific denom and channel.
+The IBC Rate Limit module is responsible for adding a governance-configurable rate limit to IBC transfers.
+This is a safety control, intended to protect assets on osmosis in event of:
 
-## Contents
+* a bug/hack on osmosis
+* a bug/hack on the counter-party chain
+* a bug/hack in IBC itself
 
-1. **[Concepts](#concepts)**
-2. **[Parameters](#parameters)**
-3. **[Contract](#contract)**
-4. **[Integration](#integration)**
+This is done in exchange for a potential (one-way) bridge liveness tradeoff, in periods of high deposits or withdrawals.
 
-## Concepts
+The architecture of this package is a minimal go package which implements an [IBC Middleware](https://github.com/cosmos/ibc-go/blob/f57170b1d4dd202a3c6c1c61dcf302b6a9546405/docs/ibc/middleware/develop.md) that wraps the [ICS20 transfer](https://ibc.cosmos.network/main/apps/transfer/overview.html) app, and calls into a cosmwasm contract.
+The cosmwasm contract then has all of the actual IBC rate limiting logic.
+The Cosmwasm code can be found in the [`contracts`](./contracts/) package, with bytecode findable in the [`bytecode`](./bytecode/) folder. The cosmwasm VM usage allows Osmosis chain governance to choose to change this safety control with no hard forks, via a parameter change proposal, a great mitigation for faster threat adaptavity.
 
-### Overview
+The status of the module is being in a state suitable for some initial governance settable rate limits for high value bridged assets.
+Its not in its long term / end state for all channels by any means, but does act as a strong protection we
+can instantiate today for high value IBC connections.
 
-The `x/ibc-rate-limit` module implements an IBC middleware and a transfer app wrapper. The middleware checks if the 
-amount of value of a specific denom transferred through a channel has exceeded a quota defined by governance for 
-that channel/denom. These checks are handled through a CosmWasm contract. The contract to be used for this is 
-configured via a parameter.
+## Motivation
 
-### Middleware
+The motivation of IBC-rate-limit comes from the empirical observations of blockchain bridge hacks that a rate limit would have massively reduced the stolen amount of assets in:
+
+- [Polynetwork Bridge Hack ($611 million)](https://rekt.news/polynetwork-rekt/)
+- [BNB Bridge Hack ($586 million)](https://rekt.news/bnb-bridge-rekt/)
+- [Wormhole Bridge Hack ($326 million)](https://rekt.news/wormhole-rekt/)
+- [Nomad Bridge Hack ($190 million)](https://rekt.news/nomad-rekt/)
+- [Harmony Bridge Hack ($100 million)](https://rekt.news/harmony-rekt/) - (Would require rate limit + monitoring)
+- [Dragonberry IBC bug](https://forum.cosmos.network/t/ibc-security-advisory-dragonberry/7702) (can't yet disclose amount at risk, but was saved due to being found first by altruistic Osmosis core developers)
+
+In the presence of a software bug on Osmosis, IBC itself, or on a counterparty chain, we would like to prevent the bridge from being fully depegged.
+This stems from the idea that a 30% asset depeg is ~infinitely better than a 100% depeg.
+Its _crazy_ that today these complex bridged assets can instantly go to 0 in event of bug.
+The goal of a rate limit is to raise an alert that something has potentially gone wrong, allowing validators and developers to have time to analyze, react, and protect larger portions of user funds.
+
+The thesis of this is that, it is worthwile to sacrifice liveness in the case of legitimate demand to send extreme amounts of funds, to prevent the terrible long-tail full fund risks.
+Rate limits aren't the end-all of safety controls, they're merely the simplest automated one. More should be explored and added onto IBC!
+
+## Rate limit types
+
+We express rate limits in time-based periods.
+This means, we set rate limits for (say) 6-hour, daily, and weekly intervals.
+The rate limit for a given time period stores the relevant amount of assets at the start of the rate limit.
+Rate limits are then defined on percentage terms of the asset.
+The time windows for rate limits are currently _not_ rolling, they have discrete start/end times.
+
+We allow setting separate rate limits for the inflow and outflow of assets.
+We do all of our rate limits based on the _net flow_ of assets on a channel pair. This prevents DOS issues, of someone repeatedly sending assets back and forth, to trigger rate limits and break liveness.
+
+We currently envision creating two kinds of rate limits:
+
+* Per denomination rate limits
+   - allows safety statements like "Only 30% of Stars on Osmosis can flow out in one day" or "The amount of Atom on Osmosis can at most double per day".
+* Per channel rate limits
+   - Limit the total inflow and outflow on a given IBC channel, based on "USDC" equivalent, using Osmosis as the price oracle.
+
+We currently only implement per denomination rate limits for non-native assets. We do not yet implement channel based rate limits.
+
+Currently these rate limits automatically "expire" at the end of the quota duration. TODO: Think of better designs here. E.g. can we have a constant number of subsequent quotas start filled? Or perhaps harmonically decreasing amounts of next few quotas pre-filled? Halted until DAO override seems not-great.
+
+## Instantiating rate limits
+
+Today all rate limit quotas must be set manually by governance.
+In the future, we should design towards some conservative rate limit to add as a safety-backstop automatically for channels.
+Ideas for how this could look:
+
+* One month after a channel has been created, automatically add in some USDC-based rate limit
+* One month after governance incentivizes an asset, add on a per-denomination rate limit.
+
+Definitely needs far more ideation and iteration!
+
+## Parameterizing the rate limit
+
+One element is we don't want any rate limit timespan thats too short, e.g. not enough time for humans to react to. So we wouldn't want a 1 hour rate limit, unless we think that if its hit, it could be assessed within an hour.
+
+### Handling rate limit boundaries
+
+We want to be safe against the case where say we have a daily rate limit ending at a given time, and an adversary attempts to attack near the boundary window.
+We would not like them to be able to "double extract funds" by timing their extraction near a window boundary.
+
+Admittedly, not a lot of thought has been put into how to deal with this well.
+Right now we envision simply handling this by saying if you want a quota of duration D, instead include two quotas of duration D, but offset by `D/2` from each other.
+
+Ideally we can change windows to be more 'rolling' in the future, to avoid this overhead and more cleanly handle the problem. (Perhaps rolling ~1 hour at a time)
+
+### Inflow parameterization
+
+The "Inflow" side of a rate limit is essentially protection against unforeseen bug on a counterparty chain.
+This can be quite conservative (e.g. bridged amount doubling in one week). This covers a few cases:
+
+* Counter-party chain B having a token theft attack
+   - TODO: description of how this looks
+* Counter-party chain B runaway mint
+   - TODO: description of how this looks
+* IBC theft
+   - TODO: description of how this looks
+
+It does get more complex when the counterparty chain is itself a DEX, but this is still much more protection than nothing.
+
+### Outflow parameterization
+
+The "Outflow" side of a rate limit is protection against a bug on Osmosis OR IBC.
+This has potential for much more user-frustrating issues, if set too low.
+E.g. if theres some event that causes many people to suddenly withdraw many STARS or many USDC.
+
+So this parameterization has to contend with being a tradeoff of withdrawal liveness in high volatility periods vs being a crucial safety rail, in event of on-Osmosis bug.
+
+TODO: Better fill out
+
+### Example suggested parameterization
+
+## Code structure
+
+As mentioned at the beginning of the README, the go code is a relatively minimal ICS 20 wrapper, that dispatches relevant calls to a cosmwasm contract that implements the rate limiting functionality.
+
+### Go Middleware
 
 To achieve this, the middleware  needs to implement  the `porttypes.Middleware` interface and the
 `porttypes.ICS4Wrapper` interface. This allows the middleware to send and receive IBC messages by wrapping 
@@ -28,23 +121,14 @@ any IBC module, and be used as an ICS4 wrapper by a transfer module (for sending
 
 Of those interfaces, just the following methods have custom logic:
 
-* `ICS4Wrapper.SendPacket` adds tracking of value sent via an ibc channel 
-* `Middleware.OnRecvPacket` adds tracking of value received via an ibc channel 
-* `Middleware.OnAcknowledgementPacket` undos the tracking of a sent packet if the acknowledgment is not a success
-* `OnTimeoutPacket` undos the tracking of a sent packet if the packet times out (is not relayed)
+* `ICS4Wrapper.SendPacket` forwards to contract, with intent of tracking of value sent via an ibc channel 
+* `Middleware.OnRecvPacket` forwards to contract, with intent of tracking of value received via an ibc channel 
+* `Middleware.OnAcknowledgementPacket` forwards to contract, with intent of undoing the tracking of a sent packet if the acknowledgment is not a success
+* `OnTimeoutPacket` forwards to contract, with intent of undoing the tracking of a sent packet if the packet times out (is not relayed)
 
 All other methods from those interfaces are passthroughs to the underlying implementations.
 
-### Contract Concepts
-
-The tracking contract uses the following concepts
-
-1. **RateLimit** - tracks the value flow transferred and the quota for a path.
-2. **Path** - is a (denom, channel) pair.
-3. **Flow** - tracks the value that has moved through a path during the current time window.
-4. **Quota** - is the percentage of the denom's total value that can be transferred through the path in a given period of time (duration)
-
-## Parameters
+#### Parameters
 
 The middleware uses the following parameters:
 
@@ -55,23 +139,45 @@ The middleware uses the following parameters:
 1. **ContractAddress** -
    The contract address is the address of an instantiated version of the contract provided under `./contracts/`
 
-## Contract
+### Cosmwasm Contract Concepts
+
+Something to keep in mind with all of the code, is that we have to reason separately about every item in the following matrix:
+
+|     Native Token     |     Non-Native Token     |
+|----------------------|--------------------------|
+| Send Native Token    | Send Non-Native Token    |
+| Receive Native Token | Receive Non-Native Token |
+| Timeout Native Send  | Timeout Non-native Send  |
 
-### Messages
+(Error ACK can reuse the same code as timeout)
+
+TODO: Spend more time on sudo messages in the following description. We need to better describe how we map the quota concepts onto the code.
+Need to describe how we get the quota beginning balance, and that its different for sends and receives.
+Explain intracacies of tracking that a timeout and/or ErrorAck must appear from the same quota, else we ignore its update to the quotas.
+
+
+The tracking contract uses the following concepts
+
+1. **RateLimit** - tracks the value flow transferred and the quota for a path.
+2. **Path** - is a (denom, channel) pair.
+3. **Flow** - tracks the value that has moved through a path during the current time window.
+4. **Quota** - is the percentage of the denom's total value that can be transferred through the path in a given period of time (duration)
+
+#### Messages
 
 The contract specifies the following messages:
 
-#### Query
+##### Query
 
 * GetQuotas - Returns the quotas for a path
 
-#### Exec
+##### Exec
 
 * AddPath - Adds a list of quotas for a path
 * RemovePath - Removes a path
 * ResetPathQuota - If a rate limit has been reached, the contract's governance address can reset the quota so that transfers are allowed again
 
-#### Sudo
+##### Sudo
 
 Sudo messages can only be executed by the chain.
 
@@ -79,7 +185,7 @@ Sudo messages can only be executed by the chain.
 * RecvPacket - Increments the amount used out of the receive quota and checks that the receive is allowed. If it isn't, it will return a RateLimitExceeded error
 * UndoSend - If a send has failed, the undo message is used to remove its cost from the send quota
 
-## Integration
+### Integration
 
 The rate limit middleware wraps the `transferIBCModule` and is added as the entry route for IBC transfers.
 
@@ -87,3 +193,30 @@ The module is also provided to the underlying `transferIBCModule` as its `ICS4Wr
 pointed to a channel, which also implements the `ICS4Wrapper` interface.
 
 This integration can be seen in [osmosis/app/keepers/keepers.go](https://github.com/osmosis-labs/osmosis/blob/main/app/keepers/keepers.go)
+
+## Testing strategy
+
+TODO: Fill this out
+
+## Known Future work
+
+Items that've been highlighted above:
+
+* Making automated rate limits get added for channels, instead of manual configuration only
+* Improving parameterization strategies / data analysis
+* Adding the USDC based rate limits
+* We need better strategies for how rate limits "expire".
+
+Not yet highlighted
+
+* Making monitoring tooling to know when approaching rate limiting and when they're hit
+* Making tooling to easily give us summaries we can use, to reason about "bug or not bug" in event of rate limit being hit
+* Enabling ways to pre-declare large transfers so as to not hit rate limits.
+   * Perhaps you can on-chain declare intent to send these assets with a large delay, that raises monitoring but bypasses rate limits?
+   * Maybe contract-based tooling to split up the transfer suffices?
+* Strategies to account for high volatility periods without hitting rate limits
+   * Can imagine "Hop network" style markets emerging
+   * Could imagine tieng it into looking at AMM volatility, or off-chain oracles
+      * but these are both things we should be wary of security bugs in.
+      * Maybe [constraint based programming with tracking of provenance](https://youtu.be/HB5TrK7A4pI?t=2852) as a solution
+* Analyze changing denom-based rate limits, to just overall withdrawal amount for Osmosis
\ No newline at end of file


Test patch:
diff --git a/osmomath/decimal_test.go b/osmomath/decimal_test.go
index cabf1409c..11732ac29 100644
--- a/osmomath/decimal_test.go
+++ b/osmomath/decimal_test.go
@@ -739,3 +739,282 @@ func (s *decimalTestSuite) TestLog2() {
 		})
 	}
 }
+
+func (s *decimalTestSuite) TestLn() {
+	var expectedErrTolerance = MustNewDecFromStr("0.000000000000000000000000000000000100")
+
+	tests := map[string]struct {
+		initialValue BigDec
+		expected     BigDec
+
+		expectedPanic bool
+	}{
+		"log_e{-1}; invalid; panic": {
+			initialValue:  OneDec().Neg(),
+			expectedPanic: true,
+		},
+		"log_e{0}; invalid; panic": {
+			initialValue:  ZeroDec(),
+			expectedPanic: true,
+		},
+		"log_e{0.001} = -6.90775527898213705205397436405309262": {
+			initialValue: MustNewDecFromStr("0.001"),
+			// From: https://www.wolframalpha.com/input?i=log0.001+to+36+digits+with+36+decimals
+			expected: MustNewDecFromStr("-6.90775527898213705205397436405309262"),
+		},
+		"log_e{0.56171821941421412902170941} = -0.576754943768592057376050794884207180": {
+			initialValue: MustNewDecFromStr("0.56171821941421412902170941"),
+			// From: https://www.wolframalpha.com/input?i=log0.56171821941421412902170941+to+36+digits
+			expected: MustNewDecFromStr("-0.576754943768592057376050794884207180"),
+		},
+		"log_e{0.999912345} = -0.000087658841924023373535614212850888": {
+			initialValue: MustNewDecFromStr("0.999912345"),
+			// From: https://www.wolframalpha.com/input?i=log0.999912345+to+32+digits
+			expected: MustNewDecFromStr("-0.000087658841924023373535614212850888"),
+		},
+		"log_e{1} = 0": {
+			initialValue: NewBigDec(1),
+			expected:     NewBigDec(0),
+		},
+		"log_e{e} = 1": {
+			initialValue: MustNewDecFromStr("2.718281828459045235360287471352662498"),
+			// From: https://www.wolframalpha.com/input?i=e+with+36+decimals
+			expected: NewBigDec(1),
+		},
+		"log_e{7} = 1.945910149055313305105352743443179730": {
+			initialValue: NewBigDec(7),
+			// From: https://www.wolframalpha.com/input?i=log7+up+to+36+decimals
+			expected: MustNewDecFromStr("1.945910149055313305105352743443179730"),
+		},
+		"log_e{512} = 6.238324625039507784755089093123589113": {
+			initialValue: NewBigDec(512),
+			// From: https://www.wolframalpha.com/input?i=log512+up+to+36+decimals
+			expected: MustNewDecFromStr("6.238324625039507784755089093123589113"),
+		},
+		"log_e{580} = 6.36302810354046502061849560850445238": {
+			initialValue: NewBigDec(580),
+			// From: https://www.wolframalpha.com/input?i=log580+up+to+36+decimals
+			expected: MustNewDecFromStr("6.36302810354046502061849560850445238"),
+		},
+		"log_e{1024.987654321} = 6.93243584693509415029056534690631614": {
+			initialValue: NewDecWithPrec(1024987654321, 9),
+			// From: https://www.wolframalpha.com/input?i=log1024.987654321+to+36+digits
+			expected: MustNewDecFromStr("6.93243584693509415029056534690631614"),
+		},
+		"log_e{912648174127941279170121098210.92821920190204131121} = 68.986147965719214790400745338243805015": {
+			initialValue: MustNewDecFromStr("912648174127941279170121098210.92821920190204131121"),
+			// From: https://www.wolframalpha.com/input?i=log912648174127941279170121098210.92821920190204131121+to+38+digits
+			expected: MustNewDecFromStr("68.986147965719214790400745338243805015"),
+		},
+	}
+
+	for name, tc := range tests {
+		s.Run(name, func() {
+			osmoassert.ConditionalPanic(s.T(), tc.expectedPanic, func() {
+				// Create a copy to test that the original was not modified.
+				// That is, that Ln() is non-mutative.
+				initialCopy := ZeroDec()
+				initialCopy.i.Set(tc.initialValue.i)
+
+				// system under test.
+				res := tc.initialValue.Ln()
+				require.True(DecApproxEq(s.T(), tc.expected, res, expectedErrTolerance))
+				require.Equal(s.T(), initialCopy, tc.initialValue)
+			})
+		})
+	}
+}
+
+func (s *decimalTestSuite) TestTickLog() {
+	tests := map[string]struct {
+		initialValue BigDec
+		expected     BigDec
+
+		expectedErrTolerance BigDec
+		expectedPanic        bool
+	}{
+		"log_1.0001{-1}; invalid; panic": {
+			initialValue:  OneDec().Neg(),
+			expectedPanic: true,
+		},
+		"log_1.0001{0}; invalid; panic": {
+			initialValue:  ZeroDec(),
+			expectedPanic: true,
+		},
+		"log_1.0001{0.001} = -69081.006609899112313305835611219486392199": {
+			initialValue: MustNewDecFromStr("0.001"),
+			// From: https://www.wolframalpha.com/input?i=log_1.0001%280.001%29+to+41+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000143031879"),
+			expected:             MustNewDecFromStr("-69081.006609899112313305835611219486392199"),
+		},
+		"log_1.0001{0.999912345} = -0.876632247930741919880461740717176538": {
+			initialValue: MustNewDecFromStr("0.999912345"),
+			// From: https://www.wolframalpha.com/input?i=log_1.0001%280.999912345%29+to+36+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000138702"),
+			expected:             MustNewDecFromStr("-0.876632247930741919880461740717176538"),
+		},
+		"log_1.0001{1} = 0": {
+			initialValue: NewBigDec(1),
+
+			expectedErrTolerance: ZeroDec(),
+			expected:             NewBigDec(0),
+		},
+		"log_1.0001{1.0001} = 1": {
+			initialValue: MustNewDecFromStr("1.0001"),
+
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000152500"),
+			expected:             OneDec(),
+		},
+		"log_1.0001{512} = 62386.365360724158196763710649998441051753": {
+			initialValue: NewBigDec(512),
+			// From: https://www.wolframalpha.com/input?i=log_1.0001%28512%29+to+41+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000129292137"),
+			expected:             MustNewDecFromStr("62386.365360724158196763710649998441051753"),
+		},
+		"log_1.0001{1024.987654321} = 69327.824629506998657531621822514042777198": {
+			initialValue: NewDecWithPrec(1024987654321, 9),
+			// From: https://www.wolframalpha.com/input?i=log_1.0001%281024.987654321%29+to+41+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000143836264"),
+			expected:             MustNewDecFromStr("69327.824629506998657531621822514042777198"),
+		},
+		"log_1.0001{912648174127941279170121098210.92821920190204131121} = 689895.972156319183538389792485913311778672": {
+			initialValue: MustNewDecFromStr("912648174127941279170121098210.92821920190204131121"),
+			// From: https://www.wolframalpha.com/input?i=log_1.0001%28912648174127941279170121098210.92821920190204131121%29+to+42+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000001429936067"),
+			expected:             MustNewDecFromStr("689895.972156319183538389792485913311778672"),
+		},
+	}
+
+	for name, tc := range tests {
+		s.Run(name, func() {
+			osmoassert.ConditionalPanic(s.T(), tc.expectedPanic, func() {
+				// Create a copy to test that the original was not modified.
+				// That is, that Ln() is non-mutative.
+				initialCopy := ZeroDec()
+				initialCopy.i.Set(tc.initialValue.i)
+
+				// system under test.
+				res := tc.initialValue.TickLog()
+				fmt.Println(name, res.Sub(tc.expected).Abs())
+				require.True(DecApproxEq(s.T(), tc.expected, res, tc.expectedErrTolerance))
+				require.Equal(s.T(), initialCopy, tc.initialValue)
+			})
+		})
+	}
+}
+
+func (s *decimalTestSuite) TestCustomBaseLog() {
+	tests := map[string]struct {
+		initialValue BigDec
+		base         BigDec
+
+		expected             BigDec
+		expectedErrTolerance BigDec
+
+		expectedPanic bool
+	}{
+		"log_2{-1}: normal base, invalid argument - panics": {
+			initialValue:  NewBigDec(-1),
+			base:          NewBigDec(2),
+			expectedPanic: true,
+		},
+		"log_2{0}: normal base, invalid argument - panics": {
+			initialValue:  NewBigDec(0),
+			base:          NewBigDec(2),
+			expectedPanic: true,
+		},
+		"log_(-1)(2): invalid base, normal argument - panics": {
+			initialValue:  NewBigDec(2),
+			base:          NewBigDec(-1),
+			expectedPanic: true,
+		},
+		"log_1(2): base cannot equal to 1 - panics": {
+			initialValue:  NewBigDec(2),
+			base:          NewBigDec(1),
+			expectedPanic: true,
+		},
+		"log_30(100) = 1.353984985057691049642502891262784015": {
+			initialValue: NewBigDec(100),
+			base:         NewBigDec(30),
+			// From: https://www.wolframalpha.com/input?i=log_30%28100%29+to+37+digits
+			expectedErrTolerance: ZeroDec(),
+			expected:             MustNewDecFromStr("1.353984985057691049642502891262784015"),
+		},
+		"log_0.2(0.99) = 0.006244624769837438271878639001855450": {
+			initialValue: MustNewDecFromStr("0.99"),
+			base:         MustNewDecFromStr("0.2"),
+			// From: https://www.wolframalpha.com/input?i=log_0.2%280.99%29+to+34+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000000013"),
+			expected:             MustNewDecFromStr("0.006244624769837438271878639001855450"),
+		},
+
+		"log_0.0001(500000) = -1.424742501084004701196565276318876743": {
+			initialValue: NewBigDec(500000),
+			base:         NewDecWithPrec(1, 4),
+			// From: https://www.wolframalpha.com/input?i=log_0.0001%28500000%29+to+37+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000000003"),
+			expected:             MustNewDecFromStr("-1.424742501084004701196565276318876743"),
+		},
+
+		"log_500000(0.0001) = -0.701881216598197542030218906945601429": {
+			initialValue: NewDecWithPrec(1, 4),
+			base:         NewBigDec(500000),
+			// From: https://www.wolframalpha.com/input?i=log_500000%280.0001%29+to+36+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000000001"),
+			expected:             MustNewDecFromStr("-0.701881216598197542030218906945601429"),
+		},
+
+		"log_10000(5000000) = 1.674742501084004701196565276318876743": {
+			initialValue: NewBigDec(5000000),
+			base:         NewBigDec(10000),
+			// From: https://www.wolframalpha.com/input?i=log_10000%285000000%29+to+37+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000000002"),
+			expected:             MustNewDecFromStr("1.674742501084004701196565276318876743"),
+		},
+		"log_0.123456789(1) = 0": {
+			initialValue: OneDec(),
+			base:         MustNewDecFromStr("0.123456789"),
+
+			expectedErrTolerance: ZeroDec(),
+			expected:             ZeroDec(),
+		},
+		"log_1111(1111) = 1": {
+			initialValue: NewBigDec(1111),
+			base:         NewBigDec(1111),
+
+			expectedErrTolerance: ZeroDec(),
+			expected:             OneDec(),
+		},
+
+		"log_1.123{1024.987654321} = 59.760484327223888489694630378785099461": {
+			initialValue: NewDecWithPrec(1024987654321, 9),
+			base:         NewDecWithPrec(1123, 3),
+			// From: https://www.wolframalpha.com/input?i=log_1.123%281024.987654321%29+to+38+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000007686"),
+			expected:             MustNewDecFromStr("59.760484327223888489694630378785099461"),
+		},
+
+		"log_1.123{912648174127941279170121098210.92821920190204131121} = 594.689327867863079177915648832621538986": {
+			initialValue: MustNewDecFromStr("912648174127941279170121098210.92821920190204131121"),
+			base:         NewDecWithPrec(1123, 3),
+			// From: https://www.wolframalpha.com/input?i=log_1.123%28912648174127941279170121098210.92821920190204131121%29+to+39+digits
+			expectedErrTolerance: MustNewDecFromStr("0.000000000000000000000000000000077705"),
+			expected:             MustNewDecFromStr("594.689327867863079177915648832621538986"),
+		},
+	}
+	for name, tc := range tests {
+		s.Run(name, func() {
+			osmoassert.ConditionalPanic(s.T(), tc.expectedPanic, func() {
+				// Create a copy to test that the original was not modified.
+				// That is, that Ln() is non-mutative.
+				initialCopy := ZeroDec()
+				initialCopy.i.Set(tc.initialValue.i)
+
+				// system under test.
+				res := tc.initialValue.CustomBaseLog(tc.base)
+				require.True(DecApproxEq(s.T(), tc.expected, res, tc.expectedErrTolerance))
+				require.Equal(s.T(), initialCopy, tc.initialValue)
+			})
+		})
+	}
+}
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index ec98a2908..97be96903 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -162,7 +162,7 @@ func (s *IntegrationTestSuite) TestIBCTokenTransferRateLimiting() {
 	// 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")
+	err = copyFile(projectDir+"/x/ibc-rate-limit/bytecode/rate_limiter.wasm", wd+"/scripts/rate_limiter.wasm")
 	s.NoError(err)
 	node.StoreWasmCode("rate_limiter.wasm", initialization.ValidatorWalletName)
 	chainA.LatestCodeId += 1
diff --git a/x/ibc-rate-limit/testdata/rate_limiter.wasm b/x/ibc-rate-limit/bytecode/rate_limiter.wasm
similarity index 100%
rename from x/ibc-rate-limit/testdata/rate_limiter.wasm
rename to x/ibc-rate-limit/bytecode/rate_limiter.wasm
diff --git a/x/ibc-rate-limit/testutil/wasm.go b/x/ibc-rate-limit/testutil/wasm.go
index 2beabb9c0..e95060597 100644
--- a/x/ibc-rate-limit/testutil/wasm.go
+++ b/x/ibc-rate-limit/testutil/wasm.go
@@ -19,7 +19,7 @@ func (chain *TestChain) StoreContractCode(suite *suite.Suite) {
 	osmosisApp := chain.GetOsmosisApp()
 
 	govKeeper := osmosisApp.GovKeeper
-	wasmCode, err := ioutil.ReadFile("./testdata/rate_limiter.wasm")
+	wasmCode, err := ioutil.ReadFile("./bytecode/rate_limiter.wasm")
 	suite.Require().NoError(err)
 
 	addr := osmosisApp.AccountKeeper.GetModuleAddress(govtypes.ModuleName)