Skip to content

Commit

Permalink
feat: role and erc7201 storage
Browse files Browse the repository at this point in the history
  • Loading branch information
loga4 committed Nov 27, 2024
1 parent 1e57a1b commit f9ca4a4
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 57 deletions.
150 changes: 94 additions & 56 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ import {IBeaconProxy} from "./interfaces/IBeaconProxy.sol";
/// in the same time
/// @author folkyatina
abstract contract VaultHub is AccessControlEnumerableUpgradeable {
/// @notice role that allows to connect vaults to the hub
bytes32 public constant VAULT_MASTER_ROLE = keccak256("Vaults.VaultHub.VaultMasterRole");
/// @dev basis points base
uint256 internal constant BPS_BASE = 100_00;
/// @dev maximum number of vaults that can be connected to the hub
uint256 internal constant MAX_VAULTS_COUNT = 500;
/// @dev maximum size of the single vault relative to Lido TVL in basis points
uint256 internal constant MAX_VAULT_SIZE_BP = 10_00;

StETH public immutable stETH;
address public immutable treasury;
/// @custom:storage-location erc7201:VaultHub
struct VaultHubStorage {
/// @notice vault sockets with vaults connected to the hub
/// @dev first socket is always zero. stone in the elevator
VaultSocket[] sockets;

/// @notice mapping from vault address to its socket
/// @dev if vault is not connected to the hub, its index is zero
mapping(IHubVault => uint256) vaultIndex;

/// @notice allowed factory addresses
mapping (address => bool) vaultFactories;
/// @notice allowed vault implementation addresses
mapping (address => bool) vaultImpl;
}

struct VaultSocket {
/// @notice vault address
Expand All @@ -46,52 +50,65 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
uint16 treasuryFeeBP;
}

/// @notice vault sockets with vaults connected to the hub
/// @dev first socket is always zero. stone in the elevator
VaultSocket[] private sockets;
/// @notice mapping from vault address to its socket
/// @dev if vault is not connected to the hub, its index is zero
mapping(IHubVault => uint256) private vaultIndex;
// keccak256(abi.encode(uint256(keccak256("VaultHub")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant VAULT_HUB_STORAGE_LOCATION =
0xb158a1a9015c52036ff69e7937a7bb424e82a8c4cbec5c5309994af06d825300;

mapping (address => bool) public vaultFactories;
mapping (address => bool) public vaultImpl;
/// @notice role that allows to connect vaults to the hub
bytes32 public constant VAULT_MASTER_ROLE = keccak256("Vaults.VaultHub.VaultMasterRole");
/// @notice role that allows to add factories and vault implementations to hub
bytes32 public constant VAULT_REGISTRY_ROLE = keccak256("Vaults.VaultHub.VaultRegistryRole");
/// @dev basis points base
uint256 internal constant BPS_BASE = 100_00;
/// @dev maximum number of vaults that can be connected to the hub
uint256 internal constant MAX_VAULTS_COUNT = 500;
/// @dev maximum size of the single vault relative to Lido TVL in basis points
uint256 internal constant MAX_VAULT_SIZE_BP = 10_00;

StETH public immutable stETH;
address public immutable treasury;

constructor(address _admin, StETH _stETH, address _treasury) {
stETH = _stETH;
treasury = _treasury;

sockets.push(VaultSocket(IHubVault(address(0)), 0, 0, 0, 0, 0)); // stone in the elevator
_getVaultHubStorage().sockets.push(VaultSocket(IHubVault(address(0)), 0, 0, 0, 0, 0)); // stone in the elevator

_grantRole(DEFAULT_ADMIN_ROLE, _admin);
}

function addFactory(address factory) public onlyRole(VAULT_MASTER_ROLE) {
if (vaultFactories[factory]) revert AlreadyExists(factory);
vaultFactories[factory] = true;
/// @notice added factory address to allowed list
function addFactory(address factory) public onlyRole(VAULT_REGISTRY_ROLE) {
VaultHubStorage storage $ = _getVaultHubStorage();
if ($.vaultFactories[factory]) revert AlreadyExists(factory);
$.vaultFactories[factory] = true;
emit VaultFactoryAdded(factory);
}

function addImpl(address impl) public onlyRole(VAULT_MASTER_ROLE) {
if (vaultImpl[impl]) revert AlreadyExists(impl);
vaultImpl[impl] = true;
/// @notice added vault implementation address to allowed list
function addImpl(address impl) public onlyRole(VAULT_REGISTRY_ROLE) {
VaultHubStorage storage $ = _getVaultHubStorage();
if ($.vaultImpl[impl]) revert AlreadyExists(impl);
$.vaultImpl[impl] = true;
emit VaultImplAdded(impl);
}

/// @notice returns the number of vaults connected to the hub
function vaultsCount() public view returns (uint256) {
return sockets.length - 1;
return _getVaultHubStorage().sockets.length - 1;
}

function vault(uint256 _index) public view returns (IHubVault) {
return sockets[_index + 1].vault;
return _getVaultHubStorage().sockets[_index + 1].vault;
}

function vaultSocket(uint256 _index) external view returns (VaultSocket memory) {
return sockets[_index + 1];
return _getVaultHubStorage().sockets[_index + 1];
}

function vaultSocket(address _vault) external view returns (VaultSocket memory) {
return sockets[vaultIndex[IHubVault(_vault)]];
VaultHubStorage storage $ = _getVaultHubStorage();
return $.sockets[$.vaultIndex[IHubVault(_vault)]];
}

/// @notice connects a vault to the hub
Expand Down Expand Up @@ -120,13 +137,15 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
if (_treasuryFeeBP == 0) revert ZeroArgument("_treasuryFeeBP");
if (_treasuryFeeBP > BPS_BASE) revert TreasuryFeeTooHigh(address(_vault), _treasuryFeeBP, BPS_BASE);

VaultHubStorage storage $ = _getVaultHubStorage();

address factory = IBeaconProxy(address (_vault)).getBeacon();
if (!vaultFactories[factory]) revert FactoryNotAllowed(factory);
if (!$.vaultFactories[factory]) revert FactoryNotAllowed(factory);

address impl = IBeacon(factory).implementation();
if (!vaultImpl[impl]) revert ImplNotAllowed(impl);
if (!$.vaultImpl[impl]) revert ImplNotAllowed(impl);

if (vaultIndex[_vault] != 0) revert AlreadyConnected(address(_vault), vaultIndex[_vault]);
if ($.vaultIndex[_vault] != 0) revert AlreadyConnected(address(_vault), $.vaultIndex[_vault]);
if (vaultsCount() == MAX_VAULTS_COUNT) revert TooManyVaults();
if (_shareLimit > (stETH.getTotalShares() * MAX_VAULT_SIZE_BP) / BPS_BASE) {
revert ShareLimitTooHigh(address(_vault), _shareLimit, stETH.getTotalShares() / 10);
Expand All @@ -146,22 +165,23 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
uint16(_reserveRatioThreshold),
uint16(_treasuryFeeBP)
);
vaultIndex[_vault] = sockets.length;
sockets.push(vr);
$.vaultIndex[_vault] = $.sockets.length;
$.sockets.push(vr);

emit VaultConnected(address(_vault), _shareLimit, _reserveRatio, _treasuryFeeBP);
}

/// @notice disconnects a vault from the hub
/// @dev can be called by vaults only
function disconnectVault(address _vault) external {
IHubVault vault_ = IHubVault(_vault);
VaultHubStorage storage $ = _getVaultHubStorage();

uint256 index = vaultIndex[vault_];
IHubVault vault_ = IHubVault(_vault);
uint256 index = $.vaultIndex[vault_];
if (index == 0) revert NotConnectedToHub(_vault);
if (msg.sender != vault_.owner()) revert NotAuthorized("disconnect", msg.sender);

VaultSocket memory socket = sockets[index];
VaultSocket memory socket = $.sockets[index];
IHubVault vaultToDisconnect = socket.vault;

if (socket.sharesMinted > 0) {
Expand All @@ -171,12 +191,12 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {

vaultToDisconnect.report(vaultToDisconnect.valuation(), vaultToDisconnect.inOutDelta(), 0);

VaultSocket memory lastSocket = sockets[sockets.length - 1];
sockets[index] = lastSocket;
vaultIndex[lastSocket.vault] = index;
sockets.pop();
VaultSocket memory lastSocket = $.sockets[$.sockets.length - 1];
$.sockets[index] = lastSocket;
$.vaultIndex[lastSocket.vault] = index;
$.sockets.pop();

delete vaultIndex[vaultToDisconnect];
delete $.vaultIndex[vaultToDisconnect];

emit VaultDisconnected(address(vaultToDisconnect));
}
Expand All @@ -190,12 +210,14 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
if (_recipient == address(0)) revert ZeroArgument("_recipient");
if (_tokens == 0) revert ZeroArgument("_tokens");

VaultHubStorage storage $ = _getVaultHubStorage();

IHubVault vault_ = IHubVault(_vault);
uint256 index = vaultIndex[vault_];
uint256 index = $.vaultIndex[vault_];
if (index == 0) revert NotConnectedToHub(_vault);
if (msg.sender != vault_.owner()) revert NotAuthorized("mint", msg.sender);

VaultSocket memory socket = sockets[index];
VaultSocket memory socket = $.sockets[index];

uint256 sharesToMint = stETH.getSharesByPooledEth(_tokens);
uint256 vaultSharesAfterMint = socket.sharesMinted + sharesToMint;
Expand All @@ -207,7 +229,7 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
revert InsufficientValuationToMint(address(vault_), vault_.valuation());
}

sockets[index].sharesMinted = uint96(vaultSharesAfterMint);
$.sockets[index].sharesMinted = uint96(vaultSharesAfterMint);

stETH.mintExternalShares(_recipient, sharesToMint);

Expand All @@ -226,17 +248,19 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
function burnStethBackedByVault(address _vault, uint256 _tokens) public {
if (_tokens == 0) revert ZeroArgument("_tokens");

VaultHubStorage storage $ = _getVaultHubStorage();

IHubVault vault_ = IHubVault(_vault);
uint256 index = vaultIndex[vault_];
uint256 index = $.vaultIndex[vault_];
if (index == 0) revert NotConnectedToHub(_vault);
if (msg.sender != vault_.owner()) revert NotAuthorized("burn", msg.sender);

VaultSocket memory socket = sockets[index];
VaultSocket memory socket = $.sockets[index];

uint256 amountOfShares = stETH.getSharesByPooledEth(_tokens);
if (socket.sharesMinted < amountOfShares) revert InsufficientSharesToBurn(_vault, socket.sharesMinted);

sockets[index].sharesMinted = socket.sharesMinted - uint96(amountOfShares);
$.sockets[index].sharesMinted = socket.sharesMinted - uint96(amountOfShares);

stETH.burnExternalShares(amountOfShares);

Expand All @@ -254,9 +278,11 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
/// @param _vault vault address
/// @dev can be used permissionlessly if the vault's min reserve ratio is broken
function forceRebalance(IHubVault _vault) external {
uint256 index = vaultIndex[_vault];
VaultHubStorage storage $ = _getVaultHubStorage();

uint256 index = $.vaultIndex[_vault];
if (index == 0) revert NotConnectedToHub(msg.sender);
VaultSocket memory socket = sockets[index];
VaultSocket memory socket = $.sockets[index];

uint256 threshold = _maxMintableShares(_vault, socket.reserveRatioThreshold);
if (socket.sharesMinted <= threshold) {
Expand Down Expand Up @@ -289,14 +315,16 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
function rebalance() external payable {
if (msg.value == 0) revert ZeroArgument("msg.value");

uint256 index = vaultIndex[IHubVault(msg.sender)];
VaultHubStorage storage $ = _getVaultHubStorage();

uint256 index = $.vaultIndex[IHubVault(msg.sender)];
if (index == 0) revert NotConnectedToHub(msg.sender);
VaultSocket memory socket = sockets[index];
VaultSocket memory socket = $.sockets[index];

uint256 sharesToBurn = stETH.getSharesByPooledEth(msg.value);
if (socket.sharesMinted < sharesToBurn) revert InsufficientSharesToBurn(msg.sender, socket.sharesMinted);

sockets[index].sharesMinted = socket.sharesMinted - uint96(sharesToBurn);
$.sockets[index].sharesMinted = socket.sharesMinted - uint96(sharesToBurn);

// mint stETH (shares+ TPE+)
(bool success, ) = address(stETH).call{value: msg.value}("");
Expand Down Expand Up @@ -327,14 +355,16 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
// | \____( )___) )___
// \______(_______;;; __;;;

VaultHubStorage storage $ = _getVaultHubStorage();

uint256 length = vaultsCount();
// for each vault
treasuryFeeShares = new uint256[](length);

lockedEther = new uint256[](length);

for (uint256 i = 0; i < length; ++i) {
VaultSocket memory socket = sockets[i + 1];
VaultSocket memory socket = $.sockets[i + 1];

// if there is no fee in Lido, then no fee in vaults
// see LIP-12 for details
Expand Down Expand Up @@ -391,9 +421,11 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
uint256[] memory _locked,
uint256[] memory _treasureFeeShares
) internal {
VaultHubStorage storage $ = _getVaultHubStorage();

uint256 totalTreasuryShares;
for (uint256 i = 0; i < _valuations.length; ++i) {
VaultSocket memory socket = sockets[i + 1];
VaultSocket memory socket = $.sockets[i + 1];
if (_treasureFeeShares[i] > 0) {
socket.sharesMinted += uint96(_treasureFeeShares[i]);
totalTreasuryShares += _treasureFeeShares[i];
Expand All @@ -414,6 +446,12 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
return stETH.getSharesByPooledEth(maxStETHMinted);
}

function _getVaultHubStorage() private pure returns (VaultHubStorage storage $) {
assembly {
$.slot := VAULT_HUB_STORAGE_LOCATION
}
}

event VaultConnected(address vault, uint256 capShares, uint256 minReserveRatio, uint256 treasuryFeeBP);
event VaultDisconnected(address vault);
event MintedStETHOnVault(address sender, uint256 tokens);
Expand Down
4 changes: 3 additions & 1 deletion test/0.8.25/vaults/vaultFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ describe("VaultFactory.sol", () => {
vaultStaffRoom = await ethers.deployContract("VaultStaffRoom", [steth], { from: deployer });
vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, vaultStaffRoom], { from: deployer });

//add role to factory
//add VAULT_MASTER_ROLE role to allow admin to connect the Vaults to the vault Hub
await vaultHub.connect(admin).grantRole(await vaultHub.VAULT_MASTER_ROLE(), admin);
//add VAULT_REGISTRY_ROLE role to allow admin to add factory and vault implementation to the hub
await vaultHub.connect(admin).grantRole(await vaultHub.VAULT_REGISTRY_ROLE(), admin);

//the initialize() function cannot be called on a contract
await expect(implOld.initialize(stranger, "0x")).to.revertedWithCustomError(implOld, "NonProxyCallsForbidden");
Expand Down

0 comments on commit f9ca4a4

Please sign in to comment.