Summary
x/crosschain/keeper/v2_zevm_inbound.go getZRC20InboundDetails loads the foreign coin entry but never reads ForeignCoins.Paused before driving the V2 inbound into ValidateInbound / InitiateOutbound. Pause enforcement on the V2 path currently relies entirely on the second EVM hook checkPausedZRC20 reverting the tx when the paused ZRC20 contract is a log emitter.
This is fine for Withdrawn and WithdrawnAndCalled because GatewayZEVM._withdrawZRC20WithGasLimit calls transferFrom and burn on the user-supplied ZRC20 and the resulting Transfer logs trip the hook.
It does not cover Called / NoAssetCall with an ERC20-type ZRC20 used as routing key. The gateway only burns the destination chain's gas-token ZRC20, the user-supplied (paused) ZRC20 is touched only via the view function withdrawGasFeeWithGasLimit, and no logs come from the paused contract. checkPausedZRC20 does not fire, the CCTX persists in PendingOutbound, and an outbound no-asset call is signed against a paused route.
No asset extraction. The residual outbound carries no value and is paid in the unpaused gas token. Reported as part of HackenProof ZCNode-253 (report) and tracked here as a defense-in-depth fix.
Suggested fix
Gate getZRC20InboundDetails on the paused flag so the contract is honored end-to-end across every V2 event, not only the ones that incidentally emit logs from the paused ZRC20.
foreignCoin, found := k.fungibleKeeper.GetForeignCoins(ctx, zrc20.Hex())
if !found {
ctx.Logger().Info(fmt.Sprintf("cannot find foreign coin associated to the zrc20 address %s", zrc20.Hex()))
return InboundDetails{}, nil
}
if foreignCoin.Paused {
return InboundDetails{}, errorsmod.Wrapf(
fungibletypes.ErrPausedZRC20,
"zrc20 %s is paused",
zrc20.Hex(),
)
}
Optionally, when coinType == NoAssetCall, also check the destination chain's gas-token ZRC20 paused flag (mirroring the x/fungible/keeper/abort.go pattern), so pausing either the routing ZRC20 or the chain's gas token disables the call. This folds naturally into the broader refactor tracked in #2627.
Add regression tests in x/crosschain/keeper/v2_zevm_inbound_test.go for paused Called, Withdrawn, WithdrawnAndCalled plus unpaused controls.
Related
Summary
x/crosschain/keeper/v2_zevm_inbound.gogetZRC20InboundDetailsloads the foreign coin entry but never readsForeignCoins.Pausedbefore driving the V2 inbound intoValidateInbound/InitiateOutbound. Pause enforcement on the V2 path currently relies entirely on the second EVM hookcheckPausedZRC20reverting the tx when the paused ZRC20 contract is a log emitter.This is fine for
WithdrawnandWithdrawnAndCalledbecauseGatewayZEVM._withdrawZRC20WithGasLimitcallstransferFromandburnon the user-supplied ZRC20 and the resultingTransferlogs trip the hook.It does not cover
Called / NoAssetCallwith an ERC20-type ZRC20 used as routing key. The gateway only burns the destination chain's gas-token ZRC20, the user-supplied (paused) ZRC20 is touched only via the view functionwithdrawGasFeeWithGasLimit, and no logs come from the paused contract.checkPausedZRC20does not fire, the CCTX persists inPendingOutbound, and an outbound no-asset call is signed against a paused route.No asset extraction. The residual outbound carries no value and is paid in the unpaused gas token. Reported as part of HackenProof ZCNode-253 (report) and tracked here as a defense-in-depth fix.
Suggested fix
Gate
getZRC20InboundDetailson the paused flag so the contract is honored end-to-end across every V2 event, not only the ones that incidentally emit logs from the paused ZRC20.Optionally, when
coinType == NoAssetCall, also check the destination chain's gas-token ZRC20 paused flag (mirroring thex/fungible/keeper/abort.gopattern), so pausing either the routing ZRC20 or the chain's gas token disables the call. This folds naturally into the broader refactor tracked in #2627.Add regression tests in
x/crosschain/keeper/v2_zevm_inbound_test.gofor pausedCalled,Withdrawn,WithdrawnAndCalledplus unpaused controls.Related
x/fungible/keeper/abort.go:100-102)