From 452b4bab950717cf8b1d28788cf87962add8f960 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 27 Feb 2025 12:20:34 +0100 Subject: [PATCH] Add Erc20FlashMint (#451) --- .gitignore | 3 +- packages/core/stylus/src/contract.ts | 8 +- packages/core/stylus/src/erc20.test.ts | 30 ++++ packages/core/stylus/src/erc20.test.ts.md | 165 +++++++++++++++++++- packages/core/stylus/src/erc20.test.ts.snap | Bin 890 -> 1192 bytes packages/core/stylus/src/erc20.ts | 100 ++++++++++-- packages/core/stylus/src/generate/erc20.ts | 3 +- packages/core/stylus/src/print.ts | 11 +- packages/ui/src/stylus/ERC20Controls.svelte | 8 + 9 files changed, 301 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index c61302beb..ebc56178c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist *.tsbuildinfo node_modules - .env .env.local + +package.json diff --git a/packages/core/stylus/src/contract.ts b/packages/core/stylus/src/contract.ts index 96c23c438..57f40db85 100644 --- a/packages/core/stylus/src/contract.ts +++ b/packages/core/stylus/src/contract.ts @@ -35,6 +35,7 @@ export interface BaseImplementedTrait { * Lower numbers are higher priority, undefined is lowest priority. */ priority?: number; + omit_inherit?: boolean; } export interface ImplementedTrait extends BaseImplementedTrait { @@ -93,7 +94,7 @@ export class ContractBuilder implements Contract { get errors(): Error[] { return [...this.errorsMap.values()]; } - + get constants(): Variable[] { return [...this.constantsMap.values()]; } @@ -116,11 +117,8 @@ export class ContractBuilder implements Contract { return existingTrait; } else { const t: ImplementedTrait = { - name: baseTrait.name, + ...baseTrait, functions: [], - storage: baseTrait.storage, - section: baseTrait.section, - priority: baseTrait.priority, }; this.implementedTraitsMap.set(key, t); return t; diff --git a/packages/core/stylus/src/erc20.test.ts b/packages/core/stylus/src/erc20.test.ts index 9dbd12c36..907643d26 100644 --- a/packages/core/stylus/src/erc20.test.ts +++ b/packages/core/stylus/src/erc20.test.ts @@ -38,6 +38,17 @@ testERC20('erc20 burnable', { burnable: true, }); +testERC20('erc20 flash-mint', { + permit: false, + flashmint: true, +}); + +testERC20('erc20 burnable flash-mint', { + permit: false, + burnable: true, + flashmint: true, +}); + // testERC20('erc20 pausable', { // permit: false, // pausable: true, @@ -49,6 +60,12 @@ testERC20('erc20 burnable', { // pausable: true, // }); +// testERC20('erc20 flash-mint pausable', { +// permit: false, +// flashmint: true, +// pausable: true, +// }); + testERC20('erc20 permit', { permit: true, }); @@ -58,6 +75,11 @@ testERC20('erc20 permit burnable', { burnable: true, }); +testERC20('erc20 permit flash-mint', { + permit: true, + flashmint: true, +}); + // testERC20('erc20 permit pausable', { // permit: true, // pausable: true, @@ -69,10 +91,17 @@ testERC20('erc20 permit burnable', { // pausable: true, // }); +// testERC20('erc20 permit flash-mint pausable', { +// permit: true, +// flashmint: true, +// pausable: true, +// }); + testERC20('erc20 full - complex name', { name: 'Custom $ Token', burnable: true, permit: true, + flashmint: true, // pausable: true, }); @@ -87,6 +116,7 @@ testAPIEquivalence('erc20 API full', { name: 'CustomToken', burnable: true, permit: true, + flashmint: true, // pausable: true, }); diff --git a/packages/core/stylus/src/erc20.test.ts.md b/packages/core/stylus/src/erc20.test.ts.md index f1d704af3..9b97293ca 100644 --- a/packages/core/stylus/src/erc20.test.ts.md +++ b/packages/core/stylus/src/erc20.test.ts.md @@ -100,6 +100,100 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 flash-mint + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Stylus ^0.2.0-alpha.3␊ + ␊ + #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]␊ + extern crate alloc;␊ + ␊ + use openzeppelin_stylus::token::erc20::{Erc20, IErc20};␊ + use openzeppelin_stylus::token::erc20::extensions::{Erc20FlashMint, IErc3156FlashLender};␊ + use stylus_sdk::abi::Bytes;␊ + use stylus_sdk::prelude::{entrypoint, public, storage};␊ + ␊ + #[entrypoint]␊ + #[storage]␊ + struct MyToken {␊ + #[borrow]␊ + erc20: Erc20,␊ + #[borrow]␊ + flash_mint: Erc20FlashMint,␊ + }␊ + ␊ + #[public]␊ + #[inherit(Erc20)]␊ + impl MyToken {␊ + fn max_flash_loan(&self, token: Address) -> U256 {␊ + self.flash_mint.max_flash_loan(token, &self.erc20)␊ + }␊ + ␊ + fn flash_fee(&self, token: Address, value: U256) -> Result> {␊ + self.flash_mint.flash_fee(token, value).map_err(|e| e.into())␊ + }␊ + ␊ + fn flash_loan(&mut self, receiver: Address, token: Address, value: U256, data: Bytes) -> Result> {␊ + self.flash_mint.flash_loan(receiver, token, value, data, &mut self.erc20).map_err(|e| e.into())␊ + }␊ + }␊ + ` + +## erc20 burnable flash-mint + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Stylus ^0.2.0-alpha.3␊ + ␊ + #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]␊ + extern crate alloc;␊ + ␊ + use alloc::vec::Vec;␊ + use alloy_primitives::{Address, U256};␊ + use openzeppelin_stylus::token::erc20::{Erc20, IErc20};␊ + use openzeppelin_stylus::token::erc20::extensions::{␊ + Erc20FlashMint, IErc20Burnable, IErc3156FlashLender␊ + };␊ + use stylus_sdk::abi::Bytes;␊ + use stylus_sdk::prelude::{entrypoint, public, storage};␊ + ␊ + #[entrypoint]␊ + #[storage]␊ + struct MyToken {␊ + #[borrow]␊ + erc20: Erc20,␊ + #[borrow]␊ + flash_mint: Erc20FlashMint,␊ + }␊ + ␊ + #[public]␊ + #[inherit(Erc20)]␊ + impl MyToken {␊ + fn burn(&mut self, value: U256) -> Result<(), Vec> {␊ + self.erc20.burn(value).map_err(|e| e.into())␊ + }␊ + ␊ + fn burn_from(&mut self, account: Address, value: U256) -> Result<(), Vec> {␊ + self.erc20.burn_from(account, value).map_err(|e| e.into())␊ + }␊ + ␊ + fn max_flash_loan(&self, token: Address) -> U256 {␊ + self.flash_mint.max_flash_loan(token, &self.erc20)␊ + }␊ + ␊ + fn flash_fee(&self, token: Address, value: U256) -> Result> {␊ + self.flash_mint.flash_fee(token, value).map_err(|e| e.into())␊ + }␊ + ␊ + fn flash_loan(&mut self, receiver: Address, token: Address, value: U256, data: Bytes) -> Result> {␊ + self.flash_mint.flash_loan(receiver, token, value, data, &mut self.erc20).map_err(|e| e.into())␊ + }␊ + }␊ + ` + ## erc20 permit > Snapshot 1 @@ -180,6 +274,58 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 permit flash-mint + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Stylus ^0.2.0-alpha.3␊ + ␊ + #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]␊ + extern crate alloc;␊ + ␊ + use openzeppelin_stylus::token::erc20::extensions::{␊ + Erc20FlashMint, Erc20Permit, IErc3156FlashLender␊ + };␊ + use openzeppelin_stylus::token::erc20::IErc20;␊ + use openzeppelin_stylus::utils::cryptography::eip712::IEip712;␊ + use stylus_sdk::abi::Bytes;␊ + use stylus_sdk::prelude::{entrypoint, public, storage};␊ + ␊ + #[entrypoint]␊ + #[storage]␊ + struct MyToken {␊ + #[borrow]␊ + flash_mint: Erc20FlashMint,␊ + #[borrow]␊ + erc20_permit: Erc20Permit,␊ + }␊ + ␊ + #[storage]␊ + struct Eip712 {}␊ + ␊ + impl IEip712 for Eip712 {␊ + const NAME: &'static str = "ERC-20 Permit Example";␊ + const VERSION: &'static str = "1";␊ + }␊ + ␊ + #[public]␊ + #[inherit(Erc20Permit)]␊ + impl MyToken {␊ + fn max_flash_loan(&self, token: Address) -> U256 {␊ + self.flash_mint.max_flash_loan(token, &self.erc20_permit)␊ + }␊ + ␊ + fn flash_fee(&self, token: Address, value: U256) -> Result> {␊ + self.flash_mint.flash_fee(token, value).map_err(|e| e.into())␊ + }␊ + ␊ + fn flash_loan(&mut self, receiver: Address, token: Address, value: U256, data: Bytes) -> Result> {␊ + self.flash_mint.flash_loan(receiver, token, value, data, &mut self.erc20_permit).map_err(|e| e.into())␊ + }␊ + }␊ + ` + ## erc20 full - complex name > Snapshot 1 @@ -192,14 +338,19 @@ Generated by [AVA](https://avajs.dev). ␊ use alloc::vec::Vec;␊ use alloy_primitives::{Address, U256};␊ - use openzeppelin_stylus::token::erc20::extensions::{Erc20Permit, IErc20Burnable};␊ + use openzeppelin_stylus::token::erc20::extensions::{␊ + Erc20FlashMint, Erc20Permit, IErc20Burnable, IErc3156FlashLender␊ + };␊ use openzeppelin_stylus::token::erc20::IErc20;␊ use openzeppelin_stylus::utils::cryptography::eip712::IEip712;␊ + use stylus_sdk::abi::Bytes;␊ use stylus_sdk::prelude::{entrypoint, public, storage};␊ ␊ #[entrypoint]␊ #[storage]␊ struct CustomToken {␊ + #[borrow]␊ + flash_mint: Erc20FlashMint,␊ #[borrow]␊ erc20_permit: Erc20Permit,␊ }␊ @@ -215,6 +366,18 @@ Generated by [AVA](https://avajs.dev). #[public]␊ #[inherit(Erc20Permit)]␊ impl CustomToken {␊ + fn max_flash_loan(&self, token: Address) -> U256 {␊ + self.flash_mint.max_flash_loan(token, &self.erc20_permit)␊ + }␊ + ␊ + fn flash_fee(&self, token: Address, value: U256) -> Result> {␊ + self.flash_mint.flash_fee(token, value).map_err(|e| e.into())␊ + }␊ + ␊ + fn flash_loan(&mut self, receiver: Address, token: Address, value: U256, data: Bytes) -> Result> {␊ + self.flash_mint.flash_loan(receiver, token, value, data, &mut self.erc20_permit).map_err(|e| e.into())␊ + }␊ + ␊ fn burn(&mut self, value: U256) -> Result<(), Vec> {␊ self.erc20_permit.burn(value).map_err(|e| e.into())␊ }␊ diff --git a/packages/core/stylus/src/erc20.test.ts.snap b/packages/core/stylus/src/erc20.test.ts.snap index 826ed3e9b29cf4030272ca4b23af91162d520dfd..d68333652750ddfc1d4bc42cadd5fd9cbdd36b4c 100644 GIT binary patch literal 1192 zcmV;Z1Xue(RzVTo zcR{UYEy7%KL2_-Wjt)KcXC#N-d+3koy@%fWM~daHEy;R7j$1p5SC^HRoCjy-`!vJ1 zL5)lQMF08@4Ko~1YE&~HQ2A>sObZ(6hBW$@xAi(T^oQx+XQv)cC7$S7I44X2Hs9`mZaK=mnb|Mr%ViX}~0yd>Eiuy)F zAQkMJ7LPUjv{GIxuaqc{8npb#aTY&6^n+uM8lwtAngSKAfqy zC0b((#i9#BdLd<^c;sNypc3FKY7i*r(yus9tPx~{;^)i}Pg~Q374qBc4U90&Oqvm(@vD777|5FG*B^9u&c!*hlLUEXlpxJ zUc)*DH0EYlW@rBC$?RV zC9w2`HquD7fvaUk=TG1p|k;esAF!{3q=uFJoZ{TxRx0 z{-pXdrJ1hL-dzGNN&y#esvD({3DXa`xt$)!eLXkHkh>+=q&mJ$qPlGnktln#`uK@` z^%6xL)qtLb))*nrTL$$9fwca0TPI#pC)uXX5#JA&A#Hl8QZA_|ET#BJ1ABoE3y1_6 zR}s+hrhG+@Rp7!>2Pju@nQjgc2iqKq$mI8MOm=zHF_1ZP4bte2NQX5P`p8ZblwCSG z!f6*0^b)Y;)O~`Ql>8beuwGo4&ManzQWGv*`j~}!JnCtF*868al7!~}=pmus<|mni zCg6Yn-1%e>g>_nift2RtoR;?*!@SF>{@zkOPnkZQYx5h9{D$NH-f+a-P#AI1F?Y~| zi5C++b^Xr`PyghVX5(tlGQP$fVH1b2|GN9iBKV;-Vcl-s;NIt~hlWN@US1Co?N0RM zb@M5xk6wn1KzBzncRp~98mMx*x~mJYEX4V6bZf)WMZ~Xn9Tvo#LkWDDlwuPE4F`0G z5B}~U`&6fW5 GCIA2<{xQ1% literal 890 zcmV-=1BLuSRzVfpI%c=-qe zv^zP>V;_qM00000000B+Ro!kIMHJ3Y5z^jq$=xB5idV9o)Cj1?AyS)KRV30>Nr@e-lZHCAQzTV+_=)sX$sW_el+RXJr<7O}zSXVHN$+7mo^u-9R5_j6C2qySH@=w|0& zr_tJk(jjz4Jhuwh+B3ML&cWgC{*$ZFdu#1$r++?)cXL6<0zf}F&HDg)-mj_-oaG` z|81*yHmncW$fHk79m;}I&;VhIK8-;0{B2tFVor = { @@ -19,8 +20,9 @@ export const defaults: Required = { burnable: false, pausable: false, permit: true, + flashmint: false, access: commonDefaults.access, - info: commonDefaults.info + info: commonDefaults.info, } as const; export function printERC20(opts: ERC20Options = defaults): string { @@ -34,6 +36,7 @@ function withDefaults(opts: ERC20Options): Required { burnable: opts.burnable ?? defaults.burnable, pausable: opts.pausable ?? defaults.pausable, permit: opts.permit ?? defaults.permit, + flashmint: opts.flashmint ?? defaults.flashmint, }; } @@ -50,7 +53,7 @@ export function buildERC20(opts: ERC20Options): Contract { const trait = allOpts.permit ? addPermit(c, allOpts.pausable) : addBase(c, allOpts.pausable); - + addMetadata(c); if (allOpts.pausable) { @@ -61,6 +64,10 @@ export function buildERC20(opts: ERC20Options): Contract { addBurnable(c, allOpts.pausable, trait); } + if (allOpts.flashmint) { + addFlashMint(c, allOpts.pausable, trait); + } + setAccessControl(c, allOpts.access); setInfo(c, allOpts.info); @@ -86,7 +93,7 @@ function addBase(c: ContractBuilder, pausable: boolean): BaseImplementedTrait { 'self.pausable.when_not_paused()?;', ]); } - + return erc20Trait; } @@ -96,9 +103,9 @@ function addPermit(c: ContractBuilder, pausable: boolean): BaseImplementedTrait c.addUseClause('openzeppelin_stylus::utils::cryptography::eip712', 'IEip712'); c.addImplementedTrait(erc20PermitTrait); - c.addEip712("ERC-20 Permit Example", "1"); - - if (pausable) { + c.addEip712('ERC-20 Permit Example', '1'); + + if (pausable) { // Add transfer & permit functions with pause checks c.addUseClause('alloc::vec', 'Vec'); c.addUseClause('alloy_primitives', 'Address'); @@ -115,7 +122,7 @@ function addPermit(c: ContractBuilder, pausable: boolean): BaseImplementedTrait 'self.pausable.when_not_paused()?;', ]); } - + return erc20PermitTrait; } @@ -144,12 +151,31 @@ function addBurnable(c: ContractBuilder, pausable: boolean, trait: BaseImplement } } +function addFlashMint(c: ContractBuilder, pausable: boolean, baseTrait: BaseImplementedTrait) { + c.addUseClause('openzeppelin_stylus::token::erc20::extensions', 'Erc20FlashMint'); + c.addUseClause('openzeppelin_stylus::token::erc20::extensions', 'IErc3156FlashLender'); + + c.addUseClause('stylus_sdk::abi', 'Bytes'); + + c.addImplementedTrait(flashMintTrait); + + c.addFunction(flashMintTrait, functions(baseTrait).max_flash_loan); + c.addFunction(flashMintTrait, functions(baseTrait).flash_fee); + c.addFunction(flashMintTrait, functions(baseTrait).flash_loan); + + if (pausable) { + c.addFunctionCodeBefore(flashMintTrait, functions(baseTrait).flash_loan, [ + 'self.pausable.when_not_paused()?;', + ]); + } +} + const erc20Trait: BaseImplementedTrait = { name: 'Erc20', storage: { name: 'erc20', type: 'Erc20', - } + }, }; const erc20PermitTrait: BaseImplementedTrait = { @@ -157,7 +183,16 @@ const erc20PermitTrait: BaseImplementedTrait = { storage: { name: 'erc20_permit', type: 'Erc20Permit', - } + }, +}; + +const flashMintTrait: BaseImplementedTrait = { + name: 'Erc20FlashMint', + storage: { + name: 'flash_mint', + type: 'Erc20FlashMint', + }, + omit_inherit: true }; // const erc20MetadataTrait: BaseImplementedTrait = { @@ -168,7 +203,7 @@ const erc20PermitTrait: BaseImplementedTrait = { // } // } -const functions = (trait: BaseImplementedTrait) => +const functions = (baseTrait: BaseImplementedTrait) => defineFunctions({ // Token Functions transfer: { @@ -179,7 +214,7 @@ const functions = (trait: BaseImplementedTrait) => ], returns: 'Result>', code: [ - `self.${trait.storage.name}.transfer(to, value).map_err(|e| e.into())`, + `self.${baseTrait.storage.name}.transfer(to, value).map_err(|e| e.into())`, ], }, transfer_from: { @@ -191,7 +226,7 @@ const functions = (trait: BaseImplementedTrait) => ], returns: 'Result>', code: [ - `self.${trait.storage.name}.transfer_from(from, to, value).map_err(|e| e.into())`, + `self.${baseTrait.storage.name}.transfer_from(from, to, value).map_err(|e| e.into())`, ], }, @@ -210,7 +245,7 @@ const functions = (trait: BaseImplementedTrait) => burn: { args: [getSelfArg(), { name: 'value', type: 'U256' }], returns: 'Result<(), Vec>', - code: [`self.${trait.storage.name}.burn(value).map_err(|e| e.into())`], + code: [`self.${baseTrait.storage.name}.burn(value).map_err(|e| e.into())`], }, burn_from: { args: [ @@ -220,7 +255,7 @@ const functions = (trait: BaseImplementedTrait) => ], returns: 'Result<(), Vec>', code: [ - `self.${trait.storage.name}.burn_from(account, value).map_err(|e| e.into())`, + `self.${baseTrait.storage.name}.burn_from(account, value).map_err(|e| e.into())`, ], }, @@ -237,7 +272,42 @@ const functions = (trait: BaseImplementedTrait) => ], returns: 'Result<(), Vec>', code: [ - `self.${trait.storage.name}.permit(owner, spender, value, deadline, v, r, s).map_err(|e| e.into())`, + `self.${baseTrait.storage.name}.permit(owner, spender, value, deadline, v, r, s).map_err(|e| e.into())`, + ], + }, + + max_flash_loan: { + args: [ + getSelfArg("immutable"), + { name: 'token', type: 'Address' }, + ], + returns: 'U256', + code: [ + `self.${flashMintTrait.storage.name}.max_flash_loan(token, &self.${baseTrait.storage.name})`, + ], + }, + flash_fee: { + args: [ + getSelfArg("immutable"), + { name: 'token', type: 'Address' }, + { name: 'value', type: 'U256' }, + ], + returns: 'Result>', + code: [ + `self.${flashMintTrait.storage.name}.flash_fee(token, value).map_err(|e| e.into())`, + ], + }, + flash_loan: { + args: [ + getSelfArg(), + { name: 'receiver', type: 'Address' }, + { name: 'token', type: 'Address' }, + { name: 'value', type: 'U256' }, + { name: 'data', type: 'Bytes' }, + ], + returns: 'Result>', + code: [ + `self.${flashMintTrait.storage.name}.flash_loan(receiver, token, value, data, &mut self.${baseTrait.storage.name}).map_err(|e| e.into())`, ], }, }); diff --git a/packages/core/stylus/src/generate/erc20.ts b/packages/core/stylus/src/generate/erc20.ts index 381ead1a0..c48ed73e4 100644 --- a/packages/core/stylus/src/generate/erc20.ts +++ b/packages/core/stylus/src/generate/erc20.ts @@ -10,8 +10,9 @@ const blueprint = { burnable: booleans, pausable: booleans, permit: booleans, + flashmint: booleans, access: accessOptions, - info: infoOptions + info: infoOptions, }; export function* generateERC20Options(): Generator> { diff --git a/packages/core/stylus/src/print.ts b/packages/core/stylus/src/print.ts index 50df6b8b1..d913e549e 100644 --- a/packages/core/stylus/src/print.ts +++ b/packages/core/stylus/src/print.ts @@ -192,11 +192,14 @@ function printEip712(eip712?: EIP712): Lines[] { } function printImplementedTraits(contractName: string, sortedGroups: [string, ImplementedTrait[]][]): Lines[] { - const traitNames = sortedGroups.flatMap(([_, impls]) => impls).map(trait => trait.name); - + const traitNames = sortedGroups + .flatMap(([_, impls]) => impls) + .filter(trait => !trait.omit_inherit) + .map(trait => trait.name); + const inheritAttribute = traitNames.length > 0 - ? `#[inherit(${traitNames.join(', ')})]` - : "#[inherit]"; + ? `#[inherit(${traitNames.join(', ')})]` + : "#[inherit]"; const header = [ '#[public]', diff --git a/packages/ui/src/stylus/ERC20Controls.svelte b/packages/ui/src/stylus/ERC20Controls.svelte index 712f99c47..a36017c4e 100644 --- a/packages/ui/src/stylus/ERC20Controls.svelte +++ b/packages/ui/src/stylus/ERC20Controls.svelte @@ -59,6 +59,14 @@ Without paying gas, token holders will be able to allow third parties to transfer from their account. + +