superfuse-wizard
Version:
Interactive smart contract generator based on Superchain 's interoperability standard.
575 lines (434 loc) • 19.8 kB
text/typescript
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' },
],
},
});