Skip to content

Commit

Permalink
chore: merge feat/vaults to feat/rebalance-gap. Resolve conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
ev-d committed Mar 3, 2025
2 parents 12f18f9 + cac27de commit 6b97090
Show file tree
Hide file tree
Showing 93 changed files with 6,525 additions and 1,611 deletions.
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ LOCAL_STAKING_VAULT_BEACON_ADDRESS=

# RPC URL for a separate, non Hardhat Network node (Anvil, Infura, Alchemy, etc.)
MAINNET_RPC_URL=http://localhost:8545

# RPC URL for Hardhat Network forking, required for running tests on mainnet fork with tracing (Infura, Alchemy, etc.)
# https://hardhat.org/hardhat-network/docs/guides/forking-other-networks#forking-other-networks
FORK_RPC_URL=https://eth.drpc.org

# https://docs.lido.fi/deployed-contracts
MAINNET_LOCATOR_ADDRESS=0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb
MAINNET_AGENT_ADDRESS=0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c
Expand Down Expand Up @@ -54,10 +59,6 @@ MAINNET_STAKING_VAULT_BEACON_ADDRESS=
HOLESKY_RPC_URL=
SEPOLIA_RPC_URL=

# RPC URL for Hardhat Network forking, required for running tests on mainnet fork with tracing (Infura, Alchemy, etc.)
# https://hardhat.org/hardhat-network/docs/guides/forking-other-networks#forking-other-networks
HARDHAT_FORKING_URL=https://eth.drpc.org

# Scratch deployment via hardhat variables
DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
GENESIS_TIME=1639659600
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests-integration-mainnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

services:
hardhat-node:
image: ghcr.io/lidofinance/hardhat-node:2.22.18
image: ghcr.io/lidofinance/hardhat-node:2.22.19
ports:
- 8545:8545
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests-integration-scratch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:

services:
hardhat-node:
image: ghcr.io/lidofinance/hardhat-node:2.22.18-scratch
image: ghcr.io/lidofinance/hardhat-node:2.22.19-scratch
ports:
- 8555:8545

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ lib/abi/*.json
accounts.json
deployed-local.json
deployed-hardhat.json
deployed-local-devnet.json

# MacOS
.DS_Store
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
yarn lint-staged
yarn typecheck
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ integration tests follows the `*.integration.ts` postfix, for example, `myScenar
Foundry's Solidity tests are specifically used for fuzzing library contracts or functions that perform complex
calculations or byte manipulation. These Solidity tests are located under `/tests` and organized into appropriate
subdirectories. The naming conventions follow
Foundry's [documentation](https://book.getfoundry.sh/tutorials/best-practices#general-test-guidance):
Foundry's [documentation](https://book.getfoundry.sh/guides/best-practices#general-test-guidance):
- For tests, use the `.t.sol` postfix (e.g., `MyContract.t.sol`).
- For scripts, use the `.s.sol` postfix (e.g., `MyScript.s.sol`).
Expand Down Expand Up @@ -327,7 +327,7 @@ This is the most common method for running integration tests. It uses an instanc
mainnet environment, allowing you to run integration tests with trace logging.
> [!NOTE]
> Ensure that `HARDHAT_FORKING_URL` is set to Ethereum Mainnet RPC and `MAINNET_*` environment variables are set in the
> Ensure that `FORK_RPC_URL` is set to Ethereum Mainnet RPC and `MAINNET_*` environment variables are set in the
> `.env` file (refer to `.env.example` for guidance). Otherwise, the tests will run against the Scratch deployment.
```bash
Expand Down
13 changes: 9 additions & 4 deletions contracts/0.4.24/Lido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -614,12 +614,14 @@ contract Lido is Versioned, StETHPermit, AragonApp {
* @notice Mint shares backed by external ether sources
* @param _recipient Address to receive the minted shares
* @param _amountOfShares Amount of shares to mint
* @dev Can be called only by accounting (authentication in mintShares method).
* @dev Can be called only by VaultHub
* NB: Reverts if the the external balance limit is exceeded.
*/
function mintExternalShares(address _recipient, uint256 _amountOfShares) external {
require(_recipient != address(0), "MINT_RECEIVER_ZERO_ADDRESS");
require(_amountOfShares != 0, "MINT_ZERO_AMOUNT_OF_SHARES");
_auth(getLidoLocator().vaultHub());
_whenNotStopped();

uint256 newExternalShares = EXTERNAL_SHARES_POSITION.getStorageUint256().add(_amountOfShares);
uint256 maxMintableExternalShares = _getMaxMintableExternalShares();
Expand All @@ -628,7 +630,10 @@ contract Lido is Versioned, StETHPermit, AragonApp {

EXTERNAL_SHARES_POSITION.setStorageUint256(newExternalShares);

mintShares(_recipient, _amountOfShares);
_mintShares(_recipient, _amountOfShares);
// emit event after minting shares because we are always having the net new ether under the hood
// for vaults we have new locked ether and for fees we have a part of rewards
_emitTransferAfterMintingShares(_recipient, _amountOfShares);

emit ExternalSharesMinted(_recipient, _amountOfShares, getPooledEthByShares(_amountOfShares));
}
Expand All @@ -639,7 +644,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
*/
function burnExternalShares(uint256 _amountOfShares) external {
require(_amountOfShares != 0, "BURN_ZERO_AMOUNT_OF_SHARES");
_auth(getLidoLocator().accounting());
_auth(getLidoLocator().vaultHub());
_whenNotStopped();

uint256 externalShares = EXTERNAL_SHARES_POSITION.getStorageUint256();
Expand All @@ -663,7 +668,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
*/
function rebalanceExternalEtherToInternal() external payable {
require(msg.value != 0, "ZERO_VALUE");
_auth(getLidoLocator().accounting());
_auth(getLidoLocator().vaultHub());
_whenNotStopped();

uint256 shares = getSharesByPooledEth(msg.value);
Expand Down
30 changes: 15 additions & 15 deletions contracts/0.8.25/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,15 @@ import {ReportValues} from "contracts/common/interfaces/ReportValues.sol";
/// @notice contract is responsible for handling accounting oracle reports
/// calculating all the state changes that is required to apply the report
/// and distributing calculated values to relevant parts of the protocol
/// @dev accounting is inherited from VaultHub contract to reduce gas costs and
/// simplify the auth flows, but they are mostly independent
contract Accounting is VaultHub {
contract Accounting {
struct Contracts {
address accountingOracleAddress;
IOracleReportSanityChecker oracleReportSanityChecker;
IBurner burner;
IWithdrawalQueue withdrawalQueue;
IPostTokenRebaseReceiver postTokenRebaseReceiver;
IStakingRouter stakingRouter;
VaultHub vaultHub;
}

struct PreReportState {
Expand Down Expand Up @@ -83,6 +82,8 @@ contract Accounting is VaultHub {
uint256 precisionPoints;
}

error NotAuthorized(string operation, address addr);

/// @notice deposit size in wei (for pre-maxEB accounting)
uint256 private constant DEPOSIT_SIZE = 32 ether;

Expand All @@ -91,20 +92,16 @@ contract Accounting is VaultHub {
/// @notice Lido contract
ILido public immutable LIDO;

/// @param _lidoLocator Lido Locator contract
/// @param _lido Lido contract
constructor(
ILidoLocator _lidoLocator,
ILido _lido
) VaultHub(_lido) {
) {
LIDO_LOCATOR = _lidoLocator;
LIDO = _lido;
}

function initialize(address _admin) external initializer {
if (_admin == address(0)) revert ZeroArgument("_admin");

__VaultHub_init(_admin);
}

/// @notice calculates all the state changes that is required to apply the report
/// @param _report report values
/// @param _withdrawalShareRate maximum share rate used for withdrawal finalization
Expand Down Expand Up @@ -226,7 +223,7 @@ contract Accounting is VaultHub {
// Calculate the amount of ether locked in the vaults to back external balance of stETH
// and the amount of shares to mint as fees to the treasury for each vaults
(update.vaultsLockedEther, update.vaultsTreasuryFeeShares, update.totalVaultsTreasuryFeeShares) =
_calculateVaultsRebase(
_contracts.vaultHub.calculateVaultsRebase(
update.postTotalShares,
update.postTotalPooledEther,
_pre.totalShares,
Expand Down Expand Up @@ -335,15 +332,16 @@ contract Accounting is VaultHub {
_update.etherToFinalizeWQ
);

_updateVaults(
// TODO: Remove this once decide on vaults reporting
_contracts.vaultHub.updateVaults(
_report.vaultValues,
_report.inOutDeltas,
_update.vaultsLockedEther,
_update.vaultsTreasuryFeeShares
);

if (_update.totalVaultsTreasuryFeeShares > 0) {
STETH.mintExternalShares(LIDO_LOCATOR.treasury(), _update.totalVaultsTreasuryFeeShares);
_contracts.vaultHub.mintVaultsTreasuryFeeShares(LIDO_LOCATOR.treasury(), _update.totalVaultsTreasuryFeeShares);
}

_notifyRebaseObserver(_contracts.postTokenRebaseReceiver, _report, _pre, _update);
Expand Down Expand Up @@ -463,7 +461,8 @@ contract Accounting is VaultHub {
address burner,
address withdrawalQueue,
address postTokenRebaseReceiver,
address stakingRouter
address stakingRouter,
address vaultHub
) = LIDO_LOCATOR.oracleReportComponents();

return
Expand All @@ -473,7 +472,8 @@ contract Accounting is VaultHub {
IBurner(burner),
IWithdrawalQueue(withdrawalQueue),
IPostTokenRebaseReceiver(postTokenRebaseReceiver),
IStakingRouter(stakingRouter)
IStakingRouter(stakingRouter),
VaultHub(vaultHub)
);
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/0.8.25/interfaces/IStakingRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ interface IStakingRouter {
uint256 precisionPoints
);

function reportRewardsMinted(uint256[] memory _stakingModuleIds, uint256[] memory _totalShares) external;
function reportRewardsMinted(uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares) external;
}
174 changes: 174 additions & 0 deletions contracts/0.8.25/utils/AccessControlConfirmable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/AccessControlEnumerable.sol";

/**
* @title AccessControlConfirmable
* @author Lido
* @notice An extension of AccessControlEnumerable that allows exectuing functions by mutual confirmation.
* @dev This contract extends AccessControlEnumerable and adds a confirmation mechanism in the form of a modifier.
*/
abstract contract AccessControlConfirmable is AccessControlEnumerable {
/**
* @notice Tracks confirmations
* - callData: msg.data of the call (selector + arguments)
* - role: role that confirmed the action
* - expiryTimestamp: timestamp of the confirmation.
*/
mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations;

/**
* @notice Minimal confirmation expiry in seconds.
*/
uint256 public constant MIN_CONFIRM_EXPIRY = 1 days;

/**
* @notice Maximal confirmation expiry in seconds.
*/
uint256 public constant MAX_CONFIRM_EXPIRY = 30 days;

/**
* @notice Confirmation expiry in seconds; after this period, the confirmation expires and no longer counts.
* @dev We cannot set this to 0 because this means that all confirmations have to be in the same block,
* which can never be guaranteed. And, more importantly, if the `_setConfirmExpiry` is restricted by
* the `onlyConfirmed` modifier, the confirmation expiry will be tricky to change.
* This is why this variable is private, set to a default value of 1 day and cannot be set to 0.
*/
uint256 private confirmExpiry = MIN_CONFIRM_EXPIRY;

/**
* @notice Returns the confirmation expiry.
* @return The confirmation expiry in seconds.
*/
function getConfirmExpiry() public view returns (uint256) {
return confirmExpiry;
}

/**
* @dev Restricts execution of the function unless confirmed by all specified roles.
* Confirmation, in this context, is a call to the same function with the same arguments.
*
* The confirmation process works as follows:
* 1. When a role member calls the function:
* - Their confirmation is counted immediately
* - If not enough confirmations exist, their confirmation is recorded
* - If they're not a member of any of the specified roles, the call reverts
*
* 2. Confirmation counting:
* - Counts the current caller's confirmations if they're a member of any of the specified roles
* - Counts existing confirmations that are not expired, i.e. expiry is not exceeded
*
* 3. Execution:
* - If all members of the specified roles have confirmed, executes the function
* - On successful execution, clears all confirmations for this call
* - If not enough confirmations, stores the current confirmations
* - Thus, if the caller has all the roles, the function is executed immediately
*
* 4. Gas Optimization:
* - Confirmations are stored in a deferred manner using a memory array
* - Confirmation storage writes only occur if the function cannot be executed immediately
* - This prevents unnecessary storage writes when all confirmations are present,
* because the confirmations are cleared anyway after the function is executed,
* - i.e. this optimization is beneficial for the deciding caller and
* saves 1 storage write for each role the deciding caller has
*
* @param _roles Array of role identifiers that must confirm the call in order to execute it
*
* @notice Confirmations past their expiry are not counted and must be recast
* @notice Only members of the specified roles can submit confirmations
* @notice The order of confirmations does not matter
*
*/
modifier onlyConfirmed(bytes32[] memory _roles) {
if (_roles.length == 0) revert ZeroConfirmingRoles();

uint256 numberOfRoles = _roles.length;
uint256 numberOfConfirms = 0;
bool[] memory deferredConfirms = new bool[](numberOfRoles);
bool isRoleMember = false;
uint256 expiryTimestamp = block.timestamp + confirmExpiry;

for (uint256 i = 0; i < numberOfRoles; ++i) {
bytes32 role = _roles[i];

if (super.hasRole(role, msg.sender)) {
isRoleMember = true;
numberOfConfirms++;
deferredConfirms[i] = true;

emit RoleMemberConfirmed(msg.sender, role, expiryTimestamp, msg.data);
} else if (confirmations[msg.data][role] >= block.timestamp) {
numberOfConfirms++;
}
}

if (!isRoleMember) revert SenderNotMember();

if (numberOfConfirms == numberOfRoles) {
for (uint256 i = 0; i < numberOfRoles; ++i) {
bytes32 role = _roles[i];
delete confirmations[msg.data][role];
}
_;
} else {
for (uint256 i = 0; i < numberOfRoles; ++i) {
if (deferredConfirms[i]) {
bytes32 role = _roles[i];
confirmations[msg.data][role] = expiryTimestamp;
}
}
}
}

/**
* @dev Sets the confirmation expiry.
* Confirmation expiry is a period during which the confirmation is counted. Once expired,
* the confirmation no longer counts and must be recasted for the confirmation to go through.
* @dev Does not retroactively apply to existing confirmations.
* @param _newConfirmExpiry The new confirmation expiry in seconds.
*/
function _setConfirmExpiry(uint256 _newConfirmExpiry) internal {
if (_newConfirmExpiry < MIN_CONFIRM_EXPIRY || _newConfirmExpiry > MAX_CONFIRM_EXPIRY)
revert ConfirmExpiryOutOfBounds();

uint256 oldConfirmExpiry = confirmExpiry;
confirmExpiry = _newConfirmExpiry;

emit ConfirmExpirySet(msg.sender, oldConfirmExpiry, _newConfirmExpiry);
}

/**
* @dev Emitted when the confirmation expiry is set.
* @param oldConfirmExpiry The old confirmation expiry.
* @param newConfirmExpiry The new confirmation expiry.
*/
event ConfirmExpirySet(address indexed sender, uint256 oldConfirmExpiry, uint256 newConfirmExpiry);

/**
* @dev Emitted when a role member confirms.
* @param member The address of the confirming member.
* @param role The role of the confirming member.
* @param expiryTimestamp The timestamp of the confirmation.
* @param data The msg.data of the confirmation (selector + arguments).
*/
event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 expiryTimestamp, bytes data);

/**
* @dev Thrown when attempting to set confirmation expiry out of bounds.
*/
error ConfirmExpiryOutOfBounds();

/**
* @dev Thrown when a caller without a required role attempts to confirm.
*/
error SenderNotMember();

/**
* @dev Thrown when the roles array is empty.
*/
error ZeroConfirmingRoles();
}
Loading

0 comments on commit 6b97090

Please sign in to comment.