diff --git a/packages/core/src/build-generic.ts b/packages/core/src/build-generic.ts index 6387058f..e0b9084d 100644 --- a/packages/core/src/build-generic.ts +++ b/packages/core/src/build-generic.ts @@ -4,6 +4,7 @@ import { ERC721Options, buildERC721 } from './erc721'; import { ERC1155Options, buildERC1155 } from './erc1155'; import { StablecoinOptions, buildStablecoin } from './stablecoin'; import { GovernorOptions, buildGovernor } from './governor'; +import { Contract } from './contract'; export interface KindedOptions { ERC20: { kind: 'ERC20' } & ERC20Options; @@ -17,7 +18,7 @@ export interface KindedOptions { export type GenericOptions = KindedOptions[keyof KindedOptions]; -export function buildGeneric(opts: GenericOptions) { +export function buildGeneric(opts: GenericOptions): Contract { switch (opts.kind) { case 'ERC20': return buildERC20(opts); diff --git a/packages/core/src/erc20.ts b/packages/core/src/erc20.ts index 2a5e1afe..54df2fe0 100644 --- a/packages/core/src/erc20.ts +++ b/packages/core/src/erc20.ts @@ -39,7 +39,7 @@ export const defaults: Required = { info: commonDefaults.info, } as const; -function withDefaults(opts: ERC20Options): Required { +export function withDefaults(opts: ERC20Options): Required { return { ...opts, ...withCommonDefaults(opts), @@ -61,7 +61,7 @@ export function isAccessControlRequired(opts: Partial): boolean { return opts.mintable || opts.pausable || opts.upgradeable === 'uups'; } -export function buildERC20(opts: ERC20Options): Contract { +export function buildERC20(opts: ERC20Options): ContractBuilder { const allOpts = withDefaults(opts); const c = new ContractBuilder(allOpts.name); @@ -118,6 +118,7 @@ function addBase(c: ContractBuilder, name: string, symbol: string) { ); c.addOverride(ERC20, functions._update); + c.addOverride(ERC20, functions._approve); // allows override from stablecoin } function addPausableExtension(c: ContractBuilder, access: Access) { @@ -202,7 +203,7 @@ function addFlashMint(c: ContractBuilder) { }); } -const functions = defineFunctions({ +export const functions = defineFunctions({ _update: { kind: 'internal' as const, args: [ @@ -212,6 +213,16 @@ const functions = defineFunctions({ ], }, + _approve: { + kind: 'internal' as const, + args: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'emitEvent', type: 'bool' }, + ], + }, + mint: { kind: 'public' as const, args: [ diff --git a/packages/core/src/stablecoin.test.ts.md b/packages/core/src/stablecoin.test.ts.md index da4c4d4d..98b34fb5 100644 --- a/packages/core/src/stablecoin.test.ts.md +++ b/packages/core/src/stablecoin.test.ts.md @@ -315,11 +315,11 @@ Generated by [AVA](https://avajs.dev). import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ ␊ - contract MyStablecoin is ERC20, ERC20Custodian, Ownable, ERC20Permit {␊ + contract MyStablecoin is ERC20, ERC20Permit, ERC20Custodian, Ownable {␊ constructor(address initialOwner)␊ ERC20("MyStablecoin", "MST")␊ - Ownable(initialOwner)␊ ERC20Permit("MyStablecoin")␊ + Ownable(initialOwner)␊ {}␊ ␊ function _isCustodian(address user) internal view override returns (bool) {␊ @@ -350,11 +350,11 @@ Generated by [AVA](https://avajs.dev). import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ ␊ - contract MyStablecoin is ERC20, ERC20Allowlist, Ownable, ERC20Permit {␊ + contract MyStablecoin is ERC20, ERC20Permit, ERC20Allowlist, Ownable {␊ constructor(address initialOwner)␊ ERC20("MyStablecoin", "MST")␊ - Ownable(initialOwner)␊ ERC20Permit("MyStablecoin")␊ + Ownable(initialOwner)␊ {}␊ ␊ function allowUser(address user) public onlyOwner {␊ @@ -396,11 +396,11 @@ Generated by [AVA](https://avajs.dev). import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ ␊ - contract MyStablecoin is ERC20, ERC20Blocklist, Ownable, ERC20Permit {␊ + contract MyStablecoin is ERC20, ERC20Permit, ERC20Blocklist, Ownable {␊ constructor(address initialOwner)␊ ERC20("MyStablecoin", "MST")␊ - Ownable(initialOwner)␊ ERC20Permit("MyStablecoin")␊ + Ownable(initialOwner)␊ {}␊ ␊ function blockUser(address user) public onlyOwner {␊ diff --git a/packages/core/src/stablecoin.test.ts.snap b/packages/core/src/stablecoin.test.ts.snap index d7bbd195..4770f427 100644 Binary files a/packages/core/src/stablecoin.test.ts.snap and b/packages/core/src/stablecoin.test.ts.snap differ diff --git a/packages/core/src/stablecoin.ts b/packages/core/src/stablecoin.ts index 1f6537e9..66ba36c0 100644 --- a/packages/core/src/stablecoin.ts +++ b/packages/core/src/stablecoin.ts @@ -1,60 +1,28 @@ import { Contract, ContractBuilder } from './contract'; import { Access, setAccessControl, requireAccessControl } from './set-access-control'; -import { addPauseFunctions } from './add-pausable'; import { defineFunctions } from './utils/define-functions'; -import { CommonOptions, withCommonDefaults, defaults as commonDefaults } from './common-options'; -import { setUpgradeable } from './set-upgradeable'; -import { setInfo } from './set-info'; import { printContract } from './print'; -import { ClockMode, clockModeDefault, setClockMode } from './set-clock-mode'; +import { buildERC20, ERC20Options, defaults as erc20defaults, withDefaults as withERC20Defaults, functions as erc20functions } from './erc20'; -export interface StablecoinOptions extends CommonOptions { - name: string; - symbol: string; - burnable?: boolean; - pausable?: boolean; - premint?: string; - mintable?: boolean; - permit?: boolean; +export interface StablecoinOptions extends ERC20Options { limitations?: false | "allowlist" | "blocklist"; - /** - * Whether to keep track of historical balances for voting in on-chain governance, and optionally specify the clock mode. - * Setting `true` is equivalent to 'blocknumber'. Setting a clock mode implies voting is enabled. - */ - votes?: boolean | ClockMode; - flashmint?: boolean; custodian?: boolean; } export const defaults: Required = { + ...erc20defaults, name: 'MyStablecoin', symbol: 'MST', - burnable: false, - pausable: false, - premint: '0', - mintable: false, - permit: true, limitations: false, - votes: false, - flashmint: false, custodian: false, - access: commonDefaults.access, - upgradeable: commonDefaults.upgradeable, - info: commonDefaults.info, } as const; function withDefaults(opts: StablecoinOptions): Required { return { - ...opts, - ...withCommonDefaults(opts), - burnable: opts.burnable ?? defaults.burnable, - pausable: opts.pausable ?? defaults.pausable, - premint: opts.premint || defaults.premint, - mintable: opts.mintable ?? defaults.mintable, - permit: opts.permit ?? defaults.permit, + ...withERC20Defaults(opts), + name: opts.name ?? defaults.name, + symbol: opts.symbol ?? defaults.symbol, limitations: opts.limitations ?? defaults.limitations, - votes: opts.votes ?? defaults.votes, - flashmint: opts.flashmint ?? defaults.flashmint, custodian: opts.custodian ?? defaults.custodian, }; } @@ -70,116 +38,22 @@ export function isAccessControlRequired(opts: Partial): boole export function buildStablecoin(opts: StablecoinOptions): Contract { const allOpts = withDefaults(opts); - const c = new ContractBuilder(allOpts.name); - - const { access, upgradeable, info } = allOpts; - - addBase(c, allOpts.name, allOpts.symbol); - - if (allOpts.burnable) { - addBurnable(c); - } - - if (allOpts.pausable) { - addPausableExtension(c, access); - } - - if (allOpts.premint) { - addPremint(c, allOpts.premint); - } - - if (allOpts.mintable) { - addMintable(c, access); - } + // Upgradeability is not yet available for the community contracts + allOpts.upgradeable = false; - if (allOpts.limitations) { - addLimitations(c, access, allOpts.limitations); - } + const c = buildERC20(allOpts); if (allOpts.custodian) { - addCustodian(c, access); - } - - // Note: Votes requires Permit - if (allOpts.permit || allOpts.votes) { - addPermit(c, allOpts.name); - } - - if (allOpts.votes) { - const clockMode = allOpts.votes === true ? clockModeDefault : allOpts.votes; - addVotes(c, clockMode); + addCustodian(c, allOpts.access); } - if (allOpts.flashmint) { - addFlashMint(c); + if (allOpts.limitations) { + addLimitations(c, allOpts.access, allOpts.limitations); } - setAccessControl(c, access); - - // Upgradeability is not yet available for the community contracts - // setUpgradeable(c, upgradeable, access); - - setInfo(c, info); - return c; } -function addBase(c: ContractBuilder, name: string, symbol: string) { - const ERC20 = { - name: 'ERC20', - path: '@openzeppelin/contracts/token/ERC20/ERC20.sol', - }; - c.addParent( - ERC20, - [name, symbol], - ); - - c.addOverride(ERC20, functions._update); - c.addOverride(ERC20, functions._approve); -} - -function addPausableExtension(c: ContractBuilder, access: Access) { - const ERC20Pausable = { - name: 'ERC20Pausable', - path: '@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol', - }; - c.addParent(ERC20Pausable); - c.addOverride(ERC20Pausable, functions._update); - - addPauseFunctions(c, access); -} - -function addBurnable(c: ContractBuilder) { - c.addParent({ - name: 'ERC20Burnable', - path: '@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol', - }); -} - -export const premintPattern = /^(\d*)(?:\.(\d+))?(?:e(\d+))?$/; - -function addPremint(c: ContractBuilder, amount: string) { - const m = amount.match(premintPattern); - if (m) { - const integer = m[1]?.replace(/^0+/, '') ?? ''; - const decimals = m[2]?.replace(/0+$/, '') ?? ''; - const exponent = Number(m[3] ?? 0); - - if (Number(integer + decimals) > 0) { - const decimalPlace = decimals.length - exponent; - const zeroes = new Array(Math.max(0, -decimalPlace)).fill('0').join(''); - const units = integer + decimals + zeroes; - const exp = decimalPlace <= 0 ? 'decimals()' : `(decimals() - ${decimalPlace})`; - c.addConstructorCode(`_mint(msg.sender, ${units} * 10 ** ${exp});`); - } - } -} - -function addMintable(c: ContractBuilder, access: Access) { - requireAccessControl(c, functions.mint, access, 'MINTER', 'minter'); - c.addFunctionCode('_mint(to, amount);', functions.mint); -} - function addLimitations(c: ContractBuilder, access: Access, mode: boolean | 'allowlist' | 'blocklist') { const type = mode === 'allowlist'; const ERC20Limitation = { @@ -249,132 +123,45 @@ function addCustodian(c: ContractBuilder, access: Access) { } } -function addPermit(c: ContractBuilder, name: string) { - const ERC20Permit = { - name: 'ERC20Permit', - path: '@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol', - }; - c.addParent(ERC20Permit, [name]); - c.addOverride(ERC20Permit, functions.nonces); - -} - -function addVotes(c: ContractBuilder, clockMode: ClockMode) { - if (!c.parents.some(p => p.contract.name === 'ERC20Permit')) { - throw new Error('Missing ERC20Permit requirement for ERC20Votes'); - } - - const ERC20Votes = { - name: 'ERC20Votes', - path: '@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol', - }; - c.addParent(ERC20Votes); - c.addOverride(ERC20Votes, functions._update); - - c.addImportOnly({ - name: 'Nonces', - path: '@openzeppelin/contracts/utils/Nonces.sol', - }); - c.addOverride({ - name: 'Nonces', - }, functions.nonces); - - setClockMode(c, ERC20Votes, clockMode); -} - -function addFlashMint(c: ContractBuilder) { - c.addParent({ - name: 'ERC20FlashMint', - path: '@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol', - }); -} - -const functions = defineFunctions({ - _update: { - kind: 'internal' as const, - args: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - ], - }, - - _approve: { - kind: 'internal' as const, - args: [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'emitEvent', type: 'bool' }, - ], - }, - - _isCustodian: { - kind: 'internal' as const, - args: [ - { name: 'user', type: 'address' }, - ], - returns: ['bool'], - mutability: 'view' as const - }, - - mint: { - kind: 'public' as const, - args: [ - { name: 'to', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - }, +const functions = { + ...erc20functions, + ...defineFunctions({ + _isCustodian: { + kind: 'internal' as const, + args: [ + { name: 'user', type: 'address' }, + ], + returns: ['bool'], + mutability: 'view' as const + }, + + allowUser: { + kind: 'public' as const, + args: [ + { name: 'user', type: 'address' } + ], + }, + + disallowUser: { + kind: 'public' as const, + args: [ + { name: 'user', type: 'address' } + ], + }, + + blockUser: { + kind: 'public' as const, + args: [ + { name: 'user', type: 'address' } + ], + }, + + unblockUser: { + kind: 'public' as const, + args: [ + { name: 'user', type: 'address' } + ], + }, + }) +}; - allowUser: { - kind: 'public' as const, - args: [ - { name: 'user', type: 'address' } - ], - }, - - disallowUser: { - kind: 'public' as const, - args: [ - { name: 'user', type: 'address' } - ], - }, - - blockUser: { - kind: 'public' as const, - args: [ - { name: 'user', type: 'address' } - ], - }, - - unblockUser: { - kind: 'public' as const, - args: [ - { name: 'user', type: 'address' } - ], - }, - - pause: { - kind: 'public' as const, - args: [], - }, - - unpause: { - kind: 'public' as const, - args: [], - }, - - snapshot: { - kind: 'public' as const, - args: [], - }, - - nonces: { - kind: 'public' as const, - args: [ - { name: 'owner', type: 'address' }, - ], - returns: ['uint256'], - mutability: 'view' as const, - } -});