Skip to content
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 .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at e4f702
32 changes: 29 additions & 3 deletions src/Pectra.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Pectra {
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

contract Pectra is IERC165 {
address public constant consolidationTarget = 0x0000BBdDc7CE488642fb579F8B00f3a590007251;
address public constant exitTarget = 0x00000961Ef480Eb55e80D19ad83579A64c007002;

Expand All @@ -19,7 +21,12 @@ contract Pectra {
/// @dev Minimum fee required per validator
uint256 public constant MIN_FEE = 1 wei;
/// @dev Maximum withdrawal amount as a uint64 (representing 2048 ether in gwei)
uint64 public constant MAX_WITHDRAWAL_AMOUNT = 0x1DCD6500000;
uint64 public constant MAX_WITHDRAWAL_AMOUNT = 2048000000000; // 2048 ETH in Gwei

// Interface IDs
bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;
bytes4 private constant _INTERFACE_ID_ERC721_RECEIVER = 0x150b7a02;
bytes4 private constant _INTERFACE_ID_ERC1155_RECEIVER = 0x4e2312e0;

// Failure reason codes as enum
enum FailureReason {
Expand All @@ -28,7 +35,8 @@ contract Pectra {
INVALID_AMOUNT_LENGTH,
INVALID_AMOUNT_VALUE,
FULL_EXIT_NOT_CONFIRMED,
AMOUNT_EXCEEDS_MAXIMUM
AMOUNT_EXCEEDS_MAXIMUM,
FULL_EXIT_WITH_AMOUNT
}

event ConsolidationFailed(FailureReason reasonCode, bytes sourcePubkey, bytes targetPubkey);
Expand All @@ -45,6 +53,19 @@ contract Pectra {
receive() external payable {}
fallback() external payable {}

/**
* @dev Implementation of the {IERC165} interface.
*
* Returns true if this contract implements the interface defined by
* `interfaceId`.
*
* This function call must use less than 30,000 gas.
*/
function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
return interfaceId == _INTERFACE_ID_ERC165 || interfaceId == _INTERFACE_ID_ERC721_RECEIVER
|| interfaceId == _INTERFACE_ID_ERC1155_RECEIVER;
}

function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
Expand Down Expand Up @@ -154,6 +175,11 @@ contract Pectra {

bool isZeroAmount = data[i].amount == 0;

if (data[i].isFullExit && !isZeroAmount) {
emit ExecutionLayerExitFailed(FailureReason.FULL_EXIT_WITH_AMOUNT, data[i].pubkey, data[i].amount);
continue;
}

if (isZeroAmount && !data[i].isFullExit) {
emit ExecutionLayerExitFailed(FailureReason.FULL_EXIT_NOT_CONFIRMED, data[i].pubkey, data[i].amount);
continue;
Expand Down
24 changes: 19 additions & 5 deletions test/Pectra.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,8 @@ contract PectraTest is Test {
function testBatchELExit_ExceedsMaximumAmount() public {
Pectra.ExitData[] memory data = new Pectra.ExitData[](1);
data[0].pubkey = validPubkey();
// Set amount to exceed MAX_WITHDRAWAL_AMOUNT
data[0].amount = pectra.MAX_WITHDRAWAL_AMOUNT() + 1;
data[0].isFullExit = true; // Not needed but included for consistency
data[0].isFullExit = false; // Changed to false since full exit can't have amount

vm.expectEmit(true, true, true, true);
emit Pectra.ExecutionLayerExitFailed(
Expand All @@ -345,21 +344,36 @@ contract PectraTest is Test {
Pectra.ExitData[] memory data = new Pectra.ExitData[](1);
data[0].pubkey = validPubkey();
data[0].amount = 1000000000; // 1 ether in gwei
data[0].isFullExit = true; // Not needed but included for consistency
data[0].isFullExit = false; // Changed to false since full exit can't have amount

vm.expectEmit(true, true, true, true);
emit Pectra.ExecutionLayerExitFailed(Pectra.FailureReason.OPERATION_FAILED, data[0].pubkey, data[0].amount);

vm.prank(address(pectra));
pectra.batchELExit{value: 1}(data);
vm.etch(exitTarget, feeCode);
}

function testBatchELExit_FullExitWithAmount() public {
Pectra.ExitData[] memory data = new Pectra.ExitData[](1);
data[0].pubkey = validPubkey();
data[0].amount = 1000000000; // 1 ether in gwei
data[0].isFullExit = true; // Setting both amount and isFullExit

vm.expectEmit(true, true, true, true);
emit Pectra.ExecutionLayerExitFailed(Pectra.FailureReason.FULL_EXIT_WITH_AMOUNT, data[0].pubkey, data[0].amount);

vm.prank(address(pectra));
pectra.batchELExit{value: 1}(data);
}

function testBatchELExit_SuccessWithValidAmount() public {
uint256 count = 2;
Pectra.ExitData[] memory data = new Pectra.ExitData[](count);
for (uint256 i = 0; i < count; i++) {
data[i].pubkey = validPubkey();
data[i].amount = 1000000000; // 1 ether in gwei
data[i].isFullExit = true; // Not needed but included for consistency
data[i].isFullExit = false; // Changed to false since full exit can't have amount
}

// Get the fee from the target
Expand Down Expand Up @@ -459,7 +473,7 @@ contract PectraTest is Test {
for (uint256 i = 0; i < count; i++) {
data[i].pubkey = validPubkey();
data[i].amount = 1000000000; // 1 ether in gwei
data[i].isFullExit = true;
data[i].isFullExit = false; // Changed to false since full exit can't have amount
}

// Get the fee from the target
Expand Down