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