UNPKG

superfuse-wizard

Version:

Interactive smart contract generator based on Superchain 's interoperability standard.

575 lines (434 loc) 19.8 kB
import type { TestContract, BaseFunction} from './contract'; import { TestBuilder } from "./contract"; import type { Access } from './set-access-control'; import {setAccessControl } from './set-access-control'; import type { SharedL2NativeSuperchainERC20Options, OpSec} from '../shared/option-l2-native-superchain-ERC20'; import { withCommonDefaults, defaults as commonDefaults } from '../shared/option-l2-native-superchain-ERC20'; import { printTestContract } from "./print"; import { setInfo } from "./set-info"; import { transformToLowerCamelCase } from '../utils/transform-camel'; import { defineFunctions } from '../utils/define-functions'; function withTestDefaults(opts: SharedL2NativeSuperchainERC20Options): Required<SharedL2NativeSuperchainERC20Options> { return { ...opts, ...withCommonDefaults(opts), ownerAddress: opts.ownerAddress || commonDefaults.ownerAddress, burnable: opts.burnable ?? commonDefaults.burnable, pausable: opts.pausable ?? commonDefaults.pausable, premint: opts.premint || commonDefaults.premint, mintable: opts.mintable ?? commonDefaults.mintable, minterAddress: opts.minterAddress || commonDefaults.minterAddress, permit: opts.permit ?? commonDefaults.permit, votes: opts.votes ?? commonDefaults.votes, flashmint: opts.flashmint ?? commonDefaults.flashmint, }; } export function printTestL2NativeSuperchainERC20(opts: SharedL2NativeSuperchainERC20Options = commonDefaults): string { return printTestContract(buildTestL2NativeSuperchainERC20(opts)); } export function buildTestL2NativeSuperchainERC20(opts: SharedL2NativeSuperchainERC20Options): TestContract { const allOpts = withTestDefaults(opts); const c = new TestBuilder(allOpts.testName); const { access, upgradeable, contractInfo } = allOpts; addBase(c, allOpts); setOpsec(c, allOpts.opSec); setConstructorTestLogic(c, allOpts); setERC7802TestLogic(c, allOpts); const camelCaseContractName = transformToLowerCamelCase(allOpts.contractName); if (allOpts.mintable) { addMintable(c, access, camelCaseContractName, opts.minterAddress); } setInfo(c, allOpts.testInfo); return c; } // const transformToLowerCamelCase = (input: string): string => // input.charAt(0).toLowerCase() + input.slice(1); function addBase(c: TestBuilder, allOpts: Required<SharedL2NativeSuperchainERC20Options>) { const camelCaseContractName = transformToLowerCamelCase(allOpts.contractName); const console = { name: 'console', path: '@forge-std-v1.9.1/console.sol', }; c.addImportOnly(console); const Test = { name: 'Test', path: '@forge-std-v1.9.1/Test.sol', }; c.addParent(Test, []); const Predeploys = { name: 'Predeploys', path: '@superfuse-core/libraries/Predeploys.sol', }; c.addImportOnly(Predeploys); const IERC20 = { name: 'IERC20', path: '@openzeppelin-v0.5.0.2/token/ERC20/IERC20.sol', }; c.addImportOnly(IERC20); const SuperchainERC20 = { name: 'SuperchainERC20', path: '@superfuse-core/L2/SuperchainERC20.sol', }; c.addImportOnly(SuperchainERC20); const IERC7802 = { name: 'IERC7802', path: '@superfuse-core/interfaces/L2/IERC7802.sol', }; c.addImportOnly(IERC7802); const ISuperchainERC20 = { name: 'ISuperchainERC20', path: '@superfuse-core/interfaces/L2/ISuperchainERC20.sol', }; c.addImportOnly(ISuperchainERC20); const L2NativeSuperchainERC20 = { name: `${allOpts.contractName}`, path: `@main/${allOpts.contractName}.sol`, }; c.addImportOnly(L2NativeSuperchainERC20); const IDeployer = { name: 'IDeployer', path: '@superfuse-deploy/deployer/DeployScript.sol', }; c.addImportOnly(IDeployer); const getDeployer = { name: 'getDeployer', path: '@superfuse-deploy/deployer/DeployScript.sol', }; c.addImportOnly(getDeployer); const DeployL2NativeSuperchainERC20Script = { name: `${allOpts.deployName}`, path: `@script/${allOpts.conventionNumber}_${allOpts.deployName}.s.sol`, }; c.addImportOnly(DeployL2NativeSuperchainERC20Script); c.addVariable(`address alice;`); c.addVariable(`address bob;`); c.addVariable(`IDeployer deployerProcedue;`); c.addVariable(`address internal constant ZERO_ADDRESS = address(0);`); c.addVariable(`address internal constant SUPERCHAIN_TOKEN_BRIDGE = Predeploys.SUPERCHAIN_TOKEN_BRIDGE;`); c.addVariable(`address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;`); c.addVariable(`${allOpts.contractName} public ${camelCaseContractName};`); c.addFunctionCode(`alice = makeAddr("alice"); bob = makeAddr("bob"); deployerProcedue = getDeployer(); deployerProcedue.setAutoBroadcast(false); console.log("Setup ${allOpts.contractName} ... "); ${allOpts.deployName} ${camelCaseContractName}Deployments = new ${allOpts.deployName}(); ${camelCaseContractName} = ${camelCaseContractName}Deployments.deploy(); deployerProcedue.deactivatePrank();`, functions.setUp); } function setOpsec(c: TestBuilder, opsec: OpSec) { switch (opsec) { case 'address': { c.addVariable(`address owner = vm.envAddress("DEPLOYER_ADDRESS");`); break; } case 'key': { c.addVariable(`uint256 ownerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");`); c.addVariable(`address owner = vm.envOr("DEPLOYER_ADDRESS", vm.addr(ownerPrivateKey));`); break; } case 'mnemonic': { c.addVariable(`string mnemonic = vm.envString("MNEMONIC");`); c.addVariable(`uint256 ownerPrivateKey = vm.deriveKey(mnemonic, "m/44'/60'/0'/0/", 1);`); c.addVariable(`address owner = vm.envOr("DEPLOYER_ADDRESS", vm.addr(ownerPrivateKey));`); break; } } } function setConstructorTestLogic(c: TestBuilder, allOpts: Required<SharedL2NativeSuperchainERC20Options>) { // const camelCaseContractName = transformToLowerCamelCase(allOpts.contractName); // c.addFunctionCode(`assertEq(${camelCaseContractName}.name(), "${allOpts.tokenName}"); // assertEq(${camelCaseContractName}.symbol(), "${allOpts.tokenSymbol}"); // assertEq(${camelCaseContractName}.decimals(), ${allOpts.decimals}); // assertEq(${camelCaseContractName}.owner(), owner);`, functions.test_constructor); const camelCaseContractName = transformToLowerCamelCase(allOpts.contractName); c.addFunctionCode(`assertEq(${camelCaseContractName}.name(), "${allOpts.tokenName}"); assertEq(${camelCaseContractName}.symbol(), "${allOpts.tokenSymbol}"); assertEq(${camelCaseContractName}.decimals(), ${allOpts.decimals});`, functions.test_constructor); // to do : handle default admin case // if (allOpts.access === 'roles') { // c.addFunctionCode(`assertEq(${camelCaseContractName}.hasRole(), 0);`, functions.test_constructor); // } } function setERC7802TestLogic(c: TestBuilder, allOpts: Required<SharedL2NativeSuperchainERC20Options>) { const camelCaseContractName = transformToLowerCamelCase(allOpts.contractName); // _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) c.addFunctionCode(`vm.mockCall(_receiver, _calldata, _returned); vm.expectCall(_receiver, _calldata);`, functions._mockAndExpect); // testFuzz_crosschainMint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) c.addFunctionCode(`// Ensure the caller is not the bridge vm.assume(_caller != SUPERCHAIN_TOKEN_BRIDGE); // Expect the revert with 'Unauthorized' selector vm.expectRevert(ISuperchainERC20.Unauthorized.selector); // Call the 'mint' function with the non-bridge caller vm.prank(_caller); ${camelCaseContractName}.crosschainMint(_to, _amount);`, functions.testFuzz_crosschainMint_callerNotBridge_reverts); // testFuzz_crosschainMint_succeeds(address _to, uint256 _amount) c.addFunctionCode(`// Ensure '_to' is not the zero address vm.assume(_to != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint208).max); // Get the total supply and balance of '_to' before the mint to compare later on the assertions uint256 _totalSupplyBefore = ${camelCaseContractName}.totalSupply(); uint256 _toBalanceBefore = ${camelCaseContractName}.balanceOf(_to); // Look for the emit of the 'Transfer' event vm.expectEmit(address(${camelCaseContractName})); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); // Look for the emit of the 'CrosschainMint' event vm.expectEmit(address(${camelCaseContractName})); emit IERC7802.CrosschainMint(_to, _amount, SUPERCHAIN_TOKEN_BRIDGE); // Call the 'mint' function with the bridge caller vm.prank(SUPERCHAIN_TOKEN_BRIDGE); ${camelCaseContractName}.crosschainMint(_to, _amount); // Check the total supply and balance of '_to' after the mint were updated correctly assertEq(${camelCaseContractName}.totalSupply(), _totalSupplyBefore + _amount); assertEq(${camelCaseContractName}.balanceOf(_to), _toBalanceBefore + _amount);`, functions.testFuzz_crosschainMint_succeeds); // testFuzz_crosschainBurn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) c.addFunctionCode(`/// Ensure the caller is not the bridge vm.assume(_caller != SUPERCHAIN_TOKEN_BRIDGE); // Expect the revert with 'Unauthorized' selector vm.expectRevert(ISuperchainERC20.Unauthorized.selector); // Call the 'burn' function with the non-bridge caller vm.prank(_caller); ${camelCaseContractName}.crosschainBurn(_from, _amount);`, functions.testFuzz_crosschainBurn_callerNotBridge_reverts); // testFuzz_crosschainBurn_succeeds(address _from, uint256 _amount) c.addFunctionCode(`// Ensure '_from' is not the zero address vm.assume(_from != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint208).max); // Mint some tokens to '_from' so then they can be burned vm.prank(SUPERCHAIN_TOKEN_BRIDGE); ${camelCaseContractName}.crosschainMint(_from, _amount); // Get the total supply and balance of '_from' before the burn to compare later on the assertions uint256 _totalSupplyBefore = ${camelCaseContractName}.totalSupply(); uint256 _fromBalanceBefore = ${camelCaseContractName}.balanceOf(_from); // Look for the emit of the 'Transfer' event vm.expectEmit(address(${camelCaseContractName})); emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); // Look for the emit of the 'CrosschainBurn' event vm.expectEmit(address(${camelCaseContractName})); emit IERC7802.CrosschainBurn(_from, _amount, SUPERCHAIN_TOKEN_BRIDGE); // Call the 'burn' function with the bridge caller vm.prank(SUPERCHAIN_TOKEN_BRIDGE); ${camelCaseContractName}.crosschainBurn(_from, _amount); // Check the total supply and balance of '_from' after the burn were updated correctly assertEq(${camelCaseContractName}.totalSupply(), _totalSupplyBefore - _amount); assertEq(${camelCaseContractName}.balanceOf(_from), _fromBalanceBefore - _amount);`, functions.testFuzz_crosschainBurn_succeeds); } function addMintable(c: TestBuilder, access: Access, targetContractName: string, minterAddress: string | undefined) { const IERC20 = { name: 'IERC20', path: '@openzeppelin-v0.5.0.2/token/ERC20/IERC20.sol', }; c.addImportOnly(IERC20); const ERC20 = { name: 'ERC20', path: '@solady-v0.1.8/tokens/ERC20.sol', }; c.addImportOnly(ERC20); setAccessControl(c, access, targetContractName); // testFuzz_mintTo_succeeds(address _to, uint256 _amount) c.addFunctionCode(`vm.expectEmit(true, true, true, true); emit IERC20.Transfer(address(0), _to, _amount); vm.prank(owner); ${targetContractName}.mintTo(_to, _amount); assertEq(${targetContractName}.totalSupply(), _amount); assertEq(${targetContractName}.balanceOf(_to), _amount);`, get_2args_testFuzz_mintTo_succeeds()); // // testFuzz_mintTo_succeeds(address _minter, address _to, uint256 _amount) // c.addFunctionCode(`vm.assume(_minter != owner); // // Expect the revert with 'Unauthorized' selector // vm.expectRevert(Ownable.Unauthorized.selector); // vm.prank(_minter); // ${targetContractName}.mintTo(_to, _amount);`, get_3args_testFuzz_mintTo_succeeds()); // testFuzz_transfer_succeeds(address _sender, uint256 _amount) c.addFunctionCode(`vm.assume(_sender != ZERO_ADDRESS); vm.assume(_sender != bob); vm.prank(owner); ${targetContractName}.mintTo(_sender, _amount); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(_sender, bob, _amount); vm.prank(_sender); assertTrue(${targetContractName}.transfer(bob, _amount)); assertEq(${targetContractName}.totalSupply(), _amount); assertEq(${targetContractName}.balanceOf(_sender), 0); assertEq(${targetContractName}.balanceOf(bob), _amount);`, functions.testFuzz_transfer_succeeds); // testFuzz_transferFrom_succeeds(address _spender, uint256 _amount) c.addFunctionCode(`vm.assume(_spender != ZERO_ADDRESS); vm.assume(_spender != bob); vm.assume(_spender != alice); vm.prank(owner); ${targetContractName}.mintTo(bob, _amount); vm.prank(bob); ${targetContractName}.approve(_spender, _amount); vm.prank(_spender); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(bob, alice, _amount); assertTrue(${targetContractName}.transferFrom(bob, alice, _amount)); assertEq(${targetContractName}.balanceOf(bob), 0); assertEq(${targetContractName}.balanceOf(alice), _amount);`, functions.testFuzz_transferFrom_succeeds); // testFuzz_transferInsufficientBalance_reverts(address _to, uint256 _mintAmount, uint256 _sendAmount) c.addFunctionCode(`vm.assume(_mintAmount < type(uint256).max); _sendAmount = bound(_sendAmount, _mintAmount + 1, type(uint256).max); vm.prank(owner); ${targetContractName}.mintTo(address(this), _mintAmount); vm.expectRevert(ERC20.InsufficientBalance.selector); ${targetContractName}.transfer(_to, _sendAmount);`, functions.testFuzz_transferInsufficientBalance_reverts); // testFuzz_transferFromInsufficientAllowance_reverts(address _to, address _from, uint256 _approval, uint256 _amount c.addFunctionCode(`vm.assume(_from != ZERO_ADDRESS); vm.assume(_approval < type(uint256).max); _amount = _bound(_amount, _approval + 1, type(uint256).max); vm.prank(owner); ${targetContractName}.mintTo(_from, _amount); vm.prank(_from); ${targetContractName}.approve(address(this), _approval); vm.expectRevert(ERC20.InsufficientAllowance.selector); ${targetContractName}.transferFrom(_from, _to, _amount);`, functions.testFuzz_transferFromInsufficientAllowance_reverts); switch (access) { case 'ownable': { const Ownable = { name: 'Ownable', path: '@solady-v0.1.8/auth/Ownable.sol', }; c.addImportOnly(Ownable); // testFuzz_mintTo_succeeds(address _minter, address _to, uint256 _amount) c.addFunctionCode(`vm.assume(_minter != owner); // Expect the revert with 'Unauthorized' selector vm.expectRevert(Ownable.Unauthorized.selector); vm.prank(_minter); ${targetContractName}.mintTo(_to, _amount);`, get_3args_testFuzz_mintTo_succeeds()); break; } case 'roles': { const EnumerableRoles = { name: 'EnumerableRoles', path: '@solady-v0.1.8/auth/EnumerableRoles.sol', }; c.addImportOnly(EnumerableRoles); c.addVariable(`address minter = ${minterAddress};`); // // testFuzz_mintTo_succeeds(address _minter, address _to, uint256 _amount) // c.addFunctionCode(`vm.assume(_minter != minter); // // Expect the revert with 'Unauthorized' selector // vm.expectRevert(Ownable.Unauthorized.selector); // vm.prank(_minter); // ${targetContractName}.mintTo(_to, _amount);`, get_3args_testFuzz_mintTo_succeeds()); c.addFunctionCode(`vm.assume(_minter != minter); // Expect the revert with 'EnumerableRolesUnauthorized' selector vm.expectRevert(EnumerableRoles.EnumerableRolesUnauthorized.selector); vm.prank(_minter); ${targetContractName}.mintTo(_to, _amount);`, get_3args_testFuzz_mintTo_succeeds()); break; } } } function get_2args_testFuzz_mintTo_succeeds() : BaseFunction { const fn = { name: 'testFuzz_mintTo_succeeds', args: [ { name: '_to', type: 'address' }, { name: '_amount', type: 'uint256' }, ], returns: [], kind: 'public' as const, }; return fn; } function get_3args_testFuzz_mintTo_succeeds() : BaseFunction { const fn = { name: 'testFuzz_mintTo_succeeds', args: [ { name: '_minter', type: 'address' }, { name: '_to', type: 'address' }, { name: '_amount', type: 'uint256' }, ], returns: [], kind: 'public' as const, }; return fn; } const functions = defineFunctions({ setUp: { kind: 'public' as const, args: [ ], }, test_constructor: { kind: 'public' as const, args: [ ], mutability: 'view' as const, }, _mockAndExpect: { kind: 'internal' as const, args: [ { name: '_receiver', type: 'address' }, { name: '_calldata', type: 'bytes memory' }, { name: '_returned', type: 'bytes memory' }, ], }, testFuzz_crosschainMint_callerNotBridge_reverts: { kind: 'public' as const, args: [ { name: '_caller', type: 'address' }, { name: '_to', type: 'address' }, { name: '_amount', type: 'uint256' }, ], }, testFuzz_crosschainMint_succeeds: { kind: 'public' as const, args: [ { name: '_to', type: 'address' }, { name: '_amount', type: 'uint256' }, ], }, testFuzz_crosschainBurn_callerNotBridge_reverts: { kind: 'public' as const, args: [ { name: '_caller', type: 'address' }, { name: '_from', type: 'address' }, { name: '_amount', type: 'uint256' }, ], }, testFuzz_crosschainBurn_succeeds: { kind: 'public' as const, args: [ { name: '_from', type: 'address' }, { name: '_amount', type: 'uint256' }, ], }, // testFuzz_mintTo_succeeds: { // kind: 'public' as const, // args: [ // { name: '_to', type: 'address' }, // { name: '_amount', type: 'uint256' }, // ], // }, testFuzz_transfer_succeeds: { kind: 'public' as const, args: [ { name: '_sender', type: 'address' }, { name: '_amount', type: 'uint256' }, ], }, testFuzz_transferFrom_succeeds: { kind: 'public' as const, args: [ { name: '_spender', type: 'address' }, { name: '_amount', type: 'uint256' }, ], }, testFuzz_transferInsufficientBalance_reverts: { kind: 'public' as const, args: [ { name: '_to', type: 'address' }, { name: '_mintAmount', type: 'uint256' }, { name: '_sendAmount', type: 'uint256' }, ], }, testFuzz_transferFromInsufficientAllowance_reverts: { kind: 'public' as const, args: [ { name: '_to', type: 'address' }, { name: '_from', type: 'address' }, { name: '_approval', type: 'uint256' }, { name: '_amount', type: 'uint256' }, ], }, });