-
Notifications
You must be signed in to change notification settings - Fork 597
feat: staking asset handler #12968
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: staking asset handler #12968
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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())); | ||
LHerskind marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| ); | ||
| } | ||
| } | ||
| 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 |
| 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); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.