@@ -22,6 +22,7 @@ import {Wormhole} from "../wormhole/ethereum/contracts/Wormhole.sol";
22
22
import {IWormhole} from "../contracts/interfaces/IWormhole.sol " ;
23
23
import {WormholeSimulator} from "./WormholeSimulator.sol " ;
24
24
import {IWormholeReceiver} from "../contracts/interfaces/IWormholeReceiver.sol " ;
25
+ import {AttackForwardIntegration} from "../contracts/mock/AttackForwardIntegration.sol " ;
25
26
import {MockRelayerIntegration} from "../contracts/mock/MockRelayerIntegration.sol " ;
26
27
import "../contracts/libraries/external/BytesLib.sol " ;
27
28
@@ -495,6 +496,106 @@ contract TestCoreRelayer is Test {
495
496
assertTrue (keccak256 (setup.source.integration.getMessage ()) == keccak256 (bytes ("received! " )));
496
497
}
497
498
499
+ function testAttackForwardRequestCache (GasParameters memory gasParams , FeeParameters memory feeParams ) public {
500
+ // General idea:
501
+ // 1. Attacker sets up a malicious integration contract in the target chain.
502
+ // 2. Attacker requests a message send to `target` chain.
503
+ // The message destination and the refund address are both the malicious integration contract in the target chain.
504
+ // 3. The delivery of the message triggers a refund to the malicious integration contract.
505
+ // 4. During the refund, the integration contract activates the forwarding mechanism.
506
+ // This is allowed due to the integration contract also being the target of the delivery.
507
+ // 5. The forward request is left as is in the `CoreRelayer` state.
508
+ // 6. The next message (i.e. the victim's message) delivery on `target` chain, from any relayer, using any `RelayProvider` and any integration contract,
509
+ // will see the forward request placed by the malicious integration contract and act on it.
510
+ // Caveat: the delivery of the victim's message must not invoke the forwarding mechanism for the attack test to be meaningful.
511
+ //
512
+ // In essence, this tries to attack the shared forwarding request cache present in the contract state.
513
+ // This attack doesn't work thanks to the check inside the `requestForward` function that only allows requesting a forward when there is a delivery being processed.
514
+
515
+ StandardSetupTwoChains memory setup = standardAssumeAndSetupTwoChains (gasParams, feeParams, 1000000 );
516
+
517
+ // Collected funds from the attack are meant to be sent here.
518
+ address attackerSourceAddress =
519
+ address (uint160 (uint256 (keccak256 (abi.encodePacked (bytes ("attackerAddress " ), setup.sourceChainId)))));
520
+ assertTrue (attackerSourceAddress.balance == 0 );
521
+
522
+ // Borrowed assumes from testForward. They should help since this test is similar.
523
+ vm.assume (
524
+ uint256 (1 ) * gasParams.targetGasPrice * feeParams.targetNativePrice
525
+ > uint256 (1 ) * gasParams.sourceGasPrice * feeParams.sourceNativePrice
526
+ );
527
+
528
+ vm.assume (
529
+ setup.source.coreRelayer.quoteGasDeliveryFee (
530
+ setup.targetChainId, gasParams.targetGasLimit, setup.source.relayProvider
531
+ ) < uint256 (2 ) ** 222
532
+ );
533
+ vm.assume (
534
+ setup.target.coreRelayer.quoteGasDeliveryFee (setup.sourceChainId, 500000 , setup.target.relayProvider)
535
+ < uint256 (2 ) ** 222 / feeParams.targetNativePrice
536
+ );
537
+
538
+ // Estimate the cost based on the initialized values
539
+ uint256 computeBudget = setup.source.coreRelayer.quoteGasDeliveryFee (
540
+ setup.targetChainId, gasParams.targetGasLimit, setup.source.relayProvider
541
+ );
542
+
543
+ {
544
+ AttackForwardIntegration attackerContract =
545
+ new AttackForwardIntegration (setup.target.wormhole, setup.target.coreRelayer, setup.targetChainId, attackerSourceAddress);
546
+ bytes memory attackMsg = "attack " ;
547
+
548
+ vm.recordLogs ();
549
+
550
+ // The attacker requests the message to be sent to the malicious contract.
551
+ // It is critical that the refund and destination (aka integrator) addresses are the same.
552
+ setup.source.integration.sendMessage {value: computeBudget + 2 * setup.source.wormhole.messageFee ()}(
553
+ attackMsg, setup.targetChainId, address (attackerContract), address (attackerContract)
554
+ );
555
+
556
+ // The relayer triggers the call to the malicious contract.
557
+ genericRelayer (setup.sourceChainId, 2 );
558
+
559
+ // The message delivery should fail
560
+ assertTrue (keccak256 (setup.target.integration.getMessage ()) != keccak256 (attackMsg));
561
+ }
562
+
563
+ {
564
+ // Now one victim sends their message. It doesn't need to be from the same source chain.
565
+ // What's necessary is that a message is delivered to the chain targeted by the attacker.
566
+ bytes memory victimMsg = "relay my message " ;
567
+
568
+ uint256 victimBalancePreDelivery = setup.target.refundAddress.balance;
569
+
570
+ // We will reutilize the compute budget estimated for the attacker to simplify the code here.
571
+ // The victim requests their message to be sent.
572
+ setup.source.integration.sendMessage {value: computeBudget + 2 * setup.source.wormhole.messageFee ()}(
573
+ victimMsg, setup.targetChainId, address (setup.target.integration), address (setup.target.refundAddress)
574
+ );
575
+
576
+ // The relayer delivers the victim's message.
577
+ // During the delivery process, the forward request injected by the malicious contract is acknowledged.
578
+ // The victim's refund address is not called due to this.
579
+ genericRelayer (setup.sourceChainId, 2 );
580
+
581
+ // Ensures the message was received.
582
+ assertTrue (keccak256 (setup.target.integration.getMessage ()) == keccak256 (victimMsg));
583
+ // Here we assert that the victim's refund is safe.
584
+ assertTrue (victimBalancePreDelivery < setup.target.refundAddress.balance);
585
+ }
586
+
587
+ Vm.Log[] memory entries = relayerWormholeSimulator.fetchWormholeMessageFromLog (vm.getRecordedLogs ());
588
+ if (entries.length > 0 ) {
589
+ // There was a wormhole message produced.
590
+ // If the attack is successful this is a forward.
591
+ // We'll invoke the relay simulation here and later assert that the attack wasn't successful.
592
+ // Relay from target chain to source chain.
593
+ genericRelayerProcessLogs (setup.targetChainId, entries);
594
+ }
595
+ // Assert that the attack wasn't successful.
596
+ assertTrue (attackerSourceAddress.balance == 0 );
597
+ }
598
+
498
599
function testRedelivery (GasParameters memory gasParams , FeeParameters memory feeParams , bytes memory message )
499
600
public
500
601
{
@@ -1219,18 +1320,34 @@ contract TestCoreRelayer is Test {
1219
1320
mapping (bytes32 => ICoreRelayer.TargetDeliveryParametersSingle) pastDeliveries;
1220
1321
1221
1322
function genericRelayer (uint16 chainId , uint8 num ) internal {
1222
- bytes [] memory encodedVMs = new bytes [](num);
1223
- {
1224
- // Filters all events to just the wormhole messages.
1225
- Vm.Log[] memory entries = relayerWormholeSimulator.fetchWormholeMessageFromLog (vm.getRecordedLogs ());
1226
- assertTrue (entries.length >= num);
1227
- for (uint256 i = 0 ; i < num; i++ ) {
1228
- encodedVMs[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs (
1229
- entries[i], chainId, address (uint160 (uint256 (bytes32 (entries[i].topics[1 ]))))
1230
- );
1231
- }
1323
+ Vm.Log[] memory entries = truncateRecordedLogs (chainId, num);
1324
+ genericRelayerProcessLogs (chainId, entries);
1325
+ }
1326
+
1327
+ /**
1328
+ * Discards wormhole events beyond `num` events.
1329
+ * Expects at least `num` wormhole events.
1330
+ */
1331
+ function truncateRecordedLogs (uint16 chainId , uint8 num ) internal returns (Vm.Log[] memory ) {
1332
+ // Filters all events to just the wormhole messages.
1333
+ Vm.Log[] memory entries = relayerWormholeSimulator.fetchWormholeMessageFromLog (vm.getRecordedLogs ());
1334
+ // We expect at least `num` events.
1335
+ assertTrue (entries.length >= num);
1336
+
1337
+ Vm.Log[] memory firstEntries = new Vm.Log [](num);
1338
+ for (uint256 i = 0 ; i < num; i++ ) {
1339
+ firstEntries[i] = entries[i];
1232
1340
}
1341
+ return firstEntries;
1342
+ }
1233
1343
1344
+ function genericRelayerProcessLogs (uint16 chainId , Vm.Log[] memory entries ) internal {
1345
+ bytes [] memory encodedVMs = new bytes [](entries.length );
1346
+ for (uint256 i = 0 ; i < encodedVMs.length ; i++ ) {
1347
+ encodedVMs[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs (
1348
+ entries[i], chainId, address (uint160 (uint256 (bytes32 (entries[i].topics[1 ]))))
1349
+ );
1350
+ }
1234
1351
IWormhole.VM[] memory parsed = new IWormhole.VM [](encodedVMs.length );
1235
1352
for (uint16 i = 0 ; i < encodedVMs.length ; i++ ) {
1236
1353
parsed[i] = relayerWormhole.parseVM (encodedVMs[i]);
0 commit comments