Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imp: allow memo strings instead of keys for transfer authorizations #6268

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

* (apps/27-interchain-accounts) [\#5533](https://github.com/cosmos/ibc-go/pull/5533) ICA host sets the host connection ID on `OnChanOpenTry`, so that ICA controller implementations are not obliged to set the value on `OnChanOpenInit` if they are not able.
* (apps/27-interchain-accounts, apps/tranfer, apps/29-fee) [\#6253](https://github.com/cosmos/ibc-go/pull/6253) Allow channel handshake to succeed if fee middleware is wired up on one side, but not the other.
* (apps/transfer) [\#6268](https://github.com/cosmos/ibc-go/pull/6268) Use JSON-encoded memo strings instead of JSON keys in `AllowedPacketData` of transfer authorization.

### Features

Expand Down
7 changes: 3 additions & 4 deletions docs/docs/02-apps/01-transfer/08-authorizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ It takes:

- an `AllowList` list that specifies the list of addresses that are allowed to receive funds. If this list is empty, then all addresses are allowed to receive funds from the `TransferAuthorization`.

- an `AllowedPacketData` list that specifies the list of memo packet data keys that are allowed to send the packet. If this list is empty, then only an empty memo is allowed (a `memo` field with non-empty content will be denied). If this list includes a single element equal to `"*"`, then any content in `memo` field will be allowed.
- an `AllowedPacketData` list that specifies the list of (JSON-encoded) memo strings that are allowed to be included in the memo field of the packet. If this list is empty, then only an empty memo is allowed (a `memo` field with non-empty content will be denied). If this list includes a single element equal to `"*"`, then any content in `memo` field will be allowed.

Setting a `TransferAuthorization` is expected to fail if:

Expand Down Expand Up @@ -51,9 +51,8 @@ type Allocation struct {
SpendLimit sdk.Coins
// allow list of receivers, an empty allow list permits any receiver address
AllowList []string
// allow list of packet data keys, an empty list prohibits all packet data keys;
// a list only with "*" permits any packet data key
// allow list of (JSON-encoded) memo strings, an empty list prohibits all memo strings;
// a list only with "*" permits any memo string
AllowedPacketData []string
}

```
4 changes: 2 additions & 2 deletions modules/apps/transfer/types/authz.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion modules/apps/transfer/types/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
// DenomPrefix is the prefix used for internal SDK coin representation.
DenomPrefix = "ibc"

// AllowAllPacketDataKeys holds the string key that allows all packet data keys in authz transfer messages
// AllowAllPacketDataKeys holds the string key that allows all memo strings in authz transfer messages
AllowAllPacketDataKeys = "*"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this constant can also be renamed in v9? We could also add a new constant with a more accurate name for the back ports.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, maybe you can rename it on this PR and then change it back in the backport

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am going to rename it in a followup PR, just to make the backport easier.


KeyTotalEscrowPrefix = "totalEscrowForDenom"
Expand Down
45 changes: 23 additions & 22 deletions modules/apps/transfer/types/transfer_authorization.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package types

import (
"bytes"
"context"
"encoding/json"
"math/big"
"sort"
"strings"

"github.com/cosmos/gogoproto/proto"
Expand Down Expand Up @@ -131,6 +131,16 @@
}
found[allocation.AllowList[i]] = true
}

if len(allocation.AllowedPacketData) > 0 && allocation.AllowedPacketData[0] != AllowAllPacketDataKeys {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we also put a limit on how many items we allow in AllowedPacketData?

jsonObject := make(map[string]interface{})
for _, elem := range allocation.AllowedPacketData {
err := json.Unmarshal([]byte(elem), &jsonObject)
if err != nil {
return errorsmod.Wrapf(ErrInvalidAuthorization, "allowed packet data contains a non JSON-encoded string: %s", elem)
Copy link
Member

Choose a reason for hiding this comment

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

Why do we require them to be JSON encoded? Can we remove this requirement please and just use any string?

Copy link
Contributor Author

@crodriguezvega crodriguezvega May 8, 2024

Choose a reason for hiding this comment

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

Sure, I can remove it. Although I thought that, even though it's not enforced, the consensus was that the memo should a JSON-encoded string. Do you know of use cases where that would not be the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I changed it here. If during the PR review we decide to go back to enforcing JSON, we can just revert that commit.

}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This checks that if the wildcard is not used, then all elements should be JSON-encoded strings.

}

return nil
Expand Down Expand Up @@ -166,39 +176,30 @@
}

// if allowedPacketDataList has only 1 element and it equals AllowAllPacketDataKeys
// then accept all the packet data keys
// then accept all the memo strings
if len(allowedPacketDataList) == 1 && allowedPacketDataList[0] == AllowAllPacketDataKeys {
return nil
}

jsonObject := make(map[string]interface{})
err := json.Unmarshal([]byte(memo), &jsonObject)
if err != nil {
return err
}

gasCostPerIteration := ctx.KVGasConfig().IterNextCostFlat

for _, key := range allowedPacketDataList {
for _, allowedMemo := range allowedPacketDataList {
crodriguezvega marked this conversation as resolved.
Show resolved Hide resolved
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "transfer authorization")

_, ok := jsonObject[key]
if ok {
delete(jsonObject, key)
}
}
dst := &bytes.Buffer{}
json.Compact(dst, []byte(allowedMemo))

Check failure on line 190 in modules/apps/transfer/types/transfer_authorization.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `json.Compact` is not checked (errcheck)
compactAllowedMemo := dst.String()

keys := make([]string, 0, len(jsonObject))
for k := range jsonObject {
keys = append(keys, k)
}
sort.Strings(keys)
dst = &bytes.Buffer{}
json.Compact(dst, []byte(memo))

Check failure on line 194 in modules/apps/transfer/types/transfer_authorization.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `json.Compact` is not checked (errcheck)
compactMemo := dst.String()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this is over engineering it, but this compacts the JSON strings to prevent a mismatch due to empty spaces.


if len(jsonObject) != 0 {
return errorsmod.Wrapf(ErrInvalidAuthorization, "not allowed packet data keys: %s", keys)
if compactMemo == compactAllowedMemo {
return nil
}
}

return nil
return errorsmod.Wrapf(ErrInvalidAuthorization, "not allowed memo: %s", memo)
crodriguezvega marked this conversation as resolved.
Show resolved Hide resolved
}

// UnboundedSpendLimit returns the sentinel value that can be used
Expand Down
89 changes: 80 additions & 9 deletions modules/apps/transfer/types/transfer_authorization_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package types_test

import (
"bytes"
"encoding/json"

sdkmath "cosmossdk.io/math"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -11,9 +14,33 @@
"github.com/cosmos/ibc-go/v8/testing/mock"
)

const testMemo = `{"wasm":{"contract":"osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg","msg":{"osmosis_swap":{"output_denom":"uosmo","slippage":{"twap":{"slippage_percentage":"20","window_seconds":10}},"receiver":"feeabs/feeabs1efd63aw40lxf3n4mhf7dzhjkr453axurwrhrrw","on_failed_delivery":"do_nothing"}}}}`
const (
testMemo1 = `{
"wasm": {
"contract": "osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg",
"msg": {
"osmosis_swap": {
"output_denom": "uosmo",
"slippage": {
"twap": {
"slippage_percentage": "20",
"window_seconds": 10
}
},
"receiver": "feeabs/feeabs1efd63aw40lxf3n4mhf7dzhjkr453axurwrhrrw",
"on_failed_delivery": "do_nothing"
}
}
}
}`
testMemo2 = `{"forward":{"channel":"channel-11","port":"transfer","receiver":"stars1twfv52yxcyykx2lcvgl42svw46hsm5dd4ww6xy","retries":2,"timeout":1712146014542131200}}`
)

func (suite *TypesTestSuite) TestTransferAuthorizationAccept() {
dst := &bytes.Buffer{}
json.Compact(dst, []byte(testMemo1))

Check failure on line 41 in modules/apps/transfer/types/transfer_authorization_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `json.Compact` is not checked (errcheck)
compactTestMemo1 := dst.String()

var (
msgTransfer types.MsgTransfer
transferAuthz types.TransferAuthorization
Expand Down Expand Up @@ -122,7 +149,22 @@
func() {
allowedList := []string{"*"}
transferAuthz.Allocations[0].AllowedPacketData = allowedList
msgTransfer.Memo = testMemo
msgTransfer.Memo = testMemo1
},
func(res authz.AcceptResponse, err error) {
suite.Require().NoError(err)

suite.Require().True(res.Accept)
suite.Require().True(res.Delete)
suite.Require().Nil(res.Updated)
},
},
{
"success: non-compact transfer memo allowed",
func() {
allowedList := []string{compactTestMemo1, testMemo2}
transferAuthz.Allocations[0].AllowedPacketData = allowedList
msgTransfer.Memo = testMemo1
},
func(res authz.AcceptResponse, err error) {
suite.Require().NoError(err)
Expand All @@ -133,11 +175,11 @@
},
},
{
"success: transfer memo allowed",
"success: compact transfer memo allowed",
func() {
allowedList := []string{"wasm", "forward"}
allowedList := []string{testMemo1, testMemo2}
transferAuthz.Allocations[0].AllowedPacketData = allowedList
msgTransfer.Memo = testMemo
msgTransfer.Memo = compactTestMemo1
},
func(res authz.AcceptResponse, err error) {
suite.Require().NoError(err)
Expand All @@ -152,7 +194,7 @@
func() {
allowedList := []string{}
transferAuthz.Allocations[0].AllowedPacketData = allowedList
msgTransfer.Memo = testMemo
msgTransfer.Memo = testMemo1
},
func(res authz.AcceptResponse, err error) {
suite.Require().Error(err)
Expand All @@ -161,13 +203,13 @@
{
"memo not allowed",
func() {
allowedList := []string{"forward"}
allowedList := []string{testMemo1}
transferAuthz.Allocations[0].AllowedPacketData = allowedList
msgTransfer.Memo = testMemo
msgTransfer.Memo = testMemo2
},
func(res authz.AcceptResponse, err error) {
suite.Require().Error(err)
suite.Require().ErrorContains(err, "not allowed packet data keys: [wasm]")
suite.Require().ErrorContains(err, "not allowed memo: {\"forward\":{\"channel\":\"channel-11\",\"port\":\"transfer\",\"receiver\":\"stars1twfv52yxcyykx2lcvgl42svw46hsm5dd4ww6xy\",\"retries\":2,\"timeout\":1712146014542131200}}")
crodriguezvega marked this conversation as resolved.
Show resolved Hide resolved
},
},
{
Expand Down Expand Up @@ -310,6 +352,20 @@
},
true,
},
{
"success: wildcard allowed packet data",
func() {
transferAuthz.Allocations[0].AllowedPacketData = []string{"*"}
},
true,
},
{
"success: JSON-encoded allowed packet data",
func() {
transferAuthz.Allocations[0].AllowedPacketData = []string{testMemo1}
},
true,
},
{
"empty allocations",
func() {
Expand Down Expand Up @@ -373,6 +429,21 @@
},
expPass: false,
},
{
name: "non JSON-encoded allowed packet data",
malleate: func() {
allocation := types.Allocation{
SourcePort: mock.PortID,
SourceChannel: transferAuthz.Allocations[0].SourceChannel,
SpendLimit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))),
AllowList: []string{ibctesting.TestAccAddress},
AllowedPacketData: []string{"forward"},
}

transferAuthz.Allocations = append(transferAuthz.Allocations, allocation)
},
expPass: false,
},
}

for _, tc := range testCases {
Expand Down
4 changes: 2 additions & 2 deletions proto/ibc/applications/transfer/v1/authz.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ message Allocation {
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
// allow list of receivers, an empty allow list permits any receiver address
repeated string allow_list = 4;
// allow list of packet data keys, an empty list prohibits all packet data keys;
// a list only with "*" permits any packet data key
// allow list of (JSON-encoded) memo strings, an empty list prohibits all memo strings;
// a list only with "*" permits any memo string
repeated string allowed_packet_data = 5;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe in v9 we can rename this field?

}

Expand Down
Loading