Skip to content

Commit be5ccf3

Browse files
authored
chore: denom traces migration handler (#1680)
* update code & test * register migrator service
1 parent 5f5a287 commit be5ccf3

File tree

8 files changed

+265
-66
lines changed

8 files changed

+265
-66
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
5959

6060
### Improvements
6161

62+
* (app/20-transfer) [\#1730](https://github.com/cosmos/ibc-go/pull/1730) parse the ics20 denomination provided via a packet using the channel identifier format specified by ibc-go.
6263
* (cleanup) [\#1335](https://github.com/cosmos/ibc-go/pull/1335/) `gofumpt -w -l .` to standardize the code layout more strictly than `go fmt ./...`
6364
* (middleware) [\#1022](https://github.com/cosmos/ibc-go/pull/1022) Add `GetAppVersion` to the ICS4Wrapper interface. This function should be used by IBC applications to obtain their own version since the version set in the channel structure may be wrapped many times by middleware.
6465
* (modules/core/04-channel) [\#1232](https://github.com/cosmos/ibc-go/pull/1232) Updating params on `NewPacketId` and moving to bottom of file.

docs/migrations/support-denoms-with-slashes.md

+5-29
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ There are four sections based on the four potential user groups of this document
99
- Relayers
1010
- IBC Light Clients
1111

12-
This document is necessary when chains are upgrading from a version that does not support base denoms with slashes (e.g. v3.0.0) to a version that does (e.g. v3.1.0). All versions of ibc-go smaller than v1.5.0 for the v1.x release line, v2.3.0 for the v2.x release line, and v3.1.0 for the v3.x release line do *NOT** support IBC token transfers of coins whose base denoms contain slashes. Therefore the in-place of genesis migration described in this document are required when upgrading.
12+
This document is necessary when chains are upgrading from a version that does not support base denoms with slashes (e.g. v3.0.0) to a version that does (e.g. v3.2.0). All versions of ibc-go smaller than v1.5.0 for the v1.x release line, v2.3.0 for the v2.x release line, and v3.1.0 for the v3.x release line do **NOT** support IBC token transfers of coins whose base denoms contain slashes. Therefore the in-place of genesis migration described in this document are required when upgrading.
1313

1414
If a chain receives coins of a base denom with slashes before it upgrades to supporting it, the receive may pass however the trace information will be incorrect.
1515

@@ -28,41 +28,17 @@ The transfer module will now support slashes in base denoms, so we must iterate
2828
### Upgrade Proposal
2929

3030
```go
31-
// Here the upgrade name is the upgrade name set by the chain
32-
app.UpgradeKeeper.SetUpgradeHandler("supportSlashedDenomsUpgrade",
31+
app.UpgradeKeeper.SetUpgradeHandler("MigrateTraces",
3332
func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
34-
// list of traces that must replace the old traces in store
35-
var newTraces []ibctransfertypes.DenomTrace
36-
app.TransferKeeper.IterateDenomTraces(ctx,
37-
func(dt ibctransfertypes.DenomTrace) bool {
38-
// check if the new way of splitting FullDenom
39-
// into Trace and BaseDenom passes validation and
40-
// is the same as the current DenomTrace.
41-
// If it isn't then store the new DenomTrace in the list of new traces.
42-
newTrace := ibctransfertypes.ParseDenomTrace(dt.GetFullDenomPath())
43-
if err := newTrace.Validate(); err == nil && !equalTraces(newTrace, dt) {
44-
newTraces = append(newTraces, newTrace)
45-
}
46-
47-
return false
48-
})
49-
50-
// replace the outdated traces with the new trace information
51-
for _, nt := range newTraces {
52-
app.TransferKeeper.SetDenomTrace(ctx, nt)
53-
}
54-
33+
// transfer module consensus version has been bumped to 2
5534
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
5635
})
57-
58-
func equalTraces(dtA, dtB ibctransfertypes.DenomTrace) bool {
59-
return dtA.BaseDenom == dtB.BaseDenom && dtA.Path == dtB.Path
60-
}
36+
6137
```
6238

6339
This is only necessary if there are denom traces in the store with incorrect trace information from previously received coins that had a slash in the base denom. However, it is recommended that any chain upgrading to support base denominations with slashes runs this code for safety.
6440

65-
For a more detailed sample, please check out the code changes in [this pull request](https://github.com/cosmos/ibc-go/pull/1527).
41+
For a more detailed sample, please check out the code changes in [this pull request](https://github.com/cosmos/ibc-go/pull/1680).
6642

6743
### Genesis Migration
6844

docs/migrations/v3-to-v4.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,27 @@ No genesis or in-place migrations required when upgrading from v1 or v2 of ibc-g
1818

1919
## Chains
2020

21-
- No relevant changes were made in this release.
21+
### Migration to fix support for base denoms with slashes
22+
23+
As part of [v1.5.0](https://github.com/cosmos/ibc-go/releases/tag/v1.5.0), [v2.3.0](https://github.com/cosmos/ibc-go/releases/tag/v2.3.0) and [v3.1.0](https://github.com/cosmos/ibc-go/releases/tag/v3.1.0) some [migration handler code sample was documented](https://github.com/cosmos/ibc-go/blob/main/docs/migrations/support-denoms-with-slashes.md#upgrade-proposal) that needs to run in order to correct the trace information of coins transferred using ICS20 whose base denom contains slashes.
24+
25+
Based on feedback from the community we add now an improved solution to run the same migration that does not require copying a large piece of code over from the migration document, but instead requires only adding a one-line upgrade handler.
26+
27+
If the chain will migrate to supporting base denoms with slashes, it must set the appropriate params during the execution of the upgrade handler in `app.go`:
28+
```go
29+
app.UpgradeKeeper.SetUpgradeHandler("MigrateTraces",
30+
func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
31+
// transfer module consensus version has been bumped to 2
32+
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
33+
})
34+
35+
```
36+
37+
If a chain receives coins of a base denom with slashes before it upgrades to supporting it, the receive may pass however the trace information will be incorrect.
38+
39+
E.g. If a base denom of `testcoin/testcoin/testcoin` is sent to a chain that does not support slashes in the base denom, the receive will be successful. However, the trace information stored on the receiving chain will be: `Trace: "transfer/{channel-id}/testcoin/testcoin", BaseDenom: "testcoin"`.
40+
41+
This incorrect trace information must be corrected when the chain does upgrade to fully supporting denominations with slashes.
2242

2343
## IBC Apps
2444

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package keeper
2+
3+
import (
4+
"fmt"
5+
6+
sdk "github.com/cosmos/cosmos-sdk/types"
7+
8+
"github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
9+
)
10+
11+
// Migrator is a struct for handling in-place store migrations.
12+
type Migrator struct {
13+
keeper Keeper
14+
}
15+
16+
// NewMigrator returns a new Migrator.
17+
func NewMigrator(keeper Keeper) Migrator {
18+
return Migrator{keeper: keeper}
19+
}
20+
21+
// MigrateTraces migrates the DenomTraces to the correct format, accounting for slashes in the BaseDenom.
22+
func (m Migrator) MigrateTraces(ctx sdk.Context) error {
23+
24+
// list of traces that must replace the old traces in store
25+
var newTraces []types.DenomTrace
26+
m.keeper.IterateDenomTraces(ctx,
27+
func(dt types.DenomTrace) (stop bool) {
28+
// check if the new way of splitting FullDenom
29+
// is the same as the current DenomTrace.
30+
// If it isn't then store the new DenomTrace in the list of new traces.
31+
newTrace := types.ParseDenomTrace(dt.GetFullDenomPath())
32+
err := newTrace.Validate()
33+
if err != nil {
34+
panic(err)
35+
}
36+
37+
if dt.IBCDenom() != newTrace.IBCDenom() {
38+
// The new form of parsing will result in a token denomination change.
39+
// A bank migration is required. A panic should occur to prevent the
40+
// chain from using corrupted state.
41+
panic(fmt.Sprintf("migration will result in corrupted state. Previous IBC token (%s) requires a bank migration. Expected denom trace (%s)", dt, newTrace))
42+
}
43+
44+
if !equalTraces(newTrace, dt) {
45+
newTraces = append(newTraces, newTrace)
46+
}
47+
return false
48+
})
49+
50+
// replace the outdated traces with the new trace information
51+
for _, nt := range newTraces {
52+
m.keeper.SetDenomTrace(ctx, nt)
53+
}
54+
return nil
55+
}
56+
57+
func equalTraces(dtA, dtB types.DenomTrace) bool {
58+
return dtA.BaseDenom == dtB.BaseDenom && dtA.Path == dtB.Path
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package keeper_test
2+
3+
import (
4+
"fmt"
5+
6+
transferkeeper "github.com/cosmos/ibc-go/v4/modules/apps/transfer/keeper"
7+
transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
8+
)
9+
10+
func (suite *KeeperTestSuite) TestMigratorMigrateTraces() {
11+
12+
testCases := []struct {
13+
msg string
14+
malleate func()
15+
expectedTraces transfertypes.Traces
16+
}{
17+
18+
{
19+
"success: two slashes in base denom",
20+
func() {
21+
suite.chainA.GetSimApp().TransferKeeper.SetDenomTrace(
22+
suite.chainA.GetContext(),
23+
transfertypes.DenomTrace{
24+
BaseDenom: "pool/1", Path: "transfer/channel-0/gamm",
25+
})
26+
},
27+
transfertypes.Traces{
28+
{
29+
BaseDenom: "gamm/pool/1", Path: "transfer/channel-0",
30+
},
31+
},
32+
},
33+
{
34+
"success: one slash in base denom",
35+
func() {
36+
suite.chainA.GetSimApp().TransferKeeper.SetDenomTrace(
37+
suite.chainA.GetContext(),
38+
transfertypes.DenomTrace{
39+
BaseDenom: "0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA", Path: "transfer/channel-149/erc",
40+
})
41+
},
42+
transfertypes.Traces{
43+
{
44+
BaseDenom: "erc/0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA", Path: "transfer/channel-149",
45+
},
46+
},
47+
},
48+
{
49+
"success: multiple slashes in a row in base denom",
50+
func() {
51+
suite.chainA.GetSimApp().TransferKeeper.SetDenomTrace(
52+
suite.chainA.GetContext(),
53+
transfertypes.DenomTrace{
54+
BaseDenom: "1", Path: "transfer/channel-5/gamm//pool",
55+
})
56+
},
57+
transfertypes.Traces{
58+
{
59+
BaseDenom: "gamm//pool/1", Path: "transfer/channel-5",
60+
},
61+
},
62+
},
63+
{
64+
"success: multihop base denom",
65+
func() {
66+
suite.chainA.GetSimApp().TransferKeeper.SetDenomTrace(
67+
suite.chainA.GetContext(),
68+
transfertypes.DenomTrace{
69+
BaseDenom: "transfer/channel-1/uatom", Path: "transfer/channel-0",
70+
})
71+
},
72+
transfertypes.Traces{
73+
{
74+
BaseDenom: "uatom", Path: "transfer/channel-0/transfer/channel-1",
75+
},
76+
},
77+
},
78+
{
79+
"success: non-standard port",
80+
func() {
81+
suite.chainA.GetSimApp().TransferKeeper.SetDenomTrace(
82+
suite.chainA.GetContext(),
83+
transfertypes.DenomTrace{
84+
BaseDenom: "customport/channel-7/uatom", Path: "transfer/channel-0/transfer/channel-1",
85+
})
86+
},
87+
transfertypes.Traces{
88+
{
89+
BaseDenom: "uatom", Path: "transfer/channel-0/transfer/channel-1/customport/channel-7",
90+
},
91+
},
92+
},
93+
}
94+
95+
for _, tc := range testCases {
96+
suite.Run(fmt.Sprintf("case %s", tc.msg), func() {
97+
suite.SetupTest() // reset
98+
99+
tc.malleate() // explicitly set up denom traces
100+
101+
migrator := transferkeeper.NewMigrator(suite.chainA.GetSimApp().TransferKeeper)
102+
err := migrator.MigrateTraces(suite.chainA.GetContext())
103+
suite.Require().NoError(err)
104+
105+
traces := suite.chainA.GetSimApp().TransferKeeper.GetAllDenomTraces(suite.chainA.GetContext())
106+
suite.Require().Equal(tc.expectedTraces, traces)
107+
})
108+
}
109+
}
110+
111+
func (suite *KeeperTestSuite) TestMigratorMigrateTracesCorruptionDetection() {
112+
// IBCDenom() previously would return "customport/channel-0/uatom", but now should return ibc/{hash}
113+
corruptedDenomTrace := transfertypes.DenomTrace{
114+
BaseDenom: "customport/channel-0/uatom",
115+
Path: "",
116+
}
117+
suite.chainA.GetSimApp().TransferKeeper.SetDenomTrace(suite.chainA.GetContext(), corruptedDenomTrace)
118+
119+
migrator := transferkeeper.NewMigrator(suite.chainA.GetSimApp().TransferKeeper)
120+
suite.Panics(func() {
121+
migrator.MigrateTraces(suite.chainA.GetContext())
122+
})
123+
}

modules/apps/transfer/module.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ func (am AppModule) LegacyQuerierHandler(*codec.LegacyAmino) sdk.Querier {
120120
func (am AppModule) RegisterServices(cfg module.Configurator) {
121121
types.RegisterMsgServer(cfg.MsgServer(), am.keeper)
122122
types.RegisterQueryServer(cfg.QueryServer(), am.keeper)
123+
124+
m := keeper.NewMigrator(am.keeper)
125+
if err := cfg.RegisterMigration(types.ModuleName, 1, m.MigrateTraces); err != nil {
126+
panic(fmt.Sprintf("failed to migrate transfer app from version 1 to 2: %v", err))
127+
}
123128
}
124129

125130
// InitGenesis performs genesis initialization for the ibc-transfer module. It returns
@@ -139,7 +144,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw
139144
}
140145

141146
// ConsensusVersion implements AppModule/ConsensusVersion.
142-
func (AppModule) ConsensusVersion() uint64 { return 1 }
147+
func (AppModule) ConsensusVersion() uint64 { return 2 }
143148

144149
// BeginBlock implements the AppModule interface
145150
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {

modules/apps/transfer/types/trace.go

+20-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
tmbytes "github.com/tendermint/tendermint/libs/bytes"
1313
tmtypes "github.com/tendermint/tendermint/types"
1414

15+
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
1516
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
1617
)
1718

@@ -20,9 +21,9 @@ import (
2021
//
2122
// Examples:
2223
//
23-
// - "transfer/channelidone/uatom" => DenomTrace{Path: "transfer/channelidone", BaseDenom: "uatom"}
24-
// - "transfer/channelidone/transfer/channelidtwo/uatom" => DenomTrace{Path: "transfer/channelidone/transfer/channelidtwo", BaseDenom: "uatom"}
25-
// - "transfer/channelidone/gamm/pool/1" => DenomTrace{Path: "transfer/channelidone", BaseDenom: "gamm/pool/1"}
24+
// - "portidone/channel-0/uatom" => DenomTrace{Path: "portidone/channel-0", BaseDenom: "uatom"}
25+
// - "portidone/channel-0/portidtwo/channel-1/uatom" => DenomTrace{Path: "portidone/channel-0/portidtwo/channel-1", BaseDenom: "uatom"}
26+
// - "portidone/channel-0/gamm/pool/1" => DenomTrace{Path: "portidone/channel-0", BaseDenom: "gamm/pool/1"}
2627
// - "gamm/pool/1" => DenomTrace{Path: "", BaseDenom: "gamm/pool/1"}
2728
// - "uatom" => DenomTrace{Path: "", BaseDenom: "uatom"}
2829
func ParseDenomTrace(rawDenom string) DenomTrace {
@@ -77,11 +78,23 @@ func (dt DenomTrace) GetFullDenomPath() string {
7778
// extractPathAndBaseFromFullDenom returns the trace path and the base denom from
7879
// the elements that constitute the complete denom.
7980
func extractPathAndBaseFromFullDenom(fullDenomItems []string) (string, string) {
80-
var path []string
81-
var baseDenom []string
81+
var (
82+
path []string
83+
baseDenom []string
84+
)
85+
8286
length := len(fullDenomItems)
8387
for i := 0; i < length; i = i + 2 {
84-
if i < length-1 && length > 2 && fullDenomItems[i] == PortID {
88+
// The IBC specification does not guarentee the expected format of the
89+
// destination port or destination channel identifier. A short term solution
90+
// to determine base denomination is to expect the channel identifier to be the
91+
// one ibc-go specifies. A longer term solution is to separate the path and base
92+
// denomination in the ICS20 packet. If an intermediate hop prefixes the full denom
93+
// with a channel identifier format different from our own, the base denomination
94+
// will be incorrectly parsed, but the token will continue to be treated correctly
95+
// as an IBC denomination. The hash used to store the token internally on our chain
96+
// will be the same value as the base denomination being correctly parsed.
97+
if i < length-1 && length > 2 && channeltypes.IsValidChannelID(fullDenomItems[i+1]) {
8598
path = append(path, fullDenomItems[i], fullDenomItems[i+1])
8699
} else {
87100
baseDenom = fullDenomItems[i:]
@@ -165,7 +178,7 @@ func (t Traces) Sort() Traces {
165178
// ValidatePrefixedDenom checks that the denomination for an IBC fungible token packet denom is correctly prefixed.
166179
// The function will return no error if the given string follows one of the two formats:
167180
//
168-
// - Prefixed denomination: 'transfer/{channelIDN}/.../transfer/{channelID0}/baseDenom'
181+
// - Prefixed denomination: '{portIDN}/{channelIDN}/.../{portID0}/{channelID0}/baseDenom'
169182
// - Unprefixed denomination: 'baseDenom'
170183
//
171184
// 'baseDenom' may or may not contain '/'s

0 commit comments

Comments
 (0)