forked from zksync-association/zk-governance
-
Notifications
You must be signed in to change notification settings - Fork 0
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: copy capped minter for v2 #3
Closed
Closed
Changes from 2 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.24; | ||
|
||
import {IMintableAndDelegatable} from "src/interfaces/IMintableAndDelegatable.sol"; | ||
|
||
/// @title ZkCappedMinterV2 | ||
/// @author [ScopeLift](https://scopelift.co) | ||
/// @notice A contract to allow a permissioned entity to mint ZK tokens up to a given amount (the cap). | ||
/// @custom:security-contact [email protected] | ||
contract ZkCappedMinterV2 { | ||
/// @notice The contract where the tokens will be minted by an authorized minter. | ||
IMintableAndDelegatable public immutable TOKEN; | ||
|
||
/// @notice The address that is allowed to mint tokens. | ||
address public immutable ADMIN; | ||
|
||
/// @notice The maximum number of tokens that may be minted by the ZkCappedMinter. | ||
uint256 public immutable CAP; | ||
|
||
/// @notice The cumulative number of tokens that have been minted by the ZkCappedMinter. | ||
uint256 public minted = 0; | ||
|
||
/// @notice Error for when the cap is exceeded. | ||
error ZkCappedMinterV2__CapExceeded(address minter, uint256 amount); | ||
|
||
/// @notice Error for when the caller is not the admin. | ||
error ZkCappedMinterV2__Unauthorized(address account); | ||
|
||
/// @notice Constructor for a new ZkCappedMinter contract | ||
/// @param _token The token contract where tokens will be minted. | ||
/// @param _admin The address that is allowed to mint tokens. | ||
/// @param _cap The maximum number of tokens that may be minted by the ZkCappedMinter. | ||
constructor(IMintableAndDelegatable _token, address _admin, uint256 _cap) { | ||
TOKEN = _token; | ||
ADMIN = _admin; | ||
CAP = _cap; | ||
} | ||
|
||
/// @notice Mints a given amount of tokens to a given address, so long as the cap is not exceeded. | ||
/// @param _to The address that will receive the new tokens. | ||
/// @param _amount The quantity of tokens, in raw decimals, that will be created. | ||
function mint(address _to, uint256 _amount) external { | ||
_revertIfUnauthorized(); | ||
_revertIfCapExceeded(_amount); | ||
minted += _amount; | ||
TOKEN.mint(_to, _amount); | ||
} | ||
|
||
/// @notice Reverts if msg.sender is not the contract admin. | ||
function _revertIfUnauthorized() internal view { | ||
if (msg.sender != ADMIN) { | ||
revert ZkCappedMinterV2__Unauthorized(msg.sender); | ||
} | ||
} | ||
|
||
/// @notice Reverts if the amount of new tokens will increase the minted tokens beyond the mint cap. | ||
/// @param _amount The quantity of tokens, in raw decimals, that will checked against the cap. | ||
function _revertIfCapExceeded(uint256 _amount) internal view { | ||
if (minted + _amount > CAP) { | ||
revert ZkCappedMinterV2__CapExceeded(msg.sender, _amount); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.24; | ||
|
||
import {L2ContractHelper} from "src/lib/L2ContractHelper.sol"; | ||
import {ZkCappedMinterV2} from "src/ZkCappedMinterV2.sol"; | ||
import {IMintableAndDelegatable} from "src/interfaces/IMintableAndDelegatable.sol"; | ||
|
||
/// @title ZkCappedMinterV2Factory | ||
/// @author [ScopeLift](https://scopelift.co) | ||
/// @notice Factory contract to deploy ZkCappedMinterV2 contracts using CREATE2. | ||
contract ZkCappedMinterV2Factory { | ||
/// @dev Bytecode hash should be updated with the correct value from | ||
/// ./zkout/ZkCappedMinterV2.sol/ZkCappedMinterV2.json. | ||
bytes32 public immutable BYTECODE_HASH; | ||
|
||
constructor(bytes32 _bytecodeHash) { | ||
BYTECODE_HASH = _bytecodeHash; | ||
} | ||
|
||
/// @notice Emitted when a new ZkCappedMinterV2 is created. | ||
/// @param minterAddress The address of the newly deployed ZkCappedMinterV2. | ||
/// @param token The token contract where tokens will be minted. | ||
/// @param admin The address authorized to mint tokens. | ||
/// @param cap The maximum number of tokens that may be minted. | ||
event CappedMinterV2Created(address indexed minterAddress, IMintableAndDelegatable token, address admin, uint256 cap); | ||
|
||
/// @notice Deploys a new ZkCappedMinterV2 contract using CREATE2. | ||
/// @param _token The token contract where tokens will be minted. | ||
/// @param _admin The address authorized to mint tokens. | ||
/// @param _cap The maximum number of tokens that may be minted. | ||
/// @param _saltNonce A user-provided nonce for salt calculation. | ||
/// @return minterAddress The address of the newly deployed ZkCappedMinterV2. | ||
function createCappedMinter(IMintableAndDelegatable _token, address _admin, uint256 _cap, uint256 _saltNonce) | ||
external | ||
returns (address minterAddress) | ||
{ | ||
bytes memory saltArgs = abi.encode(_token, _admin, _cap); | ||
bytes32 salt = _calculateSalt(saltArgs, _saltNonce); | ||
ZkCappedMinterV2 instance = new ZkCappedMinterV2{salt: salt}(_token, _admin, _cap); | ||
minterAddress = address(instance); | ||
|
||
emit CappedMinterV2Created(minterAddress, _token, _admin, _cap); | ||
} | ||
|
||
/// @notice Computes the address of a ZkCappedMinterV2 deployed via this factory. | ||
/// @param _token The token contract where tokens will be minted. | ||
/// @param _admin The address authorized to mint tokens. | ||
/// @param _cap The maximum number of tokens that may be minted. | ||
/// @param _saltNonce The nonce used for salt calculation. | ||
/// @return addr The address of the ZkCappedMinterV2. | ||
function getMinter(IMintableAndDelegatable _token, address _admin, uint256 _cap, uint256 _saltNonce) | ||
external | ||
view | ||
returns (address addr) | ||
{ | ||
bytes memory saltArgs = abi.encode(_token, _admin, _cap); | ||
bytes32 salt = _calculateSalt(saltArgs, _saltNonce); | ||
addr = L2ContractHelper.computeCreate2Address( | ||
address(this), salt, BYTECODE_HASH, keccak256(abi.encode(_token, _admin, _cap)) | ||
); | ||
} | ||
|
||
/// @notice Calculates the salt for CREATE2 deployment. | ||
/// @param _args The encoded arguments for the salt calculation. | ||
/// @param _saltNonce A user-provided nonce for additional uniqueness. | ||
/// @return The calculated salt as a bytes32 value. | ||
function _calculateSalt(bytes memory _args, uint256 _saltNonce) internal view returns (bytes32) { | ||
return keccak256(abi.encode(_args, block.chainid, _saltNonce)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.24; | ||
|
||
import {ZkTokenTest} from "test/utils/ZkTokenTest.sol"; | ||
import {IMintableAndDelegatable} from "src/interfaces/IMintableAndDelegatable.sol"; | ||
import {ZkCappedMinterV2} from "src/ZkCappedMinterV2.sol"; | ||
import {console2} from "forge-std/Test.sol"; | ||
|
||
contract ZkCappedMinterV2Test is ZkTokenTest { | ||
function setUp() public virtual override { | ||
super.setUp(); | ||
} | ||
|
||
function createCappedMinter(address _admin, uint256 _cap) internal returns (ZkCappedMinterV2) { | ||
ZkCappedMinterV2 cappedMinter = new ZkCappedMinterV2(IMintableAndDelegatable(address(token)), _admin, _cap); | ||
vm.prank(admin); | ||
token.grantRole(MINTER_ROLE, address(cappedMinter)); | ||
return cappedMinter; | ||
} | ||
} | ||
|
||
contract Constructor is ZkCappedMinterV2Test { | ||
function testFuzz_InitializesTheCappedMinterForAssociationAndFoundation(address _cappedMinterAdmin, uint256 _cap) | ||
public | ||
{ | ||
_cap = bound(_cap, 0, MAX_MINT_SUPPLY); | ||
ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap); | ||
assertEq(address(cappedMinter.TOKEN()), address(token)); | ||
assertEq(cappedMinter.ADMIN(), _cappedMinterAdmin); | ||
assertEq(cappedMinter.CAP(), _cap); | ||
} | ||
} | ||
|
||
contract Mint is ZkCappedMinterV2Test { | ||
function testFuzz_MintsNewTokensWhenTheAmountRequestedIsBelowTheCap( | ||
address _cappedMinterAdmin, | ||
address _receiver, | ||
uint256 _cap, | ||
uint256 _amount | ||
) public { | ||
_cap = bound(_cap, 0, MAX_MINT_SUPPLY); | ||
_amount = bound(_amount, 1, MAX_MINT_SUPPLY); | ||
vm.assume(_cap > _amount); | ||
vm.assume(_receiver != address(0) && _receiver != initMintReceiver); | ||
ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap); | ||
vm.prank(_cappedMinterAdmin); | ||
cappedMinter.mint(_receiver, _amount); | ||
assertEq(token.balanceOf(_receiver), _amount); | ||
} | ||
|
||
function testFuzz_MintsNewTokensInSuccessionToDifferentAccountsWhileRemainingBelowCap( | ||
address _cappedMinterAdmin, | ||
address _receiver1, | ||
address _receiver2, | ||
uint256 _cap, | ||
uint256 _amount1, | ||
uint256 _amount2 | ||
) public { | ||
_cap = bound(_cap, 0, MAX_MINT_SUPPLY); | ||
vm.assume(_amount1 < MAX_MINT_SUPPLY / 2); | ||
vm.assume(_amount2 < MAX_MINT_SUPPLY / 2); | ||
vm.assume(_amount1 + _amount2 < _cap); | ||
vm.assume(_receiver1 != address(0) && _receiver1 != initMintReceiver); | ||
vm.assume(_receiver2 != address(0) && _receiver2 != initMintReceiver); | ||
vm.assume(_receiver1 != _receiver2); | ||
ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap); | ||
vm.startPrank(_cappedMinterAdmin); | ||
cappedMinter.mint(_receiver1, _amount1); | ||
cappedMinter.mint(_receiver2, _amount2); | ||
vm.stopPrank(); | ||
assertEq(token.balanceOf(_receiver1), _amount1); | ||
assertEq(token.balanceOf(_receiver2), _amount2); | ||
} | ||
|
||
function testFuzz_RevertIf_MintAttemptedByNonAdmin(address _cappedMinterAdmin, uint256 _cap, address _nonAdmin) | ||
public | ||
{ | ||
_cap = bound(_cap, 0, MAX_MINT_SUPPLY); | ||
vm.assume(_nonAdmin != _cappedMinterAdmin); | ||
|
||
ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap); | ||
vm.expectRevert(abi.encodeWithSelector(ZkCappedMinterV2.ZkCappedMinterV2__Unauthorized.selector, _nonAdmin)); | ||
vm.startPrank(_nonAdmin); | ||
cappedMinter.mint(_nonAdmin, _cap); | ||
} | ||
|
||
function testFuzz_RevertIf_CapExceededOnMint(address _cappedMinterAdmin, address _receiver, uint256 _cap) public { | ||
_cap = bound(_cap, 4, MAX_MINT_SUPPLY); | ||
vm.assume(_receiver != address(0) && _receiver != initMintReceiver); | ||
ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap); | ||
vm.prank(_cappedMinterAdmin); | ||
cappedMinter.mint(_receiver, _cap); | ||
assertEq(token.balanceOf(_receiver), _cap); | ||
vm.expectRevert( | ||
abi.encodeWithSelector(ZkCappedMinterV2.ZkCappedMinterV2__CapExceeded.selector, _cappedMinterAdmin, _cap) | ||
); | ||
vm.prank(_cappedMinterAdmin); | ||
cappedMinter.mint(_receiver, _cap); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.24; | ||
|
||
import {ZkTokenTest} from "test/utils/ZkTokenTest.sol"; | ||
import {ZkCappedMinterV2Factory} from "src/ZkCappedMinterV2Factory.sol"; | ||
import {ZkCappedMinterV2} from "src/ZkCappedMinterV2.sol"; | ||
import {IMintableAndDelegatable} from "src/interfaces/IMintableAndDelegatable.sol"; | ||
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; | ||
import {console2} from "forge-std/console2.sol"; | ||
import {stdJson} from "forge-std/StdJson.sol"; | ||
|
||
contract ZkCappedMinterV2FactoryTest is ZkTokenTest { | ||
bytes32 bytecodeHash; | ||
ZkCappedMinterV2Factory factory; | ||
|
||
function setUp() public virtual override { | ||
super.setUp(); | ||
|
||
// Read the bytecode hash from the JSON file | ||
string memory root = vm.projectRoot(); | ||
string memory path = string.concat(root, "/zkout/ZkCappedMinterV2.sol/ZkCappedMinterV2.json"); | ||
string memory json = vm.readFile(path); | ||
bytecodeHash = bytes32(stdJson.readBytes(json, ".hash")); | ||
|
||
factory = new ZkCappedMinterV2Factory(bytecodeHash); | ||
} | ||
|
||
function _assumeValidAddress(address _addr) internal view { | ||
vm.assume(_addr != address(0) && _addr != address(factory)); | ||
} | ||
|
||
function _boundToReasonableCap(uint256 _cap) internal view returns (uint256) { | ||
return bound(_cap, 1, MAX_MINT_SUPPLY); | ||
} | ||
} | ||
|
||
contract CreateCappedMinter is ZkCappedMinterV2FactoryTest { | ||
function testFuzz_CreatesNewCappedMinter(address _cappedMinterAdmin, uint256 _cap, uint256 _saltNonce) public { | ||
_assumeValidAddress(_cappedMinterAdmin); | ||
_cap = _boundToReasonableCap(_cap); | ||
|
||
address minterAddress = | ||
factory.createCappedMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
|
||
ZkCappedMinterV2 minter = ZkCappedMinterV2(minterAddress); | ||
assertEq(address(minter.TOKEN()), address(token)); | ||
assertEq(minter.ADMIN(), _cappedMinterAdmin); | ||
assertEq(minter.CAP(), _cap); | ||
} | ||
|
||
function testFuzz_EmitsCappedMinterCreatedEvent(address _cappedMinterAdmin, uint256 _cap, uint256 _saltNonce) public { | ||
_assumeValidAddress(_cappedMinterAdmin); | ||
_cap = _boundToReasonableCap(_cap); | ||
|
||
vm.expectEmit(); | ||
emit ZkCappedMinterV2Factory.CappedMinterV2Created( | ||
factory.getMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce), | ||
IMintableAndDelegatable(address(token)), | ||
_cappedMinterAdmin, | ||
_cap | ||
); | ||
|
||
factory.createCappedMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
} | ||
|
||
function testFuzz_RevertIf_CreatingDuplicateMinter(address _cappedMinterAdmin, uint256 _cap, uint256 _saltNonce) | ||
public | ||
{ | ||
_assumeValidAddress(_cappedMinterAdmin); | ||
_cap = _boundToReasonableCap(_cap); | ||
|
||
factory.createCappedMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
|
||
vm.expectRevert("Code hash is non-zero"); | ||
factory.createCappedMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
} | ||
} | ||
|
||
contract GetMinter is ZkCappedMinterV2FactoryTest { | ||
function testFuzz_ReturnsCorrectMinterAddress(address _cappedMinterAdmin, uint256 _cap, uint256 _saltNonce) public { | ||
_assumeValidAddress(_cappedMinterAdmin); | ||
_cap = _boundToReasonableCap(_cap); | ||
|
||
address expectedMinterAddress = | ||
factory.getMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
|
||
address minterAddress = | ||
factory.createCappedMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
|
||
assertEq(minterAddress, expectedMinterAddress); | ||
} | ||
|
||
function testFuzz_GetMinterWithoutDeployment(address _cappedMinterAdmin, uint256 _cap, uint256 _saltNonce) public { | ||
_assumeValidAddress(_cappedMinterAdmin); | ||
_cap = _boundToReasonableCap(_cap); | ||
|
||
address expectedMinterAddress = | ||
factory.getMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
|
||
uint256 codeSize; | ||
assembly { | ||
codeSize := extcodesize(expectedMinterAddress) | ||
} | ||
assertEq(codeSize, 0); | ||
|
||
address minterAddress = | ||
factory.createCappedMinter(IMintableAndDelegatable(address(token)), _cappedMinterAdmin, _cap, _saltNonce); | ||
|
||
assembly { | ||
codeSize := extcodesize(expectedMinterAddress) | ||
} | ||
assertGt(codeSize, 0); | ||
assertEq(minterAddress, expectedMinterAddress); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's fix this in a followup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool updating here:
e680c31