Skip to content

Commit 8d2c000

Browse files
authored
chore: Provider exits (#25492)
Added a new mechanism to let the provider exit a staking position with an attester key. The purpose is to allow staking providers to walk away from staking without causing the their delegators to get slashed.
1 parent 3da500f commit 8d2c000

17 files changed

Lines changed: 3120 additions & 23 deletions

l1-contracts/l1-artifacts/scripts/generate-artifacts.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ contracts=(
1818
"RollupOperationsExtLib"
1919
"EpochProofExtLib"
2020
"ValidatorOperationsExtLib"
21+
"AttesterExitExtLib"
2122
"RewardExtLib"
2223
"SlasherDeploymentExtLib"
2324
"FeeJuicePortal"

l1-contracts/src/core/Rollup.sol

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,20 @@ import {
1919
RegistryRewardOverride,
2020
MAX_REGISTRY_REWARD_OVERRIDES
2121
} from "@aztec/core/interfaces/IRollup.sol";
22-
import {IStaking, AttesterConfig, Exit, AttesterView, Status} from "@aztec/core/interfaces/IStaking.sol";
22+
import {
23+
IStaking,
24+
AttesterConfig,
25+
Exit,
26+
AttesterView,
27+
Status,
28+
AttesterExitLimitState
29+
} from "@aztec/core/interfaces/IStaking.sol";
2330
import {IValidatorSelection, IEmperor} from "@aztec/core/interfaces/IValidatorSelection.sol";
2431
import {IVerifier} from "@aztec/core/interfaces/IVerifier.sol";
2532
import {TempCheckpointLog, CheckpointLog} from "@aztec/core/libraries/compressed-data/CheckpointLog.sol";
26-
import {FeeAssetValue, PriceLib} from "@aztec/core/libraries/compressed-data/fees/FeeConfig.sol";
33+
import {FeeAssetValue} from "@aztec/core/libraries/compressed-data/fees/FeeConfig.sol";
2734
import {FeeHeaderLib} from "@aztec/core/libraries/compressed-data/fees/FeeStructs.sol";
35+
import {AttesterExitExtLib} from "@aztec/core/libraries/rollup/AttesterExitExtLib.sol";
2836
import {ProposedHeader} from "@aztec/core/libraries/rollup/ProposedHeaderLib.sol";
2937
import {StakingLib} from "@aztec/core/libraries/rollup/StakingLib.sol";
3038
import {GSE} from "@aztec/governance/GSE.sol";
@@ -66,7 +74,6 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore {
6674
using TimeLib for Timestamp;
6775
using TimeLib for Slot;
6876
using TimeLib for Epoch;
69-
using PriceLib for EthValue;
7077
using CompressedTimeMath for CompressedSlot;
7178
using CompressedTimeMath for CompressedTimestamp;
7279
using ChainTipsLib for CompressedChainTips;
@@ -250,6 +257,14 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore {
250257
return StakingLib.getStorage().gse.ACTIVATION_THRESHOLD();
251258
}
252259

260+
function getAttesterExitWindow() external view override(IStaking) returns (Timestamp) {
261+
return AttesterExitExtLib.getAttesterExitWindow();
262+
}
263+
264+
function getAttesterExitLimitState() external view override(IStaking) returns (AttesterExitLimitState memory) {
265+
return AttesterExitExtLib.getAttesterExitLimitState();
266+
}
267+
253268
function getExitDelay() external view override(IStaking) returns (Timestamp) {
254269
return StakingLib.getStorage().exitDelay.decompress();
255270
}
@@ -565,7 +580,7 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore {
565580
}
566581

567582
function getProvingCostPerManaInFeeAsset() external view override(IRollup) returns (FeeAssetValue) {
568-
return RewardExtLib.getProvingCostPerMana().toFeeAsset(getEthPerFeeAsset());
583+
return RewardExtLib.getProvingCostPerManaInFeeAsset(getEthPerFeeAsset());
569584
}
570585

571586
// The config getters below go through {_getRollupConfig} rather than reading their immutable

l1-contracts/src/core/RollupCore.sol

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ import {RollupOperationsExtLib} from "@aztec/core/libraries/rollup/RollupOperati
2424
import {ValidatorOperationsExtLib} from "@aztec/core/libraries/rollup/ValidatorOperationsExtLib.sol";
2525
import {SlasherDeploymentExtLib} from "@aztec/core/libraries/rollup/SlasherDeploymentExtLib.sol";
2626
import {EthValue} from "@aztec/core/libraries/compressed-data/fees/FeeConfig.sol";
27+
import {AttesterExitExtLib} from "@aztec/core/libraries/rollup/AttesterExitExtLib.sol";
2728
import {FeeLib} from "@aztec/core/libraries/rollup/FeeLib.sol";
2829
import {ProposeArgs} from "@aztec/core/libraries/rollup/ProposeLib.sol";
2930
import {STFLib, GenesisState} from "@aztec/core/libraries/rollup/STFLib.sol";
30-
import {StakingLib} from "@aztec/core/libraries/rollup/StakingLib.sol";
31+
import {StakingLib, AttesterExitAuthorization} from "@aztec/core/libraries/rollup/StakingLib.sol";
3132
import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol";
3233
import {Inbox, INBOX_BUCKET_RING_SIZE} from "@aztec/core/messagebridge/Inbox.sol";
3334
import {Outbox} from "@aztec/core/messagebridge/Outbox.sol";
@@ -489,6 +490,53 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
489490
return ValidatorOperationsExtLib.initiateWithdraw(_attester, _recipient);
490491
}
491492

493+
/**
494+
* @notice Initiates withdrawal of a validator's stake by the attester
495+
* @dev The registered withdrawer retains control of the payout recipient.
496+
* @param _attester The validator position to exit.
497+
*/
498+
function initiateWithdrawByAttester(address _attester) external override(IStakingCore) {
499+
AttesterExitExtLib.initiateWithdrawByAttester(_attester);
500+
}
501+
502+
/**
503+
* @notice Initiates an attester withdrawal using an EIP-712 authorization.
504+
* @dev Anyone may relay the authorization. The registered withdrawer retains control of the payout recipient.
505+
* @param _authorization The attester, deadline, and signature authorizing the exit.
506+
*/
507+
function initiateWithdrawByAttesterWithSignature(AttesterExitAuthorization calldata _authorization)
508+
external
509+
override(IStakingCore)
510+
{
511+
AttesterExitExtLib.initiateWithdrawByAttesterWithSignature(_authorization);
512+
}
513+
514+
/**
515+
* @notice Initiates multiple attester withdrawals using EIP-712 authorizations.
516+
* @dev Shared instance and exit-limit checks are performed once for the whole batch.
517+
* @param _authorizations The signed exit authorizations to execute atomically.
518+
*/
519+
function initiateWithdrawByAttesterBatch(AttesterExitAuthorization[] calldata _authorizations)
520+
external
521+
override(IStakingCore)
522+
{
523+
AttesterExitExtLib.initiateWithdrawByAttesterBatch(_authorizations);
524+
}
525+
526+
/**
527+
* @notice Initiates as many authorized attester withdrawals as the current exit limit permits.
528+
* @dev Processes a prefix of the array and returns zero when no capacity remains.
529+
* @param _authorizations The ordered signed exit authorizations.
530+
* @return exitedCount The number of authorizations processed from the start of the array.
531+
*/
532+
function initiateWithdrawByAttesterBatchUpToLimit(AttesterExitAuthorization[] calldata _authorizations)
533+
external
534+
override(IStakingCore)
535+
returns (uint256 exitedCount)
536+
{
537+
return AttesterExitExtLib.initiateWithdrawByAttesterBatchUpToLimit(_authorizations);
538+
}
539+
492540
/**
493541
* @notice Completes a withdrawal after the exit delay has passed
494542
* @dev Can be called by anyone. Transfers the stake to the designated recipient.

l1-contracts/src/core/interfaces/IStaking.sol

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
pragma solidity >=0.8.27;
44

55
import {StakingQueueConfig} from "@aztec/core/libraries/compressed-data/StakingQueueConfig.sol";
6-
import {Exit, Status, AttesterView} from "@aztec/core/libraries/rollup/StakingLib.sol";
6+
import {
7+
Exit,
8+
Status,
9+
AttesterView,
10+
AttesterExitLimitState,
11+
AttesterExitAuthorization
12+
} from "@aztec/core/libraries/rollup/StakingLib.sol";
713
import {DepositArgs} from "@aztec/core/libraries/StakingQueue.sol";
814
import {AttesterConfig, GSE} from "@aztec/governance/GSE.sol";
915
import {G1Point, G2Point} from "@aztec/shared/libraries/BN254Lib.sol";
@@ -35,6 +41,9 @@ interface IStakingCore {
3541
event WithdrawFinalized(address indexed attester, address indexed recipient, uint256 amount);
3642
event Slashed(address indexed attester, uint256 amount);
3743
event StakingQueueConfigUpdated(StakingQueueConfig config);
44+
event WithdrawInitiatedByAttester(
45+
address indexed attester, address indexed withdrawer, uint256 amount, uint256 withdrawalId
46+
);
3847

3948
function queueSetSlasher(address _slasher) external;
4049
function cancelSetSlasher() external;
@@ -54,7 +63,12 @@ interface IStakingCore {
5463
function slash(address _attester, uint256 _amount) external returns (bool);
5564
function vote(uint256 _proposalId) external;
5665
function updateStakingQueueConfig(StakingQueueConfig memory _config) external;
57-
66+
function initiateWithdrawByAttester(address _attester) external;
67+
function initiateWithdrawByAttesterWithSignature(AttesterExitAuthorization calldata _authorization) external;
68+
function initiateWithdrawByAttesterBatch(AttesterExitAuthorization[] calldata _authorizations) external;
69+
function initiateWithdrawByAttesterBatchUpToLimit(AttesterExitAuthorization[] calldata _authorizations)
70+
external
71+
returns (uint256);
5872
function getEntryQueueFlushSize() external view returns (uint256);
5973
function getActiveAttesterCount() external view returns (uint256);
6074
}
@@ -73,6 +87,8 @@ interface IStaking is IStakingCore {
7387
function getActivationThreshold() external view returns (uint256);
7488
function getEjectionThreshold() external view returns (uint256);
7589
function getExitDelay() external view returns (Timestamp);
90+
function getAttesterExitWindow() external view returns (Timestamp);
91+
function getAttesterExitLimitState() external view returns (AttesterExitLimitState memory);
7692
function getGSE() external view returns (GSE);
7793
function getAttesterView(address _attester) external view returns (AttesterView memory);
7894
function getStatus(address _attester) external view returns (Status);

l1-contracts/src/core/libraries/Errors.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ library Errors {
193193
error Staking__SlasherProposerNotInitialized(address slasher);
194194
error Staking__NoPendingSlasher();
195195
error Staking__SlasherNotReady(Timestamp readyAt);
196+
error Staking__AttesterExitPoolTooSmall(uint256 validatorCount, uint256 committeeSize);
197+
error Staking__AttesterExitLimitExceeded(uint256 used, uint256 allowance);
198+
error Staking__EmptyAttesterExitBatch();
199+
error Staking__AttesterExitAuthorizationExpired(uint256 deadline, uint256 currentTime);
200+
error Staking__NotAttester(address attester, address caller);
201+
error Staking__NotLatestRollup(address rollup, address latestRollup);
196202

197203
// Fee Juice Portal
198204
error FeeJuicePortal__AlreadyInitialized(); // 0xc7a172fe
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2026 Aztec Labs.
3+
pragma solidity >=0.8.27;
4+
5+
import {Errors} from "@aztec/core/libraries/Errors.sol";
6+
import {Signature, SignatureLib} from "@aztec/shared/libraries/SignatureLib.sol";
7+
import {Timestamp} from "@aztec/shared/libraries/TimeMath.sol";
8+
import {CoordinationSignatureLib} from "./CoordinationSignatureLib.sol";
9+
import {StakingLib, AttesterExitLimitState, AttesterExitAuthorization} from "./StakingLib.sol";
10+
11+
/// @notice External attester-exit functions separated to keep rollup libraries within the contract size limit.
12+
/// @dev Library calls execute against the calling rollup's staking storage.
13+
library AttesterExitExtLib {
14+
using SignatureLib for Signature;
15+
16+
bytes32 internal constant ATTESTER_EXIT_TYPEHASH = keccak256("AttesterExit(address attester,uint256 deadline)");
17+
18+
/// @notice Initiates an exit for a position controlled by the calling attester.
19+
function initiateWithdrawByAttester(address _attester) external {
20+
StakingLib.initiateWithdrawByAttester(_attester);
21+
}
22+
23+
/// @notice Initiates an attester exit authorized by an EIP-712 signature.
24+
/// @param _authorization The signed exit authorization.
25+
function initiateWithdrawByAttesterWithSignature(AttesterExitAuthorization calldata _authorization) external {
26+
_authorize(_authorization);
27+
StakingLib.initiateWithdrawByAttesterWithSignature(_authorization.attester);
28+
}
29+
30+
/// @notice Initiates multiple attester exits authorized by EIP-712 signatures.
31+
/// @param _authorizations The signed exit authorizations to execute atomically.
32+
function initiateWithdrawByAttesterBatch(AttesterExitAuthorization[] calldata _authorizations) external {
33+
uint256 exitCount = _authorizations.length;
34+
require(exitCount > 0, Errors.Staking__EmptyAttesterExitBatch());
35+
36+
StakingLib.checkAttesterExitInstance();
37+
StakingLib.consumeAttesterExitAllowance(exitCount);
38+
39+
for (uint256 i = 0; i < exitCount;) {
40+
AttesterExitAuthorization calldata authorization = _authorizations[i];
41+
_authorize(authorization);
42+
StakingLib.initiateWithdrawByAttesterBatchItem(authorization.attester);
43+
unchecked {
44+
++i;
45+
}
46+
}
47+
}
48+
49+
/// @notice Initiates as many authorized attester exits as the current limit permits.
50+
/// @param _authorizations The ordered signed exit authorizations.
51+
/// @return exitCount The number of authorizations processed from the start of the array.
52+
function initiateWithdrawByAttesterBatchUpToLimit(AttesterExitAuthorization[] calldata _authorizations)
53+
external
54+
returns (uint256 exitCount)
55+
{
56+
uint256 requestedExitCount = _authorizations.length;
57+
require(requestedExitCount > 0, Errors.Staking__EmptyAttesterExitBatch());
58+
59+
StakingLib.checkAttesterExitInstance();
60+
exitCount = StakingLib.consumeAttesterExitAllowanceUpTo(requestedExitCount);
61+
62+
for (uint256 i = 0; i < exitCount;) {
63+
AttesterExitAuthorization calldata authorization = _authorizations[i];
64+
_authorize(authorization);
65+
StakingLib.initiateWithdrawByAttesterBatchItem(authorization.attester);
66+
unchecked {
67+
++i;
68+
}
69+
}
70+
}
71+
72+
/// @notice Returns the current window used to count attester exits.
73+
function getAttesterExitWindow() external view returns (Timestamp) {
74+
return StakingLib.getAttesterExitWindow();
75+
}
76+
77+
/// @notice Returns attester-exit capacity for the calling rollup.
78+
/// @dev canExit does not establish eligibility of an individual position.
79+
function getAttesterExitLimitState() external view returns (AttesterExitLimitState memory) {
80+
return StakingLib.getAttesterExitLimitState();
81+
}
82+
83+
function _authorize(AttesterExitAuthorization calldata _authorization) private view {
84+
require(
85+
block.timestamp <= _authorization.deadline,
86+
Errors.Staking__AttesterExitAuthorizationExpired(_authorization.deadline, block.timestamp)
87+
);
88+
89+
bytes32 structHash = keccak256(abi.encode(ATTESTER_EXIT_TYPEHASH, _authorization.attester, _authorization.deadline));
90+
bytes32 digest = CoordinationSignatureLib.toTypedDataHash(structHash);
91+
_authorization.signature.verify(_authorization.attester, digest);
92+
}
93+
}

l1-contracts/src/core/libraries/rollup/RewardExtLib.sol

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
pragma solidity >=0.8.27;
44

55
import {IRollupCore} from "@aztec/core/interfaces/IRollup.sol";
6-
import {FeeConfigLib, CompressedFeeConfig} from "@aztec/core/libraries/compressed-data/fees/FeeConfig.sol";
6+
import {
7+
FeeConfigLib,
8+
CompressedFeeConfig,
9+
FeeAssetValue,
10+
PriceLib
11+
} from "@aztec/core/libraries/compressed-data/fees/FeeConfig.sol";
712
import {Errors} from "@aztec/core/libraries/Errors.sol";
813
import {
914
FeeLib,
@@ -33,6 +38,7 @@ import {IERC20} from "@oz/token/ERC20/IERC20.sol";
3338

3439
library RewardExtLib {
3540
using FeeConfigLib for CompressedFeeConfig;
41+
using PriceLib for EthValue;
3642

3743
function initializeConfig(
3844
RewardConfig memory _config,
@@ -168,6 +174,10 @@ library RewardExtLib {
168174
return FeeLib.getProvingCostPerMana();
169175
}
170176

177+
function getProvingCostPerManaInFeeAsset(EthPerFeeAssetE12 _ethPerFeeAsset) external view returns (FeeAssetValue) {
178+
return FeeLib.getProvingCostPerMana().toFeeAsset(_ethPerFeeAsset);
179+
}
180+
171181
function summedMinFee(ManaMinFeeComponents memory _components) external pure returns (uint256) {
172182
return FeeLib.summedMinFee(_components);
173183
}

0 commit comments

Comments
 (0)