diff --git a/src/extensions/HelloWormholeConfirmation.sol b/src/extensions/HelloWormholeConfirmation.sol new file mode 100644 index 0000000..b558615 --- /dev/null +++ b/src/extensions/HelloWormholeConfirmation.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; +import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol"; +import "wormhole-solidity-sdk/WormholeRelayerSDK.sol"; + +contract HelloWormholeConfirmation is Base, IWormholeReceiver { + event GreetingReceived(string greeting, uint16 senderChain, address sender); + event GreetingSuccess(string greeting, address sender); + + uint256 constant SENDING_GAS_LIMIT = 550_000; + uint256 constant CONFIRMATION_GAS_LIMIT = 50_000; + + string public latestGreeting; + string public latestConfirmedSentGreeting; + + uint16 chainId; + + enum MessageType {GREETING, CONFIRMATION} + + constructor(address _wormholeRelayer, address _wormhole) Base(_wormholeRelayer, _wormhole) {} + + function quoteCrossChainGreeting(uint16 targetChain, uint256 receiverValue) public view returns (uint256 cost) { + (cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, SENDING_GAS_LIMIT); + } + + // receiverValueForSecondDeliveryPayment will be determined in a front-end calculation (by calling quoteConfirmation on the target chain) + // We recommend baking in a buffer to account for the possibility of the price of targetChain->sourceChain changing during the sourceChain->targetChain delivery + function sendCrossChainGreeting(uint16 targetChain, address targetAddress, string memory greeting, uint256 receiverValueForSecondDeliveryPayment) public payable { + uint256 cost = quoteCrossChainGreeting(targetChain, receiverValueForSecondDeliveryPayment); + require(msg.value == cost); + + wormholeRelayer.sendPayloadToEvm{value: cost}( + targetChain, + targetAddress, + abi.encode(MessageType.GREETING, greeting, msg.sender), // payload + receiverValueForSecondDeliveryPayment, // will be used to pay for the confirmation + SENDING_GAS_LIMIT, + // we add a refund chain and address as the requester of the cross chain greeting + // The refund from this 'send' will be tacked on to the confirmation delivery + // (because we will request the confirmation using the 'forward' feature) + chainId, + msg.sender + ); + } + + function quoteConfirmation(uint16 targetChain) public view returns (uint256 cost) { + (cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, CONFIRMATION_GAS_LIMIT); + } + + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory, // additionalVaas + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) + public + payable + override + onlyWormholeRelayer + isRegisteredSender(sourceChain, sourceAddress) + replayProtect(deliveryHash) + { + MessageType msgType = abi.decode(payload, (MessageType)); + + if(msgType == MessageType.GREETING) { + (,string memory greeting, address sender) = abi.decode(payload, (MessageType, string, address)); + latestGreeting = greeting; + emit GreetingReceived(latestGreeting, sourceChain, sender); + + uint256 confirmationCost = quoteConfirmation(sourceChain); + require(msg.value >= confirmationCost, "Didn't receive enough value for the forward!"); + wormholeRelayer.forwardPayloadToEvm{value: msg.value}( + sourceChain, + fromWormholeFormat(sourceAddress), + abi.encode(MessageType.CONFIRMATION, greeting, sender), + 0, + CONFIRMATION_GAS_LIMIT + ); + } else if(msgType == MessageType.CONFIRMATION) { + (,string memory greeting, address sender) = abi.decode(payload, (MessageType, string, address)); + emit GreetingSuccess(greeting, sender); + latestConfirmedSentGreeting = greeting; + } + } +} \ No newline at end of file diff --git a/test/extensions/HelloWormholeConfirmation.t.sol b/test/extensions/HelloWormholeConfirmation.t.sol new file mode 100644 index 0000000..84ac494 --- /dev/null +++ b/test/extensions/HelloWormholeConfirmation.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "../../src/extensions/HelloWormholeConfirmation.sol"; + +import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; + +contract HelloWormholeConfirmationTest is WormholeRelayerBasicTest { + event GreetingReceived(string greeting, uint16 senderChain, address sender); + + HelloWormholeConfirmation helloSource; + HelloWormholeConfirmation helloTarget; + + function setUpSource() public override { + helloSource = new HelloWormholeConfirmation(address(relayerSource), address(wormholeSource)); + } + + function setUpTarget() public override { + helloTarget = new HelloWormholeConfirmation(address(relayerTarget), address(wormholeTarget)); + } + + function performRegistrations() public { + vm.selectFork(targetFork); + helloTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(helloSource))); + + vm.selectFork(sourceFork); + helloSource.setRegisteredSender(targetChain, toWormholeFormat(address(helloTarget))); + } + + function testGreeting() public { + + performRegistrations(); + + // Front-end calculation for how much receiver value to request the greeting with + // to ensure a confirmation is able to come back! + vm.selectFork(targetFork); + // We bake in a 10% buffer to account for the possibility of a price change after the initial delivery but before the return delivery + uint256 receiverValueForConfirmation = helloTarget.quoteConfirmation(sourceChain) * 11 / 10; + vm.selectFork(sourceFork); + // end front-end calculation + + uint256 cost = helloSource.quoteCrossChainGreeting(targetChain, receiverValueForConfirmation); + + vm.recordLogs(); + + helloSource.sendCrossChainGreeting{value: cost}(targetChain, address(helloTarget), "Hello Wormhole!", receiverValueForConfirmation); + + performDelivery(); + + vm.selectFork(targetFork); + assertEq(helloTarget.latestGreeting(), "Hello Wormhole!"); + + performDelivery(); + + vm.selectFork(sourceFork); + assertEq(helloSource.latestConfirmedSentGreeting(), "Hello Wormhole!"); + } +}