Skip to content

Add support for general requests from execution to consensus layer #2571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contracts/contracts/beacon/BeaconConsolidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ library BeaconConsolidation {
internal
returns (uint256 fee_)
{
require(source.length == 48, "Invalid source byte length");
require(target.length == 48, "Invalid target byte length");

fee_ = fee();

// Call the Consolidation Request contract with the public keys of the source and target
Expand Down
37 changes: 37 additions & 0 deletions contracts/contracts/beacon/PartialWithdrawal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

library PartialWithdrawal {
/// @notice The address where the withdrawal request is sent to
/// See https://eips.ethereum.org/EIPS/eip-7002
address internal constant WITHDRAWAL_REQUEST_ADDRESS =
0x00000961Ef480Eb55e80D19ad83579A64c007002;

function request(bytes calldata validatorPubKey, uint64 amount)
internal
returns (uint256 fee_)
{
require(validatorPubKey.length == 48, "Invalid validator byte length");
fee_ = fee();

// Call the Withdrawal Request contract with the validator public key
// and amount to be withdrawn packed together

// This is a general purpose EL to CL request:
// https://eips.ethereum.org/EIPS/eip-7685
(bool success, ) = WITHDRAWAL_REQUEST_ADDRESS.call{ value: fee_ }(
abi.encodePacked(validatorPubKey, amount)
);

require(success, "Withdrawal request failed");
}

function fee() internal view returns (uint256) {
// Get fee from the withdrawal request contract
(bool success, bytes memory result) = WITHDRAWAL_REQUEST_ADDRESS
.staticcall("");

require(success && result.length > 0, "Failed to get fee");
return abi.decode(result, (uint256));
}
}
17 changes: 17 additions & 0 deletions contracts/contracts/mocks/MockPartialWithdrawal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { PartialWithdrawal } from "../beacon/PartialWithdrawal.sol";

contract MockPartialWithdrawal {
function fee() external view returns (uint256) {
return PartialWithdrawal.fee();
}

function request(bytes calldata validatorPubKey, uint64 amount)
external
returns (uint256 fee_)
{
return PartialWithdrawal.request(validatorPubKey, amount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { GeneralPurposeToConsensusLayerRequest } from "./GeneralPurposeToConsensusLayerRequest.sol";

contract ExecutionLayerConsolidation is GeneralPurposeToConsensusLayerRequest {
event ConsolidationRequestIssued(bytes sourceKey, bytes targetKey);
bytes public lastSource;
bytes public lastTarget;

function handleRequest(bytes calldata data) internal override {
// parameters should consist of twice the 48 bytes for 2 public keys
require(data.length == 96, "Invalid Consolidation data");
lastSource = data[:48];
lastTarget = data[48:];

emit ConsolidationRequestIssued(lastSource, lastTarget);
}
}
19 changes: 19 additions & 0 deletions contracts/contracts/mocks/beacon/ExecutionLayerWithdrawal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { GeneralPurposeToConsensusLayerRequest } from "./GeneralPurposeToConsensusLayerRequest.sol";

contract ExecutionLayerWithdrawal is GeneralPurposeToConsensusLayerRequest {
event WithdrawalRequestIssued(bytes publicKey, uint64 amount);

bytes public lastPublicKey;
uint64 public lastAmount;

function handleRequest(bytes calldata data) internal override {
// parameters should consist of 48 bytes for public key and 8 bytes for uint64 amount
require(data.length == 56, "Invalid Withdrawal data");
lastPublicKey = data[:48];
lastAmount = uint64(bytes8(data[48:]));
emit WithdrawalRequestIssued(lastPublicKey, lastAmount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

abstract contract GeneralPurposeToConsensusLayerRequest {
// solhint-disable no-complex-fallback
fallback() external payable {
// fee requested
if (msg.data.length == 0) {
uint256 fee = _fee();
// solhint-disable-next-line no-inline-assembly
assembly {
// Return a uint256 value
mstore(0, fee)
return(0, 32) // Return 32 bytes from memory
}
}

// else handle request
handleRequest(msg.data);
}

/***************************************
Abstract
****************************************/

function _fee() internal virtual returns (uint256) {
return 1;
}

function handleRequest(bytes calldata data) internal virtual;
}
113 changes: 111 additions & 2 deletions contracts/test/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
fundAccounts,
fundAccountsForOETHUnitTests,
} = require("../utils/funding");
const { deployWithConfirmation } = require("../utils/deploy");

const { replaceContractAt } = require("../utils/hardhat");
const {
Expand Down Expand Up @@ -2524,18 +2525,30 @@ async function beaconChainFixture() {
const { deploy } = deployments;
const { governorAddr } = await getNamedAccounts();

await deploy("MockBeaconRoots", {
const { beaconConsolidationReplaced, beaconWithdrawalReplaced } =
await enableExecutionLayerGeneralPurposeRequests();

await deploy("MockBeaconConsolidation", {
from: governorAddr,
});

await deploy("MockBeaconConsolidation", {
await deploy("MockPartialWithdrawal", {
from: governorAddr,
});

fixture.beaconConsolidationReplaced = beaconConsolidationReplaced;
fixture.beaconWithdrawalReplaced = beaconWithdrawalReplaced;

fixture.beaconRoots = await resolveContract("MockBeaconRoots");
fixture.beaconConsolidation = await resolveContract(
"MockBeaconConsolidation"
);
fixture.partialWithdrawal = await resolveContract("MockPartialWithdrawal");

// fund the beacon communication contracts so they can pay the fee
await hardhatSetBalance(fixture.beaconConsolidation.address, "1");
await hardhatSetBalance(fixture.partialWithdrawal.address, "1");

fixture.beaconOracle = await resolveContract("BeaconOracle");
} else {
fixture.beaconProofs = await resolveContract("MockBeaconProofs");
Expand All @@ -2544,6 +2557,102 @@ async function beaconChainFixture() {
return fixture;
}

/**
* Harhdat doesn't have a support for execution layer general purpose requests to the
* consensus layer. E.g. consolidation request and (partial) withdrawal request.
*/
async function enableExecutionLayerGeneralPurposeRequests() {
const executionLayerConsolidation = await deployWithConfirmation(
"ExecutionLayerConsolidation"
);
const executionLayerWithdrawal = await deployWithConfirmation(
"ExecutionLayerWithdrawal"
);

await replaceContractAt(
addresses.mainnet.toConsensus.consolidation,
executionLayerConsolidation
);

await replaceContractAt(
addresses.mainnet.toConsensus.withdrawals,
executionLayerWithdrawal
);

const withdrawalAbi = `[
{
"inputs": [],
"name": "lastAmount",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastPublicKey",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "view",
"type": "function"
}
]`;

const consolidationAbi = `[
{
"inputs": [],
"name": "lastSource",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastTarget",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "view",
"type": "function"
}
]`;

const beaconConsolidationReplaced = await ethers.getContractAt(
JSON.parse(consolidationAbi),
addresses.mainnet.toConsensus.consolidation
);

const beaconWithdrawalReplaced = await ethers.getContractAt(
JSON.parse(withdrawalAbi),
addresses.mainnet.toConsensus.withdrawals
);

return {
beaconConsolidationReplaced,
beaconWithdrawalReplaced,
};
}

/**
* A fixture is a setup function that is run only the first time it's invoked. On subsequent invocations,
* Hardhat will reset the state of the network to what it was at the point after the fixture was initially executed.
Expand Down
19 changes: 4 additions & 15 deletions contracts/test/beacon/beaconConsolidation.mainnet.fork-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,16 @@ describe("ForkTest: Beacon Consolidation", function () {
});

it("Should request consolidation of validators", async () => {
const { beaconConsolidation } = fixture;
const { beaconConsolidation, beaconConsolidationReplaced } = fixture;

// These are two sweeping validators
const source =
"0xa31b5e5d655a06d849a36e5b03f1b9e647f911f38857c2a263973fba90f61b528173fb7a7cddd63dbe7e6604e7d61c87";
const target =
"0xa258246e1217568a751670447879b7af5d6df585c59a15ebf0380f276069eadb11f30dea77cfb7357447dc24517be560";
const fee = await beaconConsolidation.request(source, target);

expect(fee).to.be.gt(0);
expect(fee).to.be.lt(10);
});

it("Example from mainnet", async () => {
const { beaconConsolidation } = fixture;

const source =
"0x8893a64b63187b4a6dbe555e11729f95ad7665a9329dfcb5f6cd6f8f535551415caf87bede37aaf23478a149a6bc8d8a";
const target =
"0xa5183f98e920a1cc4dab8714055f4a65a262ff1aa0c378d68d6594edd0438ed6c2173193a6b9e17360816684f91b3ffc";

await beaconConsolidation.request(source, target);

expect(await beaconConsolidationReplaced.lastSource()).to.equal(source);
expect(await beaconConsolidationReplaced.lastTarget()).to.equal(target);
});
});
37 changes: 37 additions & 0 deletions contracts/test/beacon/partialWithdrawal.mainnet.fork-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { expect } = require("chai");
const { createFixtureLoader, beaconChainFixture } = require("../_fixture");
const { ousdUnits } = require("../helpers");

const loadFixture = createFixtureLoader(beaconChainFixture);

describe("ForkTest: Partial Withdrawal", function () {
this.timeout(0);

let fixture;
beforeEach(async () => {
fixture = await loadFixture();
});

it("Should get consolidation fee", async () => {
const { partialWithdrawal } = fixture;

const fee = await partialWithdrawal.fee();
expect(fee).to.be.gt(0);
expect(fee).to.be.lt(10);
});

it("Should request a partial withdrawal", async () => {
const { partialWithdrawal, beaconWithdrawalReplaced } = fixture;

const amount = ousdUnits("1");
// These are two sweeping validators
const validatorPKey =
"0xa258246e1217568a751670447879b7af5d6df585c59a15ebf0380f276069eadb11f30dea77cfb7357447dc24517be560";
await partialWithdrawal.request(validatorPKey, amount);

expect(await beaconWithdrawalReplaced.lastPublicKey()).to.equal(
validatorPKey
);
expect(await beaconWithdrawalReplaced.lastAmount()).to.equal(amount);
});
});
7 changes: 7 additions & 0 deletions contracts/utils/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,13 @@ addresses.mainnet.passthrough.uniswap.OETH_OGN =
addresses.mainnet.passthrough.uniswap.OETH_WETH =
"0x216dEBBF25e5e67e6f5B2AD59c856Fc364478A6A";

// General purpose execution to consensus layer communication
addresses.mainnet.toConsensus = {};
addresses.mainnet.toConsensus.consolidation =
"0x0000BBdDc7CE488642fb579F8B00f3a590007251";
addresses.mainnet.toConsensus.withdrawals =
"0x00000961Ef480Eb55e80D19ad83579A64c007002";

// Arbitrum One
addresses.arbitrumOne = {};
addresses.arbitrumOne.WOETHProxy = "0xD8724322f44E5c58D7A815F542036fb17DbbF839";
Expand Down
Loading