Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fee Automation #94

Open
wants to merge 9 commits into
base: updates
Choose a base branch
from
39 changes: 39 additions & 0 deletions contracts/data/Keys.sol
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,17 @@ library Keys {
// @dev key for the buyback withdrawable fees
bytes32 public constant WITHDRAWABLE_BUYBACK_TOKEN_AMOUNT = keccak256(abi.encode("WITHDRAWABLE_BUYBACK_TOKEN_AMOUNT"));

// @dev key for MultichainReader read channel
bytes32 public constant MULTICHAIN_READ_CHANNEL = keccak256(abi.encode("MULTICHAIN_READ_CHANNEL"));
// @dev key for MultichainReader read channel to peer mapping
bytes32 public constant MULTICHAIN_PEERS = keccak256(abi.encode("MULTICHAIN_PEERS"));
// @dev key for MultichainReader number of confirmations to wait for finality
bytes32 public constant MULTICHAIN_CONFIRMATIONS = keccak256(abi.encode("MULTICHAIN_CONFIRMATIONS"));
// @dev key for MultichainReader guid to originator mapping
bytes32 public constant MULTICHAIN_GUID_TO_ORIGINATOR = keccak256(abi.encode("MULTICHAIN_GUID_TO_ORIGINATOR"));
// @dev key for MultichainReader authorized orginators
bytes32 public constant MULTICHAIN_AUTHORIZED_ORIGINATORS = keccak256(abi.encode("MULTICHAIN_AUTHORIZED_ORIGINATORS"));

// @dev constant for user initiated cancel reason
string public constant USER_INITIATED_CANCEL = "USER_INITIATED_CANCEL";

Expand Down Expand Up @@ -2076,4 +2087,32 @@ library Keys {
token
));
}

// @dev key for the multichain peers mapping (peer address stored as bytes32)
// @param readChannel the readChannel for which to retrieve the respective peer
// @return key for multichain peers
function multichainPeersKey(uint32 readChannel) internal pure returns (bytes32) {
return keccak256(abi.encode(MULTICHAIN_PEERS, readChannel));
}

// @dev key for the multichain number of confirmations
// @param chainId the chainId for which to retrieve the number of confirmations
// @return key for multichain confirmations
function multichainConfirmationsKey(uint32 chainId) internal pure returns (bytes32) {
return keccak256(abi.encode(MULTICHAIN_PEERS, chainId));
}

// @dev key for the multichain guid to originator mapping
// @param guid the guid for which to retrieve the originator address
// @return key for multichain guid to originator
function multichainGuidToOriginatorKey(bytes32 guid) internal pure returns (bytes32) {
return keccak256(abi.encode(MULTICHAIN_GUID_TO_ORIGINATOR, guid));
}

// @dev key for the multichain authorized originators
// @param originator the originator address to validate if authorized
// @return key for multichain authorized originator
function multichainAuthorizedOriginatorsKey(address originator) internal pure returns (bytes32) {
return keccak256(abi.encode(MULTICHAIN_AUTHORIZED_ORIGINATORS, originator));
}
}
12 changes: 12 additions & 0 deletions contracts/error/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -420,4 +420,16 @@ library Errors {

// Reader errors
error EmptyMarketPrice(address market);

// MultichainReader errors
error NotEnoughNative(uint256 msgValue);
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
error LzTokenUnavailable();
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
error OnlyEndpoint(address addr);
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
error ConfirmationsLengthMismatch(uint256 chainIdsLength, uint256 confirmationsLength);
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
error UnauthorizedOriginator(address originator);
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
error OriginatorCallFailed(bytes transactionCallData);
error OnlyPeer(uint32 eid, bytes32 sender);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use Errors.Unauthorized instead

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first is our own error message, and given the concerns around originator.call(transactionCallData) it may no longer be necessary anyway. The second is another error using LayerZero naming but can also be changed on our end.

error NoPeer(uint32 eid);
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
error InvalidEndpointCall();
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
error InvalidDelegate();
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
}
12 changes: 12 additions & 0 deletions contracts/external/IOriginator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {MultichainReaderUtils} from "./MultichainReaderUtils.sol";

interface IOriginator {
function processSendReadRequests(bytes32 guid, MultichainReaderUtils.ReceivedData memory receivedDataInput, bytes memory transactionCallData) external;
function processLzReceive(bytes32 guid, MultichainReaderUtils.ReceivedData memory receivedDataInput) external;
function receivedData(bytes32 guid) external view returns (uint256, uint256, bool, bytes memory);
function transactionCallData(bytes32 guid) external view returns (bytes memory);
function latestReadNumber() external view returns (uint256);
}
230 changes: 230 additions & 0 deletions contracts/external/MultichainReader.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {MessagingParams, MessagingFee, MessagingReceipt, ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import {Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import {EVMCallRequestV1, ReadCodecV1} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol";
import {MultichainReaderUtils} from "./MultichainReaderUtils.sol";
import {IOriginator} from "./IOriginator.sol";

import "../data/DataStore.sol";
import "../data/Keys.sol";
import "../role/RoleModule.sol";

contract MultichainReader is RoleModule {
uint64 internal constant SENDER_VERSION = 1;
uint64 internal constant RECEIVER_VERSION = 2;
uint8 internal constant WORKER_ID = 1;
uint8 internal constant OPTION_TYPE_LZREAD = 5;
uint16 internal constant TYPE_3 = 3;

DataStore public immutable dataStore;
EventEmitter public immutable eventEmitter;
ILayerZeroEndpointV2 public immutable endpoint;

constructor(
RoleStore _roleStore,
DataStore _dataStore,
EventEmitter _eventEmitter,
address _endpoint
) RoleModule(_roleStore) {
dataStore = _dataStore;
eventEmitter = _eventEmitter;
endpoint = ILayerZeroEndpointV2(_endpoint);
endpoint.setDelegate(msg.sender);
}

// TODO: add modifier for access control
function setDelegate(address _delegate) external {
endpoint.setDelegate(_delegate);
}

function sendReadRequests(
MultichainReaderUtils.ReadRequestInputs[] calldata readRequestInputs,
MultichainReaderUtils.ExtraOptionsInputs calldata extraOptionsInputs
) external payable returns (MessagingReceipt memory, bytes32, MultichainReaderUtils.ReceivedData memory) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might make sense to have an onlyController modifier here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, to clarify, is this referring to the setDelegate() function?

I was thinking of removing it as previously discussed, but perhaps a safer option would be to use a highly restrictive modifier like onlyController.

LayerZero responded to my questions around this on telegram saying that a delegate could be useful if faulty dvns need to be removed for example, even if other configuration changes aren't necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm referring to adding an onlyController modifier to sendReadRequests, because in lzReceive, the originator contract is called, and the originator contract would be validating that msg.sender is MultichainReader, so MultichainReader has some unique access to call potentially important contracts

so i think it would be safer to restrict access of who can call MultichainReader

address originator = msg.sender;
bool isAuthorized = datastore.getBool(Keys.multichainAuthorizedOriginatorsKey(originator));
if (!isAuthorized) {
revert Errors.UnauthorizedOriginator(originator);
}

bytes memory cmd = _getCmd(readRequestInputs);
MessagingReceipt memory messagingReceipt = _lzSend(
uint32(dataStore.getUint(Keys.MULTICHAIN_READ_CHANNEL)),
cmd,
_extraOptions(extraOptionsInputs),
MessagingFee(msg.value, 0),
payable(originator)
);

bytes32 guid = messagingReceipt.guid;
dataStore.setAddress(keys.multichainGuidToOriginatorKey(guid), originator);

MultichainReaderUtils.ReceivedData memory receivedData;
receivedData.readNumber = IOriginator(originator).latestReadNumber() + 1;
receivedData.timestamp = block.timestamp;

return (messagingReceipt, guid, receivedData);
}

function lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address _executor,
bytes calldata _extraData
) external payable {
// Ensures that only the endpoint can attempt to lzReceive() messages to this OApp.
if (address(endpoint) != msg.sender) revert Errors.OnlyEndpoint(msg.sender);

// Ensure that the sender matches the expected peer for the source endpoint.
if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) revert Errors.OnlyPeer(_origin.srcEid, _origin.sender);

// Call the internal OApp implementation of lzReceive.
_lzReceive(_origin, _guid, _message, _executor, _extraData);
}

function peers(uint32 _eid) external view returns (bytes32 peer) {
peer = datastore.getBytes32(keys.multichainPeersKey(_eid));
}

function quoteReadFee(
MultichainReaderUtils.ReadRequestInputs[] calldata readRequestInputs,
MultichainReaderUtils.ExtraOptionsInputs calldata extraOptionsInputs
) external view returns (MessagingFee memory fee) {
return _quote(uint32(dataStore.getUint(Keys.MULTICHAIN_READ_CHANNEL)), _getCmd(readRequestInputs), _extraOptions(extraOptionsInputs));
}

function isComposeMsgSender(
Origin calldata /*_origin*/,
bytes calldata /*_message*/,
address _sender
) external view returns (bool) {
return _sender == address(this);
}

function allowInitializePath(Origin calldata origin) external view returns (bool) {
return datastore.getBytes32(keys.multichainPeersKey(origin.srcEid));
}

function nextNonce(uint32 /*_srcEid*/, bytes32 /*_sender*/) external pure returns (uint64 nonce) {
return 0;
}

function oAppVersion() external pure returns (uint64 senderVersion, uint64 receiverVersion) {
return (SENDER_VERSION, RECEIVER_VERSION);
}

function _lzSend(
uint32 _dstEid,
bytes memory _message,
bytes memory _options,
MessagingFee memory _fee,
address _refundAddress
) internal returns (MessagingReceipt memory receipt) {
// @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint.
uint256 messageValue = _payNative(_fee.nativeFee);

return
// solhint-disable-next-line check-send-result
endpoint.send{value: messageValue}(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, false),
_refundAddress
);
}

function _lzReceive(
Origin calldata /*_origin*/,
bytes32 _guid,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal {
address originator = dataStore.getAddress(keys.multichainGuidToOriginatorKey(guid));

(uint256 readNumber, uint256 timestamp, bool received, bytes memory readData) = IOriginator(originator)
.receivedData(_guid);
MultichainReaderUtils.ReceivedData memory receivedData = MultichainReaderUtils.ReceivedData(
readNumber,
timestamp,
received,
readData
);
receivedData.received = true;
receivedData.readData = _message;

IOriginator(originator).processLzReceive(_guid, receivedData);
bytes memory transactionCallData = IOriginator(originator).transactionCallData(_guid);
if (transactionCallData.length != 0) {
(bool success, ) = originator.call(transactionCallData);
xvi10 marked this conversation as resolved.
Show resolved Hide resolved
if (!success) {
revert Errors.OriginatorCallFailed(transactionCallData);
}
}
}

function _payNative(uint256 _nativeFee) internal returns (uint256 nativeFee) {
if (msg.value != _nativeFee) revert Errors.NotEnoughNative(msg.value);
return _nativeFee;
}

function _getCmd(
MultichainReaderUtils.ReadRequestInputs[] calldata readRequestInputs
) internal view returns (bytes memory) {
uint256 readRequestCount = readRequestInputs.length;
EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](readRequestCount);
for (uint256 i; i < readRequestCount; i++) {
uint32 chainId = readRequestInputs[i].chainId;
readRequests[i] = EVMCallRequestV1({
appRequestLabel: 1,
targetEid: chainId, // Endpoint ID of the target chain
isBlockNum: false, // Use timestamp instead of block number
blockNumOrTimestamp: uint64(block.timestamp), // Timestamp to read the state at
confirmations: uint16(dataStore.getUint(multichainConfirmationsKey(chainId))), // Number of confirmations to wait for finality
to: readRequestInputs[i].target, // Address of the contract to call
callData: readRequestInputs[i].callData // Encoded function call data
});
}

return ReadCodecV1.encode(0, readRequests);
}

function _getPeerOrRevert(uint32 _eid) internal view returns (bytes32) {
bytes32 peer = datastore.getBytes32(keys.multichainPeersKey(_eid));
if (peer == bytes32(0)) revert Errors.NoPeer(_eid);
return peer;
}

function _quote(
uint32 _dstEid,
bytes memory _message,
bytes memory _options
) internal view returns (MessagingFee memory fee) {
return
endpoint.quote(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, false),
address(this)
);
}

function _extraOptions(
MultichainReaderUtils.ExtraOptionsInputs calldata extraOptionsInputs
) internal pure returns (bytes memory) {
bytes memory option = extraOptionsInputs.msgValue == 0
? abi.encodePacked(extraOptionsInputs.gasLimit, extraOptionsInputs.returnDataSize)
: abi.encodePacked(
extraOptionsInputs.gasLimit,
extraOptionsInputs.returnDataSize,
extraOptionsInputs.msgValue
);
return
abi.encodePacked(
abi.encodePacked(TYPE_3),
WORKER_ID,
uint16(option.length) + 1, // +1 for optionType
OPTION_TYPE_LZREAD,
option
);
}
}
23 changes: 23 additions & 0 deletions contracts/external/MultichainReaderUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

library MultichainReaderUtils {
struct ReadRequestInputs {
uint32 chainId;
address target;
bytes callData;
}

struct ExtraOptionsInputs {
uint128 gasLimit;
uint32 returnDataSize;
uint128 msgValue;
}

struct ReceivedData {
uint256 readNumber;
uint256 timestamp;
bool received;
bytes readData;
}
}
Loading