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

[Implementation][EFIP-4] Process Fees in EETH #119

Merged
merged 9 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions proposals/EFIP-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# [EFIP-4] Processing staking to {Treasury, Node Operators} in eETH

**Author**: Vaibhav Valecha ([email protected])

**Date**: 2024-05-26

## Summary

The purpose of this EFIP is to optimize reward skimming when paying out the node operator. EtherFi’s Oracle mints eETH daily, distributing 90% of staking rewards from validators to all stakers. The remaining 10% is allocated to node operators and EtherFi’s treasury, which are distributed across Eigenpods and EtherFiNodes. However, the process of claiming ETH from over 20,000 Eigenpods and EtherFiNodes has been inefficient for both node operators and EtherFi.

To address this issue, we will now mint 100% of staking rewards from validators and distribute them as follows: 5% to node operators, 5% to the treasury, and the remaining 90% to stakers, as usual.

## First Deposit of Protocol Fees for EETH

The 10% staking rewards have been accumulating and unclaimed for over 50,000 validator for the past 10 months. EtherFi will deposit all the unclaimed protocol rewards, owned by EtherFi, node operators, and bNFTs to mint eETH. As a result, 2,700 eETH will be minted for the 2,700 ETH in accrued staking rewards across Eigenpods and EtherFi Nodes, which will be distributed to the node operators and the EtherFi treasury. Thereafter, EtherFi will be depositing their 10% staking rewards and bnft rewards for eeth on a daily basis.

## Motivation

Currently, every quarter we need to skim rewards from the withdrawal safes and eigenpods to pay the node operator. This results in high gas costs due to 40k contracts accumulating ETH that must be transferred to the treasury. Additionally, the need to perform skimming for node operator payouts reduces our flexibility on timing, preventing us from waiting for very low gas prices. Furthermore, tracking eth accrued to the treasury is not optimizable since 40k contract balance needs to be queried.

Paying our node operators and the treasury in eETH reduces gas costs by allowing us to skim withdrawal safes and eigenpods at our discretion, when gas prices are low. This also allows us to skim annually instead of quarterly. If we skim annually and achieve a 25% lower gas price, we can reduce our gas costs by 80%. Additionally, minting eETH to the treasury during the rebase provides easy, daily updates on the accrued ETH for the treasury and node operators.


## Proposal
To implement this proposal, we need to report the rewards accrued to the treasury and node operators and mint the corresponding amount of shares to the treasury during each rebase. Here are the steps to implement the proposal:

1. Add the rewards accrued for the treasury and node operators to the oracle report.
2. Add functionality in liquidity pool to mint shares for treasury
3. Add call to the EtherfiAdmin to mint the shares within executeTask
4. Call setStakingRewardsSplit to set the treasury's allocation to 0 and reallocate it to the TNFT.

## Audits
-[Nethermind Audit Report](https://file.notion.so/f/f/a2eb6f5b-6767-43e2-890d-4acb71d6176b/3dde22f1-3267-4997-98cc-e4417ec0f94b/EFIP-4_Processing_staking_to_Treasury_Node_Operators_in_eETH_-_HackMD.pdf?table=block&id=10db0952-7c43-800d-a80f-eed35f1269cb&spaceId=a2eb6f5b-6767-43e2-890d-4acb71d6176b&expirationTimestamp=1727827200000&signature=DvOO3EtEMg6ul4REzVIPWaIWjnFfP5boNDorqxCCMc0&downloadName=%5BEFIP-4%5D+Processing+staking+to+%7BTreasury%2C+Node+Operators%7D+in+eETH+-+HackMD.pdf)

-[Certora Draft Report](https://file.notion.so/f/f/a2eb6f5b-6767-43e2-890d-4acb71d6176b/52e43580-5be3-484e-8009-7c5d3ae500dd/EtherFi_draft_report.pdf?table=block&id=10db0952-7c43-80bd-9f43-e55ea5974de5&spaceId=a2eb6f5b-6767-43e2-890d-4acb71d6176b&expirationTimestamp=1727827200000&signature=rPkxoNUsLz2beenBpqbu987QBbw1zX5Y0wz6fq5CBUA&downloadName=EtherFi+draft+report.pdf)

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).

39 changes: 39 additions & 0 deletions proposals/references/efip-4-nethermind-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [EFIP-4] Processing staking to {Treasury, Node Operators} in eETH

**File(s)**: [EtherFiAdmin.sol](https://github.com/etherfi-protocol/smart-contracts/blob/9cae955d2e897cb5e8bf5266a0de22da73f1ac99/src/EtherFiAdmin.sol#L20), [EtherFiOracle.sol](https://github.com/etherfi-protocol/smart-contracts/blob/9cae955d2e897cb5e8bf5266a0de22da73f1ac99/src/EtherFiOracle.sol#L14), [LiquidityPool.sol](https://github.com/etherfi-protocol/smart-contracts/blob/9cae955d2e897cb5e8bf5266a0de22da73f1ac99/src/LiquidityPool.sol#L26),

### Summary

The purpose of this `EFIP` is to optimize reward skimming when paying out the node operator. Every quarter, protocol pays 5% of validator rewards to the corresponding node operator. This is a very expensive operation because the protocol needs to skim rewards from over 20,000 eigenpods and 20,000 etherfinode contracts, where all the execution and consensus rewards are directed.

To address this, during the rebase operation, instead of minting shares of 90% of the rewards (with 5% going to the treasury and 5% to the node operator), the protocol mints 100% of the rewards and distributes 10% to the treasury. This would require a feature in the liquidity pool that mints shares to the treasury or EOA based on the rewards accrued to the protocol and node operators which would be called by the etherfiadmin.

---

### Findings

### [Best practice] `LiquidityPool::payProtocolFees` function can be frontran

**File(s)**: [LiquidityPool.sol](https://github.com/etherfi-protocol/smart-contracts/blob/9cae955d2e897cb5e8bf5266a0de22da73f1ac99/src/LiquidityPool.sol#L467)

**Description**: The `LiquidityPool::payProtocolFees()` can be frontran and in order to manipulate the amount of Ether deposited into the pool. We checked the possibility of manipulating the `LiquidityPool's` balance in order to manipulate the number of shares minted towards the `treasury`. We didn't find a clear attack path, it seems that even if the transaction is frontran, the number of shares minted will be the one expected.

**Recommendation(s)**: To mitigate the potential of being frontran consider using a private RPC network like Flashbots when distributing the rewards through this new mechanism.

**Update from client**:

---

### [Info] Centralization risk

**File(s)**: [EtherFiAdmin.sol](https://github.com/etherfi-protocol/smart-contracts/blob/9cae955d2e897cb5e8bf5266a0de22da73f1ac99/src/EtherFiAdmin.sol#L154)

**Description**: The new `EtherFiAdmin::_handleProtocolFees()` function only checks that `protocolFees` passed inside the `report` are `>=0`. There is no upper limit on how many shares can be minted to the treasury address. The owner is able to mint an indefinite amount of shares to the treasury address through this mechanism.

This function is part of the `executeTasks()` function which is guarded by an `isAdmin` modifier so we assume that the `Admin` is trusted and will always behave honestly.

**Recommendation(s)**: Consider adding some limits to the amount of shares that can be minted to the `treasury` address.

**Update from client**:

---
2 changes: 1 addition & 1 deletion script/upgrades/EtherFiAdminUpgradeScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract EtherFiAdminUpgrade is Script {
EtherFiAdmin instance = EtherFiAdmin(proxyAddress);
EtherFiAdmin v2Implementation = new EtherFiAdmin();

instance.upgradeTo(address(v2Implementation));
//instance.upgradeTo(address(v2Implementation));

vm.stopBroadcast();
}
Expand Down
2 changes: 1 addition & 1 deletion script/upgrades/EtherFiOracleUpgradeScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract EtherFiOracleUpgrade is Script {
EtherFiOracle oracleInstance = EtherFiOracle(proxyAddress);
EtherFiOracle v2Implementation = new EtherFiOracle();

oracleInstance.upgradeTo(address(v2Implementation));
//oracleInstance.upgradeTo(address(v2Implementation));

vm.stopBroadcast();
}
Expand Down
4 changes: 2 additions & 2 deletions script/upgrades/LiquidityPoolUpgradeScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ contract LiquidityPoolUpgrade is Script {

LiquidityPool LiquidityPoolV2Implementation = new LiquidityPool();

require(LiquidityPoolInstance.numPendingDeposits() == 0, "numPendingDeposits should be 0");
//require(LiquidityPoolInstance.numPendingDeposits() == 0, "numPendingDeposits should be 0");

LiquidityPoolInstance.upgradeTo(address(LiquidityPoolV2Implementation));
//LiquidityPoolInstance.upgradeTo(address(LiquidityPoolV2Implementation));

// // Phase 2
// //Ensure these inputs are correct
Expand Down
14 changes: 11 additions & 3 deletions src/EtherFiAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import "./interfaces/ILiquidityPool.sol";
import "./interfaces/IMembershipManager.sol";
import "./interfaces/IWithdrawRequestNFT.sol";

import "forge-std/console.sol";

interface IEtherFiPausable {
function paused() external view returns (bool);
}
Expand Down Expand Up @@ -153,6 +151,7 @@ contract EtherFiAdmin is Initializable, OwnableUpgradeable, UUPSUpgradeable {
numValidatorsToSpinUp = _report.numValidatorsToSpinUp;

_handleAccruedRewards(_report);
_handleProtocolFees(_report);
_handleValidators(_report, _pubKey, _signature);
_handleWithdrawals(_report);
_handleTargetFundsAllocations(_report);
Expand All @@ -164,6 +163,15 @@ contract EtherFiAdmin is Initializable, OwnableUpgradeable, UUPSUpgradeable {
emit AdminOperationsExecuted(msg.sender, reportHash);
}

//protocol owns the eth that was distributed to NO and treasury in eigenpods and etherfinodes
function _handleProtocolFees(IEtherFiOracle.OracleReport calldata _report) internal {
require(_report.protocolFees >= 0, "EtherFiAdmin: protocol fees can't be negative");
if(_report.protocolFees == 0) {
return;
}
liquidityPool.payProtocolFees(uint128(_report.protocolFees));
}

function _handleAccruedRewards(IEtherFiOracle.OracleReport calldata _report) internal {
if (_report.accruedRewards == 0) {
return;
Expand Down Expand Up @@ -262,7 +270,7 @@ contract EtherFiAdmin is Initializable, OwnableUpgradeable, UUPSUpgradeable {


modifier isAdmin() {
require(admins[msg.sender] || msg.sender == owner(), "EtherFiAdmin: not an admio");
require(admins[msg.sender] || msg.sender == owner(), "EtherFiAdmin: not an admin");
_;
}

Expand Down
3 changes: 2 additions & 1 deletion src/EtherFiOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ contract EtherFiOracle is Initializable, OwnableUpgradeable, PausableUpgradeable
_report.refSlotTo,
_report.refBlockFrom,
_report.refBlockTo,
_report.accruedRewards
_report.accruedRewards,
_report.protocolFees
)
);

Expand Down
23 changes: 19 additions & 4 deletions src/LiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import "./interfaces/ILiquidityPool.sol";
import "./interfaces/IEtherFiAdmin.sol";
import "./interfaces/IAuctionManager.sol";
import "./interfaces/ILiquifier.sol";
import "forge-std/console.sol";

contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, ILiquidityPool {
//--------------------------------------------------------------------------------------
Expand All @@ -41,7 +40,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL
uint128 public totalValueOutOfLp;
uint128 public totalValueInLp;

address public DEPRECATED_admin;
address public treasury;

uint32 public numPendingDeposits; // number of validator deposits, which needs 'registerValidator'

Expand Down Expand Up @@ -80,13 +79,15 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL
event Deposit(address indexed sender, uint256 amount, SourceOfFunds source, address referral);
event Withdraw(address indexed sender, address recipient, uint256 amount, SourceOfFunds source);
event UpdatedWhitelist(address userAddress, bool value);
event UpdatedTreasury(address newTreasury);
event BnftHolderDeregistered(address user, uint256 index);
event BnftHolderRegistered(address user, uint256 index);
event UpdatedSchedulingPeriod(uint128 newPeriodInSeconds);
event ValidatorRegistered(uint256 indexed validatorId, bytes signature, bytes pubKey, bytes32 depositRoot);
event ValidatorApproved(uint256 indexed validatorId);
event ValidatorRegistrationCanceled(uint256 indexed validatorId);
event Rebase(uint256 totalEthLocked, uint256 totalEEthShares);
event ProtocolFeePaid(uint128 protocolFees);
event WhitelistStatusUpdated(bool value);

error IncorrectCaller();
Expand Down Expand Up @@ -152,9 +153,9 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL
return _deposit(msg.sender, msg.value, 0);
}

// Used by eETH staking flow through Liquifier contract; deVamp
// Used by eETH staking flow through Liquifier contract; deVamp or to pay protocol fees
function depositToRecipient(address _recipient, uint256 _amount, address _referral) public whenNotPaused returns (uint256) {
require(msg.sender == address(liquifier), "Incorrect Caller");
require(msg.sender == address(liquifier) || msg.sender == address(etherFiAdminContract), "Incorrect Caller");

emit Deposit(_recipient, _amount, SourceOfFunds.EETH, _referral);

Expand Down Expand Up @@ -461,6 +462,20 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL

emit Rebase(getTotalPooledEther(), eETH.totalShares());
}
/// @notice pay protocol fees including 5% to treaury, 5% to node operator and ethfund bnft holders
/// @param _protocolFees The amount of protocol fees to pay in ether
function payProtocolFees(uint128 _protocolFees) external {
if (msg.sender != address(etherFiAdminContract)) revert IncorrectCaller();
seongyun-ko marked this conversation as resolved.
Show resolved Hide resolved
emit ProtocolFeePaid(_protocolFees);
depositToRecipient(treasury, _protocolFees, address(0));
}

/// @notice Set the treasury address
/// @param _treasury The address to set as the treasury
function setTreasury(address _treasury) external onlyOwner {
treasury = _treasury;
emit UpdatedTreasury(_treasury);
}

/// @notice Whether or not nodes created via bNFT deposits should be restaked
function setRestakeBnftDeposits(bool _restake) external onlyAdmin {
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/IEtherFiOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface IEtherFiOracle {
uint32 refBlockFrom;
uint32 refBlockTo;
int128 accruedRewards;
int128 protocolFees;
uint256[] validatorsToApprove;
uint256[] liquidityPoolValidatorsToExit;
uint256[] exitedValidators;
Expand Down Expand Up @@ -62,4 +63,4 @@ interface IEtherFiOracle {

function pauseContract() external;
function unPauseContract() external;
}
}
1 change: 1 addition & 0 deletions src/interfaces/ILiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ interface ILiquidityPool {
function sendExitRequests(uint256[] calldata _validatorIds) external;

function rebase(int128 _accruedRewards) external;
function payProtocolFees(uint128 _protocolFees) external;
function addEthAmountLockedForWithdrawal(uint128 _amount) external;
function reduceEthAmountLockedForWithdrawal(uint128 _amount) external;

Expand Down
22 changes: 21 additions & 1 deletion test/EtherFiOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -657,4 +657,24 @@ contract EtherFiOracleTest is TestSetup {
consensusReached = etherFiOracleInstance.submitReport(report);
assertTrue(consensusReached); // succeeded
}
}

function test_execute_task_treasury_payout() public {
vm.startPrank(owner);
etherFiOracleInstance.addCommitteeMember(chad);
etherFiOracleInstance.setQuorumSize(2);
liquidityPoolInstance.setTreasury(address(owner));
vm.stopPrank();

_moveClock(1024 + 2 * slotsPerEpoch);

IEtherFiOracle.OracleReport memory report = _emptyOracleReport();
_initReportBlockStamp(report);

// Assume that the accruedRewards must be 1 ether, all the time

// Alice submited the correct report
vm.prank(alice);
report.accruedRewards = 90 ether;
report.protocolFees = 10 ether;
}
}
26 changes: 26 additions & 0 deletions test/EtherFiTimelock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,32 @@ contract TimelockTest is TestSetup {
// _execute_timelock(target, data, false);
// }
}

function test_efip4() public {
initializeRealisticFork(MAINNET_FORK);
{
address target = address(liquidityPoolInstance);
bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0xa8A8Be862BA6301E5949ABDE93b1D892C14FfB1F);
_execute_timelock(target, data, true, true, true, true);
}
{
address target = address(liquidityPoolInstance);
bytes memory data = abi.encodeWithSelector(LiquidityPool.setTreasury.selector, 0xf40bcc0845528873784F36e5C105E62a93ff7021);
_execute_timelock(target, data, true, true, true, true);
}

{
address target = address(etherFiOracleInstance);
bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0x99BE559FAdf311D2CEdeA6265F4d36dfa4377B70);
_execute_timelock(target, data, true, true, true, true);
}

{
address target = address(etherFiAdminInstance);
bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0x92c27bA54A62fcd41d3df9Fd2dC5C8dfacbd3C4C);
_execute_timelock(target, data, true, true, true, true);
}
}
}

// {"version":"1.0","chainId":"1
Loading
Loading