From f57adcef12f8105361f13c7da98386a694d89de5 Mon Sep 17 00:00:00 2001 From: LHerskind <16536249+LHerskind@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:25:27 +0000 Subject: [PATCH] feat!: staking library --- l1-contracts/src/core/ValidatorSelection.sol | 142 ++++++++++--- l1-contracts/src/core/interfaces/IStaking.sol | 11 +- .../src/core/libraries/staking/StakingLib.sol | 145 +++++++++++++ l1-contracts/src/core/staking/Staking.sol | 193 ------------------ l1-contracts/test/base/Base.sol | 11 +- .../scenario/slashing/Slashing.t.sol | 4 +- l1-contracts/test/staking/StakingCheater.sol | 90 +++++++- l1-contracts/test/staking/base.t.sol | 2 +- l1-contracts/test/staking/deposit.t.sol | 2 +- .../test/staking/finaliseWithdraw.t.sol | 9 +- l1-contracts/test/staking/getters.t.sol | 2 +- .../test/staking/initiateWithdraw.t.sol | 9 +- l1-contracts/test/staking/slash.t.sol | 5 +- .../ValidatorSelection.t.sol | 6 +- .../cli/src/cmds/infrastructure/sequencers.ts | 2 +- .../cli/src/cmds/l1/update_l1_validators.ts | 2 +- .../end-to-end/src/e2e_p2p/slashing.test.ts | 8 +- yarn-project/ethereum/src/contracts/rollup.ts | 8 +- .../ethereum/src/deploy_l1_contracts.ts | 6 + .../scripts/generate-artifacts.sh | 1 + 20 files changed, 392 insertions(+), 266 deletions(-) create mode 100644 l1-contracts/src/core/libraries/staking/StakingLib.sol delete mode 100644 l1-contracts/src/core/staking/Staking.sol diff --git a/l1-contracts/src/core/ValidatorSelection.sol b/l1-contracts/src/core/ValidatorSelection.sol index 1d065cfaf261..9dc6b1726529 100644 --- a/l1-contracts/src/core/ValidatorSelection.sol +++ b/l1-contracts/src/core/ValidatorSelection.sol @@ -2,6 +2,7 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; +import {IStaking, ValidatorInfo, Exit, OperatorInfo} from "@aztec/core/interfaces/IStaking.sol"; import { IValidatorSelection, EpochData, @@ -10,13 +11,13 @@ import { import {Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {StakingLib} from "@aztec/core/libraries/staking/StakingLib.sol"; import { Timestamp, Slot, Epoch, SlotLib, EpochLib, TimeLib } from "@aztec/core/libraries/TimeLib.sol"; import {ValidatorSelectionLib} from "@aztec/core/libraries/ValidatorSelectionLib/ValidatorSelectionLib.sol"; -import {Staking} from "@aztec/core/staking/Staking.sol"; - +import {Slasher} from "@aztec/core/staking/Slasher.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; @@ -27,7 +28,7 @@ import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; * It is a reference implementation, it is not optimized for gas. * */ -contract ValidatorSelection is Staking, IValidatorSelection { +contract ValidatorSelection is IValidatorSelection, IStaking { using EnumerableSet for EnumerableSet.AddressSet; using SlotLib for Slot; @@ -50,10 +51,45 @@ contract ValidatorSelection is Staking, IValidatorSelection { uint256 _slotDuration, uint256 _epochDuration, uint256 _targetCommitteeSize - ) Staking(_stakingAsset, _minimumStake, _slashingQuorum, _roundSize) { + ) { TARGET_COMMITTEE_SIZE = _targetCommitteeSize; TimeLib.initialize(block.timestamp, _slotDuration, _epochDuration); + + Timestamp exitDelay = Timestamp.wrap(60 * 60 * 24); + Slasher slasher = new Slasher(_slashingQuorum, _roundSize); + StakingLib.initialize(_stakingAsset, _minimumStake, exitDelay, address(slasher)); + } + + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) + external + override(IStaking) + { + setupEpoch(); + require( + _attester != address(0) && _proposer != address(0), + Errors.ValidatorSelection__InvalidDeposit(_attester, _proposer) + ); + StakingLib.deposit(_attester, _proposer, _withdrawer, _amount); + } + + function initiateWithdraw(address _attester, address _recipient) + external + override(IStaking) + returns (bool) + { + // @note The attester might be chosen for the epoch, so the delay must be long enough + // to allow for that. + setupEpoch(); + return StakingLib.initiateWithdraw(_attester, _recipient); + } + + function finaliseWithdraw(address _attester) external override(IStaking) { + StakingLib.finaliseWithdraw(_attester); + } + + function slash(address _attester, uint256 _amount) external override(IStaking) { + StakingLib.slash(_attester, _amount); } function getGenesisTime() external view override(IValidatorSelection) returns (Timestamp) { @@ -68,6 +104,67 @@ contract ValidatorSelection is Staking, IValidatorSelection { return TimeLib.getStorage().epochDuration; } + function getSlasher() external view override(IStaking) returns (address) { + return StakingLib.getStorage().slasher; + } + + function getStakingAsset() external view override(IStaking) returns (IERC20) { + return StakingLib.getStorage().stakingAsset; + } + + function getMinimumStake() external view override(IStaking) returns (uint256) { + return StakingLib.getStorage().minimumStake; + } + + function getExitDelay() external view override(IStaking) returns (Timestamp) { + return StakingLib.getStorage().exitDelay; + } + + function getActiveAttesterCount() external view override(IStaking) returns (uint256) { + return StakingLib.getStorage().attesters.length(); + } + + function getProposerForAttester(address _attester) + external + view + override(IStaking) + returns (address) + { + return StakingLib.getStorage().info[_attester].proposer; + } + + function getAttesterAtIndex(uint256 _index) external view override(IStaking) returns (address) { + return StakingLib.getStorage().attesters.at(_index); + } + + function getProposerAtIndex(uint256 _index) external view override(IStaking) returns (address) { + return StakingLib.getStorage().info[StakingLib.getStorage().attesters.at(_index)].proposer; + } + + function getInfo(address _attester) + external + view + override(IStaking) + returns (ValidatorInfo memory) + { + return StakingLib.getStorage().info[_attester]; + } + + function getExit(address _attester) external view override(IStaking) returns (Exit memory) { + return StakingLib.getStorage().exits[_attester]; + } + + function getOperatorAtIndex(uint256 _index) + external + view + override(IStaking) + returns (OperatorInfo memory) + { + address attester = StakingLib.getStorage().attesters.at(_index); + return + OperatorInfo({proposer: StakingLib.getStorage().info[attester].proposer, attester: attester}); + } + /** * @notice Get the validator set for a given epoch * @@ -97,7 +194,7 @@ contract ValidatorSelection is Staking, IValidatorSelection { returns (address[] memory) { return ValidatorSelectionLib.getCommitteeAt( - validatorSelectionStore, stakingStore, getCurrentEpoch(), TARGET_COMMITTEE_SIZE + validatorSelectionStore, StakingLib.getStorage(), getCurrentEpoch(), TARGET_COMMITTEE_SIZE ); } @@ -115,7 +212,7 @@ contract ValidatorSelection is Staking, IValidatorSelection { returns (address[] memory) { return ValidatorSelectionLib.getCommitteeAt( - validatorSelectionStore, stakingStore, getEpochAt(_ts), TARGET_COMMITTEE_SIZE + validatorSelectionStore, StakingLib.getStorage(), getEpochAt(_ts), TARGET_COMMITTEE_SIZE ); } @@ -144,29 +241,6 @@ contract ValidatorSelection is Staking, IValidatorSelection { return ValidatorSelectionLib.getSampleSeed(validatorSelectionStore, getCurrentEpoch()); } - function initiateWithdraw(address _attester, address _recipient) - public - override(Staking) - returns (bool) - { - // @note The attester might be chosen for the epoch, so the delay must be long enough - // to allow for that. - setupEpoch(); - return super.initiateWithdraw(_attester, _recipient); - } - - function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) - public - override(Staking) - { - setupEpoch(); - require( - _attester != address(0) && _proposer != address(0), - Errors.ValidatorSelection__InvalidDeposit(_attester, _proposer) - ); - super.deposit(_attester, _proposer, _withdrawer, _amount); - } - /** * @notice Performs a setup of an epoch if needed. The setup will * - Sample the validator set for the epoch @@ -185,7 +259,7 @@ contract ValidatorSelection is Staking, IValidatorSelection { epoch.sampleSeed = ValidatorSelectionLib.getSampleSeed(validatorSelectionStore, epochNumber); epoch.nextSeed = validatorSelectionStore.lastSeed = _computeNextSeed(epochNumber); epoch.committee = ValidatorSelectionLib.sampleValidators( - stakingStore, epoch.sampleSeed, TARGET_COMMITTEE_SIZE + StakingLib.getStorage(), epoch.sampleSeed, TARGET_COMMITTEE_SIZE ); } } @@ -198,7 +272,7 @@ contract ValidatorSelection is Staking, IValidatorSelection { * @return The validator set */ function getAttesters() public view override(IValidatorSelection) returns (address[] memory) { - return stakingStore.attesters.values(); + return StakingLib.getStorage().attesters.values(); } /** @@ -271,7 +345,7 @@ contract ValidatorSelection is Staking, IValidatorSelection { Slot slot = getSlotAt(_ts); Epoch epochNumber = getEpochAtSlot(slot); return ValidatorSelectionLib.getProposerAt( - validatorSelectionStore, stakingStore, slot, epochNumber, TARGET_COMMITTEE_SIZE + validatorSelectionStore, StakingLib.getStorage(), slot, epochNumber, TARGET_COMMITTEE_SIZE ); } @@ -325,7 +399,7 @@ contract ValidatorSelection is Staking, IValidatorSelection { Errors.ValidatorSelection__InvalidDeposit(_attester, _proposer) ); - super.deposit(_attester, _proposer, _withdrawer, _amount); + StakingLib.deposit(_attester, _proposer, _withdrawer, _amount); } /** @@ -353,7 +427,7 @@ contract ValidatorSelection is Staking, IValidatorSelection { Epoch epochNumber = getEpochAtSlot(_slot); ValidatorSelectionLib.validateValidatorSelection( validatorSelectionStore, - stakingStore, + StakingLib.getStorage(), _slot, epochNumber, _signatures, diff --git a/l1-contracts/src/core/interfaces/IStaking.sol b/l1-contracts/src/core/interfaces/IStaking.sol index 63363af287bd..a3a7414f605b 100644 --- a/l1-contracts/src/core/interfaces/IStaking.sol +++ b/l1-contracts/src/core/interfaces/IStaking.sol @@ -2,7 +2,8 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; // None -> Does not exist in our setup @@ -35,6 +36,10 @@ struct Exit { } struct StakingStorage { + IERC20 stakingAsset; + address slasher; + uint256 minimumStake; + Timestamp exitDelay; EnumerableSet.AddressSet attesters; mapping(address attester => ValidatorInfo) info; mapping(address attester => Exit) exits; @@ -61,4 +66,8 @@ interface IStaking { function getProposerAtIndex(uint256 _index) external view returns (address); function getProposerForAttester(address _attester) external view returns (address); function getOperatorAtIndex(uint256 _index) external view returns (OperatorInfo memory); + function getSlasher() external view returns (address); + function getStakingAsset() external view returns (IERC20); + function getMinimumStake() external view returns (uint256); + function getExitDelay() external view returns (Timestamp); } diff --git a/l1-contracts/src/core/libraries/staking/StakingLib.sol b/l1-contracts/src/core/libraries/staking/StakingLib.sol new file mode 100644 index 000000000000..0071f09b6269 --- /dev/null +++ b/l1-contracts/src/core/libraries/staking/StakingLib.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import { + Status, + ValidatorInfo, + Exit, + Timestamp, + StakingStorage +} from "@aztec/core/interfaces/IStaking.sol"; +import {IStaking} from "@aztec/core/interfaces/IStaking.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; + +library StakingLib { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + bytes32 private constant STAKING_SLOT = keccak256("aztec.core.staking.storage"); + + function initialize( + IERC20 _stakingAsset, + uint256 _minimumStake, + Timestamp _exitDelay, + address _slasher + ) external { + StakingStorage storage store = getStorage(); + store.stakingAsset = _stakingAsset; + store.minimumStake = _minimumStake; + store.exitDelay = _exitDelay; + store.slasher = _slasher; + } + + function finaliseWithdraw(address _attester) external { + StakingStorage storage store = getStorage(); + ValidatorInfo storage validator = store.info[_attester]; + require(validator.status == Status.EXITING, Errors.Staking__NotExiting(_attester)); + + Exit storage exit = store.exits[_attester]; + require( + exit.exitableAt <= Timestamp.wrap(block.timestamp), + Errors.Staking__WithdrawalNotUnlockedYet(Timestamp.wrap(block.timestamp), exit.exitableAt) + ); + + uint256 amount = validator.stake; + address recipient = exit.recipient; + + delete store.exits[_attester]; + delete store.info[_attester]; + + store.stakingAsset.transfer(recipient, amount); + + emit IStaking.WithdrawFinalised(_attester, recipient, amount); + } + + function slash(address _attester, uint256 _amount) external { + StakingStorage storage store = getStorage(); + require(msg.sender == store.slasher, Errors.Staking__NotSlasher(store.slasher, msg.sender)); + + ValidatorInfo storage validator = store.info[_attester]; + require(validator.status != Status.NONE, Errors.Staking__NoOneToSlash(_attester)); + + // There is a special, case, if exiting and past the limit, it is untouchable! + require( + !( + validator.status == Status.EXITING + && store.exits[_attester].exitableAt <= Timestamp.wrap(block.timestamp) + ), + Errors.Staking__CannotSlashExitedStake(_attester) + ); + validator.stake -= _amount; + + // If the attester was validating AND is slashed below the MINIMUM_STAKE we update him to LIVING + // When LIVING, he can only start exiting, we don't "really" exit him, because that cost + // gas and cost edge cases around recipient, so lets just avoid that. + if (validator.status == Status.VALIDATING && validator.stake < store.minimumStake) { + require(store.attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); + validator.status = Status.LIVING; + } + + emit IStaking.Slashed(_attester, _amount); + } + + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) + external + { + StakingStorage storage store = getStorage(); + require( + _amount >= store.minimumStake, Errors.Staking__InsufficientStake(_amount, store.minimumStake) + ); + store.stakingAsset.transferFrom(msg.sender, address(this), _amount); + require( + store.info[_attester].status == Status.NONE, Errors.Staking__AlreadyRegistered(_attester) + ); + require(store.attesters.add(_attester), Errors.Staking__AlreadyActive(_attester)); + + // If BLS, need to check possession of private key to avoid attacks. + + store.info[_attester] = ValidatorInfo({ + stake: _amount, + withdrawer: _withdrawer, + proposer: _proposer, + status: Status.VALIDATING + }); + + emit IStaking.Deposit(_attester, _proposer, _withdrawer, _amount); + } + + function initiateWithdraw(address _attester, address _recipient) external returns (bool) { + StakingStorage storage store = getStorage(); + ValidatorInfo storage validator = store.info[_attester]; + + require( + msg.sender == validator.withdrawer, + Errors.Staking__NotWithdrawer(validator.withdrawer, msg.sender) + ); + require( + validator.status == Status.VALIDATING || validator.status == Status.LIVING, + Errors.Staking__NothingToExit(_attester) + ); + if (validator.status == Status.VALIDATING) { + require(store.attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); + } + + // Note that the "amount" is not stored here, but reusing the `validators` + // We always exit fully. + store.exits[_attester] = + Exit({exitableAt: Timestamp.wrap(block.timestamp) + store.exitDelay, recipient: _recipient}); + validator.status = Status.EXITING; + + emit IStaking.WithdrawInitiated(_attester, _recipient, validator.stake); + + return true; + } + + function getStorage() public pure returns (StakingStorage storage storageStruct) { + bytes32 position = STAKING_SLOT; + assembly { + storageStruct.slot := position + } + } +} diff --git a/l1-contracts/src/core/staking/Staking.sol b/l1-contracts/src/core/staking/Staking.sol deleted file mode 100644 index 6f6edffebca7..000000000000 --- a/l1-contracts/src/core/staking/Staking.sol +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.27; - -import { - IStaking, - ValidatorInfo, - Exit, - Status, - OperatorInfo, - StakingStorage -} from "@aztec/core/interfaces/IStaking.sol"; -import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {Timestamp} from "@aztec/core/libraries/TimeLib.sol"; -import {Slasher} from "@aztec/core/staking/Slasher.sol"; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; -import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; - -contract Staking is IStaking { - using SafeERC20 for IERC20; - using EnumerableSet for EnumerableSet.AddressSet; - - // Constant pulled out of the ass - Timestamp public constant EXIT_DELAY = Timestamp.wrap(60 * 60 * 24); - - Slasher public immutable SLASHER; - IERC20 public immutable STAKING_ASSET; - uint256 public immutable MINIMUM_STAKE; - - StakingStorage internal stakingStore; - - constructor( - IERC20 _stakingAsset, - uint256 _minimumStake, - uint256 _slashingQuorum, - uint256 _roundSize - ) { - SLASHER = new Slasher(_slashingQuorum, _roundSize); - STAKING_ASSET = _stakingAsset; - MINIMUM_STAKE = _minimumStake; - } - - function finaliseWithdraw(address _attester) external override(IStaking) { - ValidatorInfo storage validator = stakingStore.info[_attester]; - require(validator.status == Status.EXITING, Errors.Staking__NotExiting(_attester)); - - Exit storage exit = stakingStore.exits[_attester]; - require( - exit.exitableAt <= Timestamp.wrap(block.timestamp), - Errors.Staking__WithdrawalNotUnlockedYet(Timestamp.wrap(block.timestamp), exit.exitableAt) - ); - - uint256 amount = validator.stake; - address recipient = exit.recipient; - - delete stakingStore.exits[_attester]; - delete stakingStore.info[_attester]; - - STAKING_ASSET.transfer(recipient, amount); - - emit IStaking.WithdrawFinalised(_attester, recipient, amount); - } - - function slash(address _attester, uint256 _amount) external override(IStaking) { - require( - msg.sender == address(SLASHER), Errors.Staking__NotSlasher(address(SLASHER), msg.sender) - ); - - ValidatorInfo storage validator = stakingStore.info[_attester]; - require(validator.status != Status.NONE, Errors.Staking__NoOneToSlash(_attester)); - - // There is a special, case, if exiting and past the limit, it is untouchable! - require( - !( - validator.status == Status.EXITING - && stakingStore.exits[_attester].exitableAt <= Timestamp.wrap(block.timestamp) - ), - Errors.Staking__CannotSlashExitedStake(_attester) - ); - validator.stake -= _amount; - - // If the attester was validating AND is slashed below the MINIMUM_STAKE we update him to LIVING - // When LIVING, he can only start exiting, we don't "really" exit him, because that cost - // gas and cost edge cases around recipient, so lets just avoid that. - if (validator.status == Status.VALIDATING && validator.stake < MINIMUM_STAKE) { - require(stakingStore.attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); - validator.status = Status.LIVING; - } - - emit Slashed(_attester, _amount); - } - - function getInfo(address _attester) - external - view - override(IStaking) - returns (ValidatorInfo memory) - { - return stakingStore.info[_attester]; - } - - function getExit(address _attester) external view override(IStaking) returns (Exit memory) { - return stakingStore.exits[_attester]; - } - - function getOperatorAtIndex(uint256 _index) - external - view - override(IStaking) - returns (OperatorInfo memory) - { - address attester = stakingStore.attesters.at(_index); - return OperatorInfo({proposer: stakingStore.info[attester].proposer, attester: attester}); - } - - function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) - public - virtual - override(IStaking) - { - require(_amount >= MINIMUM_STAKE, Errors.Staking__InsufficientStake(_amount, MINIMUM_STAKE)); - STAKING_ASSET.transferFrom(msg.sender, address(this), _amount); - require( - stakingStore.info[_attester].status == Status.NONE, - Errors.Staking__AlreadyRegistered(_attester) - ); - require(stakingStore.attesters.add(_attester), Errors.Staking__AlreadyActive(_attester)); - - // If BLS, need to check possession of private key to avoid attacks. - - stakingStore.info[_attester] = ValidatorInfo({ - stake: _amount, - withdrawer: _withdrawer, - proposer: _proposer, - status: Status.VALIDATING - }); - - emit IStaking.Deposit(_attester, _proposer, _withdrawer, _amount); - } - - function initiateWithdraw(address _attester, address _recipient) - public - virtual - override(IStaking) - returns (bool) - { - ValidatorInfo storage validator = stakingStore.info[_attester]; - - require( - msg.sender == validator.withdrawer, - Errors.Staking__NotWithdrawer(validator.withdrawer, msg.sender) - ); - require( - validator.status == Status.VALIDATING || validator.status == Status.LIVING, - Errors.Staking__NothingToExit(_attester) - ); - if (validator.status == Status.VALIDATING) { - require(stakingStore.attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); - } - - // Note that the "amount" is not stored here, but reusing the `validators` - // We always exit fully. - stakingStore.exits[_attester] = - Exit({exitableAt: Timestamp.wrap(block.timestamp) + EXIT_DELAY, recipient: _recipient}); - validator.status = Status.EXITING; - - emit IStaking.WithdrawInitiated(_attester, _recipient, validator.stake); - - return true; - } - - function getActiveAttesterCount() public view override(IStaking) returns (uint256) { - return stakingStore.attesters.length(); - } - - function getProposerForAttester(address _attester) - public - view - override(IStaking) - returns (address) - { - return stakingStore.info[_attester].proposer; - } - - function getAttesterAtIndex(uint256 _index) public view override(IStaking) returns (address) { - return stakingStore.attesters.at(_index); - } - - function getProposerAtIndex(uint256 _index) public view override(IStaking) returns (address) { - return stakingStore.info[stakingStore.attesters.at(_index)].proposer; - } -} diff --git a/l1-contracts/test/base/Base.sol b/l1-contracts/test/base/Base.sol index f01da01e64eb..52d175faeabd 100644 --- a/l1-contracts/test/base/Base.sol +++ b/l1-contracts/test/base/Base.sol @@ -3,10 +3,12 @@ pragma solidity >=0.8.27; import {Timestamp, Slot, Epoch, SlotLib, EpochLib} from "@aztec/core/libraries/TimeLib.sol"; import {Test} from "forge-std/Test.sol"; +import {stdStorage, StdStorage} from "forge-std/Test.sol"; contract TestBase is Test { using SlotLib for Slot; using EpochLib for Epoch; + using stdStorage for StdStorage; function assertGt(Timestamp a, Timestamp b) internal { if (a <= b) { @@ -223,8 +225,11 @@ contract TestBase is Test { // Blobs function skipBlobCheck(address rollup) internal { - // 9 is the slot of checkBlob. We force it to be false (=0): - // Slot number can be checked by running forge inspect src/core/Rollup.sol:Rollup storage - vm.store(rollup, bytes32(uint256(9)), 0); + // For not entirely clear reasons, the checked_write and find in stdStore breaks with + // under/overflow errors if using them. But we can still use them to find the slot + // and looking in the logs. Interesting. + // Alternative, run forge inspect src/core/Rollup.sol:Rollup storageLayout --pretty + // uint256 slot = stdstore.target(address(rollup)).sig("checkBlob()").find(); + vm.store(address(rollup), bytes32(uint256(5)), bytes32(uint256(0))); } } diff --git a/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol b/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol index dd0476d7c0fe..364cd1083c32 100644 --- a/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol +++ b/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol @@ -18,8 +18,6 @@ import {Slasher, IPayload} from "@aztec/core/staking/Slasher.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; -import {Errors} from "@aztec/core/libraries/Errors.sol"; - import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; import {SlashingProposer} from "@aztec/core/staking/SlashingProposer.sol"; @@ -72,7 +70,7 @@ contract SlashingScenario is TestBase { slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE }) }); - slasher = rollup.SLASHER(); + slasher = Slasher(rollup.getSlasher()); slashingProposer = slasher.PROPOSER(); slashFactory = new SlashFactory(IValidatorSelection(address(rollup))); diff --git a/l1-contracts/test/staking/StakingCheater.sol b/l1-contracts/test/staking/StakingCheater.sol index a886a3d2f72c..15fd16795eca 100644 --- a/l1-contracts/test/staking/StakingCheater.sol +++ b/l1-contracts/test/staking/StakingCheater.sol @@ -2,11 +2,17 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {Staking, Status} from "@aztec/core/staking/Staking.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; +import { + IStaking, ValidatorInfo, Exit, OperatorInfo, Status +} from "@aztec/core/interfaces/IStaking.sol"; -contract StakingCheater is Staking { +import {Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import {StakingLib} from "@aztec/core/libraries/staking/StakingLib.sol"; +import {Slasher} from "@aztec/core/staking/Slasher.sol"; + +contract StakingCheater is IStaking { using EnumerableSet for EnumerableSet.AddressSet; constructor( @@ -14,17 +20,89 @@ contract StakingCheater is Staking { uint256 _minimumStake, uint256 _slashingQuorum, uint256 _roundSize - ) Staking(_stakingAsset, _minimumStake, _slashingQuorum, _roundSize) {} + ) { + Timestamp exitDelay = Timestamp.wrap(60 * 60 * 24); + Slasher slasher = new Slasher(_slashingQuorum, _roundSize); + StakingLib.initialize(_stakingAsset, _minimumStake, exitDelay, address(slasher)); + } + + function finaliseWithdraw(address _attester) external { + StakingLib.finaliseWithdraw(_attester); + } + + function slash(address _attester, uint256 _amount) external { + StakingLib.slash(_attester, _amount); + } + + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) + external + { + StakingLib.deposit(_attester, _proposer, _withdrawer, _amount); + } + + function initiateWithdraw(address _attester, address _recipient) external returns (bool) { + return StakingLib.initiateWithdraw(_attester, _recipient); + } + + function getActiveAttesterCount() external view returns (uint256) { + return StakingLib.getStorage().attesters.length(); + } + + function getProposerForAttester(address _attester) external view returns (address) { + return StakingLib.getStorage().info[_attester].proposer; + } + + function getAttesterAtIndex(uint256 _index) external view returns (address) { + return StakingLib.getStorage().attesters.at(_index); + } + + function getProposerAtIndex(uint256 _index) external view returns (address) { + return StakingLib.getStorage().info[StakingLib.getStorage().attesters.at(_index)].proposer; + } + + function getInfo(address _attester) external view returns (ValidatorInfo memory) { + return StakingLib.getStorage().info[_attester]; + } + + function getExit(address _attester) external view returns (Exit memory) { + return StakingLib.getStorage().exits[_attester]; + } + + function getOperatorAtIndex(uint256 _index) external view returns (OperatorInfo memory) { + address attester = StakingLib.getStorage().attesters.at(_index); + return + OperatorInfo({proposer: StakingLib.getStorage().info[attester].proposer, attester: attester}); + } + + function getSlasher() external view returns (address) { + return StakingLib.getStorage().slasher; + } + + function getStakingAsset() external view returns (IERC20) { + return StakingLib.getStorage().stakingAsset; + } + + function getMinimumStake() external view returns (uint256) { + return StakingLib.getStorage().minimumStake; + } + + function getExitDelay() external view returns (Timestamp) { + return StakingLib.getStorage().exitDelay; + } + + ////////////// + // CHEATING // + ////////////// function cheat__SetStatus(address _attester, Status _status) external { - stakingStore.info[_attester].status = _status; + StakingLib.getStorage().info[_attester].status = _status; } function cheat__AddAttester(address _attester) external { - stakingStore.attesters.add(_attester); + StakingLib.getStorage().attesters.add(_attester); } function cheat__RemoveAttester(address _attester) external { - stakingStore.attesters.remove(_attester); + StakingLib.getStorage().attesters.remove(_attester); } } diff --git a/l1-contracts/test/staking/base.t.sol b/l1-contracts/test/staking/base.t.sol index 441d418d244f..18fc8cddb105 100644 --- a/l1-contracts/test/staking/base.t.sol +++ b/l1-contracts/test/staking/base.t.sol @@ -23,6 +23,6 @@ contract StakingBase is TestBase { stakingAsset = new TestERC20("test", "TEST", address(this)); staking = new StakingCheater(stakingAsset, MINIMUM_STAKE, 1, 1); - SLASHER = address(staking.SLASHER()); + SLASHER = staking.getSlasher(); } } diff --git a/l1-contracts/test/staking/deposit.t.sol b/l1-contracts/test/staking/deposit.t.sol index 900d2a58372c..ac2f51be3ec4 100644 --- a/l1-contracts/test/staking/deposit.t.sol +++ b/l1-contracts/test/staking/deposit.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol"; -import {Staking, IStaking, Status, ValidatorInfo} from "@aztec/core/staking/Staking.sol"; +import {IStaking, Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; contract DepositTest is StakingBase { uint256 internal depositAmount; diff --git a/l1-contracts/test/staking/finaliseWithdraw.t.sol b/l1-contracts/test/staking/finaliseWithdraw.t.sol index dea6053c04d8..1cdde4fcddb6 100644 --- a/l1-contracts/test/staking/finaliseWithdraw.t.sol +++ b/l1-contracts/test/staking/finaliseWithdraw.t.sol @@ -3,8 +3,9 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {IStaking, Status, ValidatorInfo, Exit} from "@aztec/core/staking/Staking.sol"; -import {Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import { + Timestamp, Status, ValidatorInfo, Exit, IStaking +} from "@aztec/core/interfaces/IStaking.sol"; contract FinaliseWithdrawTest is StakingBase { function test_GivenStatusIsNotExiting() external { @@ -44,7 +45,7 @@ contract FinaliseWithdrawTest is StakingBase { abi.encodeWithSelector( Errors.Staking__WithdrawalNotUnlockedYet.selector, Timestamp.wrap(block.timestamp), - Timestamp.wrap(block.timestamp) + staking.EXIT_DELAY() + Timestamp.wrap(block.timestamp) + staking.getExitDelay() ) ); staking.finaliseWithdraw(ATTESTER); @@ -58,7 +59,7 @@ contract FinaliseWithdrawTest is StakingBase { Exit memory exit = staking.getExit(ATTESTER); assertEq(exit.recipient, RECIPIENT); - assertEq(exit.exitableAt, Timestamp.wrap(block.timestamp) + staking.EXIT_DELAY()); + assertEq(exit.exitableAt, Timestamp.wrap(block.timestamp) + staking.getExitDelay()); ValidatorInfo memory info = staking.getInfo(ATTESTER); assertTrue(info.status == Status.EXITING); diff --git a/l1-contracts/test/staking/getters.t.sol b/l1-contracts/test/staking/getters.t.sol index 2497c994d5af..ceb09a1d4b54 100644 --- a/l1-contracts/test/staking/getters.t.sol +++ b/l1-contracts/test/staking/getters.t.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; -import {OperatorInfo} from "@aztec/core/staking/Staking.sol"; +import {OperatorInfo} from "@aztec/core/interfaces/IStaking.sol"; contract GettersTest is StakingBase { function setUp() public override { diff --git a/l1-contracts/test/staking/initiateWithdraw.t.sol b/l1-contracts/test/staking/initiateWithdraw.t.sol index 3883e826ac98..fe69c47c7d11 100644 --- a/l1-contracts/test/staking/initiateWithdraw.t.sol +++ b/l1-contracts/test/staking/initiateWithdraw.t.sol @@ -3,8 +3,9 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {IStaking, Status, ValidatorInfo, Exit} from "@aztec/core/staking/Staking.sol"; -import {Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import { + Timestamp, Status, ValidatorInfo, Exit, IStaking +} from "@aztec/core/interfaces/IStaking.sol"; contract InitiateWithdrawTest is StakingBase { function test_WhenAttesterIsNotRegistered() external { @@ -112,7 +113,7 @@ contract InitiateWithdrawTest is StakingBase { assertEq(stakingAsset.balanceOf(address(staking)), MINIMUM_STAKE); assertEq(stakingAsset.balanceOf(RECIPIENT), 0); exit = staking.getExit(ATTESTER); - assertEq(exit.exitableAt, Timestamp.wrap(block.timestamp) + staking.EXIT_DELAY()); + assertEq(exit.exitableAt, Timestamp.wrap(block.timestamp) + staking.getExitDelay()); assertEq(exit.recipient, RECIPIENT); info = staking.getInfo(ATTESTER); assertTrue(info.status == Status.EXITING); @@ -145,7 +146,7 @@ contract InitiateWithdrawTest is StakingBase { assertEq(stakingAsset.balanceOf(address(staking)), MINIMUM_STAKE); assertEq(stakingAsset.balanceOf(RECIPIENT), 0); exit = staking.getExit(ATTESTER); - assertEq(exit.exitableAt, Timestamp.wrap(block.timestamp) + staking.EXIT_DELAY()); + assertEq(exit.exitableAt, Timestamp.wrap(block.timestamp) + staking.getExitDelay()); assertEq(exit.recipient, RECIPIENT); info = staking.getInfo(ATTESTER); assertTrue(info.status == Status.EXITING); diff --git a/l1-contracts/test/staking/slash.t.sol b/l1-contracts/test/staking/slash.t.sol index fd4ded49803e..d0cfe2e4364a 100644 --- a/l1-contracts/test/staking/slash.t.sol +++ b/l1-contracts/test/staking/slash.t.sol @@ -3,8 +3,9 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {Staking, IStaking, Status, ValidatorInfo, Exit} from "@aztec/core/staking/Staking.sol"; -import {Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import { + IStaking, Status, ValidatorInfo, Exit, Timestamp +} from "@aztec/core/interfaces/IStaking.sol"; contract SlashTest is StakingBase { uint256 internal constant DEPOSIT_AMOUNT = MINIMUM_STAKE + 2; diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 773248000c9e..adcf5ceef4ec 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -126,7 +126,7 @@ contract ValidatorSelectionTest is DecoderBase { slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE }) }); - slasher = rollup.SLASHER(); + slasher = Slasher(rollup.getSlasher()); slashFactory = new SlashFactory(IValidatorSelection(address(rollup))); testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); @@ -310,6 +310,8 @@ contract ValidatorSelectionTest is DecoderBase { skipBlobCheck(address(rollup)); if (_expectRevert && _invalidProposer) { + emit log("We do be reverting?"); + address realProposer = ree.proposer; ree.proposer = address(uint160(uint256(keccak256(abi.encode("invalid", ree.proposer))))); vm.expectRevert( @@ -319,6 +321,8 @@ contract ValidatorSelectionTest is DecoderBase { ); ree.shouldRevert = true; } + + emit log("Time to propose"); vm.prank(ree.proposer); rollup.propose(args, signatures, full.block.body, full.block.blobInputs); diff --git a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts index cf5dbe7bdc10..c54f8c7588e5 100644 --- a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts +++ b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts @@ -66,7 +66,7 @@ export async function sequencers(opts: { log(`Adding ${who} as sequencer`); const stakingAsset = getContract({ - address: await rollup.read.STAKING_ASSET(), + address: await rollup.read.getStakingAsset(), abi: TestERC20Abi, client: walletClient, }); diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts index 38a8dc6a9f2d..c19d74967831 100644 --- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts +++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts @@ -58,7 +58,7 @@ export async function addL1Validator({ }); const stakingAsset = getContract({ - address: await rollup.read.STAKING_ASSET(), + address: await rollup.read.getStakingAsset(), abi: TestERC20Abi, client: walletClient, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts index fcb6cca9c3fc..f830b8fc1b0f 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts @@ -66,7 +66,7 @@ describe('e2e_p2p_slashing', () => { }); const slasherContract = getContract({ - address: getAddress(await rollup.read.SLASHER()), + address: getAddress(await rollup.read.getSlasher()), abi: SlasherAbi, client: t.ctx.deployL1ContractsValues.publicClient, }); @@ -219,11 +219,7 @@ describe('e2e_p2p_slashing', () => { const tx = await slashingProposer.write.executeProposal([sInfo.roundNumber], { account: t.ctx.deployL1ContractsValues.walletClient.account, }); - await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: tx, - }); - - const receipt = await t.ctx.deployL1ContractsValues.publicClient.getTransactionReceipt({ + const receipt = await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({ hash: tx, }); diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index be14a371cd92..45013f3b30a8 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -80,7 +80,7 @@ export class RollupContract { @memoize public async getSlashingProposer() { - const slasherAddress = await this.rollup.read.SLASHER(); + const slasherAddress = await this.rollup.read.getSlasher(); const slasher = getContract({ address: slasherAddress, abi: SlasherAbi, client: this.client }); const proposerAddress = await slasher.read.PROPOSER(); return new SlashingProposerContract(this.client, proposerAddress); @@ -118,11 +118,11 @@ export class RollupContract { @memoize getMinimumStake() { - return this.rollup.read.MINIMUM_STAKE(); + return this.rollup.read.getMinimumStake(); } public async getSlashingProposerAddress() { - const slasherAddress = await this.rollup.read.SLASHER(); + const slasherAddress = await this.rollup.read.getSlasher(); const slasher = getContract({ address: getAddress(slasherAddress.toString()), abi: SlasherAbi, @@ -203,7 +203,7 @@ export class RollupContract { this.rollup.read.FEE_JUICE_PORTAL(), this.rollup.read.REWARD_DISTRIBUTOR(), this.rollup.read.ASSET(), - this.rollup.read.STAKING_ASSET(), + this.rollup.read.getStakingAsset(), ] as const) ).map(EthAddress.fromString); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index ff5c5f92c786..ab3a18ba3adf 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -28,6 +28,8 @@ import { RollupLinkReferences, SlashFactoryAbi, SlashFactoryBytecode, + StakingLibAbi, + StakingLibBytecode, TestERC20Abi, TestERC20Bytecode, ValidatorSelectionLibAbi, @@ -146,6 +148,10 @@ export const l1Artifacts = { contractAbi: ExtRollupLibAbi, contractBytecode: ExtRollupLibBytecode as Hex, }, + StakingLib: { + contractAbi: StakingLibAbi, + contractBytecode: StakingLibBytecode as Hex, + }, }, }, }, diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index 0e105e252e77..802a41ace83f 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -37,6 +37,7 @@ contracts=( "SlashFactory" "Forwarder" "HonkVerifier" + "StakingLib" ) # Combine error ABIs once, removing duplicates by {type, name}.