Skip to content

Commit

Permalink
feat: prevent misconfig of supply and tvl limits
Browse files Browse the repository at this point in the history
The objective of this change is to prevent deposit failures by ensuring
that the tvlLimit (as enforced on the client chain) <= totalSupply (as
enforced by Exocore). Note that, with time, the totalSupply stored on
Exocore may become outdated as well; however, it is a secondary check
which occurs only after tokens are transferred into the Vault on the
client chain.

Since the addition of tokens to the whitelist happens through Exocore,
it can be enforced easily that the tvlLimit of said token is less than
the total supply (as known to Exocore).

Whenever a transaction to update the tvlLimit on a client chain is
received, it is handled on a case-by-case basis.
- A decrease in tvl limit is always allowed.
- An increase in tvl limit is forwarded to Exocore for validation.

Exocore receives this validation request, and responds in the
affirmative if the new tvl limit <= supply of the token (again,
according to Exocore). It responds in the negative, otherwise.

Reciprocally, whenever an attempt is made to change the recorded value
of total supply corresponding to a token on Exocore, here's what
happens.
- An increase in the total supply is always allowed.
- A decrease in the total supply is forwarded to the client chain for
  validation.

when the client chain receives this validation request, it responds in
the affirmative if the tvl limit <= new total supply (which may be
totally different from the actual total supply due to changes in the
number when the message was in-flight). Otherwise, it responds in the
negative.

One caveat for both of these validations is that if a validation request
arising from the validating chain is already in-flight, the validation
is rejected. This is to ensure that in-flight messages cannot result in
a misconfiguration.

For example, if the TVL limit is increased from 400m to 500m and the
total supply is 600m, it will be approved. However, if, at the same
time, there is a validation request sent to the client chain by Exocore
to verify if the total supply can be reduced to 450m, it could cause
inconsistency. In this event, the TVL limit increase is rejected and the
total supply decrease will be approved.
  • Loading branch information
MaxMustermann2 committed Sep 7, 2024
1 parent 60faeb5 commit 25eee39
Show file tree
Hide file tree
Showing 27 changed files with 1,099 additions and 358 deletions.
4 changes: 2 additions & 2 deletions script/3_Setup.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ contract SetupScript is BaseScript {
names[0] = "RestakeToken";
metaDatas[0] = "ERC20 LST token";
oracleInfos[0] = "{'a': 'b'}";
tvlLimits[0] = restakeToken.totalSupply() / 5; // in phases of 20%
tvlLimits[0] = supplies[0] / 5; // in phases of 20%

// this stands for Native Restaking for ETH
whitelistTokensBytes32[1] = bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS));
decimals[1] = 18;
supplies[1] = type(uint256).max; // no supply limit for native restaking
supplies[1] = 0; // irrelevant for native restaking
names[1] = "StakedETH";
metaDatas[1] = "natively staked ETH on Ethereum";
oracleInfos[1] = "{'b': 'a'}";
Expand Down
18 changes: 13 additions & 5 deletions src/core/BaseRestakingController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,31 @@ abstract contract BaseRestakingController is
/// @param encodedRequest The encoded request.
function _processRequest(Action action, bytes memory actionArgs, bytes memory encodedRequest) internal {
uint64 requestNonce = _sendMsgToExocore(action, actionArgs);

_registeredRequests[requestNonce] = encodedRequest;
_registeredRequestActions[requestNonce] = action;
if (action != Action.RESPOND) {
_registeredRequests[requestNonce] = encodedRequest;
_registeredRequestActions[requestNonce] = action;
}
}

/// @dev Sends a message to Exocore.
/// @param action The action to be performed.
/// @param actionArgs The encodePacked arguments for the action.
function _sendMsgToExocore(Action action, bytes memory actionArgs) internal returns (uint64) {
bool payByApp = action == Action.RESPOND;
bytes memory payload = abi.encodePacked(action, actionArgs);
bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(
DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE
).addExecutorOrderedExecutionOption();
MessagingFee memory fee = _quote(EXOCORE_CHAIN_ID, payload, options, false);

MessagingReceipt memory receipt =
_lzSend(EXOCORE_CHAIN_ID, payload, options, MessagingFee(fee.nativeFee, 0), msg.sender, false);
MessagingReceipt memory receipt = _lzSend(
EXOCORE_CHAIN_ID,
payload,
options,
MessagingFee(fee.nativeFee, 0),
payByApp ? address(this) : msg.sender,
payByApp
);
emit MessageSent(action, receipt.guid, receipt.nonce, receipt.fee.nativeFee);

return receipt.nonce;
Expand Down
26 changes: 18 additions & 8 deletions src/core/Bootstrap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ contract Bootstrap is
if (isWhitelistedToken[token]) {
revert Errors.BootstrapAlreadyWhitelisted(token);
}

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

Expand All @@ -215,6 +214,11 @@ contract Bootstrap is
// 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) {
// during bootstrap phase, ensure that the tvl limit <= total supply of the token
uint256 totalSupply = ERC20(token).totalSupply();
if (tvlLimit > totalSupply) {
revert Errors.BootstrapTvlLimitExceedsTotalSupply();
}
_deployVault(token, tvlLimit);
}

Expand All @@ -228,13 +232,19 @@ contract Bootstrap is
}

/// @inheritdoc ITokenWhitelister
function updateTvlLimits(address[] calldata tokens, uint256[] calldata tvlLimits)
external
beforeLocked
onlyOwner
whenNotPaused
{
_updateTvlLimits(tokens, tvlLimits);
function updateTvlLimit(address token, uint256 tvlLimit) external payable beforeLocked onlyOwner whenNotPaused {
if (msg.value > 0) {
revert Errors.NonZeroValue();
}
if (!isWhitelistedToken[token]) {
revert Errors.TokenNotWhitelisted(token);
}
if (token == VIRTUAL_STAKED_ETH_ADDRESS) {
revert Errors.NoTvlLimitForNativeRestaking();
}
IVault vault = _getVault(token);
// no checks for Bootstrap phase
vault.setTvlLimit(tvlLimit);
}

/// @inheritdoc IValidatorRegistry
Expand Down
49 changes: 43 additions & 6 deletions src/core/ClientChainGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity ^0.8.19;

import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol";
import {ITokenWhitelister} from "../interfaces/ITokenWhitelister.sol";
import {IVault} from "../interfaces/IVault.sol";

import {OAppCoreUpgradeable} from "../lzApp/OAppCoreUpgradeable.sol";
import {OAppReceiverUpgradeable} from "../lzApp/OAppReceiverUpgradeable.sol";
import {MessagingFee, OAppSenderUpgradeable} from "../lzApp/OAppSenderUpgradeable.sol";
Expand Down Expand Up @@ -73,6 +75,7 @@ contract ClientChainGateway is

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

Expand Down Expand Up @@ -121,12 +124,32 @@ contract ClientChainGateway is
}

/// @inheritdoc ITokenWhitelister
function updateTvlLimits(address[] calldata tokens, uint256[] calldata tvlLimits)
external
onlyOwner
whenNotPaused
{
_updateTvlLimits(tokens, tvlLimits);
function updateTvlLimit(address token, uint256 tvlLimit) external payable onlyOwner whenNotPaused {
if (!isWhitelistedToken[token]) {
// grave error, should never happen
revert Errors.TokenNotWhitelisted(token);
}
if (token == VIRTUAL_STAKED_ETH_ADDRESS) {
// not possible to set a TVL limit for native restaking
revert Errors.NoTvlLimitForNativeRestaking();
}
IVault vault = _getVault(token);
uint256 previousLimit = vault.getTvlLimit();
if (tvlLimit <= previousLimit) {
// reduction of TVL limit is always allowed.
if (msg.value > 0) {
revert Errors.NonZeroValue();
}
vault.setTvlLimit(tvlLimit);
} else {
// queue message to Exocore for validation that the tvlLimit <= totalSupply.
// to remove a configured tvl limit, set it (close to) the total supply (as on Exocore).
// note that, since each message has an increasing nonce, multiple tvl updates may be in flight at once.
// they will be processed in order.
bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), tvlLimit);
bytes memory encodedRequest = abi.encode(token, tvlLimit);
_processRequest(Action.REQUEST_VALIDATE_LIMITS, actionArgs, encodedRequest);
}
}

/// @inheritdoc IClientChainGateway
Expand All @@ -149,4 +172,18 @@ contract ClientChainGateway is
return (SENDER_VERSION, RECEIVER_VERSION);
}

/// @notice Called after a validate-limits request is received.
/// @dev It checks that the proposed total supply >= tvlLimit.
function afterReceiveValidateLimitsRequest(uint64 lzNonce, bytes calldata requestPayload)
public
onlyCalledFromThis
whenNotPaused
{
(address token, uint256 newSupply) = _decodeTokenUint256(requestPayload, false);
IVault vault = _getVault(token);
uint256 tvlLimit = vault.getTvlLimit();
bool success = tvlLimit <= newSupply && !tvlLimitIncreaseInFlight[token];
_sendMsgToExocore(Action.RESPOND, abi.encodePacked(lzNonce, success));
}

}
50 changes: 37 additions & 13 deletions src/core/ClientGatewayLzReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
}

(bool success, bytes memory reason) =
address(this).call(abi.encodePacked(selector_, abi.encode(payload[1:])));
address(this).call(abi.encodePacked(selector_, abi.encode(_origin.nonce, payload[1:])));
if (!success) {
revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason);
}
}
emit MessageExecuted(act, _origin.nonce);
}

/// @inheritdoc OAppReceiverUpgradeable
Expand Down Expand Up @@ -100,6 +101,16 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp

if (_expectBasicResponse(requestAct)) {
success = _decodeBasicResponse(response);
if (requestAct == Action.REQUEST_VALIDATE_LIMITS) {
(address token,,, uint256 tvlLimit) = _decodeCachedRequest(requestAct, cachedRequest);
if (success) {
// since the request was sent by us, the token can never be native restaking and must be whitelisted
IVault vault = _getVault(token);
vault.setTvlLimit(tvlLimit);
}
// remove the lock regardless of success
tvlLimitIncreaseInFlight[token] = false;
}
} else if (_expectBalanceResponse(requestAct)) {
(address token, address staker,, uint256 amount) = _decodeCachedRequest(requestAct, cachedRequest);
(success, updatedBalance) = _decodeBalanceResponse(response);
Expand Down Expand Up @@ -177,7 +188,8 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
// Basic response only includes request execution status, no other informations like balance update
// and it is typically the response of a staking only operations.
function _expectBasicResponse(Action action) internal pure returns (bool) {
return action == Action.REQUEST_DELEGATE_TO || action == Action.REQUEST_UNDELEGATE_FROM;
return action == Action.REQUEST_DELEGATE_TO || action == Action.REQUEST_UNDELEGATE_FROM
|| action == Action.REQUEST_VALIDATE_LIMITS;
}

/// @dev Checks if the action is a balance response.
Expand Down Expand Up @@ -229,6 +241,8 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
(token, staker, amount) = abi.decode(cachedRequest, (address, address, uint256));
} else if (_isStakingOperationRequest(requestAct)) {
(token, staker, operator, amount) = abi.decode(cachedRequest, (address, address, string, uint256));
} else if (requestAct == Action.REQUEST_VALIDATE_LIMITS) {
(token, amount) = abi.decode(cachedRequest, (address, uint256));
} else {
revert UnsupportedRequest(requestAct);
}
Expand Down Expand Up @@ -296,20 +310,12 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
// `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 afterReceiveAddWhitelistTokenRequest(bytes calldata requestPayload)
function afterReceiveAddWhitelistTokenRequest(uint64, bytes calldata requestPayload)
public
onlyCalledFromThis
whenNotPaused
{
(bytes32 tokenAsBytes32, uint256 tvlLimit) = abi.decode(requestPayload, (bytes32, uint256));
address token = address(bytes20(tokenAsBytes32));
if (token == address(0)) {
revert Errors.ZeroAddress();
}
if (isWhitelistedToken[token]) {
// grave error, should never happen
revert Errors.ClientChainGatewayAlreadyWhitelisted(token);
}
(address token, uint256 tvlLimit) = _decodeTokenUint256(requestPayload, true);
isWhitelistedToken[token] = true;
whitelistTokens.push(token);
// since tokens cannot be removed from the whitelist, it is not possible for a vault
Expand All @@ -325,8 +331,26 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp
/// @dev Since the contract is already bootstrapped (if we are here), there is nothing to do.
/// @dev Failing this, however, will cause a nonce mismatch resulting in a system halt.
/// Hence, we silently ignore this call.
function afterReceiveMarkBootstrapRequest() public onlyCalledFromThis whenNotPaused {
function afterReceiveMarkBootstrapRequest(uint64) public onlyCalledFromThis whenNotPaused {
emit BootstrappedAlready();
}

function _decodeTokenUint256(bytes calldata payload, bool failIfWhitelisted)
internal
view
returns (address, uint256)
{
(bytes32 tokenAsBytes32, uint256 value) = abi.decode(payload, (bytes32, uint256));
address token = address(bytes20(tokenAsBytes32));
if (token == address(0)) {
// cannot happen since ExocoreGateway checks for this
revert Errors.ZeroAddress();
}
if ((failIfWhitelisted) && (isWhitelistedToken[token])) {
// grave error, should never happen
revert Errors.ClientChainGatewayAlreadyWhitelisted(token);
}
return (token, value);
}

}
Loading

0 comments on commit 25eee39

Please sign in to comment.