Skip to content

Commit

Permalink
feat(exo-gateway): add oracleInfo (#86)
Browse files Browse the repository at this point in the history
* feat(exo-gateway): add oracleInfo

ExocoreNetwork/exocore#159 adds a new `oracleInfo` parameter to the
`IAssets.sol` precompile when registering tokens. This PR follows that
change.

Previously, multiple tokens could be registered with a single call to
ExocoreGateway. However, that resulted in too many variables (and too
much gas) for Solidity to handle within a single function. To that end,
and keeping in mind that addition of new tokens isn't likely to be a
frequent occurrence, the function's capabilities have been tempered to
support only a single token within the transaction.

As a side effect, this fixes #80

* chore(fmt): forge fmt with newer nightly

* doc: update comments

* fix(test): update test from merge

* forge fmt

* feat(assets): split precompile into add/update

Since only certain parameters can be updated (TVL limit and metadata),
it does not make sense to use the same function for token additions and
updates

* fix: accept empty metadata

* fix: remove superfluous check

* forge fmt

* doc: fix outdated comments
  • Loading branch information
MaxMustermann2 authored Sep 5, 2024
1 parent c1667a1 commit 806c050
Show file tree
Hide file tree
Showing 19 changed files with 394 additions and 453 deletions.
40 changes: 27 additions & 13 deletions script/3_Setup.s.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
pragma solidity ^0.8.19;

import "../src/interfaces/IClientChainGateway.sol";
import {GatewayStorage} from "../src/storage/GatewayStorage.sol";

import "../src/interfaces/IClientChainGateway.sol";
import "../src/interfaces/IExocoreGateway.sol";
import "../src/interfaces/IVault.sol";

import {NonShortCircuitEndpointV2Mock} from "../test/mocks/NonShortCircuitEndpointV2Mock.sol";

import {BaseScript} from "./BaseScript.sol";
Expand Down Expand Up @@ -86,41 +88,53 @@ contract SetupScript is BaseScript {
exocoreGateway.registerOrUpdateClientChain(
clientChainId, address(clientGateway).toBytes32(), 20, "ClientChain", "EVM compatible network", "secp256k1"
);
vm.stopBroadcast();

// 3. adding tokens to the whtielist of both Exocore and client chain gateway to enable restaking

// first we read decimals from client chain ERC20 token contract to prepare for token data
vm.selectFork(clientChain);
bytes32[] memory whitelistTokensBytes32 = new bytes32[](2);
uint8[] memory decimals = new uint8[](2);
uint256[] memory tvlLimits = new uint256[](2);
string[] memory names = new string[](2);
string[] memory metaData = new string[](2);
string[] memory metaDatas = new string[](2);
string[] memory oracleInfos = new string[](2);

// this stands for LST restaking for restakeToken
whitelistTokensBytes32[0] = bytes32(bytes20(address(restakeToken)));
decimals[0] = restakeToken.decimals();
tvlLimits[0] = 1e10 ether;
names[0] = "RestakeToken";
metaData[0] = "ERC20 LST token";
metaDatas[0] = "ERC20 LST token";
oracleInfos[0] = "{'a': 'b'}";

// this stands for Native Restaking for ETH
whitelistTokensBytes32[1] = bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS));
decimals[1] = 18;
tvlLimits[1] = 1e8 ether;
names[1] = "StakedETH";
metaData[1] = "natively staked ETH on Ethereum";
vm.stopBroadcast();
metaDatas[1] = "natively staked ETH on Ethereum";
oracleInfos[1] = "{'b': 'a'}";

// second add whitelist tokens and their meta data on Exocore side to enable LST Restaking and Native Restaking,
// and this would also add token addresses to client chain gateway's whitelist
vm.selectFork(exocore);
uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokensBytes32.length + 2;
uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength));
exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}(
clientChainId, whitelistTokensBytes32, decimals, tvlLimits, names, metaData
);
uint256 nativeFee;
for (uint256 i = 0; i < whitelistTokensBytes32.length; i++) {
nativeFee = exocoreGateway.quote(
clientChainId,
abi.encodePacked(
GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(whitelistTokensBytes32[i])
)
);
exocoreGateway.addWhitelistToken{value: nativeFee}(
clientChainId,
whitelistTokensBytes32[i],
decimals[i],
tvlLimits[i],
names[i],
metaDatas[i],
oracleInfos[i]
);
}
vm.stopBroadcast();
}

Expand Down
8 changes: 5 additions & 3 deletions src/core/Bootstrap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,11 @@ contract Bootstrap is
whitelistTokens.push(token);
isWhitelistedToken[token] = true;

// do not deploy the vault for the virtual token address representing natively staked ETH
// deploy the corresponding vault if not deployed before
if (token != VIRTUAL_STAKED_ETH_ADDRESS && address(tokenToVault[token]) == address(0)) {
// tokens cannot be removed from the whitelist. hence, if the token is not in the
// whitelist, it means that it is missing a vault. we do not need to check for a
// pre-existing vault. however, we still do ensure that the vault is not deployed
// for restaking natively staked ETH.
if (token != VIRTUAL_STAKED_ETH_ADDRESS) {
_deployVault(token);
}

Expand Down
4 changes: 2 additions & 2 deletions src/core/ClientChainGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ contract ClientChainGateway is
revert Errors.ZeroAddress();
}

_whiteListFunctionSelectors[Action.REQUEST_ADD_WHITELIST_TOKENS] =
this.afterReceiveAddWhitelistTokensRequest.selector;
_whiteListFunctionSelectors[Action.REQUEST_ADD_WHITELIST_TOKEN] =
this.afterReceiveAddWhitelistTokenRequest.selector;
// overwrite the bootstrap function selector
_whiteListFunctionSelectors[Action.REQUEST_MARK_BOOTSTRAP] = this.afterReceiveMarkBootstrapRequest.selector;

Expand Down
41 changes: 17 additions & 24 deletions src/core/ClientGatewayLzReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -290,41 +290,34 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
}
}

/// @notice Called after an add-whitelist-tokens response is received.
/// @notice Called after an add-whitelist-token response is received.
/// @param requestPayload The request payload.
// Though `_deployVault` would make external call to newly created `Vault` contract and initialize it,
// `Vault` contract belongs to Exocore and we could make sure its implementation does not have dangerous behavior
// like reentrancy.
// slither-disable-next-line reentrancy-no-eth
function afterReceiveAddWhitelistTokensRequest(bytes calldata requestPayload)
function afterReceiveAddWhitelistTokenRequest(bytes calldata requestPayload)
public
onlyCalledFromThis
whenNotPaused
{
uint8 count = uint8(requestPayload[0]);
uint256 expectedLength = count * TOKEN_ADDRESS_BYTES_LENGTH + 1;
if (requestPayload.length != expectedLength) {
revert InvalidAddWhitelistTokensRequest(expectedLength, requestPayload.length);
address token = address(bytes20(abi.decode(requestPayload, (bytes32))));
if (token == address(0)) {
revert Errors.ZeroAddress();
}

for (uint256 i = 0; i < count; ++i) {
uint256 start = i * TOKEN_ADDRESS_BYTES_LENGTH + 1;
uint256 end = start + TOKEN_ADDRESS_BYTES_LENGTH;
address token = address(bytes20(requestPayload[start:end]));

if (!isWhitelistedToken[token]) {
isWhitelistedToken[token] = true;
whitelistTokens.push(token);

// do not deploy the vault for the virtual token address representing natively staked ETH
// deploy the corresponding vault if not deployed before
if (token != VIRTUAL_STAKED_ETH_ADDRESS && address(tokenToVault[token]) == address(0)) {
_deployVault(token);
}

emit WhitelistTokenAdded(token);
}
if (isWhitelistedToken[token]) {
// grave error, should never happen
revert Errors.ClientChainGatewayAlreadyWhitelisted(token);
}
isWhitelistedToken[token] = true;
whitelistTokens.push(token);
// since tokens cannot be removed from the whitelist, it is not possible for a vault
// to already exist. however, we should still ensure that a vault is not deployed for
// restaking native staked eth
if (token != VIRTUAL_STAKED_ETH_ADDRESS) {
_deployVault(token);
}
emit WhitelistTokenAdded(token);
}

/// @notice Called after a mark-bootstrap response is received.
Expand Down
113 changes: 48 additions & 65 deletions src/core/ExocoreGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -168,51 +168,60 @@ contract ExocoreGateway is
/// @notice If we want to activate client chain's native restaking, we should add the corresponding virtual
/// token address to the whitelist, bytes32(bytes20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) for Ethereum
/// native restaking for example.
function addOrUpdateWhitelistTokens(
function addWhitelistToken(
uint32 clientChainId,
bytes32[] calldata tokens,
uint8[] calldata decimals,
uint256[] calldata tvlLimits,
string[] calldata names,
string[] calldata metaData
bytes32 token,
uint8 decimals,
uint256 tvlLimit,
string calldata name,
string calldata metaData,
string calldata oracleInfo
) external payable onlyOwner whenNotPaused nonReentrant {
// The registration of the client chain is left for the precompile to validate.
_validateWhitelistTokensInput(tokens, decimals, tvlLimits, names, metaData);

bool success;
bool updated;
for (uint256 i = 0; i < tokens.length; ++i) {
require(tokens[i] != bytes32(0), "ExocoreGateway: token cannot be zero address");
require(tvlLimits[i] > 0, "ExocoreGateway: tvl limit should not be zero");
require(bytes(names[i]).length != 0, "ExocoreGateway: name cannot be empty");
require(bytes(metaData[i]).length != 0, "ExocoreGateway: meta data cannot be empty");

(success, updated) = ASSETS_CONTRACT.registerOrUpdateTokens(
clientChainId, abi.encodePacked(tokens[i]), decimals[i], tvlLimits[i], names[i], metaData[i]
);

if (success) {
if (!updated) {
emit WhitelistTokenAdded(clientChainId, tokens[i]);
} else {
emit WhitelistTokenUpdated(clientChainId, tokens[i]);
}
} else {
if (!updated) {
revert AddWhitelistTokenFailed(tokens[i]);
} else {
revert UpdateWhitelistTokenFailed(tokens[i]);
}
}
}

if (!updated) {
require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero");
require(token != bytes32(0), "ExocoreGateway: token cannot be zero address");
require(tvlLimit > 0, "ExocoreGateway: tvl limit should not be zero");
require(bytes(name).length != 0, "ExocoreGateway: name cannot be empty");
require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty");
require(bytes(oracleInfo).length != 0, "ExocoreGateway: oracleInfo cannot be empty");

bool success = ASSETS_CONTRACT.registerToken(
clientChainId,
abi.encodePacked(token), // convert to bytes from bytes32
decimals,
tvlLimit,
name,
metaData,
oracleInfo
);
if (success) {
emit WhitelistTokenAdded(clientChainId, token);
_sendInterchainMsg(
clientChainId,
Action.REQUEST_ADD_WHITELIST_TOKENS,
abi.encodePacked(uint8(tokens.length), tokens),
Action.REQUEST_ADD_WHITELIST_TOKEN,
abi.encodePacked(token), // convert for decoding it on the receiving end
false
);
} else {
revert AddWhitelistTokenFailed(clientChainId, token);
}
}

/// @inheritdoc IExocoreGateway
function updateWhitelistToken(uint32 clientChainId, bytes32 token, uint256 tvlLimit, string calldata metaData)
external
onlyOwner
whenNotPaused
nonReentrant
{
require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero");
require(token != bytes32(0), "ExocoreGateway: token cannot be zero address");
// setting tvlLimit to 0 is allowed as a way to disable the token
// empty metaData indicates that the token's metadata should not be updated
bool success = ASSETS_CONTRACT.updateToken(clientChainId, abi.encodePacked(token), tvlLimit, metaData);
if (success) {
emit WhitelistTokenUpdated(clientChainId, token);
} else {
revert UpdateWhitelistTokenFailed(clientChainId, token);
}
}

Expand Down Expand Up @@ -249,32 +258,6 @@ contract ExocoreGateway is
}
}

/// @dev Validates the input for whitelist tokens.
/// @param tokens The list of token addresses, length must be <= 255.
/// @param decimals The list of token decimals, length must be equal to that of @param tokens.
/// @param tvlLimits The list of token TVL limits, length must be equal to that of @param tokens.
/// @param names The list of token names, length must be equal to that of @param tokens.
/// @param metaData The list of token meta data, length must be equal to that of @param tokens.
function _validateWhitelistTokensInput(
bytes32[] calldata tokens,
uint8[] calldata decimals,
uint256[] calldata tvlLimits,
string[] calldata names,
string[] calldata metaData
) internal pure {
uint256 expectedLength = tokens.length;
if (expectedLength > type(uint8).max) {
revert WhitelistTokensListTooLong();
}

if (
decimals.length != expectedLength || tvlLimits.length != expectedLength || names.length != expectedLength
|| metaData.length != expectedLength
) {
revert InvalidWhitelistTokensInput();
}
}

/// @dev Validates that the client chain id is registered.
/// @dev This is designed to be called only in the cases wherein the precompile isn't used.
/// @dev In all other situations, it is the responsibility of the precompile to perform such
Expand Down
39 changes: 27 additions & 12 deletions src/interfaces/IExocoreGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,38 @@ interface IExocoreGateway is IOAppReceiver, IOAppCore {
string calldata signatureType
) external;

/// @notice Adds a list of whitelisted tokens to the client chain.
/// @notice Add a single whitelisted token to the client chain.
/// @param clientChainId The LayerZero chain id of the client chain.
/// @param tokens The list of token addresses to be whitelisted.
/// @param decimals The list of token decimals, in the same order as the tokens list.
/// @param tvlLimits The list of token TVL limits (typically max supply),in the same order as the tokens list.
/// @param names The names of the tokens, in the same order as the tokens list.
/// @param metaData The meta information of the tokens, in the same order as the tokens list.
/// @param token The token address to be whitelisted.
/// @param decimals The decimals of the token.
/// @param tvlLimit The TVL limit of the token.
/// @param name The name of the token.
/// @param metaData The meta information of the token.
/// @param oracleInfo The oracle information of the token.
/// @dev The chain must be registered before adding tokens.
function addOrUpdateWhitelistTokens(
/// @dev This function is payable because it sends a message to the client chain.
/// @dev Previously, we tried to use this function for multiple tokens, but that
/// results in too many local variables (stack too deep).
function addWhitelistToken(
uint32 clientChainId,
bytes32[] calldata tokens,
uint8[] calldata decimals,
uint256[] calldata tvlLimits,
string[] calldata names,
string[] calldata metaData
bytes32 token,
uint8 decimals,
uint256 tvlLimit,
string calldata name,
string calldata metaData,
string calldata oracleInfo
) external payable;

/// @notice Updates the parameters for a whitelisted token on the client chain.
/// @param clientChainId The LayerZero chain id of the client chain.
/// @param token The token address to be updated.
/// @param tvlLimit The new TVL limit of the token.
/// @param metaData The new meta information of the token.
/// @dev The token must exist in the whitelist before updating.
/// @dev Since this function does not send a cross chain message, it is not payable.
function updateWhitelistToken(uint32 clientChainId, bytes32 token, uint256 tvlLimit, string calldata metaData)
external;

/// @notice Marks the network as bootstrapped, on the client chain.
/// @dev Causes an upgrade of the Bootstrap contract to the ClientChainGateway contract.
/// @dev Only works if LZ infrastructure is set up and SetPeer has been called.
Expand Down
29 changes: 22 additions & 7 deletions src/interfaces/precompiles/IAssets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,41 @@ interface IAssets {
string calldata signatureType
) external returns (bool success, bool updated);

/// @dev register or update token addresses to exocore
/// @dev register a token to allow deposits / staking, etc.
/// @dev note that there is no way to delete a token. If a token is to be removed,
/// the TVL limit should be set to 0.
/// @param clientChainID is the identifier of the token's home chain (LZ or otherwise)
/// @param clientChainId is the identifier of the token's home chain (LZ or otherwise)
/// @param token is the address of the token on the home chain
/// @param decimals is the number of decimals of the token
/// @param tvlLimit is the number of tokens that can be deposited in the system. Set to
/// maxSupply if there is no limit
/// @param name is the name of the token
/// @param metaData is the arbitrary metadata of the token
/// @param oracleInfo is the oracle information of the token
/// @return success if the token registration is successful
/// @return updated whether the token was added or updated
function registerOrUpdateTokens(
uint32 clientChainID,
function registerToken(
uint32 clientChainId,
bytes calldata token,
uint8 decimals,
uint256 tvlLimit,
string calldata name,
string calldata metaData
) external returns (bool success, bool updated);
string calldata metaData,
string calldata oracleInfo
) external returns (bool success);

/// @dev update a token to allow deposits / staking, etc.
/// @param clientChainId is the identifier of the token's home chain (LZ or otherwise)
/// @param token is the address of the token on the home chain
/// @param tvlLimit is the number of tokens that can be deposited in the system. Set to
/// maxSupply if there is no limit
/// @param metaData is the arbitrary metadata of the token
/// @return success if the token update is successful
/// @dev The token must previously be registered before updating
/// @dev Pass a tvlLimit of 0 to disable any deposits of the token
/// @dev Pass en empty metadata to keep the existing metadata
function updateToken(uint32 clientChainId, bytes calldata token, uint256 tvlLimit, string calldata metaData)
external
returns (bool success);

/// QUERIES
/// @dev Returns the chain indices of the client chains.
Expand Down
Loading

0 comments on commit 806c050

Please sign in to comment.