-
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: capped minter v2 nested testing #23
Changes from all commits
7e8117f
d8da7d0
12b292d
b597f72
2030897
f9ed313
7483d1f
59bccb4
f5bb1a8
b85f60f
7eb43d0
29362c9
b3e1cb4
a5c8e37
29a35e0
85bf41a
ab7a46d
01ee4e6
b309fb7
112421e
fe1dbb4
9f8ea6e
2aecdce
e293e2d
b7f4222
726d0bf
31d626f
fb88648
946dc79
d9b3dba
1cfbef7
3379852
004c789
4f29f5c
edf05f6
bceb0f7
6251a0b
e680c31
6ee3fb1
72ef93c
dd15cac
61e7247
1295f88
54315f5
2558249
857ebc6
479ef77
007784b
bfbac0e
1b290b7
8a5f057
493c1a6
7d84ebe
6bb7d1a
e6a8718
fca3ffb
93e4af3
225ba92
1942a64
77acf62
ecb275d
43fa666
7da3772
57e1f4f
ef8ca96
3fc7d3a
bb6854b
0b17e88
7631a29
b0da759
ffc248a
d1a7306
70b49f0
4b8499f
246efdc
37fe9e1
083ef5f
696bd69
91c0ca8
aa47d27
7a5e5a7
eb0d858
fcc7c6a
3441ce2
15a6bf0
2d37a6e
ec8c0db
164ec5d
40c65fb
15f79ac
56d5719
637ac39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,15 +3,15 @@ pragma solidity 0.8.24; | |
|
||
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; | ||
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; | ||
import {IMintableAndDelegatable} from "src/interfaces/IMintableAndDelegatable.sol"; | ||
import {IMintable} from "src/interfaces/IMintable.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 is AccessControl, Pausable { | ||
/// @notice The contract where the tokens will be minted by an authorized minter. | ||
IMintableAndDelegatable public immutable TOKEN; | ||
IMintable public immutable MINTABLE; | ||
|
||
/// @notice The maximum number of tokens that may be minted by the ZkCappedMinter. | ||
uint256 public immutable CAP; | ||
|
@@ -53,20 +53,20 @@ contract ZkCappedMinterV2 is AccessControl, Pausable { | |
error ZkCappedMinterV2__InvalidTime(); | ||
|
||
/// @notice Constructor for a new ZkCappedMinterV2 contract | ||
/// @param _token The token contract where tokens will be minted. | ||
/// @param _mintable The contract where tokens will be minted. | ||
/// @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. | ||
/// @param _startTime The timestamp when minting can begin. | ||
/// @param _expirationTime The timestamp after which minting is no longer allowed (inclusive). | ||
constructor(IMintableAndDelegatable _token, address _admin, uint256 _cap, uint48 _startTime, uint48 _expirationTime) { | ||
constructor(IMintable _mintable, address _admin, uint256 _cap, uint48 _startTime, uint48 _expirationTime) { | ||
if (_startTime > _expirationTime) { | ||
revert ZkCappedMinterV2__InvalidTime(); | ||
} | ||
if (_startTime < block.timestamp) { | ||
revert ZkCappedMinterV2__InvalidTime(); | ||
} | ||
|
||
TOKEN = _token; | ||
MINTABLE = _mintable; | ||
CAP = _cap; | ||
START_TIME = _startTime; | ||
EXPIRATION_TIME = _expirationTime; | ||
|
@@ -102,8 +102,10 @@ contract ZkCappedMinterV2 is AccessControl, Pausable { | |
_requireNotPaused(); | ||
_checkRole(MINTER_ROLE, msg.sender); | ||
_revertIfCapExceeded(_amount); | ||
|
||
minted += _amount; | ||
TOKEN.mint(_to, _amount); | ||
|
||
MINTABLE.mint(_to, _amount); | ||
} | ||
|
||
/// @notice Reverts if the amount of new tokens will increase the minted tokens beyond the mint cap. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,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 {IMintable} from "src/interfaces/IMintable.sol"; | ||
import {ZkCappedMinterV2} from "src/ZkCappedMinterV2.sol"; | ||
import {console2} from "forge-std/Test.sol"; | ||
|
||
|
@@ -21,17 +21,25 @@ contract ZkCappedMinterV2Test is ZkTokenTest { | |
DEFAULT_START_TIME = uint48(vm.getBlockTimestamp()); | ||
DEFAULT_EXPIRATION_TIME = uint48(DEFAULT_START_TIME + 3 days); | ||
|
||
cappedMinter = _createCappedMinter(cappedMinterAdmin, DEFAULT_CAP, DEFAULT_START_TIME, DEFAULT_EXPIRATION_TIME); | ||
cappedMinter = | ||
_createCappedMinter(address(token), cappedMinterAdmin, DEFAULT_CAP, DEFAULT_START_TIME, DEFAULT_EXPIRATION_TIME); | ||
|
||
_grantMinterRoleToCappedMinter(address(cappedMinter)); | ||
} | ||
|
||
function _grantMinterRoleToCappedMinter(address _cappedMinter) internal { | ||
vm.prank(admin); | ||
token.grantRole(MINTER_ROLE, address(cappedMinter)); | ||
token.grantRole(MINTER_ROLE, address(_cappedMinter)); | ||
} | ||
|
||
function _createCappedMinter(address _admin, uint256 _cap, uint48 _startTime, uint48 _expirationTime) | ||
internal | ||
returns (ZkCappedMinterV2) | ||
{ | ||
return new ZkCappedMinterV2(IMintableAndDelegatable(address(token)), _admin, _cap, _startTime, _expirationTime); | ||
function _createCappedMinter( | ||
address _mintable, | ||
address _admin, | ||
uint256 _cap, | ||
uint48 _startTime, | ||
uint48 _expirationTime | ||
) internal returns (ZkCappedMinterV2) { | ||
return new ZkCappedMinterV2(IMintable(_mintable), _admin, _cap, _startTime, _expirationTime); | ||
} | ||
|
||
function _boundToValidTimeControls(uint48 _startTime, uint48 _expirationTime) internal view returns (uint48, uint48) { | ||
|
@@ -69,8 +77,8 @@ contract Constructor is ZkCappedMinterV2Test { | |
(_startTime, _expirationTime) = _boundToValidTimeControls(_startTime, _expirationTime); | ||
vm.warp(_startTime); | ||
|
||
ZkCappedMinterV2 cappedMinter = _createCappedMinter(_admin, _cap, _startTime, _expirationTime); | ||
assertEq(address(cappedMinter.TOKEN()), address(token)); | ||
ZkCappedMinterV2 cappedMinter = _createCappedMinter(address(token), _admin, _cap, _startTime, _expirationTime); | ||
assertEq(address(cappedMinter.MINTABLE()), address(token)); | ||
assertEq(cappedMinter.CAP(), _cap); | ||
assertEq(cappedMinter.START_TIME(), _startTime); | ||
assertEq(cappedMinter.EXPIRATION_TIME(), _expirationTime); | ||
|
@@ -85,7 +93,7 @@ contract Constructor is ZkCappedMinterV2Test { | |
_startTime = uint48(bound(_startTime, 1, type(uint48).max)); | ||
_invalidExpirationTime = uint48(bound(_invalidExpirationTime, 0, _startTime - 1)); | ||
vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__InvalidTime.selector); | ||
_createCappedMinter(_admin, _cap, _startTime, _invalidExpirationTime); | ||
_createCappedMinter(address(token), _admin, _cap, _startTime, _invalidExpirationTime); | ||
} | ||
|
||
function testFuzz_RevertIf_StartTimeInPast(address _admin, uint256 _cap, uint48 _startTime, uint48 _expirationTime) | ||
|
@@ -99,7 +107,7 @@ contract Constructor is ZkCappedMinterV2Test { | |
_expirationTime = uint48(bound(_expirationTime, _pastStartTime + 1, type(uint48).max)); | ||
|
||
vm.expectRevert(ZkCappedMinterV2.ZkCappedMinterV2__InvalidTime.selector); | ||
_createCappedMinter(_admin, _cap, _pastStartTime, _expirationTime); | ||
_createCappedMinter(address(token), _admin, _cap, _pastStartTime, _expirationTime); | ||
} | ||
} | ||
|
||
|
@@ -165,6 +173,109 @@ contract Mint is ZkCappedMinterV2Test { | |
cappedMinter.mint(_receiver, _amount); | ||
} | ||
|
||
function testFuzz_NestedMintingContributesToParentCap( | ||
address _parentAdmin, | ||
address _childAdmin, | ||
address _minter, | ||
address _receiver, | ||
uint256 _parentCap, | ||
uint256 _childCap, | ||
uint256 _amount1, | ||
uint256 _amount2, | ||
uint48 _startTime, | ||
uint48 _expirationTime | ||
) public { | ||
// Setup caps where child cap is less or equal to parent cap | ||
_parentCap = bound(_parentCap, 2, DEFAULT_CAP); | ||
_childCap = bound(_childCap, 2, _parentCap); | ||
|
||
// Two amounts that together are within child cap | ||
uint256 maxAmount = _childCap / 2; | ||
_amount1 = bound(_amount1, 1, maxAmount); | ||
_amount2 = bound(_amount2, 1, maxAmount); | ||
|
||
vm.assume(_receiver != address(0)); | ||
|
||
(_startTime, _expirationTime) = _boundToValidTimeControls(_startTime, _expirationTime); | ||
vm.warp(_startTime); | ||
|
||
ZkCappedMinterV2 parentMinter = | ||
_createCappedMinter(address(token), _parentAdmin, _parentCap, _startTime, _expirationTime); | ||
// Create child minter with parent minter as token | ||
ZkCappedMinterV2 childMinter = | ||
_createCappedMinter(address(parentMinter), _childAdmin, _childCap, _startTime, _expirationTime); | ||
|
||
_grantMinterRoleToCappedMinter(address(parentMinter)); | ||
|
||
// Parent minter grants MINTER_ROLE to child minter | ||
_grantMinterRole(parentMinter, _parentAdmin, address(childMinter)); | ||
// Child minter grants MINTER_ROLE to minter | ||
_grantMinterRole(childMinter, _childAdmin, _minter); | ||
|
||
uint256 balanceBefore = token.balanceOf(_receiver); | ||
|
||
// Minter mints through child contract | ||
vm.prank(_minter); | ||
childMinter.mint(_receiver, _amount1); | ||
|
||
uint256 balanceAfter = token.balanceOf(_receiver); | ||
|
||
// Verify amounts are tracked in both contracts | ||
assertEq(childMinter.minted(), _amount1); | ||
assertEq(parentMinter.minted(), _amount1); | ||
assertEq(balanceAfter, balanceBefore + _amount1); | ||
|
||
// Mint again | ||
vm.prank(_minter); | ||
childMinter.mint(_receiver, _amount2); | ||
|
||
balanceAfter = token.balanceOf(_receiver); | ||
|
||
// Verify total amounts are tracked in both contracts | ||
assertEq(childMinter.minted(), _amount1 + _amount2); | ||
assertEq(parentMinter.minted(), _amount1 + _amount2); | ||
assertEq(balanceAfter, balanceBefore + _amount1 + _amount2); | ||
} | ||
|
||
function testFuzz_ParentMintDoesNotCountAgainstChildCap( | ||
address _parentAdmin, | ||
address _childAdmin, | ||
address _minter, | ||
address _receiver, | ||
uint256 _parentCap, | ||
uint256 _childCap, | ||
uint256 _amount, | ||
uint48 _startTime, | ||
uint48 _expirationTime | ||
) public { | ||
vm.assume(_receiver != address(0)); | ||
|
||
_parentCap = bound(_parentCap, 1, DEFAULT_CAP); | ||
_amount = bound(_amount, 1, _parentCap); | ||
|
||
(_startTime, _expirationTime) = _boundToValidTimeControls(_startTime, _expirationTime); | ||
vm.warp(_startTime); | ||
|
||
ZkCappedMinterV2 parentMinter = | ||
_createCappedMinter(address(token), _parentAdmin, _parentCap, _startTime, _expirationTime); | ||
// Create child minter with parent minter as token | ||
ZkCappedMinterV2 childMinter = | ||
_createCappedMinter(address(parentMinter), _childAdmin, _childCap, _startTime, _expirationTime); | ||
|
||
_grantMinterRoleToCappedMinter(address(parentMinter)); | ||
|
||
// Parent minter grants MINTER_ROLE to minter | ||
_grantMinterRole(parentMinter, _parentAdmin, _minter); | ||
|
||
// Minter mints through parent contract | ||
vm.prank(_minter); | ||
parentMinter.mint(_receiver, _amount); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What we really want to test is that a minter on the child can mint from the child and that this minting consumes contributes towards the limit of the parent minter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I would change some naming in the main contract to reference |
||
|
||
// Verify child contract is not affected | ||
assertEq(childMinter.minted(), 0); | ||
assertEq(parentMinter.minted(), _amount); | ||
} | ||
|
||
function testFuzz_RevertIf_MintAttemptedByNonMinter(address _nonMinter, uint256 _amount) public { | ||
_amount = bound(_amount, 1, DEFAULT_CAP); | ||
|
||
|
@@ -230,6 +341,49 @@ contract Mint is ZkCappedMinterV2Test { | |
vm.prank(_minter); | ||
cappedMinter.mint(_receiver, _amount); | ||
} | ||
|
||
function testFuzz_RevertIf_ChildExceedsParentMintEvenThoughChildCapIsHigher( | ||
address _parentAdmin, | ||
address _childAdmin, | ||
address _minter, | ||
address _receiver, | ||
uint256 _parentCap, | ||
uint256 _childCap, | ||
uint256 _amount, | ||
uint48 _startTime, | ||
uint48 _expirationTime | ||
) public { | ||
// Parent has lower cap than child | ||
_parentCap = bound(_parentCap, 2, MAX_MINT_SUPPLY - 1); | ||
_childCap = bound(_childCap, _parentCap + 1, MAX_MINT_SUPPLY); | ||
// Amount exceeds parent cap but is within child cap | ||
_amount = bound(_amount, _parentCap + 1, _childCap); | ||
|
||
vm.assume(_parentAdmin != address(0)); | ||
vm.assume(_childAdmin != address(0)); | ||
vm.assume(_minter != address(0)); | ||
vm.assume(_receiver != address(0)); | ||
vm.assume(_receiver != initMintReceiver); | ||
|
||
(_startTime, _expirationTime) = _boundToValidTimeControls(_startTime, _expirationTime); | ||
vm.warp(_startTime); | ||
|
||
ZkCappedMinterV2 parentMinter = | ||
_createCappedMinter(address(token), _parentAdmin, _parentCap, _startTime, _expirationTime); | ||
ZkCappedMinterV2 childMinter = | ||
_createCappedMinter(address(parentMinter), _childAdmin, _childCap, _startTime, _expirationTime); | ||
|
||
// Parent minter grants MINTER_ROLE to child minter | ||
_grantMinterRole(parentMinter, _parentAdmin, address(childMinter)); | ||
|
||
// Child tries to mint more than parent's cap | ||
vm.startPrank(address(childMinter)); | ||
vm.expectRevert( | ||
abi.encodeWithSelector(ZkCappedMinterV2.ZkCappedMinterV2__CapExceeded.selector, address(childMinter), _amount) | ||
); | ||
parentMinter.mint(_receiver, _amount); | ||
vm.stopPrank(); | ||
} | ||
} | ||
|
||
contract Pause is ZkCappedMinterV2Test { | ||
|
@@ -342,7 +496,7 @@ contract SetMetadataURI is ZkCappedMinterV2Test { | |
{ | ||
(_startTime, _expirationTime) = _boundToValidTimeControls(_startTime, _expirationTime); | ||
|
||
ZkCappedMinterV2 cappedMinter = _createCappedMinter(_admin, _cap, _startTime, _expirationTime); | ||
ZkCappedMinterV2 cappedMinter = _createCappedMinter(address(token), _admin, _cap, _startTime, _expirationTime); | ||
assertEq(cappedMinter.metadataURI(), bytes32(0)); | ||
} | ||
|
||
|
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.
Maybe also add a test verifying the parent mint does not count against the child mint
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.
40c65fb