Skip to content

Commit c324781

Browse files
authored
feat: standalone ssd (#10317)
1 parent 80fad45 commit c324781

15 files changed

+1022
-5
lines changed

l1-contracts/src/core/Rollup.sol

+8-5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ struct SubmitEpochRootProofInterimValues {
5757
uint256 endBlockNumber;
5858
Epoch epochToProve;
5959
Epoch startEpoch;
60+
bool isFeeCanonical;
61+
bool isRewardDistributorCanonical;
6062
}
6163

6264
/**
@@ -319,20 +321,21 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
319321

320322
// @note Only if the rollup is the canonical will it be able to meaningfully claim fees
321323
// Otherwise, the fees are unbacked #7938.
322-
bool isFeeCanonical = address(this) == FEE_JUICE_PORTAL.canonicalRollup();
323-
bool isRewardDistributorCanonical = address(this) == REWARD_DISTRIBUTOR.canonicalRollup();
324+
interimValues.isFeeCanonical = address(this) == FEE_JUICE_PORTAL.canonicalRollup();
325+
interimValues.isRewardDistributorCanonical =
326+
address(this) == REWARD_DISTRIBUTOR.canonicalRollup();
324327

325328
uint256 totalProverReward = 0;
326329
uint256 totalBurn = 0;
327330

328-
if (isFeeCanonical || isRewardDistributorCanonical) {
331+
if (interimValues.isFeeCanonical || interimValues.isRewardDistributorCanonical) {
329332
for (uint256 i = 0; i < _args.epochSize; i++) {
330333
address coinbase = address(uint160(uint256(publicInputs[9 + i * 2])));
331334
uint256 reward = 0;
332335
uint256 toProver = 0;
333336
uint256 burn = 0;
334337

335-
if (isFeeCanonical) {
338+
if (interimValues.isFeeCanonical) {
336339
uint256 fees = uint256(publicInputs[10 + i * 2]);
337340
if (fees > 0) {
338341
// This is insanely expensive, and will be fixed as part of the general storage cost reduction.
@@ -346,7 +349,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
346349
}
347350
}
348351

349-
if (isRewardDistributorCanonical) {
352+
if (interimValues.isRewardDistributorCanonical) {
350353
reward += REWARD_DISTRIBUTOR.claim(address(this));
351354
}
352355

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2024 Aztec Labs.
3+
pragma solidity >=0.8.27;
4+
5+
import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";
6+
7+
// None -> Does not exist in our setup
8+
// Validating -> Participating as validator
9+
// Living -> Not participating as validator, but have funds in setup,
10+
// hit if slashes and going below the minimum
11+
// Exiting -> In the process of exiting the system
12+
enum Status {
13+
NONE,
14+
VALIDATING,
15+
LIVING,
16+
EXITING
17+
}
18+
19+
struct ValidatorInfo {
20+
uint256 stake;
21+
address withdrawer;
22+
address proposer;
23+
Status status;
24+
}
25+
26+
struct OperatorInfo {
27+
address proposer;
28+
address attester;
29+
}
30+
31+
struct Exit {
32+
Timestamp exitableAt;
33+
address recipient;
34+
}
35+
36+
interface IStaking {
37+
event Deposit(
38+
address indexed attester, address indexed proposer, address indexed withdrawer, uint256 amount
39+
);
40+
event WithdrawInitiated(address indexed attester, address indexed recipient, uint256 amount);
41+
event WithdrawFinalised(address indexed attester, address indexed recipient, uint256 amount);
42+
event Slashed(address indexed attester, uint256 amount);
43+
44+
function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount)
45+
external;
46+
function initiateWithdraw(address _attester, address _recipient) external returns (bool);
47+
function finaliseWithdraw(address _attester) external;
48+
function slash(address _attester, uint256 _amount) external;
49+
50+
function getInfo(address _attester) external view returns (ValidatorInfo memory);
51+
function getExit(address _attester) external view returns (Exit memory);
52+
function getActiveAttesterCount() external view returns (uint256);
53+
function getAttesterAtIndex(uint256 _index) external view returns (address);
54+
function getProposerAtIndex(uint256 _index) external view returns (address);
55+
function getProposerForAttester(address _attester) external view returns (address);
56+
function getOperatorAtIndex(uint256 _index) external view returns (OperatorInfo memory);
57+
}

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

+13
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,19 @@ library Errors {
101101
error Leonidas__InsufficientAttestations(uint256 minimumNeeded, uint256 provided); // 0xbf1ca4cb
102102
error Leonidas__InsufficientAttestationsProvided(uint256 minimumNeeded, uint256 provided); // 0xb3a697c2
103103

104+
// Staking
105+
error Staking__AlreadyActive(address attester); // 0x5e206fa4
106+
error Staking__AlreadyRegistered(address); // 0x18047699
107+
error Staking__CannotSlashExitedStake(address); // 0x45bf4940
108+
error Staking__FailedToRemove(address); // 0xa7d7baab
109+
error Staking__InsufficientStake(uint256, uint256); // 0x903aee24
110+
error Staking__NoOneToSlash(address); // 0x7e2f7f1c
111+
error Staking__NotExiting(address); // 0xef566ee0
112+
error Staking__NotSlasher(address, address); // 0x23a6f432
113+
error Staking__NotWithdrawer(address, address); // 0x8e668e5d
114+
error Staking__NothingToExit(address); // 0xd2aac9b6
115+
error Staking__WithdrawalNotUnlockedYet(Timestamp, Timestamp); // 0x88e1826c
116+
104117
// Fee Juice Portal
105118
error FeeJuicePortal__AlreadyInitialized(); // 0xc7a172fe
106119
error FeeJuicePortal__InvalidInitialization(); // 0xfd9b3208
+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2024 Aztec Labs.
3+
pragma solidity >=0.8.27;
4+
5+
import {
6+
IStaking, ValidatorInfo, Exit, Status, OperatorInfo
7+
} from "@aztec/core/interfaces/IStaking.sol";
8+
import {Errors} from "@aztec/core/libraries/Errors.sol";
9+
import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";
10+
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
11+
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";
12+
import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";
13+
14+
contract Staking is IStaking {
15+
using SafeERC20 for IERC20;
16+
using EnumerableSet for EnumerableSet.AddressSet;
17+
18+
// Constant pulled out of the ass
19+
Timestamp public constant EXIT_DELAY = Timestamp.wrap(60 * 60 * 24);
20+
21+
address public immutable SLASHER;
22+
IERC20 public immutable STAKING_ASSET;
23+
uint256 public immutable MINIMUM_STAKE;
24+
25+
// address <=> index
26+
EnumerableSet.AddressSet internal attesters;
27+
28+
mapping(address attester => ValidatorInfo) internal info;
29+
mapping(address attester => Exit) internal exits;
30+
31+
constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) {
32+
SLASHER = _slasher;
33+
STAKING_ASSET = _stakingAsset;
34+
MINIMUM_STAKE = _minimumStake;
35+
}
36+
37+
function finaliseWithdraw(address _attester) external override(IStaking) {
38+
ValidatorInfo storage validator = info[_attester];
39+
require(validator.status == Status.EXITING, Errors.Staking__NotExiting(_attester));
40+
41+
Exit storage exit = exits[_attester];
42+
require(
43+
exit.exitableAt <= Timestamp.wrap(block.timestamp),
44+
Errors.Staking__WithdrawalNotUnlockedYet(Timestamp.wrap(block.timestamp), exit.exitableAt)
45+
);
46+
47+
uint256 amount = validator.stake;
48+
address recipient = exit.recipient;
49+
50+
delete exits[_attester];
51+
delete info[_attester];
52+
53+
STAKING_ASSET.transfer(recipient, amount);
54+
55+
emit IStaking.WithdrawFinalised(_attester, recipient, amount);
56+
}
57+
58+
function slash(address _attester, uint256 _amount) external override(IStaking) {
59+
require(msg.sender == SLASHER, Errors.Staking__NotSlasher(SLASHER, msg.sender));
60+
61+
ValidatorInfo storage validator = info[_attester];
62+
require(validator.status != Status.NONE, Errors.Staking__NoOneToSlash(_attester));
63+
64+
// There is a special, case, if exiting and past the limit, it is untouchable!
65+
require(
66+
!(
67+
validator.status == Status.EXITING
68+
&& exits[_attester].exitableAt <= Timestamp.wrap(block.timestamp)
69+
),
70+
Errors.Staking__CannotSlashExitedStake(_attester)
71+
);
72+
validator.stake -= _amount;
73+
74+
// If the attester was validating AND is slashed below the MINIMUM_STAKE we update him to LIVING
75+
// When LIVING, he can only start exiting, we don't "really" exit him, because that cost
76+
// gas and cost edge cases around recipient, so lets just avoid that.
77+
if (validator.status == Status.VALIDATING && validator.stake < MINIMUM_STAKE) {
78+
require(attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester));
79+
validator.status = Status.LIVING;
80+
}
81+
82+
emit Slashed(_attester, _amount);
83+
}
84+
85+
function getInfo(address _attester)
86+
external
87+
view
88+
override(IStaking)
89+
returns (ValidatorInfo memory)
90+
{
91+
return info[_attester];
92+
}
93+
94+
function getProposerForAttester(address _attester)
95+
external
96+
view
97+
override(IStaking)
98+
returns (address)
99+
{
100+
return info[_attester].proposer;
101+
}
102+
103+
function getExit(address _attester) external view override(IStaking) returns (Exit memory) {
104+
return exits[_attester];
105+
}
106+
107+
function getAttesterAtIndex(uint256 _index) external view override(IStaking) returns (address) {
108+
return attesters.at(_index);
109+
}
110+
111+
function getProposerAtIndex(uint256 _index) external view override(IStaking) returns (address) {
112+
return info[attesters.at(_index)].proposer;
113+
}
114+
115+
function getOperatorAtIndex(uint256 _index)
116+
external
117+
view
118+
override(IStaking)
119+
returns (OperatorInfo memory)
120+
{
121+
address attester = attesters.at(_index);
122+
return OperatorInfo({proposer: info[attester].proposer, attester: attester});
123+
}
124+
125+
function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount)
126+
public
127+
virtual
128+
override(IStaking)
129+
{
130+
require(_amount >= MINIMUM_STAKE, Errors.Staking__InsufficientStake(_amount, MINIMUM_STAKE));
131+
STAKING_ASSET.transferFrom(msg.sender, address(this), _amount);
132+
require(info[_attester].status == Status.NONE, Errors.Staking__AlreadyRegistered(_attester));
133+
require(attesters.add(_attester), Errors.Staking__AlreadyActive(_attester));
134+
135+
// If BLS, need to check possession of private key to avoid attacks.
136+
137+
info[_attester] = ValidatorInfo({
138+
stake: _amount,
139+
withdrawer: _withdrawer,
140+
proposer: _proposer,
141+
status: Status.VALIDATING
142+
});
143+
144+
emit IStaking.Deposit(_attester, _proposer, _withdrawer, _amount);
145+
}
146+
147+
function initiateWithdraw(address _attester, address _recipient)
148+
public
149+
virtual
150+
override(IStaking)
151+
returns (bool)
152+
{
153+
ValidatorInfo storage validator = info[_attester];
154+
155+
require(
156+
msg.sender == validator.withdrawer,
157+
Errors.Staking__NotWithdrawer(validator.withdrawer, msg.sender)
158+
);
159+
require(
160+
validator.status == Status.VALIDATING || validator.status == Status.LIVING,
161+
Errors.Staking__NothingToExit(_attester)
162+
);
163+
if (validator.status == Status.VALIDATING) {
164+
require(attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester));
165+
}
166+
167+
// Note that the "amount" is not stored here, but reusing the `validators`
168+
// We always exit fully.
169+
exits[_attester] =
170+
Exit({exitableAt: Timestamp.wrap(block.timestamp) + EXIT_DELAY, recipient: _recipient});
171+
validator.status = Status.EXITING;
172+
173+
emit IStaking.WithdrawInitiated(_attester, _recipient, validator.stake);
174+
175+
return true;
176+
}
177+
178+
function getActiveAttesterCount() public view override(IStaking) returns (uint256) {
179+
return attesters.length();
180+
}
181+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2024 Aztec Labs.
3+
pragma solidity >=0.8.27;
4+
5+
import {Staking, Status} from "@aztec/core/staking/Staking.sol";
6+
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
7+
import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";
8+
9+
contract StakingCheater is Staking {
10+
using EnumerableSet for EnumerableSet.AddressSet;
11+
12+
constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake)
13+
Staking(_slasher, _stakingAsset, _minimumStake)
14+
{}
15+
16+
function cheat__SetStatus(address _attester, Status _status) external {
17+
info[_attester].status = _status;
18+
}
19+
20+
function cheat__AddAttester(address _attester) external {
21+
attesters.add(_attester);
22+
}
23+
24+
function cheat__RemoveAttester(address _attester) external {
25+
attesters.remove(_attester);
26+
}
27+
}

l1-contracts/test/staking/base.t.sol

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.27;
3+
4+
import {TestBase} from "@test/base/Base.sol";
5+
6+
import {StakingCheater} from "./StakingCheater.sol";
7+
import {TestERC20} from "@aztec/mock/TestERC20.sol";
8+
9+
contract StakingBase is TestBase {
10+
StakingCheater internal staking;
11+
TestERC20 internal stakingAsset;
12+
13+
uint256 internal constant MINIMUM_STAKE = 100e18;
14+
15+
address internal constant PROPOSER = address(bytes20("PROPOSER"));
16+
address internal constant ATTESTER = address(bytes20("ATTESTER"));
17+
address internal constant WITHDRAWER = address(bytes20("WITHDRAWER"));
18+
address internal constant RECIPIENT = address(bytes20("RECIPIENT"));
19+
address internal constant SLASHER = address(bytes20("SLASHER"));
20+
21+
function setUp() public virtual {
22+
stakingAsset = new TestERC20();
23+
staking = new StakingCheater(SLASHER, stakingAsset, MINIMUM_STAKE);
24+
}
25+
}

0 commit comments

Comments
 (0)