Skip to content

Commit

Permalink
feat: bridging fee middleware (#899)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtsitrin authored Jun 5, 2024
1 parent 73a8a1d commit a74ffb0
Show file tree
Hide file tree
Showing 23 changed files with 469 additions and 84 deletions.
6 changes: 5 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"path/filepath"

"github.com/dymensionxyz/dymension/v3/x/bridging_fee"
vfchooks "github.com/dymensionxyz/dymension/v3/x/vfc/hooks"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -739,9 +740,11 @@ func New(
)

transferModule := ibctransfer.NewAppModule(app.TransferKeeper)
transferMiddleware := ibctransfer.NewIBCModule(app.TransferKeeper)

var transferStack ibcporttypes.IBCModule
transferStack = ibctransfer.NewIBCModule(app.TransferKeeper)
transferStack = bridging_fee.NewIBCMiddleware(transferMiddleware, app.IBCKeeper.ChannelKeeper, app.DelayedAckKeeper, app.TransferKeeper, app.AccountKeeper.GetModuleAddress(txfeestypes.ModuleName))

transferStack = packetforwardmiddleware.NewIBCMiddleware(
transferStack,
app.PacketForwardMiddlewareKeeper,
Expand Down Expand Up @@ -1025,6 +1028,7 @@ func (app *App) ModuleAccountAddrs() map[string]bool {

// exclude the streamer as we want him to be able to get external incentives
modAccAddrs[authtypes.NewModuleAddress(streamermoduletypes.ModuleName).String()] = false
modAccAddrs[authtypes.NewModuleAddress(txfeestypes.ModuleName).String()] = false
return modAccAddrs
}

Expand Down
2 changes: 1 addition & 1 deletion app/apptesting/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ func (s *KeeperTestHelper) ExtractAttributes(event sdk.Event) map[string]string
func (s *KeeperTestHelper) AssertAttributes(event sdk.Event, eventAttributes []sdk.Attribute) {
attrs := s.ExtractAttributes(event)
for _, attr := range eventAttributes {
s.Require().Equal(attr.Value, attrs[attr.Key])
s.Assert().Equal(attr.Value, attrs[attr.Key])
}
}
105 changes: 105 additions & 0 deletions ibctesting/bridging_fee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package ibctesting_test

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/ibc-go/v6/modules/apps/transfer/types"
clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types"
ibctesting "github.com/cosmos/ibc-go/v6/testing"
"github.com/osmosis-labs/osmosis/v15/x/txfees"
"github.com/stretchr/testify/suite"
)

type BridgingFeeTestSuite struct {
IBCTestUtilSuite
}

func TestBridgingFeeTestSuite(t *testing.T) {
suite.Run(t, new(BridgingFeeTestSuite))
}

func (suite *BridgingFeeTestSuite) SetupTest() {
suite.IBCTestUtilSuite.SetupTest()
}

func (suite *BridgingFeeTestSuite) TestNotRollappNoBridgingFee() {
// setup between cosmosChain and hubChain
path := suite.NewTransferPath(suite.hubChain, suite.cosmosChain)
suite.coordinator.Setup(path)
hubEndpoint := path.EndpointA
cosmosEndpoint := path.EndpointB

timeoutHeight := clienttypes.NewHeight(100, 110)
amount, ok := sdk.NewIntFromString("10000000000000000000") // 10DYM
suite.Require().True(ok)
coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, amount)

// send from cosmosChain to hubChain
msg := types.NewMsgTransfer(cosmosEndpoint.ChannelConfig.PortID, cosmosEndpoint.ChannelID, coinToSendToB, cosmosEndpoint.Chain.SenderAccount.GetAddress().String(), hubEndpoint.Chain.SenderAccount.GetAddress().String(), timeoutHeight, 0, "")
res, err := cosmosEndpoint.Chain.SendMsgs(msg)
suite.Require().NoError(err) // message committed
packet, err := ibctesting.ParsePacketFromEvents(res.GetEvents())
suite.Require().NoError(err)
err = path.RelayPacket(packet)
suite.Require().NoError(err) // relay committed

denom := suite.GetRollappToHubIBCDenomFromPacket(packet)
finalBalance := ConvertToApp(suite.hubChain).BankKeeper.GetBalance(suite.hubChain.GetContext(), suite.hubChain.SenderAccount.GetAddress(), denom)
suite.Assert().Equal(sdk.NewCoin(denom, coinToSendToB.Amount), finalBalance)
}

func (suite *BridgingFeeTestSuite) TestBridgingFee() {
path := suite.NewTransferPath(suite.hubChain, suite.rollappChain)
suite.coordinator.Setup(path)

rollappEndpoint := path.EndpointB
rollappIBCKeeper := suite.rollappChain.App.GetIBCKeeper()

suite.CreateRollapp()
suite.RegisterSequencer()
suite.GenesisEvent(path.EndpointA.ChannelID)

// Update rollapp state
currentRollappBlockHeight := uint64(suite.rollappChain.GetContext().BlockHeight())
suite.UpdateRollappState(currentRollappBlockHeight)

timeoutHeight := clienttypes.NewHeight(100, 110)
amount, ok := sdk.NewIntFromString("10000000000000000000") // 10DYM
suite.Require().True(ok)
coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, amount)

/* --------------------- initiating transfer on rollapp --------------------- */
msg := types.NewMsgTransfer(rollappEndpoint.ChannelConfig.PortID, rollappEndpoint.ChannelID, coinToSendToB, suite.rollappChain.SenderAccount.GetAddress().String(), suite.hubChain.SenderAccount.GetAddress().String(), timeoutHeight, 0, "")
res, err := suite.rollappChain.SendMsgs(msg)
suite.Require().NoError(err) // message committed
packet, err := ibctesting.ParsePacketFromEvents(res.GetEvents())
suite.Require().NoError(err)
found := rollappIBCKeeper.ChannelKeeper.HasPacketCommitment(rollappEndpoint.Chain.GetContext(), packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
suite.Require().True(found)
err = path.RelayPacket(packet)
suite.Require().Error(err) // expecting error as no AcknowledgePacket expected to return

// check balance before finalization
denom := suite.GetRollappToHubIBCDenomFromPacket(packet)
transferredCoins := sdk.NewCoin(denom, coinToSendToB.Amount)
recipient := suite.hubChain.SenderAccount.GetAddress()
initialBalance := ConvertToApp(suite.hubChain).BankKeeper.SpendableCoins(suite.hubChain.GetContext(), recipient)
suite.Require().Equal(initialBalance.AmountOf(denom), sdk.ZeroInt())

// Finalize the rollapp state
currentRollappBlockHeight = uint64(suite.rollappChain.GetContext().BlockHeight())
_, err = suite.FinalizeRollappState(1, currentRollappBlockHeight)
suite.Require().NoError(err)

// check balance after finalization
expectedFee := ConvertToApp(suite.hubChain).DelayedAckKeeper.BridgingFeeFromAmt(suite.hubChain.GetContext(), transferredCoins.Amount)
expectedBalance := initialBalance.Add(transferredCoins).Sub(sdk.NewCoin(denom, expectedFee))
finalBalance := ConvertToApp(suite.hubChain).BankKeeper.SpendableCoins(suite.hubChain.GetContext(), recipient)
suite.Assert().Equal(expectedBalance, finalBalance)

// check fees
addr := ConvertToApp(suite.hubChain).AccountKeeper.GetModuleAccount(suite.hubChain.GetContext(), txfees.ModuleName)
txFeesBalance := ConvertToApp(suite.hubChain).BankKeeper.GetBalance(suite.hubChain.GetContext(), addr.GetAddress(), denom)
suite.Assert().Equal(expectedFee, txFeesBalance.Amount)
}
1 change: 1 addition & 0 deletions ibctesting/eibc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func (suite *EIBCTestSuite) SetupTest() {
delayedAckKeeper := ConvertToApp(suite.hubChain).DelayedAckKeeper
params := delayedAckKeeper.GetParams(suite.hubChain.GetContext())
params.EpochIdentifier = "month"
params.BridgingFee = sdk.ZeroDec()
delayedAckKeeper.SetParams(suite.hubChain.GetContext(), params)
}

Expand Down
6 changes: 6 additions & 0 deletions proto/dymension/delayedack/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ message Params {
option (gogoproto.goproto_stringer) = false;
string epoch_identifier = 1
[ (gogoproto.moretags) = "yaml:\"epoch_identifier\"" ];

string bridging_fee = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.moretags) = "yaml:\"bridging_fee\"",
(gogoproto.nullable) = false
];
}
6 changes: 6 additions & 0 deletions x/bridging_fee/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package bridging_fee

const (
EventTypeBridgingFee = "bridging_fee"
AttributeKeyFee = "fee"
)
109 changes: 109 additions & 0 deletions x/bridging_fee/ibc_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package bridging_fee

import (
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
transfer "github.com/cosmos/ibc-go/v6/modules/apps/transfer"
transferkeeper "github.com/cosmos/ibc-go/v6/modules/apps/transfer/keeper"
channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types"
porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types"
"github.com/cosmos/ibc-go/v6/modules/core/exported"

delayedaackkeeper "github.com/dymensionxyz/dymension/v3/x/delayedack/keeper"
)

const (
ModuleName = "bridging_fee"
)

var _ porttypes.Middleware = &BridgingFeeMiddleware{}

// BridgingFeeMiddleware implements the ICS26 callbacks
// The middleware is responsible for charging a bridging fee on transfers coming from rollapps
// The actual charge happens on the packet finalization
// based on ADR: https://www.notion.so/dymension/ADR-x-Bridging-Fee-Middleware-7ba8c191373f43ce81782fc759913299?pvs=4
type BridgingFeeMiddleware struct {
transfer.IBCModule
porttypes.ICS4Wrapper

delayedAckKeeper delayedaackkeeper.Keeper
transferKeeper transferkeeper.Keeper
feeModuleAddr sdk.AccAddress
}

// NewIBCMiddleware creates a new IBCMiddleware given the keeper and underlying application
func NewIBCMiddleware(transfer transfer.IBCModule, channelKeeper porttypes.ICS4Wrapper, keeper delayedaackkeeper.Keeper, transferKeeper transferkeeper.Keeper, feeModuleAddr sdk.AccAddress) *BridgingFeeMiddleware {
return &BridgingFeeMiddleware{
IBCModule: transfer,
ICS4Wrapper: channelKeeper,
delayedAckKeeper: keeper,
transferKeeper: transferKeeper,
feeModuleAddr: feeModuleAddr,
}
}

// GetFeeRecipient returns the address that will receive the bridging fee
func (im BridgingFeeMiddleware) GetFeeRecipient() sdk.AccAddress {
return im.feeModuleAddr
}

func (im *BridgingFeeMiddleware) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) exported.Acknowledgement {
if !im.delayedAckKeeper.IsRollappsEnabled(ctx) {
return im.IBCModule.OnRecvPacket(ctx, packet, relayer)
}
logger := ctx.Logger().With(
"module", ModuleName,
"packet_source", packet.SourcePort,
"packet_destination", packet.DestinationPort,
"packet_sequence", packet.Sequence)

rollappPortOnHub, rollappChannelOnHub := packet.DestinationPort, packet.DestinationChannel
rollappID, transferPacketData, err := im.delayedAckKeeper.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub)
if err != nil {
logger.Error("Failed to extract rollapp id from packet", "err", err)
return channeltypes.NewErrorAcknowledgement(err)
}

if rollappID == "" {
logger.Debug("Skipping IBC transfer OnRecvPacket for non-rollapp chain")
return im.IBCModule.OnRecvPacket(ctx, packet, relayer)
}

// parse the transfer amount
transferAmount, ok := sdk.NewIntFromString(transferPacketData.Amount)
if !ok {
err = errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "parse transfer amount into math.Int")
return channeltypes.NewErrorAcknowledgement(err)
}

// get fee
fee := im.delayedAckKeeper.BridgingFeeFromAmt(ctx, transferAmount)

// update packet data for the fee charge
feePacket := *transferPacketData
feePacket.Amount = fee.String()
feePacket.Receiver = im.GetFeeRecipient().String()

// No event emitted, as we called the transfer keeper directly (vs the transfer middleware)
err = im.transferKeeper.OnRecvPacket(ctx, packet, feePacket)
if err != nil {
logger.Error("Failed to charge bridging fee", "err", err)
// we continue as we don't want the fee charge to fail the transfer in any case
fee = sdk.ZeroInt()
} else {
logger.Debug("Charged bridging fee", "fee", fee)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
EventTypeBridgingFee,
sdk.NewAttribute(AttributeKeyFee, fee.String()),
sdk.NewAttribute(sdk.AttributeKeySender, transferPacketData.Sender),
),
)
}

// transfer the remaining amount
transferPacketData.Amount = transferAmount.Sub(fee).String()
packet.Data = transferPacketData.GetBytes()
return im.IBCModule.OnRecvPacket(ctx, packet, relayer)
}
11 changes: 9 additions & 2 deletions x/delayedack/eibc.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (im IBCMiddleware) eIBCDemandOrderHandler(ctx sdk.Context, rollappPacket co
}
}

eibcDemandOrder, err := im.createDemandOrderFromIBCPacket(data, &rollappPacket, *packetMetaData.EIBC)
eibcDemandOrder, err := im.createDemandOrderFromIBCPacket(ctx, data, &rollappPacket, *packetMetaData.EIBC)
if err != nil {
return fmt.Errorf("create eibc demand order: %w", err)
}
Expand All @@ -75,7 +75,7 @@ func (im IBCMiddleware) eIBCDemandOrderHandler(ctx sdk.Context, rollappPacket co
// It validates the fungible token packet data, extracts the fee from the memo,
// calculates the demand order price, and creates a new demand order.
// It returns the created demand order or an error if there is any.
func (im IBCMiddleware) createDemandOrderFromIBCPacket(fungibleTokenPacketData transfertypes.FungibleTokenPacketData,
func (im IBCMiddleware) createDemandOrderFromIBCPacket(ctx sdk.Context, fungibleTokenPacketData transfertypes.FungibleTokenPacketData,
rollappPacket *commontypes.RollappPacket, eibcMetaData types.EIBCMetadata,
) (*eibctypes.DemandOrder, error) {
// Validate the fungible token packet data as we're going to use it to create the demand order
Expand All @@ -98,6 +98,7 @@ func (im IBCMiddleware) createDemandOrderFromIBCPacket(fungibleTokenPacketData t
if amt.LT(fee) {
return nil, fmt.Errorf("fee cannot be larger than amount: fee: %s: amt :%s", fee, fungibleTokenPacketData.Amount)
}

/*
In case of timeout/errack:
fee = fee_multiplier*transfer_amount
Expand Down Expand Up @@ -125,6 +126,12 @@ func (im IBCMiddleware) createDemandOrderFromIBCPacket(fungibleTokenPacketData t
demandOrderDenom = trace.IBCDenom()
demandOrderRecipient = fungibleTokenPacketData.Sender // and who tried to send it (refund because it failed)
case commontypes.RollappPacket_ON_RECV:
bridgingFee := im.keeper.BridgingFeeFromAmt(ctx, amt)
if bridgingFee.GT(fee) {
// We check that the fee the fulfiller makes is at least as big as the bridging fee they will have to pay later
// this is to improve UX and help fulfillers not lose money.
return nil, fmt.Errorf("eibc fee cannot be smaller than bridging fee: eibc fee: %s: bridging fee: %s", fee, bridgingFee)
}
demandOrderDenom = im.getEIBCTransferDenom(*rollappPacket.Packet, fungibleTokenPacketData)
demandOrderRecipient = fungibleTokenPacketData.Receiver // who we tried to send to
}
Expand Down
13 changes: 13 additions & 0 deletions x/delayedack/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/require"
)

var defBridgingFee = types.DefaultParams().BridgingFee

func TestInitGenesis(t *testing.T) {
tests := []struct {
name string
Expand All @@ -22,14 +24,24 @@ func TestInitGenesis(t *testing.T) {
name: "only params - success",
params: types.Params{
EpochIdentifier: "week",
BridgingFee: defBridgingFee,
},
rollappPackets: []commontypes.RollappPacket{},
expPanic: false,
},
{
name: "only params - missing bridging fee - fail",
params: types.Params{
EpochIdentifier: "week",
},
rollappPackets: []commontypes.RollappPacket{},
expPanic: true,
},
{
name: "params and rollapp packets - panic",
params: types.Params{
EpochIdentifier: "week",
BridgingFee: defBridgingFee,
},
rollappPackets: []commontypes.RollappPacket{{RollappId: "0"}},
expPanic: true,
Expand Down Expand Up @@ -58,6 +70,7 @@ func TestExportGenesis(t *testing.T) {
// Set params
params := types.Params{
EpochIdentifier: "week",
BridgingFee: defBridgingFee,
}
k.SetParams(ctx, params)
// Set some demand orders
Expand Down
Loading

0 comments on commit a74ffb0

Please sign in to comment.