395 lines
19 KiB
Solidity
395 lines
19 KiB
Solidity
// SPDX-License-Identifier: Apache 2
|
|
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "../interfaces/IWormholeReceiver.sol";
|
|
import "../interfaces/IDelivery.sol";
|
|
import "../interfaces/IForwardWrapper.sol";
|
|
import "./CoreRelayerGovernance.sol";
|
|
import "../interfaces/IWormholeRelayerInternalStructs.sol";
|
|
import "./CoreRelayerMessages.sol";
|
|
|
|
contract CoreRelayerDelivery is CoreRelayerGovernance {
|
|
enum DeliveryStatus {
|
|
SUCCESS,
|
|
RECEIVER_FAILURE,
|
|
FORWARD_REQUEST_FAILURE,
|
|
FORWARD_REQUEST_SUCCESS
|
|
}
|
|
|
|
event Delivery(
|
|
address indexed recipientContract,
|
|
uint16 indexed sourceChain,
|
|
uint64 indexed sequence,
|
|
bytes32 deliveryVaaHash,
|
|
DeliveryStatus status
|
|
);
|
|
|
|
/**
|
|
* - Checks if enough funds were passed into a forward
|
|
* - Increases the maxTransactionFee of the first forward in the MultichainSend container
|
|
* in order to use all of the funds
|
|
* - Publishes the DeliveryInstruction, with a 'sufficientlyFunded' flag indicating whether the forward had enough funds
|
|
* - If the forward was funded, pay the relayer's reward address to deliver the forward
|
|
*
|
|
* @param transactionFeeRefundAmount amount of maxTransactionFee that was unused
|
|
* @param forwardInstruction A struct containing information about the user's forward/multichainForward request
|
|
*
|
|
*/
|
|
function emitForward(
|
|
uint256 transactionFeeRefundAmount,
|
|
IWormholeRelayerInternalStructs.ForwardInstruction memory forwardInstruction
|
|
) internal {
|
|
IWormholeRelayerInternalStructs.DeliveryInstructionsContainer memory container =
|
|
decodeDeliveryInstructionsContainer(forwardInstruction.container);
|
|
|
|
// Add any additional funds which were passed in to the forward as msg.value
|
|
uint256 fundsForForward = transactionFeeRefundAmount + forwardInstruction.msgValue;
|
|
|
|
// Checks if enough funds were passed into the forward (should always be true as it was already checked)
|
|
if (fundsForForward < forwardInstruction.totalFee) {
|
|
revert IDelivery.ForwardNotSufficientlyFunded(fundsForForward, forwardInstruction.totalFee);
|
|
}
|
|
|
|
IRelayProvider relayProvider = IRelayProvider(forwardInstruction.relayProvider);
|
|
IWormhole wormhole = wormhole();
|
|
uint256 wormholeMessageFee = wormhole.messageFee();
|
|
|
|
// Increases the maxTransactionFee of the first forward in the MultichainSend container
|
|
// in order to use all of the funds
|
|
|
|
uint256 amountUnderMaximum = relayProvider.quoteMaximumBudget(container.instructions[0].targetChain)
|
|
- (container.instructions[0].maximumRefundTarget + container.instructions[0].receiverValueTarget);
|
|
uint256 convertedExtraAmount = calculateTargetDeliveryMaximumRefundHelper(
|
|
container.instructions[0].targetChain, fundsForForward - forwardInstruction.totalFee, 0, relayProvider
|
|
);
|
|
container.instructions[0].maximumRefundTarget +=
|
|
(amountUnderMaximum > convertedExtraAmount) ? convertedExtraAmount : amountUnderMaximum;
|
|
|
|
// Publishes the DeliveryInstruction
|
|
wormhole.publishMessage{value: wormholeMessageFee}(
|
|
0, encodeDeliveryInstructionsContainer(container), relayProvider.getConsistencyLevel()
|
|
);
|
|
|
|
// if funded, pay out reward to provider. Otherwise, the delivery code will handle sending a refund.
|
|
pay(relayProvider.getRewardAddress(), fundsForForward - wormholeMessageFee);
|
|
}
|
|
|
|
/**
|
|
* Performs the following actions:
|
|
* - Calls the 'receiveWormholeMessages' endpoint on the contract 'internalInstruction.targetAddress'
|
|
* (with the gas limit and value specified in internalInstruction, and 'encodedVMs' as the input)
|
|
*
|
|
* - Calculates how much of 'maxTransactionFee' is left
|
|
* - If the call succeeded and during execution of 'receiveWormholeMessages' there was a forward/multichainForward, then:
|
|
* if there is enough 'maxTransactionFee' left to execute the forward, then execute the forward
|
|
* else emit the forward instruction but with a flag (sufficientlyFunded = false) indicating that it wasn't paid for
|
|
* - else:
|
|
* refund any of the 'maxTransactionFee' not used to internalInstruction.refundAddress
|
|
* if the call reverted, refund the 'receiverValue' to internalInstruction.refundAddress
|
|
* - refund anything leftover to the relayer
|
|
*
|
|
* @param internalInstruction instruction to execute
|
|
* @param encodedVMs list of signed wormhole messages (VAAs)
|
|
* @param relayerRefundAddress address to send the relayer's refund to
|
|
* @param vaaInfo struct specifying:
|
|
* - sourceChain chain id that the delivery originated from
|
|
* - sourceSequence sequence number of the delivery VAA on the source chain
|
|
* - deliveryVaaHash hash of delivery VAA
|
|
*/
|
|
function _executeDelivery(
|
|
IWormholeRelayerInternalStructs.DeliveryInstruction memory internalInstruction,
|
|
bytes[] memory encodedVMs,
|
|
address payable relayerRefundAddress,
|
|
IWormholeRelayerInternalStructs.DeliveryVAAInfo memory vaaInfo
|
|
) internal {
|
|
if (internalInstruction.targetAddress != 0x0) {
|
|
if (isContractLocked()) {
|
|
revert IDelivery.ReentrantCall();
|
|
}
|
|
|
|
setContractLock(true);
|
|
setLockedTargetAddress(fromWormholeFormat(internalInstruction.targetAddress));
|
|
|
|
uint256 preGas = gasleft();
|
|
|
|
(bool callToInstructionExecutorSucceeded, bytes memory data) = getWormholeRelayerCallerAddress().call{
|
|
value: internalInstruction.receiverValueTarget
|
|
}(abi.encodeCall(IForwardWrapper.executeInstruction, (internalInstruction, encodedVMs)));
|
|
|
|
uint256 postGas = gasleft();
|
|
|
|
uint256 transactionFeeRefundAmount;
|
|
bool callToTargetContractSucceeded = true;
|
|
if (callToInstructionExecutorSucceeded) {
|
|
(callToTargetContractSucceeded, transactionFeeRefundAmount) = abi.decode(data, (bool, uint256));
|
|
} else {
|
|
// Calculate the amount of gas used in the call (upperbounding at the gas limit, which shouldn't have been exceeded)
|
|
uint256 gasUsed = (preGas - postGas) > internalInstruction.executionParameters.gasLimit
|
|
? internalInstruction.executionParameters.gasLimit
|
|
: (preGas - postGas);
|
|
|
|
// Calculate the amount of maxTransactionFee to refund (multiply the maximum refund by the fraction of gas unused)
|
|
transactionFeeRefundAmount = (internalInstruction.executionParameters.gasLimit - gasUsed)
|
|
* internalInstruction.maximumRefundTarget / internalInstruction.executionParameters.gasLimit;
|
|
}
|
|
|
|
// Retrieve the forward instruction created during execution of 'receiveWormholeMessages'
|
|
IWormholeRelayerInternalStructs.ForwardInstruction memory forwardInstruction = getForwardInstruction();
|
|
|
|
//clear forwarding request from storage
|
|
clearForwardInstruction();
|
|
|
|
// unlock the contract
|
|
setContractLock(false);
|
|
|
|
DeliveryStatus status;
|
|
if (forwardInstruction.isValid) {
|
|
// If the user made a forward/multichainForward request, then try to execute it
|
|
emitForward(transactionFeeRefundAmount, forwardInstruction);
|
|
status = DeliveryStatus.FORWARD_REQUEST_SUCCESS;
|
|
} else {
|
|
status = callToTargetContractSucceeded
|
|
? (callToInstructionExecutorSucceeded ? DeliveryStatus.SUCCESS : DeliveryStatus.FORWARD_REQUEST_FAILURE)
|
|
: DeliveryStatus.RECEIVER_FAILURE;
|
|
}
|
|
|
|
// Emit a status update that can be read by a SDK
|
|
emit Delivery({
|
|
recipientContract: fromWormholeFormat(internalInstruction.targetAddress),
|
|
sourceChain: vaaInfo.sourceChain,
|
|
sequence: vaaInfo.sourceSequence,
|
|
deliveryVaaHash: vaaInfo.deliveryVaaHash,
|
|
status: status
|
|
});
|
|
|
|
payRefunds(
|
|
internalInstruction,
|
|
relayerRefundAddress,
|
|
transactionFeeRefundAmount,
|
|
callToInstructionExecutorSucceeded && callToTargetContractSucceeded,
|
|
forwardInstruction.isValid
|
|
);
|
|
}
|
|
}
|
|
|
|
function payRefunds(
|
|
IWormholeRelayerInternalStructs.DeliveryInstruction memory internalInstruction,
|
|
address payable relayerRefundAddress,
|
|
uint256 transactionFeeRefundAmount,
|
|
bool receiverValueWasPaid,
|
|
bool forwardingRequestExists
|
|
) internal {
|
|
// Amount of receiverValue that is refunded to the user (0 if the call to 'receiveWormholeMessages' did not revert, or the full receiverValue otherwise)
|
|
uint256 receiverValueRefundAmount = (receiverValueWasPaid ? 0 : internalInstruction.receiverValueTarget);
|
|
|
|
// Total refund to the user
|
|
uint256 refundToRefundAddress =
|
|
receiverValueRefundAmount + (forwardingRequestExists ? 0 : transactionFeeRefundAmount);
|
|
|
|
// Whether or not the refund succeeded
|
|
bool refundPaidToRefundAddress = payRefundToRefundAddress(internalInstruction, refundToRefundAddress);
|
|
|
|
// Refund the relayer (their extra funds) + (the amount that the relayer spent on gas)
|
|
// + (the users refund if that refund didn't succeed)
|
|
uint256 relayerRefundAmount = (
|
|
msg.value - internalInstruction.receiverValueTarget - internalInstruction.maximumRefundTarget
|
|
) + (internalInstruction.maximumRefundTarget - transactionFeeRefundAmount)
|
|
+ (refundPaidToRefundAddress ? 0 : refundToRefundAddress);
|
|
|
|
pay(relayerRefundAddress, relayerRefundAmount);
|
|
}
|
|
|
|
function payRefundToRefundAddress(
|
|
IWormholeRelayerInternalStructs.DeliveryInstruction memory instruction,
|
|
uint256 refundAmount
|
|
) internal returns (bool refundPaidToRefundAddress) {
|
|
if (instruction.refundChain == chainId()) {
|
|
refundPaidToRefundAddress = pay(payable(fromWormholeFormat(instruction.refundAddress)), refundAmount);
|
|
} else {
|
|
IWormhole wormhole = wormhole();
|
|
uint256 wormholeMessageFee = wormhole.messageFee();
|
|
|
|
IRelayProvider provider = IRelayProvider(defaultRelayProvider());
|
|
|
|
uint256 overhead = wormholeMessageFee + provider.quoteDeliveryOverhead(instruction.refundChain);
|
|
|
|
if (provider.isChainSupported(instruction.refundChain) && (refundAmount > overhead)) {
|
|
wormhole.publishMessage{value: wormholeMessageFee}(
|
|
0,
|
|
encodeDeliveryInstructionsContainer(
|
|
getInstructionsForEmptyMessageWithReceiverValue(
|
|
instruction.refundChain, instruction.refundAddress, refundAmount - overhead, provider
|
|
)
|
|
),
|
|
provider.getConsistencyLevel()
|
|
);
|
|
|
|
pay(provider.getRewardAddress(), refundAmount - wormholeMessageFee);
|
|
|
|
refundPaidToRefundAddress = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function getInstructionsForEmptyMessageWithReceiverValue(
|
|
uint16 targetChain,
|
|
bytes32 targetAddress,
|
|
uint256 receiverValue,
|
|
IRelayProvider provider
|
|
) internal view returns (IWormholeRelayerInternalStructs.DeliveryInstructionsContainer memory container) {
|
|
container = convertMultichainSendToDeliveryInstructionsContainer(
|
|
multichainSendContainer(
|
|
IWormholeRelayer.Send({
|
|
targetChain: targetChain,
|
|
targetAddress: targetAddress,
|
|
refundChain: targetChain,
|
|
refundAddress: targetAddress,
|
|
maxTransactionFee: 0,
|
|
receiverValue: receiverValue,
|
|
relayParameters: bytes("")
|
|
}),
|
|
address(provider),
|
|
new IWormholeRelayer.MessageInfo[](0)
|
|
)
|
|
);
|
|
|
|
uint256 maximumBudget = provider.quoteMaximumBudget(targetChain);
|
|
if (container.instructions[0].receiverValueTarget > maximumBudget) {
|
|
container.instructions[0].receiverValueTarget = maximumBudget;
|
|
}
|
|
}
|
|
|
|
function verifyRelayerVM(IWormhole.VM memory vm) internal view returns (bool) {
|
|
return registeredCoreRelayerContract(vm.emitterChainId) == vm.emitterAddress;
|
|
}
|
|
|
|
/**
|
|
* @notice The relay provider calls 'deliver' to relay messages as described by one delivery instruction
|
|
*
|
|
* The instruction specifies the target chain (must be this chain), target address, refund address, maximum refund (in this chain's currency),
|
|
* receiver value (in this chain's currency), upper bound on gas, and the permissioned address allowed to execute this instruction
|
|
*
|
|
* The relay provider must pass in the signed wormhole messages (VAAs) from the source chain
|
|
* as well as the signed wormhole message with the delivery instructions (the delivery VAA)
|
|
* as well as identify which of the many instructions in the multichainSend container is meant to be executed
|
|
*
|
|
* The messages will be relayed to the target address (with the specified gas limit and receiver value) iff the following checks are met:
|
|
* - the delivery VAA has a valid signature
|
|
* - the delivery VAA's emitter is one of these CoreRelayer contracts
|
|
* - the delivery instruction container in the delivery VAA was fully funded
|
|
* - msg.sender is the permissioned address allowed to execute this instruction
|
|
* - the relay provider passed in at least [(one wormhole message fee) + instruction.maximumRefundTarget + instruction.receiverValueTarget] of this chain's currency as msg.value
|
|
* - the instruction's target chain is this chain
|
|
* - the relayed signed VAAs match the descriptions in container.messages (the VAA hashes match, or the emitter address, sequence number pair matches, depending on the description given)
|
|
*
|
|
* @param targetParams struct containing the signed wormhole messages and encoded delivery instruction container (and other information)
|
|
*/
|
|
function deliver(IDelivery.TargetDeliveryParameters memory targetParams) public payable {
|
|
IWormhole wormhole = wormhole();
|
|
|
|
// Obtain the delivery VAA
|
|
(IWormhole.VM memory deliveryVM, bool valid, string memory reason) =
|
|
wormhole.parseAndVerifyVM(targetParams.encodedDeliveryVAA);
|
|
|
|
// Check that the delivery VAA has a valid signature
|
|
if (!valid) {
|
|
revert IDelivery.InvalidDeliveryVaa(reason);
|
|
}
|
|
|
|
// Check that the delivery VAA's emitter is one of these CoreRelayer contracts
|
|
if (!verifyRelayerVM(deliveryVM)) {
|
|
revert IDelivery.InvalidEmitter();
|
|
}
|
|
|
|
IWormholeRelayerInternalStructs.DeliveryInstructionsContainer memory container =
|
|
decodeDeliveryInstructionsContainer(deliveryVM.payload);
|
|
|
|
// Obtain the specific instruction that is intended to be executed in this function
|
|
// specifying the the target chain (must be this chain), target address, refund address, maximum refund (in this chain's currency),
|
|
// receiverValue (in this chain's currency), upper bound on gas
|
|
IWormholeRelayerInternalStructs.DeliveryInstruction memory deliveryInstruction =
|
|
container.instructions[targetParams.multisendIndex];
|
|
|
|
// Check that the relay provider passed in at least [(one wormhole message fee) + instruction.maximumRefund + instruction.receiverValue] of this chain's currency as msg.value
|
|
if (msg.value < deliveryInstruction.maximumRefundTarget + deliveryInstruction.receiverValueTarget) {
|
|
revert IDelivery.InsufficientRelayerFunds();
|
|
}
|
|
|
|
// Check that the instruction's target chain is this chain
|
|
if (chainId() != deliveryInstruction.targetChain) {
|
|
revert IDelivery.TargetChainIsNotThisChain(deliveryInstruction.targetChain);
|
|
}
|
|
|
|
// Check that the relayed signed VAAs match the descriptions in container.messages (the VAA hashes match, or the emitter address, sequence number pair matches, depending on the description given)
|
|
checkMessageInfosWithVAAs(container.messageInfos, targetParams.encodedVMs);
|
|
|
|
_executeDelivery(
|
|
deliveryInstruction,
|
|
targetParams.encodedVMs,
|
|
targetParams.relayerRefundAddress,
|
|
IWormholeRelayerInternalStructs.DeliveryVAAInfo({
|
|
sourceChain: deliveryVM.emitterChainId,
|
|
sourceSequence: deliveryVM.sequence,
|
|
deliveryVaaHash: deliveryVM.hash
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice checkMessageInfosWithVAAs checks that the array of signed VAAs 'signedVaas' matches the descriptions
|
|
* given by the array of MessageInfo structs 'messageInfos'
|
|
*
|
|
* @param messageInfos Array of MessageInfo structs, each describing a wormhole message (VAA)
|
|
* @param signedVaas Array of signed wormhole messages (signed VAAs)
|
|
*/
|
|
function checkMessageInfosWithVAAs(IWormholeRelayer.MessageInfo[] memory messageInfos, bytes[] memory signedVaas)
|
|
internal
|
|
view
|
|
{
|
|
if (messageInfos.length != signedVaas.length) {
|
|
revert IDelivery.MessageInfosLengthDoesNotMatchVaasLength();
|
|
}
|
|
for (uint8 i = 0; i < messageInfos.length; i++) {
|
|
if (!messageInfoMatchesVAA(messageInfos[i], signedVaas[i])) {
|
|
revert IDelivery.MessageInfosDoNotMatchVaas(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice messageInfosWithVAAs checks that signedVaa matches the description given by 'messageInfo'
|
|
* Specifically, if 'messageInfo.infoType' is MessageInfoType.EMITTER_SEQUENCE, then we check
|
|
* if the emitterAddress and sequence match
|
|
* else if 'messageInfo.infoType' is MessageInfoType.EMITTER_SEQUENCE, then we check if the VAA hash matches
|
|
*
|
|
* @param messageInfo MessageInfo struct describing a wormhole message (VAA)
|
|
* @param signedVaa signed wormhole message
|
|
*/
|
|
function messageInfoMatchesVAA(IWormholeRelayer.MessageInfo memory messageInfo, bytes memory signedVaa)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
IWormhole.VM memory parsedVaa = wormhole().parseVM(signedVaa);
|
|
if (messageInfo.infoType == IWormholeRelayer.MessageInfoType.EMITTER_SEQUENCE) {
|
|
return
|
|
(messageInfo.emitterAddress == parsedVaa.emitterAddress) && (messageInfo.sequence == parsedVaa.sequence);
|
|
} else if (messageInfo.infoType == IWormholeRelayer.MessageInfoType.VAAHASH) {
|
|
return (messageInfo.vaaHash == parsedVaa.hash);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function pay(address payable receiver, uint256 amount) internal returns (bool success) {
|
|
if (amount > 0) {
|
|
(success,) = receiver.call{value: amount}("");
|
|
} else {
|
|
success = true;
|
|
}
|
|
}
|
|
|
|
receive() external payable {}
|
|
}
|