Skip to content

Commit eae60cc

Browse files
just-mitchDanielKotov
authored andcommitted
feat: staking asset handler (#12968)
Fix #12932
1 parent 8da8580 commit eae60cc

22 files changed

Lines changed: 1015 additions & 31 deletions

l1-contracts/gas_report.md

Lines changed: 27 additions & 27 deletions
Large diffs are not rendered by default.
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.8.27;
3+
4+
import {Ownable} from "@oz/access/Ownable.sol";
5+
6+
import {IStaking} from "./../core/interfaces/IStaking.sol";
7+
import {IMintableERC20} from "./../governance/interfaces/IMintableERC20.sol";
8+
9+
/**
10+
* @title StakingAssetHandler
11+
* @notice This contract is used as a faucet for creating validators.
12+
*
13+
* It allows for anyone with the `canAddValidator` role to add validators to the rollup,
14+
* caveat being that it controls the number of validators that can be added in a time period.
15+
*
16+
* @dev For example, if minMintInterval is 60*60 and maxDepositsPerMint is 3,
17+
* then *generally* 3 validators can be added every hour.
18+
* NB: it is possible to add 1 validator at the top of the hour, and 2 validators
19+
* at the very end of the hour, then 3 validators at the top of the next hour
20+
* so the maximum "burst" rate is effectively twice the maxDepositsPerMint.
21+
*
22+
* @dev This contract must be a minter of the staking asset.
23+
*
24+
* @dev Only the owner can grant and revoke the `canAddValidator` role, and perform other administrative tasks
25+
* such as setting the rollup, deposit amount, min mint interval, max deposits per mint, and withdrawer.
26+
*
27+
*/
28+
interface IStakingAssetHandler {
29+
event ToppedUp(uint256 _amount);
30+
event ValidatorAdded(
31+
address indexed _attester, address indexed _proposer, address indexed _withdrawer
32+
);
33+
event RollupUpdated(address indexed _rollup);
34+
event DepositAmountUpdated(uint256 _depositAmount);
35+
event IntervalUpdated(uint256 _interval);
36+
event DepositsPerMintUpdated(uint256 _depositsPerMint);
37+
event WithdrawerUpdated(address indexed _withdrawer);
38+
event AddValidatorPermissionGranted(address indexed _address);
39+
event AddValidatorPermissionRevoked(address indexed _address);
40+
41+
error NotCanAddValidator(address _caller);
42+
error NotEnoughTimeSinceLastMint(uint256 _lastMintTimestamp, uint256 _minMintInterval);
43+
error CannotMintZeroAmount();
44+
error MaxDepositsTooLarge(uint256 _depositAmount, uint256 _maxDepositsPerMint);
45+
46+
function addValidator(address _attester, address _proposer) external;
47+
function setRollup(address _rollup) external;
48+
function setDepositAmount(uint256 _amount) external;
49+
function setMintInterval(uint256 _interval) external;
50+
function setDepositsPerMint(uint256 _depositsPerMint) external;
51+
function setWithdrawer(address _withdrawer) external;
52+
function grantAddValidatorPermission(address _address) external;
53+
function revokeAddValidatorPermission(address _address) external;
54+
}
55+
56+
contract StakingAssetHandler is IStakingAssetHandler, Ownable {
57+
IMintableERC20 public immutable STAKING_ASSET;
58+
59+
mapping(address => bool) public canAddValidator;
60+
61+
uint256 public depositAmount;
62+
uint256 public lastMintTimestamp;
63+
uint256 public mintInterval;
64+
uint256 public depositsPerMint;
65+
66+
IStaking public rollup;
67+
address public withdrawer;
68+
69+
modifier onlyCanAddValidator() {
70+
require(canAddValidator[msg.sender], NotCanAddValidator(msg.sender));
71+
_;
72+
}
73+
74+
constructor(
75+
address _owner,
76+
address _stakingAsset,
77+
address _rollup,
78+
address _withdrawer,
79+
uint256 _depositAmount,
80+
uint256 _mintInterval,
81+
uint256 _depositsPerMint,
82+
address[] memory _canAddValidator
83+
) Ownable(_owner) {
84+
require(_depositsPerMint > 0, CannotMintZeroAmount());
85+
86+
STAKING_ASSET = IMintableERC20(_stakingAsset);
87+
88+
rollup = IStaking(_rollup);
89+
emit RollupUpdated(_rollup);
90+
91+
withdrawer = _withdrawer;
92+
emit WithdrawerUpdated(_withdrawer);
93+
94+
depositAmount = _depositAmount;
95+
emit DepositAmountUpdated(_depositAmount);
96+
97+
mintInterval = _mintInterval;
98+
emit IntervalUpdated(_mintInterval);
99+
100+
depositsPerMint = _depositsPerMint;
101+
emit DepositsPerMintUpdated(_depositsPerMint);
102+
103+
for (uint256 i = 0; i < _canAddValidator.length; i++) {
104+
canAddValidator[_canAddValidator[i]] = true;
105+
emit AddValidatorPermissionGranted(_canAddValidator[i]);
106+
}
107+
canAddValidator[_owner] = true;
108+
emit AddValidatorPermissionGranted(_owner);
109+
}
110+
111+
function addValidator(address _attester, address _proposer) external override onlyCanAddValidator {
112+
bool needsToMint = STAKING_ASSET.balanceOf(address(this)) < depositAmount;
113+
bool canMint = block.timestamp - lastMintTimestamp >= mintInterval;
114+
115+
require(!needsToMint || canMint, NotEnoughTimeSinceLastMint(lastMintTimestamp, mintInterval));
116+
if (needsToMint) {
117+
STAKING_ASSET.mint(address(this), depositAmount * depositsPerMint);
118+
lastMintTimestamp = block.timestamp;
119+
emit ToppedUp(depositAmount * depositsPerMint);
120+
}
121+
122+
STAKING_ASSET.approve(address(rollup), depositAmount);
123+
rollup.deposit(_attester, _proposer, withdrawer, depositAmount);
124+
emit ValidatorAdded(_attester, _proposer, withdrawer);
125+
}
126+
127+
function setRollup(address _rollup) external override onlyOwner {
128+
rollup = IStaking(_rollup);
129+
emit RollupUpdated(_rollup);
130+
}
131+
132+
function setDepositAmount(uint256 _amount) external override onlyOwner {
133+
depositAmount = _amount;
134+
emit DepositAmountUpdated(_amount);
135+
}
136+
137+
function setMintInterval(uint256 _interval) external override onlyOwner {
138+
mintInterval = _interval;
139+
emit IntervalUpdated(_interval);
140+
}
141+
142+
function setDepositsPerMint(uint256 _depositsPerMint) external override onlyOwner {
143+
require(_depositsPerMint > 0, CannotMintZeroAmount());
144+
depositsPerMint = _depositsPerMint;
145+
emit DepositsPerMintUpdated(_depositsPerMint);
146+
}
147+
148+
function setWithdrawer(address _withdrawer) external override onlyOwner {
149+
withdrawer = _withdrawer;
150+
emit WithdrawerUpdated(_withdrawer);
151+
}
152+
153+
function grantAddValidatorPermission(address _address) external override onlyOwner {
154+
canAddValidator[_address] = true;
155+
emit AddValidatorPermissionGranted(_address);
156+
}
157+
158+
function revokeAddValidatorPermission(address _address) external override onlyOwner {
159+
canAddValidator[_address] = false;
160+
emit AddValidatorPermissionRevoked(_address);
161+
}
162+
}

l1-contracts/src/mock/TestERC20.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ contract TestERC20 is ERC20, IMintableERC20, Ownable {
3636
emit MinterRemoved(_minter);
3737
}
3838

39-
function transferOwnership(address newOwner) public override(Ownable) onlyOwner {
40-
if (newOwner == address(0)) {
39+
function transferOwnership(address _newOwner) public override(Ownable) onlyOwner {
40+
if (_newOwner == address(0)) {
4141
revert OwnableInvalidOwner(address(0));
4242
}
4343
removeMinter(owner());
44-
addMinter(newOwner);
45-
_transferOwnership(newOwner);
44+
addMinter(_newOwner);
45+
_transferOwnership(_newOwner);
4646
}
4747
}
4848
// docs:end:contract
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.8.27;
3+
4+
import {StakingAssetHandlerBase} from "./base.t.sol";
5+
import {StakingAssetHandler, IStakingAssetHandler} from "@aztec/mock/StakingAssetHandler.sol";
6+
import {IStakingCore} from "@aztec/core/interfaces/IStaking.sol";
7+
8+
// solhint-disable comprehensive-interface
9+
// solhint-disable func-name-mixedcase
10+
// solhint-disable ordering
11+
12+
contract AddValidatorTest is StakingAssetHandlerBase {
13+
address public canAddValidator = address(0xdead);
14+
15+
function setUp() public override {
16+
super.setUp();
17+
stakingAssetHandler = new StakingAssetHandler(
18+
address(this),
19+
address(stakingAsset),
20+
address(staking),
21+
WITHDRAWER,
22+
MINIMUM_STAKE,
23+
10, // mintInterval, usually overridden in test
24+
3, // depositsPerMint, usually overridden in test
25+
new address[](0)
26+
);
27+
stakingAsset.addMinter(address(stakingAssetHandler));
28+
stakingAssetHandler.grantAddValidatorPermission(canAddValidator);
29+
}
30+
31+
function test_WhenCallerIsNotCanAddValidator(
32+
address _caller,
33+
address _attester,
34+
address _proposer
35+
) external {
36+
// it reverts
37+
vm.assume(_caller != canAddValidator && _caller != address(this));
38+
vm.expectRevert(
39+
abi.encodeWithSelector(IStakingAssetHandler.NotCanAddValidator.selector, _caller)
40+
);
41+
vm.prank(_caller);
42+
stakingAssetHandler.addValidator(_attester, _proposer);
43+
}
44+
45+
modifier whenCallerIsCanAddValidator() {
46+
// Use the canAddValidator address
47+
_;
48+
}
49+
50+
modifier whenItNeedsToMint() {
51+
// By default it needs to mint
52+
_;
53+
}
54+
55+
function test_WhenNotEnoughTimeHasPassedSinceLastMint(uint256 _interval)
56+
external
57+
whenCallerIsCanAddValidator
58+
whenItNeedsToMint
59+
{
60+
_interval = bound(_interval, block.timestamp + 1, block.timestamp + 1e18);
61+
stakingAssetHandler.setMintInterval(_interval);
62+
63+
// it reverts
64+
vm.expectRevert(
65+
abi.encodeWithSelector(IStakingAssetHandler.NotEnoughTimeSinceLastMint.selector, 0, _interval)
66+
);
67+
vm.prank(canAddValidator);
68+
stakingAssetHandler.addValidator(address(0), address(0));
69+
}
70+
71+
modifier whenEnoughTimeHasPassedSinceLastMint(uint256 _interval) {
72+
_interval = bound(_interval, block.timestamp + 1, block.timestamp + 1e18);
73+
stakingAssetHandler.setMintInterval(_interval);
74+
vm.warp(block.timestamp + _interval);
75+
_;
76+
}
77+
78+
function test_WhenEnoughTimeHasPassedSinceLastMint(
79+
uint256 _interval,
80+
uint256 _depositsPerMint,
81+
address _attester,
82+
address _proposer
83+
)
84+
external
85+
whenCallerIsCanAddValidator
86+
whenItNeedsToMint
87+
whenEnoughTimeHasPassedSinceLastMint(_interval)
88+
{
89+
// it mints staking asset
90+
// it emits a {ToppedUp} event
91+
// it updates the lastMintTimestamp
92+
// it deposits into the rollup
93+
// it emits a {ValidatorAdded} event
94+
_depositsPerMint = bound(_depositsPerMint, 1, 1e18);
95+
vm.assume(_attester != address(0));
96+
vm.assume(_proposer != address(0));
97+
98+
stakingAssetHandler.setDepositsPerMint(_depositsPerMint);
99+
100+
vm.expectEmit(true, true, true, true, address(stakingAssetHandler));
101+
emit IStakingAssetHandler.ToppedUp(MINIMUM_STAKE * _depositsPerMint);
102+
vm.expectEmit(true, true, true, true, address(stakingAssetHandler.rollup()));
103+
emit IStakingCore.Deposit(_attester, _proposer, WITHDRAWER, stakingAssetHandler.depositAmount());
104+
vm.expectEmit(true, true, true, true, address(stakingAssetHandler));
105+
emit IStakingAssetHandler.ValidatorAdded(_attester, _proposer, WITHDRAWER);
106+
vm.prank(canAddValidator);
107+
stakingAssetHandler.addValidator(_attester, _proposer);
108+
109+
assertEq(
110+
stakingAsset.balanceOf(address(stakingAssetHandler)),
111+
(stakingAssetHandler.depositsPerMint() - 1) * MINIMUM_STAKE
112+
);
113+
}
114+
115+
function test_WhenItDoesNotNeedToMint(
116+
uint256 _interval,
117+
uint256 _depositsPerMint,
118+
address _attester,
119+
address _proposer
120+
) external whenCallerIsCanAddValidator whenEnoughTimeHasPassedSinceLastMint(_interval) {
121+
vm.assume(_attester != address(0));
122+
vm.assume(_proposer != address(0));
123+
124+
_depositsPerMint = bound(_depositsPerMint, 1, 1e18);
125+
126+
deal(address(stakingAsset), address(stakingAssetHandler), MINIMUM_STAKE * _depositsPerMint);
127+
128+
stakingAssetHandler.setDepositsPerMint(_depositsPerMint);
129+
130+
// it deposits into the rollup
131+
// it emits a {Deposited} event
132+
vm.expectEmit(true, true, true, true, address(stakingAssetHandler.rollup()));
133+
emit IStakingCore.Deposit(_attester, _proposer, WITHDRAWER, stakingAssetHandler.depositAmount());
134+
vm.expectEmit(true, true, true, true, address(stakingAssetHandler));
135+
emit IStakingAssetHandler.ValidatorAdded(_attester, _proposer, WITHDRAWER);
136+
vm.prank(canAddValidator);
137+
stakingAssetHandler.addValidator(_attester, _proposer);
138+
139+
assertEq(
140+
stakingAsset.balanceOf(address(stakingAssetHandler)),
141+
(stakingAssetHandler.depositsPerMint() - 1) * MINIMUM_STAKE
142+
);
143+
}
144+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
AddValidatorTest
2+
├── when caller is not canAddValidator
3+
│ └── it reverts
4+
└── when caller is canAddValidator
5+
├── when it needs to mint
6+
│ ├── when not enough time has passed since last mint
7+
│ │ └── it reverts
8+
│ └── when enough time has passed since last mint
9+
│ ├── it mints staking asset
10+
│ ├── it emits a {ToppedUp} event
11+
│ ├── it updates the lastMintTimestamp
12+
│ ├── it deposits into the rollup
13+
│ └── it emits a {ValidatorAdded} event
14+
└── when it does not need to mint
15+
├── it deposits into the rollup
16+
└── it emits a {ValidatorAdded} event
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.27;
3+
4+
import {TestBase} from "@test/base/Base.sol";
5+
6+
import {StakingCheater} from "./../staking/StakingCheater.sol";
7+
import {TestERC20} from "@aztec/mock/TestERC20.sol";
8+
import {StakingAssetHandler} from "@aztec/mock/StakingAssetHandler.sol";
9+
10+
// solhint-disable comprehensive-interface
11+
12+
contract StakingAssetHandlerBase is TestBase {
13+
StakingCheater internal staking;
14+
TestERC20 internal stakingAsset;
15+
StakingAssetHandler internal stakingAssetHandler;
16+
17+
uint256 internal constant MINIMUM_STAKE = 100e18;
18+
19+
address internal constant PROPOSER = address(bytes20("PROPOSER"));
20+
address internal constant ATTESTER = address(bytes20("ATTESTER"));
21+
address internal constant WITHDRAWER = address(bytes20("WITHDRAWER"));
22+
address internal constant RECIPIENT = address(bytes20("RECIPIENT"));
23+
24+
function setUp() public virtual {
25+
stakingAsset = new TestERC20("test", "TEST", address(this));
26+
staking = new StakingCheater(stakingAsset, MINIMUM_STAKE, 1, 1);
27+
}
28+
}

0 commit comments

Comments
 (0)