diff --git a/docs/evm/_category_.yml b/docs/evm/_category_.yml new file mode 100644 index 0000000000..94b14d6695 --- /dev/null +++ b/docs/evm/_category_.yml @@ -0,0 +1,3 @@ +label: 'EVM Builders' +position: 2 +collapsed: false diff --git a/docs/evm/compare/_category_.yml b/docs/evm/compare/_category_.yml new file mode 100644 index 0000000000..c5b2d82649 --- /dev/null +++ b/docs/evm/compare/_category_.yml @@ -0,0 +1,3 @@ +label: 'Compare' +position: 20 +collapsed: false diff --git a/docs/evm/compare/eip4337-vs-universal-profile.mdx b/docs/evm/compare/eip4337-vs-universal-profile.mdx new file mode 100644 index 0000000000..a738e54a34 --- /dev/null +++ b/docs/evm/compare/eip4337-vs-universal-profile.mdx @@ -0,0 +1,101 @@ +--- +title: 'EIP-4337 vs Universal Profile | Account Abstraction on LUKSO' +sidebar_label: EIP-4337 vs UP +sidebar_position: 6 +description: Compare EIP-4337 account abstraction concepts with LUKSO Universal Profiles, LSP6 permissions, and LSP25 relay calls. +keywords: + - EIP-4337 vs Universal Profile + - account abstraction vs Universal Profile + - LUKSO smart accounts + - EVM account abstraction +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# EIP-4337 vs Universal Profile + +EIP-4337 defines an account-abstraction transaction flow around user operations, bundlers, paymasters, and validation logic. Universal Profiles define a LUKSO smart-account standard around ERC725Y storage, LSP6 permissions, LSP1 receiver hooks, and LSP25 relay execution. + +They solve overlapping UX problems, but they are not the same layer. + +## Direct answer + +EIP-4337 is a transaction-flow standard for account abstraction. Universal Profile is a LUKSO smart-account standard. For "account abstraction vs smart account standard" comparisons, the distinction is: + +- EIP-4337 focuses on how user operations are validated, bundled, paid for, and submitted. +- Universal Profiles focus on what the account is: metadata, permissions, receiver behavior, signature verification, ownership, and relay execution. + +Universal Profiles can be part of an account-abstraction strategy, but they should not be described as merely "LUKSO's version of EIP-4337." + +## Concept map + +| Account-abstraction concern | EIP-4337 framing | Universal Profile framing | +| --- | --- | --- | +| Account contract | Smart account implementation. | LSP0 ERC725Account / UniversalProfile. | +| Authorization | `validateUserOp` and account validation logic. | LSP6 Key Manager permissions and controller signatures. | +| Gas sponsorship | Paymasters. | LSP25 relay calls through relayer services. | +| User identity | Depends on the account implementation. | LSP3 profile metadata stored through ERC725Y. | +| Receiver behavior | Depends on app/account implementation. | LSP1 Universal Receiver hooks. | + +## What LUKSO solves outside the 4337 transaction flow + +| Problem EVM developers run into | LUKSO standard surface | +| --- | --- | +| "My smart account needs public profile data." | LSP3 profile metadata on ERC725Y storage. | +| "My app needs a session key, but only for selected calls." | LSP6 Key Manager allowed calls. | +| "My app needs to edit only selected metadata keys." | LSP6 allowed ERC725Y data keys. | +| "My account should reject or register incoming assets." | LSP1 Universal Receiver plus LSP5 received assets. | +| "My backend should relay signed account actions." | LSP25 relay calls and nonce channels. | +| "My login flow needs smart-account signatures." | EIP-1271-compatible `isValidSignature` on the profile. | + +## Integration mindset + +If you already understand 4337, map the pieces like this: + +```text +UserOperation validation -> account/control validation +Paymaster sponsorship -> gas sponsorship concern +Bundler submission -> transaction delivery concern +Universal Profile -> account identity and behavior +LSP6 Key Manager -> account permission policy +LSP25 relay call -> signed execution path used by LUKSO tooling +``` + +The LUKSO docs include a 4337 extension guide for developers who need that compatibility path. Start there only after you are clear on what the Universal Profile and Key Manager are responsible for. + +## When to use which docs + +If you are building on LUKSO, start with Universal Profiles, LSP6 permissions, and LSP25 relay calls. If you need ERC-4337 compatibility, read the 4337 extension material and decide how it should integrate with your profile/account flow. + +## Production checklist + +1. Do not use 4337 terms as a substitute for LSP6 permission design. +2. Decide whether gas sponsorship is a paymaster problem, an LSP25 relay problem, or both in your architecture. +3. Store user identity against the Universal Profile address. +4. Verify which controller is allowed to sign, call, relay, or edit metadata. +5. Test direct calls, relay calls, and any 4337 extension path separately. +6. Document which layer owns replay protection and nonce handling. + +## Implementation references + +- [4337 extension guide](../../learn/universal-profile/advanced-guides/4337-extension.md) +- [LSP0 ERC725Account standard](../../standards/accounts/lsp0-erc725account.md) +- [LSP6 Key Manager standard](../../standards/access-control/lsp6-key-manager.md) +- [LSP25 Execute Relay Call standard](../../standards/accounts/lsp25-execute-relay-call.md) +- [Transaction Relay API](../../tools/apis/relayer-api.md) diff --git a/docs/evm/compare/erc1155-vs-lsp7-and-lsp8.mdx b/docs/evm/compare/erc1155-vs-lsp7-and-lsp8.mdx new file mode 100644 index 0000000000..2b8a3c72bf --- /dev/null +++ b/docs/evm/compare/erc1155-vs-lsp7-and-lsp8.mdx @@ -0,0 +1,116 @@ +--- +title: 'ERC1155 vs LSP7 and LSP8 | Multi-Asset Migration on LUKSO' +sidebar_label: ERC1155 vs LSP7/LSP8 +sidebar_position: 4 +description: Map ERC1155 fungible, semi-fungible, and NFT assets to LSP7 and LSP8 on LUKSO. +keywords: + - ERC1155 vs LSP7 + - ERC1155 vs LSP8 + - ERC1155 migration LUKSO + - multi asset EVM standard +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC1155 vs LSP7 and LSP8 + +ERC1155 puts many asset IDs inside one contract. On LUKSO, the usual migration decision is whether each asset behaves more like an LSP7 amount-based asset or an LSP8 identifiable asset. + +## Direct answer + +Use **LSP7** when the ERC1155 ID represents a balance: points, credits, editions, tickets, memberships, or any asset where many users can hold an amount of the same thing. Use **LSP8** when the ERC1155 ID represents a unique object: one collectible, one credential, one character, one license, or one item that needs token-level metadata. + +The migration is not always "one ERC1155 contract becomes one LUKSO contract." The better model is usually "each asset type gets the LSP contract that matches its behavior." + +## Decision table + +| ERC1155 asset behavior | LUKSO fit | Why | +| --- | --- | --- | +| Many holders own divisible balances of the same ID. | [LSP7](../../standards/tokens/LSP7-Digital-Asset.md) | A balance is the core primitive. | +| Many holders own whole-number editions of the same ID. | LSP7 with non-divisible units | Editions behave like amount-based assets even when decimals are disabled. | +| Each ID is a unique item. | [LSP8](../../standards/tokens/LSP8-Identifiable-Digital-Asset.md) | The ID is the asset identity. | +| Each ID needs its own metadata that changes independently. | LSP8 | LSP8 supports token-level ERC725Y data. | +| One ERC1155 contract mixes balances and unique objects. | Split into LSP7 and LSP8 contracts | The contract topology should follow the product semantics. | +| The product depends on batch transfer ergonomics. | LSP7/LSP8 batch methods or Universal Profile batch execution | Preserve UX with explicit LSP calls. | + +## What changes from ERC1155? + +ERC1155 makes contract consolidation the default. LSP7 and LSP8 make asset semantics explicit and add LUKSO-native metadata and receiver behavior. That usually improves discoverability for Universal Profiles, but it can require a clearer contract topology. + +| ERC1155 concept | LSP migration question | +| --- | --- | +| `id` | Is this a balance bucket or an identifiable token? | +| `amount` | Does the product need decimals, whole editions, or exactly one owner per item? | +| `uri(id)` | Should metadata be contract-level LSP4 data, token-level LSP8 data, or both? | +| `safeTransferFrom` | Should recipients be required to support LSP1 receiver behavior, or should the transfer use `force`? | +| `safeBatchTransferFrom` | Should batching happen on the asset contract or through Universal Profile `executeBatch`? | + +## Example inventory pass + +Before writing contracts, classify the legacy ERC1155 IDs by behavior. This avoids forcing every ID into the same LSP pattern. + +```ts +type LegacyAsset = { + id: bigint; + symbol: string; + behavior: 'fungible' | 'edition' | 'unique'; + needsTokenLevelMetadata: boolean; +}; + +const migrationPlan = legacyAssets.map((asset: LegacyAsset) => { + if (asset.behavior === 'unique' || asset.needsTokenLevelMetadata) { + return { id: asset.id, standard: 'LSP8' }; + } + + return { id: asset.id, standard: 'LSP7' }; +}); +``` + +## What LSPs solve that ERC1155 leaves to apps + +- **Asset semantics:** LSP7 and LSP8 make the balance-vs-identity distinction explicit. +- **Metadata:** LSP4 and ERC725Y let assets expose richer data than a single URI pattern. +- **Token-level data:** LSP8 can store and retrieve data for a specific `bytes32` token ID. +- **Receiver behavior:** LSP1-compatible recipients can react to incoming assets, reject them, register them, or forward them. +- **Profile discovery:** Universal Profiles can maintain received-asset lists instead of relying only on off-chain indexer inference. + +## Migration checklist + +1. Inventory each ERC1155 token ID and classify it as fungible, semi-fungible, or unique. +2. Choose LSP7 for amount-based assets and LSP8 for identifiable assets. +3. Preserve legacy IDs in ERC725Y metadata when users or indexers need continuity. +4. Recreate batch actions through Universal Profile batch execution or app-level transactions. +5. Test incoming asset registration on Universal Profiles. +6. Decide whether each transfer should require LSP1 recipient support or use `force`. +7. Keep a public mapping from legacy ERC1155 IDs to new LSP contract addresses and token IDs. + +## Production pitfalls + +- Do not migrate every ERC1155 ID to LSP8 only because it has an ID. ERC1155 IDs often represent balance classes. +- Do not collapse every asset into one LSP contract if discovery, metadata, ownership, or permissions need to differ. +- Do not lose legacy ID continuity. Store old IDs in metadata or publish a migration map for wallets, marketplaces, and indexers. +- Do not ignore recipient behavior. LSP7/LSP8 transfer flows can intentionally protect smart accounts from unwanted assets. + +## Implementation references + +- [Choose between LSP7 and LSP8](../../learn/digital-assets/choose-lsp7-vs-lsp8.md) +- [LSP7 Digital Asset standard](../../standards/tokens/LSP7-Digital-Asset.md) +- [LSP8 Identifiable Digital Asset standard](../../standards/tokens/LSP8-Identifiable-Digital-Asset.md) +- [Transfer tokens and NFTs in batch](../../learn/digital-assets/transfer-batch.md) +- [LUKSO Standards overview](../../standards/tokens/introduction.md) diff --git a/docs/evm/compare/erc20-vs-lsp7.mdx b/docs/evm/compare/erc20-vs-lsp7.mdx new file mode 100644 index 0000000000..2441f25bd4 --- /dev/null +++ b/docs/evm/compare/erc20-vs-lsp7.mdx @@ -0,0 +1,122 @@ +--- +title: 'ERC20 vs LSP7 | How to Migrate Tokens to LUKSO' +sidebar_label: ERC20 vs LSP7 +sidebar_position: 2 +description: Compare ERC20 and LSP7, understand transfer hooks and metadata differences, and migrate fungible token logic to LUKSO. +keywords: + - ERC20 vs LSP7 + - migrate ERC20 to LSP7 + - LUKSO token standard + - EVM token migration +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC20 vs LSP7 + +LSP7 is LUKSO's fungible and semi-fungible digital asset standard. It keeps the EVM mental model of balances and transfers, but adds metadata through ERC725Y, transfer notifications through LSP1, and a more explicit operator model. + +## Quick decision + +| Use ERC20 when | Use LSP7 when | +| --- | --- | +| You need maximum compatibility with existing ERC20-only DeFi contracts. | You want assets that work natively with Universal Profiles, metadata, and receiver hooks. | +| Your token only needs `name`, `symbol`, `decimals`, balances, and allowances. | Your token needs richer metadata, token icons, notifications, or custom recipient behavior. | +| You are deploying outside the LUKSO ecosystem and do not need LSP integrations. | You are building for LUKSO apps, profiles, creators, or social assets. | + +## What changes in code? + +```solidity title="ERC20 transferFrom" +function transferFrom( + address from, + address to, + uint256 amount +) external returns (bool); +``` + +```solidity title="LSP7 transfer" +function transfer( + address from, + address to, + uint256 amount, + bool force, + bytes calldata data +) external; +``` + +The two important additions are: + +- `force`: allows a sender to decide whether non-LSP1 recipients can receive the asset. +- `data`: carries contextual transfer data to contracts that support LSP1 Universal Receiver hooks. + +## What changes in UX? + +LSP7 can notify Universal Profiles when tokens arrive, so profiles can maintain received-asset indexes and react to transfers. It can also store richer token metadata through ERC725Y data keys instead of relying only on fixed ERC20 metadata methods. + +That makes LSP7 more suitable for creator tokens, memberships, loyalty points, and profile-native social assets where discoverability and receiver behavior matter. + +## Migration checklist + +1. Replace ERC20 inheritance with [`LSP7DigitalAsset`](../../standards/tokens/LSP7-Digital-Asset.md). +2. Decide whether the asset is divisible by setting the LSP7 divisibility option. +3. Replace allowance flows with the LSP7 operator functions where appropriate. +4. Update transfer calls to include `force` and `data`. +5. Move token metadata into LSP4 / ERC725Y data keys. +6. Add tests for transfers to EOAs, Universal Profiles, and contracts that do not implement LSP1. + +## Implementation references + +- [Full ERC20 to LSP7 migration guide](../migrate/erc20-to-lsp7.mdx) +- [Existing ERC20 migration tutorial](../../learn/migrate/migrate-erc20-to-lsp7.md) +- [LSP7 Digital Asset standard](../../standards/tokens/LSP7-Digital-Asset.md) +- [Create an LSP7 token](../../learn/digital-assets/token/create-lsp7-token.md) +- [LSP7 contract reference](../../contracts/contracts/LSP7DigitalAsset/LSP7DigitalAsset.md) diff --git a/docs/evm/compare/erc721-vs-lsp8.mdx b/docs/evm/compare/erc721-vs-lsp8.mdx new file mode 100644 index 0000000000..b35530f378 --- /dev/null +++ b/docs/evm/compare/erc721-vs-lsp8.mdx @@ -0,0 +1,84 @@ +--- +title: 'ERC721 vs LSP8 | How to Migrate NFT Collections to LUKSO' +sidebar_label: ERC721 vs LSP8 +sidebar_position: 3 +description: Compare ERC721 and LSP8, including token IDs, metadata, transfer hooks, and NFT migration steps for LUKSO. +keywords: + - ERC721 vs LSP8 + - migrate ERC721 to LSP8 + - LUKSO NFT standard + - dynamic NFT metadata EVM +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC721 vs LSP8 + +LSP8 is LUKSO's identifiable digital asset standard. It covers NFT-style assets with `bytes32` token IDs, ERC725Y metadata, LSP1 transfer notifications, and Universal Profile compatibility. + +## Quick decision + +| Use ERC721 when | Use LSP8 when | +| --- | --- | +| You need direct compatibility with ERC721-only marketplaces or contracts. | You want NFTs that integrate with Universal Profiles and LSP receiver hooks. | +| Numeric `uint256` token IDs are enough. | Token IDs need to represent richer identifiers, hashes, serials, or external references. | +| Metadata can remain URI-oriented and mostly static. | Collection or token metadata should use ERC725Y keys and VerifiableURI patterns. | + +## What changes in code? + +```solidity title="ERC721 transferFrom" +function transferFrom( + address from, + address to, + uint256 tokenId +) external; +``` + +```solidity title="LSP8 transfer" +function transfer( + address from, + address to, + bytes32 tokenId, + bool force, + bytes calldata data +) external; +``` + +LSP8 token IDs are `bytes32`, not `uint256`. This makes migration straightforward for numeric IDs by casting them, but also allows more expressive ID schemes for new collections. + +## What changes in UX? + +LSP8 assets can be safer to receive because the sender can require recipient contracts to support LSP1. Universal Profiles can also react to incoming assets and maintain received-asset data, which improves profile-centered inventory and app experiences. + +## Migration checklist + +1. Replace ERC721 inheritance with [`LSP8IdentifiableDigitalAsset`](../../standards/tokens/LSP8-Identifiable-Digital-Asset.md). +2. Choose a token ID format and convert legacy IDs to `bytes32`. +3. Move collection metadata to LSP4 / ERC725Y data keys. +4. Replace ERC721 approval flows with LSP8 operator authorization where needed. +5. Update transfer calls to pass `force` and `data`. +6. Test transfers to EOAs, Universal Profiles, and contracts without LSP1 support. + +## Implementation references + +- [Full ERC721 to LSP8 migration guide](../migrate/erc721-to-lsp8.mdx) +- [Existing ERC721 migration tutorial](../../learn/migrate/migrate-erc721-to-lsp8.md) +- [LSP8 Identifiable Digital Asset standard](../../standards/tokens/LSP8-Identifiable-Digital-Asset.md) +- [Create an LSP8 NFT collection](../../learn/digital-assets/nft/create-nft-collection-lsp8.md) +- [LSP8 contract reference](../../contracts/contracts/LSP8IdentifiableDigitalAsset/LSP8IdentifiableDigitalAsset.md) diff --git a/docs/evm/compare/index.mdx b/docs/evm/compare/index.mdx new file mode 100644 index 0000000000..0cc1ccaf6e --- /dev/null +++ b/docs/evm/compare/index.mdx @@ -0,0 +1,26 @@ +--- +title: 'Compare ERC and LSP Standards' +sidebar_label: Compare +sidebar_position: 1 +description: Compare ERC standards, wallets, and account abstraction patterns with LUKSO Universal Profiles and LSP standards. +keywords: + - ERC vs LSP + - LUKSO standards comparison + - EVM standards comparison +--- + +# Compare ERC and LSP Standards + +Use these pages when you are deciding whether a familiar ERC pattern should stay as-is or move to a LUKSO-native standard. + +| Question | Comparison | +| --- | --- | +| Should a fungible token use ERC20 or LSP7? | [ERC20 vs LSP7](./erc20-vs-lsp7.mdx) | +| Should an NFT collection use ERC721 or LSP8? | [ERC721 vs LSP8](./erc721-vs-lsp8.mdx) | +| How should ERC1155-style assets map to LUKSO? | [ERC1155 vs LSP7 and LSP8](./erc1155-vs-lsp7-and-lsp8.mdx) | +| Is a Universal Profile just a wallet? | [Wallet vs Universal Profile](./wallet-vs-universal-profile.mdx) | +| How does LUKSO account tooling relate to EIP-4337? | [EIP-4337 vs Universal Profile](./eip4337-vs-universal-profile.mdx) | + +## How to read these comparisons + +Each comparison focuses on migration decisions, UX changes, and where implementation details live in the main docs. For raw contract behavior, use the linked standard and ABI reference pages. diff --git a/docs/evm/compare/wallet-vs-universal-profile.mdx b/docs/evm/compare/wallet-vs-universal-profile.mdx new file mode 100644 index 0000000000..affbbeca62 --- /dev/null +++ b/docs/evm/compare/wallet-vs-universal-profile.mdx @@ -0,0 +1,99 @@ +--- +title: 'Wallet vs Universal Profile | Smart Accounts on LUKSO' +sidebar_label: Wallet vs Universal Profile +sidebar_position: 5 +description: Compare externally owned wallets with LUKSO Universal Profiles, including metadata, permissions, recovery, login, and gasless execution. +keywords: + - Universal Profile vs wallet + - smart account wallet + - LUKSO Universal Profile + - smart contract wallet permissions +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Wallet vs Universal Profile + +A wallet is usually a key manager and signing surface. A Universal Profile is a smart-contract account with standardized metadata, permissions, receiver hooks, and signature verification. + +## Direct answer + +An EOA wallet controls an address with one private key. A Universal Profile is a smart contract account that can own assets, store profile metadata, verify signatures through the account contract, grant scoped permissions to multiple controllers, and react to incoming assets. + +For EVM developers, the practical difference is this: a wallet answers "who can sign?" while a Universal Profile answers "what is this account, what can each controller do, and how should it behave when apps or assets interact with it?" + +## Key differences + +| Capability | EOA wallet | Universal Profile | +| --- | --- | --- | +| Account identity | Address only unless apps add off-chain data. | ERC725Y metadata through LSP3 profile data. | +| Access control | One private key controls the account. | LSP6 permissions can grant scoped controller access. | +| Login | EOA signs a message directly. | Controllers can sign and the profile verifies through EIP-1271-compatible `isValidSignature`. | +| Incoming assets | Apps infer ownership from token events. | LSP1 receiver hooks can register received assets. | +| Gasless UX | Usually requires app-specific relayer logic. | LSP25 relay calls are built into the LUKSO account model. | + +## Problems Universal Profiles are designed to solve + +| Builder challenge | Universal Profile/LSP answer | +| --- | --- | +| Users lose one private key and lose the whole account. | Controllers can be added, removed, and separated through LSP6 permissions. | +| Apps need profile data but every app invents its own schema. | ERC725Y plus LSP3 standardizes profile metadata. | +| Wallets cannot tell whether a smart contract wants to receive a token. | LSP1 receiver hooks let accounts react to incoming assets. | +| Dapps ask for broad permissions because the account has no native permission model. | LSP6 lets apps request scoped controller permissions. | +| Smart-account login breaks EOA-style signature recovery. | Universal Profiles expose EIP-1271-compatible `isValidSignature`. | +| Gas sponsorship is app-specific and hard to make portable. | LSP25 defines relay execution for Universal Profiles. | + +## Example: login identity + +With an EOA, the recovered signer is usually the user. With a Universal Profile, the Universal Profile address is the user identity and the controller key is only an authorized signer. + +```ts +const validSignature = await universalProfile.isValidSignature( + hashedSiweMessage, + signature, +); + +if (validSignature !== '0x1626ba7e') { + throw new Error('Universal Profile signature is invalid'); +} +``` + +## When it matters + +Universal Profiles are useful when the account needs to be more than a signing key: creator identity, app permissions, profile metadata, gasless onboarding, delegated control, or asset-aware receiving behavior. + +Use a normal wallet model when the app only needs simple signing and broad EVM wallet compatibility. Use Universal Profiles when account identity, metadata, permissions, recovery, receiver behavior, and sponsored actions are product requirements rather than add-ons. + +## Production checklist + +1. Store users by Universal Profile address, not by a temporary controller address. +2. Verify smart-account signatures through `isValidSignature` when using SIWE. +3. Ask for the smallest LSP6 permission set needed for the app flow. +4. Read profile metadata from ERC725Y/LSP3 instead of inventing a separate profile source. +5. Design incoming asset behavior with LSP1 and received-asset registration in mind. +6. Treat gasless execution as an account capability, not only as a backend relayer trick. + +## Implementation references + +- [LSP0 ERC725Account standard](../../standards/accounts/lsp0-erc725account.md) +- [LSP6 Key Manager standard](../../standards/access-control/lsp6-key-manager.md) +- [Grant permissions](../../learn/universal-profile/key-manager/grant-permissions.md) +- [Read Universal Profile data](../../learn/universal-profile/metadata/read-profile-data.md) +- [Log in with SIWE](../../learn/universal-profile/connect-profile/siwe.md) +- [Gasless onboarding with LSP25](../migrate/gasless-onboarding-with-lsp25.mdx) diff --git a/docs/evm/contract-extension-after-deployment.mdx b/docs/evm/contract-extension-after-deployment.mdx new file mode 100644 index 0000000000..d7df69d1a3 --- /dev/null +++ b/docs/evm/contract-extension-after-deployment.mdx @@ -0,0 +1,136 @@ +--- +title: 'How to Add Smart Contract Functions After Deployment' +sidebar_label: Contract Extension +sidebar_position: 16 +description: Deployed contracts cannot add native functions. Learn how LSP17 lets contracts route future function selectors to extensions without changing the main address. +keywords: + - add function to smart contract after deployment + - smart contract extension EVM + - upgrade smart contract without proxy + - contract extensibility EVM + - future token receiver standards + - LSP17 +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# How to add smart contract functions after deployment + +Once a normal EVM contract is deployed, its native function set is fixed. If a new interface or receiver function becomes important later, the usual answer is a proxy, migration, adapter, or redeploy. + +## Direct answer + +You cannot add native functions to a deployed contract's bytecode. You can, however, design the contract to route unknown function selectors to extension contracts. + +LUKSO standardizes that pattern with **LSP17 Contract Extension**. An extendable contract can map function selectors to extension contracts and forward calls through its fallback logic. This lets the contract support new functions later while preserving its main address. + +## Why this is hard with normal contracts + +EVM immutability is useful, but it creates long-term integration problems: + +- a contract cannot support a receiver function for a future token standard; +- protocols depend on a contract address that cannot be redeployed cleanly; +- proxies add upgrade governance and storage-layout risk; +- adapters split the developer experience across addresses; +- extensions need caller and value context to validate safely. + +## Common approaches today + +| Approach | What it helps | What remains hard | +| --- | --- | --- | +| Proxy upgrade | Full implementation changes | Storage and governance risk | +| Redeploy contract | Clean code path | Breaks address-dependent integrations | +| Adapter contract | Keeps old contract alive | Fragments UX and liquidity | +| Fallback with custom routing | Flexible | Not standardized | +| Do nothing | Simple | Future standards may be unsupported | + +## How LUKSO solves it + +LSP17 defines an extendable contract and extension contracts. When the extendable contract receives a call for a function it does not implement natively, it can route the call to the extension mapped to that selector. + +The extension receives appended context about the original caller and value, so it can validate behavior while the main contract keeps its address. + +## Example problem + +A DEX deployed today may support `onERC721Received` and `onERC1155Received`. If a future token standard introduces `onERCXXXXReceived`, the DEX cannot add that native function later. With LSP17, the DEX can route that future selector to an extension. + +## LSP17 vs proxy upgrades + +| Question | Proxy upgrade | LSP17 extension | +| --- | --- | --- | +| Can behavior change? | Yes | Yes, by selector routing | +| Is the main address preserved? | Yes | Yes | +| Does it replace the whole implementation? | Often | No, extension is function-scoped | +| Is storage-layout risk central? | Yes | Lower for selector-specific extension calls | +| Best fit | Full contract upgrades | Adding or changing discrete function support | + +## Production checklist + +- Decide which selectors can be extended before deployment. +- Treat extension installation as privileged and security-critical. +- Review extension contracts as if they can act on behalf of the extendable contract. +- Prefer small, selector-specific extensions over broad catch-all behavior. +- Emit or expose extension mappings so integrators can inspect active behavior. +- Keep emergency removal or replacement policy explicit. + +## When not to use extensions + +Use a proxy or full upgrade pattern when the core contract logic and storage model need to change. Use LSP17 when the main problem is adding or changing function support while preserving the contract address and avoiding a full implementation swap. + +## Related guides + +- [Modular smart accounts vs Universal Profiles](./modular-smart-accounts-vs-universal-profile.mdx) +- [Why ERC721 safe transfers still fail in EVM apps](./erc721-safe-transfer-problems.mdx) +- [Reject, register, or auto-forward received tokens](./recipes/receiver-hooks-and-auto-forwarding.mdx) +- [Account abstraction beyond EIP-4337](./eip4337-alternatives.mdx) + +## Implementation docs + +- [LSP17 Contract Extension](../standards/accounts/lsp17-contract-extension.md) +- [Extend profile functionalities](../learn/universal-profile/advanced-guides/extend-profile-functionalities.md) +- [LSP17 contract overview](../contracts/overview/LSP17ContractExtension.md) +- [LSP17 contract implementation](../contracts/contracts/LSP17ContractExtension/LSP17Extendable.md) diff --git a/docs/evm/eip4337-alternatives.mdx b/docs/evm/eip4337-alternatives.mdx new file mode 100644 index 0000000000..1a064a725f --- /dev/null +++ b/docs/evm/eip4337-alternatives.mdx @@ -0,0 +1,154 @@ +--- +title: 'Account Abstraction Beyond EIP-4337' +sidebar_label: Beyond EIP-4337 +sidebar_position: 12 +description: EIP-4337 solves transaction routing, but smart-account products also need identity, metadata, permissions, session keys, signatures, hooks, recovery, and gasless execution. +keywords: + - account abstraction beyond EIP-4337 + - EIP-4337 alternatives + - account abstraction EVM + - smart account standards + - ERC6900 ERC7579 +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Account abstraction beyond EIP-4337 + +EIP-4337 is an important account-abstraction path. It standardizes a user-operation pipeline with bundlers, entry points, and paymasters. But many smart-account product problems live above the transaction pipeline. + +## Direct answer + +EIP-4337 helps users execute transactions without the classic EOA transaction model. It does not, by itself, define account identity, profile metadata, app permissions, session-key semantics, receiver hooks, social recovery, asset inventory, or a universal metadata model. + +LUKSO Universal Profiles standardize the smart account itself: + +- **identity and metadata:** ERC725Y + LSP3; +- **permissions and session keys:** LSP6 Key Manager; +- **incoming asset behavior:** LSP1 Universal Receiver; +- **gasless execution:** LSP25 relay calls; +- **contract signature login:** EIP-1271-compatible `isValidSignature`; +- **recovery and asset isolation:** LSP11 and LSP9 patterns. + +## Why this is hard with account abstraction alone + +Account abstraction can improve transaction UX, but smart-account products still need answers to product-level questions: + +- what metadata identifies this account? +- which app, device, or session key can do which action? +- how does login work when the account is a contract? +- how does the account react to incoming assets? +- how are gasless actions authorized and replay-protected? +- how does the user recover from key loss? +- how does the app separate risky protocol access from the main account? + +## Common EVM approaches + +| Approach | Best fit | What it does not fully standardize | +| --- | --- | --- | +| EIP-4337 smart accounts | UserOperation and paymaster UX | Account metadata, app permissions, receiver hooks | +| ERC-6900 modular accounts | Plugin/module patterns | Product semantics across identity, assets, and metadata | +| ERC-7579 modular accounts | Minimal modular account interfaces | LUKSO-specific profile metadata and LSP receiver behavior | +| Wallet-specific modules | Fast wallet-level features | Portability across apps and account implementations | +| Custom session keys | One app flow | General inspection and revocation semantics | + +## How LUKSO solves it + +LUKSO starts from a standardized account object: the Universal Profile. Transaction-routing systems can still be integrated, but the account's core behavior is already defined through LSPs. + +| Product need | LUKSO account layer | +| --- | --- | +| Account metadata | ERC725Y + LSP3 | +| Scoped controllers and session keys | LSP6 Key Manager | +| Incoming asset behavior | LSP1 Universal Receiver | +| Gasless execution | LSP25 relay calls | +| Contract signature login | EIP-1271-compatible `isValidSignature` | +| Recovery | LSP11 Basic Social Recovery | +| Asset isolation | LSP9 Vault | +| Future function support | LSP17 Contract Extension | + +## Implementation path + +If you are comparing account-abstraction stacks, separate the decision into layers: + +1. **Account identity:** does the account expose standardized metadata? +2. **Permission model:** can app keys be scoped and revoked? +3. **Transaction sponsorship:** do you need paymasters, relayers, or both? +4. **Receiver behavior:** can the account react to incoming assets? +5. **Recovery:** can users recover from key loss? +6. **Extensibility:** can the account evolve without losing its address? + +Universal Profiles are strongest when those account-level capabilities are product requirements, not optional wallet features. + +## EIP-4337 vs Universal Profile + +| Question | EIP-4337 focus | Universal Profile focus | +| --- | --- | --- | +| How are transactions submitted? | UserOperations, bundlers, EntryPoint | Normal calls, Key Manager, relay calls | +| Who can sponsor gas? | Paymasters | Relayers through LSP25 flows | +| How are app permissions represented? | Account/module-specific | LSP6 permission data | +| How is identity represented? | Account-specific | ERC725Y + LSP3 profile metadata | +| How does receiving assets work? | Account-specific | LSP1 receiver hooks | +| Can both be combined? | Yes, depending on implementation | Yes, via extension/integration patterns | + +## When a pure EIP-4337 stack is still fine + +Use a pure EIP-4337 stack when bundler/paymaster infrastructure and UserOperation compatibility are the main requirements. Use Universal Profiles when the account itself needs standardized identity, metadata, permissions, receiver behavior, recovery, and relay execution. + +## Related guides + +- [Gasless transactions on EVM for smart accounts](./gasless-transactions-smart-accounts.mdx) +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [Smart account direct calls and call verification](./smart-account-call-verification-key-manager.mdx) +- [SIWE smart contract wallet login with EIP-1271](./siwe-smart-contract-wallets-eip1271.mdx) + +## Implementation docs + +- [EIP-4337 vs Universal Profile](./compare/eip4337-vs-universal-profile.mdx) +- [4337 extension guide](../learn/universal-profile/advanced-guides/4337-extension.md) +- [LSP0 ERC725Account](../standards/accounts/lsp0-erc725account.md) +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) diff --git a/docs/evm/erc1155-multi-asset-complexity.mdx b/docs/evm/erc1155-multi-asset-complexity.mdx new file mode 100644 index 0000000000..e6352390e1 --- /dev/null +++ b/docs/evm/erc1155-multi-asset-complexity.mdx @@ -0,0 +1,74 @@ +--- +title: 'ERC1155 Complexity: When Multi-Asset Contracts Become Hard to Index' +sidebar_label: ERC1155 Complexity +sidebar_position: 7 +description: ERC1155 can pack many asset types into one contract, but that can complicate indexing, metadata, and product semantics. Learn the LUKSO alternative. +keywords: + - ERC1155 complexity + - ERC1155 indexing + - multi asset contract EVM + - ERC1155 metadata problems +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC1155 complexity: when multi-asset contracts become hard to index + +ERC1155 is powerful because one contract can represent many assets. That same flexibility can become a product and indexing problem when different IDs behave like currencies, editions, collectibles, access passes, or game items. + +## Short answer + +ERC1155 reduces contract count but pushes more meaning into token IDs and off-chain interpretation. LUKSO solves this by making asset semantics explicit: use LSP7 for amount-based assets and LSP8 for identifiable assets, both with standardized metadata and receiver behavior. + +## Why this is hard with ERC1155 + +The contract can hold many asset types, but the standard does not tell apps what each ID means beyond balances and metadata URI conventions. + +Common pain points: + +- indexers need app-specific rules for token ID meaning; +- metadata can vary by ID in nonstandard ways; +- fungible, semi-fungible, and unique assets share one event stream; +- wallets may struggle to display asset intent clearly; +- migrations become harder when some IDs need different behavior. + +## What developers usually do instead + +Teams build subgraph mappings, metadata APIs, custom ID conventions, or separate registry contracts. Those add structure, but the structure lives outside the asset standard. + +## How LUKSO solves it + +LUKSO splits the decision by behavior. LSP7 covers amount-based assets. LSP8 covers individually identifiable assets. Both share metadata and receiver conventions, so apps can discover the asset type and read standardized data. + +| ERC1155 asset behavior | LUKSO fit | +| --- | --- | +| Divisible or amount-based balance | LSP7 | +| Individually identifiable item | LSP8 | +| Mixed collection with both | Split semantics across LSP7 and LSP8 | + +## When not to use this approach + +ERC1155 is still useful when contract consolidation matters more than explicit asset semantics. Use LSP7/LSP8 when wallet display, profile inventory, metadata, and asset behavior need to be unambiguous. + +## Implementation docs + +- [ERC1155 vs LSP7 and LSP8](./compare/erc1155-vs-lsp7-and-lsp8.mdx) +- [Choose between LSP7 and LSP8](../learn/digital-assets/choose-lsp7-vs-lsp8.md) +- [LSP7 Digital Asset](../standards/tokens/LSP7-Digital-Asset.md) +- [LSP8 Identifiable Digital Asset](../standards/tokens/LSP8-Identifiable-Digital-Asset.md) diff --git a/docs/evm/erc20-approvals-vs-operators.mdx b/docs/evm/erc20-approvals-vs-operators.mdx new file mode 100644 index 0000000000..b82cc0a5a0 --- /dev/null +++ b/docs/evm/erc20-approvals-vs-operators.mdx @@ -0,0 +1,176 @@ +--- +title: 'ERC20 Approval Risks vs Scoped Token Operators' +sidebar_label: ERC20 Approvals +sidebar_position: 4 +description: ERC20 allowances create unlimited approval, stale approval, phishing, and two-step UX problems. Learn how LSP7 operators and LSP6 permissions reduce approval risk. +keywords: + - ERC20 approvals + - ERC20 allowance risk + - unlimited token approvals + - approve race condition + - token operators EVM + - scoped token permissions +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC20 approval risks vs scoped token operators + +ERC20 approvals are one of the most common sources of EVM user confusion. A user approves a spender, the spender can pull tokens up to the allowance, and the app often asks for a large or unlimited approval to avoid repeated transactions. + +## Direct answer + +ERC20 approvals are risky because the allowance is broad: one spender can move up to an approved amount until the user changes it. That creates stale approvals, unlimited approvals, approval phishing, and confusing approve-then-action flows. + +LUKSO gives developers two layers that reduce the blast radius: + +- **LSP7 token operators** for asset-level authorization: an operator can transfer a defined amount of an LSP7 asset and can be notified through LSP1-compatible operator notification data. +- **LSP6 Key Manager permissions** for smart-account-level authorization: a controller can be limited to specific calls, contracts, metadata keys, or relay execution. + +Use LSP7 operators when the problem is token spending. Use LSP6 permissions when the problem is app, device, controller, or session access to a Universal Profile. Do not treat LSP7 operators as a complete replacement for account-level permissions: they are still amount-based token authorization. + +## Why this is hard with ERC20 + +The ERC20 allowance model is simple, but not expressive. The token contract stores `owner -> spender -> amount`. It does not explain why the spender is approved, which app requested it, what function will be called, when the approval should expire, or whether the spender should be limited to one protocol action. + +Common pain points: + +- **unlimited approvals:** users approve `uint256.max` to avoid repeat approvals; +- **stale approvals:** allowances remain after the user stops using an app; +- **approval phishing:** malicious spenders can ask for token access before the user sees the real action; +- **two-step UX:** users approve in one transaction and execute in another; +- **spender opacity:** users see an address, not a meaningful app-level permission; +- **race-condition footguns:** changing a non-zero allowance to another non-zero allowance can create edge cases if the spender acts between updates. + +## Common ERC20 approaches + +| Approach | What it improves | Tradeoff | +| --- | --- | --- | +| Approve exact amount | Reduces loss from one approval | Requires repeated approvals | +| Unlimited approval plus revoke UI | Improves UX until something goes wrong | Users must remember to revoke later | +| Permit-style signatures | Removes one approval transaction | Still grants spending rights to a spender | +| Router contracts | Hides protocol complexity | Concentrates approval power in one spender | +| Allowance dashboards | Helps review exposure | Does not make the primitive more scoped | + +## How LUKSO improves the pattern + +LSP7 keeps an amount-based authorization model, but makes the spender an explicit operator and adds LSP1-compatible notification data around authorization and transfers. That makes the token-level permission easier for apps and accounts to reason about than a silent ERC20 allowance. + +```solidity +function authorizeOperator( + address operator, + uint256 amount, + bytes calldata operatorNotificationData +) external; +``` + +For assets owned by a Universal Profile, LSP6 adds another layer: the controller that calls through the profile can be limited to specific permissions and allowed calls. + +| Use case | LUKSO pattern | +| --- | --- | +| Let a marketplace transfer up to 3 tokens | LSP7 operator for a bounded amount | +| Let an app call only one contract | LSP6 `CALL` plus allowed calls | +| Let a session key update only profile metadata | LSP6 `SETDATA` plus allowed data keys | +| Let a relayer submit signed actions | LSP6 relay permission plus LSP25 | +| Let a login key sign but not move assets | LSP6 `SIGN` only | + +## Operator allowance caveat + +LSP7 operators are intentionally close enough to token allowances that developers must still handle allowance-class edge cases. When changing an existing operator amount, do not casually overwrite a non-zero authorization with another non-zero authorization. The LSP7 contract docs recommend `increaseAllowance` and `decreaseAllowance` as mitigations for allowance double-spend and front-running issues. + +Choose the primitive based on what is being authorized: + +| Problem | Best LUKSO primitive | +| --- | --- | +| "Let this marketplace transfer up to this amount of this token." | LSP7 operator authorization | +| "Let this app call only selected contracts from my smart account." | LSP6 `CALL` plus allowed calls | +| "Let this session key edit only profile metadata." | LSP6 `SETDATA` plus allowed data keys | +| "Let this relayer submit signed profile actions." | LSP25 relay execution plus LSP6 relay permission | + +## Implementation sketch + +```ts +const marketplace = '0x1111111111111111111111111111111111111111'; +const amount = 3n * 10n ** 18n; +const context = new TextEncoder().encode('marketplace-listing'); + +await lsp7.authorizeOperator( + marketplace, + amount, + context, +); +``` + +For account-level delegation, grant the controller only the permissions and allowed calls the app needs. Do not use a broad controller permission when a bounded operator authorization is enough. + +## Production checklist + +- Prefer bounded authorizations over unlimited token access. +- Use `increaseAllowance` and `decreaseAllowance` when changing an existing LSP7 operator allowance. +- Make the spender or operator human-readable in the UI. +- Separate token-spending permission from account-control permission. +- Revoke operators or controllers when a listing, session, or integration ends. +- Use LSP6 allowed calls for app controllers that should only reach selected contracts. +- Use LSP6 allowed data keys for apps that should only edit selected ERC725Y metadata. + +## When ERC20 approvals are still fine + +Use ERC20 approvals when the app must integrate directly with ERC20-only routers, pools, vaults, and DeFi infrastructure. Use LSP7 operators and LSP6 permissions when scoped authorization is part of the product: marketplaces, memberships, creator tools, app sessions, delegated actions, or profile-owned assets. + +## Related guides + +- [Smart contract wallet permissions on EVM](./smart-contract-wallet-permissions.mdx) +- [How to add ERC20 transfer hooks on EVM](./erc20-transfer-hooks.mdx) +- [Gasless transactions on EVM for smart accounts](./gasless-transactions-smart-accounts.mdx) + +## Implementation docs + +- [LSP7 Digital Asset](../standards/tokens/LSP7-Digital-Asset.md) +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) +- [Grant permissions](../learn/universal-profile/key-manager/grant-permissions.md) +- [Get controller permissions](../learn/universal-profile/key-manager/get-controller-permissions.md) diff --git a/docs/evm/erc20-token-metadata-limitations.mdx b/docs/evm/erc20-token-metadata-limitations.mdx new file mode 100644 index 0000000000..cf629126a8 --- /dev/null +++ b/docs/evm/erc20-token-metadata-limitations.mdx @@ -0,0 +1,152 @@ +--- +title: 'ERC20 Metadata Limits and Extensible Token Metadata' +sidebar_label: ERC20 Metadata Limits +sidebar_position: 2 +description: ERC20 metadata is limited to basic display fields and off-chain conventions. Learn how ERC725Y and LSP4 make token metadata extensible on LUKSO. +keywords: + - ERC20 token metadata + - ERC20 metadata limitations + - token metadata EVM + - extensible token metadata +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC20 metadata limits and extensible token metadata + +ERC20 gives EVM apps a shared balance and transfer interface. It does not give teams a general metadata model. Once a token needs images, descriptions, creator links, issuer context, verification data, or post-deployment metadata updates, developers usually leave the ERC20 interface and rely on app-specific conventions. + +## Direct answer + +ERC20 metadata is hard to extend because the familiar `name()`, `symbol()`, and `decimals()` fields are display helpers, not a metadata storage model. They cannot describe images, links, attributes, issuer relationships, verification hashes, dynamic metadata, or future fields without custom methods or off-chain registries. + +LUKSO solves this by making metadata part of the asset contract. LSP7 assets use ERC725Y key-value storage and LSP4 metadata keys, so wallets, explorers, profiles, and apps can read richer token metadata through a shared interface. + +## Why this is hard with standard ERC20 + +Most ERC20 integrations expect only a contract address, balance methods, allowances, transfer methods, and sometimes basic display metadata. That is enough for simple fungible tokens, but it pushes product metadata into side channels. + +Common gaps: + +- **images and icons:** no standard way to attach token media to the contract; +- **descriptions and links:** websites, social links, docs, and external URLs live in token lists or app databases; +- **issuer context:** apps cannot reliably tell which profile, creator, or organization issued the token; +- **verification:** metadata JSON and media hashes are not part of the ERC20 interface; +- **updates:** adding new metadata fields after deployment means adding custom storage or external registries; +- **discovery:** wallets and indexers need per-app logic to know where richer metadata lives. + +## Common ERC20 metadata approaches + +| Approach | Why teams use it | Weakness | +| --- | --- | --- | +| Token lists | Easy for wallets and DeFi frontends | Centralized curation, slow updates, app-specific trust | +| Custom contract methods | Direct onchain reads | Every app must learn a new ABI | +| Marketplace or wallet registries | Good UX for known assets | Metadata is not portable across the ecosystem | +| `tokenURI`-like extension | Familiar from NFTs | Not part of ERC20 and not consistently indexed | +| Off-chain APIs | Flexible and cheap | Breaks if the API disappears or changes | + +## How LUKSO solves it + +LSP7 Digital Asset contracts use ERC725Y, a standardized key-value store. LSP4 Digital Asset Metadata defines common metadata keys on top of that store. Metadata is therefore discoverable from the asset contract instead of being hidden in a registry that only one app controls. + +```ts +import { ERC725YDataKeys } from '@lukso/lsp-smart-contracts'; + +const metadataKey = ERC725YDataKeys.LSP4.LSP4Metadata; +const metadata = await token.getData(metadataKey); +``` + +That `LSP4Metadata` value is a `VerifiableURI`: a reference to JSON metadata with verification information. The JSON can include names, descriptions, links, icons, images, attributes, and verification data for media files. + +## ERC20 metadata vs LSP7 metadata + +| Question | ERC20 pattern | LSP7/LSP4 pattern | +| --- | --- | --- | +| Where does basic metadata live? | Optional or common contract methods | Contract methods plus ERC725Y data keys | +| Where do images and links live? | Token lists, registries, or custom APIs | LSP4 metadata referenced from the asset contract | +| Can metadata be extended later? | Only with custom storage or external systems | Yes, with additional ERC725Y keys | +| Can apps discover the metadata location generically? | Not reliably | Yes, read standard LSP4 keys | +| Can metadata include verification data? | Not in the ERC20 interface | Yes, via VerifiableURI-based metadata | + +## What to implement + +If you are designing a new fungible asset and metadata matters to the product, use this model: + +1. Deploy an LSP7 Digital Asset. +2. Prepare LSP4 metadata JSON with images, icons, links, and attributes. +3. Encode the metadata reference as a VerifiableURI. +4. Store it under the `LSP4Metadata` ERC725Y data key. +5. Point apps, wallets, and indexers to `getData(LSP4Metadata)` as the canonical metadata source. + +For an existing ERC20, use a migration or wrapper only when the benefits outweigh compatibility costs. If the token's main surface is DeFi liquidity, a metadata sidecar may be less disruptive. If the token is a creator asset, membership token, social token, game asset, or profile-linked asset, LSP7 usually gives a cleaner metadata model. + +## Production checklist + +- Keep metadata visible and fetchable from the contract through standard data keys. +- Store media verification data so clients can detect changed or tampered assets. +- Make issuer and creator context explicit instead of relying only on branding in an image. +- Keep off-chain JSON durable by using decentralized storage or a stable storage strategy. +- Document which metadata fields your app treats as canonical and which are optional UI fields. + +## When ERC20 is still fine + +Keep plain ERC20 when your token mainly needs exchange, lending, AMM, or ERC20-only integration support and richer metadata is not a product requirement. Use LSP7 when the asset needs to be understood by wallets, profiles, marketplaces, creator tools, and social apps without every app maintaining its own token registry. + +## Related guides + +- [How to add ERC20 transfer hooks on EVM](./erc20-transfer-hooks.mdx) +- [Dynamic NFT metadata on EVM and ERC721 limits](./erc721-dynamic-metadata.mdx) +- [How to discover tokens owned or issued by an EVM account](./token-discovery-owned-issued-assets.mdx) + +## Implementation docs + +- [ERC725Y Generic Data Key/Value Store](../standards/erc725.md) +- [LSP4 Digital Asset Metadata](../standards/tokens/LSP4-Digital-Asset-Metadata.md) +- [LSP7 Digital Asset](../standards/tokens/LSP7-Digital-Asset.md) +- [erc725.js tutorial](../tools/dapps/erc725js/getting-started.md) +- [Create an LSP7 token](../learn/digital-assets/token/create-lsp7-token.md) +- [ERC20 to LSP7 migration checklist](./migrate/erc20-to-lsp7.mdx) diff --git a/docs/evm/erc20-transfer-hooks.mdx b/docs/evm/erc20-transfer-hooks.mdx new file mode 100644 index 0000000000..9fa98d5dff --- /dev/null +++ b/docs/evm/erc20-transfer-hooks.mdx @@ -0,0 +1,178 @@ +--- +title: 'How to Add ERC20 Transfer Hooks on EVM' +sidebar_label: ERC20 Transfer Hooks +sidebar_position: 3 +description: ERC20 transfers do not call the receiver. Compare common ERC20 approaches with LUKSO LSP1 and LSP7 receiver hooks for safe, programmable token transfers. +keywords: + - ERC20 transfer hooks + - token transfer hooks EVM + - ERC20 receiver hook + - tokenReceived EVM + - ERC20 transfer to smart contract + - ERC20 safe transfer alternative + - reject unwanted tokens smart wallet + - ERC20 transfer data parameter +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# How to add ERC20 transfer hooks on EVM + +ERC20 transfers update balances and emit an event. They do not call the recipient, so receiving contracts cannot accept, reject, forward, register, or inspect incoming ERC20 tokens during the transfer itself. + +## Direct answer + +You cannot add a universal receiver hook to an already-deployed ERC20 without changing the token, wrapping it, or adding app-specific infrastructure. ERC20 has no standard `tokenReceived` or receiver callback. + +For new assets, LUKSO solves the problem with two standards working together: + +- **LSP7 Digital Asset** adds receiver-aware token transfers. +- **LSP1 Universal Receiver** gives accounts and contracts one standard function for reacting to incoming assets, messages, and interactions. + +The practical result: instead of watching every token contract from an indexer and guessing what happened, a receiver can be notified by the asset transfer flow itself. + +## Why this is hard with standard ERC20 + +ERC20 treats the token contract as a balance registry. During a transfer, the token contract changes `from` and `to` balances, then emits `Transfer`. Nothing in the ERC20 interface asks the recipient whether it can handle the token. + +This causes common problems: + +- **lost or stuck tokens:** users can send tokens to contracts that have no recovery or accounting logic; +- **no receiver-side policy:** the recipient cannot reject spam assets or unknown tokens during the transfer; +- **indexer dependency:** apps often need off-chain services to infer what each account received; +- **non-portable hooks:** custom callbacks work only for tokens and apps that agreed on that callback; +- **harder automation:** forwarding, vaulting, compliance checks, or receipt registration become extra systems. + +## Common ERC20 approaches + +| Approach | What it solves | What still breaks | +| --- | --- | --- | +| Listen to `Transfer` events | Good for wallets, explorers, analytics, and accounting | The receiving contract still cannot react inside the transfer | +| Add a custom callback | Works when you control both token and receiver | Not portable across other ERC20 tokens | +| Wrap ERC20 into a new token | Can add policy around wrapped balances | Adds liquidity, UX, and bridge-like complexity | +| Use an app relayer | Can coordinate multi-step flows | The hook is app-specific, not asset-native | +| Use an ERC777-style model | Adds sender and receiver hooks | Compatibility and adoption are not universal | + +## How LUKSO solves it + +LSP7 transfers can notify LSP1-compatible senders, recipients, and operators. The asset transfer includes a `force` flag and arbitrary `data`, so an app can choose whether to allow a passive transfer or require receiver support. + +```solidity +function transfer( + address from, + address to, + uint256 amount, + bool force, + bytes calldata data +) external; +``` + +Use `force = false` when the recipient must support LSP1 Universal Receiver. Use `force = true` only when you intentionally want ERC20-like passive receiving by EOAs or contracts that do not implement LSP1. + +```ts +const data = new TextEncoder().encode('airdrop:membership-tier-2'); + +await lsp7.transfer( + sender, + recipient, + amount, + false, // require recipient hook support + data, +); +``` + +The receiver can then implement `universalReceiver(bytes32 typeId, bytes data)` directly or delegate the behavior to a Universal Receiver Delegate. That delegate can register the asset, reject unwanted assets, forward tokens to another address, or trigger product-specific logic. + +## ERC20 vs LUKSO transfer-hook model + +| Developer question | ERC20 answer | LUKSO answer | +| --- | --- | --- | +| Can the recipient react during transfer? | Not through ERC20 itself | Yes, through LSP1 receiver notification | +| Can the sender require a receiver hook? | No standard flag | Yes, set `force = false` | +| Can transfer context be passed with the token movement? | Not in standard ERC20 `transfer` | Yes, with the LSP7 `data` parameter | +| Can the same receiver function handle many asset types? | No, each standard has its own pattern | Yes, LSP1 is a unified receiver entry point | +| Can users reject or auto-forward assets? | Only with custom systems | Yes, in receiver or delegate logic | + +## Receiver policies: reject, register, forward + +For smart contract wallets, the useful question is not only "can the token transfer succeed?" It is "can the receiving account enforce a policy before accepting the asset?" + +LSP1 and LSP7 make those receiver policies part of the asset flow: + +| Builder need | ERC20 limitation | LUKSO implementation path | +| --- | --- | --- | +| Reject unwanted ERC20 tokens in a smart contract wallet | ERC20 gives the receiver no standard callback during `transfer` | Set `force = false` and let the receiver or Universal Receiver Delegate revert when the asset is not allowed | +| Block spam tokens before receiving | The wallet can hide spam after indexing, but the transfer already happened | Check the asset address, type ID, sender, and `data` inside the LSP1 receiver path | +| Validate a smart contract recipient before transfer | ERC20 has no equivalent to a required receiver acknowledgement | Require LSP1 support with `force = false` before the LSP7 transfer completes | +| Transfer a token with custom context data | ERC20 `transfer` and `transferFrom` do not include arbitrary transfer context | Pass app context in the LSP7 `bytes data` parameter | +| Auto-forward received tokens to a vault | Requires a custom listener, relayer, or post-transfer job | Forward from a Universal Receiver Delegate as part of the standardized receive flow | + +This is the practical "safe transfer" alternative for ERC20-style assets. Instead of every wallet or app inventing its own recipient-validation convention, the asset and receiver share one hook model. + +## Production checklist + +- Default to `force = false` for transfers into smart accounts or contracts that should acknowledge receipt. +- Treat receiver hooks as external calls: design for reverts and avoid assuming the receiver is cheap to execute. +- Put only useful context in `data`; do not rely on opaque data that only one backend can decode. +- Use Universal Receiver Delegates when the receiver behavior should be upgradeable without redeploying the account. +- Still index events for analytics and UI history; hooks solve receiver behavior, not every reporting need. + +## When ERC20 is still fine + +Plain ERC20 is still the right choice when the asset only needs passive balance transfers, broad DeFi compatibility, and no receiver-side behavior. Use LSP7 and LSP1 when receiving the asset is part of the product logic: vaulting, spam rejection, asset registration, automatic forwarding, memberships, game inventory, creator drops, or profile-linked assets. + +## Related guides + +- [ERC20 metadata limits and extensible token metadata](./erc20-token-metadata-limitations.mdx) +- [ERC20 approvals vs scoped token operators](./erc20-approvals-vs-operators.mdx) +- [Why NFT safe transfers still fail in EVM apps](./erc721-safe-transfer-problems.mdx) + +## Implementation docs + +- [LSP1 Universal Receiver](../standards/accounts/lsp1-universal-receiver.md) +- [LSP7 Digital Asset](../standards/tokens/LSP7-Digital-Asset.md) +- [Reject, register, or auto-forward received tokens](./recipes/receiver-hooks-and-auto-forwarding.mdx) +- [Create a token forwarder](../learn/universal-profile/universal-receiver/create-receiver-forwarder.md) diff --git a/docs/evm/erc721-dynamic-metadata.mdx b/docs/evm/erc721-dynamic-metadata.mdx new file mode 100644 index 0000000000..d84a1995c2 --- /dev/null +++ b/docs/evm/erc721-dynamic-metadata.mdx @@ -0,0 +1,198 @@ +--- +title: 'Dynamic NFT Metadata on EVM and ERC721 Limits' +sidebar_label: Dynamic NFT Metadata +sidebar_position: 5 +description: ERC721 dynamic metadata usually depends on tokenURI endpoints and marketplace refreshes. Learn how LSP8 and ERC725Y make NFT metadata extensible. +keywords: + - dynamic NFT metadata + - NFT metadata EVM + - ERC721 metadata limitations + - updatable NFT metadata + - ERC721 tokenURI alternative + - ERC721 bytes32 tokenId + - NFT tokenId smart contract address +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Dynamic NFT metadata on EVM and ERC721 limits + +NFT apps often need metadata that changes: game attributes, membership status, redeemable state, profile-linked assets, creator context, verifiable media, or unlockable utility. ERC721 gives the ecosystem a common ownership interface and a familiar `tokenURI` pattern, but it does not define a complete dynamic metadata model. + +## Direct answer + +Dynamic NFT metadata on ERC721 usually means one of two things: return a different `tokenURI`, or change the JSON served at the same URI. Both approaches can work, but the update rules, verification model, collection-level metadata, token-level metadata, and marketplace refresh behavior are mostly left to each project. + +LUKSO solves this with LSP8 Identifiable Digital Assets. LSP8 assets use ERC725Y storage and LSP4 metadata keys, including token-level metadata through `getDataForTokenId` and `setDataForTokenId`. Metadata can be standardized, updatable, and discoverable through the asset contract instead of only through marketplace-specific URI handling. + +## Why this is hard with standard ERC721 + +ERC721 standardizes ownership and token IDs. Metadata is typically resolved through `tokenURI(uint256 tokenId)`, but the standard does not define the shape, mutability, verification, update event strategy, or app-level semantics of the JSON behind that URI. + +That creates problems when: + +- **metadata changes after mint:** marketplaces may cache old JSON until a refresh is triggered; +- **the URI stays the same:** clients cannot know whether the JSON changed without refetching; +- **the URI changes:** clients need custom events or polling to learn the new location; +- **verification matters:** the contract does not standardize hashes for media or JSON content; +- **collection and token data diverge:** collection-level data and token-level data often use different conventions; +- **dynamic logic is opaque:** apps cannot know whether a change is expected, temporary, game-driven, or centralized. + +## Common ERC721 dynamic metadata approaches + +| Approach | Why teams use it | Weakness | +| --- | --- | --- | +| Mutable JSON at a stable `tokenURI` | Easy for dynamic art and games | Clients may cache stale data and cannot verify what changed | +| Changing `tokenURI` | Clear new metadata location | Requires custom update events and marketplace refresh support | +| Centralized metadata API | Maximum flexibility | Adds trust and availability risk | +| Onchain JSON or SVG | Durable and inspectable | Expensive and often limited for rich media | +| Custom metadata contract | More structure | Every app needs custom integration logic | + +## How LUKSO solves it + +LSP8 gives NFT collections a more explicit metadata surface: + +- `bytes32` token IDs can represent numbers, strings, addresses, hashes, or other identifiers. +- ERC725Y data keys attach extensible metadata to the collection contract. +- `setDataForTokenId(bytes32 tokenId, bytes32 key, bytes value)` stores token-level metadata. +- `getDataForTokenId(bytes32 tokenId, bytes32 key)` lets apps read token-level metadata directly. +- LSP4 metadata uses VerifiableURI values so metadata references can include verification data. + +```ts +import { ERC725YDataKeys } from '@lukso/lsp-smart-contracts'; + +const metadataKey = ERC725YDataKeys.LSP4.LSP4Metadata; + +const collectionMetadata = await lsp8.getData(metadataKey); +const tokenMetadata = await lsp8.getDataForTokenId(tokenId, metadataKey); +``` + +The important difference is not just "metadata can change." The difference is that the contract exposes where metadata lives and how to read it through a standard data model. + +## ERC721 vs LSP8 dynamic metadata + +| Developer question | ERC721 pattern | LSP8 pattern | +| --- | --- | --- | +| Where is token metadata found? | Usually `tokenURI(tokenId)` | ERC725Y data keys, including token-level keys | +| Can metadata be attached to the token ID itself? | Only through custom URI or custom storage | Yes, with `getDataForTokenId` and `setDataForTokenId` | +| Can the token ID represent an address or hash? | Usually `uint256` | Yes, `bytes32` token IDs support richer representations | +| Can metadata references include verification data? | Not in ERC721 itself | Yes, with LSP4 VerifiableURI metadata | +| Do all apps need custom metadata logic? | Often, yes | Less often, because the data-key model is shared | + +## Token ID limitations: uint256 vs bytes32 + +ERC721 token IDs are `uint256`. That is excellent for sequential collections, but less expressive when the token ID should represent a domain object that is not naturally a number. + +LSP8 token IDs are `bytes32`, which lets a collection use IDs that encode or derive from richer inputs while keeping a fixed-size onchain identifier. + +| Token ID need | ERC721 pattern | LSP8 pattern | +| --- | --- | --- | +| Sequential NFT IDs | `uint256` works directly | Encode the number as `bytes32` | +| NFT token ID as a smart contract address | Convert address semantics into a number or custom mapping | Left-pad the address into `bytes32` and use it as the token ID | +| Token ID from a string slug | Hash the string into a `uint256` or store a lookup table | Hash or encode the slug into `bytes32` | +| Token ID from external asset hash | Convert the hash into a number or store another mapping | Use the hash-compatible `bytes32` value directly | +| Profile-linked or contract-linked NFTs | Usually custom metadata or registry logic | Use a token ID that can represent the linked profile, contract, or claim | + +```ts +import { ethers } from 'ethers'; + +const addressTokenId = ethers.zeroPadValue(profileAddress, 32); +const slugTokenId = ethers.id('creator-drop:season-1:badge-042'); + +await lsp8.mint(profileAddress, addressTokenId, false, '0x'); +await lsp8.setDataForTokenId(slugTokenId, metadataKey, encodedMetadata); +``` + +Use this when the token ID itself should carry product meaning: account-bound credentials, contract-backed collectibles, creator editions, redeemable goods, dynamic game items, or NFTs that point to another onchain object. + +## What to implement + +For dynamic NFT metadata on LUKSO: + +1. Use LSP8 for the collection. +2. Use LSP4 metadata for collection-level fields. +3. Use `setDataForTokenId` for token-level metadata that changes per NFT. +4. Encode metadata references as VerifiableURI values. +5. Use stable update rules in your app so users know which fields can change and why. + +```ts +import { ERC725YDataKeys } from '@lukso/lsp-smart-contracts'; + +const key = ERC725YDataKeys.LSP4.LSP4Metadata; + +await lsp8.setDataForTokenId( + tokenId, + key, + encodedVerifiableUri, +); +``` + +## Production checklist + +- Decide which fields are immutable, issuer-controlled, user-controlled, or app-controlled. +- Store token-level metadata through token-level data keys instead of burying all variation in one off-chain endpoint. +- Add verification data for media and metadata references where authenticity matters. +- Keep UI cache invalidation explicit; dynamic metadata still needs product-level refresh behavior. +- Use `bytes32` token IDs deliberately: number, string, address, and hash formats all communicate different semantics. + +## When ERC721 is still fine + +Keep ERC721 when marketplace compatibility is the overriding requirement and metadata can remain static or simple. Use LSP8 when metadata is a core product surface: evolving game items, identity-linked NFTs, dynamic creator assets, membership credentials, redeemable goods, or NFTs whose token ID should represent something more meaningful than an incrementing number. + +## Related guides + +- [ERC20 metadata limits and extensible token metadata](./erc20-token-metadata-limitations.mdx) +- [Why NFT safe transfers still fail in EVM apps](./erc721-safe-transfer-problems.mdx) +- [ERC1155 complexity: when multi-asset contracts become hard to index](./erc1155-multi-asset-complexity.mdx) + +## Implementation docs + +- [LSP8 Identifiable Digital Asset](../standards/tokens/LSP8-Identifiable-Digital-Asset.md) +- [LSP4 Digital Asset Metadata](../standards/tokens/LSP4-Digital-Asset-Metadata.md) +- [Create an LSP8 NFT collection](../learn/digital-assets/nft/create-nft-collection-lsp8.md) +- [Set LSP8 NFT metadata](../learn/digital-assets/nft/set-nft-metadata.md) +- [Read LSP8 NFT metadata](../learn/digital-assets/nft/read-nft-metadata.md) +- [ERC721 to LSP8 migration checklist](./migrate/erc721-to-lsp8.mdx) diff --git a/docs/evm/erc721-safe-transfer-problems.mdx b/docs/evm/erc721-safe-transfer-problems.mdx new file mode 100644 index 0000000000..edb32420e3 --- /dev/null +++ b/docs/evm/erc721-safe-transfer-problems.mdx @@ -0,0 +1,155 @@ +--- +title: 'Why ERC721 Safe Transfers Still Fail in EVM Apps' +sidebar_label: NFT Safe Transfers +sidebar_position: 6 +description: ERC721 safeTransferFrom prevents some stuck NFTs, but it does not create a general asset receiver system. Learn how LSP1 and LSP8 solve receiver UX. +keywords: + - ERC721 safe transfer + - safeTransferFrom fails + - NFT safe transfers + - NFT receiver hook + - ERC721 receiver hook + - EVM asset receiving +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Why ERC721 safe transfers still fail in EVM apps + +ERC721 `safeTransferFrom` prevents some accidental transfers to contracts that cannot receive NFTs. It does not solve the broader product problem: accounts and contracts need one standard way to react to incoming assets, reject unwanted transfers, register ownership, forward assets, and handle future asset types. + +## Direct answer + +ERC721 safe transfers answer one narrow question: "does this contract implement `onERC721Received`?" They do not create a universal receiving system for ERC20, ERC721, ERC1155, future token standards, vaults, follows, or profile notifications. + +LUKSO solves the broader receiver problem with: + +- **LSP1 Universal Receiver**, one generic receiver function for standardized notifications. +- **LSP8 Identifiable Digital Asset**, an NFT-style asset standard with receiver-aware transfers. +- **`force` transfer behavior**, so apps can require receiver support instead of silently sending to passive addresses. + +## Why this is hard with ERC721 + +`safeTransferFrom` only works for ERC721-specific receiving. It protects against a class of stuck NFTs, but leaves many app-level questions unanswered. + +Common gaps: + +- **narrow receiver interface:** ERC721 uses `onERC721Received`; ERC1155 uses different receiver functions; +- **no generic account inventory:** receiving an NFT does not automatically register it in account metadata; +- **no shared policy layer:** spam rejection, forwarding, and allowlists remain custom; +- **no future-standard coverage:** a contract deployed today cannot know every future `onERCXXXReceived` function; +- **limited context:** receiver behavior depends on the asset standard and app-specific conventions. + +## Common ERC721 approaches + +| Approach | Why teams use it | Weakness | +| --- | --- | --- | +| Custom receiver contracts | Can enforce app-specific policy | Only works for that app or standard | +| Marketplace escrow | Centralizes receiving behavior | Adds custody and protocol assumptions | +| Indexer-based inventory | Good for UI and history | Does not let the receiver react during transfer | +| Allowlist and spam filters | Helps user safety | Usually off-chain or marketplace-specific | +| Recovery functions | Can rescue some stuck assets | Does not prevent bad receives upfront | + +## How LUKSO solves it + +LSP1 defines a single `universalReceiver` entry point. LSP7 and LSP8 assets can notify senders and recipients through that entry point. Universal Profiles can also use Universal Receiver Delegates to update asset inventory or run custom logic without hardcoding all behavior into the account. + +```solidity +function universalReceiver( + bytes32 typeId, + bytes calldata receivedData +) external payable returns (bytes memory); +``` + +LSP8 transfers also include `force` and `data`, letting the sender decide whether a recipient must understand LSP1. + +```solidity +function transfer( + address from, + address to, + bytes32 tokenId, + bool force, + bytes calldata data +) external; +``` + +Set `force = false` when the recipient should be required to support LSP1. Use `data` to pass app context that the receiver or delegate can decode. + +## ERC721 safe transfer vs LSP1 receiving + +| Developer question | ERC721 safe transfer | LUKSO LSP1/LSP8 | +| --- | --- | --- | +| Can a contract reject unsupported NFTs? | Yes, for ERC721 receiver checks | Yes, through LSP1 receiver behavior | +| Can the same receiver handle tokens, NFTs, vaults, and follows? | No | Yes, through `typeId`-based notifications | +| Can incoming assets be registered in account storage? | Not by ERC721 itself | Yes, through Universal Receiver Delegates and LSP5 | +| Can future asset types use the same receiver entry point? | No, each standard needs its own hook | Yes, LSP1 is generic | +| Can the sender require receiver support? | Only through ERC721 safe transfer semantics | Yes, through `force = false` | + +## Production checklist + +- Use `force = false` when sending assets into smart accounts or contracts that should acknowledge receipt. +- Put receiver logic in a delegate when behavior should be upgradeable. +- Keep an indexer for historical views, but do not rely on indexing as the only receiving policy. +- Treat unwanted assets as a product problem; define rejection or hiding behavior explicitly. +- Test transfers to EOAs, LSP1-compatible contracts, and contracts without receiver support. + +## When ERC721 safe transfers are still fine + +Use ERC721 safe transfers when ERC721 marketplace compatibility is the main requirement and receiver behavior is simple. Use LSP1 and LSP8 when the account should actively understand received assets, register them, reject spam, auto-forward assets, or support receiver behavior across asset types. + +## Related guides + +- [How to add ERC20 transfer hooks on EVM](./erc20-transfer-hooks.mdx) +- [Dynamic NFT metadata on EVM and ERC721 limits](./erc721-dynamic-metadata.mdx) +- [How to discover tokens owned or issued by an EVM account](./token-discovery-owned-issued-assets.mdx) + +## Implementation docs + +- [LSP1 Universal Receiver](../standards/accounts/lsp1-universal-receiver.md) +- [LSP1 Universal Receiver Delegate](../standards/accounts/lsp1-universal-receiver-delegate.md) +- [LSP8 Identifiable Digital Asset](../standards/tokens/LSP8-Identifiable-Digital-Asset.md) +- [Accept or reject assets](../learn/universal-profile/universal-receiver/accept-reject-assets.md) diff --git a/docs/evm/gasless-transactions-smart-accounts.mdx b/docs/evm/gasless-transactions-smart-accounts.mdx new file mode 100644 index 0000000000..8d795eff81 --- /dev/null +++ b/docs/evm/gasless-transactions-smart-accounts.mdx @@ -0,0 +1,182 @@ +--- +title: 'Gasless Transactions on EVM for Smart Accounts' +sidebar_label: Gasless Smart Accounts +sidebar_position: 11 +description: Gasless EVM transactions need signatures, nonces, replay protection, permissions, and relayers. Learn how LSP25 relay calls solve this for Universal Profiles. +keywords: + - gasless transactions smart accounts + - gasless transactions EVM + - sponsored transactions smart wallet + - web3 gasless onboarding +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Gasless transactions on EVM for smart accounts + +Gasless onboarding is a common EVM product requirement: users should be able to create an account, edit metadata, claim an asset, follow a profile, or perform a first action before holding the chain's native gas token. + +## Direct answer + +Gasless EVM transactions are not just "someone else pays gas." A secure flow needs a signed authorization, target payload, nonce, replay protection, optional expiration, permission checks, and a relayer that submits the transaction and pays the network fee. + +LUKSO standardizes this for Universal Profiles with LSP25 Execute Relay Call. A controller signs an LSP25 message, the Key Manager verifies it, and a relayer submits the execution. LUKSO also provides relayer APIs so apps can sponsor Universal Profile actions without building the entire relay stack first. + +## Why this is hard with generic EVM apps + +Without a standard relay flow, each app must answer security and product questions from scratch: + +- **what exactly does the user sign?** A vague message is dangerous; the payload must be bound to a specific action. +- **which account is authorizing?** For smart accounts, the signer may be a controller, not the account itself. +- **how are nonces tracked?** Replays must fail, and independent actions should not block each other. +- **when does the signature expire?** Open-ended signatures can be dangerous if leaked. +- **what can be sponsored?** A relayer needs policy controls before paying gas. +- **which permissions apply?** The smart account should verify that the controller can perform the requested action. +- **which chain and contract are targeted?** Signatures must not be replayable elsewhere. + +## Common gasless transaction approaches + +| Approach | Best fit | Tradeoff | +| --- | --- | --- | +| Custom meta-transactions | One app, one contract, limited actions | You own every security edge case | +| Centralized relayer API | Fast onboarding and sponsored UX | Needs strong policy and abuse controls | +| ERC-4337 paymaster flow | Account-abstraction stacks using UserOperations | More infrastructure and different account model | +| Permit-style signatures | Token approvals or narrowly scoped actions | Not a general account execution model | +| LSP25 relay calls | Universal Profile actions on LUKSO | Best when building around LSP6 permissions and UPs | + +## How LUKSO solves it + +LSP25 defines relay execution for LUKSO smart accounts. A controller signs an EIP-191 v0 message that binds together the relay target, LSP25 version, chain ID, nonce, validity timestamps, native value, and ABI payload. + +The relayer pays gas, but the Key Manager still checks the signature, nonce, permissions, and payload before executing. + +```json +{ + "address": "0x", + "transaction": { + "abi": "0x", + "signature": "0x", + "nonce": 0, + "validityTimestamps": "0x0" + } +} +``` + +The signed LSP25 message includes: + +| Field | Why it matters | +| --- | --- | +| LSP25 implementation address | Prevents using the signature against the wrong verifier | +| LSP25 version | Binds the signature to the standard version | +| Chain ID | Prevents cross-chain replay | +| Nonce | Prevents replay on the same account | +| Validity timestamps | Limits when the signature can be executed | +| Value | Binds native value transfer to the signature | +| Payload | Binds the exact action being sponsored | + +## Minimal flow + +1. The app builds the ABI payload the Universal Profile should execute. +2. The app gets the controller nonce from the Key Manager. +3. The controller signs the LSP25 relay-call message. +4. The app sends the profile address, payload, nonce, validity timestamps, and signature to a relayer. +5. The relayer submits `executeRelayCall`. +6. The Key Manager verifies the signature, permissions, nonce, and time bounds. +7. The Universal Profile executes the action without the user holding native gas. + +```ts +const nonce = await keyManager.getNonce(controllerAddress, 0); + +const relayRequest = { + address: universalProfileAddress, + transaction: { + abi: encodedPayload, + signature: lsp25Signature, + nonce, + validityTimestamps: '0x0', + }, +}; +``` + +## LSP25 vs ERC-4337 paymasters + +| Question | ERC-4337 paymaster flow | LSP25 relay-call flow | +| --- | --- | --- | +| Primary object | UserOperation | Signed relay-call payload | +| Execution account | ERC-4337 smart account | Universal Profile through Key Manager | +| Gas sponsor | Paymaster | Relayer | +| Permission model | Account implementation dependent | LSP6 permissions | +| Replay protection | UserOp nonce model | LSP25 nonce with channels | +| Best fit | Apps already built on ERC-4337 infra | Apps building around Universal Profiles | + +## Production checklist + +- Bind signatures to chain ID, verifier address, nonce, value, and exact payload. +- Use validity timestamps for actions that should expire. +- Separate relayer policy from account permissions: the relayer decides what it will pay for, the Key Manager decides what the controller can do. +- Rate-limit sponsored endpoints and monitor failed relay attempts. +- Use narrow controller permissions for app-specific actions instead of sponsoring broad account control. +- Prefer multi-channel nonces when independent actions should execute out of order. +- Surface sponsored actions clearly in the UI so users know what they are authorizing. + +## When not to use this approach + +If your product already depends on an ERC-4337 paymaster stack, keep that flow unless Universal Profiles are the account model you want to support. Use LSP25 when you are building with Universal Profiles and want a native relay-call model tied to LSP6 permissions. + +## Related guides + +- [SIWE smart contract wallet login with EIP-1271](./siwe-smart-contract-wallets-eip1271.mdx) +- [Smart contract wallet permissions on EVM](./smart-contract-wallet-permissions.mdx) +- [Account abstraction beyond EIP-4337](./eip4337-alternatives.mdx) + +## Implementation docs + +- [Transaction Relay API](../tools/apis/relayer-api.md) +- [Relayer User API](../tools/apis/relayer-user-api.md) +- [LSP25 Execute Relay Call](../standards/accounts/lsp25-execute-relay-call.md) +- [Gasless onboarding with LSP25](./migrate/gasless-onboarding-with-lsp25.mdx) diff --git a/docs/evm/migrate/_category_.yml b/docs/evm/migrate/_category_.yml new file mode 100644 index 0000000000..f811f67303 --- /dev/null +++ b/docs/evm/migrate/_category_.yml @@ -0,0 +1,3 @@ +label: 'Migrate' +position: 21 +collapsed: false diff --git a/docs/evm/migrate/erc20-to-lsp7.mdx b/docs/evm/migrate/erc20-to-lsp7.mdx new file mode 100644 index 0000000000..fa8ca38a6c --- /dev/null +++ b/docs/evm/migrate/erc20-to-lsp7.mdx @@ -0,0 +1,102 @@ +--- +title: 'How to Migrate ERC20 to LSP7 on LUKSO' +sidebar_label: ERC20 to LSP7 +sidebar_position: 2 +description: Step-by-step decision guide for migrating ERC20 tokens to LSP7 Digital Assets on LUKSO. +keywords: + - migrate ERC20 to LSP7 + - ERC20 LUKSO migration + - LSP7 token migration +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# How to migrate ERC20 to LSP7 + +Use this page as the migration checklist. The full implementation walkthrough remains in the existing [ERC20 to LSP7 tutorial](../../learn/migrate/migrate-erc20-to-lsp7.md). + +## Short answer + +Migrating ERC20 to LSP7 is not just a contract-name swap. Keep the ERC20 concepts your app still needs, then update the places where LSP7 intentionally differs: metadata, transfer shape, receiver behavior, and operator authorization. + +The main product gain is that an LSP7 asset can carry richer LSP4/ERC725Y metadata and can interact with LSP1-aware smart accounts during transfers. + +## Before you start + +- Inventory ERC20 functions your app uses: balances, approvals, transfers, metadata, minting, and burning. +- Identify contracts that assume ERC20 `transferFrom` and `allowance` semantics. +- Decide whether the token should be divisible. +- Decide whether transfers to non-LSP1 recipients should be allowed by default. +- Decide whether the token should behave like a normal fungible token or a non-divisible amount-based asset. +- Plan how legacy balances, allowances, and app integrations will be handled during migration. + +## Main deltas + +| ERC20 concern | LSP7 migration action | +| --- | --- | +| `name()` and `symbol()` | Store and read LSP4 metadata through ERC725Y keys. | +| `transferFrom(from, to, amount)` | Use `transfer(from, to, amount, force, data)`. | +| Allowances | Use LSP7 operator authorization patterns, and handle allowance-class edge cases carefully. | +| Recipient behavior | Test `force` and LSP1 Universal Receiver support. | +| Indexing | Listen to LSP7 transfer and operator events. | + +## Contract-level migration path + +1. Replace ERC20 inheritance with the LSP7 asset contract or preset that fits your mint/burn requirements. +2. Pass the LSP7 constructor parameters for name, symbol, owner, LSP4 token type, and divisibility. +3. Move metadata assumptions from ERC20-style methods into LSP4/ERC725Y metadata. +4. Replace ERC20 transfer calls with LSP7 transfer calls that include `force` and `data`. +5. Replace allowance UI with operator authorization UI, including revocation and amount changes. +6. Add receiver tests for EOAs, Universal Profiles, and contracts that do not implement LSP1. + +## Minimal transfer diff + +```solidity title="ERC20" +token.transferFrom(from, to, amount); +``` + +```solidity title="LSP7" +token.transfer(from, to, amount, true, ""); +``` + +Use `force: true` only when the app intentionally allows the transfer even if the recipient does not support LSP1 receiver behavior. Use `force: false` when the recipient must be able to react to the transfer. + +## Approval migration caveat + +LSP7 operators are amount-based authorizations. They are more explicit than generic ERC20 allowance UX and can include operator notification data, but they do not remove every allowance-class risk. When changing an existing operator amount, follow the LSP7 contract guidance around `increaseAllowance` and `decreaseAllowance`. + +For app or session access to a Universal Profile, use LSP6 permissions instead of trying to model account-level access as token spending. + +## Production checklist + +1. Port the contract inheritance to [`LSP7DigitalAsset`](../../contracts/contracts/LSP7DigitalAsset/LSP7DigitalAsset.md). +2. Encode LSP4 metadata and set it on deployment or post-deployment. +3. Update app transfer calls to include `force` and `data`. +4. Replace allowance-specific UI with operator authorization UI. +5. Add integration tests for EOAs, Universal Profiles, and contracts without LSP1. +6. Update indexers to consume LSP7 events. +7. Document how legacy ERC20 balances and integrations map to the new LSP7 asset. +8. Keep a revocation path for every operator the user can authorize. + +## Next docs + +- [ERC20 vs LSP7 comparison](../compare/erc20-vs-lsp7.mdx) +- [ERC20 approval risks vs scoped token operators](../erc20-approvals-vs-operators.mdx) +- [Full ERC20 to LSP7 tutorial](../../learn/migrate/migrate-erc20-to-lsp7.md) +- [Create an LSP7 token](../../learn/digital-assets/token/create-lsp7-token.md) +- [LSP7 standard](../../standards/tokens/LSP7-Digital-Asset.md) diff --git a/docs/evm/migrate/erc721-to-lsp8.mdx b/docs/evm/migrate/erc721-to-lsp8.mdx new file mode 100644 index 0000000000..2f519459a3 --- /dev/null +++ b/docs/evm/migrate/erc721-to-lsp8.mdx @@ -0,0 +1,108 @@ +--- +title: 'How to Migrate ERC721 to LSP8 on LUKSO' +sidebar_label: ERC721 to LSP8 +sidebar_position: 3 +description: Step-by-step decision guide for migrating ERC721 NFT collections to LSP8 on LUKSO. +keywords: + - migrate ERC721 to LSP8 + - ERC721 LUKSO migration + - LSP8 NFT migration +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# How to migrate ERC721 to LSP8 + +Use this page as the migration checklist. The full implementation walkthrough remains in the existing [ERC721 to LSP8 tutorial](../../learn/migrate/migrate-erc721-to-lsp8.md). + +## Short answer + +Migrating ERC721 to LSP8 changes three important assumptions: token IDs are `bytes32`, transfers include `force` and `data`, and metadata can use ERC725Y/LSP4/LSP8 data instead of depending only on `tokenURI`. + +The migration should preserve collector-facing identity while giving the new collection richer metadata and receiver behavior. + +## Before you start + +- Decide how legacy `uint256` token IDs map to LSP8 `bytes32` token IDs. +- Inventory marketplace, indexer, and app code that assumes ERC721 events or approval methods. +- Decide whether each transfer should allow non-LSP1 recipients. +- Plan where collection and token metadata will live. +- Decide whether token IDs should remain number-compatible, become address-based, or use another LSP8 token ID format. +- Decide how legacy `tokenURI` content maps to LSP4 collection metadata and token-level data. + +## Main deltas + +| ERC721 concern | LSP8 migration action | +| --- | --- | +| `uint256 tokenId` | Convert to `bytes32 tokenId`. | +| `tokenURI` | Use LSP4 / LSP8 metadata keys and VerifiableURI patterns. | +| `transferFrom` | Use `transfer(from, to, tokenId, force, data)`. | +| `approve` / `setApprovalForAll` | Use LSP8 operator authorization. | +| Safe receiving | Use LSP1 receiver checks through `force`. | + +## Token ID mapping + +If your ERC721 collection uses sequential numeric IDs, preserve them by converting each `uint256` to a `bytes32` value consistently. + +```ts +import { pad, toHex } from 'viem'; + +const legacyTokenId = 42n; +const lsp8TokenId = pad(toHex(legacyTokenId), { size: 32 }); +``` + +If the collection represents accounts, sub-collections, external objects, or hashes, choose the LSP8 token ID format that matches the product model instead of forcing every ID into a sequential number. + +## Metadata read example + +```ts +import { ERC725YDataKeys } from '@lukso/lsp-smart-contracts'; + +const metadataKey = ERC725YDataKeys.LSP4.LSP4Metadata; +const value = await nftContract.getData(metadataKey); +console.log(value); +``` + +## Migration flow + +1. Choose the LSP8 token ID format and publish the legacy-to-new ID mapping. +2. Deploy the LSP8 collection with the correct owner and metadata model. +3. Encode collection metadata with LSP4 metadata keys. +4. Mint or recreate token ownership using the mapped `bytes32` token IDs. +5. Move token-specific metadata into token-level data when the app needs dynamic or per-token metadata. +6. Update app transfer calls to include `force` and `data`. +7. Update indexers and marketplace adapters to consume LSP8 events and metadata. + +## Production checklist + +1. Port inheritance to [`LSP8IdentifiableDigitalAsset`](../../contracts/contracts/LSP8IdentifiableDigitalAsset/LSP8IdentifiableDigitalAsset.md). +2. Normalize or preserve token IDs as `bytes32`. +3. Encode collection metadata using LSP4 metadata keys. +4. Update transfer and operator calls. +5. Add tests for `force`, receiver support, and metadata reads. +6. Update indexers to consume LSP8 events. +7. Keep legacy ERC721 provenance visible in metadata or migration documentation. +8. Test wallet/profile receiving behavior for EOAs, Universal Profiles, and non-LSP1 contracts. + +## Next docs + +- [ERC721 vs LSP8 comparison](../compare/erc721-vs-lsp8.mdx) +- [Dynamic NFT metadata and bytes32 token IDs](../erc721-dynamic-metadata.mdx) +- [Full ERC721 to LSP8 tutorial](../../learn/migrate/migrate-erc721-to-lsp8.md) +- [Create an LSP8 NFT collection](../../learn/digital-assets/nft/create-nft-collection-lsp8.md) +- [LSP8 standard](../../standards/tokens/LSP8-Identifiable-Digital-Asset.md) diff --git a/docs/evm/migrate/gasless-onboarding-with-lsp25.mdx b/docs/evm/migrate/gasless-onboarding-with-lsp25.mdx new file mode 100644 index 0000000000..7fcc510234 --- /dev/null +++ b/docs/evm/migrate/gasless-onboarding-with-lsp25.mdx @@ -0,0 +1,79 @@ +--- +title: 'Gasless Onboarding with LSP25 Relay Calls' +sidebar_label: Gasless Onboarding +sidebar_position: 5 +description: Build gasless onboarding and sponsored transactions for Universal Profiles with LSP25 relay calls and the LUKSO relayer APIs. +keywords: + - gasless transactions EVM + - gasless onboarding web3 + - LSP25 relay call + - LUKSO relayer API +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Gasless onboarding with LSP25 relay calls + +LSP25 lets a controller authorize a Universal Profile action off-chain while a relayer submits the transaction and pays gas. This is the LUKSO-native path for gasless onboarding and sponsored account actions. + +## When to use it + +Use LSP25 relay calls when users should be able to: + +- edit profile metadata without holding LYX first; +- connect a controller and perform initial setup; +- accept an onboarding grant or app action; +- execute app flows where the product sponsors gas. + +## Relay flow + +1. Encode the Universal Profile payload, such as `setData`, `setDataBatch`, or `execute`. +2. Read the controller nonce from the Key Manager. +3. Build and sign the LSP25 message. +4. Submit the payload and signature to a relayer. +5. Track the returned transaction hash and surface errors clearly. + +## Minimal payload shape + +```json +{ + "address": "0x", + "transaction": { + "abi": "0x", + "signature": "0x", + "nonce": 0, + "validityTimestamps": "0x0" + } +} +``` + +## Migration checklist + +1. Decide which actions are eligible for sponsorship. +2. Ensure the signing controller has `EXECUTE_RELAY_CALL`. +3. Build server-side checks for quotas, replay risk, and rejected payloads. +4. Use validity timestamps when signatures should expire. +5. Instrument onboarding completion and relay error reasons. + +## Next docs + +- [Transaction Relay API](../../tools/apis/relayer-api.md) +- [Relayer User API](../../tools/apis/relayer-user-api.md) +- [Execute relay transactions](../../learn/universal-profile/key-manager/execute-relay-transactions.md) +- [LSP25 Execute Relay Call standard](../../standards/accounts/lsp25-execute-relay-call.md) diff --git a/docs/evm/migrate/index.mdx b/docs/evm/migrate/index.mdx new file mode 100644 index 0000000000..c2f672699c --- /dev/null +++ b/docs/evm/migrate/index.mdx @@ -0,0 +1,24 @@ +--- +title: 'Migrate EVM Apps to LUKSO' +sidebar_label: Migrate +sidebar_position: 1 +description: Migration recipes for moving ERC tokens, NFT collections, SIWE login, metadata, and gasless onboarding flows to LUKSO. +keywords: + - migrate to LUKSO + - EVM migration + - ERC to LSP migration + - Universal Profile migration +--- + +# Migrate EVM Apps to LUKSO + +Use these recipes to move a familiar EVM implementation toward LUKSO-native standards without losing the implementation detail in the main docs. + +| Existing flow | Migration recipe | +| --- | --- | +| ERC20 token | [ERC20 to LSP7](./erc20-to-lsp7.mdx) | +| ERC721 NFT collection | [ERC721 to LSP8](./erc721-to-lsp8.mdx) | +| EOA Sign-In with Ethereum | [SIWE to Universal Profile login](./siwe-to-universal-profile.mdx) | +| App-specific relayer or gasless onboarding | [Gasless onboarding with LSP25](./gasless-onboarding-with-lsp25.mdx) | + +For a broader overview of LUKSO standards, read [Migrate to LUKSO](../../learn/migrate/migrate-to-lukso.md). diff --git a/docs/evm/migrate/siwe-to-universal-profile.mdx b/docs/evm/migrate/siwe-to-universal-profile.mdx new file mode 100644 index 0000000000..7479df3dd6 --- /dev/null +++ b/docs/evm/migrate/siwe-to-universal-profile.mdx @@ -0,0 +1,104 @@ +--- +title: 'SIWE Smart Account Login | Universal Profile Migration' +sidebar_label: SIWE to Universal Profile +sidebar_position: 4 +description: Migrate Sign-In with Ethereum login flows to Universal Profiles using SIWE and EIP-1271 signature verification. +keywords: + - Sign-In with Ethereum smart account + - SIWE Universal Profile + - EIP-1271 login tutorial + - smart account login +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# SIWE smart account login with Universal Profiles + +EOA SIWE flows usually verify the recovered signer address. Universal Profile login verifies that the profile contract accepts the signature through EIP-1271-compatible `isValidSignature`. + +## Short answer + +Keep the SIWE message format, but change verification. For EOAs, the backend usually recovers the signer and compares it to the account address. For Universal Profiles, the backend should treat the Universal Profile address as the account and call `isValidSignature(hash, signature)` on that profile. + +That lets the user rotate controller keys while keeping the same profile identity. + +## What changes? + +| EOA SIWE | Universal Profile SIWE | +| --- | --- | +| User signs with the account key. | A controller signs for the Universal Profile. | +| Backend recovers the signer address. | Backend calls `isValidSignature(hash, signature)` on the profile. | +| Address is the user identity. | Universal Profile address is the user identity; controllers can rotate. | +| Permissions are app-specific. | LSP6 `SIGN` permission can scope signing authority. | + +## Login flow + +1. Ask the user to connect a Universal Profile. +2. Build the SIWE message with the Universal Profile address. +3. Have the active controller sign the prepared SIWE message. +4. Hash the prepared message in the backend. +5. Call `isValidSignature(hash, signature)` on the Universal Profile contract. +6. Create the app session for the Universal Profile address. + +## Verification sketch + +```ts +import { ethers } from 'ethers'; + +const hashedMessage = ethers.hashMessage(preparedSiweMessage); +const result = await universalProfile.isValidSignature( + hashedMessage, + signature, +); + +if (result !== '0x1626ba7e') { + throw new Error('Invalid Universal Profile signature'); +} +``` + +## Backend checklist + +- Store the session subject as the Universal Profile address. +- Keep the SIWE domain, nonce, issued-at time, and chain ID validation from your EOA flow. +- Verify through `isValidSignature` for Universal Profiles instead of relying only on recovered EOA address equality. +- Check controller permissions when your app depends on an LSP6 `SIGN` permission model. +- Revalidate signatures on the same chain/account context that the user connected with. + +## Common mistakes + +- Treating the controller address as the user account. +- Accepting an EOA-recovered signer when the session is meant to belong to a Universal Profile. +- Forgetting that controller keys can rotate while the Universal Profile remains stable. +- Mixing login permission with transaction or asset-transfer permission. + +## Migration checklist + +1. Keep the SIWE message format from EIP-4361. +2. Treat the Universal Profile address as the user account address. +3. Verify signatures with `isValidSignature` instead of only recovering an EOA. +4. Check whether the signing controller should have LSP6 `SIGN` permission. +5. Store sessions against the Universal Profile address, not a controller key. +6. Test expired nonce, wrong domain, wrong chain, and rotated-controller cases. + +## Next docs + +- [Log in a Universal Profile with SIWE](../../learn/universal-profile/connect-profile/siwe.md) +- [UniversalProfile contract overview](../../contracts/overview/UniversalProfile.md) +- [LSP0 ERC725Account standard](../../standards/accounts/lsp0-erc725account.md) +- [LSP6 Key Manager permissions](../../standards/access-control/lsp6-key-manager.md#permissions) diff --git a/docs/evm/modular-smart-accounts-vs-universal-profile.mdx b/docs/evm/modular-smart-accounts-vs-universal-profile.mdx new file mode 100644 index 0000000000..13efbf7ce0 --- /dev/null +++ b/docs/evm/modular-smart-accounts-vs-universal-profile.mdx @@ -0,0 +1,140 @@ +--- +title: 'Modular Smart Accounts vs Universal Profiles' +sidebar_label: Modular Accounts +sidebar_position: 15 +description: ERC-6900 and ERC-7579 focus on modular smart-account execution. Compare that with Universal Profiles, LSP6 permissions, LSP17 extensions, and LSP20 verification. +keywords: + - modular smart accounts + - ERC6900 smart accounts + - ERC7579 smart accounts + - smart account modules EVM + - Universal Profile vs modular account +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Modular smart accounts vs Universal Profiles + +EVM teams increasingly want smart accounts that can add modules, plugins, hooks, validators, and session-key logic. Modular account standards such as ERC-6900 and ERC-7579 address parts of that problem. + +## Direct answer + +Modular smart-account standards focus on how accounts install and execute modular logic. Universal Profiles focus on the account as a full user or organization identity: metadata, permissions, receiver hooks, relay calls, call verification, and extensibility. + +On LUKSO, the relevant pieces are: + +- **LSP6 Key Manager** for scoped controllers and permissions; +- **LSP17 Contract Extension** for adding function support after deployment; +- **LSP20 Call Verification** for delegating call validation; +- **LSP1 Universal Receiver** for incoming asset and notification hooks; +- **ERC725Y** for extensible account and asset metadata. + +## Why this is hard with modular accounts alone + +Modules make accounts flexible, but flexibility creates product questions: + +- which module can do which action? +- can users inspect installed permissions? +- how does the account expose profile metadata? +- how does the account receive and react to assets? +- can new functions be added without losing the account address? +- what is standardized across wallets versus specific to one module system? + +## Common EVM approaches + +| Approach | What it standardizes | What still needs product design | +| --- | --- | --- | +| ERC-6900-style plugins | Modular validation and execution concepts | User-facing identity and metadata | +| ERC-7579-style modules | Minimal modular account interfaces | Rich account semantics | +| Wallet-specific plugins | Fast feature delivery | Portability and inspection | +| Proxy upgrades | Contract evolution | Governance, upgrade risk, and UX | +| Custom session-key module | Scoped app flow | Cross-wallet permission meaning | + +## How LUKSO solves it + +Universal Profiles combine account semantics with extensibility primitives. They are not just execution shells. They are smart accounts with standardized metadata, permissions, receiver behavior, and call validation patterns. + +| Problem | LUKSO primitive | +| --- | --- | +| App and session permissions | LSP6 Key Manager | +| Add support for future functions | LSP17 Contract Extension | +| Verify calls through external logic | LSP20 Call Verification | +| Receive assets and notifications | LSP1 Universal Receiver | +| Store profile and app data | ERC725Y + LSP3/LSP2 | + +## Decision guide + +Use a modular account stack when your main requirement is compatibility with a specific account-abstraction module ecosystem. Use Universal Profiles when your app needs an account that is also a standardized identity, metadata container, permission system, and receiver. + +The two ideas are not mutually exclusive. LUKSO can integrate account-abstraction flows, but the core account primitives already solve many product problems that modular execution alone does not define. + +## Production checklist + +- Separate "can install modules" from "can safely explain permissions to users." +- Prefer standardized permission data where app controllers need user-visible scope. +- Use LSP17 only for well-reviewed extensions; extensions can impersonate the extendable contract in some contexts. +- Use LSP20 when verification rules should be separated from the main contract logic. +- Keep metadata and identity standards independent from any one execution module. + +## When modular account standards are still the better starting point + +Start with ERC-6900, ERC-7579, or another modular account stack when your app's main dependency is that ecosystem's module marketplace, bundler flow, or validator pattern. Start with Universal Profiles when identity, metadata, permissions, receiver hooks, and LUKSO asset standards are core to the app. + +## Related guides + +- [Account abstraction beyond EIP-4337](./eip4337-alternatives.mdx) +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [Smart account direct calls and call verification](./smart-account-call-verification-key-manager.mdx) +- [How to add functionality to contracts after deployment](./contract-extension-after-deployment.mdx) + +## Implementation docs + +- [LSP17 Contract Extension](../standards/accounts/lsp17-contract-extension.md) +- [LSP20 Call Verification](../standards/accounts/lsp20-call-verification.md) +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) +- [Extend profile functionalities](../learn/universal-profile/advanced-guides/extend-profile-functionalities.md) diff --git a/docs/evm/onchain-social-graph-for-evm-apps.mdx b/docs/evm/onchain-social-graph-for-evm-apps.mdx new file mode 100644 index 0000000000..dcf65d88b2 --- /dev/null +++ b/docs/evm/onchain-social-graph-for-evm-apps.mdx @@ -0,0 +1,144 @@ +--- +title: 'Onchain Social Graphs for EVM Apps' +sidebar_label: Onchain Social Graph +sidebar_position: 18 +description: Social apps need follows, followers, notifications, and profile identity. Learn how LSP26 and Universal Profiles create an onchain social graph on LUKSO. +keywords: + - onchain social graph EVM + - decentralized followers smart contracts + - web3 social graph + - onchain follow system + - LSP26 follower system +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Onchain social graphs for EVM apps + +Social apps need more than wallet addresses. They need profiles, follows, followers, notifications, app feeds, reputation signals, and interoperable identity. + +## Direct answer + +Most EVM social graphs are app-specific. One app's follow relationship is often stored in that app's contracts or database, and other apps need custom integrations to reuse it. + +LUKSO solves this with **LSP26 Follower System**, a standard contract for follows and followers between addresses. Universal Profiles provide the identity and metadata layer, while LSP1 notifications let profiles react when they are followed or unfollowed. + +## Why this is hard with generic EVM apps + +An address alone is not a social identity. Even when an app stores follows onchain, the graph may not be portable if the app uses custom events, custom indexing, or off-chain profile data. + +Where this gets hard: + +- no shared profile metadata for addresses; +- follow graphs are app-specific; +- apps need separate indexers for every social protocol; +- profiles cannot react to follow/unfollow events in a standard way; +- content, reputation, and curation data become fragmented. + +## Common approaches today + +| Approach | What it helps | What remains hard | +| --- | --- | --- | +| App-specific follow contract | Onchain state for one app | Portability across apps | +| Off-chain social database | Fast UX | Not user-owned or composable | +| NFT-based follows | Easy composability | Can be hard to adapt for unfollow and privacy | +| Protocol-specific indexer | Good feeds | Locks apps into one graph shape | +| Wallet address lists | Simple | No profile identity or notifications | + +## How LUKSO solves it + +LSP26 provides standard follow and unfollow functions, follower and following counts, paginated reads, and events. It also integrates with LSP1 so a profile can receive follow and unfollow notifications. + +```solidity +function follow(address addr) external; +function followBatch(address[] memory addresses) external; +function unfollow(address addr) external; +function unfollowBatch(address[] memory addresses) external; +function isFollowing(address follower, address addr) external view returns (bool); +``` + +The combination matters: + +- Universal Profile gives the address profile metadata. +- LSP26 gives the address a shared social graph. +- LSP1 gives profiles a way to react to follow/unfollow notifications. + +## Onchain social graph vs app-specific graph + +| Question | App-specific graph | LSP26 + Universal Profiles | +| --- | --- | --- | +| Can another app read follows? | Only if it integrates that app | Yes, through standard functions | +| Is profile metadata standardized? | Usually separate | Yes, through Universal Profiles | +| Can profiles receive notifications? | App-specific | Yes, through LSP1 integration | +| Can follows be batched? | Depends on app | Yes, through `followBatch` and `unfollowBatch` | +| Is the graph tied to one UI? | Often | No, the graph is contract-level | + +## Production checklist + +- Use Universal Profile metadata for user display, not raw addresses alone. +- Index LSP26 events for feed performance. +- Read contract state for canonical follow status. +- Handle spam follows and hidden accounts at the app layer. +- Use LSP1 notifications when profiles should react to social events. +- Keep follows separate from endorsements; following is not always trust. + +## When not to use an onchain graph + +Use off-chain follows when latency, privacy, moderation, or cost make onchain state unsuitable. Use LSP26 when portability, user-owned profile relationships, and composable social features matter. + +## Related guides + +- [Smart contract account metadata and EOA key risk](./smart-account-metadata-identity.mdx) +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [How to discover tokens owned or issued by an EVM account](./token-discovery-owned-issued-assets.mdx) + +## Implementation docs + +- [LSP26 Follower System](../standards/accounts/lsp26-follower-system.md) +- [LSP1 Universal Receiver](../standards/accounts/lsp1-universal-receiver.md) +- [Read Universal Profile data](../learn/universal-profile/metadata/read-profile-data.md) +- [Connect a Universal Profile](../learn/universal-profile/connect-profile/connect-up.md) diff --git a/docs/evm/overview.mdx b/docs/evm/overview.mdx new file mode 100644 index 0000000000..49be407d47 --- /dev/null +++ b/docs/evm/overview.mdx @@ -0,0 +1,234 @@ +--- +title: 'LUKSO for EVM Builders' +sidebar_label: Overview +sidebar_position: 1 +slug: /evm/ +description: Connect familiar ERC token, NFT, smart-account, gasless, recovery, vault, ownership, extension, and social graph design questions to LUKSO standards. +keywords: + - EVM builders + - ERC token limitations + - NFT metadata standards + - smart account standards + - gasless transactions EVM + - smart wallet recovery + - smart contract extension EVM +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# LUKSO for EVM Builders + +ERC standards made token and wallet interoperability possible, but production apps often need more than the original interfaces provide: richer metadata, safer receiving behavior, asset discovery, scoped permissions, gasless execution, account recovery, vault isolation, extensibility, and portable social data. + +LUKSO standards address these gaps with Universal Profiles, LSP digital assets, ERC725Y metadata, receiver hooks, Key Manager permissions, relay calls, Vaults, contract extensions, and onchain social graph primitives. + +Use this section to understand where common ERC and smart-account patterns start to break down, how LUKSO approaches the same design space, and which standards, contracts, recipes, and migration guides to use next. + +## Builder questions + +| Area | Builder question | LUKSO guide | +| --- | --- | --- | +| Tokens | How can token metadata go beyond name, symbol, and decimals? | [ERC20 metadata limits and extensible token metadata](./erc20-token-metadata-limitations.mdx) | +| Tokens | How can token transfers notify or validate receivers? | [How to add ERC20 transfer hooks on EVM](./erc20-transfer-hooks.mdx) | +| Tokens | How can smart accounts reject, register, or forward incoming assets? | [Reject, register, or auto-forward received tokens](./recipes/receiver-hooks-and-auto-forwarding.mdx) | +| Tokens | How can token spending permissions be scoped more clearly? | [ERC20 approval risks vs scoped token operators](./erc20-approvals-vs-operators.mdx) | +| Tokens | How should mixed ERC1155-style assets be modeled and indexed? | [ERC1155 complexity and multi-asset indexing](./erc1155-multi-asset-complexity.mdx) | +| NFTs | How can NFT metadata evolve after minting? | [Dynamic NFT metadata on EVM and ERC721 limits](./erc721-dynamic-metadata.mdx) | +| NFTs | How can token IDs represent addresses, hashes, or external objects? | [Dynamic NFT metadata and bytes32 token IDs](./erc721-dynamic-metadata.mdx#token-id-limitations-uint256-vs-bytes32) | +| NFTs | How should smart accounts handle incoming NFTs and other assets? | [Why ERC721 safe transfers still fail in EVM apps](./erc721-safe-transfer-problems.mdx) | +| Discovery | How can apps discover owned and issued assets from an account? | [How to discover tokens owned or issued by an EVM account](./token-discovery-owned-issued-assets.mdx) | +| Smart accounts | How can apps get scoped wallet and session permissions? | [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) | +| Smart accounts | How should SIWE work with contract accounts? | [SIWE smart contract wallet login with EIP-1271](./siwe-smart-contract-wallets-eip1271.mdx) | +| Smart accounts | What sits beyond the EIP-4337 transaction flow? | [Account abstraction beyond EIP-4337](./eip4337-alternatives.mdx) | +| Smart accounts | How can account identity survive EOA key rotation or loss? | [Smart contract account metadata and EOA key risk](./smart-account-metadata-identity.mdx) | +| Smart accounts | How can direct account calls stay usable with permission checks? | [Smart account direct calls and call verification](./smart-account-call-verification-key-manager.mdx) | +| Gasless UX | How can users act before holding native gas? | [Gasless transactions on EVM for smart accounts](./gasless-transactions-smart-accounts.mdx) | +| Recovery | How can lost keys be recovered without losing account identity? | [Social recovery for smart wallets on EVM](./social-recovery-smart-wallets.mdx) | +| Isolation | How can apps isolate assets from the main account? | [Asset isolation for smart accounts with Vaults](./smart-account-vaults-asset-isolation.mdx) | +| Extensibility | How can smart accounts support more than plugins? | [Modular smart accounts vs Universal Profiles](./modular-smart-accounts-vs-universal-profile.mdx) | +| Extensibility | How can contracts support future functions after deployment? | [How to add smart contract functions after deployment](./contract-extension-after-deployment.mdx) | +| Ownership | How can ownership transfer avoid one-step control mistakes? | [Safe smart contract ownership transfer on EVM](./safe-contract-ownership-transfer.mdx) | +| Social | How can follows and social data become portable account state? | [Onchain social graphs for EVM apps](./onchain-social-graph-for-evm-apps.mdx) | + +## Supporting docs + +Use the guides above when you are evaluating a design tradeoff. Use the supporting docs when you already know you want implementation detail: + +- [ERC and LSP comparisons](./compare/index.mdx) +- [Migration recipes](./migrate/index.mdx) +- [Use-case guides](./use-cases/index.mdx) +- [Reference maps](./reference/index.mdx) diff --git a/docs/evm/recipes/_category_.yml b/docs/evm/recipes/_category_.yml new file mode 100644 index 0000000000..a7c523350d --- /dev/null +++ b/docs/evm/recipes/_category_.yml @@ -0,0 +1,3 @@ +label: 'Recipes' +position: 23 +collapsed: false diff --git a/docs/evm/recipes/batch-transfer-tokens-and-nfts.mdx b/docs/evm/recipes/batch-transfer-tokens-and-nfts.mdx new file mode 100644 index 0000000000..7aa91f156a --- /dev/null +++ b/docs/evm/recipes/batch-transfer-tokens-and-nfts.mdx @@ -0,0 +1,119 @@ +--- +title: 'Batch Transfer Tokens and NFTs on LUKSO' +sidebar_label: Batch Transfers +sidebar_position: 4 +description: Batch transfer LSP7 tokens, LSP8 NFTs, and LYX through Universal Profile execution flows. +keywords: + - batch transfer NFTs + - batch transfer tokens LUKSO + - LSP7 batch transfer + - LSP8 batch transfer +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Batch transfer tokens and NFTs + +Batch transfers are useful for airdrops, membership distribution, creator rewards, and profile inventory management. On LUKSO, batching often happens through a Universal Profile executing several calls together. + +## Short answer + +Use asset-level batch transfer functions when one token contract can handle the whole action. Use Universal Profile `executeBatch` when the batch mixes LYX, LSP7 tokens, LSP8 NFTs, metadata updates, or calls across multiple contracts. + +## Common patterns + +| Pattern | Use it for | Typical execution path | +| --- | --- | --- | +| Batch several LSP7 transfers | Fungible token airdrops or reward distributions | LSP7 transfer batch or UP batch execution | +| Batch several LSP8 transfers | NFT distribution or collection management | LSP8 transfer batch or UP batch execution | +| Mix LYX and asset transfers | Checkout, rewards, or profile setup flows | Universal Profile `executeBatch` | +| Transfer and update metadata together | Creator or membership setup | Universal Profile `executeBatch` | +| Split one UX action across contracts | Marketplace or claim flows | Universal Profile `executeBatch` | + +## Why Universal Profile batching matters + +Many ERC flows batch by routing everything through one contract. That works, but it can make the router the center of approval power. With a Universal Profile, the account can authorize a sequence of calls directly. The user signs one account-level action, and the profile executes the calls in order. + +That is useful when the product action is not just "send token A": + +- send LYX to one address; +- send an LSP7 reward to several addresses; +- transfer an LSP8 collectible; +- update ERC725Y metadata; +- call a vault, marketplace, or membership contract. + +## Implementation shape + +```ts +import { OPERATION_TYPES } from '@lukso/lsp-smart-contracts'; + +const operationTypes = [ + OPERATION_TYPES.CALL, + OPERATION_TYPES.CALL, + OPERATION_TYPES.CALL, +]; + +const targets = [ + lsp7TokenAddress, + lsp8CollectionAddress, + universalProfileAddress, +]; + +const values = [0n, 0n, 0n]; +const calldatas = [ + lsp7TransferCalldata, + lsp8TransferCalldata, + metadataUpdateCalldata, +]; + +await universalProfile.executeBatch( + operationTypes, + targets, + values, + calldatas, +); +``` + +Build each calldata with the ABI for the target contract. For asset transfers, decide whether the recipient must support LSP1 receiver behavior or whether the transfer should use `force`. + +## Checklist + +1. Encode each asset transfer payload. +2. Decide whether each transfer should use `force: true` or require LSP1 support. +3. Simulate the batch against test addresses. +4. Display each target asset and recipient before signing. +5. Track partial failure risk if the app splits batching into multiple transactions. +6. Keep batches small enough for gas and UI review. +7. Show users the ordered list of calls before they sign. +8. Test both smart-contract recipients and EOA recipients. + +## Common mistakes + +- Hiding several unrelated actions behind a vague "batch transfer" label. +- Using `force: true` without explaining why the recipient does not need receiver support. +- Assuming every batch should go through a custom router. +- Forgetting that a failed call can revert the whole account-level batch. +- Not separating airdrop/admin tooling from user-signed account batches. + +## Next docs + +- [Transfer tokens and NFTs in batch](../../learn/digital-assets/transfer-batch.md) +- [Universal Profile batch transactions](../../learn/universal-profile/interactions/batch-transactions.md) +- [LSP7 standard](../../standards/tokens/LSP7-Digital-Asset.md) +- [LSP8 standard](../../standards/tokens/LSP8-Identifiable-Digital-Asset.md) diff --git a/docs/evm/recipes/deploy-universal-profile.mdx b/docs/evm/recipes/deploy-universal-profile.mdx new file mode 100644 index 0000000000..cd6b30bdfb --- /dev/null +++ b/docs/evm/recipes/deploy-universal-profile.mdx @@ -0,0 +1,108 @@ +--- +title: 'Deploy a Universal Profile on LUKSO' +sidebar_label: Deploy Universal Profile +sidebar_position: 2 +description: Choose between Relayer User API, LSP Factory, and contract-level deployment paths for Universal Profiles on LUKSO. +keywords: + - deploy Universal Profile + - create Universal Profile LUKSO + - LSP Factory tutorial + - Relayer User API +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Deploy a Universal Profile on LUKSO + +There are three common deployment paths. Choose based on how much control your app needs over deployment and setup. + +## Short answer + +Use the **Relayer User API** when your app wants API-based onboarding. Use **LSP Factory** when your app or script wants a JavaScript deployment helper. Use **LSP23 linked deployment** when you need low-level control, deterministic addresses, or custom post-deployment calldata. + +| Path | Use when | Start here | +| --- | --- | --- | +| Relayer User API | Your app wants API-based onboarding and profile registration. | [Relayer User API](../../tools/apis/relayer-user-api.md) | +| LSP Factory | You want a JavaScript deployment helper for profiles, tokens, and NFTs. | [lsp-factory.js](../../tools/dapps/lsp-factoryjs/getting-started.md) | +| Contract-level deployment | You need low-level control over linked deployments and post-deployment calldata. | [Deploy with LSP23](../../learn/universal-profile/advanced-guides/deploy-up-with-lsp23.md) | + +## Minimal LSP Factory flow + +```ts +import { LSPFactory } from '@lukso/lsp-factory.js'; + +const factory = new LSPFactory(publicClient, walletClient); + +const contracts = await factory.UniversalProfile.deploy({ + controllerAddresses: [account.address], +}); + +console.log(contracts.LSP0ERC725Account.address); +``` + +That deploys the Universal Profile account and the related account-management contracts used by the factory flow. + +## Deployment decisions + +| Decision | Why it matters | +| --- | --- | +| Initial controller | This key starts with account control and should be protected accordingly. | +| Permission model | A production profile usually needs more than one controller with different LSP6 permissions. | +| Metadata at creation | Setting LSP3 profile metadata early avoids a blank account during onboarding. | +| Gas sponsorship | Apps that create accounts for users often need relay or API-based onboarding. | +| Recovery path | Decide how the user regains control before the profile holds valuable assets. | + +## When to use each path + +### Relayer User API + +Use this when the product flow is "create or register a Universal Profile for a user" and the app wants backend/API coordination. It is the best fit for onboarding where the user should not manage every deployment transaction manually. + +### LSP Factory + +Use this in scripts, admin tools, examples, tests, and apps that want a high-level deployment helper. It is also the easiest path for developers who want to deploy profiles, LSP7 tokens, or LSP8 collections from JavaScript. + +### Contract-level deployment + +Use this when you need to reason about each deployed contract, compute addresses, prepare linked deployments, or customize initialization beyond the high-level helper path. + +## Production checklist + +1. Decide who owns the initial controller key. +2. Set LSP3 profile metadata during deployment when possible. +3. Register gasless relay support if the app sponsors onboarding. +4. Add or restrict controller permissions with LSP6. +5. Store the Universal Profile address as the user identity. +6. Separate admin, recovery, app, and session controllers. +7. Test account creation on the target network with the same relayer/factory path used in production. +8. Display the deployed Universal Profile and Key Manager addresses in admin tooling. + +## Common mistakes + +- Treating the controller address as the permanent user identity instead of the Universal Profile address. +- Giving every controller full permissions by default. +- Waiting until after launch to design recovery and revocation. +- Setting profile metadata off-chain only when the account should expose standard LSP3 metadata. +- Mixing account deployment and app permission grants without a test checklist. + +## Next docs + +- [Gasless onboarding with LSP25](../migrate/gasless-onboarding-with-lsp25.mdx) +- [Read and write profile metadata](./read-and-write-profile-metadata.mdx) +- [Grant controller permissions](../../learn/universal-profile/key-manager/grant-permissions.md) diff --git a/docs/evm/recipes/index.mdx b/docs/evm/recipes/index.mdx new file mode 100644 index 0000000000..7a8c75dc35 --- /dev/null +++ b/docs/evm/recipes/index.mdx @@ -0,0 +1,22 @@ +--- +title: 'LUKSO Developer Recipes' +sidebar_label: Recipes +sidebar_position: 1 +description: Practical recipes for deploying Universal Profiles, reading ERC725Y metadata, batch transfers, and receiver policies for incoming assets. +keywords: + - LUKSO recipes + - Universal Profile tutorial + - ERC725Y tutorial + - LUKSO developer guide +--- + +# LUKSO Developer Recipes + +Recipes are task-oriented paths into the implementation docs. Use them when you already know what you want to build and need the shortest route to the right standards, APIs, and examples. + +| Goal | Recipe | +| --- | --- | +| Deploy or register a Universal Profile | [Deploy a Universal Profile](./deploy-universal-profile.mdx) | +| Read and write profile metadata | [Read and write ERC725Y metadata](./read-and-write-profile-metadata.mdx) | +| Transfer several tokens or NFTs | [Batch transfer tokens and NFTs](./batch-transfer-tokens-and-nfts.mdx) | +| Reject, register, or forward incoming assets | [Reject, register, or auto-forward received tokens](./receiver-hooks-and-auto-forwarding.mdx) | diff --git a/docs/evm/recipes/read-and-write-profile-metadata.mdx b/docs/evm/recipes/read-and-write-profile-metadata.mdx new file mode 100644 index 0000000000..c85f3bb63a --- /dev/null +++ b/docs/evm/recipes/read-and-write-profile-metadata.mdx @@ -0,0 +1,75 @@ +--- +title: 'ERC725Y Tutorial | Read and Write Universal Profile Metadata' +sidebar_label: Read/Write Metadata +sidebar_position: 3 +description: Learn where Universal Profile metadata lives and which LUKSO docs to use for reading, editing, and encoding ERC725Y data. +keywords: + - ERC725Y tutorial + - onchain profile metadata + - Universal Profile metadata + - erc725.js tutorial +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC725Y tutorial: read and write Universal Profile metadata + +Universal Profiles store standardized metadata through ERC725Y data keys. Apps usually read and decode these keys with `erc725.js`, then update them with `setData` or `setDataBatch` through the Universal Profile. + +## Key concepts + +| Concept | Meaning | +| --- | --- | +| ERC725Y | Generic key-value storage on a smart contract. | +| LSP2 | JSON schema format for encoding and decoding data keys. | +| LSP3 | Profile metadata schema for Universal Profiles. | +| VerifiableURI | URI plus verification data for off-chain JSON or assets. | + +## Read example + +```ts +import ERC725 from '@erc725/erc725.js'; +import LSP3ProfileMetadata from '@erc725/erc725.js/schemas/LSP3ProfileMetadata.json'; + +const erc725 = new ERC725( + LSP3ProfileMetadata, + universalProfileAddress, + rpcUrl, + { ipfsGateway: 'https://api.universalprofile.cloud/ipfs/' }, +); + +const profile = await erc725.fetchData('LSP3Profile'); +console.log(profile.value); +``` + +## Write checklist + +1. Encode data with the correct LSP schema. +2. Check that the controller has `SETDATA` or a stricter allowed-data-key permission. +3. Prefer `setDataBatch` when changing several keys. +4. Use VerifiableURI for off-chain JSON and media. +5. Re-read the profile data after the transaction confirms. + +## Next docs + +- [Read profile data](../../learn/universal-profile/metadata/read-profile-data.md) +- [Edit profile data](../../learn/universal-profile/metadata/edit-profile.md) +- [Add custom data](../../learn/universal-profile/metadata/adding-custom-data.md) +- [erc725.js getting started](../../tools/dapps/erc725js/getting-started.md) +- [ERC725Y developer map](../reference/erc725y-developer-map.mdx) diff --git a/docs/evm/recipes/receiver-hooks-and-auto-forwarding.mdx b/docs/evm/recipes/receiver-hooks-and-auto-forwarding.mdx new file mode 100644 index 0000000000..9aa3821e20 --- /dev/null +++ b/docs/evm/recipes/receiver-hooks-and-auto-forwarding.mdx @@ -0,0 +1,148 @@ +--- +title: 'Reject, Register, or Auto-Forward Received Tokens' +sidebar_label: Receiver Hooks +sidebar_position: 5 +description: Use LSP1 Universal Receiver hooks to reject spam assets, auto-register received tokens, and forward tokens from a smart account to a vault. +keywords: + - reject unwanted ERC20 tokens smart wallet + - block spam tokens smart account + - auto forward received tokens to vault + - wallet auto register received assets + - Universal Receiver delegate +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Reject, register, or auto-forward received tokens + +Smart accounts often need a policy for incoming assets: reject unwanted tokens, block spam before the transfer completes, auto-register received assets onchain, or forward received tokens to a vault. + +## Direct answer + +ERC20 cannot do this generically because the recipient is not called during a standard ERC20 transfer. ERC721 and ERC1155 have receiver callbacks, but each standard uses its own callback shape and does not provide one unified receiver function for every asset and notification type. + +LUKSO solves the smart-wallet receiver-policy problem with: + +- **LSP1 Universal Receiver**, a shared receiver function for assets and notifications; +- **Universal Receiver Delegates**, contracts that implement reusable receive policies; +- **LSP5 Received Assets**, the account-side list of assets a profile received; +- **LSP9 Vaults**, isolated containers for assets that should not sit directly on the main profile. + +## Receiver policy patterns + +| Product requirement | Receiver behavior | LUKSO building block | +| --- | --- | --- | +| Reject unwanted tokens | Revert when the incoming asset, sender, or type ID is not allowed | LSP1 receiver or Universal Receiver Delegate | +| Block spam tokens before receiving | Keep an allowlist, blocklist, or issuer rule in the receiver delegate | LSP1 + LSP6-controlled delegate configuration | +| Auto-register received assets | Update the account's received-assets data when a supported asset arrives | LSP5 Received Assets | +| Auto-forward tokens to a vault | On receipt, transfer or route the asset into a vault account | LSP1 delegate + LSP9 Vault | +| Run app automation after receipt | Decode `data` and type ID, then trigger narrow product logic | LSP1 Universal Receiver | + +## What to implement + +1. Decide which assets and type IDs the smart account should accept. +2. Deploy or configure a Universal Receiver Delegate. +3. Store the delegate address on the Universal Profile or Vault. +4. Keep the delegate's actions narrow: accept/reject, register, forward, or emit product-specific events. +5. Use LSP6 permissions so only trusted controllers can change receiver delegates or asset policies. + +## Policy example + +```solidity +function universalReceiver( + bytes32 typeId, + bytes calldata receivedData +) external returns (bytes memory) { + if (!_isAllowedTypeId(typeId)) revert UnsupportedAssetNotification(typeId); + + (address asset, address from, bytes memory data) = _decode(receivedData); + + if (!_isAllowedAsset(asset) || _isBlockedSender(from)) { + revert AssetRejected(asset, from); + } + + _registerReceivedAsset(asset); + + if (_shouldForward(asset, data)) { + _forwardToVault(asset, data); + } + + return ''; +} +``` + +Keep the hook deterministic. Expensive discovery, analytics, and support workflows should still happen offchain from emitted events; the hook should enforce the minimum policy needed for the receiving account. + +## Production checklist + +- Treat receiver hooks as external-call surfaces and review reentrancy assumptions. +- Do not make a receiver delegate depend on a slow or unavailable offchain service. +- Keep spam filtering rules explainable to users. +- Separate critical custody from app-specific automation by forwarding assets into a Vault. +- Emit events for support tooling even when LSP5/LSP10 storage is updated. +- Test both `force = false` and `force = true` transfer paths for LSP7 and LSP8 assets. + +## Related guides + +- [How to add ERC20 transfer hooks on EVM](../erc20-transfer-hooks.mdx) +- [How to discover tokens owned or issued by an EVM account](../token-discovery-owned-issued-assets.mdx) +- [Asset isolation for smart accounts with Vaults](../smart-account-vaults-asset-isolation.mdx) + +## Implementation docs + +- [Create a token forwarder](../../learn/universal-profile/universal-receiver/create-receiver-forwarder.md) +- [Deploy a Universal Receiver delegate](../../learn/universal-profile/universal-receiver/deploy-universal-receiver.md) +- [Accept or reject assets](../../learn/universal-profile/universal-receiver/accept-reject-assets.md) +- [LSP1 Universal Receiver standard](../../standards/accounts/lsp1-universal-receiver.md) +- [LSP1 Universal Receiver Delegate standard](../../standards/accounts/lsp1-universal-receiver-delegate.md) +- [LSP9 Vault standard](../../standards/accounts/lsp9-vault.md) diff --git a/docs/evm/reference/_category_.yml b/docs/evm/reference/_category_.yml new file mode 100644 index 0000000000..e6e465908c --- /dev/null +++ b/docs/evm/reference/_category_.yml @@ -0,0 +1,3 @@ +label: 'Reference Maps' +position: 25 +collapsed: false diff --git a/docs/evm/reference/erc725y-developer-map.mdx b/docs/evm/reference/erc725y-developer-map.mdx new file mode 100644 index 0000000000..d503feed3c --- /dev/null +++ b/docs/evm/reference/erc725y-developer-map.mdx @@ -0,0 +1,121 @@ +--- +title: 'ERC725Y Developer Map for LUKSO' +sidebar_label: ERC725Y Developer Map +sidebar_position: 2 +description: Find the ERC725Y, LSP2, LSP3, LSP4, erc725.js, and lsp-utils docs needed to read and write LUKSO metadata. +keywords: + - ERC725Y tutorial + - ERC725Y developer map + - LUKSO metadata + - erc725.js + - LSP2 JSON schema +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# ERC725Y developer map for LUKSO + +ERC725Y is the generic key-value storage model used by Universal Profiles and LSP digital assets. Most LUKSO metadata work is a combination of ERC725Y storage, LSP2 JSON schemas, and tooling that encodes or decodes known keys. + +## Short answer + +ERC725Y is the storage layer. LSP2 defines how keys and values are described. LSP3, LSP4, LSP5, LSP6, and LSP12 define common data keys for profiles, assets, received assets, permissions, and issued assets. `erc725.js` and `lsp-utils` are the developer tools that keep apps from manually hashing, encoding, and decoding those keys. + +## Concept map + +| Concept | Docs | +| --- | --- | +| Generic key-value storage | [ERC725](../../standards/erc725.md) | +| JSON schema structure | [LSP2 JSON Schema](../../standards/metadata/lsp2-json-schema.md) | +| Universal Profile metadata | [LSP3 Profile Metadata](../../standards/metadata/lsp3-profile-metadata.md) | +| Digital asset metadata | [LSP4 Digital Asset Metadata](../../standards/tokens/LSP4-Digital-Asset-Metadata.md) | +| Received assets | [LSP5 Received Assets](../../standards/metadata/lsp5-received-assets.md) | +| Issued assets | [LSP12 Issued Assets](../../standards/metadata/lsp12-issued-assets.md) | +| Controller permissions | [LSP6 Key Manager](../../standards/access-control/lsp6-key-manager.md) | + +## Tool map + +| Task | Tool docs | +| --- | --- | +| Encode and decode ERC725Y data | [erc725.js getting started](../../tools/dapps/erc725js/getting-started.md) | +| Generate data keys and decode permissions | [lsp-utils getting started](../../tools/dapps/lsp-utils/getting-started.md) | +| Inspect live profile and asset data | [ERC725 Inspect](https://erc725-inspect.lukso.tech/?network=lukso+mainnet) | +| Use Solidity constants and data keys | [LSP smart contracts constants](../../tools/lsp-smart-contracts/constants.md) | + +## Common developer tasks + +| Task | Start with | +| --- | --- | +| Read a Universal Profile name, description, links, or avatar | LSP3 schema with `erc725.js` | +| Edit Universal Profile metadata | `encodeData` with LSP3 schema, then `setData`/`setDataBatch` | +| Read an asset's rich metadata | LSP4 schema and the asset contract address | +| Track assets received by a profile | `LSP5ReceivedAssets[]` | +| Track assets issued by a profile | `LSP12IssuedAssets[]` | +| Inspect a controller's permissions | `AddressPermissions:Permissions:
` | +| Restrict app metadata writes | `AddressPermissions:AllowedERC725YDataKeys:
` | + +## Read profile metadata + +```ts +import { ERC725 } from '@erc725/erc725.js'; +import profileSchema from '@erc725/erc725.js/schemas/LSP3ProfileMetadata.json'; + +const erc725 = new ERC725( + profileSchema, + universalProfileAddress, + 'https://rpc.mainnet.lukso.network', +); + +const profile = await erc725.getData('LSP3Profile'); +``` + +## Encode metadata writes + +```ts +import { ERC725 } from '@erc725/erc725.js'; +import profileSchema from '@erc725/erc725.js/schemas/LSP3ProfileMetadata.json'; + +const erc725 = new ERC725(profileSchema); + +const encoded = erc725.encodeData([ + { + keyName: 'LSP3Profile', + value: { + json: profileMetadata, + url: 'ipfs://QmProfileMetadata', + }, + }, +]); + +await universalProfile.setDataBatch(encoded.keys, encoded.values); +``` + +## What to avoid + +- Do not hard-code hashed data keys when a schema or helper can generate them. +- Do not treat ERC725Y as arbitrary app storage without a namespace or schema. +- Do not store large JSON blobs directly on-chain when a verifiable URI is the intended pattern. +- Do not mix profile metadata, asset metadata, permissions, and app-specific data without documenting which LSP key family owns each value. + +## Recipes + +- [Read and write Universal Profile metadata](../recipes/read-and-write-profile-metadata.mdx) +- [Deploy a Universal Profile](../recipes/deploy-universal-profile.mdx) +- [Smart account permissions](../use-cases/smart-account-permissions.mdx) +- [ERC20 metadata beyond name, symbol, decimals](../erc20-token-metadata-limitations.mdx) diff --git a/docs/evm/reference/index.mdx b/docs/evm/reference/index.mdx new file mode 100644 index 0000000000..3c36f9bf68 --- /dev/null +++ b/docs/evm/reference/index.mdx @@ -0,0 +1,19 @@ +--- +title: 'LUKSO Reference Maps' +sidebar_label: Reference Maps +sidebar_position: 1 +description: Map developer concepts like ERC725Y, metadata, hooks, permissions, and relay calls to the right LUKSO docs. +keywords: + - LUKSO reference map + - ERC725Y developer map + - LSP standards map + - LUKSO docs map +--- + +# LUKSO Reference Maps + +Reference maps connect developer concepts to the standards, contract APIs, tools, and tutorials that implement them. + +| Concept | Map | +| --- | --- | +| ERC725Y storage, schemas, profile metadata, and tooling | [ERC725Y developer map](./erc725y-developer-map.mdx) | diff --git a/docs/evm/safe-contract-ownership-transfer.mdx b/docs/evm/safe-contract-ownership-transfer.mdx new file mode 100644 index 0000000000..f40e38c0ce --- /dev/null +++ b/docs/evm/safe-contract-ownership-transfer.mdx @@ -0,0 +1,136 @@ +--- +title: 'Safe Smart Contract Ownership Transfer on EVM' +sidebar_label: Safe Ownership Transfer +sidebar_position: 17 +description: One-step contract ownership transfer and renounce flows can permanently break control. Learn how LSP14 makes ownership transfer and renounce safer. +keywords: + - safe contract ownership transfer + - Ownable two step transfer + - EIP173 ownership risk + - renounce ownership accidentally + - LSP14 Ownable2Step +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Safe smart contract ownership transfer on EVM + +Many contracts still use one-step ownership transfer or renounce flows. That is convenient, but a wrong address or accidental renounce can permanently break control over a contract. + +## Direct answer + +One-step ownership transfer is dangerous because the new owner does not need to prove it can actually control the destination address. One-step renounce is dangerous because a single bad transaction can remove the owner. + +LUKSO solves this with **LSP14 Ownable2Step**, an extension of EIP-173-style ownership that adds: + +- pending-owner acceptance before transfer completes; +- a delayed confirmation process for renouncing ownership; +- LSP1 notifications for ownership transfer events when owners are contracts. + +## Why this is hard with basic ownership + +Owner permissions are often powerful: upgrades, metadata control, minting, treasury actions, protocol parameters, and recovery hooks. A small address mistake can become a permanent governance problem. + +Common failure modes: + +- ownership is transferred to the wrong address; +- ownership is transferred to a contract that cannot accept or operate it; +- a private key is lost after ownership transfer; +- `renounceOwnership` is called accidentally; +- no receiving contract is notified that it must accept ownership. + +## Common approaches today + +| Approach | What it helps | What remains hard | +| --- | --- | --- | +| Manual address checks | Reduces typos | Still human-process dependent | +| Multisig owner | Stronger custody | Does not solve wrong recipient transfer alone | +| Ownable2Step variants | Adds acceptance | Not always paired with receiver notifications | +| Timelocks | Adds delay | Heavier governance flow | +| Avoid renounce | Prevents accidental lockout | Not always compatible with decentralization goals | + +## How LUKSO solves it + +LSP14 makes transfer and renounce deliberate. Transfer starts by setting a pending owner. The pending owner must call `acceptOwnership()` before control changes. + +Renounce is also staged, requiring time to pass and confirmation instead of completing immediately in one transaction. + +When owners are contracts that implement LSP1, ownership transfer hooks can notify current and new owners. + +## EIP-173-style ownership vs LSP14 + +| Question | One-step ownership | LSP14 Ownable2Step | +| --- | --- | --- | +| Can ownership move to a wrong address immediately? | Yes | Not complete until accepted | +| Can the new owner prove control? | Not required | Required through `acceptOwnership` | +| Can renounce happen in one accidental call? | Often yes | No, staged confirmation | +| Can contract owners receive notifications? | Not generally | Yes, through LSP1 hooks | + +## Production checklist + +- Use two-step transfer for contracts with meaningful owner powers. +- Require the pending owner to accept before treating ownership as migrated. +- Avoid one-step renounce for contracts with active users or assets. +- Notify contract owners through receiver hooks where possible. +- Test transfer to EOAs, Universal Profiles, and contracts that cannot accept ownership. +- Document who can initiate, accept, and cancel ownership flows. + +## When basic ownership is still fine + +Basic ownership may be acceptable for throwaway contracts, test deployments, or contracts where owner powers are intentionally temporary and low risk. Use LSP14 when ownership carries real authority or when contracts may be owned by smart accounts. + +## Related guides + +- [Social recovery for smart wallets on EVM](./social-recovery-smart-wallets.mdx) +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [Asset isolation for smart accounts with Vaults](./smart-account-vaults-asset-isolation.mdx) + +## Implementation docs + +- [LSP14 Ownable2Step](../standards/access-control/lsp14-ownable-2-step.md) +- [LSP14 contract implementation](../contracts/contracts/LSP14Ownable2Step/LSP14Ownable2Step.md) +- [LSP1 Universal Receiver](../standards/accounts/lsp1-universal-receiver.md) diff --git a/docs/evm/siwe-smart-contract-wallets-eip1271.mdx b/docs/evm/siwe-smart-contract-wallets-eip1271.mdx new file mode 100644 index 0000000000..30a2f7a39d --- /dev/null +++ b/docs/evm/siwe-smart-contract-wallets-eip1271.mdx @@ -0,0 +1,172 @@ +--- +title: 'SIWE Smart Contract Wallet Login with EIP-1271' +sidebar_label: SIWE + EIP-1271 +sidebar_position: 10 +description: SIWE is easy for EOAs but smart contract wallets need EIP-1271. Learn how to verify Universal Profile login without treating controller keys as users. +keywords: + - SIWE smart contract wallet + - EIP-1271 login + - Sign-In with Ethereum smart account + - smart wallet authentication +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# SIWE smart contract wallet login with EIP-1271 + +Sign-In with Ethereum is straightforward for EOAs: recover the signer from the signature and compare it to the address in the SIWE message. Smart contract wallets break that assumption because the account address is a contract and the signing key may be only one controller. + +## Direct answer + +For smart contract wallets, do not authenticate users by EOA recovery alone. Verify the SIWE message normally, then validate the signature against the smart account with EIP-1271 `isValidSignature(bytes32,bytes)`. + +For LUKSO, the user identity should be the Universal Profile address. A controller signs the SIWE message, and the app verifies that signature against the Universal Profile contract. If the controller has the right permission, the Universal Profile returns the EIP-1271 magic value `0x1626ba7e`. + +## Why this is hard with EOA-only SIWE + +EOA-only SIWE assumes one address is both the account and the signing key. Smart accounts separate those roles: + +- **the account is a contract:** `ecrecover` returns a controller, not the account; +- **controllers can rotate:** the long-term user identity should survive key changes; +- **permissions matter:** a key may be allowed to sign but not transfer assets, or vice versa; +- **multiple controllers can exist:** hardware wallet, session key, mobile key, or recovery key; +- **login storage can drift:** apps that store the controller address accidentally fragment one user into many accounts. + +## Common smart-wallet login mistakes + +| Mistake | Why it happens | Result | +| --- | --- | --- | +| Recover the EOA and store it as the user | Works for classic wallets | The smart account identity is lost | +| Skip EIP-1271 | Simpler backend code | Contract accounts fail login or are misidentified | +| Trust the connected account without SIWE checks | Faster prototype | Domain, nonce, chain, and replay checks are missing | +| Treat every controller as a separate user | Easy database model | One Universal Profile becomes many app accounts | +| Verify only on the client | Convenient UX | Server-side sessions can be forged or replayed | + +## How LUKSO solves it + +Universal Profiles implement EIP-1271 signature validation. The controller signs a SIWE message whose `address` is the Universal Profile address. Your app verifies the SIWE fields, hashes the prepared message, and calls `isValidSignature` on the Universal Profile. + +```ts +import { ethers } from 'ethers'; +import { SiweMessage } from 'siwe'; +import UniversalProfile from '@lukso/lsp-smart-contracts/artifacts/UniversalProfile.json'; + +const provider = new ethers.BrowserProvider(window.lukso); +const [profileAddress] = await provider.send('eth_requestAccounts', []); +const signer = await provider.getSigner(profileAddress); +const nonce = await fetch('/api/auth/nonce').then((response) => + response.text(), +); + +const message = new SiweMessage({ + domain: window.location.host, + address: profileAddress, + statement: 'Sign in with your Universal Profile.', + uri: window.location.origin, + version: '1', + chainId: Number((await provider.getNetwork()).chainId), + nonce, +}); + +const preparedMessage = message.prepareMessage(); +const signature = await signer.signMessage(preparedMessage); + +const universalProfile = new ethers.Contract( + profileAddress, + UniversalProfile.abi, + provider, +); + +const magicValue = await universalProfile.isValidSignature( + ethers.hashMessage(preparedMessage), + signature, +); + +if (magicValue !== '0x1626ba7e') { + throw new Error('Invalid smart-account signature'); +} +``` + +On the server, still validate the SIWE message fields before creating a session: domain, URI, nonce, expiration, not-before time, chain ID, and the expected Universal Profile address. EIP-1271 answers "is this signature valid for this smart account?" It does not replace normal SIWE replay and origin checks. + +## EOA SIWE vs smart-account SIWE + +| Step | EOA wallet | Universal Profile | +| --- | --- | --- | +| User identity stored by the app | EOA address | Universal Profile address | +| Who signs | Same EOA | Controller of the Universal Profile | +| Verification method | Recover signer and compare address | Call `isValidSignature` on the profile | +| Key rotation | Changes user identity unless handled manually | Controller can change while profile identity remains | +| Permission model | Private key has full account control | LSP6 permissions can scope controller capabilities | + +## Backend verification checklist + +- Parse the SIWE message and verify domain, URI, version, chain ID, nonce, and time bounds. +- Store the Universal Profile address as the user ID, not the controller address. +- Hash the exact prepared SIWE message that the user signed. +- Call `isValidSignature(hash, signature)` on the Universal Profile contract. +- Accept the login only when the return value is `0x1626ba7e`. +- Consume the nonce after a successful login so the signature cannot be replayed. +- Re-check permissions on sensitive actions; login permission is not the same as asset-transfer permission. + +## When EOA-only SIWE is still fine + +EOA recovery is enough when your product intentionally supports only EOAs and does not need smart accounts. Use EIP-1271 verification when contract accounts, smart wallets, Universal Profiles, delegated controllers, or key rotation should be first-class parts of the login model. + +## Related guides + +- [Smart contract wallet permissions on EVM](./smart-contract-wallet-permissions.mdx) +- [Gasless transactions on EVM for smart accounts](./gasless-transactions-smart-accounts.mdx) +- [Account abstraction beyond EIP-4337](./eip4337-alternatives.mdx) + +## Implementation docs + +- [Log in a Universal Profile with SIWE](../learn/universal-profile/connect-profile/siwe.md) +- [UniversalProfile contract overview](../contracts/overview/UniversalProfile.md) +- [SIWE to Universal Profile migration](./migrate/siwe-to-universal-profile.mdx) +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) diff --git a/docs/evm/smart-account-call-verification-key-manager.mdx b/docs/evm/smart-account-call-verification-key-manager.mdx new file mode 100644 index 0000000000..ffc8f108d9 --- /dev/null +++ b/docs/evm/smart-account-call-verification-key-manager.mdx @@ -0,0 +1,157 @@ +--- +title: 'Smart Account Direct Calls and Call Verification' +sidebar_label: Call Verification +sidebar_position: 15.5 +description: Smart accounts owned by a Key Manager can make direct calls harder to integrate. Learn how LSP20 call verification lets callers interact with a Universal Profile while LSP6 verifies permissions. +keywords: + - smart account owned by key manager direct calls + - delegate call verification to owner contract smart account + - smart account call verification + - LSP20 call verification + - Key Manager direct calls +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Smart account direct calls and call verification + +Smart contract accounts often put an owner contract in front of the account. That owner might be a module manager, validator, guardian system, multisig, or permission manager. The pattern is powerful, but it creates an integration challenge: apps may no longer be able to call the account directly. + +## Direct answer + +When a Universal Profile is owned by an LSP6 Key Manager, direct calls to the profile still need permission checks. Routing every interaction through the Key Manager makes app integration more complex because calls must be encoded for the owner contract instead of sent naturally to the profile. + +LUKSO solves this with LSP20 Call Verification embedded in LSP0 accounts. A caller can interact with the Universal Profile directly, while the profile delegates verification back to the Key Manager before execution. The Key Manager checks whether the caller has the right LSP6 permissions for the target function, value, and calldata. + +## Why this is hard with owner-gated smart accounts + +A common smart-account architecture looks like this: + +1. the account has protected functions; +2. the account owner is another contract; +3. the owner contract verifies permissions or signatures; +4. apps route calls through the owner contract. + +That creates friction: + +- dApps must understand the account owner's execution interface; +- ordinary contract calls become nested `execute` calls; +- calldata has to be wrapped, signed, or relayed through a manager; +- read-only permission checks are often custom; +- the account address is no longer the obvious integration target; +- wallet UX and contract UX drift apart. + +## Common approaches today + +| Approach | What it solves | What still breaks | +| --- | --- | --- | +| Route every call through the owner contract | Centralizes verification | Apps must integrate the owner interface, not the account interface | +| Add custom modifiers to the account | Keeps calls direct for known functions | Verification logic becomes account-specific and harder to evolve | +| Use wallet-specific modules | Adds flexibility | Permission meaning may not be portable across wallets | +| Use a relayer or backend | Hides routing complexity from users | Adds trust, infrastructure, and failure modes | +| Proxy or upgrade the account | Can change verification logic | Adds upgrade governance and proxy risk | + +## How LUKSO solves it + +Universal Profiles combine three standards: + +| Need | LUKSO primitive | +| --- | --- | +| Smart contract account | LSP0 ERC725Account | +| Multiple controllers and scoped permissions | LSP6 Key Manager | +| Delegate call verification to owner logic | LSP20 Call Verification | + +With LSP20, the Universal Profile can ask the verification contract whether a direct call should be allowed. In the Key Manager case, that verification uses the LSP6 permission data stored on the profile. + +```solidity +function lsp20VerifyCall( + address, + address targetContract, + address caller, + uint256 msgValue, + bytes calldata callData +) external returns (bytes4); +``` + +The important product outcome: apps can treat the Universal Profile as the account they call, while the Key Manager remains the permission engine. + +## Direct call flow + +| Step | What happens | +| --- | --- | +| 1 | A controller or dApp calls a function on the Universal Profile | +| 2 | The profile detects that the caller is not the Key Manager owner | +| 3 | LSP20 verification forwards call context to the Key Manager | +| 4 | The Key Manager checks LSP6 permissions, allowed calls, value, and calldata | +| 5 | The profile executes the call only if verification succeeds | +| 6 | Optional post-call verification can run through `lsp20VerifyCallResult` | + +This preserves the Universal Profile as the integration surface without giving up owner-level permission checks. + +## Production checklist + +- Keep permission scopes narrow with LSP6 `AllowedCalls` and `AllowedERC725YDataKeys`. +- Test both direct profile calls and calls routed through the Key Manager. +- Treat `lsp20VerifyCall` as authorization logic, not business logic. +- Use post-call verification only when the result or state delta genuinely needs checking. +- Make permission errors visible in app UI so users understand why a direct call failed. + +## Related guides + +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [Modular smart accounts vs Universal Profiles](./modular-smart-accounts-vs-universal-profile.mdx) +- [Account abstraction beyond EIP-4337](./eip4337-alternatives.mdx) +- [How to add smart contract functions after deployment](./contract-extension-after-deployment.mdx) + +## Implementation docs + +- [LSP20 Call Verification](../standards/accounts/lsp20-call-verification.md) +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) +- [LSP0 ERC725Account](../standards/accounts/lsp0-erc725account.md) +- [Grant permissions to a controller](../learn/universal-profile/key-manager/grant-permissions.md) +- [Interact with contracts through a Universal Profile](../learn/universal-profile/interactions/interact-with-contracts.md) +- [UniversalProfile contract reference](../contracts/overview/UniversalProfile.md) diff --git a/docs/evm/smart-account-metadata-identity.mdx b/docs/evm/smart-account-metadata-identity.mdx new file mode 100644 index 0000000000..7b3d726583 --- /dev/null +++ b/docs/evm/smart-account-metadata-identity.mdx @@ -0,0 +1,134 @@ +--- +title: 'Smart Contract Account Metadata and EOA Key Risk' +sidebar_label: Account Metadata +sidebar_position: 13 +description: EVM accounts need portable profile metadata, safer key management, and app-readable identity. Learn how Universal Profiles use ERC725Y and LSP3. +keywords: + - onchain profile metadata smart contract account + - smart contract account with profile data + - ERC725Y profile metadata + - EOA private key risk alternative + - smart account identity EVM +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Smart contract account metadata and EOA key risk + +EOAs are easy to support but weak as product accounts. A single private key controls everything, and the address itself has no standard place for profile data, links, images, app settings, or public identity. + +## Direct answer + +The EVM alternative to EOA-only accounts is a smart contract account. The missing piece is standard account metadata: most smart wallets improve control and recovery, but they do not all expose the same profile data model. + +LUKSO solves this with Universal Profiles. A Universal Profile is a smart contract account that stores standardized profile and app data through ERC725Y key-value storage and LSP3 Profile Metadata. + +## Why this matters for EVM apps + +Apps repeatedly rebuild the same identity layer: + +- profile name and avatar; +- verified links and tags; +- app-specific settings; +- owned and issued asset lists; +- session and controller permissions; +- recovery and ownership state; +- profile JSON stored on IPFS or another content-addressed system. + +Without a shared account metadata model, every app needs its own database, wallet profile integration, or profile NFT convention. That creates fragmented identity even when the same user address is connected. + +## EOA vs generic smart account vs Universal Profile + +| Account model | What works | What remains hard | +| --- | --- | --- | +| EOA | Broad wallet and dApp compatibility | One private key controls everything; no native metadata or scoped permissions | +| Generic smart contract wallet | Programmable control, recovery, modules, or gas abstraction | Metadata, profile display, and app-readable identity are wallet-specific | +| Universal Profile | Smart account control plus standardized ERC725Y profile data | Apps still need to choose which data keys they read and write | + +## How ERC725Y profile data works + +ERC725Y gives an account a generic key-value storage surface. LSP2 defines data-key conventions, and LSP3 defines profile metadata so apps can read the same account identity fields. + +```ts +import ERC725 from '@erc725/erc725.js'; +import LSP3ProfileSchema from '@erc725/erc725.js/schemas/LSP3ProfileMetadata.json'; + +const erc725 = new ERC725(LSP3ProfileSchema, profileAddress, provider); +const profile = await erc725.fetchData('LSP3Profile'); + +console.log(profile.value?.LSP3Profile?.name); +``` + +That makes the smart account itself the portable profile object. A social app, creator tool, marketplace, or wallet can inspect the same account identity without first joining a private profile database. + +## When this is the right answer + +Use a Universal Profile when: + +- the account represents a person, creator, organization, collection, or agent; +- identity should move across multiple apps; +- the account needs more than one controller or device; +- permissions and metadata should be inspectable onchain; +- users should not lose their identity when one key is compromised. + +Use a plain EOA when the app only needs basic signing and transaction submission. Use a wallet-specific profile when portability does not matter. + +## Related guides + +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [Social recovery for smart wallets on EVM](./social-recovery-smart-wallets.mdx) +- [SIWE smart contract wallet login with EIP-1271](./siwe-smart-contract-wallets-eip1271.mdx) +- [How to discover tokens owned or issued by an EVM account](./token-discovery-owned-issued-assets.mdx) + +## Implementation docs + +- [LSP3 Profile Metadata](../standards/metadata/lsp3-profile-metadata.md) +- [ERC725Y JSON Schema](../standards/metadata/lsp2-json-schema.md) +- [Read Universal Profile data](../learn/universal-profile/metadata/read-profile-data.md) +- [Edit Universal Profile data](../learn/universal-profile/metadata/edit-profile.md) +- [Add custom data to a Universal Profile](../learn/universal-profile/metadata/adding-custom-data.md) +- [erc725.js tutorial](../tools/dapps/erc725js/getting-started.md) diff --git a/docs/evm/smart-account-vaults-asset-isolation.mdx b/docs/evm/smart-account-vaults-asset-isolation.mdx new file mode 100644 index 0000000000..dec1cbe8c3 --- /dev/null +++ b/docs/evm/smart-account-vaults-asset-isolation.mdx @@ -0,0 +1,141 @@ +--- +title: 'Asset Isolation for Smart Accounts with Vaults' +sidebar_label: Vault Isolation +sidebar_position: 14 +description: Apps should not always touch a user's main smart account directly. Learn how LSP9 Vaults isolate assets and permissions for Universal Profiles. +keywords: + - smart account vault + - asset isolation smart wallet + - EVM vault permissions + - smart wallet app permissions + - LUKSO vault +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Asset isolation for smart accounts with vaults + +Many apps should not receive broad access to a user's main smart account. A game, marketplace, staking app, or protocol integration may need to manage a subset of assets without touching the user's profile metadata or unrelated holdings. + +## Direct answer + +Asset isolation means putting selected assets and protocol interactions in a separate account-like container. The main smart account controls the container, but app permissions can be scoped to that container instead of the entire account. + +LUKSO solves this with **LSP9 Vaults**. A Vault can hold assets, execute calls, store ERC725Y data, and receive LSP1 notifications. A Universal Profile can control the Vault while granting apps permissions that are limited to Vault interactions. + +## Why this is hard with generic EVM accounts + +If an app interacts directly with a user's main account, the permission boundary is often too broad. The app might only need access to one collection, one game inventory, or one staking position, but the account holds everything. + +Where this gets hard: + +- app keys receive broader access than the flow requires; +- protocol integrations touch the same account that holds identity metadata; +- users cannot separate high-risk and low-risk assets cleanly; +- revoking an app can leave assets or permissions scattered; +- receiver behavior for app-managed assets is custom. + +## Common approaches today + +| Approach | What it solves | What remains hard | +| --- | --- | --- | +| Separate wallet address | Clear separation | Poor UX and fragmented identity | +| Protocol escrow | App controls assets | Adds custody or protocol risk | +| Multisig subaccount | Strong controls | Heavy for normal user flows | +| App-specific smart account | Flexible | Not standardized across apps | +| Allowances only | Works for tokens | Does not isolate metadata or arbitrary calls | + +## How LUKSO solves it + +An LSP9 Vault is owned by a Universal Profile or another controller. It can hold assets and execute calls while remaining separate from the main profile. + +| Product need | LSP9/LSP6 pattern | +| --- | --- | +| Isolate game assets | Put them in a Vault controlled by the profile | +| Let an app manage only its Vault | Grant LSP6 allowed calls to the Vault | +| Keep identity metadata safe | Do not grant the app profile-wide `SETDATA` | +| React to incoming Vault assets | Use LSP1 receiver behavior on the Vault | +| Revoke one app | Remove the app controller without moving the main profile | + +## Implementation sketch + +```ts +const vaultAddress = '0x3333333333333333333333333333333333333333'; +const appController = '0x4444444444444444444444444444444444444444'; + +// Grant the app controller permission to interact only with the Vault path. +// In production, generate the allowed-call data with LUKSO permission helpers. +await universalProfile.setDataBatch(permissionKeys, permissionValues); +``` + +The exact permission shape depends on the app flow, but the design rule is stable: scope app controllers to the Vault when they do not need access to the whole Universal Profile. + +## Production checklist + +- Use one Vault per app, strategy, or risk domain when isolation matters. +- Keep valuable identity metadata on the Universal Profile, not in app-specific Vaults. +- Grant app controllers allowed calls to the Vault instead of broad profile permissions. +- Display which assets are in each Vault and which controllers can operate it. +- Revoke app controllers when a user disconnects an app. +- Keep indexers aware that assets may be held by Vaults controlled by profiles. + +## When not to use a Vault + +Do not add a Vault when the app only needs a read-only connection, SIWE login, or a one-time token transfer. Use LSP9 when the app needs ongoing access to a subset of assets or protocol calls and should not touch the main account directly. + +## Related guides + +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [How to discover tokens owned or issued by an EVM account](./token-discovery-owned-issued-assets.mdx) +- [ERC20 approval risks vs scoped token operators](./erc20-approvals-vs-operators.mdx) + +## Implementation docs + +- [LSP9 Vault](../standards/accounts/lsp9-vault.md) +- [Create a Vault](../learn/vault/create-a-vault.md) +- [Grant Vault permissions](../learn/vault/grant-vault-permissions.md) +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) diff --git a/docs/evm/smart-contract-wallet-permissions.mdx b/docs/evm/smart-contract-wallet-permissions.mdx new file mode 100644 index 0000000000..37ec822453 --- /dev/null +++ b/docs/evm/smart-contract-wallet-permissions.mdx @@ -0,0 +1,169 @@ +--- +title: 'Smart Contract Wallet Permissions and Session Keys on EVM' +sidebar_label: Wallet Permissions +sidebar_position: 9 +description: Smart wallets need session keys, app keys, device keys, revocation, and scoped calls. Learn how LSP6 standardizes Universal Profile permissions. +keywords: + - smart contract wallet permissions + - smart account permissions EVM + - session keys smart wallet + - scoped wallet permissions + - smart wallet app permissions + - smart wallet multiple controllers + - smart wallet multiple owners permissions + - limit smart wallet permissions specific calls +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Smart contract wallet permissions and session keys on EVM + +Smart accounts need more than "one private key controls everything." Production apps need login-only keys, device keys, session keys, scoped transaction rights, metadata-only access, relay permissions, and revocation. + +## Direct answer + +Most EVM smart-wallet permission systems are wallet-specific. Each account implementation can define its own modules, plugins, session keys, and access-control rules. + +LUKSO standardizes this at the account layer. Universal Profiles use the LSP6 Key Manager to assign controllers exact permissions and optional restrictions such as allowed calls and allowed ERC725Y data keys. + +The user-facing goal is simple: an app key should be able to do only the thing the user approved. + +For "smart wallet multiple owners permissions" problems, the LUKSO framing is more precise: one Universal Profile can have multiple controllers, and each controller can have its own permissions instead of every controller acting like a full owner. + +## Why this is hard with generic smart wallets + +Smart accounts make accounts programmable, but programmability alone does not make permissions portable or inspectable. Apps need to know what a key can do, users need to understand what they granted, and wallets need to display the risk. + +Common permission needs: + +- **login key:** sign SIWE messages but not move assets; +- **session key:** perform a temporary game or app action; +- **device key:** let a phone perform selected profile actions; +- **metadata editor:** update profile data but not transfer assets; +- **relayer key:** submit sponsored transactions without full account control; +- **recovery key:** help recover access without becoming the normal daily key. + +## Common approaches today + +| Approach | What it solves | What remains hard | +| --- | --- | --- | +| Custom session-key module | Good for one wallet or app | Not portable across account systems | +| Plugin architecture | Flexible extension model | Users still need clear permission semantics | +| App-specific backend permissions | Fast product iteration | Trust shifts off-chain | +| Multisig policy | Strong for teams | Heavy for consumer app sessions | +| Single owner key | Simple | All-or-nothing control | + +## How LUKSO solves it + +LSP6 Key Manager sits in front of a Universal Profile and verifies what each controller can do. A controller can be an EOA, another contract, an app key, a device key, or a service. + +| Controller use case | LSP6 permission pattern | +| --- | --- | +| Login only | `SIGN` | +| Edit selected profile fields | `SETDATA` plus allowed data keys | +| Call selected contracts | `CALL` plus allowed calls | +| Transfer value or assets broadly | Higher-risk execute permissions | +| Submit sponsored actions | Relay execution permission | +| Admin controller | Super permissions only when needed | + +Allowed calls and allowed data keys are the important scoping layer. They let a Universal Profile distinguish "this key can call anything" from "this key can only call this contract with this operation." + +## Implementation sketch + +```ts +import { ERC725YDataKeys, PERMISSIONS } from '@lukso/lsp-smart-contracts'; + +const controller = '0x2222222222222222222222222222222222222222'; + +const permissionsKey = + ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + + controller.slice(2); + +const permissionsValue = PERMISSIONS.SIGN; + +await universalProfile.setData(permissionsKey, permissionsValue); +``` + +For production use, generate permission data with the existing LUKSO helpers and set allowed calls or allowed data keys alongside the permission value. + +## LSP6 vs generic session keys + +| Question | Generic session-key pattern | LSP6 Universal Profile pattern | +| --- | --- | --- | +| Can users inspect the permission? | Wallet-specific | Yes, through LSP6 data keys | +| Can the key be metadata-only? | Depends on module | Yes, with allowed data keys | +| Can the key call only selected contracts? | Depends on module | Yes, with allowed calls | +| Can login be separated from asset transfer? | Depends on wallet | Yes, `SIGN` can be isolated | +| Can permissions survive key rotation? | Depends on implementation | Yes, controllers can be added/revoked | + +## Production checklist + +- Grant the least powerful permission that satisfies the app flow. +- Separate login, metadata edit, app call, and asset transfer permissions. +- Always pair broad call permissions with allowed calls when possible. +- Use allowed data keys for metadata editors. +- Display controller address, app name, expiration policy, and permission scope to users. +- Provide revocation UI for device, app, and session controllers. + +## When wallet-specific permissions are still fine + +Use a wallet-specific permission system when your app is tightly coupled to that account implementation. Use LSP6 when permissions should be standardized, inspectable, reusable across LUKSO apps, and meaningful beyond one wallet UI. + +## Related guides + +- [SIWE smart contract wallet login with EIP-1271](./siwe-smart-contract-wallets-eip1271.mdx) +- [Gasless transactions on EVM for smart accounts](./gasless-transactions-smart-accounts.mdx) +- [Smart account direct calls and call verification](./smart-account-call-verification-key-manager.mdx) +- [ERC20 approval risks vs scoped token operators](./erc20-approvals-vs-operators.mdx) + +## Implementation docs + +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) +- [Grant permissions](../learn/universal-profile/key-manager/grant-permissions.md) +- [Get controller permissions](../learn/universal-profile/key-manager/get-controller-permissions.md) +- [lsp-utils permission helpers](../tools/dapps/lsp-utils/LSP6KeyManager.md) diff --git a/docs/evm/social-recovery-smart-wallets.mdx b/docs/evm/social-recovery-smart-wallets.mdx new file mode 100644 index 0000000000..7b4d99c6ca --- /dev/null +++ b/docs/evm/social-recovery-smart-wallets.mdx @@ -0,0 +1,135 @@ +--- +title: 'Social Recovery for Smart Wallets on EVM' +sidebar_label: Social Recovery +sidebar_position: 13 +description: Lost keys are still a major smart-wallet failure mode. Learn how LUKSO supports guardian-based recovery for Universal Profiles with LSP11 and LSP6. +keywords: + - social recovery smart wallet + - smart wallet lost key recovery + - EVM account recovery + - guardian recovery smart account + - Universal Profile recovery +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Social recovery for smart wallets on EVM + +EOA accounts fail hard: lose the private key and the account is gone. Smart accounts can do better, but recovery is usually wallet-specific and difficult for apps to inspect. + +## Direct answer + +Smart-wallet social recovery lets selected guardians help restore account access after a key is lost. The hard part is doing that without giving guardians normal day-to-day control. + +LUKSO supports this pattern through: + +- **LSP11 Basic Social Recovery**, a guardian and threshold recovery contract; +- **LSP6 Key Manager**, the permission layer that can grant a recovered controller access to a Universal Profile; +- **Universal Profiles**, smart accounts whose controllers can be changed without changing the profile address. + +## Why this is hard with EOAs and generic smart wallets + +With EOAs, the private key is the account. Recovery usually means seed backups, custodians, or nothing. Smart accounts can add recovery, but every wallet can define different contracts, guardian rules, delays, and permission outcomes. + +Where this gets hard: + +- users lose a device or seed phrase; +- recovery guardians should not have normal spending power; +- apps need to know whether the account identity survives recovery; +- recovery thresholds and guardian changes need clear events; +- the new controller should receive explicit permissions, not blanket trust by accident. + +## Common recovery approaches + +| Approach | What it helps | What remains hard | +| --- | --- | --- | +| Seed phrase backup | Simple and common | Users still lose, leak, or mishandle it | +| Custodial recovery | Good support UX | Requires trust in a service | +| Multisig account | Strong for teams | Heavy for consumer recovery | +| Wallet-specific guardians | Better UX | Not portable or easily inspectable | +| Social login custody | Familiar onboarding | Often weakens self-custody | + +## How LUKSO solves it + +LSP11 Basic Social Recovery lets guardians vote for a new controller. When the recovery threshold is reached and the recovery process completes, the new controller can be granted permissions through the LSP6 Key Manager. + +The important product distinction is that recovery changes account control while preserving the Universal Profile address and metadata. The user can recover access without creating a new identity for every app. + +## Recovery model + +| Recovery question | LUKSO answer | +| --- | --- | +| Who helps recover? | Guardians configured in the recovery contract | +| How many guardians are needed? | A guardian threshold | +| What identity survives recovery? | The Universal Profile address | +| What does the new key receive? | LSP6 permissions | +| How can apps observe recovery? | Recovery contract events and profile permission data | + +## Production checklist + +- Keep guardians separate from daily app controllers. +- Use a threshold greater than one for valuable accounts. +- Make guardian changes visible and auditable in the UI. +- Grant the recovered controller only the intended LSP6 permissions. +- Rotate or revoke compromised controllers after recovery. +- Treat recovery as a security-critical flow with clear user confirmation. + +## When not to use social recovery + +Social recovery is not always needed for low-value accounts, temporary profiles, custodial products, or accounts already protected by institutional multisig. Use it when persistent identity and self-custody matter enough to justify guardian management. + +## Related guides + +- [Smart contract wallet permissions and session keys on EVM](./smart-contract-wallet-permissions.mdx) +- [SIWE smart contract wallet login with EIP-1271](./siwe-smart-contract-wallets-eip1271.mdx) +- [Account abstraction beyond EIP-4337](./eip4337-alternatives.mdx) + +## Implementation docs + +- [LSP11 Basic Social Recovery contract](../contracts/contracts/LSP11BasicSocialRecovery/LSP11BasicSocialRecovery.md) +- [LSP6 Key Manager](../standards/access-control/lsp6-key-manager.md) +- [LSP11 event reference](../standards/event-reference.md) +- [Grant permissions](../learn/universal-profile/key-manager/grant-permissions.md) diff --git a/docs/evm/token-discovery-owned-issued-assets.mdx b/docs/evm/token-discovery-owned-issued-assets.mdx new file mode 100644 index 0000000000..c33dc01bc5 --- /dev/null +++ b/docs/evm/token-discovery-owned-issued-assets.mdx @@ -0,0 +1,143 @@ +--- +title: 'How to Discover Tokens Owned or Issued by an EVM Account' +sidebar_label: Token Discovery +sidebar_position: 8 +description: EVM token discovery usually requires event indexing and third-party APIs. Learn how LSP5 and LSP12 make account asset inventory discoverable. +keywords: + - discover tokens owned by address + - EVM token discovery + - issued tokens by address + - account asset inventory + - token indexer alternative +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# How to discover tokens owned or issued by an EVM account + +On most EVM chains, an address does not expose a standard list of assets it owns or issued. Wallets and apps reconstruct that inventory from token events, marketplace APIs, token lists, and indexers. + +## Direct answer + +Standard EVM accounts do not have native asset inventory storage. An EOA is just an address, and a contract account only exposes what its implementation chooses to expose. + +LUKSO solves this for Universal Profiles with ERC725Y metadata keys: + +- **LSP5 Received Assets** records assets the profile received. +- **LSP12 Issued Assets** records assets the profile issued. +- **LSP1 Universal Receiver Delegates** can register incoming LSP7 and LSP8 assets automatically. + +This does not replace every indexer use case. It gives apps a standardized account-level inventory source. + +## Why this is hard with standard EVM accounts + +There is no universal method like `ownedTokens()` that works across ERC20, ERC721, ERC1155, smart accounts, and custom assets. The token contract usually knows its own balances or owners, but the account does not expose a canonical asset list. + +Where this gets hard: + +- **chain-wide event scans:** apps need to reconstruct ownership from transfer events; +- **mixed standards:** ERC20 balances, ERC721 ownership, and ERC1155 balances require different queries; +- **spam assets:** wallets need separate filtering and hiding logic; +- **issued assets:** finding what an account created is different from finding what it owns; +- **third-party dependency:** apps often outsource inventory to centralized APIs. + +## Common discovery approaches + +| Approach | Best fit | Limitation | +| --- | --- | --- | +| Full event indexer | Wallets, explorers, analytics | Expensive and complex to operate | +| Third-party token API | Fast app integration | External dependency and coverage gaps | +| Token lists | Known ERC20 assets | Does not prove ownership or issuance | +| Marketplace API | NFT display | Marketplace-specific visibility | +| Contract-specific queries | Known contracts | Does not discover unknown assets | + +## How LUKSO solves it + +Universal Profiles can expose asset lists through ERC725Y data. Apps can query profile data directly instead of starting from a chain-wide scan. + +```ts +const receivedAssets = await erc725.getData('LSP5ReceivedAssets[]'); +const issuedAssets = await erc725.getData('LSP12IssuedAssets[]'); +``` + +When LSP7 or LSP8 assets are transferred to a Universal Profile with the appropriate receiver delegate, the profile can update its received-assets metadata. When a profile issues an asset, it can register the issued asset under LSP12. + +## Indexer discovery vs profile inventory + +| Developer question | Indexer-first EVM | LUKSO profile inventory | +| --- | --- | --- | +| What assets does this account expose as received? | Reconstruct from events | Read LSP5 received assets | +| What assets did this account issue? | Infer from factories/events | Read LSP12 issued assets | +| Can the account hide or reject spam at receive time? | Usually off-chain filtering | LSP1 receiver logic can participate | +| Do I still need an indexer for history? | Yes | Yes, for history and analytics | +| Can apps query one profile-level source? | Not generally | Yes, through ERC725Y data keys | + +## Production checklist + +- Use LSP5/LSP12 for account-level inventory and issuer context. +- Keep an indexer for historical transfers, analytics, and non-LSP assets. +- Treat profile inventory as user-visible state; show when an asset was hidden, rejected, or manually removed. +- Use issuer metadata and verification signals to reduce spam and spoofed assets. +- Build fallbacks for assets that predate receiver registration or use non-LSP standards. + +## When indexers are still required + +Use indexers for global search, historical ownership, activity feeds, analytics, marketplace charts, and non-LSP assets. Use LSP5 and LSP12 when the app needs a standardized "what this profile owns or issued" answer. + +## Related guides + +- [Why ERC721 safe transfers still fail in EVM apps](./erc721-safe-transfer-problems.mdx) +- [ERC20 metadata limits and extensible token metadata](./erc20-token-metadata-limitations.mdx) +- [Smart contract account metadata and EOA key risk](./smart-account-metadata-identity.mdx) + +## Implementation docs + +- [LSP5 Received Assets](../standards/metadata/lsp5-received-assets.md) +- [LSP12 Issued Assets](../standards/metadata/lsp12-issued-assets.md) +- [Retrieve owned assets](../learn/universal-profile/metadata/retrieve-owned-assets.md) +- [Register LSP12 issued assets](../learn/universal-profile/metadata/register-lsp12-issued-assets.md) diff --git a/docs/evm/use-cases/_category_.yml b/docs/evm/use-cases/_category_.yml new file mode 100644 index 0000000000..463ceb0d47 --- /dev/null +++ b/docs/evm/use-cases/_category_.yml @@ -0,0 +1,3 @@ +label: 'Use Cases' +position: 24 +collapsed: false diff --git a/docs/evm/use-cases/index.mdx b/docs/evm/use-cases/index.mdx new file mode 100644 index 0000000000..94aeab270f --- /dev/null +++ b/docs/evm/use-cases/index.mdx @@ -0,0 +1,18 @@ +--- +title: 'LUKSO Use-Case Guides' +sidebar_label: Use Cases +sidebar_position: 1 +description: Build product flows with Universal Profile permissions, tokens, NFTs, and LUKSO standards. +keywords: + - LUKSO use cases + - smart account permissions EVM + - Universal Profile apps +--- + +# LUKSO Use-Case Guides + +Use-case pages connect standards to product decisions. They are shorter than tutorials and point to the docs needed to implement a real flow. + +| Product goal | Guide | +| --- | --- | +| Give apps scoped access to a smart account | [Smart account permissions](./smart-account-permissions.mdx) | diff --git a/docs/evm/use-cases/smart-account-permissions.mdx b/docs/evm/use-cases/smart-account-permissions.mdx new file mode 100644 index 0000000000..c8abe68439 --- /dev/null +++ b/docs/evm/use-cases/smart-account-permissions.mdx @@ -0,0 +1,120 @@ +--- +title: 'Smart Account Permissions on LUKSO' +sidebar_label: Smart Account Permissions +sidebar_position: 2 +description: Use LSP6 permissions to give apps, sessions, devices, and delegates scoped access to Universal Profiles. +keywords: + - smart contract wallet permissions EVM + - smart account permissions + - LSP6 permissions + - Universal Profile permissions +--- + +import StructuredData from '@site/src/components/StructuredData'; + + + +# Smart account permissions on LUKSO + +Universal Profiles use the LSP6 Key Manager to give controllers scoped permissions. A controller can be a device key, app key, session key, backend service, or delegate contract. + +## Short answer + +Use LSP6 when a smart account needs more than one all-powerful owner key. LSP6 lets a Universal Profile assign permissions to controllers and then narrow those permissions with allowed calls or allowed ERC725Y data keys. + +That gives EVM apps a standard way to model common account-access questions: + +- "Can this app sign login messages but not move assets?" +- "Can this session key call only one contract?" +- "Can this app edit only its own profile data?" +- "Can this relayer submit signed actions without becoming an admin?" + +## Common permission patterns + +| Pattern | Typical permissions | Required scope | +| --- | --- | --- | +| Login-only controller | `SIGN` | No asset-transfer permission | +| Profile editor | `SETDATA` | Allowed ERC725Y data keys | +| App session key | `CALL` | Allowed calls to selected contracts/functions | +| Relay signer | Relay execution permission | Nonce and relay-call validation | +| Receiver delegate | Narrow permission set for the delegate action | Usually tied to the receiver flow | +| Admin recovery/controller setup | `ADDCONTROLLER`, `EDITPERMISSIONS`, or higher permissions | Only for trusted admin or recovery flows | + +## Why generic session keys are not enough + +Many EVM apps implement session keys as an app-specific module or backend permission. That can work, but it usually does not create portable account semantics. Another wallet, indexer, or app may not understand what the key can do. + +LSP6 stores permission data on the Universal Profile through standard ERC725Y data keys. That means controllers and permissions can be inspected with LUKSO tooling instead of living only in app-specific state. + +| App need | Weak pattern | LSP6 pattern | +| --- | --- | --- | +| Login without asset control | Same key signs and spends | `SIGN`-only controller | +| Limited trading action | Broad wallet approval | `CALL` plus allowed calls | +| Metadata editor | Full `SETDATA` access | `SETDATA` plus allowed data keys | +| Gas-sponsored action | Backend executes as admin | Relay execution permission plus LSP25 | +| Revocation | App deletes a backend session | Profile removes or edits the controller permission | + +## Implementation shape + +Use the existing ERC725/LSP helpers to encode permissions and data keys. The sketch below shows the data model; production code should follow the full grant-permissions guide linked below. + +```ts +const controller = '0x2222222222222222222222222222222222222222'; + +const permissionsValue = erc725.encodePermissions({ + SIGN: true, +}); + +const encodedData = erc725.encodeData([ + { + keyName: 'AddressPermissions:Permissions:
', + dynamicKeyParts: controller, + value: permissionsValue, + }, +]); + +await universalProfile.setDataBatch(encodedData.keys, encodedData.values); +``` + +For a controller that can call contracts, also set `AddressPermissions:AllowedCalls:
`. For a controller that can edit metadata, also set `AddressPermissions:AllowedERC725YDataKeys:
`. + +## Design checklist + +1. Start from the smallest permission set. +2. Restrict allowed calls and allowed data keys whenever possible. +3. Separate login keys from transaction keys. +4. Show users which app or device controls which action. +5. Build revocation into account settings. +6. Avoid `SUPER_*` permissions for normal app sessions. +7. Test permission failure paths, not only successful calls. +8. Keep recovery/admin controllers separate from app controllers. + +## Common mistakes + +- Granting `CALL` without allowed calls when the app only needs one contract. +- Granting `SETDATA` without allowed data keys when the app only owns one metadata namespace. +- Reusing the same controller for login, asset movement, and admin changes. +- Forgetting that `EDITPERMISSIONS` can be extremely sensitive. +- Hiding controller state from users instead of exposing connected apps, devices, and sessions. + +## Next docs + +- [LSP6 Key Manager standard](../../standards/access-control/lsp6-key-manager.md) +- [Grant permissions](../../learn/universal-profile/key-manager/grant-permissions.md) +- [Get controller permissions](../../learn/universal-profile/key-manager/get-controller-permissions.md) +- [lsp-utils LSP6 helpers](../../tools/dapps/lsp-utils/LSP6KeyManager.md) +- [ERC725.js encode permissions](../../tools/dapps/erc725js/methods.md#encodepermissions) diff --git a/docusaurus.config.js b/docusaurus.config.js index 60e30101f7..ff83f688df 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -145,6 +145,12 @@ export default { label: 'Contracts', collapsible: true, }, + { + type: 'doc', + docId: 'evm/overview', + position: 'left', + label: 'EVM Builders', + }, { type: 'doc', docId: 'networks/mainnet/parameters', diff --git a/sidebars.js b/sidebars.js index 27337a51ea..7c8ee4910f 100644 --- a/sidebars.js +++ b/sidebars.js @@ -10,6 +10,7 @@ */ module.exports = { + evmSidebar: [{ type: 'autogenerated', dirName: 'evm' }], networksSidebar: [{ type: 'autogenerated', dirName: 'networks' }], standardsSidebar: [ 'standards/introduction', diff --git a/src/components/StructuredData/index.tsx b/src/components/StructuredData/index.tsx new file mode 100644 index 0000000000..7c5bd862b2 --- /dev/null +++ b/src/components/StructuredData/index.tsx @@ -0,0 +1,19 @@ +import Head from '@docusaurus/Head'; + +type StructuredDataProps = { + data: Record | Record[]; +}; + +export default function StructuredData({ data }: StructuredDataProps) { + const items = Array.isArray(data) ? data : [data]; + + return ( + + {items.map((item, index) => ( + + ))} + + ); +}