diff --git a/l1-contracts/gas_report.md b/l1-contracts/gas_report.md index 2c2e280a8f62..8d5d1a4ef5d4 100644 --- a/l1-contracts/gas_report.md +++ b/l1-contracts/gas_report.md @@ -4,7 +4,7 @@ | 589795 | 2886 | | | | | | Function Name | min | avg | median | max | # calls | | L2_TOKEN_ADDRESS | 194 | 194 | 194 | 194 | 256 | -| UNDERLYING | 270 | 270 | 270 | 270 | 3031 | +| UNDERLYING | 270 | 270 | 270 | 270 | 3080 | | canonicalRollup | 1016 | 3632 | 5516 | 5516 | 5574 | | depositToAztecPublic | 42812 | 127386 | 128025 | 128025 | 258 | | distributeFees | 27333 | 56798 | 57006 | 57006 | 258 | @@ -12,19 +12,19 @@ | src/core/Rollup.sol:Rollup contract | | | | | | |-------------------------------------|-----------------|---------|---------|----------|---------| | Deployment Cost | Deployment Size | | | | | -| 7965416 | 38750 | | | | | +| 7925568 | 38750 | | | | | | Function Name | min | avg | median | max | # calls | -| archive | 605 | 605 | 605 | 605 | 2474 | +| archive | 605 | 605 | 605 | 605 | 2475 | | cheat__InitialiseValidatorSet | 752109 | 7052330 | 752133 | 13524799 | 519 | | claimProverRewards | 31794 | 53315 | 34239 | 93914 | 3 | | claimSequencerRewards | 57174 | 57174 | 57174 | 57174 | 1 | -| deposit | 169778 | 328470 | 342652 | 342652 | 256 | +| deposit | 169778 | 329145 | 342652 | 342652 | 256 | | getAttesters | 1970 | 26343 | 26629 | 26629 | 259 | | getBlock | 1230 | 1230 | 1230 | 1230 | 910 | | getCollectiveProverRewardsForEpoch | 636 | 1636 | 1636 | 2636 | 4 | | getCurrentEpoch | 1017 | 1017 | 1017 | 1017 | 1032 | | getCurrentEpochCommittee | 42004 | 42004 | 42004 | 42004 | 1 | -| getCurrentProposer | 44246 | 117396 | 52217 | 263958 | 795 | +| getCurrentProposer | 44246 | 117406 | 52217 | 263958 | 795 | | getCurrentSlot | 823 | 1442 | 823 | 4823 | 142 | | getEpochCommittee | 2010 | 14068 | 14218 | 14218 | 520 | | getEpochDuration | 439 | 439 | 439 | 439 | 256 | @@ -36,7 +36,7 @@ | getOutbox | 496 | 873 | 496 | 2496 | 5434 | | getPendingBlockNumber | 507 | 507 | 507 | 507 | 1546 | | getProofSubmissionWindow | 404 | 404 | 404 | 404 | 4 | -| getProvenBlockNumber | 490 | 762 | 490 | 2490 | 7551 | +| getProvenBlockNumber | 490 | 762 | 490 | 2490 | 7553 | | getProvingCostPerManaInEth | 429 | 429 | 429 | 429 | 1 | | getProvingCostPerManaInFeeAsset | 4164 | 4164 | 4164 | 4164 | 1 | | getSequencerRewards | 671 | 1071 | 671 | 2671 | 5 | @@ -45,11 +45,11 @@ | getSpecificProverRewardsForEpoch | 822 | 2130 | 1634 | 3634 | 5 | | getTargetCommitteeSize | 462 | 462 | 462 | 462 | 768 | | getTimestampForSlot | 887 | 888 | 887 | 4887 | 2462 | -| propose | 129177 | 362294 | 363530 | 572838 | 2601 | +| propose | 129177 | 362402 | 363530 | 572838 | 2601 | | prune | 25731 | 36205 | 37466 | 41951 | 6 | | setProvingCostPerMana | 28691 | 28691 | 28691 | 28691 | 2 | | setupEpoch | 208152 | 1372793 | 1400090 | 1400090 | 262 | -| submitEpochRootProof | 64866 | 421855 | 420725 | 448480 | 909 | +| submitEpochRootProof | 64866 | 421805 | 420725 | 448480 | 909 | | src/core/messagebridge/Inbox.sol:Inbox contract | | | | | | |-------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | @@ -64,10 +64,10 @@ | Deployment Cost | Deployment Size | | | | | | 586673 | 2646 | | | | | | Function Name | min | avg | median | max | # calls | -| consume | 28894 | 72192 | 73138 | 73400 | 4546 | -| getRootData | 940 | 1343 | 1149 | 3217 | 2732 | +| consume | 28894 | 72144 | 73138 | 73400 | 4707 | +| getRootData | 940 | 1343 | 1149 | 3217 | 2733 | | hasMessageBeenConsumedAtBlockAndIndex | 591 | 2583 | 2591 | 2591 | 259 | -| insert | 22188 | 57508 | 68252 | 68264 | 1097 | +| insert | 22188 | 57527 | 68264 | 68264 | 1099 | | src/core/staking/Slasher.sol:Slasher contract | | | | | | |-----------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | @@ -85,7 +85,7 @@ | src/governance/CoinIssuer.sol:CoinIssuer contract | | | | | | |---------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | -| 326553 | 1465 | | | | | +| 326385 | 1465 | | | | | | Function Name | min | avg | median | max | # calls | | RATE | 239 | 239 | 239 | 239 | 768 | | mint | 23901 | 43850 | 26637 | 81131 | 768 | @@ -96,25 +96,25 @@ | Deployment Cost | Deployment Size | | | | | | 2332350 | 10841 | | | | | | Function Name | min | avg | median | max | # calls | -| deposit | 27965 | 171787 | 186596 | 188519 | 9729 | +| deposit | 27965 | 171786 | 186596 | 188519 | 9729 | | dropProposal | 23739 | 40533 | 33600 | 63600 | 2307 | | execute | 26209 | 71295 | 71327 | 161717 | 3076 | -| finaliseWithdraw | 23757 | 45164 | 48283 | 65383 | 6033 | +| finaliseWithdraw | 23757 | 45077 | 48283 | 65383 | 6057 | | getConfiguration | 1913 | 12163 | 19913 | 19913 | 5396 | | getProposal | 3523 | 8023 | 3523 | 31523 | 10590 | | getProposalState | 469 | 11470 | 13558 | 21242 | 23311 | -| getWithdrawal | 1075 | 1075 | 1075 | 1075 | 10124 | +| getWithdrawal | 1075 | 1075 | 1075 | 1075 | 10010 | | governanceProposer | 424 | 1418 | 424 | 2424 | 515 | -| initiateWithdraw | 30945 | 199150 | 211342 | 228958 | 7551 | +| initiateWithdraw | 30945 | 199027 | 211342 | 228958 | 7499 | | powerAt | 1042 | 1412 | 1042 | 3029 | 4608 | | proposalCount | 338 | 1714 | 2338 | 2338 | 1116 | -| propose | 23763 | 321926 | 320487 | 337587 | 606 | -| proposeWithLock | 26545 | 421004 | 422627 | 422627 | 257 | -| totalPowerAt | 612 | 1567 | 883 | 3568 | 6087 | +| propose | 23763 | 321927 | 320487 | 337587 | 606 | +| proposeWithLock | 26545 | 421003 | 422627 | 422627 | 257 | +| totalPowerAt | 612 | 1569 | 883 | 3568 | 6067 | | updateConfiguration | 23457 | 32910 | 24180 | 48186 | 6145 | -| updateGovernanceProposer | 21705 | 27186 | 28016 | 28028 | 2048 | +| updateGovernanceProposer | 21693 | 27184 | 28016 | 28028 | 2048 | | vote | 30670 | 87818 | 94478 | 94500 | 12289 | -| withdrawalCount | 383 | 391 | 383 | 2383 | 2502 | +| withdrawalCount | 383 | 391 | 383 | 2383 | 2482 | | src/governance/Registry.sol:Registry contract | | | | | | |-----------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | @@ -122,21 +122,21 @@ | Function Name | min | avg | median | max | # calls | | getCurrentSnapshot | 664 | 2664 | 2664 | 4664 | 514 | | getGovernance | 341 | 2159 | 2341 | 2341 | 2829 | -| getRollup | 374 | 2358 | 2374 | 2374 | 870225 | +| getRollup | 374 | 2358 | 2374 | 2374 | 869977 | | getSnapshot | 4740 | 4740 | 4740 | 4740 | 257 | | getVersionFor | 743 | 3527 | 2927 | 4927 | 773 | | isRollupRegistered | 657 | 3805 | 2812 | 4812 | 515 | | numberOfVersions | 350 | 1685 | 2350 | 2350 | 770 | | transferOwnership | 28592 | 28592 | 28592 | 28592 | 106 | -| upgrade | 23672 | 103284 | 106801 | 106801 | 6127 | +| upgrade | 23672 | 103312 | 106801 | 106801 | 6176 | | src/governance/RewardDistributor.sol:RewardDistributor contract | | | | | | |-----------------------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | | 513664 | 2360 | | | | | | Function Name | min | avg | median | max | # calls | -| BLOCK_REWARD | 238 | 238 | 238 | 238 | 377 | +| BLOCK_REWARD | 238 | 238 | 238 | 238 | 376 | | canonicalRollup | 1143 | 3143 | 3143 | 5643 | 904 | -| claim | 30122 | 45867 | 35665 | 64090 | 513 | +| claim | 30122 | 45856 | 35665 | 64090 | 513 | | owner | 2384 | 2384 | 2384 | 2384 | 257 | | registry | 347 | 1347 | 1347 | 2347 | 2 | | updateRegistry | 23757 | 23781 | 23757 | 30119 | 257 | @@ -150,11 +150,11 @@ | N | 260 | 260 | 260 | 260 | 1949 | | REGISTRY | 205 | 205 | 205 | 205 | 256 | | computeRound | 435 | 435 | 435 | 435 | 266 | -| executeProposal | 29491 | 43508 | 37213 | 366485 | 2053 | +| executeProposal | 29491 | 43507 | 37213 | 366485 | 2053 | | getExecutor | 3397 | 3397 | 3397 | 3397 | 256 | | getInstance | 951 | 951 | 951 | 951 | 256 | | rounds | 865 | 865 | 865 | 865 | 522 | -| vote | 29794 | 50139 | 50074 | 126085 | 856035 | +| vote | 29794 | 50139 | 50074 | 126085 | 855787 | | yeaCount | 851 | 851 | 851 | 851 | 16 | | src/periphery/Forwarder.sol:Forwarder contract | | | | | | |------------------------------------------------|-----------------|-------|--------|--------|---------| diff --git a/l1-contracts/src/mock/StakingAssetHandler.sol b/l1-contracts/src/mock/StakingAssetHandler.sol new file mode 100644 index 000000000000..70b1919882d4 --- /dev/null +++ b/l1-contracts/src/mock/StakingAssetHandler.sol @@ -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 { + 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); + } +} diff --git a/l1-contracts/src/mock/TestERC20.sol b/l1-contracts/src/mock/TestERC20.sol index 594a12f9ebc1..b24a93cf766c 100644 --- a/l1-contracts/src/mock/TestERC20.sol +++ b/l1-contracts/src/mock/TestERC20.sol @@ -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 { + if (_newOwner == address(0)) { revert OwnableInvalidOwner(address(0)); } removeMinter(owner()); - addMinter(newOwner); - _transferOwnership(newOwner); + addMinter(_newOwner); + _transferOwnership(_newOwner); } } // docs:end:contract diff --git a/l1-contracts/test/staking_asset_handler/addValidator.t.sol b/l1-contracts/test/staking_asset_handler/addValidator.t.sol new file mode 100644 index 000000000000..749feb38015f --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/addValidator.t.sol @@ -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); + + 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 + ); + } +} diff --git a/l1-contracts/test/staking_asset_handler/addValidator.tree b/l1-contracts/test/staking_asset_handler/addValidator.tree new file mode 100644 index 000000000000..08ef4ceb9d42 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/addValidator.tree @@ -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 diff --git a/l1-contracts/test/staking_asset_handler/base.t.sol b/l1-contracts/test/staking_asset_handler/base.t.sol new file mode 100644 index 000000000000..80d8328c3277 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/base.t.sol @@ -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); + } +} diff --git a/l1-contracts/test/staking_asset_handler/constructor.t.sol b/l1-contracts/test/staking_asset_handler/constructor.t.sol new file mode 100644 index 000000000000..faeb1e6c3c7b --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/constructor.t.sol @@ -0,0 +1,71 @@ +// 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"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering + +contract ConstructorTest is StakingAssetHandlerBase { + function test_WhenDepositsPerMintIs0() external { + // it reverts + vm.expectRevert(abi.encodeWithSelector(IStakingAssetHandler.CannotMintZeroAmount.selector)); + new StakingAssetHandler( + address(this), address(0), address(0), address(0), 0, 0, 0, new address[](0) + ); + } + + function test_WhenDepositsPerMintIsNot0( + address _owner, + address _stakingAsset, + address _staking, + address _withdrawer, + uint256 _depositAmount, + uint256 _mintInterval, + uint256 _depositsPerMint, + address[] memory _canAddValidator + ) external { + _depositsPerMint = bound(_depositsPerMint, 1, 1000); + // it sets the owner + // it sets the staking asset + // it sets the staking contract and emits a {RollupUpdated} event + // it sets the withdrawer and emits a {WithdrawerUpdated} event + // it sets the deposit amount and emits a {DepositAmountUpdated} event + // it sets the mint interval and emits a {MintIntervalUpdated} event + // it sets the deposits per mint and emits a {DepositsPerMintUpdated} event + // it adds the array of addresses to the can add validator array and emits a {AddValidatorPermissionGranted} event for each address + vm.expectEmit(true, true, true, true); + emit IStakingAssetHandler.RollupUpdated(_staking); + emit IStakingAssetHandler.WithdrawerUpdated(_withdrawer); + emit IStakingAssetHandler.DepositAmountUpdated(_depositAmount); + emit IStakingAssetHandler.IntervalUpdated(_mintInterval); + emit IStakingAssetHandler.DepositsPerMintUpdated(_depositsPerMint); + for (uint256 i = 0; i < _canAddValidator.length; i++) { + emit IStakingAssetHandler.AddValidatorPermissionGranted(_canAddValidator[i]); + } + emit IStakingAssetHandler.AddValidatorPermissionGranted(_owner); + vm.prank(_owner); + stakingAssetHandler = new StakingAssetHandler( + _owner, + _stakingAsset, + _staking, + _withdrawer, + _depositAmount, + _mintInterval, + _depositsPerMint, + _canAddValidator + ); + assertEq(stakingAssetHandler.owner(), _owner); + assertEq(address(stakingAssetHandler.STAKING_ASSET()), _stakingAsset); + assertEq(address(stakingAssetHandler.rollup()), _staking); + assertEq(stakingAssetHandler.withdrawer(), _withdrawer); + assertEq(stakingAssetHandler.depositAmount(), _depositAmount); + assertEq(stakingAssetHandler.mintInterval(), _mintInterval); + assertEq(stakingAssetHandler.depositsPerMint(), _depositsPerMint); + for (uint256 i = 0; i < _canAddValidator.length; i++) { + assertEq(stakingAssetHandler.canAddValidator(_canAddValidator[i]), true); + } + } +} diff --git a/l1-contracts/test/staking_asset_handler/constructor.tree b/l1-contracts/test/staking_asset_handler/constructor.tree new file mode 100644 index 000000000000..d9f7d4b25d2b --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/constructor.tree @@ -0,0 +1,12 @@ +ConstructorTest +├── when deposits per mint is 0 +│ └── it reverts +└── when deposits per mint is not 0 + ├── it sets the owner + ├── it sets the staking asset + ├── it sets the staking contract and emits a {RollupUpdated} event + ├── it sets the withdrawer and emits a {WithdrawerUpdated} event + ├── it sets the deposit amount and emits a {DepositAmountUpdated} event + ├── it sets the mint interval and emits a {MintIntervalUpdated} event + ├── it sets the deposits per mint and emits a {DepositsPerMintUpdated} event + └── it adds the array of addresses to the can add validator array and emits a {AddValidatorPermissionGranted} event for each address diff --git a/l1-contracts/test/staking_asset_handler/grantAddValidatorPermission.t.sol b/l1-contracts/test/staking_asset_handler/grantAddValidatorPermission.t.sol new file mode 100644 index 000000000000..6c021413d9d2 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/grantAddValidatorPermission.t.sol @@ -0,0 +1,61 @@ +// 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 {Ownable} from "@oz/access/Ownable.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering + +contract GrantAddValidatorPermissionTest is StakingAssetHandlerBase { + uint256 internal constant MINT_INTERVAL = 1; + uint256 internal constant DEPOSITS_PER_MINT = 1; + + function setUp() public override { + super.setUp(); + stakingAssetHandler = new StakingAssetHandler( + address(this), + address(stakingAsset), + address(staking), + WITHDRAWER, + MINIMUM_STAKE, + MINT_INTERVAL, + DEPOSITS_PER_MINT, + new address[](0) + ); + stakingAsset.addMinter(address(stakingAssetHandler)); + } + + function test_WhenCallerOfGrantAddValidatorPermissionIsNotOwner(address _caller) external { + // it reverts + vm.assume(_caller != address(this)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + vm.prank(_caller); + stakingAssetHandler.grantAddValidatorPermission(address(1)); + } + + function test_WhenCallerOfGrantAddValidatorPermissionIsOwner(address _address) external { + // it grants add validator permission + // it emits a {AddValidatorPermissionGranted} event + vm.assume(_address != address(this)); + + // first check that we cannot add a validator + vm.expectRevert( + abi.encodeWithSelector(IStakingAssetHandler.NotCanAddValidator.selector, _address) + ); + vm.prank(_address); + stakingAssetHandler.addValidator(address(1), address(2)); + + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.AddValidatorPermissionGranted(_address); + stakingAssetHandler.grantAddValidatorPermission(_address); + assertEq(stakingAssetHandler.canAddValidator(_address), true); + + // now check that we can add a validator + vm.warp(MINT_INTERVAL + 1); + vm.prank(_address); + stakingAssetHandler.addValidator(address(1), address(2)); + } +} diff --git a/l1-contracts/test/staking_asset_handler/grantAddValidatorPermission.tree b/l1-contracts/test/staking_asset_handler/grantAddValidatorPermission.tree new file mode 100644 index 000000000000..c48433f1a0be --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/grantAddValidatorPermission.tree @@ -0,0 +1,6 @@ +GrantAddValidatorPermissionTest +├── when caller of grantAddValidatorPermission is not owner +│ └── it reverts +└── when caller of grantAddValidatorPermission is owner + ├── it grants add validator permission + └── it emits a {AddValidatorPermissionGranted} event diff --git a/l1-contracts/test/staking_asset_handler/revokeAddValidatorPermission.t.sol b/l1-contracts/test/staking_asset_handler/revokeAddValidatorPermission.t.sol new file mode 100644 index 000000000000..cb7902ff3709 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/revokeAddValidatorPermission.t.sol @@ -0,0 +1,69 @@ +// 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 {Ownable} from "@oz/access/Ownable.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering + +contract RevokeAddValidatorPermissionTest is StakingAssetHandlerBase { + uint256 internal constant MINT_INTERVAL = 1; + uint256 internal constant DEPOSITS_PER_MINT = 1; + + function setUp() public override { + super.setUp(); + stakingAssetHandler = new StakingAssetHandler( + address(this), + address(stakingAsset), + address(staking), + WITHDRAWER, + MINIMUM_STAKE, + MINT_INTERVAL, + DEPOSITS_PER_MINT, + new address[](0) + ); + stakingAsset.addMinter(address(stakingAssetHandler)); + } + + function test_WhenCallerOfRevokeAddValidatorPermissionIsNotOwner(address _caller) external { + // it reverts + vm.assume(_caller != address(this)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + vm.prank(_caller); + stakingAssetHandler.revokeAddValidatorPermission(address(1)); + } + + function test_WhenCallerOfRevokeAddValidatorPermissionIsOwner( + address _caller, + address _attester, + address _proposer + ) external { + vm.assume(_attester != address(0)); + vm.assume(_proposer != address(0)); + + // it revokes add validator permission + // it emits a {AddValidatorPermissionRevoked} event + stakingAssetHandler.grantAddValidatorPermission(_caller); + assertEq(stakingAssetHandler.canAddValidator(_caller), true); + + // first check that we can add a validator + vm.warp(MINT_INTERVAL + 1); + vm.prank(_caller); + stakingAssetHandler.addValidator(_attester, _proposer); + + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.AddValidatorPermissionRevoked(_caller); + stakingAssetHandler.revokeAddValidatorPermission(_caller); + assertEq(stakingAssetHandler.canAddValidator(_caller), false); + + // it reverts + vm.expectRevert( + abi.encodeWithSelector(IStakingAssetHandler.NotCanAddValidator.selector, _caller) + ); + vm.prank(_caller); + stakingAssetHandler.addValidator(_attester, _proposer); + } +} diff --git a/l1-contracts/test/staking_asset_handler/revokeAddValidatorPermission.tree b/l1-contracts/test/staking_asset_handler/revokeAddValidatorPermission.tree new file mode 100644 index 000000000000..14778fd03144 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/revokeAddValidatorPermission.tree @@ -0,0 +1,7 @@ +RevokeAddValidatorPermissionTest +├── when caller of revokeAddValidatorPermission is not owner +│ └── it reverts +└── when caller of revokeAddValidatorPermission is owner + ├── it revokes add validator permission + ├── it emits a {AddValidatorPermissionRevoked} event + └── it subsequently reverts when address tries to add a validator diff --git a/l1-contracts/test/staking_asset_handler/setDepositAmount.t.sol b/l1-contracts/test/staking_asset_handler/setDepositAmount.t.sol new file mode 100644 index 000000000000..097a938a8d8b --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setDepositAmount.t.sol @@ -0,0 +1,67 @@ +// 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"; +import {Ownable} from "@oz/access/Ownable.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering + +contract SetDepositAmountTest is StakingAssetHandlerBase { + uint256 internal constant MINT_INTERVAL = 1; + uint256 internal constant DEPOSITS_PER_MINT = 2; + + function setUp() public override { + super.setUp(); + stakingAssetHandler = new StakingAssetHandler( + address(this), + address(stakingAsset), + address(staking), + WITHDRAWER, + MINIMUM_STAKE, + MINT_INTERVAL, + DEPOSITS_PER_MINT, + new address[](0) + ); + stakingAsset.addMinter(address(stakingAssetHandler)); + } + + function test_WhenCallerIsNotOwner(address _caller) external { + vm.assume(_caller != address(this)); + // it reverts + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + vm.prank(_caller); + stakingAssetHandler.setDepositAmount(MINIMUM_STAKE); + } + + function test_WhenCallerIsOwner(uint256 _newDepositAmount) external { + // it sets the deposit amount + // it emits a {DepositAmountUpdated} event + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.DepositAmountUpdated(_newDepositAmount); + stakingAssetHandler.setDepositAmount(_newDepositAmount); + assertEq(stakingAssetHandler.depositAmount(), _newDepositAmount); + } + + function test_WhenValidatorIsAddedAfterSettingTheDepositAmount(uint256 _newDepositAmount) + external + { + // it deposits the new amount + _newDepositAmount = bound(_newDepositAmount, MINIMUM_STAKE + 1, 1e22); + stakingAssetHandler.setDepositAmount(_newDepositAmount); + + vm.warp(block.timestamp + MINT_INTERVAL); + + vm.expectEmit(true, true, true, true, address(staking)); + emit IStakingCore.Deposit(address(1), address(1), WITHDRAWER, _newDepositAmount); + stakingAssetHandler.addValidator(address(1), address(1)); + assertEq(stakingAssetHandler.depositAmount(), _newDepositAmount); + assertEq( + stakingAsset.balanceOf(address(stakingAssetHandler)), + _newDepositAmount * (DEPOSITS_PER_MINT - 1) + ); + } +} diff --git a/l1-contracts/test/staking_asset_handler/setDepositAmount.tree b/l1-contracts/test/staking_asset_handler/setDepositAmount.tree new file mode 100644 index 000000000000..b6219c18d912 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setDepositAmount.tree @@ -0,0 +1,8 @@ +SetDepositAmountTest +├── when caller is not owner +│ └── it reverts +├── when caller is owner +│ ├── it sets the deposit amount +│ └── it emits a {DepositAmountUpdated} event +└── when validator is added after setting the deposit amount + └── it deposits the new amount diff --git a/l1-contracts/test/staking_asset_handler/setDepositsPerMint.t.sol b/l1-contracts/test/staking_asset_handler/setDepositsPerMint.t.sol new file mode 100644 index 000000000000..f2267ffb6f66 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setDepositsPerMint.t.sol @@ -0,0 +1,90 @@ +// 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 {Ownable} from "@oz/access/Ownable.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering +// solhint-disable private-vars-leading-underscore + +contract SetDepositsPerMintTest is StakingAssetHandlerBase { + uint256 internal constant MINT_INTERVAL = 1; + uint256 internal constant INITIAL_DEPOSITS_PER_MINT = 1; + + function setUp() public override { + super.setUp(); + stakingAssetHandler = new StakingAssetHandler( + address(this), + address(stakingAsset), + address(staking), + WITHDRAWER, + MINIMUM_STAKE, + MINT_INTERVAL, + INITIAL_DEPOSITS_PER_MINT, + new address[](0) + ); + stakingAsset.addMinter(address(stakingAssetHandler)); + } + + function test_WhenCallerOfSetDepositsPerMintIsNotOwner(address _caller) external { + vm.assume(_caller != address(this)); + // it reverts + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + vm.prank(_caller); + stakingAssetHandler.setDepositsPerMint(INITIAL_DEPOSITS_PER_MINT); + } + + modifier whenCallerOfSetDepositsPerMintIsOwner() { + // caller is owner by default + _; + } + + function test_WhenDepositsPerMintIs0() external whenCallerOfSetDepositsPerMintIsOwner { + // it reverts + vm.expectRevert(abi.encodeWithSelector(IStakingAssetHandler.CannotMintZeroAmount.selector)); + stakingAssetHandler.setDepositsPerMint(0); + } + + function test_WhenDepositsPerMintIsNot0(uint256 _newDepositsPerMint) + external + whenCallerOfSetDepositsPerMintIsOwner + { + _newDepositsPerMint = bound(_newDepositsPerMint, 1, 1000); + // it sets the deposits per mint + // it emits a {DepositsPerMintUpdated} event + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.DepositsPerMintUpdated(_newDepositsPerMint); + stakingAssetHandler.setDepositsPerMint(_newDepositsPerMint); + assertEq(stakingAssetHandler.depositsPerMint(), _newDepositsPerMint); + } + + function test_WhenOwnerAddsValidators(uint256 _depositsPerMint) external { + // it can add up to the deposits per mint without minting + _depositsPerMint = bound(_depositsPerMint, 1, 1000); + address[] memory validators = new address[](_depositsPerMint); + for (uint256 i = 0; i < _depositsPerMint; i++) { + validators[i] = address(uint160(i + 1)); + } + + stakingAssetHandler.setDepositsPerMint(_depositsPerMint); + + for (uint256 i = 0; i < _depositsPerMint; i++) { + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.ValidatorAdded(validators[i], validators[i], WITHDRAWER); + stakingAssetHandler.addValidator(validators[i], validators[i]); + } + + // it reverts when adding one more validator + vm.expectRevert( + abi.encodeWithSelector( + IStakingAssetHandler.NotEnoughTimeSinceLastMint.selector, block.timestamp, MINT_INTERVAL + ) + ); + stakingAssetHandler.addValidator( + address(uint160(_depositsPerMint)), address(uint160(_depositsPerMint + 2)) + ); + } +} diff --git a/l1-contracts/test/staking_asset_handler/setDepositsPerMint.tree b/l1-contracts/test/staking_asset_handler/setDepositsPerMint.tree new file mode 100644 index 000000000000..1a18d90a3557 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setDepositsPerMint.tree @@ -0,0 +1,11 @@ +SetDepositsPerMintTest +├── when caller of setDepositsPerMint is not owner +│ └── it reverts +├── when caller of setDepositsPerMint is owner +│ ├── when deposits per mint is 0 +│ │ └── it reverts +│ └── when deposits per mint is not 0 +│ ├── it sets the deposits per mint +│ └── it emits a {DepositsPerMintUpdated} event +└── when owner adds validators + └── it can add up to the deposits per mint without minting diff --git a/l1-contracts/test/staking_asset_handler/setMintInterval.t.sol b/l1-contracts/test/staking_asset_handler/setMintInterval.t.sol new file mode 100644 index 000000000000..f7227209c1cd --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setMintInterval.t.sol @@ -0,0 +1,79 @@ +// 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 {Ownable} from "@oz/access/Ownable.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering + +contract SetMintIntervalTest is StakingAssetHandlerBase { + uint256 internal constant INITIAL_MINT_INTERVAL = 1; + uint256 internal constant DEPOSITS_PER_MINT = 1; + + function setUp() public override { + super.setUp(); + stakingAssetHandler = new StakingAssetHandler( + address(this), + address(stakingAsset), + address(staking), + WITHDRAWER, + MINIMUM_STAKE, + INITIAL_MINT_INTERVAL, + DEPOSITS_PER_MINT, + new address[](0) + ); + stakingAsset.addMinter(address(stakingAssetHandler)); + } + + function test_WhenCallerOfSetMintIntervalIsNotOwner(address _caller) external { + vm.assume(_caller != address(this)); + // it reverts + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + vm.prank(_caller); + stakingAssetHandler.setMintInterval(INITIAL_MINT_INTERVAL); + } + + function test_WhenCallerOfSetMintIntervalIsOwner(uint256 _newMintInterval) external { + // it sets the mint interval + // it emits a {IntervalUpdated} event + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.IntervalUpdated(_newMintInterval); + stakingAssetHandler.setMintInterval(_newMintInterval); + assertEq(stakingAssetHandler.mintInterval(), _newMintInterval); + } + + function test_WhenOwnerTriesToMintBeforeTheNewIntervalHasPassed( + uint256 _newMintInterval, + uint256 _jump + ) external { + _newMintInterval = bound(_newMintInterval, INITIAL_MINT_INTERVAL + 1, 1e18); + _jump = bound(_jump, 1, _newMintInterval); + stakingAssetHandler.setMintInterval(_newMintInterval); + // the "last mint timestamp" is 0 before the first mint + vm.warp(_newMintInterval - _jump); + + // it reverts + vm.expectRevert( + abi.encodeWithSelector( + IStakingAssetHandler.NotEnoughTimeSinceLastMint.selector, 0, _newMintInterval + ) + ); + stakingAssetHandler.addValidator(address(1), address(1)); + } + + function test_WhenOwnerTriesToMintAfterTheNewIntervalHasPassed(uint256 _newMintInterval) external { + _newMintInterval = bound(_newMintInterval, INITIAL_MINT_INTERVAL + 1, 1e18); + stakingAssetHandler.setMintInterval(_newMintInterval); + vm.warp(block.timestamp + _newMintInterval); + // it mints + // it emits a {Minted} event + // it updates the last mint timestamp + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.ValidatorAdded(address(1), address(1), WITHDRAWER); + stakingAssetHandler.addValidator(address(1), address(1)); + assertEq(stakingAssetHandler.lastMintTimestamp(), block.timestamp); + } +} diff --git a/l1-contracts/test/staking_asset_handler/setMintInterval.tree b/l1-contracts/test/staking_asset_handler/setMintInterval.tree new file mode 100644 index 000000000000..9144759de5a9 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setMintInterval.tree @@ -0,0 +1,12 @@ +SetMintIntervalTest +├── when caller of setMintInterval is not owner +│ └── it reverts +├── when caller of setMintInterval is owner +│ ├── it sets the mint interval +│ └── it emits a {IntervalUpdated} event +├── when owner tries to mint before the new interval has passed +│ └── it reverts +└── when owner tries to mint after the new interval has passed + ├── it mints + ├── it emits a {Minted} event + └── it updates the last mint timestamp diff --git a/l1-contracts/test/staking_asset_handler/setRollup.t.sol b/l1-contracts/test/staking_asset_handler/setRollup.t.sol new file mode 100644 index 000000000000..deefc226624e --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setRollup.t.sol @@ -0,0 +1,65 @@ +// 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"; +import {Ownable} from "@oz/access/Ownable.sol"; +import {StakingCheater} from "./../staking/StakingCheater.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering + +contract SetRollupTest is StakingAssetHandlerBase { + uint256 internal constant MINIMUM_MINT_INTERVAL = 1; + uint256 internal constant MAX_DEPOSITS_PER_MINT = 1; + + function setUp() public override { + super.setUp(); + stakingAssetHandler = new StakingAssetHandler( + address(this), + address(stakingAsset), + address(staking), + WITHDRAWER, + MINIMUM_STAKE, + MINIMUM_MINT_INTERVAL, + MAX_DEPOSITS_PER_MINT, + new address[](0) + ); + stakingAsset.addMinter(address(stakingAssetHandler)); + } + + function test_WhenCallerIsNotOwner(address _caller) external { + vm.assume(_caller != address(this)); + // it reverts + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + stakingAssetHandler.setRollup(address(0)); + } + + function test_WhenCallerIsOwner(address _newRollup) external { + // it sets the rollup + // it emits a {RollupUpdated} event + vm.assume(_newRollup != address(staking)); + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.RollupUpdated(_newRollup); + stakingAssetHandler.setRollup(_newRollup); + assertEq(address(stakingAssetHandler.rollup()), _newRollup); + } + + function test_WhenValidatorIsAddedAfterSettingTheRollup() external { + // it deposits into the new rollup + StakingCheater newStaking = new StakingCheater(stakingAsset, MINIMUM_STAKE, 1, 1); + assertNotEq(address(newStaking), address(staking)); + + stakingAssetHandler.setRollup(address(newStaking)); + assertEq(address(stakingAssetHandler.rollup()), address(newStaking)); + + vm.warp(block.timestamp + MINIMUM_MINT_INTERVAL); + + vm.expectEmit(true, true, true, true, address(newStaking)); + emit IStakingCore.Deposit(address(1), address(1), WITHDRAWER, MINIMUM_STAKE); + stakingAssetHandler.addValidator(address(1), address(1)); + } +} diff --git a/l1-contracts/test/staking_asset_handler/setRollup.tree b/l1-contracts/test/staking_asset_handler/setRollup.tree new file mode 100644 index 000000000000..47f5a72f35e4 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setRollup.tree @@ -0,0 +1,8 @@ +SetRollupTest +├── when caller is not owner +│ └── it reverts +├── when caller is owner +│ ├── it sets the rollup +│ └── it emits a {RollupUpdated} event +└── when validator is added after setting the rollup + └── it deposits into the new rollup diff --git a/l1-contracts/test/staking_asset_handler/setWithdrawer.t.sol b/l1-contracts/test/staking_asset_handler/setWithdrawer.t.sol new file mode 100644 index 000000000000..62ff1519bec6 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setWithdrawer.t.sol @@ -0,0 +1,59 @@ +// 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 {Ownable} from "@oz/access/Ownable.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase +// solhint-disable ordering + +contract SetWithdrawerTest is StakingAssetHandlerBase { + uint256 internal constant MINIMUM_MINT_INTERVAL = 1; + uint256 internal constant MAX_DEPOSITS_PER_MINT = 1; + + function setUp() public override { + super.setUp(); + stakingAssetHandler = new StakingAssetHandler( + address(this), + address(stakingAsset), + address(staking), + WITHDRAWER, + MINIMUM_STAKE, + MINIMUM_MINT_INTERVAL, + MAX_DEPOSITS_PER_MINT, + new address[](0) + ); + stakingAsset.addMinter(address(stakingAssetHandler)); + } + + function test_WhenCallerOfSetWithdrawerIsNotOwner(address _caller) external { + // it reverts + vm.assume(_caller != address(this)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + vm.prank(_caller); + stakingAssetHandler.setWithdrawer(address(1)); + } + + function test_WhenCallerOfSetWithdrawerIsOwner(address _newWithdrawer) external { + // it sets the withdrawer + // it emits a {WithdrawerUpdated} event + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.WithdrawerUpdated(_newWithdrawer); + stakingAssetHandler.setWithdrawer(_newWithdrawer); + assertEq(stakingAssetHandler.withdrawer(), _newWithdrawer); + } + + function test_WhenOwnerCallsAddValidatorAfterSettingTheWithdrawer(address _newWithdrawer) + external + { + // it uses the new withdrawer + stakingAssetHandler.setWithdrawer(_newWithdrawer); + vm.warp(MINIMUM_MINT_INTERVAL); + vm.expectEmit(true, true, true, true, address(stakingAssetHandler)); + emit IStakingAssetHandler.ValidatorAdded(address(1), address(1), _newWithdrawer); + stakingAssetHandler.addValidator(address(1), address(1)); + assertEq(stakingAssetHandler.withdrawer(), _newWithdrawer); + } +} diff --git a/l1-contracts/test/staking_asset_handler/setWithdrawer.tree b/l1-contracts/test/staking_asset_handler/setWithdrawer.tree new file mode 100644 index 000000000000..5129023f8b87 --- /dev/null +++ b/l1-contracts/test/staking_asset_handler/setWithdrawer.tree @@ -0,0 +1,9 @@ +SetWithdrawerTest +├── when caller of setWithdrawer is not owner +│ └── it reverts +├── when caller of setWithdrawer is owner +│ ├── it sets the withdrawer +│ └── it emits a {WithdrawerUpdated} event +└── when owner calls addValidator after setting the withdrawer + └── it uses the new withdrawer +