Skip to content

Commit

Permalink
feat: copy capped minter for v2 template
Browse files Browse the repository at this point in the history
  • Loading branch information
marcomariscal committed Dec 4, 2024
1 parent 663418c commit 0c54fe2
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 0 deletions.
63 changes: 63 additions & 0 deletions l2-contracts/src/ZkCappedMinterV2.sol
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);
}
}
}
70 changes: 70 additions & 0 deletions l2-contracts/src/ZkCappedMinterV2Factory.sol
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));
}
}
100 changes: 100 additions & 0 deletions l2-contracts/test/ZkCappedMinterV2.t.sol
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);
}
}

0 comments on commit 0c54fe2

Please sign in to comment.