Skip to content

Commit

Permalink
Merge branch 'feat/vaults' into feat/vault-setup
Browse files Browse the repository at this point in the history
  • Loading branch information
loga4 committed Feb 21, 2025
2 parents c47f467 + bd81a22 commit 7b5395b
Show file tree
Hide file tree
Showing 58 changed files with 3,780 additions and 948 deletions.
8 changes: 2 additions & 6 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# CODEOWNERS: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners

# Any PR to `master` branch with changes to production contracts notifies the protocol team
/contracts/ @lidofinance/lido-eth-protocol

# Any PR to `master` branch with changes to GitHub workflows notifies the workflow review team
/.github/workflows/ @lidofinance/review-gh-workflows
* @lidofinance/lido-eth-protocol
.github @lidofinance/review-gh-workflows
65 changes: 36 additions & 29 deletions .github/workflows/tests-integration-mainnet.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
name: Integration Tests
#on: [push]
#
#jobs:
# test_hardhat_integration_fork:
# name: Hardhat / Mainnet
# runs-on: ubuntu-latest
# timeout-minutes: 120
#
# services:
# hardhat-node:
# image: ghcr.io/lidofinance/hardhat-node:2.22.18
# ports:
# - 8545:8545
# env:
# ETH_RPC_URL: "${{ secrets.ETH_RPC_URL }}"
#
# steps:
# - uses: actions/checkout@v4
#
# - name: Common setup
# uses: ./.github/workflows/setup
#
# - name: Set env
# run: cp .env.example .env
#
# - name: Run integration tests
# run: yarn test:integration:fork:mainnet
# env:
# LOG_LEVEL: debug

on: workflow_dispatch

# TODO: uncomment
#on:
# push:
# schedule:
# - cron: "0 10 */2 * *"

jobs:
test_hardhat_integration_fork:
name: Hardhat / Mainnet
runs-on: ubuntu-latest
timeout-minutes: 120

services:
hardhat-node:
image: ghcr.io/lidofinance/hardhat-node:2.22.18
ports:
- 8545:8545
env:
ETH_RPC_URL: "${{ secrets.ETH_RPC_URL }}"

steps:
- uses: actions/checkout@v4

- name: Common setup
uses: ./.github/workflows/setup

- name: Set env
run: cp .env.example .env

- name: Run integration tests
run: yarn test:integration:fork:mainnet
env:
LOG_LEVEL: debug
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 <info@lido.fi>
// 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 7b5395b

Please sign in to comment.