UNPKG

superfuse-wizard

Version:

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

315 lines (241 loc) 10.9 kB
import type { TestContract, BaseFunction} from './contract'; import { TestBuilder } from "./contract"; import type { SharedERC20VotesOptions, OpSec} from '../shared/option-erc20-votes'; import { withCommonDefaults, defaults as commonDefaults } from '../shared/option-erc20-votes'; import { printTestContract } from "./print"; import { setInfo } from "./set-info"; import { defineFunctions } from '../utils/define-functions'; function withTestDefaults(opts: SharedERC20VotesOptions): Required<SharedERC20VotesOptions> { return { ...opts, ...withCommonDefaults(opts), burnable: opts.burnable ?? commonDefaults.burnable, pausable: opts.pausable ?? commonDefaults.pausable, premint: opts.premint || commonDefaults.premint, mintable: opts.mintable ?? commonDefaults.mintable, permit: opts.permit ?? commonDefaults.permit, votes: opts.votes ?? commonDefaults.votes, flashmint: opts.flashmint ?? commonDefaults.flashmint, }; } export function printTestERC20Votes(opts: SharedERC20VotesOptions = commonDefaults): string { return printTestContract(buildTestERC20Votes(opts)); } export function buildTestERC20Votes(opts: SharedERC20VotesOptions): TestContract { const allOpts = withTestDefaults(opts); const c = new TestBuilder(allOpts.testName); addBase(c, allOpts); setOpsec(c, allOpts.opSec); setConstructorTestLogic(c, allOpts); setERC7802TestLogic(c, allOpts); setInfo(c, allOpts.testInfo); return c; } const transformToLowerCamelCase = (input: string): string => input.charAt(0).toLowerCase() + input.slice(1); function addBase(c: TestBuilder, allOpts: Required<SharedERC20VotesOptions>) { 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 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 MyERC20Votes = { name: `${allOpts.contractName}`, path: `@main/${allOpts.contractName}.sol`, }; c.addImportOnly(MyERC20Votes); 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 DeployMyERC20VotesScript = { name: `${allOpts.deployName}`, path: `@script/${allOpts.conventionNumber}_${allOpts.deployName}.s.sol`, }; c.addImportOnly(DeployMyERC20VotesScript); 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<SharedERC20VotesOptions>) { const camelCaseContractName = transformToLowerCamelCase(allOpts.contractName); c.addFunctionCode(`assertEq(${camelCaseContractName}.name(), "${allOpts.tokenName}"); assertEq(${camelCaseContractName}.symbol(), "${allOpts.tokenSymbol}");`, functions.test_constructor); } function setERC7802TestLogic(c: TestBuilder, allOpts: Required<SharedERC20VotesOptions>) { 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); } 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' }, ], } });