511 lines
21 KiB
Solidity
511 lines
21 KiB
Solidity
// contracts/Bridge.sol
|
|
// SPDX-License-Identifier: Apache 2
|
|
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "../interfaces/IWormholeRelayer.sol";
|
|
import "../interfaces/IWormholeReceiver.sol";
|
|
import "../interfaces/IDelivery.sol";
|
|
import "./CoreRelayerGovernance.sol";
|
|
import "./CoreRelayerStructs.sol";
|
|
|
|
contract CoreRelayer 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
|
|
);
|
|
|
|
function send(
|
|
uint16 targetChain,
|
|
bytes32 targetAddress,
|
|
bytes32 refundAddress,
|
|
uint256 maxTransactionFee,
|
|
uint256 receiverValue,
|
|
uint32 nonce
|
|
) external payable returns (uint64 sequence) {
|
|
sequence = send(
|
|
IWormholeRelayer.Send(
|
|
targetChain, targetAddress, refundAddress, maxTransactionFee, receiverValue, getDefaultRelayParams()
|
|
),
|
|
nonce,
|
|
getDefaultRelayProvider()
|
|
);
|
|
}
|
|
|
|
function send(IWormholeRelayer.Send memory request, uint32 nonce, address relayProvider)
|
|
public
|
|
payable
|
|
returns (uint64 sequence)
|
|
{
|
|
sequence = multichainSend(multichainSendContainer(request, relayProvider), nonce);
|
|
}
|
|
|
|
function forward(IWormholeRelayer.Send memory request, uint32 nonce, address relayProvider) public payable {
|
|
multichainForward(multichainSendContainer(request, relayProvider), nonce);
|
|
}
|
|
|
|
function resend(IWormholeRelayer.ResendByTx memory request, address relayProvider)
|
|
public
|
|
payable
|
|
returns (uint64 sequence)
|
|
{
|
|
updateWormholeMessageFee();
|
|
bool isSufficient = request.newMaxTransactionFee + request.newReceiverValue + wormholeMessageFee() <= msg.value;
|
|
if (!isSufficient) {
|
|
revert IWormholeRelayer.MsgValueTooLow();
|
|
}
|
|
|
|
IRelayProvider provider = IRelayProvider(relayProvider);
|
|
RedeliveryByTxHashInstruction memory instruction = convertResendToRedeliveryInstruction(request, provider);
|
|
checkRedeliveryInstruction(instruction, provider);
|
|
|
|
sequence = wormhole().publishMessage{value: wormholeMessageFee()}(
|
|
0, encodeRedeliveryInstruction(instruction), provider.getConsistencyLevel()
|
|
);
|
|
|
|
//Send the delivery fees to the specified address of the provider.
|
|
pay(provider.getRewardAddress(), msg.value - wormholeMessageFee());
|
|
}
|
|
|
|
/**
|
|
* TODO: Correct this comment
|
|
* @dev `multisend` generates a VAA with DeliveryInstructions to be delivered to the specified target
|
|
* contract based on user parameters.
|
|
* it parses the RelayParameters to determine the target chain ID
|
|
* it estimates the cost of relaying the batch
|
|
* it confirms that the user has passed enough value to pay the relayer
|
|
* it checks that the passed nonce is not zero (VAAs with a nonce of zero will not be batched)
|
|
* it generates a VAA with the encoded DeliveryInstructions
|
|
*/
|
|
function multichainSend(IWormholeRelayer.MultichainSend memory deliveryRequests, uint32 nonce)
|
|
public
|
|
payable
|
|
returns (uint64 sequence)
|
|
{
|
|
updateWormholeMessageFee();
|
|
uint256 totalFee = getTotalFeeMultichainSend(deliveryRequests);
|
|
if (totalFee > msg.value) {
|
|
revert IWormholeRelayer.MsgValueTooLow();
|
|
}
|
|
if (nonce == 0) {
|
|
revert IWormholeRelayer.NonceIsZero();
|
|
}
|
|
|
|
IRelayProvider relayProvider = IRelayProvider(deliveryRequests.relayProviderAddress);
|
|
DeliveryInstructionsContainer memory container =
|
|
convertMultichainSendToDeliveryInstructionsContainer(deliveryRequests);
|
|
checkInstructions(container, IRelayProvider(deliveryRequests.relayProviderAddress));
|
|
container.sufficientlyFunded = true;
|
|
|
|
// emit delivery message
|
|
sequence = wormhole().publishMessage{value: wormholeMessageFee()}(
|
|
nonce, encodeDeliveryInstructionsContainer(container), relayProvider.getConsistencyLevel()
|
|
);
|
|
|
|
//pay fee to provider
|
|
pay(relayProvider.getRewardAddress(), totalFee - wormholeMessageFee());
|
|
}
|
|
|
|
/**
|
|
* TODO correct this comment
|
|
* @dev `forward` queues up a 'send' which will be executed after the present delivery is complete
|
|
* & uses the gas refund to cover the costs.
|
|
* contract based on user parameters.
|
|
* it parses the RelayParameters to determine the target chain ID
|
|
* it estimates the cost of relaying the batch
|
|
* it confirms that the user has passed enough value to pay the relayer
|
|
* it checks that the passed nonce is not zero (VAAs with a nonce of zero will not be batched)
|
|
* it generates a VAA with the encoded DeliveryInstructions
|
|
*/
|
|
function multichainForward(IWormholeRelayer.MultichainSend memory deliveryRequests, uint32 nonce) public payable {
|
|
if (!isContractLocked()) {
|
|
revert IWormholeRelayer.NoDeliveryInProgress();
|
|
}
|
|
if (getForwardInstruction().isValid) {
|
|
revert IWormholeRelayer.MultipleForwardsRequested();
|
|
}
|
|
if (nonce == 0) {
|
|
revert IWormholeRelayer.NonceIsZero();
|
|
}
|
|
if (msg.sender != lockedTargetAddress()) {
|
|
revert IWormholeRelayer.ForwardRequestFromWrongAddress();
|
|
}
|
|
|
|
uint256 totalFee = getTotalFeeMultichainSend(deliveryRequests);
|
|
DeliveryInstructionsContainer memory container =
|
|
convertMultichainSendToDeliveryInstructionsContainer(deliveryRequests);
|
|
checkInstructions(container, IRelayProvider(deliveryRequests.relayProviderAddress));
|
|
|
|
setForwardInstruction(
|
|
ForwardInstruction({
|
|
container: container,
|
|
nonce: nonce,
|
|
msgValue: msg.value,
|
|
totalFee: totalFee,
|
|
sender: msg.sender,
|
|
relayProvider: deliveryRequests.relayProviderAddress,
|
|
isValid: true
|
|
})
|
|
);
|
|
}
|
|
|
|
function emitForward(uint256 transactionFeeRefundAmount, ForwardInstruction memory forwardInstruction)
|
|
internal
|
|
returns (bool forwardIsFunded)
|
|
{
|
|
DeliveryInstructionsContainer memory container = forwardInstruction.container;
|
|
|
|
//Add any additional funds which were passed in to the refund amount
|
|
transactionFeeRefundAmount = transactionFeeRefundAmount + forwardInstruction.msgValue;
|
|
|
|
//make sure the refund amount covers the native gas amounts
|
|
forwardIsFunded = (transactionFeeRefundAmount >= forwardInstruction.totalFee);
|
|
container.sufficientlyFunded = forwardIsFunded;
|
|
|
|
IRelayProvider relayProvider = IRelayProvider(forwardInstruction.relayProvider);
|
|
|
|
if (forwardIsFunded) {
|
|
// the rollover chain is the chain in the first request
|
|
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;
|
|
}
|
|
|
|
//emit forwarding instruction
|
|
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 cache
|
|
clearForwardInstruction();
|
|
}
|
|
|
|
function multichainSendContainer(IWormholeRelayer.Send memory request, address relayProvider)
|
|
internal
|
|
pure
|
|
returns (IWormholeRelayer.MultichainSend memory container)
|
|
{
|
|
IWormholeRelayer.Send[] memory requests = new IWormholeRelayer.Send[](1);
|
|
requests[0] = request;
|
|
container = IWormholeRelayer.MultichainSend({relayProviderAddress: relayProvider, requests: requests});
|
|
}
|
|
|
|
function _executeDelivery(
|
|
DeliveryInstruction memory internalInstruction,
|
|
bytes[] memory encodedVMs,
|
|
bytes32 deliveryVaaHash,
|
|
address payable relayerRefund,
|
|
uint16 sourceChain,
|
|
uint64 sourceSequence
|
|
) internal {
|
|
//REVISE Decide whether we want to remove the DeliveryInstructionsContainer from encodedVMs.
|
|
|
|
// lock the contract to prevent reentrancy
|
|
if (isContractLocked()) {
|
|
revert IDelivery.ReentrantCall();
|
|
}
|
|
setContractLock(true);
|
|
setLockedTargetAddress(fromWormholeFormat(internalInstruction.targetAddress));
|
|
// store gas budget pre target invocation to calculate unused gas budget
|
|
uint256 preGas = gasleft();
|
|
|
|
// call the receiveWormholeMessages endpoint on the target contract
|
|
(bool callToTargetContractSucceeded,) = fromWormholeFormat(internalInstruction.targetAddress).call{
|
|
gas: internalInstruction.executionParameters.gasLimit,
|
|
value: internalInstruction.receiverValueTarget
|
|
}(abi.encodeCall(IWormholeReceiver.receiveWormholeMessages, (encodedVMs, new bytes[](0))));
|
|
|
|
uint256 postGas = gasleft();
|
|
// There's no easy way to measure the exact cost of the CALL instruction.
|
|
// This is due to the fact that the compiler probably emits DUPN or MSTORE instructions
|
|
// to setup the arguments for the call just after our measurement.
|
|
// This means the refund could be off by a few units of gas.
|
|
// Thus, we ensure the overhead doesn't cause an overflow in our refund formula here.
|
|
uint256 gasUsed = (preGas - postGas) > internalInstruction.executionParameters.gasLimit
|
|
? internalInstruction.executionParameters.gasLimit
|
|
: (preGas - postGas);
|
|
|
|
// refund unused gas budget
|
|
uint256 transactionFeeRefundAmount = (internalInstruction.executionParameters.gasLimit - gasUsed)
|
|
* internalInstruction.maximumRefundTarget / internalInstruction.executionParameters.gasLimit;
|
|
|
|
// unlock the contract
|
|
setContractLock(false);
|
|
|
|
ForwardInstruction memory forwardingRequest = getForwardInstruction();
|
|
DeliveryStatus status;
|
|
bool forwardIsFunded = false;
|
|
if (forwardingRequest.isValid) {
|
|
forwardIsFunded = emitForward(transactionFeeRefundAmount, forwardingRequest);
|
|
status = forwardIsFunded ? DeliveryStatus.FORWARD_REQUEST_SUCCESS : DeliveryStatus.FORWARD_REQUEST_FAILURE;
|
|
} else {
|
|
status = callToTargetContractSucceeded ? DeliveryStatus.SUCCESS : DeliveryStatus.RECEIVER_FAILURE;
|
|
}
|
|
|
|
uint256 receiverValueRefundAmount =
|
|
(callToTargetContractSucceeded ? 0 : internalInstruction.receiverValueTarget);
|
|
uint256 refundToRefundAddress = receiverValueRefundAmount + (forwardIsFunded ? 0 : transactionFeeRefundAmount);
|
|
|
|
bool refundPaidToRefundAddress =
|
|
pay(payable(fromWormholeFormat(internalInstruction.refundAddress)), refundToRefundAddress);
|
|
|
|
emit Delivery({
|
|
recipientContract: fromWormholeFormat(internalInstruction.targetAddress),
|
|
sourceChain: sourceChain,
|
|
sequence: sourceSequence,
|
|
deliveryVaaHash: deliveryVaaHash,
|
|
status: status
|
|
});
|
|
|
|
uint256 extraRelayerFunds = (
|
|
msg.value - internalInstruction.receiverValueTarget - internalInstruction.maximumRefundTarget
|
|
- wormholeMessageFee()
|
|
);
|
|
uint256 relayerRefundAmount = extraRelayerFunds
|
|
+ (internalInstruction.maximumRefundTarget - transactionFeeRefundAmount)
|
|
+ (forwardingRequest.isValid ? 0 : wormholeMessageFee())
|
|
+ (refundPaidToRefundAddress ? 0 : refundToRefundAddress);
|
|
// refund the rest to relayer
|
|
pay(relayerRefund, relayerRefundAmount);
|
|
}
|
|
|
|
function verifyRelayerVM(IWormhole.VM memory vm) internal view returns (bool) {
|
|
return registeredCoreRelayerContract(vm.emitterChainId) == vm.emitterAddress;
|
|
}
|
|
|
|
function getDefaultRelayProvider() public view returns (address) {
|
|
return defaultRelayProvider();
|
|
}
|
|
|
|
function redeliverSingle(IDelivery.TargetRedeliveryByTxHashParamsSingle memory targetParams) public payable {
|
|
//cache wormhole
|
|
IWormhole wormhole = wormhole();
|
|
updateWormholeMessageFee();
|
|
|
|
//validate the redelivery VM
|
|
(IWormhole.VM memory redeliveryVM, bool valid, string memory reason) =
|
|
wormhole.parseAndVerifyVM(targetParams.redeliveryVM);
|
|
if (!valid) {
|
|
revert IDelivery.InvalidRedeliveryVM(reason);
|
|
}
|
|
if (!verifyRelayerVM(redeliveryVM)) {
|
|
// Redelivery VM has an invalid emitter
|
|
revert IDelivery.InvalidEmitterInRedeliveryVM();
|
|
}
|
|
|
|
RedeliveryByTxHashInstruction memory redeliveryInstruction = decodeRedeliveryInstruction(redeliveryVM.payload);
|
|
|
|
//validate the original delivery VM
|
|
IWormhole.VM memory originalDeliveryVM;
|
|
(originalDeliveryVM, valid, reason) =
|
|
wormhole.parseAndVerifyVM(targetParams.sourceEncodedVMs[redeliveryInstruction.deliveryIndex]);
|
|
if (!valid) {
|
|
revert IDelivery.InvalidVaa(redeliveryInstruction.deliveryIndex, reason);
|
|
}
|
|
if (!verifyRelayerVM(originalDeliveryVM)) {
|
|
// Original Delivery VM has a invalid emitter
|
|
revert IDelivery.InvalidEmitterInOriginalDeliveryVM(redeliveryInstruction.deliveryIndex);
|
|
}
|
|
|
|
DeliveryInstruction memory instruction;
|
|
(instruction, valid) = validateRedeliverySingle(
|
|
redeliveryInstruction,
|
|
decodeDeliveryInstructionsContainer(originalDeliveryVM.payload).instructions[redeliveryInstruction
|
|
.multisendIndex]
|
|
);
|
|
|
|
if (!valid) {
|
|
emit Delivery({
|
|
recipientContract: fromWormholeFormat(instruction.targetAddress),
|
|
sourceChain: redeliveryVM.emitterChainId,
|
|
sequence: redeliveryVM.sequence,
|
|
deliveryVaaHash: redeliveryVM.hash,
|
|
status: DeliveryStatus.INVALID_REDELIVERY
|
|
});
|
|
pay(targetParams.relayerRefundAddress, msg.value);
|
|
return;
|
|
}
|
|
|
|
_executeDelivery(
|
|
instruction,
|
|
targetParams.sourceEncodedVMs,
|
|
originalDeliveryVM.hash,
|
|
targetParams.relayerRefundAddress,
|
|
originalDeliveryVM.emitterChainId,
|
|
originalDeliveryVM.sequence
|
|
);
|
|
}
|
|
|
|
function validateRedeliverySingle(
|
|
RedeliveryByTxHashInstruction memory redeliveryInstruction,
|
|
DeliveryInstruction memory originalInstruction
|
|
) internal view returns (DeliveryInstruction memory deliveryInstruction, bool isValid) {
|
|
// All the same checks as delivery single, with a couple additional
|
|
|
|
// The same relay provider must be specified when doing a single VAA redeliver.
|
|
address providerAddress = fromWormholeFormat(redeliveryInstruction.executionParameters.providerDeliveryAddress);
|
|
if (providerAddress != fromWormholeFormat(originalInstruction.executionParameters.providerDeliveryAddress)) {
|
|
revert IDelivery.MismatchingRelayProvidersInRedelivery();
|
|
}
|
|
|
|
// relayer must have covered the necessary funds
|
|
if (
|
|
msg.value
|
|
< redeliveryInstruction.newMaximumRefundTarget + redeliveryInstruction.newReceiverValueTarget
|
|
+ wormholeMessageFee()
|
|
) {
|
|
revert IDelivery.InsufficientRelayerFunds();
|
|
}
|
|
|
|
uint16 whChainId = chainId();
|
|
// msg.sender must be the provider
|
|
// "Relay provider differed from the specified address");
|
|
isValid = msg.sender == providerAddress
|
|
// redelivery must target this chain
|
|
// "Redelivery request does not target this chain.");
|
|
&& whChainId == redeliveryInstruction.targetChain
|
|
// original delivery must target this chain
|
|
// "Original delivery request did not target this chain.");
|
|
&& whChainId == originalInstruction.targetChain
|
|
// gasLimit & receiverValue must be at least as large as the initial delivery
|
|
// "New receiver value is smaller than the original"
|
|
&& originalInstruction.receiverValueTarget <= redeliveryInstruction.newReceiverValueTarget
|
|
// "New gasLimit is smaller than the original"
|
|
&& originalInstruction.executionParameters.gasLimit <= redeliveryInstruction.executionParameters.gasLimit;
|
|
|
|
// Overwrite compute budget and application budget on the original request and proceed.
|
|
deliveryInstruction = originalInstruction;
|
|
deliveryInstruction.maximumRefundTarget = redeliveryInstruction.newMaximumRefundTarget;
|
|
deliveryInstruction.receiverValueTarget = redeliveryInstruction.newReceiverValueTarget;
|
|
deliveryInstruction.executionParameters = redeliveryInstruction.executionParameters;
|
|
}
|
|
|
|
function deliverSingle(IDelivery.TargetDeliveryParametersSingle memory targetParams) public payable {
|
|
// cache wormhole instance
|
|
IWormhole wormhole = wormhole();
|
|
updateWormholeMessageFee();
|
|
|
|
// validate the deliveryIndex
|
|
(IWormhole.VM memory deliveryVM, bool valid, string memory reason) =
|
|
wormhole.parseAndVerifyVM(targetParams.encodedVMs[targetParams.deliveryIndex]);
|
|
if (!valid) {
|
|
revert IDelivery.InvalidVaa(targetParams.deliveryIndex, reason);
|
|
}
|
|
if (!verifyRelayerVM(deliveryVM)) {
|
|
revert IDelivery.InvalidEmitter();
|
|
}
|
|
|
|
DeliveryInstructionsContainer memory container = decodeDeliveryInstructionsContainer(deliveryVM.payload);
|
|
//ensure this is a funded delivery, not a failed forward.
|
|
if (!container.sufficientlyFunded) {
|
|
revert IDelivery.SendNotSufficientlyFunded();
|
|
}
|
|
|
|
// parse the deliveryVM payload into the DeliveryInstructions struct
|
|
DeliveryInstruction memory deliveryInstruction = container.instructions[targetParams.multisendIndex];
|
|
|
|
//make sure the specified relayer is the relayer delivering this message
|
|
if (fromWormholeFormat(deliveryInstruction.executionParameters.providerDeliveryAddress) != msg.sender) {
|
|
revert IDelivery.UnexpectedRelayer();
|
|
}
|
|
|
|
//make sure relayer passed in sufficient funds
|
|
if (
|
|
msg.value
|
|
< deliveryInstruction.maximumRefundTarget + deliveryInstruction.receiverValueTarget + wormholeMessageFee()
|
|
) {
|
|
revert IDelivery.InsufficientRelayerFunds();
|
|
}
|
|
|
|
//make sure this delivery is intended for this chain
|
|
if (chainId() != deliveryInstruction.targetChain) {
|
|
revert IDelivery.TargetChainIsNotThisChain(deliveryInstruction.targetChain);
|
|
}
|
|
|
|
_executeDelivery(
|
|
deliveryInstruction,
|
|
targetParams.encodedVMs,
|
|
deliveryVM.hash,
|
|
targetParams.relayerRefundAddress,
|
|
deliveryVM.emitterChainId,
|
|
deliveryVM.sequence
|
|
);
|
|
}
|
|
|
|
function toWormholeFormat(address addr) public pure returns (bytes32 whFormat) {
|
|
return bytes32(uint256(uint160(addr)));
|
|
}
|
|
|
|
function fromWormholeFormat(bytes32 whFormatAddress) public pure returns (address addr) {
|
|
return address(uint160(uint256(whFormatAddress)));
|
|
}
|
|
|
|
function getDefaultRelayParams() public pure returns (bytes memory relayParams) {
|
|
return new bytes(0);
|
|
}
|
|
|
|
function quoteGas(uint16 targetChain, uint32 gasLimit, IRelayProvider provider)
|
|
public
|
|
view
|
|
returns (uint256 deliveryQuote)
|
|
{
|
|
deliveryQuote = provider.quoteDeliveryOverhead(targetChain) + (gasLimit * provider.quoteGasPrice(targetChain));
|
|
}
|
|
|
|
function quoteGasResend(uint16 targetChain, uint32 gasLimit, IRelayProvider provider)
|
|
public
|
|
view
|
|
returns (uint256 redeliveryQuote)
|
|
{
|
|
redeliveryQuote =
|
|
provider.quoteRedeliveryOverhead(targetChain) + (gasLimit * provider.quoteGasPrice(targetChain));
|
|
}
|
|
|
|
//If the integrator pays at least nativeQuote, they should receive at least targetAmount as their application budget
|
|
function quoteReceiverValue(uint16 targetChain, uint256 targetAmount, IRelayProvider provider)
|
|
public
|
|
view
|
|
returns (uint256 nativeQuote)
|
|
{
|
|
(uint16 buffer, uint16 denominator) = provider.getAssetConversionBuffer(targetChain);
|
|
nativeQuote = assetConversionHelper(
|
|
targetChain, targetAmount, chainId(), uint256(0) + denominator + buffer, denominator, true, provider
|
|
);
|
|
}
|
|
|
|
function pay(address payable receiver, uint256 amount) internal returns (bool success) {
|
|
if (amount > 0) {
|
|
(success,) = receiver.call{value: amount}("");
|
|
} else {
|
|
success = true;
|
|
}
|
|
}
|
|
}
|