Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: standalone ssd #10317

Merged
merged 3 commits into from
Dec 3, 2024
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
13 changes: 8 additions & 5 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ struct SubmitEpochRootProofInterimValues {
uint256 endBlockNumber;
Epoch epochToProve;
Epoch startEpoch;
bool isFeeCanonical;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this into the interim struct since I could not run coverage due to the stack :)

bool isRewardDistributorCanonical;
}

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

// @note Only if the rollup is the canonical will it be able to meaningfully claim fees
// Otherwise, the fees are unbacked #7938.
bool isFeeCanonical = address(this) == FEE_JUICE_PORTAL.canonicalRollup();
bool isRewardDistributorCanonical = address(this) == REWARD_DISTRIBUTOR.canonicalRollup();
interimValues.isFeeCanonical = address(this) == FEE_JUICE_PORTAL.canonicalRollup();
interimValues.isRewardDistributorCanonical =
address(this) == REWARD_DISTRIBUTOR.canonicalRollup();

uint256 totalProverReward = 0;
uint256 totalBurn = 0;

if (isFeeCanonical || isRewardDistributorCanonical) {
if (interimValues.isFeeCanonical || interimValues.isRewardDistributorCanonical) {
for (uint256 i = 0; i < _args.epochSize; i++) {
address coinbase = address(uint160(uint256(publicInputs[9 + i * 2])));
uint256 reward = 0;
uint256 toProver = 0;
uint256 burn = 0;

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

if (isRewardDistributorCanonical) {
if (interimValues.isRewardDistributorCanonical) {
reward += REWARD_DISTRIBUTOR.claim(address(this));
}

Expand Down
57 changes: 57 additions & 0 deletions l1-contracts/src/core/interfaces/IStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.27;

import {Timestamp} from "@aztec/core/libraries/TimeMath.sol";

// None -> Does not exist in our setup
// Validating -> Participating as validator
// Living -> Not participating as validator, but have funds in setup,
// hit if slashes and going below the minimum
// Exiting -> In the process of exiting the system
enum Status {
NONE,
VALIDATING,
LIVING,
EXITING
}

struct ValidatorInfo {
uint256 stake;
address withdrawer;
address proposer;
Status status;
}

struct OperatorInfo {
address proposer;
address attester;
}

struct Exit {
Timestamp exitableAt;
address recipient;
}

interface IStaking {
event Deposit(
address indexed attester, address indexed proposer, address indexed withdrawer, uint256 amount
);
event WithdrawInitiated(address indexed attester, address indexed recipient, uint256 amount);
event WithdrawFinalised(address indexed attester, address indexed recipient, uint256 amount);
event Slashed(address indexed attester, uint256 amount);

function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount)
external;
function initiateWithdraw(address _attester, address _recipient) external returns (bool);
function finaliseWithdraw(address _attester) external;
function slash(address _attester, uint256 _amount) external;

function getInfo(address _attester) external view returns (ValidatorInfo memory);
function getExit(address _attester) external view returns (Exit memory);
function getActiveAttesterCount() external view returns (uint256);
function getAttesterAtIndex(uint256 _index) external view returns (address);
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);
}
13 changes: 13 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ library Errors {
error Leonidas__InsufficientAttestations(uint256 minimumNeeded, uint256 provided); // 0xbf1ca4cb
error Leonidas__InsufficientAttestationsProvided(uint256 minimumNeeded, uint256 provided); // 0xb3a697c2

// Staking
error Staking__AlreadyActive(address attester); // 0x5e206fa4
error Staking__AlreadyRegistered(address); // 0x18047699
error Staking__CannotSlashExitedStake(address); // 0x45bf4940
error Staking__FailedToRemove(address); // 0xa7d7baab
error Staking__InsufficientStake(uint256, uint256); // 0x903aee24
error Staking__NoOneToSlash(address); // 0x7e2f7f1c
error Staking__NotExiting(address); // 0xef566ee0
error Staking__NotSlasher(address, address); // 0x23a6f432
error Staking__NotWithdrawer(address, address); // 0x8e668e5d
error Staking__NothingToExit(address); // 0xd2aac9b6
error Staking__WithdrawalNotUnlockedYet(Timestamp, Timestamp); // 0x88e1826c

// Fee Juice Portal
error FeeJuicePortal__AlreadyInitialized(); // 0xc7a172fe
error FeeJuicePortal__InvalidInitialization(); // 0xfd9b3208
Expand Down
181 changes: 181 additions & 0 deletions l1-contracts/src/core/staking/Staking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.27;

import {
IStaking, ValidatorInfo, Exit, Status, OperatorInfo
} from "@aztec/core/interfaces/IStaking.sol";
import {Errors} from "@aztec/core/libraries/Errors.sol";
import {Timestamp} from "@aztec/core/libraries/TimeMath.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);

address public immutable SLASHER;
IERC20 public immutable STAKING_ASSET;
uint256 public immutable MINIMUM_STAKE;

// address <=> index
EnumerableSet.AddressSet internal attesters;

mapping(address attester => ValidatorInfo) internal info;
mapping(address attester => Exit) internal exits;

constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) {
SLASHER = _slasher;
STAKING_ASSET = _stakingAsset;
MINIMUM_STAKE = _minimumStake;
}

function finaliseWithdraw(address _attester) external override(IStaking) {
ValidatorInfo storage validator = info[_attester];
require(validator.status == Status.EXITING, Errors.Staking__NotExiting(_attester));

Exit storage exit = 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 exits[_attester];
delete 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 == SLASHER, Errors.Staking__NotSlasher(SLASHER, msg.sender));

ValidatorInfo storage validator = 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
&& 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(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 info[_attester];
}

function getProposerForAttester(address _attester)
external
view
override(IStaking)
returns (address)
{
return info[_attester].proposer;
}

function getExit(address _attester) external view override(IStaking) returns (Exit memory) {
return exits[_attester];
}

function getAttesterAtIndex(uint256 _index) external view override(IStaking) returns (address) {
return attesters.at(_index);
}

function getProposerAtIndex(uint256 _index) external view override(IStaking) returns (address) {
return info[attesters.at(_index)].proposer;
}

function getOperatorAtIndex(uint256 _index)
external
view
override(IStaking)
returns (OperatorInfo memory)
{
address attester = attesters.at(_index);
return OperatorInfo({proposer: 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(info[_attester].status == Status.NONE, Errors.Staking__AlreadyRegistered(_attester));
require(attesters.add(_attester), Errors.Staking__AlreadyActive(_attester));

// If BLS, need to check possession of private key to avoid attacks.

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 = 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(attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester));
}

// Note that the "amount" is not stored here, but reusing the `validators`
// We always exit fully.
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 attesters.length();
}
}
27 changes: 27 additions & 0 deletions l1-contracts/test/staking/StakingCheater.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
// 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";

contract StakingCheater is Staking {
using EnumerableSet for EnumerableSet.AddressSet;

constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake)
Staking(_slasher, _stakingAsset, _minimumStake)
{}

function cheat__SetStatus(address _attester, Status _status) external {
info[_attester].status = _status;
}

function cheat__AddAttester(address _attester) external {
attesters.add(_attester);
}

function cheat__RemoveAttester(address _attester) external {
attesters.remove(_attester);
}
}
25 changes: 25 additions & 0 deletions l1-contracts/test/staking/base.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.27;

import {TestBase} from "@test/base/Base.sol";

import {StakingCheater} from "./StakingCheater.sol";
import {TestERC20} from "@aztec/mock/TestERC20.sol";

contract StakingBase is TestBase {
StakingCheater internal staking;
TestERC20 internal stakingAsset;

uint256 internal constant MINIMUM_STAKE = 100e18;

address internal constant PROPOSER = address(bytes20("PROPOSER"));
address internal constant ATTESTER = address(bytes20("ATTESTER"));
address internal constant WITHDRAWER = address(bytes20("WITHDRAWER"));
address internal constant RECIPIENT = address(bytes20("RECIPIENT"));
address internal constant SLASHER = address(bytes20("SLASHER"));

function setUp() public virtual {
stakingAsset = new TestERC20();
staking = new StakingCheater(SLASHER, stakingAsset, MINIMUM_STAKE);
}
}
Loading
Loading