Skip to content
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
54 changes: 27 additions & 27 deletions l1-contracts/gas_report.md

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions l1-contracts/src/mock/StakingAssetHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.27;

import {Ownable} from "@oz/access/Ownable.sol";

import {IStaking} from "./../core/interfaces/IStaking.sol";
import {IMintableERC20} from "./../governance/interfaces/IMintableERC20.sol";

/**
* @title StakingAssetHandler
* @notice This contract is used as a faucet for creating validators.
*
* It allows for anyone with the `canAddValidator` role to add validators to the rollup,
* caveat being that it controls the number of validators that can be added in a time period.
*
* @dev For example, if minMintInterval is 60*60 and maxDepositsPerMint is 3,
* then *generally* 3 validators can be added every hour.
* NB: it is possible to add 1 validator at the top of the hour, and 2 validators
* at the very end of the hour, then 3 validators at the top of the next hour
* so the maximum "burst" rate is effectively twice the maxDepositsPerMint.
*
* @dev This contract must be a minter of the staking asset.
*
* @dev Only the owner can grant and revoke the `canAddValidator` role, and perform other administrative tasks
* such as setting the rollup, deposit amount, min mint interval, max deposits per mint, and withdrawer.
*
*/
interface IStakingAssetHandler {
event ToppedUp(uint256 _amount);
event ValidatorAdded(
address indexed _attester, address indexed _proposer, address indexed _withdrawer
);
event RollupUpdated(address indexed _rollup);
event DepositAmountUpdated(uint256 _depositAmount);
event IntervalUpdated(uint256 _interval);
event DepositsPerMintUpdated(uint256 _depositsPerMint);
event WithdrawerUpdated(address indexed _withdrawer);
event AddValidatorPermissionGranted(address indexed _address);
event AddValidatorPermissionRevoked(address indexed _address);

error NotCanAddValidator(address _caller);
error NotEnoughTimeSinceLastMint(uint256 _lastMintTimestamp, uint256 _minMintInterval);
error CannotMintZeroAmount();
error MaxDepositsTooLarge(uint256 _depositAmount, uint256 _maxDepositsPerMint);

function addValidator(address _attester, address _proposer) external;
function setRollup(address _rollup) external;
function setDepositAmount(uint256 _amount) external;
function setMintInterval(uint256 _interval) external;
function setDepositsPerMint(uint256 _depositsPerMint) external;
function setWithdrawer(address _withdrawer) external;
function grantAddValidatorPermission(address _address) external;
function revokeAddValidatorPermission(address _address) external;
}

contract StakingAssetHandler is IStakingAssetHandler, Ownable {
IMintableERC20 public immutable STAKING_ASSET;

mapping(address => bool) public canAddValidator;

uint256 public depositAmount;
uint256 public lastMintTimestamp;
uint256 public mintInterval;
uint256 public depositsPerMint;

IStaking public rollup;
address public withdrawer;

modifier onlyCanAddValidator() {
require(canAddValidator[msg.sender], NotCanAddValidator(msg.sender));
_;
}

constructor(
address _owner,
address _stakingAsset,
address _rollup,
address _withdrawer,
uint256 _depositAmount,
uint256 _mintInterval,
uint256 _depositsPerMint,
address[] memory _canAddValidator
) Ownable(_owner) {
require(_depositsPerMint > 0, CannotMintZeroAmount());

STAKING_ASSET = IMintableERC20(_stakingAsset);

rollup = IStaking(_rollup);
emit RollupUpdated(_rollup);

withdrawer = _withdrawer;
emit WithdrawerUpdated(_withdrawer);

depositAmount = _depositAmount;
emit DepositAmountUpdated(_depositAmount);

mintInterval = _mintInterval;
emit IntervalUpdated(_mintInterval);

depositsPerMint = _depositsPerMint;
emit DepositsPerMintUpdated(_depositsPerMint);

for (uint256 i = 0; i < _canAddValidator.length; i++) {
canAddValidator[_canAddValidator[i]] = true;
emit AddValidatorPermissionGranted(_canAddValidator[i]);
}
canAddValidator[_owner] = true;
emit AddValidatorPermissionGranted(_owner);
}

function addValidator(address _attester, address _proposer) external override onlyCanAddValidator {
bool needsToMint = STAKING_ASSET.balanceOf(address(this)) < depositAmount;
bool canMint = block.timestamp - lastMintTimestamp >= mintInterval;

require(!needsToMint || canMint, NotEnoughTimeSinceLastMint(lastMintTimestamp, mintInterval));
if (needsToMint) {
STAKING_ASSET.mint(address(this), depositAmount * depositsPerMint);
lastMintTimestamp = block.timestamp;
emit ToppedUp(depositAmount * depositsPerMint);
}

STAKING_ASSET.approve(address(rollup), depositAmount);
rollup.deposit(_attester, _proposer, withdrawer, depositAmount);
emit ValidatorAdded(_attester, _proposer, withdrawer);
}

function setRollup(address _rollup) external override onlyOwner {
rollup = IStaking(_rollup);
emit RollupUpdated(_rollup);
}

function setDepositAmount(uint256 _amount) external override onlyOwner {
depositAmount = _amount;
emit DepositAmountUpdated(_amount);
}

function setMintInterval(uint256 _interval) external override onlyOwner {
mintInterval = _interval;
emit IntervalUpdated(_interval);
}

function setDepositsPerMint(uint256 _depositsPerMint) external override onlyOwner {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do sanity check here when it is updated instead. That way we only pay for it when there is a real probability to invalidate it.

require(_depositsPerMint > 0, CannotMintZeroAmount());
depositsPerMint = _depositsPerMint;
emit DepositsPerMintUpdated(_depositsPerMint);
}

function setWithdrawer(address _withdrawer) external override onlyOwner {
withdrawer = _withdrawer;
emit WithdrawerUpdated(_withdrawer);
}

function grantAddValidatorPermission(address _address) external override onlyOwner {
canAddValidator[_address] = true;
emit AddValidatorPermissionGranted(_address);
}

function revokeAddValidatorPermission(address _address) external override onlyOwner {
canAddValidator[_address] = false;
emit AddValidatorPermissionRevoked(_address);
}
}
8 changes: 4 additions & 4 deletions l1-contracts/src/mock/TestERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ contract TestERC20 is ERC20, IMintableERC20, Ownable {
emit MinterRemoved(_minter);
}

function transferOwnership(address newOwner) public override(Ownable) onlyOwner {
if (newOwner == address(0)) {
function transferOwnership(address _newOwner) public override(Ownable) onlyOwner {
Copy link
Contributor

Choose a reason for hiding this comment

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

🙏

if (_newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
removeMinter(owner());
addMinter(newOwner);
_transferOwnership(newOwner);
addMinter(_newOwner);
_transferOwnership(_newOwner);
}
}
// docs:end:contract
144 changes: 144 additions & 0 deletions l1-contracts/test/staking_asset_handler/addValidator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.27;

import {StakingAssetHandlerBase} from "./base.t.sol";
import {StakingAssetHandler, IStakingAssetHandler} from "@aztec/mock/StakingAssetHandler.sol";
import {IStakingCore} from "@aztec/core/interfaces/IStaking.sol";

// solhint-disable comprehensive-interface
// solhint-disable func-name-mixedcase
// solhint-disable ordering

contract AddValidatorTest is StakingAssetHandlerBase {
address public canAddValidator = address(0xdead);

function setUp() public override {
super.setUp();
stakingAssetHandler = new StakingAssetHandler(
address(this),
address(stakingAsset),
address(staking),
WITHDRAWER,
MINIMUM_STAKE,
10, // mintInterval, usually overridden in test
3, // depositsPerMint, usually overridden in test
new address[](0)
);
stakingAsset.addMinter(address(stakingAssetHandler));
stakingAssetHandler.grantAddValidatorPermission(canAddValidator);
}

function test_WhenCallerIsNotCanAddValidator(
address _caller,
address _attester,
address _proposer
) external {
// it reverts
vm.assume(_caller != canAddValidator && _caller != address(this));
vm.expectRevert(
abi.encodeWithSelector(IStakingAssetHandler.NotCanAddValidator.selector, _caller)
);
vm.prank(_caller);
stakingAssetHandler.addValidator(_attester, _proposer);
}

modifier whenCallerIsCanAddValidator() {
// Use the canAddValidator address
_;
}

modifier whenItNeedsToMint() {
// By default it needs to mint
_;
}

function test_WhenNotEnoughTimeHasPassedSinceLastMint(uint256 _interval)
external
whenCallerIsCanAddValidator
whenItNeedsToMint
{
_interval = bound(_interval, block.timestamp + 1, block.timestamp + 1e18);
stakingAssetHandler.setMintInterval(_interval);

// it reverts
vm.expectRevert(
abi.encodeWithSelector(IStakingAssetHandler.NotEnoughTimeSinceLastMint.selector, 0, _interval)
);
vm.prank(canAddValidator);
stakingAssetHandler.addValidator(address(0), address(0));
}

modifier whenEnoughTimeHasPassedSinceLastMint(uint256 _interval) {
_interval = bound(_interval, block.timestamp + 1, block.timestamp + 1e18);
stakingAssetHandler.setMintInterval(_interval);
vm.warp(block.timestamp + _interval);
_;
}

function test_WhenEnoughTimeHasPassedSinceLastMint(
uint256 _interval,
uint256 _depositsPerMint,
address _attester,
address _proposer
)
external
whenCallerIsCanAddValidator
whenItNeedsToMint
whenEnoughTimeHasPassedSinceLastMint(_interval)
{
// it mints staking asset
// it emits a {ToppedUp} event
// it updates the lastMintTimestamp
// it deposits into the rollup
// it emits a {ValidatorAdded} event
_depositsPerMint = bound(_depositsPerMint, 1, 1e18);
vm.assume(_attester != address(0));
vm.assume(_proposer != address(0));

stakingAssetHandler.setDepositsPerMint(_depositsPerMint);

vm.expectEmit(true, true, true, true, address(stakingAssetHandler));
emit IStakingAssetHandler.ToppedUp(MINIMUM_STAKE * _depositsPerMint);
vm.expectEmit(true, true, true, true, address(stakingAssetHandler.rollup()));
emit IStakingCore.Deposit(_attester, _proposer, WITHDRAWER, stakingAssetHandler.depositAmount());
vm.expectEmit(true, true, true, true, address(stakingAssetHandler));
emit IStakingAssetHandler.ValidatorAdded(_attester, _proposer, WITHDRAWER);
vm.prank(canAddValidator);
stakingAssetHandler.addValidator(_attester, _proposer);

assertEq(
stakingAsset.balanceOf(address(stakingAssetHandler)),
(stakingAssetHandler.depositsPerMint() - 1) * MINIMUM_STAKE
);
}

function test_WhenItDoesNotNeedToMint(
uint256 _interval,
uint256 _depositsPerMint,
address _attester,
address _proposer
) external whenCallerIsCanAddValidator whenEnoughTimeHasPassedSinceLastMint(_interval) {
vm.assume(_attester != address(0));
vm.assume(_proposer != address(0));

_depositsPerMint = bound(_depositsPerMint, 1, 1e18);

deal(address(stakingAsset), address(stakingAssetHandler), MINIMUM_STAKE * _depositsPerMint);
Copy link
Contributor

Choose a reason for hiding this comment

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

🫡


stakingAssetHandler.setDepositsPerMint(_depositsPerMint);

// it deposits into the rollup
// it emits a {Deposited} event
vm.expectEmit(true, true, true, true, address(stakingAssetHandler.rollup()));
emit IStakingCore.Deposit(_attester, _proposer, WITHDRAWER, stakingAssetHandler.depositAmount());
vm.expectEmit(true, true, true, true, address(stakingAssetHandler));
emit IStakingAssetHandler.ValidatorAdded(_attester, _proposer, WITHDRAWER);
vm.prank(canAddValidator);
stakingAssetHandler.addValidator(_attester, _proposer);

assertEq(
stakingAsset.balanceOf(address(stakingAssetHandler)),
(stakingAssetHandler.depositsPerMint() - 1) * MINIMUM_STAKE
);
}
}
16 changes: 16 additions & 0 deletions l1-contracts/test/staking_asset_handler/addValidator.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
AddValidatorTest
├── when caller is not canAddValidator
│ └── it reverts
└── when caller is canAddValidator
├── when it needs to mint
│ ├── when not enough time has passed since last mint
│ │ └── it reverts
│ └── when enough time has passed since last mint
│ ├── it mints staking asset
│ ├── it emits a {ToppedUp} event
│ ├── it updates the lastMintTimestamp
│ ├── it deposits into the rollup
│ └── it emits a {ValidatorAdded} event
└── when it does not need to mint
├── it deposits into the rollup
└── it emits a {ValidatorAdded} event
28 changes: 28 additions & 0 deletions l1-contracts/test/staking_asset_handler/base.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.27;

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

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

// solhint-disable comprehensive-interface

contract StakingAssetHandlerBase is TestBase {
StakingCheater internal staking;
TestERC20 internal stakingAsset;
StakingAssetHandler internal stakingAssetHandler;

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"));

function setUp() public virtual {
stakingAsset = new TestERC20("test", "TEST", address(this));
staking = new StakingCheater(stakingAsset, MINIMUM_STAKE, 1, 1);
}
}
Loading