-
Notifications
You must be signed in to change notification settings - Fork 541
/
transfer_authorization.go
194 lines (155 loc) · 6.58 KB
/
transfer_authorization.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package types
import (
"context"
"math/big"
"slices"
"strings"
"github.com/cosmos/gogoproto/proto"
errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors"
)
var _ authz.Authorization = (*TransferAuthorization)(nil)
// maxUint256 is the maximum value for a 256 bit unsigned integer.
var maxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
// NewTransferAuthorization creates a new TransferAuthorization object.
func NewTransferAuthorization(allocations ...Allocation) *TransferAuthorization {
return &TransferAuthorization{
Allocations: allocations,
}
}
// MsgTypeURL implements Authorization.MsgTypeURL.
func (TransferAuthorization) MsgTypeURL() string {
return sdk.MsgTypeURL(&MsgTransfer{})
}
// Accept implements Authorization.Accept.
func (a TransferAuthorization) Accept(ctx context.Context, msg proto.Message) (authz.AcceptResponse, error) {
msgTransfer, ok := msg.(*MsgTransfer)
if !ok {
return authz.AcceptResponse{}, errorsmod.Wrap(ibcerrors.ErrInvalidType, "type mismatch")
}
for index, allocation := range a.Allocations {
if !(allocation.SourceChannel == msgTransfer.SourceChannel && allocation.SourcePort == msgTransfer.SourcePort) {
continue
}
if !isAllowedAddress(sdk.UnwrapSDKContext(ctx), msgTransfer.Receiver, allocation.AllowList) {
return authz.AcceptResponse{}, errorsmod.Wrap(ibcerrors.ErrInvalidAddress, "not allowed receiver address for transfer")
}
err := validateMemo(sdk.UnwrapSDKContext(ctx), msgTransfer.Memo, allocation.AllowedPacketData)
if err != nil {
return authz.AcceptResponse{}, err
}
// If the spend limit is set to the MaxUint256 sentinel value, do not subtract the amount from the spend limit.
if allocation.SpendLimit.AmountOf(msgTransfer.Token.Denom).Equal(UnboundedSpendLimit()) {
return authz.AcceptResponse{Accept: true, Delete: false, Updated: nil}, nil
}
limitLeft, isNegative := allocation.SpendLimit.SafeSub(msgTransfer.Token)
if isNegative {
return authz.AcceptResponse{}, errorsmod.Wrapf(ibcerrors.ErrInsufficientFunds, "requested amount is more than spend limit")
}
if limitLeft.IsZero() {
a.Allocations = append(a.Allocations[:index], a.Allocations[index+1:]...)
if len(a.Allocations) == 0 {
return authz.AcceptResponse{Accept: true, Delete: true}, nil
}
return authz.AcceptResponse{Accept: true, Delete: false, Updated: &TransferAuthorization{
Allocations: a.Allocations,
}}, nil
}
a.Allocations[index] = Allocation{
SourcePort: allocation.SourcePort,
SourceChannel: allocation.SourceChannel,
SpendLimit: limitLeft,
AllowList: allocation.AllowList,
AllowedPacketData: allocation.AllowedPacketData,
}
return authz.AcceptResponse{Accept: true, Delete: false, Updated: &TransferAuthorization{
Allocations: a.Allocations,
}}, nil
}
return authz.AcceptResponse{}, errorsmod.Wrapf(ibcerrors.ErrNotFound, "requested port and channel allocation does not exist")
}
// ValidateBasic implements Authorization.ValidateBasic.
func (a TransferAuthorization) ValidateBasic() error {
if len(a.Allocations) == 0 {
return errorsmod.Wrap(ErrInvalidAuthorization, "allocations cannot be empty")
}
foundChannels := make(map[string]bool, 0)
for _, allocation := range a.Allocations {
if _, found := foundChannels[allocation.SourceChannel]; found {
return errorsmod.Wrapf(channeltypes.ErrInvalidChannel, "duplicate source channel ID: %s", allocation.SourceChannel)
}
foundChannels[allocation.SourceChannel] = true
if allocation.SpendLimit == nil {
return errorsmod.Wrap(ibcerrors.ErrInvalidCoins, "spend limit cannot be nil")
}
if err := allocation.SpendLimit.Validate(); err != nil {
return errorsmod.Wrapf(ibcerrors.ErrInvalidCoins, err.Error())
}
if err := host.PortIdentifierValidator(allocation.SourcePort); err != nil {
return errorsmod.Wrap(err, "invalid source port ID")
}
if err := host.ChannelIdentifierValidator(allocation.SourceChannel); err != nil {
return errorsmod.Wrap(err, "invalid source channel ID")
}
found := make(map[string]bool, 0)
for i := 0; i < len(allocation.AllowList); i++ {
if found[allocation.AllowList[i]] {
return errorsmod.Wrapf(ErrInvalidAuthorization, "duplicate entry in allow list %s", allocation.AllowList[i])
}
found[allocation.AllowList[i]] = true
}
}
return nil
}
// isAllowedAddress returns a boolean indicating if the receiver address is valid for transfer.
// gasCostPerIteration gas is consumed for each iteration.
func isAllowedAddress(ctx sdk.Context, receiver string, allowedAddrs []string) bool {
if len(allowedAddrs) == 0 {
return true
}
gasCostPerIteration := ctx.KVGasConfig().IterNextCostFlat
for _, addr := range allowedAddrs {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "transfer authorization")
if addr == receiver {
return true
}
}
return false
}
// validateMemo returns a nil error indicating if the memo is valid for transfer.
func validateMemo(ctx sdk.Context, memo string, allowedMemos []string) error {
// if the allow list is empty, then the memo must be an empty string
if len(allowedMemos) == 0 {
if len(strings.TrimSpace(memo)) != 0 {
return errorsmod.Wrapf(ErrInvalidAuthorization, "memo must be empty because allowed packet data in allocation is empty")
}
return nil
}
// if allowedPacketDataList has only 1 element and it equals AllowAllPacketDataKeys
// then accept all the memo strings
if len(allowedMemos) == 1 && allowedMemos[0] == AllowAllPacketDataKeys {
return nil
}
gasCostPerIteration := ctx.KVGasConfig().IterNextCostFlat
isMemoAllowed := slices.ContainsFunc(allowedMemos, func(allowedMemo string) bool {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "transfer authorization")
return strings.TrimSpace(memo) == strings.TrimSpace(allowedMemo)
})
if !isMemoAllowed {
return errorsmod.Wrapf(ErrInvalidAuthorization, "not allowed memo: %s", memo)
}
return nil
}
// UnboundedSpendLimit returns the sentinel value that can be used
// as the amount for a denomination's spend limit for which spend limit updating
// should be disabled. Please note that using this sentinel value means that a grantee
// will be granted the privilege to do ICS20 token transfers for the total amount
// of the denomination available at the granter's account.
func UnboundedSpendLimit() sdkmath.Int {
return sdkmath.NewIntFromBigInt(maxUint256)
}