-
Notifications
You must be signed in to change notification settings - Fork 618
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
feat: allow ability to lock fee module in presence of severe bug #1031
Changes from 8 commits
f1b196b
1ff4fa9
b140255
5466343
fbce570
6974982
7399b52
3cc256f
b128ea0
e81e4e5
8e59f13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -230,6 +230,13 @@ func (im IBCModule) OnAcknowledgementPacket( | |
return sdkerrors.Wrapf(err, "cannot unmarshal ICS-29 incentivized packet acknowledgement: %v", ack) | ||
} | ||
|
||
if im.keeper.IsLocked(ctx) { | ||
// if the fee keeper is locked then fee logic should be skipped | ||
// this may occur in the presence of a severe bug which leads invalid state | ||
// the fee keeper will be unlocked after manual intervention | ||
return im.app.OnAcknowledgementPacket(ctx, packet, ack.Result, relayer) | ||
} | ||
|
||
packetID := channeltypes.NewPacketId(packet.SourceChannel, packet.SourcePort, packet.Sequence) | ||
feesInEscrow, found := im.keeper.GetFeesInEscrow(ctx, packetID) | ||
if !found { | ||
|
@@ -253,7 +260,10 @@ func (im IBCModule) OnTimeoutPacket( | |
packet channeltypes.Packet, | ||
relayer sdk.AccAddress, | ||
) error { | ||
if !im.keeper.IsFeeEnabled(ctx, packet.SourcePort, packet.SourceChannel) { | ||
// if the fee keeper is locked then fee logic should be skipped | ||
// this may occur in the presence of a severe bug which leads invalid state | ||
// the fee keeper will be unlocked after manual intervention | ||
if !im.keeper.IsFeeEnabled(ctx, packet.SourcePort, packet.SourceChannel) || im.keeper.IsLocked(ctx) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Document why we're just passing through to underlying app in case of locked module |
||
return im.app.OnTimeoutPacket(ctx, packet, relayer) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,6 +96,10 @@ func (k Keeper) DistributePacketFeesOnTimeout(ctx sdk.Context, timeoutRelayer sd | |
// If the distribution fails for any reason (such as the receiving address being blocked), | ||
// the state changes will be discarded. | ||
func (k Keeper) distributeFee(ctx sdk.Context, receiver sdk.AccAddress, fee sdk.Coins) { | ||
if k.IsLocked(ctx) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how much extra computation this does. It is a little annoying we need to check if the fee module is locked for each single fee distribution, but otherwise we have to rework the API of all our other functions to properly abort a transaction without returning an error There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's an extra state read. From a efficiency point of view, this will probs be cached so i wouldn't worry about it. But it is additional gas There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this line even necessary? I believe in both Ack and Timeout we're checking locked before we even call this function. Why don't we just add it as CONTRACT that we must check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it is. The module may become locked during fee distribution. I'd have to add checks around every call to distribute fee (7 calls total). If the balance becomes negative due to locking, I'd need to return immediately I find this to be much easier to reason about from a code point of view. I also don't like the idea of function contracts since they are unenforceable and prone to misuse There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I should actually go with a solution similar to this 2b9aa5d Since we now have multiple fees per ID, we probably want to revert all state changes if a negative balance would be reached and then return immediately. If I use the cached context then I can remove the is locked check since the entry points are blocked and the state changes are dropped upon locking the fee module during distribution It's partially annoying that this same format will be duplicated 3 times, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, I think caching and discarding a partially executed tx is much cleaner. We should cache the context and if the balance goes negative. Let's discard state changes and commit just the write to the locked flag |
||
return | ||
} | ||
|
||
// cache context before trying to distribute fees | ||
cacheCtx, writeFn := ctx.CacheContext() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,22 +124,62 @@ func (suite *KeeperTestSuite) TestDistributeFee() { | |
reverseRelayer sdk.AccAddress | ||
forwardRelayer string | ||
refundAcc sdk.AccAddress | ||
refundAccBal sdk.Coin | ||
fee types.Fee | ||
packetID channeltypes.PacketId | ||
) | ||
|
||
validSeq := uint64(1) | ||
|
||
testCases := []struct { | ||
name string | ||
malleate func() | ||
expPass bool | ||
name string | ||
malleate func() | ||
expResult func() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made these changes to test |
||
}{ | ||
{ | ||
"success", func() {}, true, | ||
"success", func() {}, func() { | ||
// check if the reverse relayer is paid | ||
hasBalance := suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), reverseRelayer, fee.AckFee[0].Add(fee.AckFee[0])) | ||
suite.Require().True(hasBalance) | ||
|
||
// check if the forward relayer is paid | ||
forward, err := sdk.AccAddressFromBech32(forwardRelayer) | ||
suite.Require().NoError(err) | ||
hasBalance = suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), forward, fee.RecvFee[0].Add(fee.RecvFee[0])) | ||
suite.Require().True(hasBalance) | ||
|
||
// check if the refund acc has been refunded the timeoutFee | ||
expectedRefundAccBal := refundAccBal.Add(fee.TimeoutFee[0].Add(fee.TimeoutFee[0])) | ||
hasBalance = suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), refundAcc, expectedRefundAccBal) | ||
suite.Require().True(hasBalance) | ||
|
||
// check the module acc wallet is now empty | ||
hasBalance = suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), suite.chainA.GetSimApp().IBCFeeKeeper.GetFeeModuleAddress(), sdk.Coin{Denom: sdk.DefaultBondDenom, Amount: sdk.NewInt(0)}) | ||
suite.Require().True(hasBalance) | ||
}, | ||
}, | ||
{ | ||
"invalid forward address", func() { | ||
forwardRelayer = "invalid address" | ||
}, false, | ||
}, | ||
func() { | ||
// check if the refund acc has been refunded the timeoutFee & onRecvFee | ||
expectedRefundAccBal := refundAccBal.Add(fee.TimeoutFee[0]).Add(fee.RecvFee[0]).Add(fee.TimeoutFee[0]).Add(fee.RecvFee[0]) | ||
hasBalance := suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), refundAcc, expectedRefundAccBal) | ||
suite.Require().True(hasBalance) | ||
|
||
}, | ||
}, | ||
{ | ||
"fee module is locked, no distribution occurs", func() { | ||
lockFeeModule(suite.chainA) | ||
}, | ||
func() { | ||
// check the module acc wallet still has the fee | ||
hasBalance := suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), suite.chainA.GetSimApp().IBCFeeKeeper.GetFeeModuleAddress(), fee.Total()[0]) | ||
suite.Require().True(hasBalance) | ||
|
||
}, | ||
}, | ||
} | ||
|
||
|
@@ -155,8 +195,8 @@ func (suite *KeeperTestSuite) TestDistributeFee() { | |
reverseRelayer = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) | ||
forwardRelayer = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()).String() | ||
|
||
packetID := channeltypes.NewPacketId(suite.path.EndpointA.ChannelID, suite.path.EndpointA.ChannelConfig.PortID, validSeq) | ||
fee := types.Fee{ | ||
packetID = channeltypes.NewPacketId(suite.path.EndpointA.ChannelID, suite.path.EndpointA.ChannelConfig.PortID, validSeq) | ||
fee = types.Fee{ | ||
RecvFee: defaultReceiveFee, | ||
AckFee: defaultAckFee, | ||
TimeoutFee: defaultTimeoutFee, | ||
|
@@ -174,35 +214,12 @@ func (suite *KeeperTestSuite) TestDistributeFee() { | |
tc.malleate() | ||
|
||
// refundAcc balance after escrow | ||
refundAccBal := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), refundAcc, sdk.DefaultBondDenom) | ||
refundAccBal = suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), refundAcc, sdk.DefaultBondDenom) | ||
|
||
suite.chainA.GetSimApp().IBCFeeKeeper.DistributePacketFees(suite.chainA.GetContext(), forwardRelayer, reverseRelayer, []types.PacketFee{packetFee, packetFee}) | ||
|
||
if tc.expPass { | ||
// check if the reverse relayer is paid | ||
hasBalance := suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), reverseRelayer, fee.AckFee[0].Add(fee.AckFee[0])) | ||
suite.Require().True(hasBalance) | ||
|
||
// check if the forward relayer is paid | ||
forward, err := sdk.AccAddressFromBech32(forwardRelayer) | ||
suite.Require().NoError(err) | ||
hasBalance = suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), forward, fee.RecvFee[0].Add(fee.RecvFee[0])) | ||
suite.Require().True(hasBalance) | ||
|
||
// check if the refund acc has been refunded the timeoutFee | ||
expectedRefundAccBal := refundAccBal.Add(fee.TimeoutFee[0].Add(fee.TimeoutFee[0])) | ||
hasBalance = suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), refundAcc, expectedRefundAccBal) | ||
suite.Require().True(hasBalance) | ||
tc.expResult() | ||
|
||
// check the module acc wallet is now empty | ||
hasBalance = suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), suite.chainA.GetSimApp().IBCFeeKeeper.GetFeeModuleAddress(), sdk.Coin{Denom: sdk.DefaultBondDenom, Amount: sdk.NewInt(0)}) | ||
suite.Require().True(hasBalance) | ||
} else { | ||
// check if the refund acc has been refunded the timeoutFee & onRecvFee | ||
expectedRefundAccBal := refundAccBal.Add(fee.TimeoutFee[0]).Add(fee.RecvFee[0]).Add(fee.TimeoutFee[0]).Add(fee.RecvFee[0]) | ||
hasBalance := suite.chainA.GetSimApp().BankKeeper.HasBalance(suite.chainA.GetContext(), refundAcc, expectedRefundAccBal) | ||
suite.Require().True(hasBalance) | ||
} | ||
}) | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,10 @@ func (k Keeper) RegisterCounterpartyAddress(goCtx context.Context, msg *types.Ms | |
func (k Keeper) PayPacketFee(goCtx context.Context, msg *types.MsgPayPacketFee) (*types.MsgPayPacketFeeResponse, error) { | ||
ctx := sdk.UnwrapSDKContext(goCtx) | ||
|
||
if k.IsLocked(ctx) { | ||
return nil, types.ErrFeeModuleLocked | ||
} | ||
|
||
// get the next sequence | ||
sequence, found := k.GetNextSequenceSend(ctx, msg.SourcePortId, msg.SourceChannelId) | ||
if !found { | ||
|
@@ -57,6 +61,10 @@ func (k Keeper) PayPacketFee(goCtx context.Context, msg *types.MsgPayPacketFee) | |
func (k Keeper) PayPacketFeeAsync(goCtx context.Context, msg *types.MsgPayPacketFeeAsync) (*types.MsgPayPacketFeeAsyncResponse, error) { | ||
ctx := sdk.UnwrapSDKContext(goCtx) | ||
|
||
if k.IsLocked(ctx) { | ||
return nil, types.ErrFeeModuleLocked | ||
} | ||
|
||
if err := k.EscrowPacketFee(ctx, msg.PacketId, msg.PacketFee); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we move the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return nil, err | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also document why we're still unmarshalling the acknowledgement because the counterparty is still sending the incentivized ack over