Skip to content

Commit fc750ab

Browse files
authored
Merge pull request #717 from lidofinance/fix/lido-fee-distribution
Fix/lido fee distribution
2 parents 92a0dea + 5cc268e commit fc750ab

File tree

3 files changed

+73
-36
lines changed

3 files changed

+73
-36
lines changed

contracts/0.4.24/Lido.sol

+31-11
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ interface IStakingRouter {
112112
external
113113
view
114114
returns (uint256);
115+
116+
function TOTAL_BASIS_POINTS() external view returns (uint256);
115117
}
116118

117119
interface IWithdrawalQueue {
@@ -673,7 +675,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
673675
* Depends on the bunker state and protocol's pause state
674676
*/
675677
function canDeposit() public view returns (bool) {
676-
return !IWithdrawalQueue(getLidoLocator().withdrawalQueue()).isBunkerModeActive() && !isStopped();
678+
return !_withdrawalQueue().isBunkerModeActive() && !isStopped();
677679
}
678680

679681
/**
@@ -682,7 +684,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
682684
*/
683685
function getDepositableEther() public view returns (uint256) {
684686
uint256 bufferedEther = _getBufferedEther();
685-
uint256 withdrawalReserve = IWithdrawalQueue(getLidoLocator().withdrawalQueue()).unfinalizedStETH();
687+
uint256 withdrawalReserve = _withdrawalQueue().unfinalizedStETH();
686688
return bufferedEther > withdrawalReserve ? bufferedEther - withdrawalReserve : 0;
687689
}
688690

@@ -698,7 +700,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
698700
require(msg.sender == locator.depositSecurityModule(), "APP_AUTH_DSM_FAILED");
699701
require(canDeposit(), "CAN_NOT_DEPOSIT");
700702

701-
IStakingRouter stakingRouter = IStakingRouter(locator.stakingRouter());
703+
IStakingRouter stakingRouter = _stakingRouter();
702704
uint256 depositsCount = Math256.min(
703705
_maxDepositsCount,
704706
stakingRouter.getStakingModuleMaxDepositsCount(_stakingModuleId, getDepositableEther())
@@ -730,7 +732,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
730732
* @dev DEPRECATED: use StakingRouter.getWithdrawalCredentials() instead
731733
*/
732734
function getWithdrawalCredentials() external view returns (bytes32) {
733-
return IStakingRouter(getLidoLocator().stakingRouter()).getWithdrawalCredentials();
735+
return _stakingRouter().getWithdrawalCredentials();
734736
}
735737

736738
/**
@@ -746,7 +748,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
746748
* @dev DEPRECATED: use LidoLocator.treasury()
747749
*/
748750
function getTreasury() external view returns (address) {
749-
return getLidoLocator().treasury();
751+
return _treasury();
750752
}
751753

752754
/**
@@ -757,11 +759,11 @@ contract Lido is Versioned, StETHPermit, AragonApp {
757759
* inaccurate because the actual value is truncated here to 1e4 precision.
758760
*/
759761
function getFee() external view returns (uint16 totalFee) {
760-
totalFee = IStakingRouter(getLidoLocator().stakingRouter()).getTotalFeeE4Precision();
762+
totalFee = _stakingRouter().getTotalFeeE4Precision();
761763
}
762764

763765
/**
764-
* @notice Returns current fee distribution
766+
* @notice Returns current fee distribution, values relative to the total fee (getFee())
765767
* @dev DEPRECATED: Now fees information is stored in StakingRouter and
766768
* with higher precision. Use StakingRouter.getStakingFeeAggregateDistribution() instead.
767769
* @return treasuryFeeBasisPoints return treasury fee in TOTAL_BASIS_POINTS (10000 is 100% fee) precision
@@ -780,9 +782,15 @@ contract Lido is Versioned, StETHPermit, AragonApp {
780782
uint16 operatorsFeeBasisPoints
781783
)
782784
{
783-
insuranceFeeBasisPoints = 0; // explicitly set to zero
784-
(treasuryFeeBasisPoints, operatorsFeeBasisPoints) = IStakingRouter(getLidoLocator().stakingRouter())
785+
IStakingRouter stakingRouter = _stakingRouter();
786+
uint256 totalBasisPoints = stakingRouter.TOTAL_BASIS_POINTS();
787+
uint256 totalFee = stakingRouter.getTotalFeeE4Precision();
788+
(uint256 treasuryFeeBasisPointsAbs, uint256 operatorsFeeBasisPointsAbs) = stakingRouter
785789
.getStakingFeeAggregateDistributionE4Precision();
790+
791+
insuranceFeeBasisPoints = 0; // explicitly set to zero
792+
treasuryFeeBasisPoints = uint16((treasuryFeeBasisPointsAbs * totalBasisPoints) / totalFee);
793+
operatorsFeeBasisPoints = uint16((operatorsFeeBasisPointsAbs * totalBasisPoints) / totalFee);
786794
}
787795

788796
/*
@@ -970,7 +978,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
970978
StakingRewardsDistribution memory ret,
971979
IStakingRouter router
972980
) {
973-
router = IStakingRouter(getLidoLocator().stakingRouter());
981+
router = _stakingRouter();
974982

975983
(
976984
ret.recipients,
@@ -1075,7 +1083,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
10751083
}
10761084

10771085
function _transferTreasuryRewards(uint256 treasuryReward) internal {
1078-
address treasury = getLidoLocator().treasury();
1086+
address treasury = _treasury();
10791087
_transferShares(address(this), treasury, treasuryReward);
10801088
_emitTransferAfterMintingShares(treasury, treasuryReward);
10811089
}
@@ -1371,4 +1379,16 @@ contract Lido is Versioned, StETHPermit, AragonApp {
13711379
ret.postTokenRebaseReceiver
13721380
) = getLidoLocator().oracleReportComponentsForLido();
13731381
}
1382+
1383+
function _stakingRouter() internal view returns (IStakingRouter) {
1384+
return IStakingRouter(getLidoLocator().stakingRouter());
1385+
}
1386+
1387+
function _withdrawalQueue() internal view returns (IWithdrawalQueue) {
1388+
return IWithdrawalQueue(getLidoLocator().withdrawalQueue());
1389+
}
1390+
1391+
function _treasury() internal view returns (address) {
1392+
return getLidoLocator().treasury();
1393+
}
13741394
}

contracts/0.8.9/StakingRouter.sol

+6-6
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version
319319
/// @param _stakingModuleIds Ids of the staking modules to be updated.
320320
/// @param _exitedValidatorsCounts New counts of exited validators for the specified staking modules.
321321
///
322-
/// @return The total increase in the aggregate number of exited validators accross all updated modules.
322+
/// @return The total increase in the aggregate number of exited validators across all updated modules.
323323
///
324324
/// The total numbers are stored in the staking router and can differ from the totals obtained by calling
325325
/// `IStakingModule.getStakingModuleSummary()`. The overall process of updating validator counts is the following:
@@ -329,27 +329,27 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version
329329
/// distribute new stake and staking fees between the modules. There can only be single call of this function
330330
/// per oracle reporting frame.
331331
///
332-
/// 2. In the first part of the second data submittion phase, the oracle calls
332+
/// 2. In the first part of the second data submission phase, the oracle calls
333333
/// `StakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator` on the staking router which passes the
334334
/// counts by node operator to the staking module by calling `IStakingModule.updateStuckValidatorsCount`.
335335
/// This can be done multiple times for the same module, passing data for different subsets of node operators.
336336
///
337-
/// 3. In the second part of the second data submittion phase, the oracle calls
337+
/// 3. In the second part of the second data submission phase, the oracle calls
338338
/// `StakingRouter.reportStakingModuleExitedValidatorsCountByNodeOperator` on the staking router which passes
339339
/// the counts by node operator to the staking module by calling `IStakingModule.updateExitedValidatorsCount`.
340340
/// This can be done multiple times for the same module, passing data for different subsets of node
341341
/// operators.
342342
///
343-
/// 4. At the end of the second data submission phase, it's expected for the aggragate exited validators count
344-
/// accross all module's node operators (stored in the module) to match the total count for this module
343+
/// 4. At the end of the second data submission phase, it's expected for the aggregate exited validators count
344+
/// across all module's node operators (stored in the module) to match the total count for this module
345345
/// (stored in the staking router). However, it might happen that the second phase of data submission doesn't
346346
/// finish until the new oracle reporting frame is started, in which case staking router will emit a warning
347347
/// event `StakingModuleExitedValidatorsIncompleteReporting` when the first data submission phase is performed
348348
/// for a new reporting frame. This condition will result in the staking module having an incomplete data about
349349
/// the exited and maybe stuck validator counts during the whole reporting frame. Handling this condition is
350350
/// the responsibility of each staking module.
351351
///
352-
/// 5. When the second reporting phase is finshed, i.e. when the oracle submitted the complete data on the stuck
352+
/// 5. When the second reporting phase is finished, i.e. when the oracle submitted the complete data on the stuck
353353
/// and exited validator counts per node operator for the current reporting frame, the oracle calls
354354
/// `StakingRouter.onValidatorsCountsByNodeOperatorReportingFinished` which, in turn, calls
355355
/// `IStakingModule.onExitedAndStuckValidatorsCountsUpdated` on all modules.

test/scenario/lido_happy_path.test.js

+36-19
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ const { INITIAL_HOLDER } = require('../helpers/constants')
1414

1515
const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry')
1616
const CURATED_MODULE_ID = 1
17+
const TOTAL_BASIS_POINTS = 10000
18+
const CURATED_MODULE_MODULE_FEE = 500
19+
const CURATED_MODULE_TREASURY_FEE = 500
20+
21+
// Fee and its distribution are in basis points, 10000 corresponding to 100%
22+
// Total fee is 10%
23+
const totalFeePoints = 0.1 * TOTAL_BASIS_POINTS
1724

1825
contract('Lido: happy path', (addresses) => {
1926
const [
@@ -41,14 +48,14 @@ contract('Lido: happy path', (addresses) => {
4148
async () => {
4249
const deployed = await deployProtocol({
4350
stakingModulesFactory: async (protocol) => {
44-
const curatedModule = await setupNodeOperatorsRegistry(protocol)
51+
const curatedModule = await setupNodeOperatorsRegistry(protocol, true)
4552
return [
4653
{
4754
module: curatedModule,
4855
name: 'Curated',
4956
targetShares: 10000,
50-
moduleFee: 500,
51-
treasuryFee: 500,
57+
moduleFee: CURATED_MODULE_MODULE_FEE,
58+
treasuryFee: CURATED_MODULE_TREASURY_FEE,
5259
},
5360
]
5461
},
@@ -84,22 +91,6 @@ contract('Lido: happy path', (addresses) => {
8491
}
8592
)
8693

87-
// Fee and its distribution are in basis points, 10000 corresponding to 100%
88-
89-
// Total fee is 10%
90-
const totalFeePoints = 0.1 * 10000
91-
92-
it.skip('voting sets fee and its distribution', async () => {
93-
// Fee and distribution were set
94-
assert.equals(await pool.getFee({ from: nobody }), totalFeePoints, 'total fee')
95-
const distribution = await pool.getFeeDistribution({ from: nobody })
96-
console.log('distribution', distribution)
97-
const treasuryFeePoints = 0 // TODO
98-
const nodeOperatorsFeePoints = 0 // TODO
99-
assert.equals(distribution.treasuryFeeBasisPoints, treasuryFeePoints, 'treasury fee')
100-
assert.equals(distribution.operatorsFeeBasisPoints, nodeOperatorsFeePoints, 'node operators fee')
101-
})
102-
10394
it('voting sets withdrawal credentials', async () => {
10495
const wc = '0x'.padEnd(66, '1234')
10596
assert.equal(await pool.getWithdrawalCredentials({ from: nobody }), wc, 'withdrawal credentials')
@@ -546,4 +537,30 @@ contract('Lido: happy path', (addresses) => {
546537
nodeOperatorInfo = await nodeOperatorsRegistry.getNodeOperator(nodeOperator2.id, false)
547538
assert.equals(nodeOperatorInfo.totalSigningKeys, nodeOperatorInfo.usedSigningKeys)
548539
})
540+
541+
it('getFee and getFeeDistribution works as expected', async () => {
542+
// Need to have at least single deposited key, otherwise StakingRouter.getStakingRewardsDistribution
543+
// will return zero fees because no modules with non-zero total active validators
544+
// This is done in the changes in the tests above, but assuming there are no such changes
545+
// one could use the following:
546+
// await nodeOperatorsRegistry.increaseNodeOperatorDepositedSigningKeysCount(0, 1)
547+
548+
function getFeeRelativeToTotalFee(absoluteFee) {
549+
return (absoluteFee * TOTAL_BASIS_POINTS) / totalFeePoints
550+
}
551+
552+
assert.equals(await pool.getFee({ from: nobody }), totalFeePoints, 'total fee')
553+
const distribution = await pool.getFeeDistribution({ from: nobody })
554+
assert.equals(distribution.insuranceFeeBasisPoints, 0, 'insurance fee')
555+
assert.equals(
556+
distribution.treasuryFeeBasisPoints,
557+
getFeeRelativeToTotalFee(CURATED_MODULE_TREASURY_FEE),
558+
'treasury fee'
559+
)
560+
assert.equals(
561+
distribution.operatorsFeeBasisPoints,
562+
getFeeRelativeToTotalFee(CURATED_MODULE_MODULE_FEE),
563+
'node operators fee'
564+
)
565+
})
549566
})

0 commit comments

Comments
 (0)