// SendPacket implements source callbacks for sending packets.
// It defers to the underlying application and then calls the contract callback.
// If the contract callback returns an error, panics, or runs out of gas, then
// the packet send is rejected.
func (im IBCMiddleware) SendPacket(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
sourcePort string,
sourceChannel string,
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
data []byte,
) (uint64, error) {
// run underlying app logic first
// IBCActor logic will postprocess
seq, err := im.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data)
if err != nil {
return 0, err
}
// use underlying app to get source callback information from packet data
callbackData, err := types.GetSourceCallbackData(im.app, data, sourcePort, ctx.GasMeter().GasRemaining(), im.maxCallbackGas)
// SendPacket is not blocked if the packet does not opt-in to callbacks
if err != nil {
return seq, nil
}
callbackExecutor := func(cachedCtx sdk.Context) error {
return im.contractKeeper.IBCSendPacketCallback(
cachedCtx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data, callbackData.CallbackAddress, callbackData.SenderAddress,
)
}
err = im.processCallback(ctx, types.CallbackTypeSendPacket, callbackData, callbackExecutor)
// contract keeper is allowed to reject the packet send.
if err != nil {
return 0, err
}
types.EmitCallbackEvent(ctx, sourcePort, sourceChannel, seq, types.CallbackTypeSendPacket, callbackData, nil)
return seq, nil
}
// WriteAcknowledgement implements the ReceivePacket destination callbacks for the ibc-callbacks middleware
// during asynchronous packet acknowledgement.
// It defers to the underlying application and then calls the contract callback.
// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are
// reverted via a panic.
func (im IBCMiddleware) WriteAcknowledgement(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
packet ibcexported.PacketI,
ack ibcexported.Acknowledgement,
) error {
// run underlying app logic first
// IBCActor logic will postprocess
err := im.ics4Wrapper.WriteAcknowledgement(ctx, chanCap, packet, ack)
if err != nil {
return err
}
// use underlying app to get destination callback information from packet data
callbackData, err := types.GetDestCallbackData(
im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
)
// WriteAcknowledgement is not blocked if the packet does not opt-in to callbacks
if err != nil {
return nil
}
callbackExecutor := func(cachedCtx sdk.Context) error {
return im.contractKeeper.IBCReceivePacketCallback(cachedCtx, packet, ack, callbackData.CallbackAddress)
}
// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
err = im.processCallback(ctx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
// emit events
types.EmitCallbackEvent(
ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
types.CallbackTypeAcknowledgementPacket, callbackData, err,
)
return nil
}
// Call the IBCActor recvPacket callback after processing the packet
// if the recvPacket callback exists. If the callback returns an error
// then return an error ack to revert all packet data processing.
func (im IBCMiddleware) OnRecvPacket(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) (ack exported.Acknowledgement) {
// run underlying app logic first
// IBCActor logic will postprocess
ack := im.app.OnRecvPacket(ctx, packet, relayer)
// if ack is nil (asynchronous acknowledgements), then the callback will be handled in WriteAcknowledgement
// if ack is not successful, all state changes are reverted. If a packet cannot be received, then there is
// no need to execute a callback on the receiving chain.
if ack == nil || !ack.Success() {
return ack
}
// use underlying app to get destination callback information from packet data
callbackData, err := types.GetDestCallbackData(
im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
)
// OnRecvPacket is not blocked if the packet does not opt-in to callbacks
if err != nil {
return ack
}
callbackExecutor := func(cachedCtx sdk.Context) error {
return im.contractKeeper.IBCReceivePacketCallback(cachedCtx, packet, ack, callbackData.CallbackAddress)
}
// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
err = im.processCallback(ctx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
types.EmitCallbackEvent(
ctx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(),
types.CallbackTypeReceivePacket, callbackData, err,
)
if err != nil {
return channeltypes.NewErrorAcknowledgement(err)
}
return ack
}
// Call the IBCActor acknowledgementPacket callback after processing the packet
// if the ackPacket callback exists and returns an error
// DO NOT return the error upstream. The acknowledgement must complete for the packet
// lifecycle to end, so the custom callback cannot block completion.
// Instead we emit error events and set the error in state
// so that users and on-chain logic can handle this appropriately
func (im IBCModule) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
acknowledgement []byte,
relayer sdk.AccAddress,
) error {
// we first call the underlying app to handle the acknowledgement
// IBCActor logic will postprocess
err := im.app.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
if err != nil {
return err
}
// use underlying app to get source callback information from packet data
callbackData, err := types.GetSourceCallbackData(
im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
)
// OnAcknowledgementPacket is not blocked if the packet does not opt-in to callbacks
if err != nil {
return nil
}
callbackExecutor := func(cachedCtx sdk.Context) error {
return im.contractKeeper.IBCOnAcknowledgementPacketCallback(
cachedCtx, packet, acknowledgement, relayer, callbackData.CallbackAddress, callbackData.SenderAddress,
)
}
// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
err = im.processCallback(ctx, types.CallbackTypeAcknowledgementPacket, callbackData, callbackExecutor)
types.EmitCallbackEvent(
ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
types.CallbackTypeAcknowledgementPacket, callbackData, err,
)
return nil
}
// Call the IBCActor timeoutPacket callback after processing the packet
// if the timeoutPacket callback exists and returns an error
// DO NOT return the error upstream. The timeout must complete for the packet
// lifecycle to end, so the custom callback cannot block completion.
// Instead we emit error events and set the error in state
// so that users and on-chain logic can handle this appropriately
func (im IBCModule) OnTimeoutPacket(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) error {
// application-specific onTimeoutPacket logic
err := im.app.OnTimeoutPacket(ctx, packet, relayer)
if err != nil {
return err
}
// use underlying app to get source callback information from packet data
callbackData, err := types.GetSourceCallbackData(
im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
)
// OnTimeoutPacket is not blocked if the packet does not opt-in to callbacks
if err != nil {
return nil
}
callbackExecutor := func(cachedCtx sdk.Context) error {
return im.contractKeeper.IBCOnTimeoutPacketCallback(cachedCtx, packet, relayer, callbackData.CallbackAddress, callbackData.SenderAddress)
}
// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
err = im.processCallback(ctx, types.CallbackTypeTimeoutPacket, callbackData, callbackExecutor)
types.EmitCallbackEvent(
ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
types.CallbackTypeTimeoutPacket, callbackData, err,
)
return nil
}
// processCallback executes the callbackExecutor and reverts contract changes if the callbackExecutor fails.
//
// Error Precedence and Returns:
// - oogErr: Takes the highest precedence. If the callback runs out of gas, an error wrapped with types.ErrCallbackOutOfGas is returned.
// - panicErr: Takes the second-highest precedence. If a panic occurs and it is not propagated, an error wrapped with types.ErrCallbackPanic is returned.
// - callbackErr: If the callbackExecutor returns an error, it is returned as-is.
//
// panics if
// - the contractExecutor panics for any reason, and the callbackType is SendPacket, or
// - the contractExecutor runs out of gas and the relayer has not reserved gas grater than or equal to
// CommitGasLimit.
func (IBCMiddleware) processCallback(
ctx sdk.Context, callbackType types.CallbackType,
callbackData types.CallbackData, callbackExecutor func(sdk.Context) error,
) (err error) {
cachedCtx, writeFn := ctx.CacheContext()
cachedCtx = cachedCtx.WithGasMeter(storetypes.NewGasMeter(callbackData.ExecutionGasLimit))
defer func() {
// consume the minimum of g.consumed and g.limit
ctx.GasMeter().ConsumeGas(cachedCtx.GasMeter().GasConsumedToLimit(), fmt.Sprintf("ibc %s callback", callbackType))
// recover from all panics except during SendPacket callbacks
if r := recover(); r != nil {
if callbackType == types.CallbackTypeSendPacket {
panic(r)
}
err = errorsmod.Wrapf(types.ErrCallbackPanic, "ibc %s callback panicked with: %v", callbackType, r)
}
// if the callback ran out of gas and the relayer has not reserved enough gas, then revert the state
if cachedCtx.GasMeter().IsPastLimit() {
if callbackData.AllowRetry() {
panic(storetypes.ErrorOutOfGas{Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", callbackType, callbackData.CommitGasLimit)})
}
err = errorsmod.Wrapf(types.ErrCallbackOutOfGas, "ibc %s callback out of gas", callbackType)
}
// allow the transaction to be committed, continuing the packet lifecycle
}()
err = callbackExecutor(cachedCtx)
if err == nil {
writeFn()
}
return err
}