Skip to content

Commit

Permalink
feat: standalone ssd (#10317)
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind authored Dec 3, 2024
1 parent 80fad45 commit c324781
Show file tree
Hide file tree
Showing 15 changed files with 1,022 additions and 5 deletions.
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;
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

0 comments on commit c324781

Please sign in to comment.