Skip to content

Commit

Permalink
feat: check invariant_vaultsDonAffectSharesRate
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeyWh1te committed Feb 4, 2025
1 parent 490652e commit 8b88a72
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 107 deletions.
92 changes: 5 additions & 87 deletions test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
// SPDX-License-Identifier: UNLICENSED
// for testing purposes only

pragma solidity 0.8.9;

import {StakingRouter} from "contracts/0.8.9/StakingRouter.sol";
pragma solidity 0.4.24;

contract StakingRouter__MockForLidoAccounting {
event Mock__MintedRewardsReported();
event Mock__MintedTotalShares(uint256 indexed _totalShares);

address[] private recipients__mocked;
uint256[] private stakingModuleIds__mocked;
Expand All @@ -33,21 +29,14 @@ contract StakingRouter__MockForLidoAccounting {
precisionPoints = precisionPoint__mocked;
}

function reportRewardsMinted(uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares) external {
function reportRewardsMinted(uint256[], uint256[]) external {
emit Mock__MintedRewardsReported();

uint256 totalShares = 0;
for (uint256 i = 0; i < _totalShares.length; i++) {
totalShares += _totalShares[i];
}

emit Mock__MintedTotalShares(totalShares);
}

function mock__getStakingRewardsDistribution(
address[] calldata _recipients,
uint256[] calldata _stakingModuleIds,
uint96[] calldata _stakingModuleFees,
address[] _recipients,
uint256[] _stakingModuleIds,
uint96[] _stakingModuleFees,
uint96 _totalFee,
uint256 _precisionPoints
) external {
Expand All @@ -57,75 +46,4 @@ contract StakingRouter__MockForLidoAccounting {
totalFee__mocked = _totalFee;
precisionPoint__mocked = _precisionPoints;
}

function getStakingModuleIds() public view returns (uint256[] memory) {
return stakingModuleIds__mocked;
}

function getRecipients() public view returns (address[] memory) {
return recipients__mocked;
}

function getStakingModule(uint256 _stakingModuleId) public view returns (StakingRouter.StakingModule memory) {
if (_stakingModuleId >= 4) {
revert("Staking module does not exist");
}

if (_stakingModuleId == 1) {
return
StakingRouter.StakingModule({
id: 1,
stakingModuleAddress: 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5,
stakingModuleFee: 500,
treasuryFee: 500,
stakeShareLimit: 10000,
status: 0,
name: "curated-onchain-v1",
lastDepositAt: 1732694279,
lastDepositBlock: 21277744,
exitedValidatorsCount: 88207,
priorityExitShareThreshold: 10000,
maxDepositsPerBlock: 150,
minDepositBlockDistance: 25
});
}

if (_stakingModuleId == 2) {
return
StakingRouter.StakingModule({
id: 2,
stakingModuleAddress: 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433,
stakingModuleFee: 800,
treasuryFee: 200,
stakeShareLimit: 400,
status: 0,
name: "SimpleDVT",
lastDepositAt: 1735217831,
lastDepositBlock: 21486781,
exitedValidatorsCount: 5,
priorityExitShareThreshold: 444,
maxDepositsPerBlock: 150,
minDepositBlockDistance: 25
});
}

if (_stakingModuleId == 3) {
return
StakingRouter.StakingModule({
id: 3,
stakingModuleAddress: 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F,
stakingModuleFee: 600,
treasuryFee: 400,
stakeShareLimit: 100,
status: 0,
name: "Community Staking",
lastDepositAt: 1735217387,
lastDepositBlock: 21486745,
exitedValidatorsCount: 104,
priorityExitShareThreshold: 125,
maxDepositsPerBlock: 30,
minDepositBlockDistance: 25
});
}
}
}
131 changes: 131 additions & 0 deletions test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: UNLICENSED
// for testing purposes only

pragma solidity 0.8.9;

import {StakingRouter} from "contracts/0.8.9/StakingRouter.sol";

contract StakingRouter__MockForLidoAccountingFuzzing {
event Mock__MintedRewardsReported();
event Mock__MintedTotalShares(uint256 indexed _totalShares);

address[] private recipients__mocked;
uint256[] private stakingModuleIds__mocked;
uint96[] private stakingModuleFees__mocked;
uint96 private totalFee__mocked;
uint256 private precisionPoint__mocked;

function getStakingRewardsDistribution()
public
view
returns (
address[] memory recipients,
uint256[] memory stakingModuleIds,
uint96[] memory stakingModuleFees,
uint96 totalFee,
uint256 precisionPoints
)
{
recipients = recipients__mocked;
stakingModuleIds = stakingModuleIds__mocked;
stakingModuleFees = stakingModuleFees__mocked;
totalFee = totalFee__mocked;
precisionPoints = precisionPoint__mocked;
}

function reportRewardsMinted(uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares) external {
emit Mock__MintedRewardsReported();

uint256 totalShares = 0;
for (uint256 i = 0; i < _totalShares.length; i++) {
totalShares += _totalShares[i];
}

emit Mock__MintedTotalShares(totalShares);
}

function mock__getStakingRewardsDistribution(
address[] calldata _recipients,
uint256[] calldata _stakingModuleIds,
uint96[] calldata _stakingModuleFees,
uint96 _totalFee,
uint256 _precisionPoints
) external {
recipients__mocked = _recipients;
stakingModuleIds__mocked = _stakingModuleIds;
stakingModuleFees__mocked = _stakingModuleFees;
totalFee__mocked = _totalFee;
precisionPoint__mocked = _precisionPoints;
}

function getStakingModuleIds() public view returns (uint256[] memory) {
return stakingModuleIds__mocked;
}

function getRecipients() public view returns (address[] memory) {
return recipients__mocked;
}

function getStakingModule(uint256 _stakingModuleId) public view returns (StakingRouter.StakingModule memory) {
if (_stakingModuleId >= 4) {
revert("Staking module does not exist");
}

if (_stakingModuleId == 1) {
return
StakingRouter.StakingModule({
id: 1,
stakingModuleAddress: 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5,
stakingModuleFee: 500,
treasuryFee: 500,
stakeShareLimit: 10000,
status: 0,
name: "curated-onchain-v1",
lastDepositAt: 1732694279,
lastDepositBlock: 21277744,
exitedValidatorsCount: 88207,
priorityExitShareThreshold: 10000,
maxDepositsPerBlock: 150,
minDepositBlockDistance: 25
});
}

if (_stakingModuleId == 2) {
return
StakingRouter.StakingModule({
id: 2,
stakingModuleAddress: 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433,
stakingModuleFee: 800,
treasuryFee: 200,
stakeShareLimit: 400,
status: 0,
name: "SimpleDVT",
lastDepositAt: 1735217831,
lastDepositBlock: 21486781,
exitedValidatorsCount: 5,
priorityExitShareThreshold: 444,
maxDepositsPerBlock: 150,
minDepositBlockDistance: 25
});
}

if (_stakingModuleId == 3) {
return
StakingRouter.StakingModule({
id: 3,
stakingModuleAddress: 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F,
stakingModuleFee: 600,
treasuryFee: 400,
stakeShareLimit: 100,
status: 0,
name: "Community Staking",
lastDepositAt: 1735217387,
lastDepositBlock: 21486745,
exitedValidatorsCount: 104,
priorityExitShareThreshold: 125,
maxDepositsPerBlock: 30,
minDepositBlockDistance: 25
});
}
}
}
60 changes: 48 additions & 12 deletions test/0.8.25/Accounting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
// for testing purposes only
pragma solidity ^0.8.0;

import "foundry/lib/forge-std/src/Vm.sol";
import {CommonBase} from "forge-std/Base.sol";
import {StdCheats} from "forge-std/StdCheats.sol";
import {StdUtils} from "forge-std/StdUtils.sol";
import {Vm} from "forge-std/Vm.sol";
import {console2} from "../../foundry/lib/forge-std/src/console2.sol";
import {console2} from "forge-std/console2.sol";

import {BaseProtocolTest} from "./Protocol__Deployment.t.sol";
import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol";
Expand All @@ -26,6 +25,10 @@ interface IAccounting {
interface ILido {
function getTotalShares() external view returns (uint256);

function getBufferedEther() external view returns (uint256);

function getExternalShares() external view returns (uint256);

function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256);

function resume() external;
Expand All @@ -48,7 +51,7 @@ interface ISecondOpinionOracleMock {

// 0.002792 * 10^18
// 0.0073 * 10^18
uint256 constant maxYiedPerOperatorWei = 2_792_000_000_000_000; // which % of slashing could be?
uint256 constant maxYieldPerOperatorWei = 2_792_000_000_000_000; // which % of slashing could be?
uint256 constant maxLossPerOperatorWei = 7_300_000_000_000_000;
uint256 constant stableBalanceWei = 32 * 1 ether;

Expand Down Expand Up @@ -120,7 +123,7 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils {

function handleOracleReport(FuzzValues memory fuzz) external {
uint256 _timeElapsed = 86_400;
uint256 _timestamp = 1_737_366_566 + _timeElapsed;
uint256 _timestamp = block.timestamp + _timeElapsed;

// cheatCode for
// if (_report.timestamp >= block.timestamp) revert IncorrectReportTimestamp(_report.timestamp, block.timestamp);
Expand All @@ -145,14 +148,14 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils {
);

uint256 minBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei - maxLossPerOperatorWei);
uint256 maxBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei + maxYiedPerOperatorWei);
uint256 maxBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei + maxYieldPerOperatorWei);
fuzz._clBalanceWei = bound(fuzz._clBalanceWei, minBalancePerValidatorWei, maxBalancePerValidatorWei);

// depositedValidators is always greater or equal to beaconValidators
// Todo: Upper extremum ?
uint256 depositedValidators = bound(
fuzz._preClValidators,
fuzz._clValidators,
fuzz._clValidators + 1,
fuzz._clValidators + limitList.appearedValidatorsPerDayLimit
);
ghost.depositedValidators = int256(depositedValidators);
Expand Down Expand Up @@ -278,10 +281,6 @@ contract AccountingTest is BaseProtocolTest {
targetSelector(FuzzSelector({addr: address(accountingHandler), selectors: selectors}));
}

// - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal
// - vault params do not affect protocol share rate
//

/**
* https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs
* forge-config: default.invariant.runs = 128
Expand Down Expand Up @@ -339,8 +338,7 @@ contract AccountingTest is BaseProtocolTest {
}

/**
* Lido.Transfer from (0x00, to treasure or burner. Other -> collect and check what is it)
*
* Lido.Transfer from (0x00, to treasure or burner. Other -> collect and check what is it)
* https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs
* forge-config: default.invariant.runs = 128
* forge-config: default.invariant.depth = 128
Expand All @@ -357,4 +355,42 @@ contract AccountingTest is BaseProtocolTest {
);
}
}

/**
* solvency - stETH <> ETH = 1:1 - internal and total share rates are equal
* vault params do not affect protocol share rate
*
* https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs
* forge-config: default.invariant.runs = 128
* forge-config: default.invariant.depth = 128
* forge-config: default.invariant.fail-on-revert = true
*/
function invariant_vaultsDonAffectSharesRate() public view {
ILido lido = ILido(lidoLocator.lido());

uint256 totalShares = lido.getTotalShares();
uint256 totalEth = lido.getBufferedEther();
uint256 totalShareRate = totalEth / totalShares;

console2.log("totalShares", totalShares);
console2.log("totalEth", totalEth);
console2.log("totalShareRate", totalShareRate);

(uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat();
// clValidators can never be less than deposited ones.
uint256 transientEther = (depositedValidators - clValidators) * 32 ether;
console2.log("transientEther", transientEther);

uint256 internalEther = totalEth + clBalance + transientEther;
console2.log("internalEther", internalEther);
uint256 internalShares = totalShares - lido.getExternalShares();
console2.log("internalShares", internalShares);
console2.log("getExternalShares", lido.getExternalShares());

uint256 internalShareRate = internalEther / internalShares;

console2.log("internalShareRate", internalShareRate);

assertEq(totalShareRate, internalShareRate);
}
}
Loading

0 comments on commit 8b88a72

Please sign in to comment.