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