From 1d70b62b8c4bcc899cb6cf3b22bf701d487fa71a Mon Sep 17 00:00:00 2001 From: immrsd <103599616+immrsd@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:47:22 +0300 Subject: [PATCH 1/5] Cairo: Add ERC20FlashMint extension to `cairo` and `cairo_alpha` (#801) Co-authored-by: Daniel Bigos --- .changeset/erc20-flash-mint.md | 11 + .../workflows/compile-cairo-alpha-project.yml | 39 +- .github/workflows/compile-cairo-project.yml | 39 +- packages/cli/src/cli-options.test.ts | 42 + packages/cli/src/cli.test.ts.md | 5 + packages/cli/src/cli.test.ts.snap | Bin 13038 -> 13490 bytes packages/common/src/ai/descriptions/cairo.ts | 12 + packages/common/src/ai/schemas/cairo.ts | 13 + packages/core/cairo/src/contract.ts | 25 + packages/core/cairo/src/erc20.ts | 224 +++++ packages/core/cairo/src/generate/erc20.ts | 5 +- packages/core/cairo/src/generate/sources.ts | 47 +- packages/core/cairo/src/index.ts | 3 +- packages/core/cairo/src/print.ts | 9 +- .../cairo/src/scripts/update-scarb-project.ts | 27 +- packages/core/cairo/src/test.ts | 8 +- .../with_components_off/erc20/erc20.test.ts | 124 ++- .../erc20/erc20.test.ts.md | 774 ++++++++++++++++++ .../erc20/erc20.test.ts.snap | Bin 4162 -> 5666 bytes .../with_components_on/erc20/erc20.test.ts | 124 ++- .../with_components_on/erc20/erc20.test.ts.md | 487 +++++++++++ .../erc20/erc20.test.ts.snap | Bin 2879 -> 3802 bytes packages/core/cairo_alpha/CHANGELOG.md | 4 + packages/core/cairo_alpha/src/contract.ts | 25 + packages/core/cairo_alpha/src/erc20.ts | 224 +++++ .../core/cairo_alpha/src/generate/erc20.ts | 5 +- .../core/cairo_alpha/src/generate/sources.ts | 47 +- packages/core/cairo_alpha/src/index.ts | 3 +- packages/core/cairo_alpha/src/print.ts | 9 +- .../src/scripts/update-scarb-project.ts | 27 +- packages/core/cairo_alpha/src/test.ts | 8 +- .../with_components_off/erc20/erc20.test.ts | 124 ++- .../erc20/erc20.test.ts.md | 774 ++++++++++++++++++ .../erc20/erc20.test.ts.snap | Bin 4162 -> 5666 bytes .../with_components_on/erc20/erc20.test.ts | 124 ++- .../with_components_on/erc20/erc20.test.ts.md | 487 +++++++++++ .../erc20/erc20.test.ts.snap | Bin 2879 -> 3802 bytes packages/mcp/src/cairo/tools/erc20.test.ts | 7 + packages/mcp/src/cairo/tools/erc20.ts | 2 + .../function-definitions/cairo-alpha.ts | 28 + .../function-definitions/cairo.ts | 28 + packages/ui/src/cairo/ERC20Controls.svelte | 169 +++- .../ui/src/cairo_alpha/ERC20Controls.svelte | 175 +++- 43 files changed, 4230 insertions(+), 58 deletions(-) create mode 100644 .changeset/erc20-flash-mint.md diff --git a/.changeset/erc20-flash-mint.md b/.changeset/erc20-flash-mint.md new file mode 100644 index 000000000..e936078e5 --- /dev/null +++ b/.changeset/erc20-flash-mint.md @@ -0,0 +1,11 @@ +--- +'@openzeppelin/wizard-cairo': minor +'@openzeppelin/wizard-common': patch +'@openzeppelin/contracts-mcp': patch +'@openzeppelin/contracts-cli': patch +'ui': patch +--- + +Cairo: Add ERC20FlashMint extension for the ERC20 token kind. +- Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). +- Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. diff --git a/.github/workflows/compile-cairo-alpha-project.yml b/.github/workflows/compile-cairo-alpha-project.yml index 76752d3d5..b409c92fd 100644 --- a/.github/workflows/compile-cairo-alpha-project.yml +++ b/.github/workflows/compile-cairo-alpha-project.yml @@ -56,6 +56,7 @@ jobs: declare -a all_access_options=("disabled" "ownable" "roles" "roles-dar-default" "roles-dar-custom") declare -a all_upgradeable_options=("true" "false") declare -a all_royalty_options=("disabled" "enabled-default" "enabled-custom") + declare -a all_flashmint_options=("disabled" "enabled-default" "enabled-percent-fee" "enabled-custom-fee") kind="$KIND" @@ -78,18 +79,38 @@ jobs: done done - elif [[ "$kind" == "ERC20" || "$kind" == "ERC6909" || "$kind" == "Custom" ]]; then + elif [[ "$kind" == "ERC20" ]]; then for access_option in "${all_access_options[@]}"; do - proj_name="'$kind, macros: $macros_option, access: $access_option' test project" - echo "Generating $proj_name..." - yarn run update_scarb_project --kind=$kind --macros=$macros_option --access=$access_option + for upgradeable_option in "${all_upgradeable_options[@]}"; do + for flashmint_option in "${all_flashmint_options[@]}"; do + proj_name="'$kind, macros: $macros_option, access: $access_option, upgradeable: $upgradeable_option, flashmint: $flashmint_option' test project" + echo "Generating $proj_name..." + yarn run update_scarb_project --kind=$kind --macros=$macros_option --access=$access_option --upgradeable=$upgradeable_option --flashmint=$flashmint_option + + echo "Compiling $proj_name..." + scarb clean + scarb build + + echo "✅ Compiled $proj_name!" + echo "---------------------------------" + done + done + done - echo "Compiling $proj_name..." - scarb clean - scarb build + elif [[ "$kind" == "ERC6909" || "$kind" == "Custom" ]]; then + for access_option in "${all_access_options[@]}"; do + for upgradeable_option in "${all_upgradeable_options[@]}"; do + proj_name="'$kind, macros: $macros_option, access: $access_option, upgradeable: $upgradeable_option' test project" + echo "Generating $proj_name..." + yarn run update_scarb_project --kind=$kind --macros=$macros_option --access=$access_option --upgradeable=$upgradeable_option - echo "✅ Compiled $proj_name!" - echo "---------------------------------" + echo "Compiling $proj_name..." + scarb clean + scarb build + + echo "✅ Compiled $proj_name!" + echo "---------------------------------" + done done else diff --git a/.github/workflows/compile-cairo-project.yml b/.github/workflows/compile-cairo-project.yml index d11da8142..9506c0317 100644 --- a/.github/workflows/compile-cairo-project.yml +++ b/.github/workflows/compile-cairo-project.yml @@ -56,6 +56,7 @@ jobs: declare -a all_access_options=("disabled" "ownable" "roles" "roles-dar-default" "roles-dar-custom") declare -a all_upgradeable_options=("true" "false") declare -a all_royalty_options=("disabled" "enabled-default" "enabled-custom") + declare -a all_flashmint_options=("disabled" "enabled-default" "enabled-percent-fee" "enabled-custom-fee") kind="$KIND" @@ -78,18 +79,38 @@ jobs: done done - elif [[ "$kind" == "ERC20" || "$kind" == "ERC6909" || "$kind" == "Custom" ]]; then + elif [[ "$kind" == "ERC20" ]]; then for access_option in "${all_access_options[@]}"; do - proj_name="'$kind, macros: $macros_option, access: $access_option' test project" - echo "Generating $proj_name..." - yarn run update_scarb_project --kind=$kind --macros=$macros_option --access=$access_option + for upgradeable_option in "${all_upgradeable_options[@]}"; do + for flashmint_option in "${all_flashmint_options[@]}"; do + proj_name="'$kind, macros: $macros_option, access: $access_option, upgradeable: $upgradeable_option, flashmint: $flashmint_option' test project" + echo "Generating $proj_name..." + yarn run update_scarb_project --kind=$kind --macros=$macros_option --access=$access_option --upgradeable=$upgradeable_option --flashmint=$flashmint_option + + echo "Compiling $proj_name..." + scarb clean + scarb build + + echo "✅ Compiled $proj_name!" + echo "---------------------------------" + done + done + done - echo "Compiling $proj_name..." - scarb clean - scarb build + elif [[ "$kind" == "ERC6909" || "$kind" == "Custom" ]]; then + for access_option in "${all_access_options[@]}"; do + for upgradeable_option in "${all_upgradeable_options[@]}"; do + proj_name="'$kind, macros: $macros_option, access: $access_option, upgradeable: $upgradeable_option' test project" + echo "Generating $proj_name..." + yarn run update_scarb_project --kind=$kind --macros=$macros_option --access=$access_option --upgradeable=$upgradeable_option - echo "✅ Compiled $proj_name!" - echo "---------------------------------" + echo "Compiling $proj_name..." + scarb clean + scarb build + + echo "✅ Compiled $proj_name!" + echo "---------------------------------" + done done else diff --git a/packages/cli/src/cli-options.test.ts b/packages/cli/src/cli-options.test.ts index 2e28a29c4..c6f19217c 100644 --- a/packages/cli/src/cli-options.test.ts +++ b/packages/cli/src/cli-options.test.ts @@ -8,6 +8,7 @@ import { erc721 as cairoErc721, erc1155 as cairoErc1155, erc6909 as cairoErc6909, + flashMintDefaults as cairoFlashMintDefaults, account as cairoAccount, multisig as cairoMultisig, governor as cairoGovernor, @@ -401,6 +402,47 @@ test('cairo-erc20: access ownable without dar delays', t => { t.is(output, cairoErc20.print(opts)); }); +test('cairo-erc20: flash mint with defaults', t => { + const opts = { + name: 'TestToken', + symbol: 'TST', + flashmint: { ...cairoFlashMintDefaults, enabled: true }, + }; + const output = run('cairo-erc20', '--name', opts.name, '--symbol', opts.symbol, '--flashmint.enabled'); + t.is(output, cairoErc20.print(opts)); +}); + +test('cairo-erc20: flash mint with custom config', t => { + const opts = { + name: 'TestToken', + symbol: 'TST', + flashmint: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent' as const, + feePercent: '0.5', + feeDestination: 'fee_receiver' as const, + }, + }; + const output = run( + 'cairo-erc20', + '--name', + opts.name, + '--symbol', + opts.symbol, + '--flashmint.enabled', + '--flashmint.maxAmount', + opts.flashmint.maxAmount, + '--flashmint.feeMode', + opts.flashmint.feeMode, + '--flashmint.feePercent', + opts.flashmint.feePercent, + '--flashmint.feeDestination', + opts.flashmint.feeDestination, + ); + t.is(output, cairoErc20.print(opts)); +}); + test('cairo-erc721: most options', t => { const opts = { name: 'TestNFT', diff --git a/packages/cli/src/cli.test.ts.md b/packages/cli/src/cli.test.ts.md index e22511951..4c1a6f2e9 100644 --- a/packages/cli/src/cli.test.ts.md +++ b/packages/cli/src/cli.test.ts.md @@ -270,6 +270,11 @@ Generated by [AVA](https://avajs.dev). --votes Whether to keep track of historical balances for voting in on-chain governance, with a way to delegate one's voting power to a trusted account.␊ --appName Required when votes is enabled, for hashing and signing typed structured data. Name for domain separator implementing SNIP12Metadata trait. Prevents two applications from producing the same hash.␊ --appVersion Required when votes is enabled, for hashing and signing typed structured data. Version for domain separator implementing SNIP12Metadata trait. Prevents two versions of the same application from producing the same hash.␊ + --flashmint.enabled Whether to include ERC20FlashMint functionality, allowing flash loans of tokens compliant with ERC-3156.␊ + --flashmint.maxAmount Maximum amount of tokens that can be flash-loaned in a single call. Use the literal string "max" to inherit the default (the maximum representable u256 minus the current total supply), or a non-negative number in the token's decimal units to set a custom cap. A value of 0 effectively disables flash loans without removing the extension.␊ + --flashmint.feeMode Mode for the flash loan fee. 'percent' charges a percentage of the loaned amount (value provided via feePercent). 'custom' emits a TODO stub for the caller to implement.␊ + --flashmint.feePercent Percentage of the loan amount charged as the flash loan fee. Number between 0 and 100, fractional values supported (e.g. "0.0013725"). Used when feeMode is 'percent'. Defaults to 0 (no fee).␊ + --flashmint.feeDestination Where the flash loan fee is sent. 'burn' sends it to the zero address (effectively burning it). 'fee_receiver' adds a constructor argument that the deployer must populate with a non-zero address; the address is stored on-chain and validated at deploy time.␊ --access.type The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Roles (Default Admin Rules) provides additional enforced security measures on top of standard Roles mechanism for managing the most privileged role: default admin.␊ --access.darInitialDelay The initial delay for the default admin role (in case Roles (Default Admin Rules) is used). Default is "1 day".␊ --access.darDefaultDelayIncrease The default delay increase for the default admin role (in case Roles (Default Admin Rules) is used). Default is "5 days".␊ diff --git a/packages/cli/src/cli.test.ts.snap b/packages/cli/src/cli.test.ts.snap index b0f12a5da973375bdfb6d731cc11b329c8b78561..2d4436500c0e223c6e76ea89f4f1584f4ed54c79 100644 GIT binary patch delta 13225 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 delta 12771 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 = new Map(); private useClausesMap: Map = new Map(); private interfaceFlagsSet: Set = new Set(); + private storageMembersMap: Map = new Map(); constructor(name: string, macros: MacrosOptions, account: boolean = false) { this.name = toIdentifier(name, true); @@ -145,6 +152,24 @@ export class ContractBuilder implements Contract { return [...this.useClausesMap.values()]; } + get storageMembers(): StorageMember[] { + return [...this.storageMembersMap.values()]; + } + + addStorageMember(member: StorageMember): boolean { + const existing = this.storageMembersMap.get(member.name); + if (existing !== undefined) { + if (existing.type !== member.type) { + throw new Error( + `Tried to add duplicate storage member ${member.name} with different type: ${member.type} instead of ${existing.type}.`, + ); + } + return false; + } + this.storageMembersMap.set(member.name, member); + return true; + } + /** * Custom flags to denote that the contract implements a specific interface, e.g. ISRC5, to avoid duplicates **/ diff --git a/packages/core/cairo/src/erc20.ts b/packages/core/cairo/src/erc20.ts index 0db80febe..d0c232c82 100644 --- a/packages/core/cairo/src/erc20.ts +++ b/packages/core/cairo/src/erc20.ts @@ -18,6 +18,66 @@ import { addVotesComponent } from './common-components'; const DEFAULT_DECIMALS = BigInt(18); +export type FlashMintFeeMode = 'percent' | 'custom'; +export type FlashMintFeeDestination = 'burn' | 'fee_receiver'; + +export type FlashMintOptions = { + enabled: boolean; + maxAmount: string; + feeMode: FlashMintFeeMode; + feePercent: string; + feeDestination: FlashMintFeeDestination; +}; + +export const flashMintDefaults: FlashMintOptions = { + enabled: false, + maxAmount: 'max', + feeMode: 'percent', + feePercent: '0', + feeDestination: 'burn', +}; + +export type FlashMintSubset = 'all' | 'disabled' | 'enabled-default' | 'enabled-percent-fee' | 'enabled-custom-fee'; + +export const flashMintOptions = { + disabled: flashMintDefaults, + enabledDefault: { ...flashMintDefaults, enabled: true } satisfies FlashMintOptions, + enabledPercentFee: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '0.5', + feeDestination: 'fee_receiver', + } satisfies FlashMintOptions, + enabledCustomFee: { + enabled: true, + maxAmount: 'max', + feeMode: 'custom', + feePercent: '0', + feeDestination: 'fee_receiver', + } satisfies FlashMintOptions, +}; + +export function resolveFlashMintOptionsSubset(subset: FlashMintSubset): FlashMintOptions[] { + const { disabled, enabledDefault, enabledPercentFee, enabledCustomFee } = flashMintOptions; + switch (subset) { + case 'all': + return [disabled, enabledDefault, enabledPercentFee, enabledCustomFee]; + case 'disabled': + return [disabled]; + case 'enabled-default': + return [enabledDefault]; + case 'enabled-percent-fee': + return [enabledPercentFee]; + case 'enabled-custom-fee': + return [enabledCustomFee]; + default: { + const _: never = subset; + throw new Error('Unknown FlashMintSubset'); + } + } +} + export const defaults: Required = { name: 'MyToken', symbol: 'MTK', @@ -30,6 +90,7 @@ export const defaults: Required = { votes: false, appName: '', // Defaults to empty string, but user must provide a non-empty value if votes are enabled appVersion: 'v1', + flashmint: flashMintDefaults, access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, info: commonDefaults.info, @@ -52,6 +113,7 @@ export interface ERC20Options extends CommonContractOptions { votes?: boolean; appName?: string; appVersion?: string; + flashmint?: FlashMintOptions; } function withDefaults(opts: ERC20Options): Required { @@ -67,6 +129,7 @@ function withDefaults(opts: ERC20Options): Required { votes: opts.votes ?? defaults.votes, appName: opts.appName ?? defaults.appName, appVersion: opts.appVersion ?? defaults.appVersion, + flashmint: opts.flashmint ?? defaults.flashmint, }; } @@ -102,6 +165,10 @@ export function buildERC20(opts: ERC20Options): Contract { addWrapper(c); } + if (allOpts.flashmint.enabled) { + addFlashMint(c, allOpts.flashmint, decimals); + } + addHooks(c, allOpts); setAccessControl(c, allOpts.access); @@ -294,6 +361,145 @@ function addWrapper(c: ContractBuilder) { c.addComponent(components.ERC20WrapperComponent, [{ lit: 'underlying' }], true); } +function parseFlashMintMaxAmount(value: string, decimals: bigint): bigint | null { + if (value === 'max') { + return null; + } + if (value === '' || !premintPattern.test(value)) { + throw new OptionsError({ flashMintMaxAmount: 'Must be "max" or a non-negative number' }); + } + return toUint(getInitialSupply(value, Number(decimals)), 'flashMintMaxAmount', 'u256'); +} + +function parseFlashMintFeePercent(value: string): { numerator: bigint; denominator: bigint } | null { + if (value === '') { + return null; + } + if (!premintPattern.test(value)) { + throw new OptionsError({ flashMintFeePercent: 'Must be a number between 0 and 100' }); + } + const [intPart = '', fracPart = ''] = value.split('.'); + const decimalDigits = fracPart.length; + const combined = (intPart + fracPart).replace(/^0+/, ''); + if (combined === '') { + return null; + } + const numerator = BigInt(combined); + const decimalScale = 10n ** BigInt(decimalDigits); + // value = numerator / decimalScale, must be <= 100 + if (numerator > 100n * decimalScale) { + throw new OptionsError({ flashMintFeePercent: 'Must be a number between 0 and 100' }); + } + // For percent of amount: amount * value / 100 = amount * numerator / (100 * decimalScale). + // Both literals are emitted into Cairo and must fit u256; numerator <= denominator from the + // check above, so bounding the denominator is sufficient. + const denominator = 100n * decimalScale; + toUint(denominator.toString(), 'flashMintFeePercent', 'u256'); + return { numerator, denominator }; +} + +function buildFlashFeeOverrideBody(opts: FlashMintOptions): string[] | null { + switch (opts.feeMode) { + case 'percent': { + const parsed = parseFlashMintFeePercent(opts.feePercent); + if (parsed === null) { + return null; + } + return [`amount * ${parsed.numerator} / ${parsed.denominator}`]; + } + case 'custom': + return ['// TODO: Must be implemented according to the desired flash fee logic', '0']; + default: { + const _: never = opts.feeMode; + throw new Error(`Unknown flashMintFeeMode: ${_}`); + } + } +} + +function addFlashMint(c: ContractBuilder, opts: FlashMintOptions, decimals: bigint) { + c.addComponent(components.ERC20FlashMintComponent, [], false); + + const customMax = parseFlashMintMaxAmount(opts.maxAmount, decimals); + const overridesMax = customMax !== null; + const overridesReceiver = opts.feeDestination === 'fee_receiver'; + const feeOverrideBody = buildFlashFeeOverrideBody(opts); + const overridesFee = feeOverrideBody !== null; + + if (!overridesMax && !overridesFee && !overridesReceiver) { + c.addUseClause('openzeppelin_token::erc20::extensions::erc20_flash_mint', 'DefaultConfig', { + alias: 'ERC20FlashMintDefaultConfig', + }); + return; + } + + const flashMintConfigTrait: BaseImplementedTrait = { + name: 'FlashMintConfigImpl', + of: 'ERC20FlashMintComponent::FlashMintConfigTrait', + tags: [], + }; + c.addImplementedTrait(flashMintConfigTrait); + + if (overridesMax || overridesFee || overridesReceiver) { + c.addUseClause('starknet', 'ContractAddress'); + } + + if (overridesMax) { + c.addUseClause('starknet', 'get_contract_address'); + c.addUseClause('core::num::traits', 'Bounded'); + const fn = c.addFunction(flashMintConfigTrait, { + name: 'max_flash_loan', + args: [ + { name: 'self', type: '@ERC20FlashMintComponent::ComponentState' }, + { name: 'token', type: 'ContractAddress' }, + { name: 'total_supply', type: 'u256' }, + ], + returns: 'u256', + code: [], + }); + // Clamp the configured cap to the remaining mint headroom so the loan size we report is + // never larger than what the underlying mint path can actually honor. + fn.code.push( + 'if token != get_contract_address() {', + ' return 0;', + '}', + 'let headroom = Bounded::::MAX - total_supply;', + `let cap: u256 = ${customMax!};`, + 'if cap < headroom { cap } else { headroom }', + ); + } + + if (overridesFee) { + const fn = c.addFunction(flashMintConfigTrait, { + name: 'flash_fee', + args: [ + { name: 'self', type: '@ERC20FlashMintComponent::ComponentState' }, + { name: 'token', type: 'ContractAddress' }, + { name: 'amount', type: 'u256' }, + ], + returns: 'u256', + code: [], + }); + fn.code.push(...feeOverrideBody!); + } + + if (overridesReceiver) { + c.addUseClause('core::num::traits', 'Zero'); + c.addUseClause('starknet::storage', 'StoragePointerReadAccess'); + c.addUseClause('starknet::storage', 'StoragePointerWriteAccess'); + c.addStorageMember({ name: 'flash_fee_receiver', type: 'ContractAddress' }); + c.addConstructorArgument({ name: 'flash_fee_receiver', type: 'ContractAddress' }); + c.addConstructorCode(`assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver')`); + c.addConstructorCode(`self.flash_fee_receiver.write(flash_fee_receiver)`); + const fn = c.addFunction(flashMintConfigTrait, { + name: 'flash_fee_receiver', + args: [{ name: 'self', type: '@ERC20FlashMintComponent::ComponentState' }], + returns: 'ContractAddress', + code: [], + }); + fn.code.push('self.get_contract().flash_fee_receiver.read()'); + } +} + const components = defineComponents({ ERC20Component: { path: 'openzeppelin_token::erc20', @@ -336,6 +542,24 @@ const components = defineComponents({ }, ], }, + ERC20FlashMintComponent: { + path: 'openzeppelin_token::erc20::extensions::erc20_flash_mint', + substorage: { + name: 'erc20_flash_mint', + type: 'ERC20FlashMintComponent::Storage', + }, + event: { + name: 'ERC20FlashMintEvent', + type: 'ERC20FlashMintComponent::Event', + }, + impls: [ + { + name: 'ERC20FlashMintImpl', + embed: true, + value: 'ERC20FlashMintComponent::ERC20FlashMintImpl', + }, + ], + }, }); const functions = defineFunctions({ diff --git a/packages/core/cairo/src/generate/erc20.ts b/packages/core/cairo/src/generate/erc20.ts index 2849f72b3..0b7d6b428 100644 --- a/packages/core/cairo/src/generate/erc20.ts +++ b/packages/core/cairo/src/generate/erc20.ts @@ -1,4 +1,5 @@ -import type { ERC20Options } from '../erc20'; +import type { ERC20Options, FlashMintSubset } from '../erc20'; +import { resolveFlashMintOptionsSubset } from '../erc20'; import type { AccessSubset } from '../set-access-control'; import { resolveAccessControlOptions } from '../set-access-control'; import { infoOptions } from '../set-info'; @@ -13,6 +14,7 @@ const booleans = [true, false]; type GeneratorOptions = { access: AccessSubset; upgradeable: UpgradeableSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }; @@ -29,6 +31,7 @@ function prepareBlueprint(opts: GeneratorOptions) { votes: booleans, appName: ['MyApp'], appVersion: ['v1'], + flashmint: resolveFlashMintOptionsSubset(opts.flashmint), access: resolveAccessControlOptions(opts.access), upgradeable: resolveUpgradeableOptionsSubset(opts.upgradeable), info: infoOptions, diff --git a/packages/core/cairo/src/generate/sources.ts b/packages/core/cairo/src/generate/sources.ts index 72b1b371c..76f5a8392 100644 --- a/packages/core/cairo/src/generate/sources.ts +++ b/packages/core/cairo/src/generate/sources.ts @@ -18,6 +18,7 @@ import { OptionsError } from '../error'; import { findCover } from '../utils/find-cover'; import type { Contract } from '../contract'; import type { RoyaltyInfoSubset } from '../set-royalty-info'; +import type { FlashMintSubset } from '../erc20'; import type { MacrosSubset } from '../set-macros'; import type { AccessSubset } from '../set-access-control'; import type { UpgradeableSubset } from '../set-upgradeable'; @@ -31,11 +32,12 @@ export function* generateOptions(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): Generator { - const { kind, access, upgradeable, royaltyInfo, macros } = params; + const { kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; if (kind === 'all' || kind === 'ERC20') { - for (const kindOpts of generateERC20Options({ access, upgradeable, macros })) { + for (const kindOpts of generateERC20Options({ access, upgradeable, flashmint, macros })) { yield { kind: 'ERC20', ...kindOpts }; } } @@ -105,12 +107,13 @@ function generateContractSubset(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): GeneratedContract[] { - const { subset, kind, access, upgradeable, royaltyInfo, macros } = params; + const { subset, kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; const contracts = []; - for (const options of generateOptions({ kind, access, upgradeable, royaltyInfo, macros })) { + for (const options of generateOptions({ kind, access, upgradeable, royaltyInfo, flashmint, macros })) { const id = crypto.createHash('sha1').update(JSON.stringify(options)).digest().toString('hex'); try { const contract = buildGeneric(options); @@ -163,11 +166,12 @@ export function* generateSources(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): Generator { - const { subset, uniqueName, kind, access, upgradeable, royaltyInfo, macros } = params; + const { subset, uniqueName, kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; let counter = 1; - for (const c of generateContractSubset({ subset, kind, access, upgradeable, royaltyInfo, macros })) { + for (const c of generateContractSubset({ subset, kind, access, upgradeable, royaltyInfo, flashmint, macros })) { if (uniqueName) { c.contract.name = `Contract${counter++}`; } @@ -184,10 +188,11 @@ export async function writeGeneratedSources(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; logsEnabled: boolean; }): Promise { - const { dir, subset, uniqueName, kind, access, upgradeable, royaltyInfo, macros, logsEnabled } = params; + const { dir, subset, uniqueName, kind, access, upgradeable, royaltyInfo, flashmint, macros, logsEnabled } = params; await fs.mkdir(dir, { recursive: true }); const contractNames = []; @@ -198,6 +203,7 @@ export async function writeGeneratedSources(params: { access, upgradeable, royaltyInfo, + flashmint, macros, })) { const name = uniqueName ? contract.name : id; @@ -205,7 +211,7 @@ export async function writeGeneratedSources(params: { contractNames.push(name); } if (logsEnabled) { - const sourceLabel = resolveSourceLabel({ kind, access, upgradeable, royaltyInfo, macros }); + const sourceLabel = resolveSourceLabel({ kind, access, upgradeable, royaltyInfo, flashmint, macros }); console.log(`Generated ${contractNames.length} contracts for ${sourceLabel}`); } @@ -217,15 +223,17 @@ function resolveSourceLabel(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): string { - const { kind, access, upgradeable, royaltyInfo, macros } = params; + const { kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; return [ resolveKindLabel(kind), resolveMacrosLabel(macros), resolveAccessLabel(kind, access), resolveUpgradeableLabel(kind, upgradeable), resolveRoyaltyInfoLabel(kind, royaltyInfo), + resolveFlashMintLabel(kind, flashmint), ] .filter(elem => elem !== undefined) .join(', '); @@ -318,3 +326,24 @@ function resolveRoyaltyInfoLabel(kind: KindSubset, royaltyInfo: RoyaltyInfoSubse } } } + +function resolveFlashMintLabel(kind: KindSubset, flashmint: FlashMintSubset): string | undefined { + switch (kind) { + case 'all': + case 'ERC20': + return `flashmint: ${flashmint}`; + case 'ERC721': + case 'ERC1155': + case 'ERC6909': + case 'Account': + case 'Custom': + case 'Multisig': + case 'Governor': + case 'Vesting': + return undefined; + default: { + const _: never = kind; + throw new Error('Unknown kind'); + } + } +} diff --git a/packages/core/cairo/src/index.ts b/packages/core/cairo/src/index.ts index 27bd959f3..bceb4c1ad 100644 --- a/packages/core/cairo/src/index.ts +++ b/packages/core/cairo/src/index.ts @@ -13,7 +13,8 @@ export type { Info } from './set-info'; export type { RoyaltyInfoOptions } from './set-royalty-info'; export type { MacrosOptions } from './set-macros'; -export { premintPattern } from './erc20'; +export { premintPattern, flashMintDefaults } from './erc20'; +export type { FlashMintOptions, FlashMintFeeMode, FlashMintFeeDestination } from './erc20'; export { defaults as infoDefaults } from './set-info'; export { defaults as royaltyInfoDefaults } from './set-royalty-info'; diff --git a/packages/core/cairo/src/print.ts b/packages/core/cairo/src/print.ts index 817ba2925..0916103d6 100644 --- a/packages/core/cairo/src/print.ts +++ b/packages/core/cairo/src/print.ts @@ -219,11 +219,16 @@ function printImpl(impl: Impl): Lines[] { } function printStorage(contract: Contract): (string | string[])[] { + const memberLines = contract.storageMembers.map(m => `${m.name}: ${m.type},`); + if (contract.macros.withComponents || contract.components.length === 0) { // storage is required regardless of whether there are components - return ['#[storage]', 'struct Storage {}']; + if (memberLines.length === 0) { + return ['#[storage]', 'struct Storage {}']; + } + return ['#[storage]', 'struct Storage {', memberLines, '}']; } - const storageLines = []; + const storageLines = [...memberLines]; for (const component of contract.components) { storageLines.push(`#[substorage(v0)]`); storageLines.push(`${component.substorage.name}: ${component.substorage.type},`); diff --git a/packages/core/cairo/src/scripts/update-scarb-project.ts b/packages/core/cairo/src/scripts/update-scarb-project.ts index a76869aeb..73b2bbff1 100644 --- a/packages/core/cairo/src/scripts/update-scarb-project.ts +++ b/packages/core/cairo/src/scripts/update-scarb-project.ts @@ -5,6 +5,7 @@ import type { KindSubset } from '../generate/sources'; import type { AccessSubset } from '../set-access-control'; import type { UpgradeableSubset } from '../set-upgradeable'; import type { RoyaltyInfoSubset } from '../set-royalty-info'; +import type { FlashMintSubset } from '../erc20'; import type { MacrosSubset } from '../set-macros'; import { writeGeneratedSources } from '../generate/sources'; import { contractsVersion, edition, cairoVersion, scarbVersion } from '../utils/version'; @@ -14,6 +15,7 @@ type Arguments = { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }; @@ -22,6 +24,7 @@ const defaults = { access: 'all', upgradeable: 'all', royaltyInfo: 'all', + flashmint: 'all', macros: 'all', } as const; @@ -33,6 +36,7 @@ export function resolveArguments(): Arguments { access: parseAccessSubset(args.access ?? defaults.access), upgradeable: parseUpgradeableSubset(args.upgradeable ?? defaults.upgradeable), royaltyInfo: parseRoyaltyInfoSubset(args.royalty ?? defaults.royaltyInfo), + flashmint: parseFlashMintSubset(args.flashmint ?? defaults.flashmint), macros: parseMacrosSubset(args.macros ?? defaults.macros), }; } @@ -42,7 +46,7 @@ export async function updateScarbProject() { await fs.rm(generatedSourcesPath, { force: true, recursive: true }); // Generate the contracts source code - const { kind, access, upgradeable, royaltyInfo, macros } = resolveArguments(); + const { kind, access, upgradeable, royaltyInfo, flashmint, macros } = resolveArguments(); const contractNames = await writeGeneratedSources({ dir: generatedSourcesPath, subset: 'all', @@ -51,6 +55,7 @@ export async function updateScarbProject() { access, upgradeable, royaltyInfo, + flashmint, macros, logsEnabled: true, }); @@ -189,6 +194,26 @@ function parseRoyaltyInfoSubset(value: string): RoyaltyInfoSubset { } } +function parseFlashMintSubset(value: string): FlashMintSubset { + switch (value.toLowerCase()) { + case 'all': + return 'all'; + case 'disabled': + return 'disabled'; + case 'enabled-default': + case 'enabled_default': + return 'enabled-default'; + case 'enabled-percent-fee': + case 'enabled_percent_fee': + return 'enabled-percent-fee'; + case 'enabled-custom-fee': + case 'enabled_custom_fee': + return 'enabled-custom-fee'; + default: + throw new Error(`Failed to resolve flashmint subset from '${value}' value.`); + } +} + function parseMacrosSubset(value: string): MacrosSubset { switch (value.toLowerCase()) { case 'all': diff --git a/packages/core/cairo/src/test.ts b/packages/core/cairo/src/test.ts index 3559218f5..6bb72a19c 100644 --- a/packages/core/cairo/src/test.ts +++ b/packages/core/cairo/src/test.ts @@ -8,6 +8,7 @@ import type { KindSubset } from './generate/sources'; import type { AccessSubset } from './set-access-control'; import type { UpgradeableSubset } from './set-upgradeable'; import type { RoyaltyInfoSubset } from './set-royalty-info'; +import type { FlashMintSubset } from './erc20'; import type { GenericOptions } from './build-generic'; import type { MacrosSubset } from './set-macros'; import { generateSources, writeGeneratedSources } from './generate/sources'; @@ -20,7 +21,7 @@ interface Context { const test = _test as TestFn; test.serial('erc20 results generated', async ctx => { - await testGenerate({ ctx, kind: 'ERC20', access: 'all' }); + await testGenerate({ ctx, kind: 'ERC20', access: 'all', flashmint: 'all' }); }); test.serial('erc721 results generated', async ctx => { @@ -61,9 +62,10 @@ async function testGenerate(params: { access?: AccessSubset; upgradeable?: UpgradeableSubset; royaltyInfo?: RoyaltyInfoSubset; + flashmint?: FlashMintSubset; macros?: MacrosSubset; }) { - const { ctx, kind, access, upgradeable, royaltyInfo, macros } = params; + const { ctx, kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; const generatedSourcesPath = path.join(os.tmpdir(), 'oz-wizard-cairo'); await fs.rm(generatedSourcesPath, { force: true, recursive: true }); await writeGeneratedSources({ @@ -74,6 +76,7 @@ async function testGenerate(params: { access: access || 'all', upgradeable: upgradeable || 'all', royaltyInfo: royaltyInfo || 'all', + flashmint: flashmint || 'all', macros: macros || 'all', logsEnabled: false, }); @@ -114,6 +117,7 @@ test('is access control required', async t => { access: 'all', upgradeable: 'all', royaltyInfo: 'all', + flashmint: 'all', macros: 'none', }); for (const contract of allSources) { diff --git a/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts b/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts index f723379c3..1b789eaf4 100644 --- a/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts +++ b/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts @@ -1,7 +1,7 @@ import test from 'ava'; import type { ERC20Options } from '../../../erc20'; -import { buildERC20, getInitialSupply } from '../../../erc20'; +import { buildERC20, flashMintDefaults, getInitialSupply } from '../../../erc20'; import { printContract } from '../../../print'; import { AccessControl, darDefaultOpts, darCustomOpts } from '../../../set-access-control'; @@ -76,6 +76,128 @@ testERC20('erc20 wrapper', { wrapper: true, }); +testERC20('erc20 flash mint', { + flashmint: { ...flashMintDefaults, enabled: true }, +}); + +testERC20('erc20 flash mint with custom max', { + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '1000000' }, +}); + +testERC20('erc20 flash mint with percent fee', { + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '5' }, +}); + +testERC20('erc20 flash mint with fractional percent fee', { + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '0.0013725' }, +}); + +testERC20('erc20 flash mint with percent fee and fee receiver', { + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, +}); + +testERC20('erc20 flash mint with custom fee impl', { + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'custom' }, +}); + +testERC20('erc20 flash mint with all custom config', { + flashmint: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, +}); + +testERC20('erc20 flash mint with max amount of zero', { + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '0' }, +}); + +test('erc20 flash mint, max amount empty string', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid max amount', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid percent fee', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, percent fee out of range', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '100.5' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, max amount overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + decimals: '0', + flashmint: { + ...flashMintDefaults, + enabled: true, + // 2^256 — one past u256::MAX with decimals: '0' so getInitialSupply preserves the value. + maxAmount: '115792089237316195423570985008687907853269984665640564039457584007913129639936', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Value is greater than u256 max value'); +}); + +test('erc20 flash mint, percent denominator overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + // 77 fractional digits — 100 * 10^77 > u256::MAX. + feePercent: '0.' + '0'.repeat(76) + '1', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Value is greater than u256 max value'); +}); + testERC20('erc20 preminted', { premint: '1000', }); diff --git a/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts.md b/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts.md index 6f03c9d88..6c1b7bc17 100644 --- a/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts.md +++ b/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts.md @@ -896,6 +896,780 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 flash mint + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::{␊ + DefaultConfig as ERC20FlashMintDefaultConfig, ERC20FlashMintComponent␊ + };␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom max + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with fractional percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 13725 / 1000000000␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee and fee receiver + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::Zero;␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom fee impl + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + // TODO: Must be implemented according to the desired flash fee logic;␊ + 0␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with all custom config + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::{Bounded, Zero};␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + ␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with max amount of zero + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 0;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + ## erc20 preminted > Snapshot 1 diff --git a/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts.snap b/packages/core/cairo/src/tests/with_components_off/erc20/erc20.test.ts.snap index 42b7d04e4271c84da75d324a0c1ab9d2b2da9c15..9447a01a81cc9c327c1d4754f76cde2a51ecd522 100644 GIT binary patch literal 5666 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 literal 4162 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 { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid max amount', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid percent fee', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, percent fee out of range', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '100.5' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, max amount overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + decimals: '0', + flashmint: { + ...flashMintDefaults, + enabled: true, + // 2^256 — one past u256::MAX with decimals: '0' so getInitialSupply preserves the value. + maxAmount: '115792089237316195423570985008687907853269984665640564039457584007913129639936', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Value is greater than u256 max value'); +}); + +test('erc20 flash mint, percent denominator overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + // 77 fractional digits — 100 * 10^77 > u256::MAX. + feePercent: '0.' + '0'.repeat(76) + '1', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Value is greater than u256 max value'); +}); + testERC20('erc20 preminted', { premint: '1000', }); diff --git a/packages/core/cairo/src/tests/with_components_on/erc20/erc20.test.ts.md b/packages/core/cairo/src/tests/with_components_on/erc20/erc20.test.ts.md index 27b5372e2..8e75ac0df 100644 --- a/packages/core/cairo/src/tests/with_components_on/erc20/erc20.test.ts.md +++ b/packages/core/cairo/src/tests/with_components_on/erc20/erc20.test.ts.md @@ -589,6 +589,493 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 flash mint + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::DefaultConfig as ERC20FlashMintDefaultConfig;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom max + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with fractional percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 13725 / 1000000000␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee and fee receiver + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::Zero;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom fee impl + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + // TODO: Must be implemented according to the desired flash fee logic;␊ + 0␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with all custom config + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::{Bounded, Zero};␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + ␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with max amount of zero + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 0;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + ## erc20 preminted > Snapshot 1 diff --git a/packages/core/cairo/src/tests/with_components_on/erc20/erc20.test.ts.snap b/packages/core/cairo/src/tests/with_components_on/erc20/erc20.test.ts.snap index f064e21421ef0904f07dbadc10b21e9bef4aac92..ffc3117e021a513d5f6639d6831c913e4f01bea8 100644 GIT binary patch literal 3802 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 QK^HUs2ThCsTB8#I0Dxd-2><{9 literal 2879 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 diff --git a/packages/core/cairo_alpha/CHANGELOG.md b/packages/core/cairo_alpha/CHANGELOG.md index 08f145b32..e850917ce 100644 --- a/packages/core/cairo_alpha/CHANGELOG.md +++ b/packages/core/cairo_alpha/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add ERC20FlashMint extension ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) + ## 4.0.1 (2026-08-31) - Add ERC6909 contract kind ([#771](https://github.com/OpenZeppelin/contracts-wizard/pull/771)) diff --git a/packages/core/cairo_alpha/src/contract.ts b/packages/core/cairo_alpha/src/contract.ts index 2543086dc..759503eb6 100644 --- a/packages/core/cairo_alpha/src/contract.ts +++ b/packages/core/cairo_alpha/src/contract.ts @@ -15,6 +15,12 @@ export interface Contract { upgradeable: boolean; implementedTraits: ImplementedTrait[]; superVariables: Variable[]; + storageMembers: StorageMember[]; +} + +export interface StorageMember { + name: string; + type: string; } export type Value = string | number | bigint | { lit: string } | { note: string; value: Value }; @@ -118,6 +124,7 @@ export class ContractBuilder implements Contract { private constantsMap: Map = new Map(); private useClausesMap: Map = new Map(); private interfaceFlagsSet: Set = new Set(); + private storageMembersMap: Map = new Map(); constructor(name: string, macros: MacrosOptions, account: boolean = false) { this.name = toIdentifier(name, true); @@ -145,6 +152,24 @@ export class ContractBuilder implements Contract { return [...this.useClausesMap.values()]; } + get storageMembers(): StorageMember[] { + return [...this.storageMembersMap.values()]; + } + + addStorageMember(member: StorageMember): boolean { + const existing = this.storageMembersMap.get(member.name); + if (existing !== undefined) { + if (existing.type !== member.type) { + throw new Error( + `Tried to add duplicate storage member ${member.name} with different type: ${member.type} instead of ${existing.type}.`, + ); + } + return false; + } + this.storageMembersMap.set(member.name, member); + return true; + } + /** * Custom flags to denote that the contract implements a specific interface, e.g. ISRC5, to avoid duplicates **/ diff --git a/packages/core/cairo_alpha/src/erc20.ts b/packages/core/cairo_alpha/src/erc20.ts index 0db80febe..d0c232c82 100644 --- a/packages/core/cairo_alpha/src/erc20.ts +++ b/packages/core/cairo_alpha/src/erc20.ts @@ -18,6 +18,66 @@ import { addVotesComponent } from './common-components'; const DEFAULT_DECIMALS = BigInt(18); +export type FlashMintFeeMode = 'percent' | 'custom'; +export type FlashMintFeeDestination = 'burn' | 'fee_receiver'; + +export type FlashMintOptions = { + enabled: boolean; + maxAmount: string; + feeMode: FlashMintFeeMode; + feePercent: string; + feeDestination: FlashMintFeeDestination; +}; + +export const flashMintDefaults: FlashMintOptions = { + enabled: false, + maxAmount: 'max', + feeMode: 'percent', + feePercent: '0', + feeDestination: 'burn', +}; + +export type FlashMintSubset = 'all' | 'disabled' | 'enabled-default' | 'enabled-percent-fee' | 'enabled-custom-fee'; + +export const flashMintOptions = { + disabled: flashMintDefaults, + enabledDefault: { ...flashMintDefaults, enabled: true } satisfies FlashMintOptions, + enabledPercentFee: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '0.5', + feeDestination: 'fee_receiver', + } satisfies FlashMintOptions, + enabledCustomFee: { + enabled: true, + maxAmount: 'max', + feeMode: 'custom', + feePercent: '0', + feeDestination: 'fee_receiver', + } satisfies FlashMintOptions, +}; + +export function resolveFlashMintOptionsSubset(subset: FlashMintSubset): FlashMintOptions[] { + const { disabled, enabledDefault, enabledPercentFee, enabledCustomFee } = flashMintOptions; + switch (subset) { + case 'all': + return [disabled, enabledDefault, enabledPercentFee, enabledCustomFee]; + case 'disabled': + return [disabled]; + case 'enabled-default': + return [enabledDefault]; + case 'enabled-percent-fee': + return [enabledPercentFee]; + case 'enabled-custom-fee': + return [enabledCustomFee]; + default: { + const _: never = subset; + throw new Error('Unknown FlashMintSubset'); + } + } +} + export const defaults: Required = { name: 'MyToken', symbol: 'MTK', @@ -30,6 +90,7 @@ export const defaults: Required = { votes: false, appName: '', // Defaults to empty string, but user must provide a non-empty value if votes are enabled appVersion: 'v1', + flashmint: flashMintDefaults, access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, info: commonDefaults.info, @@ -52,6 +113,7 @@ export interface ERC20Options extends CommonContractOptions { votes?: boolean; appName?: string; appVersion?: string; + flashmint?: FlashMintOptions; } function withDefaults(opts: ERC20Options): Required { @@ -67,6 +129,7 @@ function withDefaults(opts: ERC20Options): Required { votes: opts.votes ?? defaults.votes, appName: opts.appName ?? defaults.appName, appVersion: opts.appVersion ?? defaults.appVersion, + flashmint: opts.flashmint ?? defaults.flashmint, }; } @@ -102,6 +165,10 @@ export function buildERC20(opts: ERC20Options): Contract { addWrapper(c); } + if (allOpts.flashmint.enabled) { + addFlashMint(c, allOpts.flashmint, decimals); + } + addHooks(c, allOpts); setAccessControl(c, allOpts.access); @@ -294,6 +361,145 @@ function addWrapper(c: ContractBuilder) { c.addComponent(components.ERC20WrapperComponent, [{ lit: 'underlying' }], true); } +function parseFlashMintMaxAmount(value: string, decimals: bigint): bigint | null { + if (value === 'max') { + return null; + } + if (value === '' || !premintPattern.test(value)) { + throw new OptionsError({ flashMintMaxAmount: 'Must be "max" or a non-negative number' }); + } + return toUint(getInitialSupply(value, Number(decimals)), 'flashMintMaxAmount', 'u256'); +} + +function parseFlashMintFeePercent(value: string): { numerator: bigint; denominator: bigint } | null { + if (value === '') { + return null; + } + if (!premintPattern.test(value)) { + throw new OptionsError({ flashMintFeePercent: 'Must be a number between 0 and 100' }); + } + const [intPart = '', fracPart = ''] = value.split('.'); + const decimalDigits = fracPart.length; + const combined = (intPart + fracPart).replace(/^0+/, ''); + if (combined === '') { + return null; + } + const numerator = BigInt(combined); + const decimalScale = 10n ** BigInt(decimalDigits); + // value = numerator / decimalScale, must be <= 100 + if (numerator > 100n * decimalScale) { + throw new OptionsError({ flashMintFeePercent: 'Must be a number between 0 and 100' }); + } + // For percent of amount: amount * value / 100 = amount * numerator / (100 * decimalScale). + // Both literals are emitted into Cairo and must fit u256; numerator <= denominator from the + // check above, so bounding the denominator is sufficient. + const denominator = 100n * decimalScale; + toUint(denominator.toString(), 'flashMintFeePercent', 'u256'); + return { numerator, denominator }; +} + +function buildFlashFeeOverrideBody(opts: FlashMintOptions): string[] | null { + switch (opts.feeMode) { + case 'percent': { + const parsed = parseFlashMintFeePercent(opts.feePercent); + if (parsed === null) { + return null; + } + return [`amount * ${parsed.numerator} / ${parsed.denominator}`]; + } + case 'custom': + return ['// TODO: Must be implemented according to the desired flash fee logic', '0']; + default: { + const _: never = opts.feeMode; + throw new Error(`Unknown flashMintFeeMode: ${_}`); + } + } +} + +function addFlashMint(c: ContractBuilder, opts: FlashMintOptions, decimals: bigint) { + c.addComponent(components.ERC20FlashMintComponent, [], false); + + const customMax = parseFlashMintMaxAmount(opts.maxAmount, decimals); + const overridesMax = customMax !== null; + const overridesReceiver = opts.feeDestination === 'fee_receiver'; + const feeOverrideBody = buildFlashFeeOverrideBody(opts); + const overridesFee = feeOverrideBody !== null; + + if (!overridesMax && !overridesFee && !overridesReceiver) { + c.addUseClause('openzeppelin_token::erc20::extensions::erc20_flash_mint', 'DefaultConfig', { + alias: 'ERC20FlashMintDefaultConfig', + }); + return; + } + + const flashMintConfigTrait: BaseImplementedTrait = { + name: 'FlashMintConfigImpl', + of: 'ERC20FlashMintComponent::FlashMintConfigTrait', + tags: [], + }; + c.addImplementedTrait(flashMintConfigTrait); + + if (overridesMax || overridesFee || overridesReceiver) { + c.addUseClause('starknet', 'ContractAddress'); + } + + if (overridesMax) { + c.addUseClause('starknet', 'get_contract_address'); + c.addUseClause('core::num::traits', 'Bounded'); + const fn = c.addFunction(flashMintConfigTrait, { + name: 'max_flash_loan', + args: [ + { name: 'self', type: '@ERC20FlashMintComponent::ComponentState' }, + { name: 'token', type: 'ContractAddress' }, + { name: 'total_supply', type: 'u256' }, + ], + returns: 'u256', + code: [], + }); + // Clamp the configured cap to the remaining mint headroom so the loan size we report is + // never larger than what the underlying mint path can actually honor. + fn.code.push( + 'if token != get_contract_address() {', + ' return 0;', + '}', + 'let headroom = Bounded::::MAX - total_supply;', + `let cap: u256 = ${customMax!};`, + 'if cap < headroom { cap } else { headroom }', + ); + } + + if (overridesFee) { + const fn = c.addFunction(flashMintConfigTrait, { + name: 'flash_fee', + args: [ + { name: 'self', type: '@ERC20FlashMintComponent::ComponentState' }, + { name: 'token', type: 'ContractAddress' }, + { name: 'amount', type: 'u256' }, + ], + returns: 'u256', + code: [], + }); + fn.code.push(...feeOverrideBody!); + } + + if (overridesReceiver) { + c.addUseClause('core::num::traits', 'Zero'); + c.addUseClause('starknet::storage', 'StoragePointerReadAccess'); + c.addUseClause('starknet::storage', 'StoragePointerWriteAccess'); + c.addStorageMember({ name: 'flash_fee_receiver', type: 'ContractAddress' }); + c.addConstructorArgument({ name: 'flash_fee_receiver', type: 'ContractAddress' }); + c.addConstructorCode(`assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver')`); + c.addConstructorCode(`self.flash_fee_receiver.write(flash_fee_receiver)`); + const fn = c.addFunction(flashMintConfigTrait, { + name: 'flash_fee_receiver', + args: [{ name: 'self', type: '@ERC20FlashMintComponent::ComponentState' }], + returns: 'ContractAddress', + code: [], + }); + fn.code.push('self.get_contract().flash_fee_receiver.read()'); + } +} + const components = defineComponents({ ERC20Component: { path: 'openzeppelin_token::erc20', @@ -336,6 +542,24 @@ const components = defineComponents({ }, ], }, + ERC20FlashMintComponent: { + path: 'openzeppelin_token::erc20::extensions::erc20_flash_mint', + substorage: { + name: 'erc20_flash_mint', + type: 'ERC20FlashMintComponent::Storage', + }, + event: { + name: 'ERC20FlashMintEvent', + type: 'ERC20FlashMintComponent::Event', + }, + impls: [ + { + name: 'ERC20FlashMintImpl', + embed: true, + value: 'ERC20FlashMintComponent::ERC20FlashMintImpl', + }, + ], + }, }); const functions = defineFunctions({ diff --git a/packages/core/cairo_alpha/src/generate/erc20.ts b/packages/core/cairo_alpha/src/generate/erc20.ts index 2849f72b3..0b7d6b428 100644 --- a/packages/core/cairo_alpha/src/generate/erc20.ts +++ b/packages/core/cairo_alpha/src/generate/erc20.ts @@ -1,4 +1,5 @@ -import type { ERC20Options } from '../erc20'; +import type { ERC20Options, FlashMintSubset } from '../erc20'; +import { resolveFlashMintOptionsSubset } from '../erc20'; import type { AccessSubset } from '../set-access-control'; import { resolveAccessControlOptions } from '../set-access-control'; import { infoOptions } from '../set-info'; @@ -13,6 +14,7 @@ const booleans = [true, false]; type GeneratorOptions = { access: AccessSubset; upgradeable: UpgradeableSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }; @@ -29,6 +31,7 @@ function prepareBlueprint(opts: GeneratorOptions) { votes: booleans, appName: ['MyApp'], appVersion: ['v1'], + flashmint: resolveFlashMintOptionsSubset(opts.flashmint), access: resolveAccessControlOptions(opts.access), upgradeable: resolveUpgradeableOptionsSubset(opts.upgradeable), info: infoOptions, diff --git a/packages/core/cairo_alpha/src/generate/sources.ts b/packages/core/cairo_alpha/src/generate/sources.ts index 72b1b371c..76f5a8392 100644 --- a/packages/core/cairo_alpha/src/generate/sources.ts +++ b/packages/core/cairo_alpha/src/generate/sources.ts @@ -18,6 +18,7 @@ import { OptionsError } from '../error'; import { findCover } from '../utils/find-cover'; import type { Contract } from '../contract'; import type { RoyaltyInfoSubset } from '../set-royalty-info'; +import type { FlashMintSubset } from '../erc20'; import type { MacrosSubset } from '../set-macros'; import type { AccessSubset } from '../set-access-control'; import type { UpgradeableSubset } from '../set-upgradeable'; @@ -31,11 +32,12 @@ export function* generateOptions(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): Generator { - const { kind, access, upgradeable, royaltyInfo, macros } = params; + const { kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; if (kind === 'all' || kind === 'ERC20') { - for (const kindOpts of generateERC20Options({ access, upgradeable, macros })) { + for (const kindOpts of generateERC20Options({ access, upgradeable, flashmint, macros })) { yield { kind: 'ERC20', ...kindOpts }; } } @@ -105,12 +107,13 @@ function generateContractSubset(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): GeneratedContract[] { - const { subset, kind, access, upgradeable, royaltyInfo, macros } = params; + const { subset, kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; const contracts = []; - for (const options of generateOptions({ kind, access, upgradeable, royaltyInfo, macros })) { + for (const options of generateOptions({ kind, access, upgradeable, royaltyInfo, flashmint, macros })) { const id = crypto.createHash('sha1').update(JSON.stringify(options)).digest().toString('hex'); try { const contract = buildGeneric(options); @@ -163,11 +166,12 @@ export function* generateSources(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): Generator { - const { subset, uniqueName, kind, access, upgradeable, royaltyInfo, macros } = params; + const { subset, uniqueName, kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; let counter = 1; - for (const c of generateContractSubset({ subset, kind, access, upgradeable, royaltyInfo, macros })) { + for (const c of generateContractSubset({ subset, kind, access, upgradeable, royaltyInfo, flashmint, macros })) { if (uniqueName) { c.contract.name = `Contract${counter++}`; } @@ -184,10 +188,11 @@ export async function writeGeneratedSources(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; logsEnabled: boolean; }): Promise { - const { dir, subset, uniqueName, kind, access, upgradeable, royaltyInfo, macros, logsEnabled } = params; + const { dir, subset, uniqueName, kind, access, upgradeable, royaltyInfo, flashmint, macros, logsEnabled } = params; await fs.mkdir(dir, { recursive: true }); const contractNames = []; @@ -198,6 +203,7 @@ export async function writeGeneratedSources(params: { access, upgradeable, royaltyInfo, + flashmint, macros, })) { const name = uniqueName ? contract.name : id; @@ -205,7 +211,7 @@ export async function writeGeneratedSources(params: { contractNames.push(name); } if (logsEnabled) { - const sourceLabel = resolveSourceLabel({ kind, access, upgradeable, royaltyInfo, macros }); + const sourceLabel = resolveSourceLabel({ kind, access, upgradeable, royaltyInfo, flashmint, macros }); console.log(`Generated ${contractNames.length} contracts for ${sourceLabel}`); } @@ -217,15 +223,17 @@ function resolveSourceLabel(params: { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }): string { - const { kind, access, upgradeable, royaltyInfo, macros } = params; + const { kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; return [ resolveKindLabel(kind), resolveMacrosLabel(macros), resolveAccessLabel(kind, access), resolveUpgradeableLabel(kind, upgradeable), resolveRoyaltyInfoLabel(kind, royaltyInfo), + resolveFlashMintLabel(kind, flashmint), ] .filter(elem => elem !== undefined) .join(', '); @@ -318,3 +326,24 @@ function resolveRoyaltyInfoLabel(kind: KindSubset, royaltyInfo: RoyaltyInfoSubse } } } + +function resolveFlashMintLabel(kind: KindSubset, flashmint: FlashMintSubset): string | undefined { + switch (kind) { + case 'all': + case 'ERC20': + return `flashmint: ${flashmint}`; + case 'ERC721': + case 'ERC1155': + case 'ERC6909': + case 'Account': + case 'Custom': + case 'Multisig': + case 'Governor': + case 'Vesting': + return undefined; + default: { + const _: never = kind; + throw new Error('Unknown kind'); + } + } +} diff --git a/packages/core/cairo_alpha/src/index.ts b/packages/core/cairo_alpha/src/index.ts index 27bd959f3..bceb4c1ad 100644 --- a/packages/core/cairo_alpha/src/index.ts +++ b/packages/core/cairo_alpha/src/index.ts @@ -13,7 +13,8 @@ export type { Info } from './set-info'; export type { RoyaltyInfoOptions } from './set-royalty-info'; export type { MacrosOptions } from './set-macros'; -export { premintPattern } from './erc20'; +export { premintPattern, flashMintDefaults } from './erc20'; +export type { FlashMintOptions, FlashMintFeeMode, FlashMintFeeDestination } from './erc20'; export { defaults as infoDefaults } from './set-info'; export { defaults as royaltyInfoDefaults } from './set-royalty-info'; diff --git a/packages/core/cairo_alpha/src/print.ts b/packages/core/cairo_alpha/src/print.ts index 817ba2925..0916103d6 100644 --- a/packages/core/cairo_alpha/src/print.ts +++ b/packages/core/cairo_alpha/src/print.ts @@ -219,11 +219,16 @@ function printImpl(impl: Impl): Lines[] { } function printStorage(contract: Contract): (string | string[])[] { + const memberLines = contract.storageMembers.map(m => `${m.name}: ${m.type},`); + if (contract.macros.withComponents || contract.components.length === 0) { // storage is required regardless of whether there are components - return ['#[storage]', 'struct Storage {}']; + if (memberLines.length === 0) { + return ['#[storage]', 'struct Storage {}']; + } + return ['#[storage]', 'struct Storage {', memberLines, '}']; } - const storageLines = []; + const storageLines = [...memberLines]; for (const component of contract.components) { storageLines.push(`#[substorage(v0)]`); storageLines.push(`${component.substorage.name}: ${component.substorage.type},`); diff --git a/packages/core/cairo_alpha/src/scripts/update-scarb-project.ts b/packages/core/cairo_alpha/src/scripts/update-scarb-project.ts index a76869aeb..73b2bbff1 100644 --- a/packages/core/cairo_alpha/src/scripts/update-scarb-project.ts +++ b/packages/core/cairo_alpha/src/scripts/update-scarb-project.ts @@ -5,6 +5,7 @@ import type { KindSubset } from '../generate/sources'; import type { AccessSubset } from '../set-access-control'; import type { UpgradeableSubset } from '../set-upgradeable'; import type { RoyaltyInfoSubset } from '../set-royalty-info'; +import type { FlashMintSubset } from '../erc20'; import type { MacrosSubset } from '../set-macros'; import { writeGeneratedSources } from '../generate/sources'; import { contractsVersion, edition, cairoVersion, scarbVersion } from '../utils/version'; @@ -14,6 +15,7 @@ type Arguments = { access: AccessSubset; upgradeable: UpgradeableSubset; royaltyInfo: RoyaltyInfoSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }; @@ -22,6 +24,7 @@ const defaults = { access: 'all', upgradeable: 'all', royaltyInfo: 'all', + flashmint: 'all', macros: 'all', } as const; @@ -33,6 +36,7 @@ export function resolveArguments(): Arguments { access: parseAccessSubset(args.access ?? defaults.access), upgradeable: parseUpgradeableSubset(args.upgradeable ?? defaults.upgradeable), royaltyInfo: parseRoyaltyInfoSubset(args.royalty ?? defaults.royaltyInfo), + flashmint: parseFlashMintSubset(args.flashmint ?? defaults.flashmint), macros: parseMacrosSubset(args.macros ?? defaults.macros), }; } @@ -42,7 +46,7 @@ export async function updateScarbProject() { await fs.rm(generatedSourcesPath, { force: true, recursive: true }); // Generate the contracts source code - const { kind, access, upgradeable, royaltyInfo, macros } = resolveArguments(); + const { kind, access, upgradeable, royaltyInfo, flashmint, macros } = resolveArguments(); const contractNames = await writeGeneratedSources({ dir: generatedSourcesPath, subset: 'all', @@ -51,6 +55,7 @@ export async function updateScarbProject() { access, upgradeable, royaltyInfo, + flashmint, macros, logsEnabled: true, }); @@ -189,6 +194,26 @@ function parseRoyaltyInfoSubset(value: string): RoyaltyInfoSubset { } } +function parseFlashMintSubset(value: string): FlashMintSubset { + switch (value.toLowerCase()) { + case 'all': + return 'all'; + case 'disabled': + return 'disabled'; + case 'enabled-default': + case 'enabled_default': + return 'enabled-default'; + case 'enabled-percent-fee': + case 'enabled_percent_fee': + return 'enabled-percent-fee'; + case 'enabled-custom-fee': + case 'enabled_custom_fee': + return 'enabled-custom-fee'; + default: + throw new Error(`Failed to resolve flashmint subset from '${value}' value.`); + } +} + function parseMacrosSubset(value: string): MacrosSubset { switch (value.toLowerCase()) { case 'all': diff --git a/packages/core/cairo_alpha/src/test.ts b/packages/core/cairo_alpha/src/test.ts index 66de81a77..6b58008e7 100644 --- a/packages/core/cairo_alpha/src/test.ts +++ b/packages/core/cairo_alpha/src/test.ts @@ -8,6 +8,7 @@ import type { KindSubset } from './generate/sources'; import type { AccessSubset } from './set-access-control'; import type { UpgradeableSubset } from './set-upgradeable'; import type { RoyaltyInfoSubset } from './set-royalty-info'; +import type { FlashMintSubset } from './erc20'; import type { GenericOptions } from './build-generic'; import type { MacrosSubset } from './set-macros'; import { generateSources, writeGeneratedSources } from './generate/sources'; @@ -20,7 +21,7 @@ interface Context { const test = _test as TestFn; test.serial('erc20 results generated', async ctx => { - await testGenerate({ ctx, kind: 'ERC20', access: 'all' }); + await testGenerate({ ctx, kind: 'ERC20', access: 'all', flashmint: 'all' }); }); test.serial('erc721 results generated', async ctx => { @@ -61,9 +62,10 @@ async function testGenerate(params: { access?: AccessSubset; upgradeable?: UpgradeableSubset; royaltyInfo?: RoyaltyInfoSubset; + flashmint?: FlashMintSubset; macros?: MacrosSubset; }) { - const { ctx, kind, access, upgradeable, royaltyInfo, macros } = params; + const { ctx, kind, access, upgradeable, royaltyInfo, flashmint, macros } = params; const generatedSourcesPath = path.join(os.tmpdir(), 'oz-wizard-cairo-alpha'); await fs.rm(generatedSourcesPath, { force: true, recursive: true }); await writeGeneratedSources({ @@ -74,6 +76,7 @@ async function testGenerate(params: { access: access || 'all', upgradeable: upgradeable || 'all', royaltyInfo: royaltyInfo || 'all', + flashmint: flashmint || 'all', macros: macros || 'all', logsEnabled: false, }); @@ -114,6 +117,7 @@ test('is access control required', async t => { access: 'all', upgradeable: 'all', royaltyInfo: 'all', + flashmint: 'all', macros: 'none', }); for (const contract of allSources) { diff --git a/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts b/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts index f723379c3..1b789eaf4 100644 --- a/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts +++ b/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts @@ -1,7 +1,7 @@ import test from 'ava'; import type { ERC20Options } from '../../../erc20'; -import { buildERC20, getInitialSupply } from '../../../erc20'; +import { buildERC20, flashMintDefaults, getInitialSupply } from '../../../erc20'; import { printContract } from '../../../print'; import { AccessControl, darDefaultOpts, darCustomOpts } from '../../../set-access-control'; @@ -76,6 +76,128 @@ testERC20('erc20 wrapper', { wrapper: true, }); +testERC20('erc20 flash mint', { + flashmint: { ...flashMintDefaults, enabled: true }, +}); + +testERC20('erc20 flash mint with custom max', { + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '1000000' }, +}); + +testERC20('erc20 flash mint with percent fee', { + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '5' }, +}); + +testERC20('erc20 flash mint with fractional percent fee', { + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '0.0013725' }, +}); + +testERC20('erc20 flash mint with percent fee and fee receiver', { + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, +}); + +testERC20('erc20 flash mint with custom fee impl', { + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'custom' }, +}); + +testERC20('erc20 flash mint with all custom config', { + flashmint: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, +}); + +testERC20('erc20 flash mint with max amount of zero', { + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '0' }, +}); + +test('erc20 flash mint, max amount empty string', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid max amount', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid percent fee', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, percent fee out of range', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '100.5' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, max amount overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + decimals: '0', + flashmint: { + ...flashMintDefaults, + enabled: true, + // 2^256 — one past u256::MAX with decimals: '0' so getInitialSupply preserves the value. + maxAmount: '115792089237316195423570985008687907853269984665640564039457584007913129639936', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Value is greater than u256 max value'); +}); + +test('erc20 flash mint, percent denominator overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + // 77 fractional digits — 100 * 10^77 > u256::MAX. + feePercent: '0.' + '0'.repeat(76) + '1', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Value is greater than u256 max value'); +}); + testERC20('erc20 preminted', { premint: '1000', }); diff --git a/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts.md b/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts.md index 6f03c9d88..6c1b7bc17 100644 --- a/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts.md +++ b/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts.md @@ -896,6 +896,780 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 flash mint + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::{␊ + DefaultConfig as ERC20FlashMintDefaultConfig, ERC20FlashMintComponent␊ + };␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom max + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with fractional percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 13725 / 1000000000␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee and fee receiver + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::Zero;␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom fee impl + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + // TODO: Must be implemented according to the desired flash fee logic;␊ + 0␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with all custom config + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::{Bounded, Zero};␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + ␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with max amount of zero + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_access::ownable::OwnableComponent;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{␊ + DefaultConfig as ERC20DefaultConfig, ERC20Component, ERC20HooksEmptyImpl␊ + };␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::ERC20FlashMintComponent;␊ + use openzeppelin_upgrades::UpgradeableComponent;␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + component!(path: ERC20Component, storage: erc20, event: ERC20Event);␊ + component!(path: ERC20FlashMintComponent, storage: erc20_flash_mint, event: ERC20FlashMintEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + // Internal␊ + impl ERC20InternalImpl = ERC20Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc20: ERC20Component::Storage,␊ + #[substorage(v0)]␊ + erc20_flash_mint: ERC20FlashMintComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC20Event: ERC20Component::Event,␊ + #[flat]␊ + ERC20FlashMintEvent: ERC20FlashMintComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 0;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + ## erc20 preminted > Snapshot 1 diff --git a/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts.snap b/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts.snap index 42b7d04e4271c84da75d324a0c1ab9d2b2da9c15..9447a01a81cc9c327c1d4754f76cde2a51ecd522 100644 GIT binary patch literal 5666 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 literal 4162 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 { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid max amount', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); +}); + +test('erc20 flash mint, invalid percent fee', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: 'abc' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, percent fee out of range', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '100.5' }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + +test('erc20 flash mint, max amount overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + decimals: '0', + flashmint: { + ...flashMintDefaults, + enabled: true, + // 2^256 — one past u256::MAX with decimals: '0' so getInitialSupply preserves the value. + maxAmount: '115792089237316195423570985008687907853269984665640564039457584007913129639936', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintMaxAmount, 'Value is greater than u256 max value'); +}); + +test('erc20 flash mint, percent denominator overflows u256', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + // 77 fractional digits — 100 * 10^77 > u256::MAX. + feePercent: '0.' + '0'.repeat(76) + '1', + }, + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Value is greater than u256 max value'); +}); + testERC20('erc20 preminted', { premint: '1000', }); diff --git a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts.md b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts.md index 27b5372e2..8e75ac0df 100644 --- a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts.md +++ b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts.md @@ -589,6 +589,493 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 flash mint + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use openzeppelin_token::erc20::extensions::erc20_flash_mint::DefaultConfig as ERC20FlashMintDefaultConfig;␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom max + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with fractional percent fee + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 13725 / 1000000000␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with percent fee and fee receiver + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::Zero;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with custom fee impl + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + // TODO: Must be implemented according to the desired flash fee logic;␊ + 0␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with all custom config + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::{Bounded, Zero};␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + flash_fee_receiver: ContractAddress,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + flash_fee_receiver: ContractAddress,␊ + owner: ContractAddress,␊ + ) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + ␊ + assert(!flash_fee_receiver.is_zero(), 'FlashMint: invalid receiver');␊ + self.flash_fee_receiver.write(flash_fee_receiver);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + ␊ + fn flash_fee(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + amount: u256,␊ + ) -> u256 {␊ + amount * 5 / 100␊ + }␊ + ␊ + fn flash_fee_receiver(self: @ERC20FlashMintComponent::ComponentState) -> ContractAddress {␊ + self.get_contract().flash_fee_receiver.read()␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## erc20 flash mint with max amount of zero + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^4.0.1␊ + ␊ + #[starknet::contract]␊ + #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ + mod MyToken {␊ + use core::num::traits::Bounded;␊ + use openzeppelin_interfaces::upgrades::IUpgradeable;␊ + use openzeppelin_token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20HooksEmptyImpl};␊ + use starknet::{ClassHash, ContractAddress, get_contract_address};␊ + ␊ + // External␊ + #[abi(embed_v0)]␊ + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC20FlashMintImpl = ERC20FlashMintComponent::ERC20FlashMintImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + #[storage]␊ + struct Storage {}␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc20.initializer("MyToken", "MTK");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl FlashMintConfigImpl of ERC20FlashMintComponent::FlashMintConfigTrait {␊ + fn max_flash_loan(␊ + self: @ERC20FlashMintComponent::ComponentState,␊ + token: ContractAddress,␊ + total_supply: u256,␊ + ) -> u256 {␊ + if token != get_contract_address() {␊ + return 0;␊ + }␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 0;␊ + if cap < headroom { cap } else { headroom }␊ + }␊ + }␊ + ␊ + //␊ + // Upgradeable␊ + //␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + ## erc20 preminted > Snapshot 1 diff --git a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts.snap b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts.snap index f064e21421ef0904f07dbadc10b21e9bef4aac92..ffc3117e021a513d5f6639d6831c913e4f01bea8 100644 GIT binary patch literal 3802 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 QK^HUs2ThCsTB8#I0Dxd-2><{9 literal 2879 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 diff --git a/packages/mcp/src/cairo/tools/erc20.test.ts b/packages/mcp/src/cairo/tools/erc20.test.ts index af1a1eddf..1a7857231 100644 --- a/packages/mcp/src/cairo/tools/erc20.test.ts +++ b/packages/mcp/src/cairo/tools/erc20.test.ts @@ -57,6 +57,13 @@ test('all', async t => { votes: true, appName: 'MyToken', appVersion: 'v1', + flashmint: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '0.5', + feeDestination: 'fee_receiver', + }, access: { type: 'roles', darInitialDelay: '0', diff --git a/packages/mcp/src/cairo/tools/erc20.ts b/packages/mcp/src/cairo/tools/erc20.ts index dd01f8f6e..12725978d 100644 --- a/packages/mcp/src/cairo/tools/erc20.ts +++ b/packages/mcp/src/cairo/tools/erc20.ts @@ -27,6 +27,7 @@ export function registerCairoERC20(server: McpServer): RegisteredTool { votes, appName, appVersion, + flashmint, access, upgradeable, info, @@ -44,6 +45,7 @@ export function registerCairoERC20(server: McpServer): RegisteredTool { votes, appName, appVersion, + flashmint, access, upgradeable, info, diff --git a/packages/ui/api/ai-assistant/function-definitions/cairo-alpha.ts b/packages/ui/api/ai-assistant/function-definitions/cairo-alpha.ts index d75ca185d..2ff13b9af 100644 --- a/packages/ui/api/ai-assistant/function-definitions/cairo-alpha.ts +++ b/packages/ui/api/ai-assistant/function-definitions/cairo-alpha.ts @@ -52,6 +52,34 @@ export const cairoAlphaERC20AIFunctionDefinition = { type: 'boolean', description: cairoERC20Descriptions.votes, }, + flashmint: { + type: 'object', + description: cairoERC20Descriptions.flashmint, + properties: { + enabled: { + type: 'boolean', + description: cairoERC20Descriptions.flashMintEnabled, + }, + maxAmount: { + type: 'string', + description: cairoERC20Descriptions.flashMintMaxAmount, + }, + feeMode: { + type: 'string', + enum: ['percent', 'custom'], + description: cairoERC20Descriptions.flashMintFeeMode, + }, + feePercent: { + type: 'string', + description: cairoERC20Descriptions.flashMintFeePercent, + }, + feeDestination: { + type: 'string', + enum: ['burn', 'fee_receiver'], + description: cairoERC20Descriptions.flashMintFeeDestination, + }, + }, + }, }, required: contractExactRequiredKeys<'cairoAlpha', 'ERC20'>()(['name', 'symbol']), additionalProperties: false, diff --git a/packages/ui/api/ai-assistant/function-definitions/cairo.ts b/packages/ui/api/ai-assistant/function-definitions/cairo.ts index 87223f53e..3e9844363 100644 --- a/packages/ui/api/ai-assistant/function-definitions/cairo.ts +++ b/packages/ui/api/ai-assistant/function-definitions/cairo.ts @@ -52,6 +52,34 @@ export const cairoERC20AIFunctionDefinition = { type: 'boolean', description: cairoERC20Descriptions.votes, }, + flashmint: { + type: 'object', + description: cairoERC20Descriptions.flashmint, + properties: { + enabled: { + type: 'boolean', + description: cairoERC20Descriptions.flashMintEnabled, + }, + maxAmount: { + type: 'string', + description: cairoERC20Descriptions.flashMintMaxAmount, + }, + feeMode: { + type: 'string', + enum: ['percent', 'custom'], + description: cairoERC20Descriptions.flashMintFeeMode, + }, + feePercent: { + type: 'string', + description: cairoERC20Descriptions.flashMintFeePercent, + }, + feeDestination: { + type: 'string', + enum: ['burn', 'fee_receiver'], + description: cairoERC20Descriptions.flashMintFeeDestination, + }, + }, + }, }, required: contractExactRequiredKeys<'cairo', 'ERC20'>()(['name', 'symbol']), additionalProperties: false, diff --git a/packages/ui/src/cairo/ERC20Controls.svelte b/packages/ui/src/cairo/ERC20Controls.svelte index adbc2d014..6722aefdf 100644 --- a/packages/ui/src/cairo/ERC20Controls.svelte +++ b/packages/ui/src/cairo/ERC20Controls.svelte @@ -1,8 +1,8 @@
@@ -126,6 +158,139 @@ + +
+ + Max Flash Loan + Maximum amount of tokens that can be flash-loaned in a single call. + +
+ + +
+
+ +
+ + Flash Fee + Fee charged for each flash loan. + +
+ + +
+
+ + {#if hasPositiveFlashFee} +
+ + Flash Fee Destination + Where the flash loan fee goes. + +
+ + +
+
+ {/if} +
+ import HelpTooltip from '../common/HelpTooltip.svelte'; - import type { KindedOptions, OptionsErrorMessages } from '@openzeppelin/wizard-cairo-alpha'; - import { premintPattern, erc20, infoDefaults, macrosDefaults } from '@openzeppelin/wizard-cairo-alpha'; + import type { FlashMintOptions, KindedOptions, OptionsErrorMessages } from '@openzeppelin/wizard-cairo-alpha'; + import { + premintPattern, + erc20, + flashMintDefaults, + infoDefaults, + macrosDefaults, + } from '@openzeppelin/wizard-cairo-alpha'; import AccessControlSection from './AccessControlSection.svelte'; import UpgradeabilityField from './UpgradeabilityField.svelte'; @@ -10,6 +16,7 @@ import MacrosSection from './MacrosSection.svelte'; import ExpandableCheckbox from '../common/ExpandableCheckbox.svelte'; import { error } from '../common/error-tooltip'; + import { resizeToFit } from '../common/resize-to-fit'; export let opts: Required = { kind: 'ERC20', @@ -18,11 +25,42 @@ info: { ...infoDefaults }, // create new object since Info is nested macros: { ...macrosDefaults }, // create new object since MacrosOptions is nested access: { ...erc20.defaults.access }, // create new object since Access is nested + flashmint: { ...flashMintDefaults }, // create new object since FlashMintOptions is nested }; export let errors: undefined | OptionsErrorMessages; $: requireAccessControl = erc20.isAccessControlRequired(opts); + $: hasPositiveFlashFee = opts.flashmint.enabled && computeHasPositiveFlashFee(opts.flashmint); + + function computeHasPositiveFlashFee(o: FlashMintOptions): boolean { + switch (o.feeMode) { + case 'percent': + return parseFloat(o.feePercent || '0') > 0; + case 'custom': + return true; + } + } + + // Stash the last custom value so toggling Max → Custom restores what the user previously typed. + let lastCustomMax: string = opts.flashmint.maxAmount === 'max' ? '0' : opts.flashmint.maxAmount; + $: if (opts.flashmint.maxAmount !== 'max') { + lastCustomMax = opts.flashmint.maxAmount; + } + + function selectMaxAmountMax() { + opts.flashmint.maxAmount = 'max'; + } + function selectMaxAmountCustom() { + opts.flashmint.maxAmount = lastCustomMax; + } + + // The fee is a percentage of the loan amount, so cap typed values at 100 instead of surfacing an error. + function clampFlashFeePercent(event: Event) { + if (parseFloat((event.currentTarget as HTMLInputElement).value) > 100) { + opts.flashmint.feePercent = '100'; + } + }
@@ -126,6 +164,139 @@ + +
+ + Max Flash Loan + Maximum amount of tokens that can be flash-loaned in a single call. + +
+ + +
+
+ +
+ + Flash Fee + Fee charged for each flash loan. + +
+ + +
+
+ + {#if hasPositiveFlashFee} +
+ + Flash Fee Destination + Where the flash loan fee goes. + +
+ + +
+
+ {/if} +
+ Date: Tue, 8 Sep 2026 13:23:44 -0400 Subject: [PATCH 2/5] Prepare Release (#868) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/erc20-flash-mint.md | 11 ----------- packages/cli/CHANGELOG.md | 9 +++++++++ packages/cli/package.json | 6 +++--- packages/common/CHANGELOG.md | 6 ++++++ packages/common/package.json | 4 ++-- packages/core/cairo/CHANGELOG.md | 6 ++++++ packages/core/cairo/package.json | 2 +- packages/mcp/CHANGELOG.md | 9 +++++++++ packages/mcp/package.json | 6 +++--- packages/ui/CHANGELOG.md | 6 ++++++ packages/ui/package.json | 2 +- 11 files changed, 46 insertions(+), 21 deletions(-) delete mode 100644 .changeset/erc20-flash-mint.md diff --git a/.changeset/erc20-flash-mint.md b/.changeset/erc20-flash-mint.md deleted file mode 100644 index e936078e5..000000000 --- a/.changeset/erc20-flash-mint.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@openzeppelin/wizard-cairo': minor -'@openzeppelin/wizard-common': patch -'@openzeppelin/contracts-mcp': patch -'@openzeppelin/contracts-cli': patch -'ui': patch ---- - -Cairo: Add ERC20FlashMint extension for the ERC20 token kind. -- Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). -- Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a4674bc5d..2bea84797 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog +## 0.2.1 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. +- Updated dependencies [[`1d70b62`](https://github.com/OpenZeppelin/contracts-wizard/commit/1d70b62b8c4bcc899cb6cf3b22bf701d487fa71a)]: + - @openzeppelin/wizard-cairo@4.1.0 + - @openzeppelin/wizard-common@0.5.9 + ## 0.2.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/cli/package.json b/packages/cli/package.json index 2c3d0f600..c4f58ffa5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/contracts-cli", - "version": "0.2.0", + "version": "0.2.1", "description": "CLI for generating smart contracts using OpenZeppelin Contracts Wizard", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -27,9 +27,9 @@ }, "dependencies": { "zod": "^4.0", - "@openzeppelin/wizard-common": "^0.5.8", + "@openzeppelin/wizard-common": "^0.5.9", "@openzeppelin/wizard": "^0.10.14", - "@openzeppelin/wizard-cairo": "^4.0.0", + "@openzeppelin/wizard-cairo": "^4.1.0", "@openzeppelin/wizard-stellar": "^0.6.4", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-confidential": "^0.1.2", diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 03b09b213..cd24d1eca 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## 0.5.9 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. + ## 0.5.8 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/common/package.json b/packages/common/package.json index 5ce1ad2d2..be1e6678f 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-common", - "version": "0.5.8", + "version": "0.5.9", "description": "Common library for OpenZeppelin Contracts Wizard components. Used internally.", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -35,7 +35,7 @@ }, "devDependencies": { "@openzeppelin/wizard": "^0.10.14", - "@openzeppelin/wizard-cairo": "^4.0.0", + "@openzeppelin/wizard-cairo": "^4.1.0", "@openzeppelin/wizard-stellar": "^0.6.4", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-confidential": "^0.1.2", diff --git a/packages/core/cairo/CHANGELOG.md b/packages/core/cairo/CHANGELOG.md index 50a49f099..7f71a3c97 100644 --- a/packages/core/cairo/CHANGELOG.md +++ b/packages/core/cairo/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## 4.1.0 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. + ## 4.0.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/core/cairo/package.json b/packages/core/cairo/package.json index 0576177d0..1bdc579ca 100644 --- a/packages/core/cairo/package.json +++ b/packages/core/cairo/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-cairo", - "version": "4.0.0", + "version": "4.1.0", "description": "A boilerplate generator to get started with OpenZeppelin Contracts for Cairo", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index ecbdc5ceb..b6caf064a 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog +## 0.7.1 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. +- Updated dependencies [[`1d70b62`](https://github.com/OpenZeppelin/contracts-wizard/commit/1d70b62b8c4bcc899cb6cf3b22bf701d487fa71a)]: + - @openzeppelin/wizard-cairo@4.1.0 + - @openzeppelin/wizard-common@0.5.9 + ## 0.7.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 91d014229..9e68ec560 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/contracts-mcp", - "version": "0.7.0", + "version": "0.7.1", "description": "OpenZeppelin Contracts MCP Server", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -27,11 +27,11 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", - "@openzeppelin/wizard-common": "^0.5.8", + "@openzeppelin/wizard-common": "^0.5.9", "@openzeppelin/wizard": "^0.10.14", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-stellar": "^0.6.4", - "@openzeppelin/wizard-cairo": "^4.0.0", + "@openzeppelin/wizard-cairo": "^4.1.0", "@openzeppelin/wizard-confidential": "^0.1.2", "@openzeppelin/wizard-uniswap-hooks": "^0.1.1" }, diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 86f686726..6c8e8c48d 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## 0.0.5 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. + ## 0.0.4 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/ui/package.json b/packages/ui/package.json index 2a088a23b..c0b4461ae 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "0.0.4", + "version": "0.0.5", "private": true, "scripts": { "prepare": "echo \"Nothing to prepare\"", From 5ca9bbd13725dc7baf74ca8d8ca8e1bdf5e0a94f Mon Sep 17 00:00:00 2001 From: Eric Lau Date: Tue, 8 Sep 2026 13:58:18 -0400 Subject: [PATCH 3/5] Revert "Prepare Release (#868)" (#870) --- .changeset/erc20-flash-mint.md | 11 +++++++++++ packages/cli/CHANGELOG.md | 9 --------- packages/cli/package.json | 6 +++--- packages/common/CHANGELOG.md | 6 ------ packages/common/package.json | 4 ++-- packages/core/cairo/CHANGELOG.md | 6 ------ packages/core/cairo/package.json | 2 +- packages/mcp/CHANGELOG.md | 9 --------- packages/mcp/package.json | 6 +++--- packages/ui/CHANGELOG.md | 6 ------ packages/ui/package.json | 2 +- 11 files changed, 21 insertions(+), 46 deletions(-) create mode 100644 .changeset/erc20-flash-mint.md diff --git a/.changeset/erc20-flash-mint.md b/.changeset/erc20-flash-mint.md new file mode 100644 index 000000000..e936078e5 --- /dev/null +++ b/.changeset/erc20-flash-mint.md @@ -0,0 +1,11 @@ +--- +'@openzeppelin/wizard-cairo': minor +'@openzeppelin/wizard-common': patch +'@openzeppelin/contracts-mcp': patch +'@openzeppelin/contracts-cli': patch +'ui': patch +--- + +Cairo: Add ERC20FlashMint extension for the ERC20 token kind. +- Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). +- Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 2bea84797..a4674bc5d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,15 +1,6 @@ # Changelog -## 0.2.1 (2026-09-08) - -- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) - - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). - - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. -- Updated dependencies [[`1d70b62`](https://github.com/OpenZeppelin/contracts-wizard/commit/1d70b62b8c4bcc899cb6cf3b22bf701d487fa71a)]: - - @openzeppelin/wizard-cairo@4.1.0 - - @openzeppelin/wizard-common@0.5.9 - ## 0.2.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/cli/package.json b/packages/cli/package.json index c4f58ffa5..2c3d0f600 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/contracts-cli", - "version": "0.2.1", + "version": "0.2.0", "description": "CLI for generating smart contracts using OpenZeppelin Contracts Wizard", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -27,9 +27,9 @@ }, "dependencies": { "zod": "^4.0", - "@openzeppelin/wizard-common": "^0.5.9", + "@openzeppelin/wizard-common": "^0.5.8", "@openzeppelin/wizard": "^0.10.14", - "@openzeppelin/wizard-cairo": "^4.1.0", + "@openzeppelin/wizard-cairo": "^4.0.0", "@openzeppelin/wizard-stellar": "^0.6.4", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-confidential": "^0.1.2", diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index cd24d1eca..03b09b213 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,12 +1,6 @@ # Changelog -## 0.5.9 (2026-09-08) - -- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) - - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). - - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. - ## 0.5.8 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/common/package.json b/packages/common/package.json index be1e6678f..5ce1ad2d2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-common", - "version": "0.5.9", + "version": "0.5.8", "description": "Common library for OpenZeppelin Contracts Wizard components. Used internally.", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -35,7 +35,7 @@ }, "devDependencies": { "@openzeppelin/wizard": "^0.10.14", - "@openzeppelin/wizard-cairo": "^4.1.0", + "@openzeppelin/wizard-cairo": "^4.0.0", "@openzeppelin/wizard-stellar": "^0.6.4", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-confidential": "^0.1.2", diff --git a/packages/core/cairo/CHANGELOG.md b/packages/core/cairo/CHANGELOG.md index 7f71a3c97..50a49f099 100644 --- a/packages/core/cairo/CHANGELOG.md +++ b/packages/core/cairo/CHANGELOG.md @@ -1,12 +1,6 @@ # Changelog -## 4.1.0 (2026-09-08) - -- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) - - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). - - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. - ## 4.0.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/core/cairo/package.json b/packages/core/cairo/package.json index 1bdc579ca..0576177d0 100644 --- a/packages/core/cairo/package.json +++ b/packages/core/cairo/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-cairo", - "version": "4.1.0", + "version": "4.0.0", "description": "A boilerplate generator to get started with OpenZeppelin Contracts for Cairo", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index b6caf064a..ecbdc5ceb 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,15 +1,6 @@ # Changelog -## 0.7.1 (2026-09-08) - -- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) - - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). - - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. -- Updated dependencies [[`1d70b62`](https://github.com/OpenZeppelin/contracts-wizard/commit/1d70b62b8c4bcc899cb6cf3b22bf701d487fa71a)]: - - @openzeppelin/wizard-cairo@4.1.0 - - @openzeppelin/wizard-common@0.5.9 - ## 0.7.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 9e68ec560..91d014229 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/contracts-mcp", - "version": "0.7.1", + "version": "0.7.0", "description": "OpenZeppelin Contracts MCP Server", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -27,11 +27,11 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", - "@openzeppelin/wizard-common": "^0.5.9", + "@openzeppelin/wizard-common": "^0.5.8", "@openzeppelin/wizard": "^0.10.14", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-stellar": "^0.6.4", - "@openzeppelin/wizard-cairo": "^4.1.0", + "@openzeppelin/wizard-cairo": "^4.0.0", "@openzeppelin/wizard-confidential": "^0.1.2", "@openzeppelin/wizard-uniswap-hooks": "^0.1.1" }, diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 6c8e8c48d..86f686726 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,12 +1,6 @@ # Changelog -## 0.0.5 (2026-09-08) - -- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) - - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). - - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. - ## 0.0.4 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/ui/package.json b/packages/ui/package.json index c0b4461ae..2a088a23b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "0.0.5", + "version": "0.0.4", "private": true, "scripts": { "prepare": "echo \"Nothing to prepare\"", From df1fa2ffb82bc7ee3652b165e7ef045b17b3664a Mon Sep 17 00:00:00 2001 From: Eric Lau Date: Tue, 8 Sep 2026 15:59:41 -0400 Subject: [PATCH 4/5] Improve performance of premint amount parsing (#872) Co-authored-by: Cursor --- .changeset/linear-premint-parse.md | 7 +++++++ packages/core/cairo/src/erc20.ts | 2 +- packages/core/cairo_alpha/CHANGELOG.md | 1 + packages/core/cairo_alpha/src/erc20.ts | 2 +- packages/core/solidity/src/erc20.test.ts | 8 +++++++- packages/core/solidity/src/erc20.ts | 20 ++++++++++++++++++-- packages/core/stellar/src/fungible.ts | 2 +- 7 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 .changeset/linear-premint-parse.md diff --git a/.changeset/linear-premint-parse.md b/.changeset/linear-premint-parse.md new file mode 100644 index 000000000..0f286a99a --- /dev/null +++ b/.changeset/linear-premint-parse.md @@ -0,0 +1,7 @@ +--- +'@openzeppelin/wizard-cairo': patch +'@openzeppelin/wizard-stellar': patch +'@openzeppelin/wizard': patch +--- + +Improve performance of premint amount parsing. diff --git a/packages/core/cairo/src/erc20.ts b/packages/core/cairo/src/erc20.ts index d0c232c82..cae4ea6a8 100644 --- a/packages/core/cairo/src/erc20.ts +++ b/packages/core/cairo/src/erc20.ts @@ -291,7 +291,7 @@ function addBurnable(c: ContractBuilder) { c.addFunction(externalTrait, functions.burn); } -export const premintPattern = /^(\d*\.?\d*)$/; +export const premintPattern = /^\d*(?:\.\d*)?$/; function addPremint(c: ContractBuilder, amount: string, decimals: bigint) { if (amount !== undefined && amount !== '0') { diff --git a/packages/core/cairo_alpha/CHANGELOG.md b/packages/core/cairo_alpha/CHANGELOG.md index e850917ce..1e4ca8a44 100644 --- a/packages/core/cairo_alpha/CHANGELOG.md +++ b/packages/core/cairo_alpha/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Add ERC20FlashMint extension ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) +- Improve performance of premint amount parsing ([#872](https://github.com/OpenZeppelin/contracts-wizard/pull/872)) ## 4.0.1 (2026-08-31) diff --git a/packages/core/cairo_alpha/src/erc20.ts b/packages/core/cairo_alpha/src/erc20.ts index d0c232c82..cae4ea6a8 100644 --- a/packages/core/cairo_alpha/src/erc20.ts +++ b/packages/core/cairo_alpha/src/erc20.ts @@ -291,7 +291,7 @@ function addBurnable(c: ContractBuilder) { c.addFunction(externalTrait, functions.burn); } -export const premintPattern = /^(\d*\.?\d*)$/; +export const premintPattern = /^\d*(?:\.\d*)?$/; function addPremint(c: ContractBuilder, amount: string, decimals: bigint) { if (amount !== undefined && amount !== '0') { diff --git a/packages/core/solidity/src/erc20.test.ts b/packages/core/solidity/src/erc20.test.ts index 99c533077..a462a966c 100644 --- a/packages/core/solidity/src/erc20.test.ts +++ b/packages/core/solidity/src/erc20.test.ts @@ -3,7 +3,7 @@ import type { OptionsError } from '.'; import { erc20 } from '.'; import type { ERC20Options } from './erc20'; -import { buildERC20 } from './erc20'; +import { buildERC20, calculatePremint } from './erc20'; import { printContract } from './print'; function testERC20(title: string, opts: Partial) { @@ -159,6 +159,12 @@ testPremint( testPremint('e notation', '1e59'); testPremint('e notation arithmetic overflow', '1e60', 'Amount would overflow uint256 after applying decimals'); +test('calculatePremint strips redundant zeros', t => { + t.deepEqual(calculatePremint('1.5000'), calculatePremint('1.5')); + t.deepEqual(calculatePremint('001.5'), calculatePremint('1.5')); + t.deepEqual(calculatePremint('0.0'), calculatePremint('0')); +}); + testERC20('erc20 mintable', { mintable: true, access: 'ownable', diff --git a/packages/core/solidity/src/erc20.ts b/packages/core/solidity/src/erc20.ts index b4b070826..d3324f534 100644 --- a/packages/core/solidity/src/erc20.ts +++ b/packages/core/solidity/src/erc20.ts @@ -219,6 +219,22 @@ export interface PremintCalculation { decimalPlace: number; } +function stripLeadingZeros(value: string): string { + let i = 0; + while (i < value.length && value[i] === '0') { + i++; + } + return value.slice(i); +} + +function stripTrailingZeros(value: string): string { + let i = value.length; + while (i > 0 && value[i - 1] === '0') { + i--; + } + return value.slice(0, i); +} + export function calculatePremint(amount: string): PremintCalculation | undefined { const m = amount.match(premintPattern); if (!m) { @@ -227,8 +243,8 @@ export function calculatePremint(amount: string): PremintCalculation | undefined }); } - const integer = m[1]?.replace(/^0+/, '') ?? ''; - const decimals = m[2]?.replace(/0+$/, '') ?? ''; + const integer = m[1] !== undefined ? stripLeadingZeros(m[1]) : ''; + const decimals = m[2] !== undefined ? stripTrailingZeros(m[2]) : ''; const exponent = Number(m[3] ?? 0); if (Number(integer + decimals) > 0) { diff --git a/packages/core/stellar/src/fungible.ts b/packages/core/stellar/src/fungible.ts index f6ddf3505..f4bc3b639 100644 --- a/packages/core/stellar/src/fungible.ts +++ b/packages/core/stellar/src/fungible.ts @@ -250,7 +250,7 @@ function addBurnable(c: ContractBuilder, votes: boolean, pausable: boolean, expl } } -export const premintPattern = /^(\d*\.?\d*)$/; +export const premintPattern = /^\d*(?:\.\d*)?$/; function addPremint(c: ContractBuilder, amount: string, decimals: bigint, votes: boolean) { if (amount !== undefined && amount !== '0') { From 28a81035e073d0aa9409febe71cabb576176801b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:48:42 -0400 Subject: [PATCH 5/5] Prepare Release (#871) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/erc20-flash-mint.md | 11 ----------- .changeset/linear-premint-parse.md | 7 ------- packages/cli/CHANGELOG.md | 11 +++++++++++ packages/cli/package.json | 10 +++++----- packages/common/CHANGELOG.md | 6 ++++++ packages/common/package.json | 8 ++++---- packages/core/cairo/CHANGELOG.md | 8 ++++++++ packages/core/cairo/package.json | 2 +- packages/core/solidity/CHANGELOG.md | 4 ++++ packages/core/solidity/package.json | 2 +- packages/core/stellar/CHANGELOG.md | 4 ++++ packages/core/stellar/package.json | 2 +- packages/mcp/CHANGELOG.md | 11 +++++++++++ packages/mcp/package.json | 10 +++++----- packages/ui/CHANGELOG.md | 6 ++++++ packages/ui/package.json | 2 +- 16 files changed, 68 insertions(+), 36 deletions(-) delete mode 100644 .changeset/erc20-flash-mint.md delete mode 100644 .changeset/linear-premint-parse.md diff --git a/.changeset/erc20-flash-mint.md b/.changeset/erc20-flash-mint.md deleted file mode 100644 index e936078e5..000000000 --- a/.changeset/erc20-flash-mint.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@openzeppelin/wizard-cairo': minor -'@openzeppelin/wizard-common': patch -'@openzeppelin/contracts-mcp': patch -'@openzeppelin/contracts-cli': patch -'ui': patch ---- - -Cairo: Add ERC20FlashMint extension for the ERC20 token kind. -- Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). -- Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. diff --git a/.changeset/linear-premint-parse.md b/.changeset/linear-premint-parse.md deleted file mode 100644 index 0f286a99a..000000000 --- a/.changeset/linear-premint-parse.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@openzeppelin/wizard-cairo': patch -'@openzeppelin/wizard-stellar': patch -'@openzeppelin/wizard': patch ---- - -Improve performance of premint amount parsing. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a4674bc5d..786390b88 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog +## 0.2.1 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#870](https://github.com/OpenZeppelin/contracts-wizard/pull/870)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. +- Updated dependencies [[`5ca9bbd`](https://github.com/OpenZeppelin/contracts-wizard/commit/5ca9bbd13725dc7baf74ca8d8ca8e1bdf5e0a94f), [`df1fa2f`](https://github.com/OpenZeppelin/contracts-wizard/commit/df1fa2ffb82bc7ee3652b165e7ef045b17b3664a)]: + - @openzeppelin/wizard-cairo@4.1.0 + - @openzeppelin/wizard-common@0.5.9 + - @openzeppelin/wizard-stellar@0.6.5 + - @openzeppelin/wizard@0.10.16 + ## 0.2.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/cli/package.json b/packages/cli/package.json index 2c3d0f600..33616344e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/contracts-cli", - "version": "0.2.0", + "version": "0.2.1", "description": "CLI for generating smart contracts using OpenZeppelin Contracts Wizard", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -27,10 +27,10 @@ }, "dependencies": { "zod": "^4.0", - "@openzeppelin/wizard-common": "^0.5.8", - "@openzeppelin/wizard": "^0.10.14", - "@openzeppelin/wizard-cairo": "^4.0.0", - "@openzeppelin/wizard-stellar": "^0.6.4", + "@openzeppelin/wizard-common": "^0.5.9", + "@openzeppelin/wizard": "^0.10.16", + "@openzeppelin/wizard-cairo": "^4.1.0", + "@openzeppelin/wizard-stellar": "^0.6.5", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-confidential": "^0.1.2", "@openzeppelin/wizard-uniswap-hooks": "^0.1.1" diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 03b09b213..4cd0e0118 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## 0.5.9 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#870](https://github.com/OpenZeppelin/contracts-wizard/pull/870)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. + ## 0.5.8 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/common/package.json b/packages/common/package.json index 5ce1ad2d2..4f3c516d2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-common", - "version": "0.5.8", + "version": "0.5.9", "description": "Common library for OpenZeppelin Contracts Wizard components. Used internally.", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -34,9 +34,9 @@ "zod": "^4.0" }, "devDependencies": { - "@openzeppelin/wizard": "^0.10.14", - "@openzeppelin/wizard-cairo": "^4.0.0", - "@openzeppelin/wizard-stellar": "^0.6.4", + "@openzeppelin/wizard": "^0.10.16", + "@openzeppelin/wizard-cairo": "^4.1.0", + "@openzeppelin/wizard-stellar": "^0.6.5", "@openzeppelin/wizard-stylus": "^0.3.0", "@openzeppelin/wizard-confidential": "^0.1.2", "@openzeppelin/wizard-uniswap-hooks": "^0.1.1", diff --git a/packages/core/cairo/CHANGELOG.md b/packages/core/cairo/CHANGELOG.md index 50a49f099..03459fa81 100644 --- a/packages/core/cairo/CHANGELOG.md +++ b/packages/core/cairo/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog +## 4.1.0 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#870](https://github.com/OpenZeppelin/contracts-wizard/pull/870)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. + +- Improve performance of premint amount parsing. ([#872](https://github.com/OpenZeppelin/contracts-wizard/pull/872)) + ## 4.0.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/core/cairo/package.json b/packages/core/cairo/package.json index 0576177d0..1bdc579ca 100644 --- a/packages/core/cairo/package.json +++ b/packages/core/cairo/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-cairo", - "version": "4.0.0", + "version": "4.1.0", "description": "A boilerplate generator to get started with OpenZeppelin Contracts for Cairo", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", diff --git a/packages/core/solidity/CHANGELOG.md b/packages/core/solidity/CHANGELOG.md index 7609fa9c0..1369f2802 100644 --- a/packages/core/solidity/CHANGELOG.md +++ b/packages/core/solidity/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +## 0.10.16 (2026-09-08) + +- Improve performance of premint amount parsing. ([#872](https://github.com/OpenZeppelin/contracts-wizard/pull/872)) + ## 0.10.15 (2026-09-03) - Update `@openzeppelin/tron-contracts` and `@openzeppelin/tron-contracts-upgradeable` dependencies to 5.6.0. ([#865](https://github.com/OpenZeppelin/contracts-wizard/pull/865)) diff --git a/packages/core/solidity/package.json b/packages/core/solidity/package.json index 24eee9912..ee5039efb 100644 --- a/packages/core/solidity/package.json +++ b/packages/core/solidity/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard", - "version": "0.10.15", + "version": "0.10.16", "description": "A boilerplate generator to get started with OpenZeppelin Contracts", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", diff --git a/packages/core/stellar/CHANGELOG.md b/packages/core/stellar/CHANGELOG.md index aab55819e..1ada67934 100644 --- a/packages/core/stellar/CHANGELOG.md +++ b/packages/core/stellar/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +## 0.6.5 (2026-09-08) + +- Improve performance of premint amount parsing. ([#872](https://github.com/OpenZeppelin/contracts-wizard/pull/872)) + ## 0.6.4 (2026-08-17) - Add Stellar `Account` contract type, a smart account combining Delegated, Ed25519 and WebAuthn (passkey) signers with an optional simple or weighted threshold policy, and support for execution entry point and upgradeable options. ([#826](https://github.com/OpenZeppelin/contracts-wizard/pull/826)) diff --git a/packages/core/stellar/package.json b/packages/core/stellar/package.json index e3e153088..d8e2bce80 100644 --- a/packages/core/stellar/package.json +++ b/packages/core/stellar/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-stellar", - "version": "0.6.4", + "version": "0.6.5", "description": "A boilerplate generator to get started with OpenZeppelin Stellar Soroban Contracts", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index ecbdc5ceb..e24b35695 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog +## 0.7.1 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#870](https://github.com/OpenZeppelin/contracts-wizard/pull/870)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. +- Updated dependencies [[`5ca9bbd`](https://github.com/OpenZeppelin/contracts-wizard/commit/5ca9bbd13725dc7baf74ca8d8ca8e1bdf5e0a94f), [`df1fa2f`](https://github.com/OpenZeppelin/contracts-wizard/commit/df1fa2ffb82bc7ee3652b165e7ef045b17b3664a)]: + - @openzeppelin/wizard-cairo@4.1.0 + - @openzeppelin/wizard-common@0.5.9 + - @openzeppelin/wizard-stellar@0.6.5 + - @openzeppelin/wizard@0.10.16 + ## 0.7.0 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 91d014229..c0b02ef85 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/contracts-mcp", - "version": "0.7.0", + "version": "0.7.1", "description": "OpenZeppelin Contracts MCP Server", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", @@ -27,11 +27,11 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", - "@openzeppelin/wizard-common": "^0.5.8", - "@openzeppelin/wizard": "^0.10.14", + "@openzeppelin/wizard-common": "^0.5.9", + "@openzeppelin/wizard": "^0.10.16", "@openzeppelin/wizard-stylus": "^0.3.0", - "@openzeppelin/wizard-stellar": "^0.6.4", - "@openzeppelin/wizard-cairo": "^4.0.0", + "@openzeppelin/wizard-stellar": "^0.6.5", + "@openzeppelin/wizard-cairo": "^4.1.0", "@openzeppelin/wizard-confidential": "^0.1.2", "@openzeppelin/wizard-uniswap-hooks": "^0.1.1" }, diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 86f686726..a8b630e83 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## 0.0.5 (2026-09-08) + +- Cairo: Add ERC20FlashMint extension for the ERC20 token kind. ([#870](https://github.com/OpenZeppelin/contracts-wizard/pull/870)) + - Configurable max flash loan (default or custom cap), flash fee (percent of the loan amount or custom stub), and fee destination (burn or fee receiver). + - Add `flashmint` options to the `cairo-erc20` MCP tool and CLI command. + ## 0.0.4 (2026-09-01) - Cairo: support OpenZeppelin Contracts for Cairo v4.0.1. ([#861](https://github.com/OpenZeppelin/contracts-wizard/pull/861)) diff --git a/packages/ui/package.json b/packages/ui/package.json index 2a088a23b..c0b4461ae 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "0.0.4", + "version": "0.0.5", "private": true, "scripts": { "prepare": "echo \"Nothing to prepare\"",