Skip to content

Commit d25836d

Browse files
authored
feat: daily bridge transfer limits (#1198)
# Related Github tickets - Closes VolumeFi#1711 # Background Add a limit to the amount of tokens transferred out from paloma within a specified window, per token. This is defined by a new governance vote. - Add governance vote. Can define token, window size, token limit and list of exempt addresses. - Keep a counter of the number of tokens transferred and the block height at the start of the limiting window. - Before accepting a transfer, update the counters and check the limits. - Transfers are refused if over the transfer limit. # Testing completed - [x] test coverage exists or has been added/updated - [x] tested in a private testnet # Breaking changes - [x] I have checked my code for breaking changes - [x] If there are breaking changes, there is a supporting migration.
1 parent 4ff05b3 commit d25836d

24 files changed

+2740
-155
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
syntax = "proto3";
2+
package palomachain.paloma.gravity;
3+
4+
import "gogoproto/gogo.proto";
5+
6+
option go_package = "github.com/palomachain/paloma/x/gravity/types";
7+
8+
enum LimitPeriod {
9+
NONE = 0;
10+
DAILY = 1;
11+
WEEKLY = 2;
12+
MONTHLY = 3;
13+
YEARLY = 4;
14+
}
15+
16+
// Allow at most `limit` tokens of `token` to be transferred within a
17+
// `limit_period` window. `limit_period` will be converted to blocks.
18+
// If more than `limit` tokens are attempted to be transferred between those
19+
// block heights, the transfer is not allowed.
20+
// If the sender is in `exempt_addresses`, the limits are not checked nor
21+
// updated.
22+
message BridgeTransferLimit {
23+
string token = 1;
24+
string limit = 2 [
25+
(gogoproto.customtype) = "cosmossdk.io/math.Int",
26+
(gogoproto.nullable) = false
27+
];
28+
LimitPeriod limit_period = 3;
29+
repeated bytes exempt_addresses = 4 [
30+
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
31+
];
32+
}
33+
34+
// Transfer usage counters used to check for transfer limits for a single denom.
35+
// `total` maintains the total amount transferred during the current window.
36+
// `start_block_height` maintains the block height of the first transfer in the
37+
// current window.
38+
message BridgeTransferUsage {
39+
string total = 1 [
40+
(gogoproto.customtype) = "cosmossdk.io/math.Int",
41+
(gogoproto.nullable) = false
42+
];
43+
int64 start_block_height = 2;
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
syntax = "proto3";
2+
package palomachain.paloma.gravity;
3+
4+
import "gogoproto/gogo.proto";
5+
import "palomachain/paloma/gravity/bridge_transfer_limit.proto";
6+
7+
option go_package = "github.com/palomachain/paloma/x/gravity/types";
8+
9+
message SetBridgeTransferLimitProposal {
10+
string title = 1;
11+
string description = 2;
12+
string token = 3;
13+
string limit = 4 [
14+
(gogoproto.customtype) = "cosmossdk.io/math.Int",
15+
(gogoproto.nullable) = false
16+
];
17+
LimitPeriod limit_period = 5;
18+
repeated string exempt_addresses = 6;
19+
}

proto/palomachain/paloma/gravity/genesis.proto

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "palomachain/paloma/gravity/types.proto";
66
import "palomachain/paloma/gravity/msgs.proto";
77
import "palomachain/paloma/gravity/batch.proto";
88
import "palomachain/paloma/gravity/bridge_tax.proto";
9+
import "palomachain/paloma/gravity/bridge_transfer_limit.proto";
910
import "palomachain/paloma/gravity/attestation.proto";
1011
import "cosmos/base/v1beta1/coin.proto";
1112
import "palomachain/paloma/gravity/params.proto";
@@ -22,7 +23,8 @@ message GenesisState {
2223
repeated ERC20ToDenom erc20_to_denoms = 9 [ (gogoproto.nullable) = false ];
2324
repeated OutgoingTransferTx unbatched_transfers = 10
2425
[ (gogoproto.nullable) = false ];
25-
BridgeTax BridgeTax = 11;
26+
BridgeTax bridge_tax = 11;
27+
repeated BridgeTransferLimit bridge_transfer_limits = 12;
2628
}
2729

2830
// GravityCounters contains the many noces and counters required to maintain the

proto/palomachain/paloma/gravity/query.proto

+16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import "palomachain/paloma/gravity/types.proto";
1111
import "palomachain/paloma/gravity/msgs.proto";
1212
import "palomachain/paloma/gravity/params.proto";
1313
import "palomachain/paloma/gravity/bridge_tax.proto";
14+
import "palomachain/paloma/gravity/bridge_transfer_limit.proto";
1415
import "google/protobuf/empty.proto";
1516

1617
option go_package = "github.com/palomachain/paloma/x/gravity/types";
@@ -48,6 +49,12 @@ service Query {
4849
rpc GetBridgeTax(google.protobuf.Empty) returns (QueryBridgeTaxResponse) {
4950
option (google.api.http).get = "/palomachain/paloma/gravity/bridge_tax";
5051
}
52+
53+
rpc GetBridgeTransferLimits(google.protobuf.Empty)
54+
returns (QueryBridgeTransferLimitsResponse) {
55+
option (google.api.http).get =
56+
"/palomachain/paloma/gravity/all_bridge_transfer_limits";
57+
}
5158
}
5259

5360
message QueryParamsRequest {}
@@ -157,3 +164,12 @@ message QueryPendingSendToEthResponse {
157164
message QueryBridgeTaxResponse {
158165
BridgeTax bridge_tax = 1;
159166
}
167+
168+
message QueryBridgeTransferLimitsResponse {
169+
message LimitUsage {
170+
BridgeTransferLimit limit = 1;
171+
BridgeTransferUsage usage = 2;
172+
}
173+
174+
repeated LimitUsage limits = 1;
175+
}

x/gravity/client/cli/query.go

+31
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func GetQueryCmd() *cobra.Command {
3737
CmdGetLastObservedEthNonce(),
3838
GetCmdQueryParams(),
3939
GetCmdQueryBridgeTax(),
40+
GetCmdQueryBridgeTransferLimits(),
4041
}...)
4142

4243
return gravityQueryCmd
@@ -366,3 +367,33 @@ func GetCmdQueryBridgeTax() *cobra.Command {
366367

367368
return cmd
368369
}
370+
371+
// GetCmdQueryBridgeTransferLimits fetches transfer limits for all tokens
372+
func GetCmdQueryBridgeTransferLimits() *cobra.Command {
373+
cmd := &cobra.Command{
374+
Use: "bridge-transfer-limits",
375+
Short: "Query bridge transfer limits for all tokens",
376+
Args: cobra.ExactArgs(0),
377+
RunE: func(cmd *cobra.Command, args []string) (err error) {
378+
clientCtx, err := client.GetClientTxContext(cmd)
379+
if err != nil {
380+
return err
381+
}
382+
383+
queryClient := types.NewQueryClient(clientCtx)
384+
385+
params := &emptypb.Empty{}
386+
387+
res, err := queryClient.GetBridgeTransferLimits(cmd.Context(), params)
388+
if err != nil {
389+
return err
390+
}
391+
392+
return clientCtx.PrintProto(res)
393+
},
394+
}
395+
396+
flags.AddQueryFlagsToCmd(cmd)
397+
398+
return cmd
399+
}

x/gravity/client/cli/tx_proposal.go

+79-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package cli
33
import (
44
"fmt"
55
"math/big"
6+
"strings"
67

8+
"cosmossdk.io/math"
79
"github.com/VolumeFi/whoops"
810
"github.com/cosmos/cosmos-sdk/client"
911
"github.com/cosmos/cosmos-sdk/client/flags"
@@ -28,6 +30,7 @@ func CmdGravityProposalHandler() *cobra.Command {
2830
cmd.AddCommand([]*cobra.Command{
2931
CmdSetErc20ToDenom(),
3032
CmdSetBridgeTax(),
33+
CmdSetBridgeTransferLimit(),
3134
}...)
3235

3336
return cmd
@@ -111,7 +114,7 @@ func CmdSetBridgeTax() *cobra.Command {
111114
return err
112115
}
113116

114-
description, err := cmd.Flags().GetString(cli.FlagTitle)
117+
description, err := cmd.Flags().GetString(cli.FlagSummary)
115118
if err != nil {
116119
return err
117120
}
@@ -158,3 +161,78 @@ func CmdSetBridgeTax() *cobra.Command {
158161
applyFlags(cmd)
159162
return cmd
160163
}
164+
165+
func CmdSetBridgeTransferLimit() *cobra.Command {
166+
cmd := &cobra.Command{
167+
Use: "set-bridge-transfer-limit [token] [limit] [limit-period]",
168+
Short: "Sets the bridge transfer limit, and optionally exempt addresses",
169+
Long: `Set the bridge transfer limit for the specified token.
170+
[limit-period] must be one of: NONE, DAILY, WEEKLY, MONTHLY, YEARLY. Setting it to NONE effectively disables the limit.
171+
[limit-period] will be converted to a block window. At most, [limit] tokens can be transferred within each block window. After that transfers will fail.`,
172+
Example: "set-bridge-transfer-limit ugrain 1000000 DAILY",
173+
Args: cobra.ExactArgs(3),
174+
RunE: func(cmd *cobra.Command, args []string) error {
175+
cliCtx, err := client.GetClientTxContext(cmd)
176+
if err != nil {
177+
return err
178+
}
179+
180+
token, limitRaw, limitPeriodRaw := args[0], args[1], args[2]
181+
182+
limit, ok := math.NewIntFromString(limitRaw)
183+
if !ok {
184+
return fmt.Errorf("invalid limit: %v", limitRaw)
185+
}
186+
187+
// Accept both lower case and upper case limit period strings
188+
limitPeriod, ok := types.LimitPeriod_value[strings.ToUpper(limitPeriodRaw)]
189+
if !ok {
190+
return fmt.Errorf("invalid limit period: %v", limitPeriodRaw)
191+
}
192+
193+
title, err := cmd.Flags().GetString(cli.FlagTitle)
194+
if err != nil {
195+
return err
196+
}
197+
198+
description, err := cmd.Flags().GetString(cli.FlagSummary)
199+
if err != nil {
200+
return err
201+
}
202+
203+
exemptAddresses, err := cmd.Flags().GetStringSlice(flagExemptAddresses)
204+
if err != nil {
205+
return err
206+
}
207+
208+
prop := &types.SetBridgeTransferLimitProposal{
209+
Title: title,
210+
Description: description,
211+
Token: token,
212+
Limit: limit,
213+
LimitPeriod: types.LimitPeriod(limitPeriod),
214+
ExemptAddresses: exemptAddresses,
215+
}
216+
217+
from := cliCtx.GetFromAddress()
218+
219+
deposit, err := getDeposit(cmd)
220+
if err != nil {
221+
return err
222+
}
223+
224+
msg, err := govv1beta1types.NewMsgSubmitProposal(prop, deposit, from)
225+
if err != nil {
226+
return err
227+
}
228+
229+
return tx.GenerateOrBroadcastTxCLI(cliCtx, cmd.Flags(), msg)
230+
},
231+
}
232+
233+
cmd.Flags().StringSlice(flagExemptAddresses, []string{},
234+
"Comma separated list of addresses exempt from the bridge tax. Can be passed multiple times.")
235+
236+
applyFlags(cmd)
237+
return cmd
238+
}

x/gravity/keeper/genesis.go

+22-8
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ func InitGenesis(ctx context.Context, k Keeper, data types.GenesisState) {
142142
panic(err)
143143
}
144144
}
145+
146+
if data.BridgeTransferLimits != nil {
147+
for _, limit := range data.BridgeTransferLimits {
148+
if err := k.SetBridgeTransferLimit(ctx, limit); err != nil {
149+
panic(err)
150+
}
151+
}
152+
}
145153
}
146154

147155
// ExportGenesis exports all the state needed to restart the chain
@@ -236,14 +244,20 @@ func ExportGenesis(ctx context.Context, k Keeper) types.GenesisState {
236244
panic(err)
237245
}
238246

247+
limits, err := k.AllBridgeTransferLimits(ctx)
248+
if err != nil && !errors.Is(err, keeperutil.ErrNotFound) {
249+
panic(err)
250+
}
251+
239252
return types.GenesisState{
240-
Params: &p,
241-
GravityNonces: nonces,
242-
Batches: extBatches,
243-
BatchConfirms: batchconfs,
244-
Attestations: attestations,
245-
Erc20ToDenoms: erc20ToDenoms,
246-
UnbatchedTransfers: unbatchedTxs,
247-
BridgeTax: tax,
253+
Params: &p,
254+
GravityNonces: nonces,
255+
Batches: extBatches,
256+
BatchConfirms: batchconfs,
257+
Attestations: attestations,
258+
Erc20ToDenoms: erc20ToDenoms,
259+
UnbatchedTransfers: unbatchedTxs,
260+
BridgeTax: tax,
261+
BridgeTransferLimits: limits,
248262
}
249263
}

x/gravity/keeper/genesis_test.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
"cosmossdk.io/math"
99
sdk "github.com/cosmos/cosmos-sdk/types"
10-
"github.com/palomachain/paloma/testutil/nullify"
1110
"github.com/palomachain/paloma/x/gravity/types"
1211
"github.com/stretchr/testify/require"
1312
)
@@ -152,6 +151,14 @@ func TestGenesis(t *testing.T) {
152151
ExcludedTokens: []string{"test"},
153152
ExemptAddresses: addresses,
154153
},
154+
BridgeTransferLimits: []*types.BridgeTransferLimit{
155+
{
156+
Token: "test",
157+
Limit: math.NewInt(1000),
158+
LimitPeriod: types.LimitPeriod_DAILY,
159+
ExemptAddresses: addresses,
160+
},
161+
},
155162
}
156163

157164
input := CreateTestEnv(t)
@@ -160,13 +167,11 @@ func TestGenesis(t *testing.T) {
160167
got := ExportGenesis(input.Context, input.GravityKeeper)
161168
require.NotNil(t, got)
162169

163-
nullify.Fill(&genesisState)
164-
nullify.Fill(got)
165-
166170
require.Equal(t, genesisState.BridgeTax, got.BridgeTax)
171+
require.Equal(t, genesisState.BridgeTransferLimits, got.BridgeTransferLimits)
167172
}
168173

169-
func TestGenesisEmptyBridgeTax(t *testing.T) {
174+
func TestGenesisEmptyOptionalValues(t *testing.T) {
170175
genesisState := types.GenesisState{
171176
Params: types.DefaultParams(),
172177
}
@@ -177,8 +182,6 @@ func TestGenesisEmptyBridgeTax(t *testing.T) {
177182
got := ExportGenesis(input.Context, input.GravityKeeper)
178183
require.NotNil(t, got)
179184

180-
nullify.Fill(&genesisState)
181-
nullify.Fill(got)
182-
183185
require.Nil(t, got.BridgeTax)
186+
require.Empty(t, got.BridgeTransferLimits)
184187
}

x/gravity/keeper/governance_proposals.go

+19
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@ func NewGravityProposalHandler(k Keeper) govv1beta1types.Handler {
3434
}
3535

3636
return k.SetBridgeTax(ctx, bridgeTax)
37+
case *types.SetBridgeTransferLimitProposal:
38+
addresses := make([]sdk.AccAddress, 0, len(c.ExemptAddresses))
39+
for _, addr := range c.ExemptAddresses {
40+
address, err := sdk.AccAddressFromBech32(addr)
41+
if err != nil {
42+
return err
43+
}
44+
45+
addresses = append(addresses, address)
46+
}
47+
48+
limit := &types.BridgeTransferLimit{
49+
Token: c.Token,
50+
Limit: c.Limit,
51+
LimitPeriod: c.LimitPeriod,
52+
ExemptAddresses: addresses,
53+
}
54+
55+
return k.SetBridgeTransferLimit(ctx, limit)
3756
}
3857

3958
return sdkerrors.ErrUnknownRequest

0 commit comments

Comments
 (0)