Skip to content

Commit

Permalink
MVP of triggerable exits
Browse files Browse the repository at this point in the history
  • Loading branch information
loga4 committed Dec 7, 2023
1 parent bfc9fb6 commit ba34b2f
Show file tree
Hide file tree
Showing 7 changed files with 8,412 additions and 7,358 deletions.
11 changes: 11 additions & 0 deletions contracts/0.8.9/WithdrawalVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,15 @@ contract WithdrawalVault is Versioned {

_token.transferFrom(address(this), TREASURY, _tokenId);
}

/**
* @dev simulate triggerable exits EIP-7002
*/
event TriggerExit(bytes validator_pubkey);

function validatorExit(bytes calldata validatorPubkey) external {
//role(VALIDATOR_EXIT_ROLE) - only VEBO can trigger exits

emit TriggerExit(validatorPubkey);
}
}
40 changes: 40 additions & 0 deletions contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { BaseOracle } from "./BaseOracle.sol";
interface IOracleReportSanityChecker {
function checkExitBusOracleReport(uint256 _exitRequestsCount) external view;
}
interface IWithdrawalVault {
function validatorExit(bytes calldata validatorPubkey) external;
}


contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
Expand Down Expand Up @@ -174,6 +177,10 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
bytes data;
}

/// @dev Storage slot: mapping(uint256 => bytes) reportHash
/// A mapping from the `refSlot` to the report hash.
bytes32 internal constant REPORT_HASH_POSITION = keccak256("lido.ValidatorsExitBusOracle.reportHash");

/// @notice The list format of the validator exit requests data. Used when all
/// requests fit into a single transaction.
///
Expand Down Expand Up @@ -218,10 +225,32 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
_checkContractVersion(contractVersion);
// it's a waste of gas to copy the whole calldata into mem but seems there's no way around
_checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data)));

//save reportHash for refSlot
_storageReportHash()[data.refSlot] = keccak256(abi.encode(data));

_startProcessing();
_handleConsensusReportData(data);
}

error EmptyRefSlotHash();
error InvalidReportData();

event ValidatorTriggereableExitRequest(
uint256 refSlot,
bytes32 hash,
uint256 from,
uint256 limit
);
function submitValidatorsExit(uint256 refSlot, ReportData calldata data, uint256 from, uint256 limit) external {
bytes32 refslotHash = _storageReportHash()[refSlot];
if (refslotHash == bytes32(0)) revert EmptyRefSlotHash();
if (refslotHash != keccak256(abi.encode(data))) revert InvalidReportData();

// IWithdrawalVault(LOCATOR.withdrawalVault()).validatorExit(pubkey);
emit ValidatorTriggereableExitRequest(refSlot, refslotHash, from, limit);
}

/// @notice Returns the total number of validator exit requests ever processed
/// across all received reports.
///
Expand Down Expand Up @@ -380,6 +409,8 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {

uint256 timestamp = _getTime();

IWithdrawalVault withdrawalVault = IWithdrawalVault(LOCATOR.withdrawalVault());

while (offset < offsetPastEnd) {
uint256 dataWithoutPubkey;
assembly {
Expand Down Expand Up @@ -428,6 +459,8 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
lastDataWithoutPubkey = dataWithoutPubkey;

emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp);

withdrawalVault.validatorExit(pubkey);
}

if (lastNodeOpKey != 0) {
Expand Down Expand Up @@ -460,4 +493,11 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
bytes32 position = DATA_PROCESSING_STATE_POSITION;
assembly { r.slot := position }
}

function _storageReportHash() internal pure returns (
mapping(uint256 => bytes32) storage r
) {
bytes32 position = REPORT_HASH_POSITION;
assembly { r.slot := position }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ contract ValidatorsExitBusTimeTravellable is ValidatorsExitBusOracle, ITimeProvi
function getDataProcessingState() external view returns (DataProcessingState memory) {
return _storageDataProcessingState().value;
}

function getRefSlotReportHash(uint256 refSlot) external view returns (bytes32) {
return _storageReportHash()[refSlot];
}
}
4 changes: 2 additions & 2 deletions hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ require('@nomiclabs/hardhat-truffle5')
require('@nomiclabs/hardhat-ganache')
require('@nomiclabs/hardhat-etherscan')
require('hardhat-gas-reporter')
require('solidity-coverage')
require('hardhat-contract-sizer')
// require('solidity-coverage')
// require('hardhat-contract-sizer')
require('hardhat-ignore-warnings')
require('./foundry/skip-sol-tests-compilation')

Expand Down
145 changes: 145 additions & 0 deletions test/0.8.9/oracle/triggerable-exit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
const { contract, ethers } = require('hardhat')
const { assert } = require('../../helpers/assert')
const { EvmSnapshot } = require('../../helpers/blockchain')

const {
CONSENSUS_VERSION,
DATA_FORMAT_LIST,
getValidatorsExitBusReportDataItems,
calcValidatorsExitBusReportDataHash,
encodeExitRequestsDataList,
deployExitBusOracle,
ZERO_HASH,
} = require('./validators-exit-bus-oracle-deploy.test')

const PUBKEYS = [
'0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
'0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
]

contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger]) => {
context('TriggerableExits', () => {
const LAST_PROCESSING_REF_SLOT = 1

let consensus
let oracle
let oracleVersion
let snapshot

async function deployAndSetup() {
snapshot = new EvmSnapshot(ethers.provider)
const deployed = await deployExitBusOracle(admin, {
lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT,
resumeAfterDeploy: true,
})

consensus = deployed.consensus
oracle = deployed.oracle

oracleVersion = +(await oracle.getContractVersion())

await consensus.addMember(member1, 1, { from: admin })
await consensus.addMember(member2, 2, { from: admin })
await consensus.addMember(member3, 2, { from: admin })
await snapshot.make()
}

async function rollback() {
await snapshot.rollback()
}

async function triggerConsensusOnHash(hash) {
const { refSlot } = await consensus.getCurrentFrame()
await consensus.submitReport(refSlot, hash, CONSENSUS_VERSION, { from: member1 })
await consensus.submitReport(refSlot, hash, CONSENSUS_VERSION, { from: member3 })
assert.equal((await consensus.getConsensusState()).consensusReport, hash)
}

const getDefaultReportFields = (overrides) => ({
consensusVersion: CONSENSUS_VERSION,
dataFormat: DATA_FORMAT_LIST,
// required override: refSlot
// required override: requestsCount
// required override: data
...overrides,
})

async function prepareReportAndSubmitHash(
exitRequests = [{ moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }],
options = {}
) {
const { reportFields: reportFieldsArg = {} } = options
const { refSlot } = await consensus.getCurrentFrame()

const reportFields = getDefaultReportFields({
refSlot: +refSlot,
requestsCount: exitRequests.length,
data: encodeExitRequestsDataList(exitRequests),
...reportFieldsArg,
})

const reportItems = getValidatorsExitBusReportDataItems(reportFields)
const reportHash = calcValidatorsExitBusReportDataHash(reportItems)

await triggerConsensusOnHash(reportHash)

return reportItems
}

before(deployAndSetup)

context('_handleConsensusReportData', () => {
beforeEach(async () => {
await consensus.advanceTimeToNextFrameStart()
})

afterEach(rollback)

it('validator exits', async () => {
const { refSlot } = await consensus.getCurrentFrame()

const hashBefore = await oracle.getRefSlotReportHash(refSlot)
assert.equals(hashBefore, ZERO_HASH)

const stateBefore = await oracle.getProcessingState()
assert.equals(stateBefore.dataHash, ZERO_HASH)

const requests = [
{ moduleId: 4, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] },
{ moduleId: 4, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[3] },
{ moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[4] },
]
const report = await prepareReportAndSubmitHash(requests)
const receipt = await oracle.submitReportData(report, oracleVersion, { from: member1 })

assert.emitsNumberOfEvents(receipt, 'TriggerExit', 1)

const reportHash = calcValidatorsExitBusReportDataHash(report)

const hashAfter = await oracle.getRefSlotReportHash(refSlot)
assert.equals(hashAfter, reportHash)

const stateAfter = await oracle.getProcessingState()
assert.equals(stateAfter.dataHash, reportHash)

await assert.reverts(oracle.submitValidatorsExit(0, report, 0, 10), `EmptyRefSlotHash()`)

const exitRequests = [{ moduleId: 2, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[4] }]
const wrongReportFields = getDefaultReportFields({
refSlot: +refSlot,
requestsCount: exitRequests.length,
data: encodeExitRequestsDataList(exitRequests),
})
const wrongReport = getValidatorsExitBusReportDataItems(wrongReportFields)

await assert.reverts(oracle.submitValidatorsExit(refSlot, wrongReport, 0, 10), `InvalidReportData()`)

// good refslot and report
await oracle.submitValidatorsExit(refSlot, report, 0, 10)
})
})
})
})
11 changes: 10 additions & 1 deletion test/0.8.9/oracle/validators-exit-bus-oracle-deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
const { calcValidatorsExitBusReportDataHash, getValidatorsExitBusReportDataItems } = require('../../helpers/reportData')

const ValidatorsExitBusOracle = artifacts.require('ValidatorsExitBusTimeTravellable')
const WithdrawalVault = artifacts.require('WithdrawalVault')

const DATA_FORMAT_LIST = 1

Expand Down Expand Up @@ -72,7 +73,13 @@ module.exports = {
encodeExitRequestsDataList,
deployExitBusOracle,
deployOracleReportSanityCheckerForExitBus,
deployWithdrawalVault,
}
async function deployWithdrawalVault(lidoAddress, treasuryAddress) {
const withdrawalVault = await WithdrawalVault.new(lidoAddress, treasuryAddress)
return withdrawalVault
}

async function deployOracleReportSanityCheckerForExitBus(lidoLocator, admin) {
const maxValidatorExitRequestsPerReport = 2000
const limitsList = [0, 0, 0, 0, maxValidatorExitRequestsPerReport, 0, 0, 0, 0]
Expand Down Expand Up @@ -106,9 +113,11 @@ async function deployExitBusOracle(
})

const oracleReportSanityChecker = await deployOracleReportSanityCheckerForExitBus(locator, admin)
const withdrawalVault = await deployWithdrawalVault(locator, admin)
await updateLocatorImplementation(locator, admin, {
validatorsExitBusOracle: oracle.address,
oracleReportSanityChecker: oracleReportSanityChecker.address,
withdrawalVault: withdrawalVault.address,
})

const initTx = await oracle.initialize(admin, consensus.address, CONSENSUS_VERSION, lastProcessingRefSlot, {
Expand Down Expand Up @@ -145,7 +154,7 @@ async function deployExitBusOracle(
await oracle.resume({ from: admin })
}

return { consensus, oracle, oracleReportSanityChecker, locator, initTx }
return { consensus, oracle, oracleReportSanityChecker, locator, initTx, withdrawalVault }
}

contract('ValidatorsExitBusOracle', ([admin, member1]) => {
Expand Down
Loading

0 comments on commit ba34b2f

Please sign in to comment.