Skip to content

Commit

Permalink
feat: invariant_handleOracleReport
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeyWh1te committed Jan 29, 2025
1 parent ea08979 commit f7870a8
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 24 deletions.
77 changes: 77 additions & 0 deletions test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

pragma solidity 0.8.9;

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

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

address[] private recipients__mocked;
uint256[] private stakingModuleIds__mocked;
Expand Down Expand Up @@ -32,6 +35,13 @@ contract StakingRouter__MockForLidoAccounting {

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(
Expand All @@ -47,4 +57,71 @@ contract StakingRouter__MockForLidoAccounting {
totalFee__mocked = _totalFee;
precisionPoint__mocked = _precisionPoints;
}

function getStakingModuleIds() public view returns (uint256[] memory) {
return stakingModuleIds__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
});
}
}
}
68 changes: 56 additions & 12 deletions test/0.8.25/Accounting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface IAccounting {
interface ILido {
function getTotalShares() external view returns (uint256);

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

function resume() external;

function getBeaconStat()
Expand All @@ -36,6 +38,11 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils {

uint256 public ghost_clValidators;
uint256 public ghost_depositedValidators;
uint256 public ghost_sharesMintAsFees;
uint256 public ghost_transferShares;
uint256 public ghost_totalRewards;
uint256 public ghost_principalClBalance;
uint256 public ghost_unifiedClBalance;

address private accountingOracle;
address private lidoExecutionLayerRewardVault;
Expand All @@ -51,7 +58,6 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils {
accounting = IAccounting(_accounting);
lido = ILido(_lido);
accountingOracle = _accountingOracle;
ghost_clValidators = 0;
limitList = _limitList;
lidoExecutionLayerRewardVault = _lidoExecutionLayerRewardVault;
}
Expand Down Expand Up @@ -93,7 +99,6 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils {
_preClBalance = bound(_preClBalance, _preClValidators * stableBalance, _preClValidators * stableBalance);
ghost_clValidators = _preClValidators;

// _clValidators = bound(_clValidators, _preClValidators, _preClValidators + 900);
_clValidators = bound(
_clValidators,
_preClValidators,
Expand All @@ -114,32 +119,56 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils {
vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(_preClValidators));
vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(_preClBalance * 1 ether));

vm.deal(lidoExecutionLayerRewardVault, 1000 * 1 ether);
// IncorrectELRewardsVaultBalance(0)
// sharesToMintAsFees
// research correlation with elRewardsVaultBalance
vm.deal(lidoExecutionLayerRewardVault, 300 ether);

ReportValues memory currentReport = ReportValues({
timestamp: _timestamp,
timeElapsed: _timeElapsed,
clValidators: _clValidators,
clBalance: _clBalance * 1 ether,
withdrawalVaultBalance: 0,
elRewardsVaultBalance: 1_000 * 1 ether,
elRewardsVaultBalance: 200 ether,
sharesRequestedToBurn: 0,
withdrawalFinalizationBatches: new uint256[](0),
vaultValues: new uint256[](0),
netCashFlows: new int256[](0)
});

ghost_principalClBalance =
_preClBalance *
1 ether +
(currentReport.clValidators - _preClValidators) *
stableBalance *
1 ether;
ghost_unifiedClBalance = currentReport.clBalance + currentReport.withdrawalVaultBalance; // ?

ghost_totalRewards = ghost_unifiedClBalance - ghost_principalClBalance + currentReport.elRewardsVaultBalance;

vm.prank(accountingOracle);

vm.recordLogs();
accounting.handleOracleReport(currentReport);
Vm.Log[] memory entries = vm.getRecordedLogs();

bytes32 totalSharesSignature = keccak256("Mock__MintedTotalShares(uint256)");
bytes32 transferSharesSignature = keccak256("TransferShares(address,address,uint256)");
for (uint256 i = 0; i < entries.length; i++) {
if (entries[i].topics[0] == totalSharesSignature) {
ghost_sharesMintAsFees = abi.decode(abi.encodePacked(entries[i].topics[1]), (uint256));
}

if (entries[i].topics[0] == transferSharesSignature) {
ghost_transferShares = abi.decode(entries[i].data, (uint256));
}
}
}
}

contract AccountingTest is BaseProtocolTest {
AccountingHandler private accountingHandler;

uint256 private protocolStartBalance = 15_000 ether;
uint256 private protocolStartBalance = 1 ether;

address private rootAccount = address(0x123);
address private userAccount = address(0x321);
Expand Down Expand Up @@ -172,26 +201,41 @@ contract AccountingTest is BaseProtocolTest {
// - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler)
// CLb + ELr <= 10%

// - user tokens must not be used except burner contract (from Zero / to Zero)
// - user tokens must not be used except burner as source (from Zero / to Zero). From burner to zerop
// - from zero to Treasure, burner
//
// - 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 = 1
* forge-config: default.invariant.depth = 1
* forge-config: default.invariant.runs = 256
* forge-config: default.invariant.depth = 256
* forge-config: default.invariant.fail-on-revert = true
*
* Should not be able to decrease validator number
*/
function invariant_clValidators() public view {
function invariant_handleOracleReport() public view {
ILido lido = ILido(lidoLocator.lido());
(uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat();

assertGe(clValidators, accountingHandler.ghost_clValidators());
assertEq(depositedValidators, accountingHandler.ghost_depositedValidators());

// console2.log(depositedValidators);
if (accountingHandler.ghost_unifiedClBalance() > accountingHandler.ghost_principalClBalance()) {
uint256 treasuryFeesETH = lido.getPooledEthByShares(accountingHandler.ghost_sharesMintAsFees()) / 1 ether;
uint256 reportRewardsMintedETH = lido.getPooledEthByShares(accountingHandler.ghost_transferShares()) /
1 ether;
uint256 totalFees = treasuryFeesETH + reportRewardsMintedETH;
uint256 totalRewards = accountingHandler.ghost_totalRewards() / 1 ether;

if (totalRewards != 0) {
uint256 percents = (totalFees * 100) / totalRewards;

assertTrue(percents <= 10);
assertTrue(percents > 0);
}
}
}
}
45 changes: 33 additions & 12 deletions test/0.8.25/Protocol__Deployment.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@

pragma solidity ^0.8.0;

import "../0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol";
import "forge-std/Test.sol";
import {CommonBase} from "forge-std/Base.sol";
import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";
import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.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 "forge-std/console2.sol";

import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";
import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol";

interface IAccounting {
function initialize(address _admin) external;
}
Expand Down Expand Up @@ -132,8 +133,33 @@ contract BaseProtocolTest is Test {
acl.createPermission(userAccount, lidoProxyAddress, keccak256("RESUME_ROLE"), rootAccount);
acl.createPermission(userAccount, lidoProxyAddress, keccak256("PAUSE_ROLE"), rootAccount);

StakingRouter__MockForLidoAccounting stakingRouter = new StakingRouter__MockForLidoAccounting();

uint256[] memory stakingModuleIds = new uint256[](3);
stakingModuleIds[0] = 1;
stakingModuleIds[1] = 2;
stakingModuleIds[2] = 3;

uint96[] memory stakingModuleFees = new uint96[](3);
stakingModuleFees[0] = 4876942047684326532;
stakingModuleFees[1] = 145875332634464962;
stakingModuleFees[2] = 38263043302959438;

address[] memory recipients = new address[](3);
recipients[0] = 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5;
recipients[1] = 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433;
recipients[2] = 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F;

stakingRouter.mock__getStakingRewardsDistribution(
recipients,
stakingModuleIds,
stakingModuleFees,
9999999999999999996,
100000000000000000000
);

/// @dev deploy lido locator with dummy default values
lidoLocator = _deployLidoLocator(lidoProxyAddress);
lidoLocator = _deployLidoLocator(lidoProxyAddress, address(stakingRouter));

// Add accounting contract with handler to the protocol
address accountingImpl = deployCode(
Expand Down Expand Up @@ -168,13 +194,10 @@ contract BaseProtocolTest is Test {
// Add burner contract to the protocol
deployCodeTo(
"LidoExecutionLayerRewardsVault.sol:LidoExecutionLayerRewardsVault",
abi.encode(rootAccount, lidoProxyAddress, lidoTreasury),
abi.encode(lidoProxyAddress, lidoTreasury),
lidoLocator.elRewardsVault()
);

// Add staking router contract to the protocol
deployCodeTo("StakingRouter.sol:StakingRouter", abi.encode(depositContract), lidoLocator.stakingRouter());

// Add oracle report sanity checker contract to the protocol
deployCodeTo(
"OracleReportSanityChecker.sol:OracleReportSanityChecker",
Expand Down Expand Up @@ -207,8 +230,6 @@ contract BaseProtocolTest is Test {

IAccounting(lidoLocator.accounting()).initialize(rootAccount);

// contracts/0.8.9/LidoExecutionLayerRewardsVault.sol

/// @dev deploy eip712steth
address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress));

Expand Down Expand Up @@ -254,7 +275,7 @@ contract BaseProtocolTest is Test {
}

/// @dev deploy lido locator with dummy default values
function _deployLidoLocator(address lido) internal returns (ILidoLocator) {
function _deployLidoLocator(address lido, address stakingRouterAddress) internal returns (ILidoLocator) {
LidoLocatorConfig memory config = LidoLocatorConfig({
accountingOracle: makeAddr("dummy-locator:accountingOracle"),
depositSecurityModule: makeAddr("dummy-locator:depositSecurityModule"),
Expand All @@ -264,7 +285,7 @@ contract BaseProtocolTest is Test {
oracleReportSanityChecker: makeAddr("dummy-locator:oracleReportSanityChecker"),
postTokenRebaseReceiver: address(0),
burner: makeAddr("dummy-locator:burner"),
stakingRouter: makeAddr("dummy-locator:stakingRouter"),
stakingRouter: stakingRouterAddress,
treasury: makeAddr("dummy-locator:treasury"),
validatorsExitBusOracle: makeAddr("dummy-locator:validatorsExitBusOracle"),
withdrawalQueue: makeAddr("dummy-locator:withdrawalQueue"),
Expand Down

0 comments on commit f7870a8

Please sign in to comment.