trustless-generic-relayer/ethereum/contracts/coreRelayer/CoreRelayerDelivery.sol

489 lines
26 KiB
Solidity

// contracts/Bridge.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "../interfaces/IWormholeReceiver.sol";
import "../interfaces/IDelivery.sol";
import "./CoreRelayerGovernance.sol";
import "./CoreRelayerStructs.sol";
contract CoreRelayerDelivery is CoreRelayerGovernance {
enum DeliveryStatus {
SUCCESS,
RECEIVER_FAILURE,
FORWARD_REQUEST_FAILURE,
FORWARD_REQUEST_SUCCESS,
INVALID_REDELIVERY
}
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
*
* @return forwardIsFunded whether or not the funds for the forward were enough
*/
function emitForward(uint256 transactionFeeRefundAmount, ForwardInstruction memory forwardInstruction)
internal
returns (bool forwardIsFunded)
{
DeliveryInstructionsContainer memory container =
decodeDeliveryInstructionsContainer(forwardInstruction.container);
// Add any additional funds which were passed in to the forward as msg.value
transactionFeeRefundAmount = transactionFeeRefundAmount + forwardInstruction.msgValue;
// Checks if enough funds were passed into the forward
forwardIsFunded = (transactionFeeRefundAmount >= 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
if (forwardIsFunded) {
uint256 amountUnderMaximum = relayProvider.quoteMaximumBudget(container.instructions[0].targetChain)
- (
wormholeMessageFee + container.instructions[0].maximumRefundTarget
+ container.instructions[0].receiverValueTarget
);
uint256 convertedExtraAmount = calculateTargetDeliveryMaximumRefund(
container.instructions[0].targetChain,
transactionFeeRefundAmount - forwardInstruction.totalFee,
relayProvider
);
container.instructions[0].maximumRefundTarget +=
(amountUnderMaximum > convertedExtraAmount) ? convertedExtraAmount : amountUnderMaximum;
}
// Publishes the DeliveryInstruction, with a 'sufficientlyFunded' flag indicating whether the forward had enough funds
container.sufficientlyFunded = forwardIsFunded;
wormhole.publishMessage{value: wormholeMessageFee}(
forwardInstruction.nonce,
encodeDeliveryInstructionsContainer(container),
relayProvider.getConsistencyLevel()
);
// if funded, pay out reward to provider. Otherwise, the delivery code will handle sending a refund.
if (forwardIsFunded) {
pay(relayProvider.getRewardAddress(), transactionFeeRefundAmount);
}
//clear forwarding request from storage
clearForwardInstruction();
}
/**
* 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(
DeliveryInstruction memory internalInstruction,
bytes[] memory encodedVMs,
address payable relayerRefundAddress,
DeliveryVAAInfo memory vaaInfo
) internal {
// lock the contract to prevent reentrancy
if (isContractLocked()) {
revert IDelivery.ReentrantCall();
}
setContractLock(true);
setLockedTargetAddress(fromWormholeFormat(internalInstruction.targetAddress));
uint256 preGas = gasleft();
// Calls the 'receiveWormholeMessages' endpoint on the contract 'internalInstruction.targetAddress'
// (with the gas limit and value specified in internalInstruction, and 'encodedVMs' as the input)
(bool callToTargetContractSucceeded,) = fromWormholeFormat(internalInstruction.targetAddress).call{
gas: internalInstruction.executionParameters.gasLimit,
value: internalInstruction.receiverValueTarget
}(abi.encodeCall(IWormholeReceiver.receiveWormholeMessages, (encodedVMs, new bytes[](0))));
uint256 postGas = gasleft();
// 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)
uint256 transactionFeeRefundAmount = (internalInstruction.executionParameters.gasLimit - gasUsed)
* internalInstruction.maximumRefundTarget / internalInstruction.executionParameters.gasLimit;
// unlock the contract
setContractLock(false);
// Retrieve the forward instruction created during execution of 'receiveWormholeMessages'
ForwardInstruction memory forwardingRequest = getForwardInstruction();
DeliveryStatus status;
// Represents whether or not (amount user passed into forward as msg.value) + (remaining maxTransactionFee) is enough to fund the forward
bool forwardIsFunded = false;
if (forwardingRequest.isValid) {
// If the user made a forward/multichainForward request, then try to execute it
forwardIsFunded = emitForward(transactionFeeRefundAmount, forwardingRequest);
status = forwardIsFunded ? DeliveryStatus.FORWARD_REQUEST_SUCCESS : DeliveryStatus.FORWARD_REQUEST_FAILURE;
} else {
status = callToTargetContractSucceeded ? DeliveryStatus.SUCCESS : 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,
callToTargetContractSucceeded,
forwardingRequest.isValid,
forwardIsFunded
);
}
function payRefunds(
DeliveryInstruction memory internalInstruction,
address payable relayerRefundAddress,
uint256 transactionFeeRefundAmount,
bool callToTargetContractSucceeded,
bool forwardingRequestExists,
bool forwardWasFunded
) 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 =
(callToTargetContractSucceeded ? 0 : internalInstruction.receiverValueTarget);
// Total refund to the user
uint256 refundToRefundAddress = receiverValueRefundAmount + (forwardWasFunded ? 0 : transactionFeeRefundAmount);
// Whether or not the refund succeeded
bool refundPaidToRefundAddress =
pay(payable(fromWormholeFormat(internalInstruction.refundAddress)), refundToRefundAddress);
uint256 wormholeMessageFee = wormhole().messageFee();
// Funds that the relayer passed as msg.value over what they needed
uint256 extraRelayerFunds = (
msg.value - internalInstruction.receiverValueTarget - internalInstruction.maximumRefundTarget
- wormholeMessageFee
);
// Refund the relayer (their extra funds) + (the amount that the relayer spent on gas) + (the wormhole message fee if no forward was sent)
// + (the users refund if that refund didn't succeed)
uint256 relayerRefundAmount = extraRelayerFunds
+ (internalInstruction.maximumRefundTarget - transactionFeeRefundAmount)
+ (forwardingRequestExists ? 0 : wormholeMessageFee) + (refundPaidToRefundAddress ? 0 : refundToRefundAddress);
pay(relayerRefundAddress, relayerRefundAmount);
}
function verifyRelayerVM(IWormhole.VM memory vm) internal view returns (bool) {
return registeredCoreRelayerContract(vm.emitterChainId) == vm.emitterAddress;
}
/**
* @notice The relay provider calls 'redeliverSingle' to relay messages as described by one redelivery instruction
*
* The instruction specifies, among other things, the target chain (must be this chain), refund address, new maximum refund (in this chain's currency),
* new receiverValue (in this chain's currency), new upper bound on gas
*
* The relay provider must pass in the original signed wormhole messages from the source chain of the same nonce
* (the wormhole message with the original delivery instructions (the delivery VAA) must be one of these messages)
* as well as the wormhole message with the new redelivery instruction (the redelivery VAA)
*
* The messages will be relayed to the target address (with the specified gas limit and receiver value) iff the following checks are met:
* - the redelivery VAA (targetParams.redeliveryVM) has a valid signature
* - the redelivery VAA's emitter is one of these CoreRelayer contracts
* - the original delivery VAA has a valid signature
* - the original delivery VAA's emitter is one of these CoreRelayer contracts
* - the new redelivery instruction's upper bound on gas >= the original instruction's upper bound on gas
* - the new redelivery instruction's 'receiver value' amount >= the original instruction's 'receiver value' amount
* - the redelivery instruction's target chain = this chain
* - the original instruction's target chain = this chain
* - for the redelivery instruction, the relay provider passed in at least [(one wormhole message fee) + instruction.newMaximumRefundTarget + instruction.newReceiverValueTarget] of this chain's currency as msg.value
* - msg.sender is the permissioned address allowed to execute this redelivery instruction
* - the permissioned address allowed to execute this redelivery instruction is the permissioned address allowed to execute the old instruction
*
* @param targetParams struct containing the signed wormhole messages and encoded redelivery instruction (and other information)
*/
function redeliverSingle(IDelivery.TargetRedeliveryByTxHashParamsSingle memory targetParams) public payable {
IWormhole wormhole = wormhole();
(IWormhole.VM memory redeliveryVM, bool valid, string memory reason) =
wormhole.parseAndVerifyVM(targetParams.redeliveryVM);
// Check that the redelivery VAA (targetParams.redeliveryVM) has a valid signature
if (!valid) {
revert IDelivery.InvalidRedeliveryVM(reason);
}
// Check that the redelivery VAA's emitter is one of these CoreRelayer contracts
if (!verifyRelayerVM(redeliveryVM)) {
revert IDelivery.InvalidEmitterInRedeliveryVM();
}
RedeliveryByTxHashInstruction memory redeliveryInstruction = decodeRedeliveryInstruction(redeliveryVM.payload);
// Obtain the original delivery VAA
IWormhole.VM memory originalDeliveryVM;
(originalDeliveryVM, valid, reason) =
wormhole.parseAndVerifyVM(targetParams.sourceEncodedVMs[redeliveryInstruction.deliveryIndex]);
// Check that the original delivery VAA has a valid signature
if (!valid) {
revert IDelivery.InvalidVaa(redeliveryInstruction.deliveryIndex, reason);
}
// Check that the original delivery VAA's emitter is one of these CoreRelayer contracts
if (!verifyRelayerVM(originalDeliveryVM)) {
revert IDelivery.InvalidEmitterInOriginalDeliveryVM(redeliveryInstruction.deliveryIndex);
}
// Obtain the specific old instruction that was originally executed (and is meant to be re-executed with new parameters)
// specifying the the target chain (must be this chain), target address, refund address, old maximum refund (in this chain's currency),
// old receiverValue (in this chain's currency), old upper bound on gas, and the permissioned address allowed to execute this instruction
DeliveryInstruction memory originalInstruction = decodeDeliveryInstructionsContainer(originalDeliveryVM.payload)
.instructions[redeliveryInstruction.multisendIndex];
// Perform the following checks:
// - the new redelivery instruction's upper bound on gas >= the original instruction's upper bound on gas
// - the new redelivery instruction's 'receiver value' amount >= the original instruction's 'receiver value' amount
// - the redelivery instruction's target chain = this chain
// - the original instruction's target chain = this chain
// - for the redelivery instruction, the relay provider passed in at least [(one wormhole message fee) + instruction.newMaximumRefundTarget + instruction.newReceiverValueTarget] of this chain's currency as msg.value
// - msg.sender is the permissioned address allowed to execute this redelivery instruction
// - the permissioned address allowed to execute this redelivery instruction is the permissioned address allowed to execute the old instruction
valid = checkRedeliveryInstructionTarget(redeliveryInstruction, originalInstruction);
// Emit an 'Invalid Redelivery' event if one of the following four checks failed:
// - the permissioned address allowed to execute this redelivery instruction is the permissioned address allowed to execute the old instruction
// - the original instruction's target chain = this chain
// - the new redelivery instruction's 'receiver value' amount >= the original instruction's 'receiver value' amount
// - the new redelivery instruction's upper bound on gas >= the original instruction's upper bound on gas
if (!valid) {
emit Delivery({
recipientContract: fromWormholeFormat(originalInstruction.targetAddress),
sourceChain: originalDeliveryVM.emitterChainId,
sequence: originalDeliveryVM.sequence,
deliveryVaaHash: originalDeliveryVM.hash,
status: DeliveryStatus.INVALID_REDELIVERY
});
pay(targetParams.relayerRefundAddress, msg.value);
return;
}
// Replace maximumRefund, receiverValue, and the gasLimit on the original request
originalInstruction.maximumRefundTarget = redeliveryInstruction.newMaximumRefundTarget;
originalInstruction.receiverValueTarget = redeliveryInstruction.newReceiverValueTarget;
originalInstruction.executionParameters = redeliveryInstruction.executionParameters;
_executeDelivery(
originalInstruction,
targetParams.sourceEncodedVMs,
targetParams.relayerRefundAddress,
DeliveryVAAInfo({
sourceChain: originalDeliveryVM.emitterChainId,
sourceSequence: originalDeliveryVM.sequence,
deliveryVaaHash: originalDeliveryVM.hash
})
);
}
/**
* Check that:
* - the new redelivery instruction's upper bound on gas >= the original instruction's upper bound on gas
* - the new redelivery instruction's 'receiver value' amount >= the original instruction's 'receiver value' amount
* - the redelivery instruction's target chain = this chain
* - the original instruction's target chain = this chain
* - for the redelivery instruction, the relay provider passed in at least [(one wormhole message fee) + instruction.newMaximumRefundTarget + instruction.newReceiverValueTarget] of this chain's currency as msg.value
* - msg.sender is the permissioned address allowed to execute this redelivery instruction
* - the permissioned address allowed to execute this redelivery instruction is the permissioned address allowed to execute the old instruction
* @param redeliveryInstruction redelivery instruction
* @param originalInstruction old instruction
*/
function checkRedeliveryInstructionTarget(
RedeliveryByTxHashInstruction memory redeliveryInstruction,
DeliveryInstruction memory originalInstruction
) internal view returns (bool isValid) {
address providerAddress = fromWormholeFormat(redeliveryInstruction.executionParameters.providerDeliveryAddress);
// Check that msg.sender is the permissioned address allowed to execute this redelivery instruction
if (providerAddress != msg.sender) {
revert IDelivery.UnexpectedRelayer();
}
uint16 whChainId = chainId();
// Check that the redelivery instruction's target chain = this chain
if (whChainId != redeliveryInstruction.targetChain) {
revert IDelivery.TargetChainIsNotThisChain(redeliveryInstruction.targetChain);
}
uint256 wormholeMessageFee = wormhole().messageFee();
// Check that for the redelivery instruction, the relay provider passed in at least [(one wormhole message fee) + instruction.newMaximumRefundTarget + instruction.newReceiverValueTarget] of this chain's currency as msg.value
if (
msg.value
< redeliveryInstruction.newMaximumRefundTarget + redeliveryInstruction.newReceiverValueTarget
+ wormholeMessageFee
) {
revert IDelivery.InsufficientRelayerFunds();
}
// Check that the permissioned address allowed to execute this redelivery instruction is the permissioned address allowed to execute the old instruction
isValid = (
providerAddress == fromWormholeFormat(originalInstruction.executionParameters.providerDeliveryAddress)
)
// Check that the original instruction's target chain = this chain
&& whChainId == originalInstruction.targetChain
// Check that the new redelivery instruction's 'receiver value' amount >= the original instruction's 'receiver value' amount
&& originalInstruction.receiverValueTarget <= redeliveryInstruction.newReceiverValueTarget
// Check that the new redelivery instruction's upper bound on gas >= the original instruction's upper bound on gas
&& originalInstruction.executionParameters.gasLimit <= redeliveryInstruction.executionParameters.gasLimit;
}
/**
* @notice The relay provider calls 'deliverSingle' 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 of the same nonce
* (the wormhole message with the delivery instructions (the delivery VAA) must be one of these messages)
* as well as identify which of these messages is the delivery VAA and 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
* - the instruction's target chain is this chain
* - the relay provider passed in at least [(one wormhole message fee) + instruction.maximumRefundTarget + instruction.receiverValueTarget] of this chain's currency as msg.value
* - msg.sender is the permissioned address allowed to execute this instruction
*
* @param targetParams struct containing the signed wormhole messages and encoded delivery instruction container (and other information)
*/
function deliverSingle(IDelivery.TargetDeliveryParametersSingle memory targetParams) public payable {
IWormhole wormhole = wormhole();
// Obtain the delivery VAA
(IWormhole.VM memory deliveryVM, bool valid, string memory reason) =
wormhole.parseAndVerifyVM(targetParams.encodedVMs[targetParams.deliveryIndex]);
// Check that the delivery VAA has a valid signature
if (!valid) {
revert IDelivery.InvalidVaa(targetParams.deliveryIndex, reason);
}
// Check that the delivery VAA's emitter is one of these CoreRelayer contracts
if (!verifyRelayerVM(deliveryVM)) {
revert IDelivery.InvalidEmitter();
}
DeliveryInstructionsContainer memory container = decodeDeliveryInstructionsContainer(deliveryVM.payload);
// Check that the delivery instruction container in the delivery VAA was fully funded
if (!container.sufficientlyFunded) {
revert IDelivery.SendNotSufficientlyFunded();
}
// 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, and the permissioned address allowed to execute this instruction
DeliveryInstruction memory deliveryInstruction = container.instructions[targetParams.multisendIndex];
// Check that msg.sender is the permissioned address allowed to execute this instruction
if (fromWormholeFormat(deliveryInstruction.executionParameters.providerDeliveryAddress) != msg.sender) {
revert IDelivery.UnexpectedRelayer();
}
uint256 wormholeMessageFee = wormhole.messageFee();
// 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 + wormholeMessageFee
) {
revert IDelivery.InsufficientRelayerFunds();
}
// Check that the instruction's target chain is this chain
if (chainId() != deliveryInstruction.targetChain) {
revert IDelivery.TargetChainIsNotThisChain(deliveryInstruction.targetChain);
}
_executeDelivery(
deliveryInstruction,
targetParams.encodedVMs,
targetParams.relayerRefundAddress,
DeliveryVAAInfo({
sourceChain: deliveryVM.emitterChainId,
sourceSequence: deliveryVM.sequence,
deliveryVaaHash: deliveryVM.hash
})
);
}
/**
* @notice Helper function that converts an EVM address to wormhole format
* @param addr (EVM 20-byte address)
* @return whFormat (32-byte address in Wormhole format)
*/
function toWormholeFormat(address addr) public pure returns (bytes32 whFormat) {
return bytes32(uint256(uint160(addr)));
}
/**
* @notice Helper function that converts an Wormhole format (32-byte) address to the EVM 'address' 20-byte format
* @param whFormatAddress (32-byte address in Wormhole format)
* @return addr (EVM 20-byte address)
*/
function fromWormholeFormat(bytes32 whFormatAddress) public pure returns (address addr) {
return address(uint160(uint256(whFormatAddress)));
}
function pay(address payable receiver, uint256 amount) internal returns (bool success) {
if (amount > 0) {
(success,) = receiver.call{value: amount}("");
} else {
success = true;
}
}
}