Skip to content

Conversation

iboss-ptk
Copy link
Contributor

Closes: CHAIN-708

What is the purpose of the change

According to the document, anyone can create a token and set a send hook, where BlockBeforeSend can cause bankKeeper.SendCoins to return an error.

https://docs.osmosis.zone/overview/features/tokenfactory#bank-hooks-trackbeforesend-blockbeforesend

 That is, any error triggered by the BlockBeforeSend hook implementation would cancel the state transition and, consequently, the send itself, while any error omitted from TrackBeforeSend would be gracefully silenced.

func (k BaseSendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error {
    // BlockBeforeSend hook should always be called before the TrackBeforeSend hook.
    err := k.BlockBeforeSend(ctx, fromAddr, toAddr, amt)
    if err != nil {
        return err
    }

    return k.sendCoins(ctx, fromAddr, toAddr, amt)

}

In the OnRecvPacket of ibc transfer, when the chain reclaims the previously cross-chained token via ibc, it will call unescrowCoin and then call bankKeeper.SendCoins.

escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
if err := k.unescrowCoin(ctx, escrowAddress, receiver, coin); err != nil {
return err
}
// unescrowCoin will send the given coin from the escrow address to the provided receiver. It will also
// update the total escrow by deducting the unescrowed coin's amount from the current total escrow.
func (k Keeper) unescrowCoin(ctx context.Context, escrowAddress, receiver sdk.AccAddress, coin sdk.Coin) error {
if err := k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(coin)); err != nil {
// NOTE: this error is only expected to occur given an unexpected bug or a malicious
// counterparty module. The bug may occur in bank or any part of the code that allows
// the escrow address to be drained. A malicious counterparty module could drain the
// escrow address by allowing more tokens to be sent back then were escrowed.
return errorsmod.Wrap(err, "unable to unescrow tokens, this may be caused by a malicious counterparty module or a bug: please open an issue on counterparty module")
}

Then, an attacker can set a send hook to make unescrowCoin return an error, causing the relayer's message execution to fail. The relayer will repeatedly attempt the failed message, especially when the channel is ordered, eventually rendering the channel unusable and the relayer being subjected to a DoS attack.

Testing and Verifying

Unit tested

Documentation and Release Note

  • Does this pull request introduce a new feature or user-facing behavior changes?
  • Changelog entry added to Unreleased section of CHANGELOG.md?

Where is the change documented?

  • Specification (x/{module}/README.md)
  • Osmosis documentation site
  • Code comments?
  • N/A

@github-actions github-actions bot added C:app-wiring Changes to the app folder C:x/tokenfactory labels Sep 26, 2025
@iboss-ptk iboss-ptk added V:state/breaking State machine breaking PR and removed C:app-wiring Changes to the app folder C:x/tokenfactory labels Sep 26, 2025
@github-actions github-actions bot added C:app-wiring Changes to the app folder C:x/tokenfactory labels Sep 26, 2025
Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Walkthrough

TokenFactory keeper now depends on an IBC ChannelKeeper. Keeper struct and constructor were extended to include ChannelKeeper. Before-send hook logic now suppresses errors when the sender is an IBC escrow address. Tests added for escrow-skip behavior and gas expectation adjusted. Types add ChannelKeeper interface.

Changes

Cohort / File(s) Summary
App wiring: inject ChannelKeeper
app/keepers/keepers.go
Passes appKeepers.IBCKeeper.ChannelKeeper into tokenfactorykeeper.NewKeeper, updating initialization to include the new dependency.
Keeper struct and constructor API
x/tokenfactory/keeper/keeper.go
Adds channelKeeper types.ChannelKeeper field to Keeper. Extends NewKeeper(...) to accept channelKeeper and assigns it.
Before-send hook: IBC escrow handling
x/tokenfactory/keeper/before_send.go
Adds IsIBCEscrowAddress(ctx, addr) that scans ChannelKeeper.GetAllChannels and compares transfertypes.GetEscrowAddress. Updates before-send flow to suppress errors when sender is an IBC escrow address; includes nil-check on channelKeeper.
Tests for escrow skip and gas change
x/tokenfactory/keeper/before_send_test.go
Adds TestSkipBeforeSendHookForIBCEscrowAddress, sets up mock channel, validates escrow-originated sends bypass hook errors; adjusts gas expectation from 501435 to 501465; imports IBC types.
Expected keepers interface
x/tokenfactory/types/expected_keepers.go
Introduces ChannelKeeper interface with GetAllChannels(ctx) []channeltypes.IdentifiedChannel for dependency injection.
Changelog
CHANGELOG.md
Notes "skip IBC escrow address beforesend hook" under Unreleased.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as AppKeepers
  participant TF as TokenFactoryKeeper
  participant IBC as IBC ChannelKeeper

  App->>TF: NewKeeper(..., channelKeeper=IBC, ...)
  Note over TF: Keeper now holds ChannelKeeper reference
Loading
sequenceDiagram
  autonumber
  participant Sender as Sender
  participant TF as TokenFactoryKeeper
  participant IBC as ChannelKeeper
  participant WASM as BeforeSendContract

  Sender->>TF: TrackBeforeSend / callBeforeSendListener
  alt channelKeeper available
    TF->>IBC: GetAllChannels()
    IBC-->>TF: [IdentifiedChannel...]
    TF->>TF: IsIBCEscrowAddress(addr)?
    alt Sender is IBC escrow
      Note over TF: Suppress before-send errors
      TF-->>Sender: nil (skip hook error)
    else Not escrow
      TF->>WASM: Execute before-send hook
      alt Contract error
        TF-->>Sender: propagate error (unless module/no-contract)
      else Success
        TF-->>Sender: nil
      end
    end
  else No channelKeeper
    TF->>WASM: Execute before-send hook (previous behavior)
    TF-->>Sender: result
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely summarizes the primary change by indicating that the before-send hook will be skipped for IBC escrow addresses, which directly reflects the core fix implemented in this pull request. It avoids unnecessary details and uses precise terminology that aligns with the changeset.
Description Check ✅ Passed The pull request description adheres to the repository’s template by including the Closes line, a detailed purpose section, Testing and Verifying, and the Documentation and Release Note checklist, providing clear context and rationale for the change.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch boss/skip-ibc-escrow-beforesend

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 807fc78 and 5fbf22d.

📒 Files selected for processing (1)
  • CHANGELOG.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
x/tokenfactory/keeper/before_send.go (1)

203-220: Minor: filter channels by port to cut unnecessary iterations.

Only transfer port channels produce escrow addresses. Skip others for small savings.

 func (k Keeper) IsIBCEscrowAddress(ctx sdk.Context, addr sdk.AccAddress) bool {
@@
-	for _, channel := range channels {
+	for _, channel := range channels {
+		if channel.PortId != transfertypes.PortID {
+			continue
+		}
 		escrowAddr := transfertypes.GetEscrowAddress(channel.PortId, channel.ChannelId)
 		if escrowAddr.Equals(addr) {
 			return true
 		}
 	}
x/tokenfactory/keeper/before_send_test.go (1)

318-373: Escrow skip test: good coverage.

The channel setup and escrow address assertions exercise the intended path well. After implementing the early short‑circuit, consider asserting that no contract execution happens (if you can stub/meter it) to prevent regressions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc2ca45 and 807fc78.

📒 Files selected for processing (5)
  • app/keepers/keepers.go (1 hunks)
  • x/tokenfactory/keeper/before_send.go (2 hunks)
  • x/tokenfactory/keeper/before_send_test.go (2 hunks)
  • x/tokenfactory/keeper/keeper.go (3 hunks)
  • x/tokenfactory/types/expected_keepers.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
x/tokenfactory/keeper/keeper.go (1)
x/tokenfactory/types/expected_keepers.go (1)
  • ChannelKeeper (47-49)
x/tokenfactory/keeper/before_send_test.go (2)
x/tokenfactory/types/msgs.go (2)
  • NewMsgCreateDenom (26-31)
  • NewMsgSetBeforeSendHook (248-254)
x/tokenfactory/types/expected_keepers.go (1)
  • ChannelKeeper (47-49)
app/keepers/keepers.go (2)
x/tokenfactory/types/expected_keepers.go (1)
  • ChannelKeeper (47-49)
x/ibc-hooks/types/expected_keepers.go (1)
  • ChannelKeeper (10-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: go-split-test-files
  • GitHub Check: e2e
  • GitHub Check: Run golangci-lint
  • GitHub Check: test
  • GitHub Check: Summary
🔇 Additional comments (4)
x/tokenfactory/types/expected_keepers.go (1)

8-9: ChannelKeeper interface addition looks correct.

Minimal surface area and sdk.Context usage are appropriate. Please just confirm ibc-go v8 ChannelKeeper exposes GetAllChannels with this exact signature.

Also applies to: 46-49

x/tokenfactory/keeper/keeper.go (1)

32-33: Keeper now depends on ChannelKeeper — dependency injection is fine.

Field, constructor param, and assignment are consistent. Nil-safety is handled by callers via IsIBCEscrowAddress.

Also applies to: 44-45, 66-67

x/tokenfactory/keeper/before_send_test.go (1)

315-316: Gas number fragility.

Hard-coding exact gas deltas can be brittle across toolchain/runtime bumps. If this starts flaking, prefer asserting within a tight range.

app/keepers/keepers.go (1)

536-544: Approve ChannelKeeper wiring in TokenFactoryKeeper
Verified there are no other tokenfactorykeeper.NewKeeper call sites needing the new ChannelKeeper parameter.

Comment on lines +191 to +194
// Suppress errors when sending from IBC escrow addresses to prevent DoS attacks
if k.IsIBCEscrowAddress(ctx, from) {
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Short‑circuit: skip calling the hook entirely for IBC escrow senders.

Currently, the contract is still executed and errors are suppressed. This wastes up to BeforeSendHookGasLimit per coin and can amplify load during IBC unescrow. Skip the call up-front for escrow addresses (both Track and Block) and compute escrow once per call to avoid repeated channel scans.

Apply this diff:

 func (k Keeper) callBeforeSendListener(context context.Context, from, to sdk.AccAddress, amount sdk.Coins, blockBeforeSend bool) (err error) {
@@
-	ctx := sdk.UnwrapSDKContext(context)
+	ctx := sdk.UnwrapSDKContext(context)
+	// Compute once to avoid repeated channel scans
+	isEscrow := k.IsIBCEscrowAddress(ctx, from)
 	for _, coin := range amount {
 		cosmwasmAddress := k.GetBeforeSendHook(ctx, coin.Denom)
 		if cosmwasmAddress != "" {
+			// Skip hooks for IBC escrow-sourced sends to avoid DoS and wasted gas
+			if isEscrow {
+				if blockBeforeSend {
+					return nil
+				}
+				// For TrackBeforeSend, just skip processing
+				continue
+			}
 			cwAddr, err := sdk.AccAddressFromBech32(cosmwasmAddress)
 			if err != nil {
 				return err
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Suppress errors when sending from IBC escrow addresses to prevent DoS attacks
if k.IsIBCEscrowAddress(ctx, from) {
return nil
}
func (k Keeper) callBeforeSendListener(
context context.Context,
from, to sdk.AccAddress,
amount sdk.Coins,
blockBeforeSend bool,
) (err error) {
ctx := sdk.UnwrapSDKContext(context)
// Compute once to avoid repeated channel scans
isEscrow := k.IsIBCEscrowAddress(ctx, from)
for _, coin := range amount {
cosmwasmAddress := k.GetBeforeSendHook(ctx, coin.Denom)
if cosmwasmAddress != "" {
// Skip hooks for IBC escrow-sourced sends to avoid DoS and wasted gas
if isEscrow {
if blockBeforeSend {
return nil
}
// For TrackBeforeSend, just skip processing this coin
continue
}
cwAddr, err := sdk.AccAddressFromBech32(cosmwasmAddress)
if err != nil {
return err
}
// … rest of hook invocation logic unchanged …
}
}
return nil
}
🤖 Prompt for AI Agents
In x/tokenfactory/keeper/before_send.go around lines 191-194, avoid executing
the contract hook (and suppressing its errors) for IBC escrow senders and
prevent repeated expensive escrow-address checks per coin: compute a single
boolean isEscrow := k.IsIBCEscrowAddress(ctx, from) once at the start of
BeforeSend handling and if isEscrow is true return nil immediately (skip both
Track and Block hook paths); for non-escrow senders, reuse that boolean and
ensure you do not call IsIBCEscrowAddress inside per-coin loops or twice, so the
hook is only invoked when appropriate and escrow address scanning is done once
per call.

Copy link
Contributor

github-actions bot commented Oct 5, 2025

This pull request has been automatically marked as stale because it has not had any recent activity. It will be closed if no further activity occurs. Thank you!

@github-actions github-actions bot added the Stale label Oct 5, 2025
@github-actions github-actions bot closed this Oct 9, 2025
@iboss-ptk iboss-ptk reopened this Oct 16, 2025
@github-actions github-actions bot removed the Stale label Oct 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C:app-wiring Changes to the app folder C:x/tokenfactory V:state/breaking State machine breaking PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant