Skip to content

Commit b7144a9

Browse files
fix to correctly parse denoms with slashes in the base denom (backport #1451) (#1536)
* fix to correctly parse denoms with slashes in the base denom (#1451) * fix to correctly parse denoms with slashes in the base denom * some logic refinement * review comments * add changelog entry an other review comments * review comment * Add slash migration guide (#1518) * add migration guide * Update docs/migrations/support-slashed-denoms.md Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> * clarify upgrade name * remove unnecessary store loader * review comment, update migration code Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> Co-authored-by: Carlos Rodriguez <crodveg@gmail.com> * rename migration file Co-authored-by: Carlos Rodriguez <crodveg@gmail.com> Co-authored-by: Aditya <adityasripal@gmail.com> Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> (cherry picked from commit 3a235af) # Conflicts: # CHANGELOG.md * fix conflict Co-authored-by: Carlos Rodriguez <carlos@interchain.io>
1 parent 4f22170 commit b7144a9

File tree

4 files changed

+162
-11
lines changed

4 files changed

+162
-11
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
6262
### Bug Fixes
6363

6464
* (modules/core/04-channel) [\#1130](https://github.com/cosmos/ibc-go/pull/1130) Call `packet.GetSequence()` rather than passing func in `WriteAcknowledgement` log output
65+
* (apps/transfer) [\#1451](https://github.com/cosmos/ibc-go/pull/1451) Fixing the support for base denoms that contain slashes.
6566

6667
## [v3.0.0](https://github.com/cosmos/ibc-go/releases/tag/v3.0.0) - 2022-03-15
6768

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Migrating from not supporing base denoms with slashes to supporting base denoms with slashes
2+
3+
This document is intended to highlight significant changes which may require more information than presented in the CHANGELOG.
4+
Any changes that must be done by a user of ibc-go should be documented here.
5+
6+
There are four sections based on the four potential user groups of this document:
7+
- Chains
8+
- IBC Apps
9+
- Relayers
10+
- IBC Light Clients
11+
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.
13+
14+
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.
15+
16+
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"`.
17+
18+
This incorrect trace information must be corrected when the chain does upgrade to fully supporting denominations with slashes.
19+
20+
To do so, chain binaries should include a migration script that will run when the chain upgrades from not supporting base denominations with slashes to supporting base denominations with slashes.
21+
22+
## Chains
23+
24+
### ICS20 - Transfer
25+
26+
The transfer module will now support slashes in base denoms, so we must iterate over current traces to check if any of them are incorrectly formed and correct the trace information.
27+
28+
### Upgrade Proposal
29+
30+
```go
31+
// Here the upgrade name is the upgrade name set by the chain
32+
app.UpgradeKeeper.SetUpgradeHandler("supportSlashedDenomsUpgrade",
33+
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 && !reflect.DeepEqual(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+
55+
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
56+
})
57+
```
58+
59+
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.
60+
61+
For a more detailed sample, please check out the code changes in [this pull request](https://github.com/cosmos/ibc-go/pull/1527).
62+
63+
### Genesis Migration
64+
65+
If the chain chooses to add support for slashes in base denoms via genesis export, then the trace information must be corrected during genesis migration.
66+
67+
The migration code required may look like:
68+
69+
```go
70+
func migrateGenesisSlashedDenomsUpgrade(appState genutiltypes.AppMap, clientCtx client.Context, genDoc *tmtypes.GenesisDoc) (genutiltypes.AppMap, error) {
71+
if appState[ibctransfertypes.ModuleName] != nil {
72+
transferGenState := &ibctransfertypes.GenesisState{}
73+
clientCtx.Codec.MustUnmarshalJSON(appState[ibctransfertypes.ModuleName], transferGenState)
74+
75+
substituteTraces := make([]ibctransfertypes.DenomTrace, len(transferGenState.DenomTraces))
76+
for i, dt := range transferGenState.DenomTraces {
77+
// replace all previous traces with the latest trace if validation passes
78+
// note most traces will have same value
79+
newTrace := ibctransfertypes.ParseDenomTrace(dt.GetFullDenomPath())
80+
81+
if err := newTrace.Validate(); err != nil {
82+
substituteTraces[i] = dt
83+
} else {
84+
substituteTraces[i] = newTrace
85+
}
86+
}
87+
88+
transferGenState.DenomTraces = substituteTraces
89+
90+
// delete old genesis state
91+
delete(appState, ibctransfertypes.ModuleName)
92+
93+
// set new ibc transfer genesis state
94+
appState[ibctransfertypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(transferGenState)
95+
}
96+
97+
return appState, nil
98+
}
99+
```
100+
101+
For a more detailed sample, please check out the code changes in [this pull request](https://github.com/cosmos/ibc-go/pull/1528).

modules/apps/transfer/types/trace.go

+36-6
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ import (
2020
//
2121
// Examples:
2222
//
23-
// - "portidone/channelidone/uatom" => DenomTrace{Path: "portidone/channelidone", BaseDenom: "uatom"}
24-
// - "uatom" => DenomTrace{Path: "", BaseDenom: "uatom"}
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"}
26+
// - "gamm/pool/1" => DenomTrace{Path: "", BaseDenom: "gamm/pool/1"}
27+
// - "uatom" => DenomTrace{Path: "", BaseDenom: "uatom"}
2528
func ParseDenomTrace(rawDenom string) DenomTrace {
2629
denomSplit := strings.Split(rawDenom, "/")
2730

@@ -32,9 +35,10 @@ func ParseDenomTrace(rawDenom string) DenomTrace {
3235
}
3336
}
3437

38+
path, baseDenom := extractPathAndBaseFromFullDenom(denomSplit)
3539
return DenomTrace{
36-
Path: strings.Join(denomSplit[:len(denomSplit)-1], "/"),
37-
BaseDenom: denomSplit[len(denomSplit)-1],
40+
Path: path,
41+
BaseDenom: baseDenom,
3842
}
3943
}
4044

@@ -70,6 +74,24 @@ func (dt DenomTrace) GetFullDenomPath() string {
7074
return dt.GetPrefix() + dt.BaseDenom
7175
}
7276

77+
// extractPathAndBaseFromFullDenom returns the trace path and the base denom from
78+
// the elements that constitute the complete denom.
79+
func extractPathAndBaseFromFullDenom(fullDenomItems []string) (string, string) {
80+
var path []string
81+
var baseDenom []string
82+
length := len(fullDenomItems)
83+
for i := 0; i < length; i = i + 2 {
84+
if i < length-1 && length > 2 && fullDenomItems[i] == PortID {
85+
path = append(path, fullDenomItems[i], fullDenomItems[i+1])
86+
} else {
87+
baseDenom = fullDenomItems[i:]
88+
break
89+
}
90+
}
91+
92+
return strings.Join(path, "/"), strings.Join(baseDenom, "/")
93+
}
94+
7395
func validateTraceIdentifiers(identifiers []string) error {
7496
if len(identifiers) == 0 || len(identifiers)%2 != 0 {
7597
return fmt.Errorf("trace info must come in pairs of port and channel identifiers '{portID}/{channelID}', got the identifiers: %s", identifiers)
@@ -143,8 +165,10 @@ func (t Traces) Sort() Traces {
143165
// ValidatePrefixedDenom checks that the denomination for an IBC fungible token packet denom is correctly prefixed.
144166
// The function will return no error if the given string follows one of the two formats:
145167
//
146-
// - Prefixed denomination: '{portIDN}/{channelIDN}/.../{portID0}/{channelID0}/baseDenom'
168+
// - Prefixed denomination: 'transfer/{channelIDN}/.../transfer/{channelID0}/baseDenom'
147169
// - Unprefixed denomination: 'baseDenom'
170+
//
171+
// 'baseDenom' may or may not contain '/'s
148172
func ValidatePrefixedDenom(denom string) error {
149173
denomSplit := strings.Split(denom, "/")
150174
if denomSplit[0] == denom && strings.TrimSpace(denom) != "" {
@@ -156,7 +180,13 @@ func ValidatePrefixedDenom(denom string) error {
156180
return sdkerrors.Wrap(ErrInvalidDenomForTransfer, "base denomination cannot be blank")
157181
}
158182

159-
identifiers := denomSplit[:len(denomSplit)-1]
183+
path, _ := extractPathAndBaseFromFullDenom(denomSplit)
184+
if path == "" {
185+
// NOTE: base denom contains slashes, so no base denomination validation
186+
return nil
187+
}
188+
189+
identifiers := strings.Split(path, "/")
160190
return validateTraceIdentifiers(identifiers)
161191
}
162192

modules/apps/transfer/types/trace_test.go

+24-5
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@ func TestParseDenomTrace(t *testing.T) {
1414
}{
1515
{"empty denom", "", DenomTrace{}},
1616
{"base denom", "uatom", DenomTrace{BaseDenom: "uatom"}},
17+
{"base denom ending with '/'", "uatom/", DenomTrace{BaseDenom: "uatom/"}},
18+
{"base denom with single '/'s", "gamm/pool/1", DenomTrace{BaseDenom: "gamm/pool/1"}},
19+
{"base denom with double '/'s", "gamm//pool//1", DenomTrace{BaseDenom: "gamm//pool//1"}},
1720
{"trace info", "transfer/channelToA/uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}},
18-
{"incomplete path", "transfer/uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer"}},
21+
{"trace info with base denom ending in '/'", "transfer/channelToA/uatom/", DenomTrace{BaseDenom: "uatom/", Path: "transfer/channelToA"}},
22+
{"trace info with single '/' in base denom", "transfer/channelToA/erc20/0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA", DenomTrace{BaseDenom: "erc20/0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA", Path: "transfer/channelToA"}},
23+
{"trace info with multiple '/'s in base denom", "transfer/channelToA/gamm/pool/1", DenomTrace{BaseDenom: "gamm/pool/1", Path: "transfer/channelToA"}},
24+
{"trace info with multiple double '/'s in base denom", "transfer/channelToA/gamm//pool//1", DenomTrace{BaseDenom: "gamm//pool//1", Path: "transfer/channelToA"}},
25+
{"trace info with multiple port/channel pairs", "transfer/channelToA/transfer/channelToB/uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"}},
26+
{"incomplete path", "transfer/uatom", DenomTrace{BaseDenom: "transfer/uatom"}},
1927
{"invalid path (1)", "transfer//uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer/"}},
20-
{"invalid path (2)", "transfer/channelToA/uatom/", DenomTrace{BaseDenom: "", Path: "transfer/channelToA/uatom"}},
28+
{"invalid path (2)", "channelToA/transfer/uatom", DenomTrace{BaseDenom: "channelToA/transfer/uatom"}},
29+
{"invalid path (3)", "uatom/transfer", DenomTrace{BaseDenom: "uatom/transfer"}},
30+
{"invalid path (4)", "transfer/channelToA", DenomTrace{BaseDenom: "transfer/channelToA"}},
31+
{"invalid path (5)", "transfer/channelToA/", DenomTrace{Path: "transfer/channelToA"}},
32+
{"invalid path (6)", "transfer/channelToA/transfer", DenomTrace{BaseDenom: "transfer", Path: "transfer/channelToA"}},
33+
{"invalid path (7)", "transfer/channelToA/transfer/channelToB", DenomTrace{Path: "transfer/channelToA/transfer/channelToB"}},
2134
}
2235

2336
for _, tc := range testCases {
@@ -49,6 +62,8 @@ func TestDenomTrace_Validate(t *testing.T) {
4962
expError bool
5063
}{
5164
{"base denom only", DenomTrace{BaseDenom: "uatom"}, false},
65+
{"base denom only with single '/'", DenomTrace{BaseDenom: "erc20/0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA"}, false},
66+
{"base denom only with multiple '/'s", DenomTrace{BaseDenom: "gamm/pool/1"}, false},
5267
{"empty DenomTrace", DenomTrace{}, true},
5368
{"valid single trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}, false},
5469
{"valid multiple trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"}, false},
@@ -104,12 +119,15 @@ func TestValidatePrefixedDenom(t *testing.T) {
104119
expError bool
105120
}{
106121
{"prefixed denom", "transfer/channelToA/uatom", false},
122+
{"prefixed denom with '/'", "transfer/channelToA/gamm/pool/1", false},
123+
{"empty prefix", "/uatom", false},
124+
{"empty identifiers", "//uatom", false},
107125
{"base denom", "uatom", false},
126+
{"base denom with single '/'", "erc20/0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA", false},
127+
{"base denom with multiple '/'s", "gamm/pool/1", false},
128+
{"invalid port ID", "(transfer)/channelToA/uatom", false},
108129
{"empty denom", "", true},
109-
{"empty prefix", "/uatom", true},
110-
{"empty identifiers", "//uatom", true},
111130
{"single trace identifier", "transfer/", true},
112-
{"invalid port ID", "(transfer)/channelToA/uatom", true},
113131
{"invalid channel ID", "transfer/(channelToA)/uatom", true},
114132
}
115133

@@ -131,6 +149,7 @@ func TestValidateIBCDenom(t *testing.T) {
131149
}{
132150
{"denom with trace hash", "ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", false},
133151
{"base denom", "uatom", false},
152+
{"base denom ending with '/'", "uatom/", false},
134153
{"base denom with single '/'s", "gamm/pool/1", false},
135154
{"base denom with double '/'s", "gamm//pool//1", false},
136155
{"non-ibc prefix with hash", "notibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", false},

0 commit comments

Comments
 (0)