Skip to content

Conversation

@rkolpakov
Copy link

Enhanced accounting integration tests.

Context

The accounting integration tests validate Lido's rebase mechanism and fee distribution logic. These tests ensure that share rate calculations, fee distribution between staking modules and treasury, and share minting/burning operations work correctly after each oracle report.

Problem

Fee distribution validation (TODO: check math, why it's not equal?)**

Previously:

The commented-out code attempted to verify:

// Calculate sum of shares sent to all staking modules
let stakingModulesSharesAsFees = 0n;
for (let i = 1; i <= stakingModulesCount; i++) {
  stakingModulesSharesAsFees += transferSharesEvents[i].args.sharesValue;
}

// Compare with treasury shares
const treasurySharesAsFees = transferSharesEvents[lastIndex].args.sharesValue;

// Incorrect: This assumes treasury and modules should get equal shares
expect(stakingModulesSharesAsFees).to.approximately(
  treasurySharesAsFees,
  100,
  "Shares minted to DAO and staking modules mismatch"
);

Why this was not fully correct:

  • Assumed sum(module shares) ≈ treasury shares, which is not a protocol requirement
  • Treasury receives totalFee - sum(moduleFees), which includes:
    • Configured treasury fees from all modules
    • Redirected fees from Stopped modules
    • Integer division rounding errors
  • These values are only coincidentally similar when moduleFee ≈ treasuryFee for all modules

Now:

Check 1: All shares accounted

const mintedSharesSum = transferSharesEvents
  .filter(({ args }) => args.from === ZeroAddress)
  .reduce((acc, { args }) => acc + args.sharesValue, 0n);

expect(tokenRebasedEvent.args.sharesMintedAsFees).to.equal(
  mintedSharesSum,
  "Sum of all minted shares must equal sharesMintedAsFees"
);

Check 2: Treasury received exact remainder (Accounting.sol:325)

const totalModuleShares = moduleMintEvents.reduce((sum, { args }) => sum + args.sharesValue, 0n);
const treasuryShares = treasuryMintEvent.args.sharesValue;

// Invariant: treasury = total - modules
expect(treasuryShares).to.equal(
  tokenRebasedEvent.args.sharesMintedAsFees - totalModuleShares,
  "Treasury must receive exactly the remainder after module distribution"
);

Check 3: Each module received proportional share (Accounting.sol:319)

for (let i = 0; i < recipients.length; i++) {
  const expectedModuleShares = (totalSharesMinted * stakingModuleFees[i]) / totalFee;
  const actualModuleShares = actualTransfer.args.sharesValue;

  // Allow 1 wei difference due to integer division rounding
  expect(actualModuleShares).to.be.closeTo(
    expectedModuleShares,
    1n,
    "Module shares must match expected proportion"
  );
}

Test setup and actual calculations

Staking modules configuration (from scripts/scratch/steps/0140-plug-staking-modules.ts):

Module stakingModuleFee treasuryFee Active Validators
NOR 500 BP (5%) 500 BP (5%) ~100 (96.15%)
SDVT 800 BP (8%) 200 BP (2%) ~4 (3.85%)

Fee distribution calculation:

Step 1: Calculate totalFee (weighted by validators)
NOR contribution: 96.15% × (5% + 5%) = 9.615%
SDVT contribution: 3.85% × (8% + 2%) = 0.385%
────────────────────────────────────────────────
totalFee = 10.0%

Step 2: Calculate module fees (only stakingModuleFee)
NOR module fee: 96.15% × 5% = 4.808%
SDVT module fee: 3.85% × 8% = 0.308%
──────────────────────────────────────
sum(moduleFees) = 5.116%

Step 3: Calculate treasury fee (remainder)
treasuryFee = totalFee - sum(moduleFees)
= 10.0% - 5.116%
= 4.884%

Verification (sum of configured treasury fees):
NOR treasury: 96.15% × 5% = 4.808%
SDVT treasury: 3.85% × 2% = 0.077%
─────────────────────────────────────
Expected = 4.885% ≈ 4.884% ✓

Solution

Replaced incorrect fee distribution check with proper validation

Instead of the incorrect assumption that treasury and module fees should be equal, added two helper functions that validate the actual protocol invariants:

validateTreasuryReceivedRemainder() - validates Accounting.sol:325:
treasurySharesToMint = totalSharesToMintAsFees - sum(moduleSharesToMint)

validateModuleFeeProportions() - validates Accounting.sol:319:
moduleFeeShares[i] = (totalSharesToMintAsFees × moduleFee[i]) / totalFee

These checks guarantee:

  • All minted shares are accounted for (existing check: sum(all minted shares) == sharesMintedAsFees)
  • Treasury receives exactly the remainder after module distribution
  • Each module receives shares proportional to their fee weight (within ±1 wei rounding tolerance)
  • No shares are lost in distribution

@rkolpakov rkolpakov requested a review from tamtamchik November 26, 2025 09:25
@rkolpakov rkolpakov requested a review from a team as a code owner November 26, 2025 09:25
@rkolpakov rkolpakov added the vaults Lido stVaults related changes label Nov 26, 2025
@github-actions
Copy link

badge

Hardhat Unit Tests Coverage Summary

Filename                                                                Stmts    Miss  Cover    Missing
--------------------------------------------------------------------  -------  ------  -------  -----------------------------------------------------------------------------------------------------------
contracts/0.4.24/Lido.sol                                                 281      11  96.09%   825-844, 940-952
contracts/0.4.24/StETH.sol                                                 80       0  100.00%
contracts/0.4.24/StETHPermit.sol                                           15       0  100.00%
contracts/0.4.24/lib/Packed64x4.sol                                         5       0  100.00%
contracts/0.4.24/lib/SigningKeys.sol                                       36       0  100.00%
contracts/0.4.24/lib/StakeLimitUtils.sol                                   41       0  100.00%
contracts/0.4.24/nos/NodeOperatorsRegistry.sol                            435       0  100.00%
contracts/0.4.24/utils/Pausable.sol                                         9       0  100.00%
contracts/0.4.24/utils/UnstructuredStorageExt.sol                          14       0  100.00%
contracts/0.4.24/utils/Versioned.sol                                        5       0  100.00%
contracts/0.6.12/WstETH.sol                                                17       0  100.00%
contracts/0.8.25/ValidatorExitDelayVerifier.sol                            75       0  100.00%
contracts/0.8.25/utils/AccessControlConfirmable.sol                         2       0  100.00%
contracts/0.8.25/utils/Confirmable2Addresses.sol                            5       0  100.00%
contracts/0.8.25/utils/Confirmations.sol                                   37       0  100.00%
contracts/0.8.25/utils/PausableUntilWithRoles.sol                           3       0  100.00%
contracts/0.8.25/utils/V3TemporaryAdmin.sol                                43      43  0.00%    90-208
contracts/0.8.25/vaults/LazyOracle.sol                                    134      18  86.57%   203-209, 248, 276-279, 436, 449, 467, 515, 556-558, 650, 658
contracts/0.8.25/vaults/OperatorGrid.sol                                  196       1  99.49%   203
contracts/0.8.25/vaults/PinnedBeaconProxy.sol                               6       0  100.00%
contracts/0.8.25/vaults/StakingVault.sol                                  111      14  87.39%   307-341
contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol                 48       3  93.75%   183, 187, 199
contracts/0.8.25/vaults/VaultFactory.sol                                   34       0  100.00%
contracts/0.8.25/vaults/VaultHub.sol                                      425      76  82.12%   257-266, 281-287, 342-366, 383, 552-553, 595-688, 997-999, 1087-1091, 1147, 1202-1209, 1495-1496, 1511-1521
contracts/0.8.25/vaults/dashboard/Dashboard.sol                           137       8  94.16%   183-201, 327, 636-649
contracts/0.8.25/vaults/dashboard/NodeOperatorFee.sol                      70       0  100.00%
contracts/0.8.25/vaults/dashboard/Permissions.sol                          47       2  95.74%   321-330
contracts/0.8.25/vaults/interfaces/IPinnedBeaconProxy.sol                   0       0  100.00%
contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol                 0       0  100.00%
contracts/0.8.25/vaults/interfaces/IStakingVault.sol                        0       0  100.00%
contracts/0.8.25/vaults/interfaces/IVaultFactory.sol                        0       0  100.00%
contracts/0.8.25/vaults/lib/PinnedBeaconUtils.sol                           5       0  100.00%
contracts/0.8.25/vaults/lib/RecoverTokens.sol                               5       0  100.00%
contracts/0.8.25/vaults/lib/RefSlotCache.sol                               36       0  100.00%
contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol           16       1  93.75%   214
contracts/0.8.25/vaults/predeposit_guarantee/MeIfNobodyElse.sol             3       0  100.00%
contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol      213      12  94.37%   482-502, 531, 670, 677, 699
contracts/0.8.9/Accounting.sol                                             95       2  97.89%   349-350
contracts/0.8.9/BeaconChainDepositor.sol                                   21       2  90.48%   48, 51
contracts/0.8.9/Burner.sol                                                 92       0  100.00%
contracts/0.8.9/DepositSecurityModule.sol                                 128       0  100.00%
contracts/0.8.9/EIP712StETH.sol                                            16       0  100.00%
contracts/0.8.9/LidoExecutionLayerRewardsVault.sol                         16       0  100.00%
contracts/0.8.9/LidoLocator.sol                                            26       0  100.00%
contracts/0.8.9/OracleDaemonConfig.sol                                     28       0  100.00%
contracts/0.8.9/StakingRouter.sol                                         305       0  100.00%
contracts/0.8.9/TriggerableWithdrawalsGateway.sol                          54       1  98.15%   271
contracts/0.8.9/WithdrawalQueue.sol                                        88       0  100.00%
contracts/0.8.9/WithdrawalQueueBase.sol                                   146       0  100.00%
contracts/0.8.9/WithdrawalQueueERC721.sol                                  89       0  100.00%
contracts/0.8.9/WithdrawalVault.sol                                        32       0  100.00%
contracts/0.8.9/WithdrawalVaultEIP7002.sol                                 21       0  100.00%
contracts/0.8.9/lib/ExitLimitUtils.sol                                     35       0  100.00%
contracts/0.8.9/lib/Math.sol                                                4       0  100.00%
contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol                         22       0  100.00%
contracts/0.8.9/lib/UnstructuredRefStorage.sol                              2       0  100.00%
contracts/0.8.9/oracle/AccountingOracle.sol                               174       0  100.00%
contracts/0.8.9/oracle/BaseOracle.sol                                      89       1  98.88%   401
contracts/0.8.9/oracle/HashConsensus.sol                                  263       1  99.62%   1005
contracts/0.8.9/oracle/ValidatorsExitBus.sol                              138      10  92.75%   458-471, 541
contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol                         52       1  98.08%   217
contracts/0.8.9/proxy/OssifiableProxy.sol                                  17       0  100.00%
contracts/0.8.9/proxy/WithdrawalsManagerProxy.sol                          60       0  100.00%
contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol               232      12  94.83%   307-309, 600-605, 800-835, 956
contracts/0.8.9/utils/DummyEmptyContract.sol                                0       0  100.00%
contracts/0.8.9/utils/PausableUntil.sol                                    31       0  100.00%
contracts/0.8.9/utils/Versioned.sol                                        11       0  100.00%
contracts/0.8.9/utils/access/AccessControl.sol                             23       0  100.00%
contracts/0.8.9/utils/access/AccessControlEnumerable.sol                    9       0  100.00%
contracts/common/utils/PausableUntil.sol                                   29       0  100.00%
TOTAL                                                                    4937     219  95.56%

Diff against master

Filename                                                                Stmts    Miss  Cover
--------------------------------------------------------------------  -------  ------  --------
contracts/0.4.24/Lido.sol                                                 +69     +11  -3.91%
contracts/0.4.24/StETH.sol                                                 +8       0  +100.00%
contracts/0.4.24/lib/StakeLimitUtils.sol                                   +4       0  +100.00%
contracts/0.4.24/utils/UnstructuredStorageExt.sol                         +14       0  +100.00%
contracts/0.8.25/utils/AccessControlConfirmable.sol                        +2       0  +100.00%
contracts/0.8.25/utils/Confirmable2Addresses.sol                           +5       0  +100.00%
contracts/0.8.25/utils/Confirmations.sol                                  +37       0  +100.00%
contracts/0.8.25/utils/PausableUntilWithRoles.sol                          +3       0  +100.00%
contracts/0.8.25/utils/V3TemporaryAdmin.sol                               +43     +43  +100.00%
contracts/0.8.25/vaults/LazyOracle.sol                                   +134     +18  +86.57%
contracts/0.8.25/vaults/OperatorGrid.sol                                 +196      +1  +99.49%
contracts/0.8.25/vaults/PinnedBeaconProxy.sol                              +6       0  +100.00%
contracts/0.8.25/vaults/StakingVault.sol                                 +111     +14  +87.39%
contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol                +48      +3  +93.75%
contracts/0.8.25/vaults/VaultFactory.sol                                  +34       0  +100.00%
contracts/0.8.25/vaults/VaultHub.sol                                     +425     +76  +82.12%
contracts/0.8.25/vaults/dashboard/Dashboard.sol                          +137      +8  +94.16%
contracts/0.8.25/vaults/dashboard/NodeOperatorFee.sol                     +70       0  +100.00%
contracts/0.8.25/vaults/dashboard/Permissions.sol                         +47      +2  +95.74%
contracts/0.8.25/vaults/interfaces/IPinnedBeaconProxy.sol                   0       0  +100.00%
contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol                 0       0  +100.00%
contracts/0.8.25/vaults/interfaces/IStakingVault.sol                        0       0  +100.00%
contracts/0.8.25/vaults/interfaces/IVaultFactory.sol                        0       0  +100.00%
contracts/0.8.25/vaults/lib/PinnedBeaconUtils.sol                          +5       0  +100.00%
contracts/0.8.25/vaults/lib/RecoverTokens.sol                              +5       0  +100.00%
contracts/0.8.25/vaults/lib/RefSlotCache.sol                              +36       0  +100.00%
contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol          +16      +1  +93.75%
contracts/0.8.25/vaults/predeposit_guarantee/MeIfNobodyElse.sol            +3       0  +100.00%
contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol     +213     +12  +94.37%
contracts/0.8.9/Accounting.sol                                            +95      +2  +97.89%
contracts/0.8.9/Burner.sol                                                +21       0  +100.00%
contracts/0.8.9/LidoLocator.sol                                            +6       0  +100.00%
contracts/0.8.9/oracle/AccountingOracle.sol                               -19       0  +100.00%
contracts/0.8.9/oracle/ValidatorsExitBus.sol                                0      +9  -6.53%
contracts/0.8.9/proxy/WithdrawalsManagerProxy.sol                         +60       0  +100.00%
contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol                 0     +12  -5.17%
contracts/common/utils/PausableUntil.sol                                  +29       0  +100.00%
TOTAL                                                                   +1863    +212  -1.76%

Results for commit: 7f3c3a6

Minimum allowed coverage is 80%

♻️ This comment has been updated with latest results

@folkyatina folkyatina added the tests When it comes to testing the code label Dec 1, 2025
Base automatically changed from feat/vaults to develop December 2, 2025 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests When it comes to testing the code vaults Lido stVaults related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants