From 0f2157c306c0099ba5515e088f36e4546f662282 Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 6 May 2026 16:12:31 +0200 Subject: [PATCH 1/9] Cairo: Add ERC20FlashMint extension --- .changeset/erc20-flash-mint.md | 5 + packages/common/src/ai/descriptions/cairo.ts | 9 + packages/core/cairo_alpha/CHANGELOG.md | 1 + packages/core/cairo_alpha/src/contract.ts | 25 + packages/core/cairo_alpha/src/erc20.ts | 165 ++++ .../core/cairo_alpha/src/generate/erc20.ts | 5 + packages/core/cairo_alpha/src/print.ts | 9 +- .../with_components_off/erc20/erc20.test.ts | 96 +++ .../erc20/erc20.test.ts.md | 774 ++++++++++++++++++ .../erc20/erc20.test.ts.snap | Bin 4169 -> 5570 bytes .../with_components_on/erc20/erc20.test.ts | 96 +++ .../with_components_on/erc20/erc20.test.ts.md | 487 +++++++++++ .../erc20/erc20.test.ts.snap | Bin 2888 -> 3742 bytes .../function-definitions/cairo-alpha.ts | 22 + .../ui/src/cairo_alpha/ERC20Controls.svelte | 140 ++++ 15 files changed, 1832 insertions(+), 2 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..0358aa104 --- /dev/null +++ b/.changeset/erc20-flash-mint.md @@ -0,0 +1,5 @@ +--- +'@openzeppelin/wizard-common': patch +--- + +Cairo: Add ERC20FlashMint extension and AI descriptions for ERC20 token kind. diff --git a/packages/common/src/ai/descriptions/cairo.ts b/packages/common/src/ai/descriptions/cairo.ts index b2c18ab42..929b950e0 100644 --- a/packages/common/src/ai/descriptions/cairo.ts +++ b/packages/common/src/ai/descriptions/cairo.ts @@ -55,6 +55,15 @@ export const cairoERC20Descriptions = { wrapper: 'Whether to include ERC20Wrapper functionality for depositing and withdrawing an underlying token.', 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.", + flashmint: 'Whether to include ERC20FlashMint functionality, allowing flash loans of tokens compliant with ERC-3156.', + flashMintMaxAmount: + '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.', + flashMintFeeMode: + "Mode for the flash loan fee. 'percent' charges a percentage of the loaned amount (value provided via flashMintFeePercent). 'custom' emits a TODO stub for the caller to implement.", + flashMintFeePercent: + '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 flashMintFeeMode is \'percent\'. Defaults to 0 (no fee).', + flashMintFeeDestination: + "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.", }; export const cairoERC721Descriptions = { diff --git a/packages/core/cairo_alpha/CHANGELOG.md b/packages/core/cairo_alpha/CHANGELOG.md index 1c59ef218..d663347bc 100644 --- a/packages/core/cairo_alpha/CHANGELOG.md +++ b/packages/core/cairo_alpha/CHANGELOG.md @@ -11,6 +11,7 @@ - Add ERC721URIStorage extension ([#772](https://github.com/OpenZeppelin/contracts-wizard/pull/772)) - Add ERC721Wrapper extension ([#764](https://github.com/OpenZeppelin/contracts-wizard/pull/764)) - Add ERC20Wrapper extension ([#763](https://github.com/OpenZeppelin/contracts-wizard/pull/763)) +- Add ERC20FlashMint extension - **Breaking changes**: - Use OpenZeppelin Contracts for Cairo v4.0.0-alpha.0. 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 8a02a534f..01896377c 100644 --- a/packages/core/cairo_alpha/src/erc20.ts +++ b/packages/core/cairo_alpha/src/erc20.ts @@ -30,6 +30,11 @@ 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: false, + flashMintMaxAmount: 'max', + flashMintFeeMode: 'percent', + flashMintFeePercent: '0', + flashMintFeeDestination: 'burn', access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, info: commonDefaults.info, @@ -52,8 +57,16 @@ export interface ERC20Options extends CommonContractOptions { votes?: boolean; appName?: string; appVersion?: string; + flashmint?: boolean; + flashMintMaxAmount?: string; + flashMintFeeMode?: FlashMintFeeMode; + flashMintFeePercent?: string; + flashMintFeeDestination?: FlashMintFeeDestination; } +export type FlashMintFeeMode = 'percent' | 'custom'; +export type FlashMintFeeDestination = 'burn' | 'fee_receiver'; + function withDefaults(opts: ERC20Options): Required { return { ...opts, @@ -67,6 +80,11 @@ 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, + flashMintMaxAmount: opts.flashMintMaxAmount ?? defaults.flashMintMaxAmount, + flashMintFeeMode: opts.flashMintFeeMode ?? defaults.flashMintFeeMode, + flashMintFeePercent: opts.flashMintFeePercent ?? defaults.flashMintFeePercent, + flashMintFeeDestination: opts.flashMintFeeDestination ?? defaults.flashMintFeeDestination, }; } @@ -102,6 +120,10 @@ export function buildERC20(opts: ERC20Options): Contract { addWrapper(c); } + if (allOpts.flashmint) { + addFlashMint(c, allOpts, decimals); + } + addHooks(c, allOpts); setAccessControl(c, allOpts.access); @@ -294,6 +316,131 @@ 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 BigInt(getInitialSupply(value, Number(decimals))); +} + +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) + return { numerator, denominator: 100n * decimalScale }; +} + +function buildFlashFeeOverrideBody(allOpts: Required): string[] | null { + switch (allOpts.flashMintFeeMode) { + case 'percent': { + const parsed = parseFlashMintFeePercent(allOpts.flashMintFeePercent); + 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 = allOpts.flashMintFeeMode; + throw new Error(`Unknown flashMintFeeMode: ${_}`); + } + } +} + +function addFlashMint(c: ContractBuilder, allOpts: Required, decimals: bigint) { + c.addComponent(components.ERC20FlashMintComponent, [], true); + + const customMax = parseFlashMintMaxAmount(allOpts.flashMintMaxAmount, decimals); + const overridesMax = customMax !== null; + const overridesReceiver = allOpts.flashMintFeeDestination === 'fee_receiver'; + const feeOverrideBody = buildFlashFeeOverrideBody(allOpts); + 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'); + 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: [], + }); + fn.code.push('if token != get_contract_address() {', ' return 0;', '}', `${customMax!}`); + } + + 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 +483,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..83624607a 100644 --- a/packages/core/cairo_alpha/src/generate/erc20.ts +++ b/packages/core/cairo_alpha/src/generate/erc20.ts @@ -29,6 +29,11 @@ function prepareBlueprint(opts: GeneratorOptions) { votes: booleans, appName: ['MyApp'], appVersion: ['v1'], + flashmint: booleans, + flashMintMaxAmount: ['max', '1000000'], + flashMintFeeMode: ['percent', 'custom'] as const, + flashMintFeePercent: ['5', '0.5'], + flashMintFeeDestination: ['burn', 'fee_receiver'] as const, access: resolveAccessControlOptions(opts.access), upgradeable: resolveUpgradeableOptionsSubset(opts.upgradeable), info: infoOptions, 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/tests/with_components_off/erc20/erc20.test.ts b/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts index f723379c3..273280271 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 @@ -76,6 +76,102 @@ testERC20('erc20 wrapper', { wrapper: true, }); +testERC20('erc20 flash mint', { + flashmint: true, +}); + +testERC20('erc20 flash mint with custom max', { + flashmint: true, + flashMintMaxAmount: '1000000', +}); + +testERC20('erc20 flash mint with percent fee', { + flashmint: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '5', +}); + +testERC20('erc20 flash mint with fractional percent fee', { + flashmint: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '0.0013725', +}); + +testERC20('erc20 flash mint with percent fee and fee receiver', { + flashmint: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '5', + flashMintFeeDestination: 'fee_receiver', +}); + +testERC20('erc20 flash mint with custom fee impl', { + flashmint: true, + flashMintFeeMode: 'custom', +}); + +testERC20('erc20 flash mint with all custom config', { + flashmint: true, + flashMintMaxAmount: '1000000', + flashMintFeeMode: 'percent', + flashMintFeePercent: '5', + flashMintFeeDestination: 'fee_receiver', +}); + +testERC20('erc20 flash mint with max amount of zero', { + flashmint: true, + flashMintMaxAmount: '0', +}); + +test('erc20 flash mint, max amount empty string', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: true, + flashMintMaxAmount: '', + }), + ); + 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: true, + flashMintMaxAmount: '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: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '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: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '100.5', + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + 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 dcf6bb2a3..d4bdef503 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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, 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.erc20_flash_mint.initializer();␊ + 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;␊ + }␊ + 1000000000000000000000000␊ + }␊ + }␊ + ␊ + //␊ + // 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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, 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.erc20_flash_mint.initializer();␊ + 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;␊ + }␊ + 1000000000000000000000000␊ + }␊ + ␊ + 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.0-alpha.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, 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.erc20_flash_mint.initializer();␊ + 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;␊ + }␊ + 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 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 fd269e1680adfefb8186c9a9f109b61a8cefe239..f456aff4a59cad0b23c88e23d2732abd98272604 100644 GIT binary patch literal 5570 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 Q%mZ`(56ZB#=#k3-00SSRz5oCK literal 4169 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 TATYP^z})`<_k@y*Z)5=gNpc4V diff --git a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts index c84a85015..3bf553664 100644 --- a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts +++ b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts @@ -71,6 +71,102 @@ testERC20('erc20 wrapper', { wrapper: true, }); +testERC20('erc20 flash mint', { + flashmint: true, +}); + +testERC20('erc20 flash mint with custom max', { + flashmint: true, + flashMintMaxAmount: '1000000', +}); + +testERC20('erc20 flash mint with percent fee', { + flashmint: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '5', +}); + +testERC20('erc20 flash mint with fractional percent fee', { + flashmint: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '0.0013725', +}); + +testERC20('erc20 flash mint with percent fee and fee receiver', { + flashmint: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '5', + flashMintFeeDestination: 'fee_receiver', +}); + +testERC20('erc20 flash mint with custom fee impl', { + flashmint: true, + flashMintFeeMode: 'custom', +}); + +testERC20('erc20 flash mint with all custom config', { + flashmint: true, + flashMintMaxAmount: '1000000', + flashMintFeeMode: 'percent', + flashMintFeePercent: '5', + flashMintFeeDestination: 'fee_receiver', +}); + +testERC20('erc20 flash mint with max amount of zero', { + flashmint: true, + flashMintMaxAmount: '0', +}); + +test('erc20 flash mint, max amount empty string', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + flashmint: true, + flashMintMaxAmount: '', + }), + ); + 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: true, + flashMintMaxAmount: '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: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '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: true, + flashMintFeeMode: 'percent', + flashMintFeePercent: '100.5', + }), + ); + t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); +}); + 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 3041c00d1..f5d5ce470 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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, 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.erc20_flash_mint.initializer();␊ + 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;␊ + }␊ + 1000000000000000000000000␊ + }␊ + }␊ + ␊ + //␊ + // 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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.erc20_flash_mint.initializer();␊ + 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.0-alpha.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, 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.erc20_flash_mint.initializer();␊ + 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;␊ + }␊ + 1000000000000000000000000␊ + }␊ + ␊ + 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.0-alpha.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, 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.erc20_flash_mint.initializer();␊ + 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;␊ + }␊ + 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 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 abc75e9e759c98ed236708b8aaf673328affd6ab..886895fb40300264559437ef0eaef2965045f3ba 100644 GIT binary patch literal 3742 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 II{gy?0CQ+Cp8x;= literal 2888 zcmV-O3%B$^RzVN5NP9!eg#!{2S5ELBaOcJmapA%NE~r1Q&XNs$c!y>#EmZw@*~PdCtE2_U{N+xJur9aDtiAM8s?s@;O9xx@H-t zCTbH^$gjT6XnG4(uNwL} zq3Ak;5JDCsNH++5>y;{V`=%s0t(K(gJ<`UO%1b(JE2juEH0YnD?~T(DRBieZdy%_>h3jkedDKU>eRibn{LWBMWJh1le7^dYFk1= zF{_@|9N9!|uRCg5SE#}jtSWDbnO*kuyPQMI)y7YkS9hEoH<_D^H?Yn_P&+idU;9hAHxkUg>quHag#7f@+K3Lq)JHO&ZI}BFTT^_>ynBY`wX+Q zT%fFCt7Q_#ray|~rF+T_%l9sYO6X2BlbMo3`X#l8XqQHBKZAlCy4yY>k5gDM)x19Q zyDl`m!gmJfI+V*%wcbYIf!d|jP_x<;d1Jf3awxf!oA$7i&oDcaP%s3;`srygW{j9z z)~R|WJ1x$JbQ7cwty2E)tXN(s*G)G%QKv)DiDlC0b?)I?0iezw0zl0UpmOMxaJh+9 zm6$RH&`LXKtsI})GZ1qka}z84bx5}cF=d4lP3SqVgMEbZ|2OQ+p<1sT6Yowo=p0Wk z_Dt`ukPEC@L_${K`F12Uxteff^p3>rswW@LL*@&QC< zc8$=kt7K<|@$PiPs+eQl$3`EIsm^8l8fj%CkTtZ_Onv=zYBh|*p=YcOx6}lha0?Pz zKTQ2BI<*@5MFCytN_{uF@a8)rUvSrVQ&$P=r4E^&3)JB_GX?74KsP!p9@Wb=x%P6u zETJ}0dEt4%o%zHD^$)Ylp%gX%yV;Hoe_Si>#b*@K*jaDSR*KCgVa#ciuI5m^R(gI5 z=7Jq5lH`6jU1^fESN^nkwBL}6d)3N;Q`tTQCdtevNN*X$l%(rWujRzvS}2+%44iNi zgd3rjbD|aolpB^M%n4UTBjgq)Q7+IRP|eNDpeghO!>YBHiF@HG;DKQ=qJW2g({lSv_)Bv=^+Jr4lx7 z4r@!PvOUPS1b_!UjFxC=iHp6l)5k)k_qr{OL*i;pav@hw*lO0{yaUY5ToRvJEFZ|ApO z1m1>ALe9GcRf}I%sz+5>6xgU059*(mYhB=9$p2tFKz~~p15yv99!UKlQhzCKy^q`9 z`A31j|MoHL@0==MPQ$i)*z*Z{K7pgni-&Qs_IwUgM>w~Vx{ausOK>htY#79hI9m84 zf!5>`c(l;@`oe9%A{{hL8oLSiLs-6dsk@j^$~k5_7Httzy;A6D@1i_88sy}Vy-6+P z?9>O&a8DCwb?@PjNi-X0S`kqu{w5&pUm?l_U~4KNFUAa+jo}w1+n$Blpr$#rnUDGa z^o;@fOac1rRzr*(K)-oKzgfGs0HkiWd*=fer7m=C?a<8x(XbkECiB6c1-$wL z;8ibPMIaFX(ZT>xr#m7Zr+4?t;HT!LO0~FOmyq?sel`w}H@1*9HYtmKr1|jo0>*un zK;PfYA7lb211L;l2r0`S@VLuqKaD-FDdP|j+aU#1B&{JI;9Oh6In7kg zwT;YKZg;M2o_S03*%II`2|u0VAGz<#U*eD4KY3rEFHfHUeVH76S#%u_v$`(~P%i1jqQH0^wWfy3Wk2F1Nv(w2U8u=Y9UKKl%VvS^1(b^}0 zT`Vf*S@o5?S3EqF4~o_Dvrr|&LN17D`_g0OTx>oY)WpquLmN?#!m#Y zu>)iS$Oe!NAR9n7f)9|5xg#6CoX^X(dgbK-Fb^^HSPy+sovwuh_EL~aa|%O5&4E~}35&G~bZDw{2tiO=2x>b@ zP}^ZDH7Ccr({39K^v(r_!bQhUK(QvmyOv-gwBJpiiI9@l3M9_ZfOi{!%>g#YptS{T z4zM{Ar%13lg6;*I18fekIbcUXI(Gzo7;UG;hSGZPH-gFbtM`)r>gFULKkf6zXaC&k zT@B5Igyvc-Llwmxq#gWL>xD3JCl!V)zSQ|OdIO9fcr()(['name', 'symbol']), additionalProperties: false, diff --git a/packages/ui/src/cairo_alpha/ERC20Controls.svelte b/packages/ui/src/cairo_alpha/ERC20Controls.svelte index 3f4737344..81bae116a 100644 --- a/packages/ui/src/cairo_alpha/ERC20Controls.svelte +++ b/packages/ui/src/cairo_alpha/ERC20Controls.svelte @@ -10,6 +10,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', @@ -23,6 +24,29 @@ export let errors: undefined | OptionsErrorMessages; $: requireAccessControl = erc20.isAccessControlRequired(opts); + $: hasPositiveFlashFee = computeHasPositiveFlashFee(opts); + + function computeHasPositiveFlashFee(o: typeof opts): boolean { + switch (o.flashMintFeeMode) { + case 'percent': + return parseFloat(o.flashMintFeePercent || '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.flashMintMaxAmount === 'max' ? '0' : opts.flashMintMaxAmount; + $: if (opts.flashMintMaxAmount !== 'max') { + lastCustomMax = opts.flashMintMaxAmount; + } + + function selectMaxAmountMax() { + opts.flashMintMaxAmount = 'max'; + } + function selectMaxAmountCustom() { + opts.flashMintMaxAmount = lastCustomMax; + }
@@ -126,6 +150,122 @@ + +
+ + Max Flash Loan + + Maximum amount of tokens that can be flash-loaned in a single call. Max inherits the default (the + maximum representable amount minus the current total supply). Custom lets you set a non-negative + cap; setting it to 0 effectively disables flash loans. + + +
+ + +
+
+ +
+ + Flash Fee + + Fee charged for each flash loan. Choose Percent to charge a percentage of the loan amount, or + Custom to emit a TODO stub you can fill in manually. + + +
+ + +
+
+ + {#if hasPositiveFlashFee} +
+ + Flash Fee Destination + + Where the flash loan fee goes. Burn it (default) or send it to a fee receiver address set at deploy time. + + +
+ + +
+
+ {/if} +
+ Date: Wed, 6 May 2026 16:30:03 +0200 Subject: [PATCH 2/9] Update PR link in changelog --- packages/core/cairo_alpha/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/cairo_alpha/CHANGELOG.md b/packages/core/cairo_alpha/CHANGELOG.md index d663347bc..3bc2122ee 100644 --- a/packages/core/cairo_alpha/CHANGELOG.md +++ b/packages/core/cairo_alpha/CHANGELOG.md @@ -11,7 +11,7 @@ - Add ERC721URIStorage extension ([#772](https://github.com/OpenZeppelin/contracts-wizard/pull/772)) - Add ERC721Wrapper extension ([#764](https://github.com/OpenZeppelin/contracts-wizard/pull/764)) - Add ERC20Wrapper extension ([#763](https://github.com/OpenZeppelin/contracts-wizard/pull/763)) -- Add ERC20FlashMint extension +- Add ERC20FlashMint extension ([#801](https://github.com/OpenZeppelin/contracts-wizard/pull/801)) - **Breaking changes**: - Use OpenZeppelin Contracts for Cairo v4.0.0-alpha.0. From 111094f474e3b4d8d016276691849bcd58ae1a42 Mon Sep 17 00:00:00 2001 From: immrsd Date: Thu, 7 May 2026 14:54:21 +0200 Subject: [PATCH 3/9] Cairo: Bundle ERC20 flashmint options --- packages/common/src/ai/descriptions/cairo.ts | 9 +- packages/core/cairo_alpha/src/erc20.ts | 58 +++++++------ .../core/cairo_alpha/src/generate/erc20.ts | 28 +++++-- packages/core/cairo_alpha/src/index.ts | 3 +- .../with_components_off/erc20/erc20.test.ts | 58 ++++++------- .../with_components_on/erc20/erc20.test.ts | 58 ++++++------- .../function-definitions/cairo-alpha.ts | 44 +++++----- .../ui/src/cairo_alpha/ERC20Controls.svelte | 82 +++++++++++-------- 8 files changed, 183 insertions(+), 157 deletions(-) diff --git a/packages/common/src/ai/descriptions/cairo.ts b/packages/common/src/ai/descriptions/cairo.ts index 929b950e0..facbe2ffe 100644 --- a/packages/common/src/ai/descriptions/cairo.ts +++ b/packages/common/src/ai/descriptions/cairo.ts @@ -55,13 +55,16 @@ export const cairoERC20Descriptions = { wrapper: 'Whether to include ERC20Wrapper functionality for depositing and withdrawing an underlying token.', 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.", - flashmint: 'Whether to include ERC20FlashMint functionality, allowing flash loans of tokens compliant with ERC-3156.', + flashmint: + 'Configuration object for the ERC20FlashMint extension (ERC-3156 flash loans). The extension is included only when `enabled` is true; the other fields tune the loan limit, fee, and fee destination.', + flashMintEnabled: + 'Whether to include ERC20FlashMint functionality, allowing flash loans of tokens compliant with ERC-3156.', flashMintMaxAmount: '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.', flashMintFeeMode: - "Mode for the flash loan fee. 'percent' charges a percentage of the loaned amount (value provided via flashMintFeePercent). 'custom' emits a TODO stub for the caller to implement.", + "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.", flashMintFeePercent: - '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 flashMintFeeMode is \'percent\'. Defaults to 0 (no fee).', + '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).', flashMintFeeDestination: "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.", }; diff --git a/packages/core/cairo_alpha/src/erc20.ts b/packages/core/cairo_alpha/src/erc20.ts index 01896377c..f87215dd3 100644 --- a/packages/core/cairo_alpha/src/erc20.ts +++ b/packages/core/cairo_alpha/src/erc20.ts @@ -18,6 +18,25 @@ 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 const defaults: Required = { name: 'MyToken', symbol: 'MTK', @@ -30,11 +49,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: false, - flashMintMaxAmount: 'max', - flashMintFeeMode: 'percent', - flashMintFeePercent: '0', - flashMintFeeDestination: 'burn', + flashmint: flashMintDefaults, access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, info: commonDefaults.info, @@ -57,16 +72,9 @@ export interface ERC20Options extends CommonContractOptions { votes?: boolean; appName?: string; appVersion?: string; - flashmint?: boolean; - flashMintMaxAmount?: string; - flashMintFeeMode?: FlashMintFeeMode; - flashMintFeePercent?: string; - flashMintFeeDestination?: FlashMintFeeDestination; + flashmint?: FlashMintOptions; } -export type FlashMintFeeMode = 'percent' | 'custom'; -export type FlashMintFeeDestination = 'burn' | 'fee_receiver'; - function withDefaults(opts: ERC20Options): Required { return { ...opts, @@ -81,10 +89,6 @@ function withDefaults(opts: ERC20Options): Required { appName: opts.appName ?? defaults.appName, appVersion: opts.appVersion ?? defaults.appVersion, flashmint: opts.flashmint ?? defaults.flashmint, - flashMintMaxAmount: opts.flashMintMaxAmount ?? defaults.flashMintMaxAmount, - flashMintFeeMode: opts.flashMintFeeMode ?? defaults.flashMintFeeMode, - flashMintFeePercent: opts.flashMintFeePercent ?? defaults.flashMintFeePercent, - flashMintFeeDestination: opts.flashMintFeeDestination ?? defaults.flashMintFeeDestination, }; } @@ -120,8 +124,8 @@ export function buildERC20(opts: ERC20Options): Contract { addWrapper(c); } - if (allOpts.flashmint) { - addFlashMint(c, allOpts, decimals); + if (allOpts.flashmint.enabled) { + addFlashMint(c, allOpts.flashmint, decimals); } addHooks(c, allOpts); @@ -349,10 +353,10 @@ function parseFlashMintFeePercent(value: string): { numerator: bigint; denominat return { numerator, denominator: 100n * decimalScale }; } -function buildFlashFeeOverrideBody(allOpts: Required): string[] | null { - switch (allOpts.flashMintFeeMode) { +function buildFlashFeeOverrideBody(opts: FlashMintOptions): string[] | null { + switch (opts.feeMode) { case 'percent': { - const parsed = parseFlashMintFeePercent(allOpts.flashMintFeePercent); + const parsed = parseFlashMintFeePercent(opts.feePercent); if (parsed === null) { return null; } @@ -361,19 +365,19 @@ function buildFlashFeeOverrideBody(allOpts: Required): string[] | case 'custom': return ['// TODO: Must be implemented according to the desired flash fee logic', '0']; default: { - const _: never = allOpts.flashMintFeeMode; + const _: never = opts.feeMode; throw new Error(`Unknown flashMintFeeMode: ${_}`); } } } -function addFlashMint(c: ContractBuilder, allOpts: Required, decimals: bigint) { +function addFlashMint(c: ContractBuilder, opts: FlashMintOptions, decimals: bigint) { c.addComponent(components.ERC20FlashMintComponent, [], true); - const customMax = parseFlashMintMaxAmount(allOpts.flashMintMaxAmount, decimals); + const customMax = parseFlashMintMaxAmount(opts.maxAmount, decimals); const overridesMax = customMax !== null; - const overridesReceiver = allOpts.flashMintFeeDestination === 'fee_receiver'; - const feeOverrideBody = buildFlashFeeOverrideBody(allOpts); + const overridesReceiver = opts.feeDestination === 'fee_receiver'; + const feeOverrideBody = buildFlashFeeOverrideBody(opts); const overridesFee = feeOverrideBody !== null; if (!overridesMax && !overridesFee && !overridesReceiver) { diff --git a/packages/core/cairo_alpha/src/generate/erc20.ts b/packages/core/cairo_alpha/src/generate/erc20.ts index 83624607a..25387da5f 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, FlashMintOptions } from '../erc20'; +import { flashMintDefaults } from '../erc20'; import type { AccessSubset } from '../set-access-control'; import { resolveAccessControlOptions } from '../set-access-control'; import { infoOptions } from '../set-info'; @@ -10,6 +11,25 @@ import { resolveMacrosOptions } from '../set-macros'; const booleans = [true, false]; +const flashMintAlternatives: FlashMintOptions[] = [ + flashMintDefaults, + { ...flashMintDefaults, enabled: true }, + { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '0.5', + feeDestination: 'fee_receiver', + }, + { + enabled: true, + maxAmount: 'max', + feeMode: 'custom', + feePercent: '0', + feeDestination: 'fee_receiver', + }, +]; + type GeneratorOptions = { access: AccessSubset; upgradeable: UpgradeableSubset; @@ -29,11 +49,7 @@ function prepareBlueprint(opts: GeneratorOptions) { votes: booleans, appName: ['MyApp'], appVersion: ['v1'], - flashmint: booleans, - flashMintMaxAmount: ['max', '1000000'], - flashMintFeeMode: ['percent', 'custom'] as const, - flashMintFeePercent: ['5', '0.5'], - flashMintFeeDestination: ['burn', 'fee_receiver'] as const, + flashmint: flashMintAlternatives, access: resolveAccessControlOptions(opts.access), upgradeable: resolveUpgradeableOptionsSubset(opts.upgradeable), info: infoOptions, 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/tests/with_components_off/erc20/erc20.test.ts b/packages/core/cairo_alpha/src/tests/with_components_off/erc20/erc20.test.ts index 273280271..e46141ed8 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'; @@ -77,49 +77,47 @@ testERC20('erc20 wrapper', { }); testERC20('erc20 flash mint', { - flashmint: true, + flashmint: { ...flashMintDefaults, enabled: true }, }); testERC20('erc20 flash mint with custom max', { - flashmint: true, - flashMintMaxAmount: '1000000', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '1000000' }, }); testERC20('erc20 flash mint with percent fee', { - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '5', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '5' }, }); testERC20('erc20 flash mint with fractional percent fee', { - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '0.0013725', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '0.0013725' }, }); testERC20('erc20 flash mint with percent fee and fee receiver', { - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '5', - flashMintFeeDestination: 'fee_receiver', + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, }); testERC20('erc20 flash mint with custom fee impl', { - flashmint: true, - flashMintFeeMode: 'custom', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'custom' }, }); testERC20('erc20 flash mint with all custom config', { - flashmint: true, - flashMintMaxAmount: '1000000', - flashMintFeeMode: 'percent', - flashMintFeePercent: '5', - flashMintFeeDestination: 'fee_receiver', + flashmint: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, }); testERC20('erc20 flash mint with max amount of zero', { - flashmint: true, - flashMintMaxAmount: '0', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '0' }, }); test('erc20 flash mint, max amount empty string', async t => { @@ -127,8 +125,7 @@ test('erc20 flash mint, max amount empty string', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintMaxAmount: '', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '' }, }), ); t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); @@ -139,8 +136,7 @@ test('erc20 flash mint, invalid max amount', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintMaxAmount: 'abc', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: 'abc' }, }), ); t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); @@ -151,9 +147,7 @@ test('erc20 flash mint, invalid percent fee', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: 'abc', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: 'abc' }, }), ); t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); @@ -164,9 +158,7 @@ test('erc20 flash mint, percent fee out of range', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '100.5', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '100.5' }, }), ); t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); diff --git a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts index 3bf553664..0e66cabf7 100644 --- a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts +++ b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts @@ -1,7 +1,7 @@ import test from 'ava'; import type { ERC20Options } from '../../../erc20'; -import { buildERC20, getInitialSupply, defaults } from '../../../erc20'; +import { buildERC20, defaults, flashMintDefaults, getInitialSupply } from '../../../erc20'; import { printContract } from '../../../print'; import { AccessControl, darDefaultOpts, darCustomOpts } from '../../../set-access-control'; @@ -72,49 +72,47 @@ testERC20('erc20 wrapper', { }); testERC20('erc20 flash mint', { - flashmint: true, + flashmint: { ...flashMintDefaults, enabled: true }, }); testERC20('erc20 flash mint with custom max', { - flashmint: true, - flashMintMaxAmount: '1000000', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '1000000' }, }); testERC20('erc20 flash mint with percent fee', { - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '5', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '5' }, }); testERC20('erc20 flash mint with fractional percent fee', { - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '0.0013725', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '0.0013725' }, }); testERC20('erc20 flash mint with percent fee and fee receiver', { - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '5', - flashMintFeeDestination: 'fee_receiver', + flashmint: { + ...flashMintDefaults, + enabled: true, + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, }); testERC20('erc20 flash mint with custom fee impl', { - flashmint: true, - flashMintFeeMode: 'custom', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'custom' }, }); testERC20('erc20 flash mint with all custom config', { - flashmint: true, - flashMintMaxAmount: '1000000', - flashMintFeeMode: 'percent', - flashMintFeePercent: '5', - flashMintFeeDestination: 'fee_receiver', + flashmint: { + enabled: true, + maxAmount: '1000000', + feeMode: 'percent', + feePercent: '5', + feeDestination: 'fee_receiver', + }, }); testERC20('erc20 flash mint with max amount of zero', { - flashmint: true, - flashMintMaxAmount: '0', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '0' }, }); test('erc20 flash mint, max amount empty string', async t => { @@ -122,8 +120,7 @@ test('erc20 flash mint, max amount empty string', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintMaxAmount: '', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: '' }, }), ); t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); @@ -134,8 +131,7 @@ test('erc20 flash mint, invalid max amount', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintMaxAmount: 'abc', + flashmint: { ...flashMintDefaults, enabled: true, maxAmount: 'abc' }, }), ); t.is((error as OptionsError).messages.flashMintMaxAmount, 'Must be "max" or a non-negative number'); @@ -146,9 +142,7 @@ test('erc20 flash mint, invalid percent fee', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: 'abc', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: 'abc' }, }), ); t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); @@ -159,9 +153,7 @@ test('erc20 flash mint, percent fee out of range', async t => { buildERC20({ name: 'MyToken', symbol: 'MTK', - flashmint: true, - flashMintFeeMode: 'percent', - flashMintFeePercent: '100.5', + flashmint: { ...flashMintDefaults, enabled: true, feeMode: 'percent', feePercent: '100.5' }, }), ); t.is((error as OptionsError).messages.flashMintFeePercent, 'Must be a number between 0 and 100'); 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 ca64fb550..38dc32ab8 100644 --- a/packages/ui/api/ai-assistant/function-definitions/cairo-alpha.ts +++ b/packages/ui/api/ai-assistant/function-definitions/cairo-alpha.ts @@ -53,26 +53,32 @@ export const cairoAlphaERC20AIFunctionDefinition = { description: cairoERC20Descriptions.votes, }, flashmint: { - type: 'boolean', + type: 'object', description: cairoERC20Descriptions.flashmint, - }, - flashMintMaxAmount: { - type: 'string', - description: cairoERC20Descriptions.flashMintMaxAmount, - }, - flashMintFeeMode: { - type: 'string', - enum: ['percent', 'custom'], - description: cairoERC20Descriptions.flashMintFeeMode, - }, - flashMintFeePercent: { - type: 'string', - description: cairoERC20Descriptions.flashMintFeePercent, - }, - flashMintFeeDestination: { - type: 'string', - enum: ['burn', 'fee_receiver'], - description: cairoERC20Descriptions.flashMintFeeDestination, + 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']), diff --git a/packages/ui/src/cairo_alpha/ERC20Controls.svelte b/packages/ui/src/cairo_alpha/ERC20Controls.svelte index 81bae116a..dac56ea07 100644 --- a/packages/ui/src/cairo_alpha/ERC20Controls.svelte +++ b/packages/ui/src/cairo_alpha/ERC20Controls.svelte @@ -1,8 +1,14 @@ @@ -152,7 +159,7 @@ @@ -161,37 +168,37 @@ Max Flash Loan Maximum amount of tokens that can be flash-loaned in a single call. Max inherits the default (the - maximum representable amount minus the current total supply). Custom lets you set a non-negative - cap; setting it to 0 effectively disables flash loans. + maximum representable amount minus the current total supply). Custom lets you set a non-negative cap; + setting it to 0 effectively disables flash loans.
-
- J zx@z8a)#_VsH$!~|cGVcvRPe-VN)M)Na}IAxulsSqRQsmS3-Jg4JNPdIO1fK5%qKf?J?3C!{aCC=_A5 z9L>@ma6E?nK?CQ_Kl%E3WE+1*rlsSQ0Seb|Wox;lyYdU-SbXu=-FUo6NT)~G(A5|( z@22z35p~QbPd$4B_`=Y(ZEn@#2CffPEx$OaYUHVOaZPL90B{Ia>kGhbgw#WFo39nV zR^^n2H&2c&{wsmbdJ~v7S#CO?(oK7fVAN33FT{ ze-ilO@4+=v1v&`i4ICyFb(jEo5pg{FfCoiPJ)BCk!r-b7;j@FNIj6M$Sx|g`gj3q# z9RRALjwuM^Go8Rmd9wK|tj8s7^f1+6^XUkn&k>-H*Lo!f{f-6nI{@?p^s6NLO*=Fk zfYjw&7HC<(u0xc}?w#LjE#v^RH)k6lWnnDBW` z!Z8`6KE^bA+V_mC63aR3Dba;J&Uz|~A{*>*rOKS8H0T*h{%eRbouQO|V_29rd8$!% zS~XDmprt`&3BxvMq6<7d(D_QUMF#qv^POl=Uk4%SiAxq9_CUP$1g*nUR&V`(1-jaVd8*rDBpKcc&S>2Pr6dzfA{mTM< zxiN{YX>&_o=AB5C{QPR55mMv?bcaV3`;rmwf=**-fr&dCUF@y?KOV6j|k_~X4bxg z?SoTEa;a{wHYI6iv%R(2>=1@|-&TBeLeTA@EI|8 z9_gkpYSG1nz?|4L*98jl-x6+!5GcrCWMg(XLkiJ=CiJEWlThK8SJj|_K?9dX1Lv9h zT@CV;Fw&W47y~Gf8~+v*$lq?1Pl0S<&lN1zIoBy?nKxhlq{49sNy+ zPo|iP30a)S+^NcnM$Kss5H)8EN_>xKmP2Mti}%;yeYCe7&gsZ>q85|+HKNu9Ee&*N zqI3vNae0T|{e#eRbUE`o9&$lO`NOQBd>)-x38SnoT*f`mp}k@I2pb+LSOpW{rY|GQ z8+aUWU%-V6H6z=*t8N?g_0GkH!co^wK!qm4Yo1^tXs=bzM3~;{;A$Hh{lYGT%>gzC z*c@PUfXz|5h6I}b_uU#i zh&3?iwqc*|6>S?X-M-@Duxpnv!F%O1u8?g4egy4bu7Gdgi~wf@I3ud!jOZ7VF9A1Y z@lK4Y$l$K1LeNX_UsTM0G3_9q_xidt1`76h^Q(fF<4ZTe%Q12dSsfp6nnYPnlK_7Z_=CV71pXlK2c6gc56&F{ II{gy?0CQ+Cp8x;= From f69d97647ce522135fbfa420f255332374bf5616 Mon Sep 17 00:00:00 2001 From: immrsd Date: Fri, 8 May 2026 09:46:57 +0200 Subject: [PATCH 5/9] Decrease the number of ERC20FlashMint generator options --- packages/core/cairo_alpha/src/generate/erc20.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/core/cairo_alpha/src/generate/erc20.ts b/packages/core/cairo_alpha/src/generate/erc20.ts index 25387da5f..1f4867aaf 100644 --- a/packages/core/cairo_alpha/src/generate/erc20.ts +++ b/packages/core/cairo_alpha/src/generate/erc20.ts @@ -21,13 +21,6 @@ const flashMintAlternatives: FlashMintOptions[] = [ feePercent: '0.5', feeDestination: 'fee_receiver', }, - { - enabled: true, - maxAmount: 'max', - feeMode: 'custom', - feePercent: '0', - feeDestination: 'fee_receiver', - }, ]; type GeneratorOptions = { From a81713c4b06a8441b7ad5677d4517d23d4402817 Mon Sep 17 00:00:00 2001 From: immrsd Date: Fri, 8 May 2026 10:26:08 +0200 Subject: [PATCH 6/9] Cairo: Validate flashmint inputs against u256 --- packages/core/cairo_alpha/src/erc20.ts | 10 ++++-- .../with_components_off/erc20/erc20.test.ts | 34 +++++++++++++++++++ .../with_components_on/erc20/erc20.test.ts | 34 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/packages/core/cairo_alpha/src/erc20.ts b/packages/core/cairo_alpha/src/erc20.ts index 09e368fec..8d2711cb3 100644 --- a/packages/core/cairo_alpha/src/erc20.ts +++ b/packages/core/cairo_alpha/src/erc20.ts @@ -327,7 +327,7 @@ function parseFlashMintMaxAmount(value: string, decimals: bigint): bigint | null if (value === '' || !premintPattern.test(value)) { throw new OptionsError({ flashMintMaxAmount: 'Must be "max" or a non-negative number' }); } - return BigInt(getInitialSupply(value, Number(decimals))); + return toUint(getInitialSupply(value, Number(decimals)), 'flashMintMaxAmount', 'u256'); } function parseFlashMintFeePercent(value: string): { numerator: bigint; denominator: bigint } | null { @@ -349,8 +349,12 @@ function parseFlashMintFeePercent(value: string): { numerator: bigint; denominat 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) - return { numerator, denominator: 100n * decimalScale }; + // 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 { 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 e46141ed8..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 @@ -164,6 +164,40 @@ test('erc20 flash mint, percent fee out of range', async t => { 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 b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts index 0e66cabf7..ecd63988e 100644 --- a/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts +++ b/packages/core/cairo_alpha/src/tests/with_components_on/erc20/erc20.test.ts @@ -159,6 +159,40 @@ test('erc20 flash mint, percent fee out of range', async t => { 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', }); From 8d0dfffd2eab2d3b5b4104ccf751cb69b49142bc Mon Sep 17 00:00:00 2001 From: immrsd Date: Fri, 8 May 2026 10:27:53 +0200 Subject: [PATCH 7/9] Cairo: Clamp flashmint max loan to mint headroom --- packages/core/cairo_alpha/src/erc20.ts | 12 +++++++++++- .../erc20/erc20.test.ts.md | 16 ++++++++++++---- .../erc20/erc20.test.ts.snap | Bin 5575 -> 5676 bytes .../with_components_on/erc20/erc20.test.ts.md | 16 ++++++++++++---- .../erc20/erc20.test.ts.snap | Bin 3700 -> 3812 bytes 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/core/cairo_alpha/src/erc20.ts b/packages/core/cairo_alpha/src/erc20.ts index 8d2711cb3..49235b5ae 100644 --- a/packages/core/cairo_alpha/src/erc20.ts +++ b/packages/core/cairo_alpha/src/erc20.ts @@ -404,6 +404,7 @@ function addFlashMint(c: ContractBuilder, opts: FlashMintOptions, decimals: bigi if (overridesMax) { c.addUseClause('starknet', 'get_contract_address'); + c.addUseClause('core::num::traits', 'Bounded'); const fn = c.addFunction(flashMintConfigTrait, { name: 'max_flash_loan', args: [ @@ -414,7 +415,16 @@ function addFlashMint(c: ContractBuilder, opts: FlashMintOptions, decimals: bigi returns: 'u256', code: [], }); - fn.code.push('if token != get_contract_address() {', ' return 0;', '}', `${customMax!}`); + // 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) { 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 2a89dd4b6..63b0dbfda 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 @@ -988,6 +988,7 @@ Generated by [AVA](https://avajs.dev). ␊ #[starknet::contract]␊ mod MyToken {␊ + use core::num::traits::Bounded;␊ use openzeppelin_access::ownable::OwnableComponent;␊ use openzeppelin_interfaces::upgrades::IUpgradeable;␊ use openzeppelin_token::erc20::{␊ @@ -1055,7 +1056,9 @@ Generated by [AVA](https://avajs.dev). if token != get_contract_address() {␊ return 0;␊ }␊ - 1000000000000000000000000␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ }␊ }␊ ␊ @@ -1461,7 +1464,7 @@ Generated by [AVA](https://avajs.dev). ␊ #[starknet::contract]␊ mod MyToken {␊ - use core::num::traits::Zero;␊ + use core::num::traits::{Bounded, Zero};␊ use openzeppelin_access::ownable::OwnableComponent;␊ use openzeppelin_interfaces::upgrades::IUpgradeable;␊ use openzeppelin_token::erc20::{␊ @@ -1538,7 +1541,9 @@ Generated by [AVA](https://avajs.dev). if token != get_contract_address() {␊ return 0;␊ }␊ - 1000000000000000000000000␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ }␊ ␊ fn flash_fee(␊ @@ -1577,6 +1582,7 @@ Generated by [AVA](https://avajs.dev). ␊ #[starknet::contract]␊ mod MyToken {␊ + use core::num::traits::Bounded;␊ use openzeppelin_access::ownable::OwnableComponent;␊ use openzeppelin_interfaces::upgrades::IUpgradeable;␊ use openzeppelin_token::erc20::{␊ @@ -1644,7 +1650,9 @@ Generated by [AVA](https://avajs.dev). if token != get_contract_address() {␊ return 0;␊ }␊ - 0␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 0;␊ + if cap < headroom { cap } else { headroom }␊ }␊ }␊ ␊ 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 cf8906a952d4e99e5507a177cf5541978708c3fa..2a8f47d6e9e8208b32b757c50d7360f0161d24a5 100644 GIT binary patch literal 5676 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 VLtqX9b5jq@{XbSau&*h~0RRzNxF-Mr 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 5a53bbbe2..66b501d24 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 @@ -645,6 +645,7 @@ Generated by [AVA](https://avajs.dev). #[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};␊ @@ -675,7 +676,9 @@ Generated by [AVA](https://avajs.dev). if token != get_contract_address() {␊ return 0;␊ }␊ - 1000000000000000000000000␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ }␊ }␊ ␊ @@ -939,7 +942,7 @@ Generated by [AVA](https://avajs.dev). #[starknet::contract]␊ #[with_components(ERC20, ERC20FlashMint, Upgradeable, Ownable)]␊ mod MyToken {␊ - use core::num::traits::Zero;␊ + 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};␊ @@ -980,7 +983,9 @@ Generated by [AVA](https://avajs.dev). if token != get_contract_address() {␊ return 0;␊ }␊ - 1000000000000000000000000␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 1000000000000000000000000;␊ + if cap < headroom { cap } else { headroom }␊ }␊ ␊ fn flash_fee(␊ @@ -1020,6 +1025,7 @@ Generated by [AVA](https://avajs.dev). #[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};␊ @@ -1050,7 +1056,9 @@ Generated by [AVA](https://avajs.dev). if token != get_contract_address() {␊ return 0;␊ }␊ - 0␊ + let headroom = Bounded::::MAX - total_supply;␊ + let cap: u256 = 0;␊ + if cap < headroom { cap } else { headroom }␊ }␊ }␊ ␊ 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 64f6b4d80b8612ca7433ffc68a5021e3f9b2f515..ec9168dc49ac51b697e6508e1375450e172a973b 100644 GIT binary patch literal 3812 zcmV?stV zJ<50TF&~Qv00000000B+UCnPKM->kto85R7lsyK5RJ$wWSr~s!cAcdiCk(bj26nv0 z_GXi;qNdzkV>j-uYIRrpV=W_b;l>duHxw=$kdU}?g8zU!H;#x47Y=Yi)nC)q-P1F+ z=c9Z4=3-Az)vNE{`@PpyufFQ+nM^x$zeK+y+~6iTd1nv1hK7ivH5TiLGCJqk`wrF# z-ZP2eQf#{ijJv=5O4l*RpS_B{{>sf)#OG@_Ub*w?omUqY(9YKSr*lsXjZl}&wRA$c z(J_dlpmu9_W?=!XG0Vo>*fR+_HuwR0Y7_c7v29`+6sY)tF^7N+@@08{2D*#Rp%^SWHLl(_MB* zC_0-#2qDiUh}nd`3`r~kE88@xqi5Y^P)ARXsrYBG$z@PZoDSBAt0>`OauubO5V$hxSs97%w)kw##IE}> zb`R=tXEk-*A+9_ASzIhV?Chlc@KK_KbfSr@lsaO^R7}xSk6wNS#W`5eJ}Hl1SiIE2 zF^h&SF};$%^y%wFF2Ac;2PG$Js#g=u>QEHU?cSY3$+4=5X{R1w_dr4M6ioI{rp4HG ziNjS!%~REHaV^m|aq5JYivF+3kp9&cxb-P(Tjg07hRKqiyWD`51lchJ?}oxmiAp z-U=%p1GEI&V@2x$z`EkRG3xA4cJwtNKA8gWePsCF9cy3j$FQ9@*nSEh$P007k93$r zRL|BiCp8Iby4{d2ApX~vn37+YgmfHcWqPchDn~PHLkKQThb`ubL_(W%e@VlsNjQoC zM0G`t&}z`hzJ>{ZNyDm~V?M&h2#+b_s(429S`M-%RL$7+uV1S{91fYeHkhi3dm<=E zYW*Pf^XS-W7!?I{A(i@Sbm8P}nJ)zT-C-tiv(#bc=Mr^zF*640;6pdsYChZ9*jBfn zKG{%EhnT$as1dAuB0&9a?Oq~<0N^0ov#lR(H`j|ZN@+ya%d?fHrV-cm8^z2z+Sy)v zbP48yeJP3({M7jBsnQt&3TAQuiR`ZFvzVW2_nS#9KhiDZ&UVgZ^xFYan z3F-J0nDT0$*;5(n91>8qvJ_g?u_I-dCU8B!fqkg9FY`(iIU@mRGSK8n&}4riR*x!Q zt=iX?F6VdKs44UJ`cP)gb2+nmG3ML9mKgIdZUbW;2V=fA+;!=_#^aE@HUxQffxQBI z1@;Q;^}4awEFxqUe#^2v9>9-}=d3e0Ql#^C8ypq#j6pKT>}z zZ@n+t-}y(0zyJ0Y?C%^aU#`Nod)V^{dp?1qO^b)|dhPifq>gZIrE(ilt$^ShJJ_~~ zlXJB2Ly6Wbmhfny|L1wofa`S7ux#umISxtr;iGghiIh{!bUdmP$2>LYepV@ujt2QT z)NoNtIlJ|NGu-3ESv$blj)~m^WEqs_KrNA=_LH(g?MI>wZG-YD0owxc$&FLNn(_xw z6$9AIh>g2vnocXqElZXVKTH=TrJiDhpR_FtfPA^v^(c+V0fL5a=`DPcMd+Iy389~t z@C^5r5L#mnQ55P~isGByuA=;q3C5t0cIW4~V{=44ijuks0c=YPwtj~tMnTL>$$OYL zLaFrqeTf!635kZuFs)^vPeDKES=a?!ioqf*_7!r*v8lSAZJVcAkKx=0{-YF@8=XKj zy1SaY-fC&^N51otdk#g5GSHuAT#^R_>keZUT16o?Da!kz5+5i^yZI@ald8Ou$**BM zXoq+$y4d5hl5UTNZS;P(>@)wtIU=S@&{_BSc}CxWioSx1zMHS2g)K!WM;(&$9`5yJ zNk`AVK4m((()t%PEojO{snK`QBeWn?80ef~>YRGM&gqES z8;oMJkNRm!>Zbwf2ht~Wa`e_NbB@WwV`8jdel=v%i(}&2bC}$zwZZW@I+XU;uHuXA^e|hd0oCH6C3LKLP z?4nSF@oKb6d%*Ds4h9Wew)o_$+@YQLDN?*~$^ckvce53rH(d24ahTW$HXBa?1sU`R zn}!}k;B30w98t$&J=C&400p?R1)AHmxPxm~YL;J}R5S9lw7fqxZvr?3>$L|!SESTe zq$}Ske4i>ew&!xR7XTr#2<1da zSksuJ8+0FWhWG(NI&lq$=pm2_+%(y~p~-*^ruI;E*KEG-!loJNFU_ByIf3rBCGE8~ zjzD)4wy)2^_wIEhEbU$!DXXUGdTtHMN&y4B3K|s*a4^7Ov(+`(Y*lrN2p$z+r5RRQ z%V(t)K8fZF>mUKiIXl%iC0hC+>{Oo?Itt_w+%e^G#{hYf;XC>an>@je^irr~7KV6r zh)~Bw&$%o9Z<6BuE8GjBJlSFu*5aDhd)Sz8UUmr3=Sa{eTD_Qq ze#a8}9RT_P`c)GBCe7Xfq%P+&c_%=rBgREc`0(AGPBg>9X3ov3ul-rVt3Lo{JVDBEZeZdYzn3uX%Myl_ zns_*3;A8-WeW7pyL1{z#_7Wy~^7Z2oFe=~N4hn&YtV0F#lec9=PuwGw^Pca261e|s z9PjzU>nB>9yTzA3B~py})+p(+j$vP>n!R&;N?tkU+&GmO!X7tHl|_*acEwX=Zgd*- zCMW+jM44`KlCt)>p-H-1DZ8xdD1F`2pt7WC8+6eHau0ON({z!6ez%P$IuzK!g?n#I zceUpbhJo&Czm;;@kK>4V#W)1q&|U#5Qq_yw;daWBbDFW-PI+O@a=F_nr=*x8{5+%Py8>j?OiLp@$xJbDy7bye70)|z@ zunrTuqH_E{s}g~lFJXwlO-41rT0RN(bxnLv)P=4umgIQcFx_sORn27Dp|ACq-+iyK zYgokP*s|Be<31vsQ=7AW2ipgyisDkkUTP@H&Sq;e?n;d8*HSg((TK*!-+FxmoHo1AQv#Jme=nRW*_(Gxr3?OA&z>)xIy<- zj~d*qiIycO!r*#202&k_O-48SgDj&_G|=)>c_1maNX5h~#{-vAxrJx(UofPCe8^UzE|ggutAD!*56w6Ah-W5DUg5ME}sI~!k#Nxtk+zpoMqm8 z`HKphCUMFdTrC>IP$y2JfT2 z?Ql&;rW0jc70-xXle9F@p|R2-G{Nm1{_qb%&(Y<~?|8}u1?5k(g8F%MY&DFEx^Nx$ zJcss%?IUb@q+l0JfSbOIEN>72(R~3IuGEwq@2gzC*c`AUpqe`Z?&jNRg`u?G z`i*3={pzi;og>|9KHDii4w^{OJx5yRD!=*b{TpV`o5+-=Baz+)hO~Q|$|H~Ed z4V)3+i~wguRh$w1Lh>cxrYzoxVO1I26;%j&3I26@^XCR z4tP0+?&atiZfittk7@)D1!u_xoF!F_Wm*E~$`GQP6;}hiErXdm7Y)`{J+I4fqPY#v zJCljyc44o|L}ST;cLlsF;9aSlcO^fhUFkh6SF1L7S*j915B4odRvM^ zli&G^@Yyr1NsoWkVkGu|!4{&@$X~6LTjt_8rfa9Z-9a&b#2izu6mfIx2 a9|Zm&@CSiE2>e0kGyexKTp}=o6afI9&sxa< literal 3700 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 S1pc6lx&H$)**Z^^5&-}LcNqx) From 7bac6cebe3846bdb223f8cfd1d896917d239f26e Mon Sep 17 00:00:00 2001 From: immrsd Date: Fri, 8 May 2026 14:56:04 +0200 Subject: [PATCH 8/9] Split Cairo-alpha generated contracts for ERC20, ERC6909 and Custom kinds by upgradeable option --- .../workflows/compile-cairo-alpha-project.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/compile-cairo-alpha-project.yml b/.github/workflows/compile-cairo-alpha-project.yml index 76752d3d5..ea5eaa443 100644 --- a/.github/workflows/compile-cairo-alpha-project.yml +++ b/.github/workflows/compile-cairo-alpha-project.yml @@ -80,16 +80,18 @@ jobs: elif [[ "$kind" == "ERC20" || "$kind" == "ERC6909" || "$kind" == "Custom" ]]; 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 + 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 "Compiling $proj_name..." - scarb clean - scarb build + echo "Compiling $proj_name..." + scarb clean + scarb build - echo "✅ Compiled $proj_name!" - echo "---------------------------------" + echo "✅ Compiled $proj_name!" + echo "---------------------------------" + done done else From 15a26db68bb84ec8f6d75e56153efdd8c26ca6ec Mon Sep 17 00:00:00 2001 From: immrsd Date: Mon, 11 May 2026 11:50:41 +0200 Subject: [PATCH 9/9] Split ERC20 test_project validation workflow by flashmint options to keep number of generated contracts per run equal to 128 --- .../workflows/compile-cairo-alpha-project.yml | 21 ++++++++- packages/core/cairo_alpha/src/erc20.ts | 41 ++++++++++++++++ .../core/cairo_alpha/src/generate/erc20.ts | 19 ++------ .../core/cairo_alpha/src/generate/sources.ts | 47 +++++++++++++++---- .../src/scripts/update-scarb-project.ts | 27 ++++++++++- packages/core/cairo_alpha/src/test.ts | 8 +++- 6 files changed, 135 insertions(+), 28 deletions(-) diff --git a/.github/workflows/compile-cairo-alpha-project.yml b/.github/workflows/compile-cairo-alpha-project.yml index ea5eaa443..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,7 +79,25 @@ jobs: done done - elif [[ "$kind" == "ERC20" || "$kind" == "ERC6909" || "$kind" == "Custom" ]]; then + elif [[ "$kind" == "ERC20" ]]; then + for access_option in "${all_access_options[@]}"; do + 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 + + 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" diff --git a/packages/core/cairo_alpha/src/erc20.ts b/packages/core/cairo_alpha/src/erc20.ts index 49235b5ae..cd3769e26 100644 --- a/packages/core/cairo_alpha/src/erc20.ts +++ b/packages/core/cairo_alpha/src/erc20.ts @@ -37,6 +37,47 @@ export const flashMintDefaults: FlashMintOptions = { 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', diff --git a/packages/core/cairo_alpha/src/generate/erc20.ts b/packages/core/cairo_alpha/src/generate/erc20.ts index 1f4867aaf..0b7d6b428 100644 --- a/packages/core/cairo_alpha/src/generate/erc20.ts +++ b/packages/core/cairo_alpha/src/generate/erc20.ts @@ -1,5 +1,5 @@ -import type { ERC20Options, FlashMintOptions } from '../erc20'; -import { flashMintDefaults } 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'; @@ -11,21 +11,10 @@ import { resolveMacrosOptions } from '../set-macros'; const booleans = [true, false]; -const flashMintAlternatives: FlashMintOptions[] = [ - flashMintDefaults, - { ...flashMintDefaults, enabled: true }, - { - enabled: true, - maxAmount: '1000000', - feeMode: 'percent', - feePercent: '0.5', - feeDestination: 'fee_receiver', - }, -]; - type GeneratorOptions = { access: AccessSubset; upgradeable: UpgradeableSubset; + flashmint: FlashMintSubset; macros: MacrosSubset; }; @@ -42,7 +31,7 @@ function prepareBlueprint(opts: GeneratorOptions) { votes: booleans, appName: ['MyApp'], appVersion: ['v1'], - flashmint: flashMintAlternatives, + 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/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) {