Skip to content
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: capped minter v2 minter role #4

Merged
merged 45 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9f8ea6e
feat: inherit from AccessControl and update accordingly
marcomariscal Dec 4, 2024
2aecdce
feat: update tests to handle new auth functionality
marcomariscal Dec 4, 2024
e293e2d
feat: add minter role
marcomariscal Dec 4, 2024
b7f4222
feat: test admin can't mint by default
marcomariscal Dec 4, 2024
726d0bf
fix: name
marcomariscal Dec 4, 2024
31d626f
fix: structure
marcomariscal Dec 4, 2024
e680c31
chore: comment
marcomariscal Dec 10, 2024
6ee3fb1
feat: _grantMinterRole helper
marcomariscal Dec 10, 2024
72ef93c
fix: use _checkRole and remove unused unauth error
marcomariscal Dec 10, 2024
dd15cac
fix: assume cap
marcomariscal Dec 10, 2024
61e7247
fix: toml
marcomariscal Dec 10, 2024
1295f88
chore: format
marcomariscal Dec 10, 2024
54315f5
fix: import
marcomariscal Dec 10, 2024
2558249
fix: compile
marcomariscal Dec 10, 2024
857ebc6
chore: name
marcomariscal Dec 10, 2024
479ef77
fix: import
marcomariscal Dec 10, 2024
007784b
fix: remove unnecessary
marcomariscal Dec 10, 2024
c462fbe
fix: remove unnecessary remapping
marcomariscal Dec 12, 2024
9ee9e5b
fix: revert permish
marcomariscal Dec 12, 2024
d0798f7
fix: revert to original
marcomariscal Dec 12, 2024
783cac7
fix: ci era test node version
marcomariscal Dec 12, 2024
d68fd3a
Merge branch 'feat/capped-minter-v2' into feat/capped-minter-v2-minte…
marcomariscal Dec 12, 2024
ad3713d
fix: revert
marcomariscal Dec 12, 2024
67944e4
fix: copy root ci.yml to l2-contracts
marcomariscal Dec 12, 2024
21dff29
Revert "fix: copy root ci.yml to l2-contracts"
marcomariscal Dec 12, 2024
b8fbe7e
chore: lock
marcomariscal Dec 12, 2024
513a10d
fix: ci
marcomariscal Dec 12, 2024
09308b6
fix: ci
marcomariscal Dec 12, 2024
7edf867
fix: remove era node
marcomariscal Dec 12, 2024
153717f
fix: ci
marcomariscal Dec 12, 2024
c2a3347
fix: no zk flag for now
marcomariscal Dec 12, 2024
b2ed160
fix: use build
marcomariscal Dec 12, 2024
4a60adf
fix: ci
marcomariscal Dec 12, 2024
cb54f31
fix: cache
marcomariscal Dec 12, 2024
a38ef6b
fix: simplify
marcomariscal Dec 12, 2024
c37c3e1
fix: simplify
marcomariscal Dec 12, 2024
d15cfe7
fix: revert
marcomariscal Dec 12, 2024
4a6edfd
chore: remove comment
marcomariscal Dec 12, 2024
5cb22a6
fix: remove forge i to see if it works
marcomariscal Dec 12, 2024
4f19a0a
Revert "fix: remove forge i to see if it works"
marcomariscal Dec 12, 2024
b3814ba
fix: building with forge
marcomariscal Dec 12, 2024
e769eca
fix: use npm scripts and re-add hardhat
marcomariscal Dec 12, 2024
4068298
chore: name
marcomariscal Dec 12, 2024
5adc73c
fix: match path
marcomariscal Dec 12, 2024
6f17280
fix: remove unnecessary match path
marcomariscal Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion l2-contracts/foundry.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[profile.default]
evm_version = "paris"
fs_permissions = [{ access = "read", path = "./zkout" }]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, why was this added?

fuzz = { runs = 50 }
optimizer = true
optimizer_runs = 10_000_000
Expand All @@ -8,10 +9,10 @@
"@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/",
"@openzeppelin/foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/",
"@murky/=lib/murky/",
"src/=src/",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this necessary?

]
solc_version = "0.8.24"
verbosity = 3
fs_permissions = [{ access = "read", path = "./zkout" }]

[profile.ci]
fuzz = { runs = 5000 }
Expand Down
27 changes: 9 additions & 18 deletions l2-contracts/src/ZkCappedMinterV2.sol
Original file line number Diff line number Diff line change
@@ -1,58 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
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 {
contract ZkCappedMinterV2 is AccessControl {
/// @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;

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

/// @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
/// @notice Constructor for a new ZkCappedMinterV2 contract
/// @param _token The token contract where tokens will be minted.
/// @param _admin The address that is allowed to mint tokens.
/// @param _admin The address that will be granted the admin role.
/// @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;

_grantRole(DEFAULT_ADMIN_ROLE, _admin);
}

/// @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();
_checkRole(MINTER_ROLE, msg.sender);
_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 {
Expand Down
2 changes: 1 addition & 1 deletion l2-contracts/test/ZkCappedMinterFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ contract ZkCappedMinterFactoryTest is ZkTokenTest {

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/ZkCappedMinter.sol/ZkCappedMinter.json");
Expand Down
92 changes: 78 additions & 14 deletions l2-contracts/test/ZkCappedMinterV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.24;

import {ZkTokenTest} from "test/utils/ZkTokenTest.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {IMintableAndDelegatable} from "src/interfaces/IMintableAndDelegatable.sol";
import {ZkCappedMinterV2} from "src/ZkCappedMinterV2.sol";
import {console2} from "forge-std/Test.sol";
Expand All @@ -17,6 +18,11 @@ contract ZkCappedMinterV2Test is ZkTokenTest {
token.grantRole(MINTER_ROLE, address(cappedMinter));
return cappedMinter;
}

function _grantMinterRole(ZkCappedMinterV2 _cappedMinter, address _cappedMinterAdmin, address _minter) internal {
vm.prank(_cappedMinterAdmin);
_cappedMinter.grantRole(MINTER_ROLE, _minter);
}
}

contract Constructor is ZkCappedMinterV2Test {
Expand All @@ -26,14 +32,15 @@ contract Constructor is ZkCappedMinterV2Test {
_cap = bound(_cap, 0, MAX_MINT_SUPPLY);
ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap);
assertEq(address(cappedMinter.TOKEN()), address(token));
assertEq(cappedMinter.ADMIN(), _cappedMinterAdmin);
assertEq(cappedMinter.hasRole(DEFAULT_ADMIN_ROLE, _cappedMinterAdmin), true);
assertEq(cappedMinter.CAP(), _cap);
}
}

contract Mint is ZkCappedMinterV2Test {
function testFuzz_MintsNewTokensWhenTheAmountRequestedIsBelowTheCap(
address _cappedMinterAdmin,
address _minter,
address _receiver,
uint256 _cap,
uint256 _amount
Expand All @@ -42,14 +49,20 @@ contract Mint is ZkCappedMinterV2Test {
_amount = bound(_amount, 1, MAX_MINT_SUPPLY);
vm.assume(_cap > _amount);
vm.assume(_receiver != address(0) && _receiver != initMintReceiver);
vm.assume(_minter != address(0));

ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap);
vm.prank(_cappedMinterAdmin);

_grantMinterRole(cappedMinter, _cappedMinterAdmin, _minter);

vm.prank(_minter);
cappedMinter.mint(_receiver, _amount);
assertEq(token.balanceOf(_receiver), _amount);
}

function testFuzz_MintsNewTokensInSuccessionToDifferentAccountsWhileRemainingBelowCap(
address _cappedMinterAdmin,
address _minter,
address _receiver1,
address _receiver2,
uint256 _cap,
Expand All @@ -63,38 +76,89 @@ contract Mint is ZkCappedMinterV2Test {
vm.assume(_receiver1 != address(0) && _receiver1 != initMintReceiver);
vm.assume(_receiver2 != address(0) && _receiver2 != initMintReceiver);
vm.assume(_receiver1 != _receiver2);
vm.assume(_minter != address(0));

ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap);
vm.startPrank(_cappedMinterAdmin);

_grantMinterRole(cappedMinter, _cappedMinterAdmin, _minter);

vm.startPrank(_minter);
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)
function testFuzz_RevertIf_MintAttemptedByNonMinter(address _cappedMinterAdmin, address _nonMinter, uint256 _cap)
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);

vm.assume(_nonMinter != address(0));
vm.assume(!cappedMinter.hasRole(MINTER_ROLE, _nonMinter));

vm.expectRevert(
bytes(
string.concat(
"AccessControl: account ",
Strings.toHexString(uint160(_nonMinter), 20),
" is missing role ",
Strings.toHexString(uint256(MINTER_ROLE))
)
)
);
vm.prank(_nonMinter);
cappedMinter.mint(_nonMinter, _cap);
}

function testFuzz_RevertIf_CapExceededOnMint(address _cappedMinterAdmin, address _receiver, uint256 _cap) public {
function testFuzz_RevertIf_CapExceededOnMint(
address _cappedMinterAdmin,
address _minter,
address _receiver,
uint256 _cap
) public {
_cap = bound(_cap, 4, MAX_MINT_SUPPLY);
vm.assume(_receiver != address(0) && _receiver != initMintReceiver);
vm.assume(_minter != address(0));

ZkCappedMinterV2 cappedMinter = createCappedMinter(_cappedMinterAdmin, _cap);
vm.prank(_cappedMinterAdmin);

_grantMinterRole(cappedMinter, _cappedMinterAdmin, _minter);

vm.prank(_minter);
cappedMinter.mint(_receiver, _cap);
assertEq(token.balanceOf(_receiver), _cap);

vm.expectRevert(abi.encodeWithSelector(ZkCappedMinterV2.ZkCappedMinterV2__CapExceeded.selector, _minter, _cap));
vm.prank(_minter);
cappedMinter.mint(_receiver, _cap);
}

function testFuzz_RevertIf_AdminMintsByDefault(address _admin, address _receiver, uint256 _cap, uint256 _amount)
public
{
_cap = bound(_cap, 0, MAX_MINT_SUPPLY);
vm.assume(_cap > 0);
_amount = bound(_amount, 1, _cap);
vm.assume(_admin != address(0));
vm.assume(_receiver != address(0) && _receiver != initMintReceiver);

ZkCappedMinterV2 cappedMinter = createCappedMinter(_admin, _cap);

vm.expectRevert(
abi.encodeWithSelector(ZkCappedMinterV2.ZkCappedMinterV2__CapExceeded.selector, _cappedMinterAdmin, _cap)
bytes(
string.concat(
"AccessControl: account ",
Strings.toHexString(uint160(_admin), 20),
" is missing role ",
Strings.toHexString(uint256(MINTER_ROLE))
)
)
);
vm.prank(_cappedMinterAdmin);
cappedMinter.mint(_receiver, _cap);
vm.prank(_admin);
cappedMinter.mint(_receiver, _amount);
}
}
2 changes: 1 addition & 1 deletion l2-contracts/test/ZkCappedMinterV2Factory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ contract CreateCappedMinter is ZkCappedMinterV2FactoryTest {

ZkCappedMinterV2 minter = ZkCappedMinterV2(minterAddress);
assertEq(address(minter.TOKEN()), address(token));
assertEq(minter.ADMIN(), _cappedMinterAdmin);
assertEq(minter.hasRole(DEFAULT_ADMIN_ROLE, _cappedMinterAdmin), true);
assertEq(minter.CAP(), _cap);
}

Expand Down
Loading