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

refactor: addWhitelistTokens require more token info than just token address #39

Merged
merged 19 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/contracts-owner-manual.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Contract Owner's Manual

Currently, almost all contracts are ownable contracts including `Bootstrap`, `ClientChainGateway`, `Vault`, `ExocoreGateway`. The owner of these contracts is a privileged account in many aspects including upgrading contract by changing implementation, registering some client chain to Exocore before enabling restaking from that client chain, adding token info to whitelist and even pausing/unpausing contracts in case of emergencies.

## Owner Only Functionalities

## `pause`/`unpause`

In case of emergencies where some unexpected errors happened to the protocol, contract owner could pause the contract to disable all user-facing functions and re-enable these functions after successfully recovering from emergencies. Currently, pauseable contracts include `Bootstrap`, `ClientChainGateway`, `ExocoreGateway`. For `Vault` and `ExoCapsule`, as all the functions are limited to be called by their management contract(`ClientChainGateway`, `Bootstrap`), their functionalities are also paused when their management contract get paused.

## upgrade to new implementation

Most contracts including `Bootstrap`, `ClientChainGateway`, `ExocoreGateway`, `Vault` and `ExoCapsule` are upgradeable. For `Bootstrap`, `ClientChainGateway` and `ExocoreGateway`, they are upgradeable through `ProxyAdmin` contract, so the owner of `ProxyAdmin` could upgrade these contracts by changing implementation contract.
While for `Vault` and `ExoCapsule`, they are deployed with upgradeable beacon proxy, so only `upgradeableBeacon` could access the upgrading functionalities of these contracts, that is to say only the owner of `upgradeableBeacon` could upgrade these contracts by changing implementation contract. Notice as all `Vault`s are deployed with upgradeable beacon proxy and all these proxies point to the same `upgradeableBeacon` contract, when the owner change implementation contract stored in `upgradeableBeacon`, all `Vault`s get upgraded to the same new version of contract, and same case for `ExoCapsule`.

## register client chain

After all contracts are deployed, before the protocol starts to work, there are a few works left to be done by contract owner to enable restaking. One of the most important tasks is to register the client chain id and meta info to Exocore to mark this client chain as valid. This is done by the contract caller calling `ExocoreGateway.registerOrUpdateClientChain` to write `clientChainId`, `addressLength`, `name`, `signatureType` and other meta data to Exocore native module to finish registration. This operation would also call `ExocoreGateway.setPeer` to enable LayerZero messaging by setting remote `ClientChainGateway` as trusted peer to send/receive messages. After finishing registration, contract owner could call `ExocoreGateway.registerOrUpdateClientChain` again to update the meta data and set new peer, or contract owner could solely call `ExocoreGateway.setPeer` to change the address of remote peer contract.

## add tokens to whitelist

Another important task before restaking being activated is to add tokens to whitelist to mark them as stake-able on both Exocore and client chain. This is done by contract owner calling `ExocoreGateway.addWhitelistTokens` to write token addresses, decimals, TVL limits and other metadata to Exocore, as well as sending a cross-chain message through layerzero to client chain to add these token addresses to the whitelist of `ClientChainGateway`.

Notice: contract owner must make sure the token data is correct like address, decimals and TVL limit, more importantly contract owner must ensure that for the same index, the data in different arrays like `tokens`, `decimals`, `tvlLimits` must point to the same token to be composed as complete token data.

## upgrade the meta data of whitelisted tokens

After adding tokens to whitelist, contract owner could call `ExocoreGateway.updateWhitelistedTokens` to update the meta data of already whitelisted tokens, and this function would not send a cross-chain message to client chain since the whitelist of `ClientChainGateway` only stores the token addresses.
1 change: 0 additions & 1 deletion script/13_DepositValidator.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ contract DepositScript is BaseScript {

uint256 internal constant GENESIS_BLOCK_TIMESTAMP = 1_695_902_400;
uint256 internal constant SECONDS_PER_SLOT = 12;
address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint256 constant GWEI_TO_WEI = 1e9;

function setUp() public virtual override {
Expand Down
52 changes: 44 additions & 8 deletions script/3_Setup.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ contract SetupScript is BaseScript {
exocoreLzEndpoint = ILayerZeroEndpointV2(stdJson.readAddress(deployedContracts, ".exocore.lzEndpoint"));
require(address(exocoreLzEndpoint) != address(0), "exocoreLzEndpoint address should not be empty");

if (!useExocorePrecompileMock) {
_bindPrecompileMocks();
}

// transfer some gas fee to exocore validator set address
clientChain = vm.createSelectFork(clientChainRPCURL);
_topUpPlayer(clientChain, address(0), deployer, exocoreValidatorSet.addr, 0.2 ether);
Expand All @@ -50,6 +54,7 @@ contract SetupScript is BaseScript {

function run() public {
// 1. setup client chain contracts to make them ready for sending and receiving messages from exocore gateway

vm.selectFork(clientChain);
// Exocore validator set should be the owner of these contracts and only owner could setup contracts state
vm.startBroadcast(exocoreValidatorSet.privateKey);
Expand All @@ -65,7 +70,8 @@ contract SetupScript is BaseScript {
vm.stopBroadcast();

// 2. setup Exocore contracts to make them ready for sending and receiving messages from client chain
// gateway
// gateway, and register client chain meta data to Exocore native module

vm.selectFork(exocore);
// Exocore validator set should be the owner of these contracts and only owner could setup contracts state
vm.startBroadcast(exocoreValidatorSet.privateKey);
Expand All @@ -75,16 +81,46 @@ contract SetupScript is BaseScript {
address(clientGateway), address(clientChainLzEndpoint)
);
}
// this would also register clientChainId to Exocore native module
exocoreGateway.setPeer(clientChainId, address(clientGateway).toBytes32());
// register clientChainId to Exocore native module and set peer for client chain gateway to be ready for
// messaging
exocoreGateway.registerOrUpdateClientChain(
clientChainId, address(clientGateway).toBytes32(), 20, "ClientChain", "EVM compatible network", "secp256k1"
);
vm.stopBroadcast();

// 3. we should register whitelist tokens to exocore
// 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);
// Exocore validator set should be the owner of these contracts and only owner could add whitelist tokens
vm.startBroadcast(exocoreValidatorSet.privateKey);
whitelistTokens.push(address(restakeToken));
clientGateway.addWhitelistTokens(whitelistTokens);
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);

// 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";

// 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();

// 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.addWhitelistTokens{value: nativeFee}(
clientChainId, whitelistTokensBytes32, decimals, tvlLimits, names, metaData
);
vm.stopBroadcast();
}

Expand Down
2 changes: 2 additions & 0 deletions script/BaseScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ contract BaseScript is Script {

uint256 constant DEPOSIT_AMOUNT = 1 ether;
uint256 constant WITHDRAW_AMOUNT = 1 ether;
address internal constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint256 internal constant TOKEN_ADDRESS_BYTES_LENGTH = 32;

bool useExocorePrecompileMock;
bool useEndpointMock;
Expand Down
2 changes: 1 addition & 1 deletion src/core/Bootstrap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ contract Bootstrap is
}

// implementation of ITokenWhitelister
function addWhitelistTokens(address[] calldata tokens) external payable beforeLocked onlyOwner whenNotPaused {
function addWhitelistTokens(address[] calldata tokens) external beforeLocked onlyOwner whenNotPaused {
_addWhitelistTokens(tokens);
}

Expand Down
25 changes: 5 additions & 20 deletions src/core/ClientChainGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ contract ClientChainGateway is
this.afterReceiveWithdrawRewardResponse.selector;
_registeredResponseHooks[Action.REQUEST_DEPOSIT_THEN_DELEGATE_TO] =
this.afterReceiveDepositThenDelegateToResponse.selector;
_registeredResponseHooks[Action.REQUEST_REGISTER_TOKENS] = this.afterReceiveRegisterTokensResponse.selector;

_whiteListFunctionSelectors[Action.REQUEST_ADD_WHITELIST_TOKENS] =
this.afterReceiveAddWhitelistTokensRequest.selector;

bootstrapped = true;

Expand Down Expand Up @@ -119,25 +121,8 @@ contract ClientChainGateway is
_unpause();
}

// implementation of ITokenWhitelister
function addWhitelistTokens(address[] calldata tokens) external payable onlyOwner whenNotPaused nonReentrant {
_addWhitelistTokens(tokens);
}

function _addWhitelistTokens(address[] calldata tokens) internal {
require(tokens.length <= type(uint8).max, "ClientChainGateway: tokens length should not execeed 255");

bytes memory actionArgs = abi.encodePacked(uint8(tokens.length));
for (uint256 i; i < tokens.length; i++) {
address token = tokens[i];
require(token != address(0), "ClientChainGateway: zero token address");
require(!isWhitelistedToken[token], "ClientChainGateway: token should not be whitelisted before");

actionArgs = abi.encodePacked(actionArgs, bytes32(bytes20(token)));
}

bytes memory encodedRequest = abi.encode(tokens);
_processRequest(Action.REQUEST_REGISTER_TOKENS, actionArgs, encodedRequest);
function addWhitelistTokens(address[] calldata) external onlyOwner whenNotPaused {
revert("this function is not supported for client chain, please register on Exocore");
}

// implementation of ITokenWhitelister
Expand Down
23 changes: 14 additions & 9 deletions src/core/ClientGatewayLzReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
error UnsupportedResponse(Action act);
error UnexpectedResponse(uint64 nonce);
error DepositShouldNotFailOnExocore(address token, address depositor);
error InvalidAddWhitelistTokensRequest(uint256 expectedLength, uint256 actualLength);

modifier onlyCalledFromThis() {
require(
Expand Down Expand Up @@ -182,30 +183,34 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
emit DepositThenDelegateResult(delegateSuccess, delegator, operator, token, amount);
}

function afterReceiveRegisterTokensResponse(bytes calldata requestPayload, bytes calldata responsePayload)
function afterReceiveAddWhitelistTokensRequest(bytes calldata requestPayload)
public
onlyCalledFromThis
whenNotPaused
{
address[] memory tokens = abi.decode(requestPayload, (address[]));
uint8 count = uint8(requestPayload[0]);
uint256 expectedLength = count * TOKEN_ADDRESS_BYTES_LENGTH + 1;
if (requestPayload.length != expectedLength) {
revert InvalidAddWhitelistTokensRequest(expectedLength, requestPayload.length);
}

bool success = (uint8(bytes1(responsePayload[0])) == 1);
if (success) {
for (uint256 i; i < tokens.length; i++) {
address token = tokens[i];
for (uint256 i; 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);

// deploy the corresponding vault if not deployed before
if (address(tokenToVault[token]) == address(0)) {
if (token != VIRTUAL_STAKED_ETH_ADDRESS && address(tokenToVault[token]) == address(0)) {
_deployVault(token);
}

emit WhitelistTokenAdded(token);
}
}

emit RegisterTokensResult(success);
}

}
Loading
Loading