diff --git a/.gitmodules b/.gitmodules index 888d42d..690924b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..e4f7021 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/src/Pectra.sol b/src/Pectra.sol index 6fdbf0b..4b33fc6 100644 --- a/src/Pectra.sol +++ b/src/Pectra.sol @@ -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; @@ -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 { @@ -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); @@ -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; } @@ -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; diff --git a/test/Pectra.t.sol b/test/Pectra.t.sol index 4a2ae97..0fac0c7 100644 --- a/test/Pectra.t.sol +++ b/test/Pectra.t.sol @@ -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( @@ -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 @@ -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