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
40 changes: 21 additions & 19 deletions src/Pectra.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ contract Pectra {
/// @dev Maximum withdrawal amount as a uint64 (representing 2048 ether in gwei)
uint64 public constant MAX_WITHDRAWAL_AMOUNT = 0x1DCD6500000;

// Failure reason codes
uint8 public constant INVALID_PUBKEY_LENGTH = 1;
uint8 public constant OPERATION_FAILED = 2;
uint8 public constant INVALID_AMOUNT_LENGTH = 3;
uint8 public constant INVALID_AMOUNT_VALUE = 4;
uint8 public constant FULL_EXIT_NOT_CONFIRMED = 5;
uint8 public constant AMOUNT_EXCEEDS_MAXIMUM = 6;

event ConsolidationFailed(uint8 reasonCode, bytes sourcePubkey, bytes targetPubkey);
event SwitchFailed(uint8 reasonCode, bytes pubkey);
event ExecutionLayerExitFailed(uint8 reasonCode, bytes pubkey, bytes amount);
// Failure reason codes as enum
enum FailureReason {
INVALID_PUBKEY_LENGTH,
OPERATION_FAILED,
INVALID_AMOUNT_LENGTH,
INVALID_AMOUNT_VALUE,
FULL_EXIT_NOT_CONFIRMED,
AMOUNT_EXCEEDS_MAXIMUM
}

event ConsolidationFailed(FailureReason reasonCode, bytes sourcePubkey, bytes targetPubkey);
event SwitchFailed(FailureReason reasonCode, bytes pubkey);
event ExecutionLayerExitFailed(FailureReason reasonCode, bytes pubkey, uint64 amount);

error Unauthorized();
error InvalidTargetPubkeyLength(bytes invalidTargetPubkey);
Expand Down Expand Up @@ -93,14 +95,14 @@ contract Pectra {

for (uint256 i = 0; i < batchSize; ++i) {
if (sourcePubkeys[i].length != VALIDATOR_PUBKEY_LENGTH) {
emit ConsolidationFailed(INVALID_PUBKEY_LENGTH, sourcePubkeys[i], targetPubkey);
emit ConsolidationFailed(FailureReason.INVALID_PUBKEY_LENGTH, sourcePubkeys[i], targetPubkey);
continue;
}

bytes memory concatenated = abi.encodePacked(sourcePubkeys[i], targetPubkey);
(bool success,) = consolidationTarget.call{value: consolidationFee}(concatenated);
if (!success) {
emit ConsolidationFailed(OPERATION_FAILED, sourcePubkeys[i], targetPubkey);
emit ConsolidationFailed(FailureReason.OPERATION_FAILED, sourcePubkeys[i], targetPubkey);
continue;
}
}
Expand All @@ -116,14 +118,14 @@ contract Pectra {

for (uint256 i = 0; i < batchSize; ++i) {
if (pubkeys[i].length != VALIDATOR_PUBKEY_LENGTH) {
emit SwitchFailed(INVALID_PUBKEY_LENGTH, pubkeys[i]);
emit SwitchFailed(FailureReason.INVALID_PUBKEY_LENGTH, pubkeys[i]);
continue;
}

bytes memory concatenated = abi.encodePacked(pubkeys[i], pubkeys[i]);
(bool success,) = consolidationTarget.call{value: switchFee}(concatenated);
if (!success) {
emit SwitchFailed(OPERATION_FAILED, pubkeys[i]);
emit SwitchFailed(FailureReason.OPERATION_FAILED, pubkeys[i]);
continue;
}
}
Expand All @@ -146,19 +148,19 @@ contract Pectra {

for (uint256 i = 0; i < batchSize; ++i) {
if (data[i].pubkey.length != VALIDATOR_PUBKEY_LENGTH) {
emit ExecutionLayerExitFailed(INVALID_PUBKEY_LENGTH, data[i].pubkey, abi.encodePacked(data[i].amount));
emit ExecutionLayerExitFailed(FailureReason.INVALID_PUBKEY_LENGTH, data[i].pubkey, data[i].amount);
continue;
}

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

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

if (!isZeroAmount && data[i].amount > MAX_WITHDRAWAL_AMOUNT) {
emit ExecutionLayerExitFailed(AMOUNT_EXCEEDS_MAXIMUM, data[i].pubkey, abi.encodePacked(data[i].amount));
emit ExecutionLayerExitFailed(FailureReason.AMOUNT_EXCEEDS_MAXIMUM, data[i].pubkey, data[i].amount);
continue;
}

Expand All @@ -167,7 +169,7 @@ contract Pectra {
bytes memory concatenated = abi.encodePacked(data[i].pubkey, amountBytes);
(bool success,) = exitTarget.call{value: exitFee}(concatenated);
if (!success) {
emit ExecutionLayerExitFailed(OPERATION_FAILED, data[i].pubkey, amountBytes);
emit ExecutionLayerExitFailed(FailureReason.OPERATION_FAILED, data[i].pubkey, data[i].amount);
continue;
}
}
Expand Down
28 changes: 12 additions & 16 deletions test/Pectra.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,7 @@ contract PectraTest is Test {
sources[0] = new bytes(pectra.VALIDATOR_PUBKEY_LENGTH() - 1);
bytes memory target = validPubkey();
vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.INVALID_PUBKEY_LENGTH();
emit Pectra.ConsolidationFailed(reasonCode, sources[0], target);
emit Pectra.ConsolidationFailed(Pectra.FailureReason.INVALID_PUBKEY_LENGTH, sources[0], target);
vm.prank(address(pectra));
pectra.batchConsolidation{value: 1}(sources, target);
}
Expand All @@ -168,8 +167,7 @@ contract PectraTest is Test {
sources[0] = validPubkey();
bytes memory target = validPubkey();
vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.OPERATION_FAILED();
emit Pectra.ConsolidationFailed(reasonCode, sources[0], target);
emit Pectra.ConsolidationFailed(Pectra.FailureReason.OPERATION_FAILED, sources[0], target);
vm.prank(address(pectra));
pectra.batchConsolidation{value: 1}(sources, target);
// Restore successful code.
Expand Down Expand Up @@ -232,8 +230,7 @@ contract PectraTest is Test {
bytes[] memory pubkeys = new bytes[](1);
pubkeys[0] = new bytes(pectra.VALIDATOR_PUBKEY_LENGTH() - 1); // one byte less than required
vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.INVALID_PUBKEY_LENGTH();
emit Pectra.SwitchFailed(reasonCode, pubkeys[0]);
emit Pectra.SwitchFailed(Pectra.FailureReason.INVALID_PUBKEY_LENGTH, pubkeys[0]);
vm.prank(address(pectra));
pectra.batchSwitch{value: 1}(pubkeys);
}
Expand All @@ -243,8 +240,7 @@ contract PectraTest is Test {
bytes[] memory pubkeys = new bytes[](1);
pubkeys[0] = validPubkey();
vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.OPERATION_FAILED();
emit Pectra.SwitchFailed(reasonCode, pubkeys[0]);
emit Pectra.SwitchFailed(Pectra.FailureReason.OPERATION_FAILED, pubkeys[0]);
vm.prank(address(pectra));
pectra.batchSwitch{value: 1}(pubkeys);
vm.etch(consolidationTarget, feeCode);
Expand Down Expand Up @@ -310,8 +306,7 @@ contract PectraTest is Test {
data[0].amount = 0;
data[0].isFullExit = true;
vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.INVALID_PUBKEY_LENGTH();
emit Pectra.ExecutionLayerExitFailed(reasonCode, data[0].pubkey, abi.encodePacked(data[0].amount));
emit Pectra.ExecutionLayerExitFailed(Pectra.FailureReason.INVALID_PUBKEY_LENGTH, data[0].pubkey, data[0].amount);
vm.prank(address(pectra));
pectra.batchELExit{value: 1}(data);
}
Expand All @@ -322,8 +317,9 @@ contract PectraTest is Test {
data[0].amount = 0; // Zero amount
data[0].isFullExit = false; // Flag set to false
vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.FULL_EXIT_NOT_CONFIRMED();
emit Pectra.ExecutionLayerExitFailed(reasonCode, data[0].pubkey, abi.encodePacked(data[0].amount));
emit Pectra.ExecutionLayerExitFailed(
Pectra.FailureReason.FULL_EXIT_NOT_CONFIRMED, data[0].pubkey, data[0].amount
);
vm.prank(address(pectra));
pectra.batchELExit{value: 1}(data);
}
Expand All @@ -336,8 +332,9 @@ contract PectraTest is Test {
data[0].isFullExit = true; // Not needed but included for consistency

vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.AMOUNT_EXCEEDS_MAXIMUM();
emit Pectra.ExecutionLayerExitFailed(reasonCode, data[0].pubkey, abi.encodePacked(data[0].amount));
emit Pectra.ExecutionLayerExitFailed(
Pectra.FailureReason.AMOUNT_EXCEEDS_MAXIMUM, data[0].pubkey, data[0].amount
);

vm.prank(address(pectra));
pectra.batchELExit{value: 1}(data);
Expand All @@ -350,8 +347,7 @@ contract PectraTest is Test {
data[0].amount = 1000000000; // 1 ether in gwei
data[0].isFullExit = true; // Not needed but included for consistency
vm.expectEmit(true, true, true, true);
uint8 reasonCode = pectra.OPERATION_FAILED();
emit Pectra.ExecutionLayerExitFailed(reasonCode, data[0].pubkey, abi.encodePacked(data[0].amount));
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);
Expand Down