Skip to content

Commit

Permalink
feat: paloma bridge exit tax (#1196)
Browse files Browse the repository at this point in the history
# Related Github tickets

- VolumeFi#1709

# Background

We're going to charge a tax fee for every transfer out of paloma. The
tax rate is defined by governance vote and we can set exceptions to
tokens and addresses.

- Add new governance vote to gravity module
- The bridge tax information is added to genesis
- Before adding transfers to the batched pool in gravity, we set the tax
amount, if applicable. This amount is removed from the total amount to
transfer. Compass only sees the decreased amount.
- On receiving the transfer complete event we burn the total amount of
tokens (transferred + taxed)
- On cancelling a transfer, we return the total amount including tax

# Testing completed

- [x] test coverage exists or has been added/updated
- [ ] 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.
  • Loading branch information
maharifu authored Jun 19, 2024
1 parent 91cd199 commit d3bc02e
Show file tree
Hide file tree
Showing 31 changed files with 2,414 additions and 475 deletions.
10 changes: 2 additions & 8 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,13 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
go-version: '1.22'
cache: false
- name: Go lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.55.2
version: v1.59.1
args: --verbose
# Optional: if set to true then the all caching functionality will be complete disabled,
# takes precedence over all other caching options.
skip-cache: true

# Optional: if set to true then the action don't cache or restore ~/go/pkg.
skip-pkg-cache: true

# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
skip-build-cache: true
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
run:
go: '1.21'
go: '1.22'
issues-exit-code: 1
timeout: 10m
modules-download-mode: readonly
Expand Down Expand Up @@ -38,3 +38,4 @@ issues:
# gosec
- G101 # Potential hardcoded credentials
- G114 # Use of net/http serve function that has no support for setting timeouts
- G113 # Potential uncontrolled memory consumption in Rat.SetString - this is fixed since go 1.17.7
29 changes: 29 additions & 0 deletions docs/Gravity-Bridge-Tax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Gravity Bridge Tax

All outbound transactions from the bridge to the target EVM and other chains pay
a tax on the gravity bridge. This tax is added to the cost of the transfer.
A governance vote is needed to define the tax rate, as well as a list of tokens
and addresses that are exempt from the bridge tax.

## Tax Rate

The tax rate must be defined as a non-negative value, with 0 meaning no tax is
applied.

The tax is added to the cost of the transfer and will stay locked until the
transfer is finished.
If the transfer is successful, the taxed amount is burned on the Paloma side.
If a transfer is canceled before being executed, the full initial amount, plus
tax, is refunded.

## Excluded Tokens

The governance vote can define a list of tokens that are excluded from the
bridge tax. Transfers of these tokens will never pay bridge tax and will be
transferred in the full amount.

## Exempt Addresses

Similarly, the governance vote can define a list of addresses that are exempt
from paying the bridge tax. Transfers from these senders will never pay bridge
tax.
6 changes: 5 additions & 1 deletion proto/palomachain/paloma/gravity/batch.proto
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ message OutgoingTransferTx {
string sender = 2;
string dest_address = 3;
ERC20Token erc20_token = 4 [ (gogoproto.nullable) = false ];
string bridge_tax_amount = 5 [
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false
];
}

message EventOutgoingBatchCanceled {
Expand All @@ -39,4 +43,4 @@ message EventOutgoingBatch {
string batch_id = 3;
string nonce = 4;
string assignee = 5;
}
}
14 changes: 14 additions & 0 deletions proto/palomachain/paloma/gravity/bridge_tax.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";
package palomachain.paloma.gravity;

import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";

option go_package = "github.com/palomachain/paloma/x/gravity/types";

message BridgeTax {
string rate = 1 [ (cosmos_proto.scalar) = "cosmos.Dec" ];
repeated string excluded_tokens = 2;
repeated bytes exempt_addresses = 3
[ (gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress" ];
}
14 changes: 14 additions & 0 deletions proto/palomachain/paloma/gravity/bridge_tax_proposal.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";
package palomachain.paloma.gravity;

import "cosmos_proto/cosmos.proto";

option go_package = "github.com/palomachain/paloma/x/gravity/types";

message SetBridgeTaxProposal {
string title = 1;
string description = 2;
string rate = 3 [ (cosmos_proto.scalar) = "cosmos.Dec" ];
repeated string excluded_tokens = 4;
repeated string exempt_addresses = 5;
}
2 changes: 2 additions & 0 deletions proto/palomachain/paloma/gravity/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "gogoproto/gogo.proto";
import "palomachain/paloma/gravity/types.proto";
import "palomachain/paloma/gravity/msgs.proto";
import "palomachain/paloma/gravity/batch.proto";
import "palomachain/paloma/gravity/bridge_tax.proto";
import "palomachain/paloma/gravity/attestation.proto";
import "cosmos/base/v1beta1/coin.proto";
import "palomachain/paloma/gravity/params.proto";
Expand All @@ -21,6 +22,7 @@ message GenesisState {
repeated ERC20ToDenom erc20_to_denoms = 9 [ (gogoproto.nullable) = false ];
repeated OutgoingTransferTx unbatched_transfers = 10
[ (gogoproto.nullable) = false ];
BridgeTax BridgeTax = 11;
}

// GravityCounters contains the many noces and counters required to maintain the
Expand Down
10 changes: 10 additions & 0 deletions proto/palomachain/paloma/gravity/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import "palomachain/paloma/gravity/genesis.proto";
import "palomachain/paloma/gravity/types.proto";
import "palomachain/paloma/gravity/msgs.proto";
import "palomachain/paloma/gravity/params.proto";
import "palomachain/paloma/gravity/bridge_tax.proto";
import "google/protobuf/empty.proto";

option go_package = "github.com/palomachain/paloma/x/gravity/types";

Expand Down Expand Up @@ -42,6 +44,10 @@ service Query {

rpc GetPendingSendToEth(QueryPendingSendToEth)
returns (QueryPendingSendToEthResponse) {}

rpc GetBridgeTax(google.protobuf.Empty) returns (QueryBridgeTaxResponse) {
option (google.api.http).get = "/palomachain/paloma/gravity/bridge_tax";
}
}

message QueryParamsRequest {}
Expand Down Expand Up @@ -147,3 +153,7 @@ message QueryPendingSendToEthResponse {
repeated OutgoingTransferTx unbatched_transfers = 2
[ (gogoproto.nullable) = false ];
}

message QueryBridgeTaxResponse {
BridgeTax bridge_tax = 1;
}
32 changes: 32 additions & 0 deletions x/gravity/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/palomachain/paloma/x/gravity/types"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)

const (
Expand Down Expand Up @@ -35,6 +36,7 @@ func GetQueryCmd() *cobra.Command {
CmdGetLastObservedEthBlock(),
CmdGetLastObservedEthNonce(),
GetCmdQueryParams(),
GetCmdQueryBridgeTax(),
}...)

return gravityQueryCmd
Expand Down Expand Up @@ -334,3 +336,33 @@ func GetCmdQueryParams() *cobra.Command {
flags.AddQueryFlagsToCmd(cmd)
return cmd
}

// GetCmdQueryBridgeTax fetches the current Gravity module bridge tax settings
func GetCmdQueryBridgeTax() *cobra.Command {
cmd := &cobra.Command{
Use: "bridge-tax",
Short: "Query BridgeTax Settings",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

queryClient := types.NewQueryClient(clientCtx)

params := &emptypb.Empty{}

res, err := queryClient.GetBridgeTax(cmd.Context(), params)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}
81 changes: 81 additions & 0 deletions x/gravity/client/cli/tx_proposal.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cli

import (
"fmt"
"math/big"

"github.com/VolumeFi/whoops"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
Expand All @@ -12,13 +15,19 @@ import (
"github.com/spf13/cobra"
)

const (
flagExcludedTokens = "excluded-tokens"
flagExemptAddresses = "exempt-addresses"
)

func CmdGravityProposalHandler() *cobra.Command {
cmd := &cobra.Command{
Use: "gravity",
Short: "Gravity proposals",
}
cmd.AddCommand([]*cobra.Command{
CmdSetErc20ToDenom(),
CmdSetBridgeTax(),
}...)

return cmd
Expand Down Expand Up @@ -77,3 +86,75 @@ func CmdSetErc20ToDenom() *cobra.Command {
applyFlags(cmd)
return cmd
}

func CmdSetBridgeTax() *cobra.Command {
cmd := &cobra.Command{
Use: "set-bridge-tax [tax-rate]",
Short: "Sets the bridge tax rate, and optionally token exceptions and exempt addresses",
Long: "Each outgoing transfer from Paloma will pay a tax. Tax amount is calculated using [tax-rate], which must be non-negative.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

rateRaw := args[0]

rate, ok := new(big.Rat).SetString(rateRaw)
if !ok || rate.Sign() < 0 {
return fmt.Errorf("invalid tax rate: %s", rateRaw)
}

title, err := cmd.Flags().GetString(cli.FlagTitle)
if err != nil {
return err
}

description, err := cmd.Flags().GetString(cli.FlagTitle)
if err != nil {
return err
}

excludedTokens, err := cmd.Flags().GetStringSlice(flagExcludedTokens)
if err != nil {
return err
}

exemptAddresses, err := cmd.Flags().GetStringSlice(flagExemptAddresses)
if err != nil {
return err
}

prop := &types.SetBridgeTaxProposal{
Title: title,
Description: description,
Rate: rateRaw,
ExcludedTokens: excludedTokens,
ExemptAddresses: exemptAddresses,
}

from := cliCtx.GetFromAddress()

deposit, err := getDeposit(cmd)
if err != nil {
return err
}

msg, err := govv1beta1types.NewMsgSubmitProposal(prop, deposit, from)
if err != nil {
return err
}

return tx.GenerateOrBroadcastTxCLI(cliCtx, cmd.Flags(), msg)
},
}

cmd.Flags().StringSlice(flagExcludedTokens, []string{},
"Comma separated list of tokens excluded from the bridge tax. Can be passed multiple times.")
cmd.Flags().StringSlice(flagExemptAddresses, []string{},
"Comma separated list of addresses exempt from the bridge tax. Can be passed multiple times.")

applyFlags(cmd)
return cmd
}
4 changes: 3 additions & 1 deletion x/gravity/keeper/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ func (k Keeper) OutgoingTxBatchExecuted(ctx context.Context, tokenContract types

totalToBurn := math.NewInt(0)
for _, tx := range b.Transactions {
totalToBurn = totalToBurn.Add(tx.Erc20Token.Amount)
// We need to burn the total amount transferred, as well as the total
// amount taxed
totalToBurn = totalToBurn.Add(tx.Erc20Token.Amount).Add(tx.BridgeTaxAmount)
}

denom, err := k.GetDenomOfERC20(ctx, claim.GetChainReferenceId(), tokenContract)
Expand Down
Loading

0 comments on commit d3bc02e

Please sign in to comment.