wormhole/ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerDelivery.sol

551 lines
23 KiB
Solidity

// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.19;
import {IWormhole} from "../../interfaces/IWormhole.sol";
import {
InvalidDeliveryVaa,
InvalidEmitter,
InsufficientRelayerFunds,
TargetChainIsNotThisChain,
MessageKeysLengthDoesNotMatchMessagesLength,
VaaKeysDoNotMatchVaas,
InvalidOverrideGasLimit,
InvalidOverrideReceiverValue,
RequesterNotWormholeRelayer,
DeliveryProviderCannotReceivePayment,
MessageKey,
VAA_KEY_TYPE,
VaaKey,
IWormholeRelayerDelivery,
IWormholeRelayerSend,
RETURNDATA_TRUNCATION_THRESHOLD
} from "../../interfaces/relayer/IWormholeRelayerTyped.sol";
import {IWormholeReceiver} from "../../interfaces/relayer/IWormholeReceiver.sol";
import {IDeliveryProvider} from "../../interfaces/relayer/IDeliveryProviderTyped.sol";
import {pay, pay, min, toWormholeFormat, fromWormholeFormat, returnLengthBoundedCall, returnLengthBoundedCall} from "../../relayer/libraries/Utils.sol";
import {
DeliveryInstruction,
DeliveryOverride,
EvmDeliveryInstruction
} from "../../relayer/libraries/RelayerInternalStructs.sol";
import {BytesParsing} from "../../relayer/libraries/BytesParsing.sol";
import {WormholeRelayerSerde} from "./WormholeRelayerSerde.sol";
import {
DeliverySuccessState,
DeliveryFailureState,
getDeliverySuccessState,
getDeliveryFailureState
} from "./WormholeRelayerStorage.sol";
import {WormholeRelayerBase} from "./WormholeRelayerBase.sol";
import "../../interfaces/relayer/TypedUnits.sol";
import "../../relayer/libraries/ExecutionParameters.sol";
uint256 constant QUOTE_LENGTH_BYTES = 32;
uint256 constant GAS_LIMIT_EXTERNAL_CALL = 100_000;
abstract contract WormholeRelayerDelivery is WormholeRelayerBase, IWormholeRelayerDelivery {
using WormholeRelayerSerde for *;
using BytesParsing for bytes;
using WeiLib for Wei;
using GasLib for Gas;
using GasPriceLib for GasPrice;
using TargetNativeLib for TargetNative;
using LocalNativeLib for LocalNative;
function deliver(
bytes[] memory encodedVMs,
bytes memory encodedDeliveryVAA,
address payable relayerRefundAddress,
bytes memory deliveryOverrides
) public payable nonReentrant {
// Parse and verify VAA containing delivery instructions, revert if invalid
(IWormhole.VM memory vm, bool valid, string memory reason) =
getWormhole().parseAndVerifyVM(encodedDeliveryVAA);
if (!valid) {
revert InvalidDeliveryVaa(reason);
}
// Revert if the emitter of the VAA is not a Wormhole Relayer contract
bytes32 registeredWormholeRelayer = getRegisteredWormholeRelayerContract(vm.emitterChainId);
if (vm.emitterAddress != registeredWormholeRelayer) {
revert InvalidEmitter(vm.emitterAddress, registeredWormholeRelayer, vm.emitterChainId);
}
DeliveryInstruction memory instruction = vm.payload.decodeDeliveryInstruction();
// Record information about the delivery's refund in temporary storage
recordRefundInformation(
instruction.refundChain,
instruction.refundAddress
);
DeliveryVAAInfo memory deliveryVaaInfo = DeliveryVAAInfo({
sourceChain: vm.emitterChainId,
sourceSequence: vm.sequence,
deliveryVaaHash: vm.hash,
relayerRefundAddress: relayerRefundAddress,
encodedVMs: encodedVMs,
deliveryInstruction: instruction,
gasLimit: Gas.wrap(0),
targetChainRefundPerGasUnused: GasPrice.wrap(0),
totalReceiverValue: TargetNative.wrap(0),
encodedOverrides: deliveryOverrides,
redeliveryHash: bytes32(0)
});
// Decode information from the execution parameters
// (overriding them if there was an override requested)
// Assumes execution parameters and info are of version EVM_V1
(
deliveryVaaInfo.gasLimit,
deliveryVaaInfo.targetChainRefundPerGasUnused,
deliveryVaaInfo.totalReceiverValue,
deliveryVaaInfo.redeliveryHash
) = getDeliveryParametersEvmV1(instruction, deliveryOverrides);
// Revert if msg.value is not enough to fund both the receiver value
// as well as the maximum possible refund
// Note: instruction's TargetNative is delivery's LocalNative
LocalNative requiredFunds = (deliveryVaaInfo.gasLimit.toWei(
deliveryVaaInfo.targetChainRefundPerGasUnused
) + deliveryVaaInfo.totalReceiverValue.asNative()).asLocalNative();
if (msgValue() < requiredFunds) {
revert InsufficientRelayerFunds(msgValue(), requiredFunds);
}
// Revert if the instruction's target chain is not this chain
if (getChainId() != instruction.targetChain) {
revert TargetChainIsNotThisChain(instruction.targetChain);
}
// Revert if the VAAs delivered do not match the descriptions specified in the instruction
checkMessageKeysWithMessages(instruction.messageKeys, encodedVMs);
executeDelivery(deliveryVaaInfo);
// Clear temporary storage of refund information
clearRefundInformation();
}
// ------------------------------------------- PRIVATE -------------------------------------------
struct DeliveryVAAInfo {
uint16 sourceChain;
uint64 sourceSequence;
bytes32 deliveryVaaHash;
address payable relayerRefundAddress;
bytes[] encodedVMs;
DeliveryInstruction deliveryInstruction;
Gas gasLimit;
GasPrice targetChainRefundPerGasUnused;
TargetNative totalReceiverValue;
bytes encodedOverrides;
bytes32 redeliveryHash; //optional (0 if not present)
}
function getDeliveryParametersEvmV1(
DeliveryInstruction memory instruction,
bytes memory encodedOverrides
)
internal
pure
returns (
Gas gasLimit,
GasPrice targetChainRefundPerGasUnused,
TargetNative totalReceiverValue,
bytes32 redeliveryHash
)
{
ExecutionInfoVersion instructionExecutionInfoVersion =
decodeExecutionInfoVersion(instruction.encodedExecutionInfo);
if (instructionExecutionInfoVersion != ExecutionInfoVersion.EVM_V1) {
revert UnexpectedExecutionInfoVersion(
uint8(instructionExecutionInfoVersion), uint8(ExecutionInfoVersion.EVM_V1)
);
}
EvmExecutionInfoV1 memory executionInfo =
decodeEvmExecutionInfoV1(instruction.encodedExecutionInfo);
// If present, apply redelivery deliveryOverrides to current instruction
if (encodedOverrides.length != 0) {
DeliveryOverride memory deliveryOverrides = encodedOverrides.decodeDeliveryOverride();
// Check to see if gasLimit >= original gas limit, receiver value >= original receiver value, and refund >= original refund
// If so, replace the corresponding variables with the overriden variables
// If not, revert
(instruction.requestedReceiverValue, executionInfo) = decodeAndCheckOverridesEvmV1(
instruction.requestedReceiverValue, executionInfo, deliveryOverrides
);
instruction.extraReceiverValue = TargetNative.wrap(0);
redeliveryHash = deliveryOverrides.redeliveryHash;
}
gasLimit = executionInfo.gasLimit;
targetChainRefundPerGasUnused = executionInfo.targetChainRefundPerGasUnused;
totalReceiverValue = instruction.requestedReceiverValue + instruction.extraReceiverValue;
}
function decodeAndCheckOverridesEvmV1(
TargetNative receiverValue,
EvmExecutionInfoV1 memory executionInfo,
DeliveryOverride memory deliveryOverrides
)
internal
pure
returns (
TargetNative deliveryOverridesReceiverValue,
EvmExecutionInfoV1 memory deliveryOverridesExecutionInfo
)
{
if (deliveryOverrides.newReceiverValue.unwrap() < receiverValue.unwrap()) {
revert InvalidOverrideReceiverValue();
}
ExecutionInfoVersion deliveryOverridesExecutionInfoVersion =
decodeExecutionInfoVersion(deliveryOverrides.newExecutionInfo);
if (ExecutionInfoVersion.EVM_V1 != deliveryOverridesExecutionInfoVersion) {
revert VersionMismatchOverride(
uint8(ExecutionInfoVersion.EVM_V1), uint8(deliveryOverridesExecutionInfoVersion)
);
}
deliveryOverridesExecutionInfo =
decodeEvmExecutionInfoV1(deliveryOverrides.newExecutionInfo);
deliveryOverridesReceiverValue = deliveryOverrides.newReceiverValue;
if (deliveryOverridesExecutionInfo.gasLimit < executionInfo.gasLimit) {
revert InvalidOverrideGasLimit();
}
}
struct DeliveryResults {
Gas gasUsed;
DeliveryStatus status;
bytes additionalStatusInfo;
}
/**
* Performs the following actions:
* - Calls the `receiveWormholeMessages` method on the contract
* `vaaInfo.deliveryInstruction.targetAddress` (with the gas limit and value specified in
* vaaInfo.gasLimit and vaaInfo.totalReceiverValue, and `encodedVMs` as the input)
*
* - Calculates how much gas from `vaaInfo.gasLimit` is left
* - Refund anything leftover to the relayer
*
* @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
* - relayerRefundAddress address that should be paid for relayer refunds
* - encodedVMs list of signed wormhole messages (VAAs)
* - deliveryInstruction the specific instruction which is being executed
* - gasLimit the gas limit to call targetAddress with
* - targetChainRefundPerGasUnused the amount of (this chain) wei to refund to refundAddress
* per unit of gas unused (from gasLimit)
* - totalReceiverValue the msg.value to call targetAddress with
* - encodedOverrides any (encoded) overrides that were applied
* - (optional) redeliveryHash hash of redelivery Vaa
*/
function executeDelivery(DeliveryVAAInfo memory vaaInfo) private {
// If the targetAddress is the 0 address
// Then emit event and return
// (This is used for cross-chain refunds)
if (vaaInfo.deliveryInstruction.targetAddress == 0x0) {
handleCrossChainRefund(vaaInfo);
return;
}
DeliveryResults memory results;
// Check replay protection - if so, set status to receiver failure
if(getDeliverySuccessState().deliverySuccessBlock[vaaInfo.deliveryVaaHash] != 0) {
results = DeliveryResults(
Gas.wrap(0),
DeliveryStatus.RECEIVER_FAILURE,
bytes("Delivery already performed")
);
} else {
results = executeInstruction(
EvmDeliveryInstruction({
sourceChain: vaaInfo.sourceChain,
targetAddress: vaaInfo.deliveryInstruction.targetAddress,
payload: vaaInfo.deliveryInstruction.payload,
gasLimit: vaaInfo.gasLimit,
totalReceiverValue: vaaInfo.totalReceiverValue,
targetChainRefundPerGasUnused: vaaInfo.targetChainRefundPerGasUnused,
senderAddress: vaaInfo.deliveryInstruction.senderAddress,
deliveryHash: vaaInfo.deliveryVaaHash,
signedVaas: vaaInfo.encodedVMs
})
);
setDeliveryBlock(results.status, vaaInfo.deliveryVaaHash);
}
RefundStatus refundStatus = payRefunds(
vaaInfo.deliveryInstruction,
vaaInfo.relayerRefundAddress,
(vaaInfo.gasLimit - results.gasUsed).toWei(vaaInfo.targetChainRefundPerGasUnused).asLocalNative(),
results.status
);
emitDeliveryEvent(vaaInfo, results, refundStatus);
}
function executeInstruction(EvmDeliveryInstruction memory evmInstruction)
internal
returns (DeliveryResults memory results)
{
Gas gasLimit = evmInstruction.gasLimit;
bool success;
{
address payable deliveryTarget = payable(fromWormholeFormat(evmInstruction.targetAddress));
bytes memory callData = abi.encodeCall(IWormholeReceiver.receiveWormholeMessages, (
evmInstruction.payload,
evmInstruction.signedVaas,
evmInstruction.senderAddress,
evmInstruction.sourceChain,
evmInstruction.deliveryHash
));
// Measure gas usage of call
Gas preGas = Gas.wrap(gasleft());
// Calls the `receiveWormholeMessages` endpoint on the contract `evmInstruction.targetAddress`
// (with the gas limit and value specified in instruction, and `encodedVMs` as the input)
// If it reverts, returns the first 132 bytes of the revert message
(success, results.additionalStatusInfo) = returnLengthBoundedCall(
deliveryTarget,
callData,
gasLimit.unwrap(),
evmInstruction.totalReceiverValue.unwrap(),
RETURNDATA_TRUNCATION_THRESHOLD
);
Gas postGas = Gas.wrap(gasleft());
unchecked {
results.gasUsed = (preGas - postGas).min(gasLimit);
}
}
if (success) {
results.additionalStatusInfo = new bytes(0);
results.status = DeliveryStatus.SUCCESS;
} else {
// Call to 'receiveWormholeMessages' on targetAddress reverted
results.status = DeliveryStatus.RECEIVER_FAILURE;
}
}
function handleCrossChainRefund(DeliveryVAAInfo memory vaaInfo) internal {
RefundStatus refundStatus = payRefunds(
vaaInfo.deliveryInstruction,
vaaInfo.relayerRefundAddress,
LocalNative.wrap(0),
DeliveryStatus.RECEIVER_FAILURE
);
emitDeliveryEvent(
vaaInfo,
DeliveryResults(
Gas.wrap(0),
DeliveryStatus.SUCCESS,
bytes("")
),
refundStatus
);
}
function emitDeliveryEvent(DeliveryVAAInfo memory vaaInfo, DeliveryResults memory results, RefundStatus refundStatus) private {
emit Delivery(
fromWormholeFormat(vaaInfo.deliveryInstruction.targetAddress),
vaaInfo.sourceChain,
vaaInfo.sourceSequence,
vaaInfo.deliveryVaaHash,
results.status,
results.gasUsed,
refundStatus,
results.additionalStatusInfo,
(vaaInfo.redeliveryHash != 0) ? vaaInfo.encodedOverrides : new bytes(0)
);
}
function payRefunds(
DeliveryInstruction memory deliveryInstruction,
address payable relayerRefundAddress,
LocalNative transactionFeeRefundAmount,
DeliveryStatus status
) private returns (RefundStatus refundStatus) {
//Amount of receiverValue that is refunded to the user (0 if the call to
// 'receiveWormholeMessages' did not revert, or the full receiverValue otherwise)
LocalNative receiverValueRefundAmount = LocalNative.wrap(0);
if (
status == DeliveryStatus.RECEIVER_FAILURE
) {
receiverValueRefundAmount = (
deliveryInstruction.requestedReceiverValue + deliveryInstruction.extraReceiverValue
).asNative().asLocalNative(); // NOTE: instruction's target is delivery's local
}
// Total refund to the user
// (If the forward succeeded, the 'transactionFeeRefundAmount' was used there already)
LocalNative refundToRefundAddress = receiverValueRefundAmount
+ transactionFeeRefundAmount;
//Refund the user
refundStatus = deliveryInstruction.refundAddress == bytes32(0x0) ? RefundStatus.NO_REFUND_REQUESTED : payRefundToRefundAddress(
deliveryInstruction.refundChain,
deliveryInstruction.refundAddress,
refundToRefundAddress,
deliveryInstruction.refundDeliveryProvider
);
//If sending the user's refund failed, this gets added to the relayer's refund
LocalNative leftoverUserRefund = refundToRefundAddress;
if (
refundStatus == RefundStatus.REFUND_SENT
|| refundStatus == RefundStatus.CROSS_CHAIN_REFUND_SENT
) {
leftoverUserRefund = LocalNative.wrap(0);
}
// Refund the relayer all remaining funds
LocalNative relayerRefundAmount = calcRelayerRefundAmount(deliveryInstruction, transactionFeeRefundAmount, leftoverUserRefund);
bool paymentSucceeded = pay(relayerRefundAddress, relayerRefundAmount);
if(!paymentSucceeded) {
revert DeliveryProviderCannotReceivePayment();
}
}
function calcRelayerRefundAmount(
DeliveryInstruction memory deliveryInstruction,
LocalNative transactionFeeRefundAmount,
LocalNative leftoverUserRefund
) private view returns (LocalNative) {
return msgValue()
// Note: instruction's target is delivery's local
- (deliveryInstruction.requestedReceiverValue + deliveryInstruction.extraReceiverValue).asNative().asLocalNative()
- transactionFeeRefundAmount + leftoverUserRefund;
}
function payRefundToRefundAddress(
uint16 refundChain,
bytes32 refundAddress,
LocalNative refundAmount,
bytes32 deliveryProvider
) private returns (RefundStatus) {
// User requested refund on this chain
if (refundChain == getChainId()) {
return pay(payable(fromWormholeFormat(refundAddress)), refundAmount, GAS_LIMIT_EXTERNAL_CALL)
? RefundStatus.REFUND_SENT
: RefundStatus.REFUND_FAIL;
}
// User requested refund on a different chain
// Determine price of an 'empty' delivery
// (Note: assumes refund chain is an EVM chain)
(bool success, LocalNative baseDeliveryPrice) = untrustedBaseDeliveryPrice(fromWormholeFormat(deliveryProvider), refundChain);
// If the unstrusted call failed, or the refundAmount is not greater than the 'empty delivery price', then the refund does not go through
// Note: We first check 'refundAmount <= baseDeliveryPrice', in case an untrusted delivery provider returns a value that overflows once
// the wormhole message fee is added to it
unchecked {
if (!success || (refundAmount <= baseDeliveryPrice) || (refundAmount <= getWormholeMessageFee() + baseDeliveryPrice)) {
return RefundStatus.CROSS_CHAIN_REFUND_FAIL_NOT_ENOUGH;
}
}
return sendCrossChainRefund(refundChain, refundAddress, refundAmount, refundAmount - getWormholeMessageFee() - baseDeliveryPrice, deliveryProvider);
}
function untrustedBaseDeliveryPrice(address deliveryProvider, uint16 refundChain) internal returns (bool success, LocalNative baseDeliveryPrice) {
(bool externalCallSuccess, bytes memory returnData) = returnLengthBoundedCall(
deliveryProvider,
abi.encodeCall(IDeliveryProvider.quoteDeliveryPrice, (refundChain, TargetNative.wrap(0), encodeEvmExecutionParamsV1(getEmptyEvmExecutionParamsV1()))),
GAS_LIMIT_EXTERNAL_CALL,
QUOTE_LENGTH_BYTES
);
if(externalCallSuccess && returnData.length == QUOTE_LENGTH_BYTES) {
baseDeliveryPrice = abi.decode(returnData, (LocalNative));
success = true;
} else {
success = false;
}
}
function sendCrossChainRefund(uint16 refundChain, bytes32 refundAddress, LocalNative sendAmount, LocalNative receiveAmount, bytes32 deliveryProvider) internal returns (RefundStatus status) {
// Request a 'send' with 'paymentForExtraReceiverValue' equal to the refund minus the 'empty delivery price'
// We limit the gas because we are within a delivery, so thus the trust assumptions on the delivery provider are different
// Normally, in 'send', a revert is no problem; but here, we want to prevent such reverts in this try-catch
try IWormholeRelayerSend(address(this)).send{value: sendAmount.unwrap(), gas: GAS_LIMIT_EXTERNAL_CALL}(
refundChain,
bytes32(0),
bytes(""),
TargetNative.wrap(0),
receiveAmount,
encodeEvmExecutionParamsV1(getEmptyEvmExecutionParamsV1()),
refundChain,
refundAddress,
fromWormholeFormat(deliveryProvider),
new VaaKey[](0),
CONSISTENCY_LEVEL_INSTANT
) returns (uint64) {
return RefundStatus.CROSS_CHAIN_REFUND_SENT;
} catch (bytes memory) {
return RefundStatus.CROSS_CHAIN_REFUND_FAIL_PROVIDER_NOT_SUPPORTED;
}
}
function checkMessageKeysWithMessages(
MessageKey[] memory messageKeys,
bytes[] memory signedMessages
) private view {
if (messageKeys.length != signedMessages.length) {
revert MessageKeysLengthDoesNotMatchMessagesLength(messageKeys.length, signedMessages.length);
}
uint256 len = messageKeys.length;
for (uint256 i = 0; i < len;) {
if (messageKeys[i].keyType == VAA_KEY_TYPE) {
IWormhole.VM memory parsedVaa = getWormhole().parseVM(signedMessages[i]);
(VaaKey memory vaaKey,) = WormholeRelayerSerde.decodeVaaKey(messageKeys[i].encodedKey, 0);
if (
vaaKey.chainId != parsedVaa.emitterChainId
|| vaaKey.emitterAddress != parsedVaa.emitterAddress
|| vaaKey.sequence != parsedVaa.sequence
) {
revert VaaKeysDoNotMatchVaas(uint8(i));
}
}
unchecked {
++i;
}
}
}
// Ensures current block number is set to implement replay protection and for indexing purposes
function setDeliveryBlock(DeliveryStatus status, bytes32 deliveryHash) private {
if (status == DeliveryStatus.SUCCESS) {
getDeliverySuccessState().deliverySuccessBlock[deliveryHash] = block.number;
// Clear out failure block if it exists from previous delivery failure
delete getDeliveryFailureState().deliveryFailureBlock[deliveryHash];
} else {
getDeliveryFailureState().deliveryFailureBlock[deliveryHash] = block.number;
}
}
}