TransactionManager - Relayer may use user's cancel after expiry signature to steal user's funds by colluding with a router  Acknowledged
Users that are willing to have a lower trust dependency on a relayer should have the ability to opt-in only for the service that allows the relayer to withdraw back users' funds from the sending chain after expiry. However, in practice, a user is forced to opt-in for the service that refunds the router before the expiry, since the same signature is used for both services (lines 795,817 use the same signature).\\nLet's consider the case of a user willing to call `fulfill` on his own, but to use the relayer only to withdraw back his funds from the sending chain after expiry. In this case, the relayer can collude with the router and use the user's `cancel` signature (meant for withdrawing his only after expiry) as a front-running transaction for a user call to `fulfill`. This way the router will be able to withdraw both his funds and the user's funds since the user's `fulfill` signature is now public data residing in the mem-pool.\\n```\\n      require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, "cancel", signature) == txData.user, "#C:022");\\n\\n      Asset.transferAsset(txData.sendingAssetId, payable(msg.sender), relayerFee);\\n    }\\n\\n    // Get the amount to refund the user\\n    uint256 toRefund;\\n    unchecked {\\n      toRefund = amount - relayerFee;\\n    }\\n\\n    // Return locked funds to sending chain fallback\\n    if (toRefund > 0) {\\n      Asset.transferAsset(txData.sendingAssetId, payable(txData.sendingChainFallback), toRefund);\\n    }\\n  }\\n\\n} else {\\n  // Receiver side, router liquidity is returned\\n  if (txData.expiry >= block.timestamp) {\\n    // Timeout has not expired and tx may only be cancelled by user\\n    // Validate signature\\n    require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, "cancel", signature) == txData.user, "#C:022");\\n```\\n
The crucial point here is that the user must never sign a “cancel” that could be used on the receiving chain while fulfillment on the sending chain is still a possibility.\\nOr, to put it differently: A user may only sign a “cancel” that is valid on the receiving chain after sending-chain expiry or if they never have and won't ever sign a “fulfill” (or at least won't sign until sending-chain expiry — but it is pointless to sign a “fulfill” after that, so “never” is a reasonable simplification).\\nOr, finally, a more symmetric perspective on this requirement: If a user has signed “fulfill”, they must not sign a receiving-chain-valid “cancel” until sending-chain expiry, and if they have signed a receiving-chain-valid “cancel”, they must not sign a “fulfill” (until sending-chain expiry).\\nIn this sense, “cancel” signatures that are valid on the receiving chain are dangerous, while sending-side cancellations are not. So the principle stated in the previous paragraph might be easier to follow with different signatures for sending- and receiving-chain cancellations.
```\\n      require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, "cancel", signature) == txData.user, "#C:022");\\n\\n      Asset.transferAsset(txData.sendingAssetId, payable(msg.sender), relayerFee);\\n    }\\n\\n    // Get the amount to refund the user\\n    uint256 toRefund;\\n    unchecked {\\n      toRefund = amount - relayerFee;\\n    }\\n\\n    // Return locked funds to sending chain fallback\\n    if (toRefund > 0) {\\n      Asset.transferAsset(txData.sendingAssetId, payable(txData.sendingChainFallback), toRefund);\\n    }\\n  }\\n\\n} else {\\n  // Receiver side, router liquidity is returned\\n  if (txData.expiry >= block.timestamp) {\\n    // Timeout has not expired and tx may only be cancelled by user\\n    // Validate signature\\n    require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, "cancel", signature) == txData.user, "#C:022");\\n```\\n