Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/learn/migrate/migrate-erc1155-to-lsp7-lsp8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
sidebar_label: '🎒 ERC1155 to LSP7 + LSP8'
sidebar_position: 4
description: 'Step-by-step guide to migrating an ERC1155 multi-asset contract to LSP7 and LSP8 on LUKSO by splitting the collection along fungible vs identifiable shape.'
---

# 🎒 Migrate ERC1155 to LSP7 + LSP8

ERC1155 packs fungible and identifiable token IDs into one contract, with the type signaled by convention in the ID bits. Migrating to LUKSO splits that surface along asset semantics instead: fungible IDs become an [**LSP7**](../../standards/tokens/LSP7-Digital-Asset.md) contract, identifiable IDs become an [**LSP8**](../../standards/tokens/LSP8-Identifiable-Digital-Asset.md) contract. Both dispatch transfer notifications through the same [LSP1](../../standards/accounts/lsp1-universal-receiver.md) `universalReceiver(typeId, data)` hook — receivers branch on `typeId`, not on a per-collection ID-bit convention.

:::info Estimate
Roughly a week for a typical game or edition contract. Effort scales with how much of your existing code assumed the ERC1155 type-bit decoding convention rather than reading semantics from the standard itself.
:::

## When to migrate

Migrate when downstream code needs to dispatch on asset shape — fungible vs. identifiable — by interface, without a per-collection decoding rule. Keep ERC1155 when atomic cross-ID batch transfers are a primitive your protocol depends on, or when per-ID semantics are baked into your contract's state machine in a way that resists splitting.

## Step 1 — classify every token ID

For each token ID (or ID range) in the existing ERC1155 contract, decide: is it **fungible** (interchangeable units, summable) or **identifiable** (each unit unique)? Fungible → LSP7. Identifiable → LSP8.

:::warning Each distinct fungible ID normally needs its own LSP7 contract
LSP7 has one `balanceOf(address)` per contract — there's no token-ID dimension to a balance. If ID 1 (5 units) and ID 2 (7 units) were two _different_ fungible assets in the ERC1155 contract, minting both into the same LSP7 contract collapses them into one indistinguishable balance of 12. Give each distinct fungible asset its own LSP7 deployment unless merging them into a single balance is actually what you want. A semi-fungible edition (e.g. 100 prints of the same piece, one ID) is the case where a single LSP7 contract with `decimals` set to `0` is correct — it was one fungible asset to begin with.
:::

Some ERC1155 collections signal fungible-vs-unique by convention in the high bits of the token ID — that's one pattern some contracts use, not a property ERC1155 itself defines. Don't assume it applies to a given contract without checking its actual minting logic.

## Step 2 — deploy the contracts

One LSP7 contract per distinct fungible asset in the old collection, one LSP8 contract for the identifiable inventory (a single LSP8 contract can hold many unique token IDs, since LSP8 balances are already per-ID). [LSP4](../../standards/tokens/LSP4-Digital-Asset-Metadata.md) metadata lives independently on each.

## Step 3 — port the holders

Snapshot ERC1155 balances per token ID, then cut the old contract off from further transfers **before or atomically with** minting the new balances — see Step 5 for how, since minting from a snapshot that the old contract can still transfer against leaves both the old and new balances simultaneously spendable. For each fungible ID, mint the equivalent amount into that ID's dedicated LSP7 contract — don't combine amounts from different fungible IDs into one contract's balance. For LSP8 IDs, mint `bytes32`-encoded token IDs into the LSP8 contract. This is typically one coordinated mint script per new contract, sometimes merkle-claimed for large holder sets.

## Step 4 — port the integrations

Every place that implemented `IERC1155Receiver` needs [LSP1](../../standards/accounts/lsp1-universal-receiver.md) `universalReceiver` support instead. The `typeId` on each call lets a receiver branch on "is this an LSP7 transfer?" vs. "is this an LSP8 transfer?" — one hook function handles both, rather than separate single/batch receiver interfaces.

## Step 5 — cut over from the old contract

Minting new balances from a snapshot only works if the old contract can no longer move value after that snapshot — otherwise a holder can still transfer, sell, or double-spend the old balance after already receiving the new one. **ERC-1155 defines no standard pause function**, so most existing deployments have no built-in way to freeze transfers; check your specific contract before assuming this step is available. Pick a cutover that matches what your contract actually supports:

- **Atomic pause + burn** — if your contract does have a pause/freeze modifier (common on OpenZeppelin-based deployments that added `Pausable`), pause it in the same transaction or block as the final snapshot, so nothing can transfer after the snapshot but before the new contracts go live.
- **Escrow-and-claim** — holders deposit their ERC1155 balance into an escrow or burn contract in exchange for the new LSP7/LSP8 tokens. This works even without a pause function, since it only relies on the standard `safeTransferFrom`, not owner-level control.

A delay-and-reconcile approach without one of the above isn't a fix — catching transfers that happened _during_ a window does nothing to stop the old balances from moving again _after_ the new claims go live, so both representations stay permanently spendable, not just spendable during the window. Whichever method you use, the old contract should end up paused or drained into escrow — a persistent, on-chain cutoff — not just watched during a delay period. Keep the contract deployed for reference; don't redeploy over it.

## Gotchas

- **Snapshot-then-mint without a hard cutoff is a double-spend risk** — if the old ERC1155 contract can still be transferred after the balances used for minting were read, a holder can spend both the old and new balance. Confirm your contract actually has a pause function before planning around one, and use escrow-and-claim if it doesn't (see Step 5) — a delay period alone isn't replay protection.
- Multiple contracts instead of one — one LSP7 deployment per distinct fungible asset, plus one LSP8 deployment for the identifiable inventory. Deployment cost and indexing surface scale with how many distinct fungible IDs the old contract actually held.
- Batch transfers are now per-standard and per-contract, not cross-standard — a batch can't mix LSP7 and LSP8 items, or items from two different LSP7 contracts, in one call.
- Holders with mixed ERC1155 IDs need a coordinated mint across every new contract that inherits part of their balance.
- Marketplaces that supported your ERC1155 contract won't automatically pick up the new LSP7 + LSP8 contracts — plan for marketplace re-listing.

## Verify the migration

- Every ERC1155 token ID is mapped to either an LSP7 amount (in that asset's own contract) or an LSP8 token ID — no two distinct fungible assets share one LSP7 balance.
- The old contract is actually unable to honor transfers against re-minted balances — paused or escrowed with a persistent on-chain cutoff, not just "planned to be ignored."
- Holder balances are reproduced identically across every new contract.
- Batch transfer behavior is tested per standard.
- LSP1 receivers correctly handle both LSP7 and LSP8 `typeId`s.

**Related reading:** [ERC1155 vs LSP7 + LSP8](../why-lukso/compare/erc1155-vs-lsp7-lsp8.md) · [ERC1155's complexity problem](../why-lukso/problems/erc1155-complexity.md)
63 changes: 63 additions & 0 deletions docs/learn/migrate/migrate-safe-to-universal-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
sidebar_label: '🔐 Safe to Universal Profile'
sidebar_position: 5
description: 'Step-by-step guide to migrating custody from a Gnosis Safe multisig to a LUKSO Universal Profile: mapping signers to LSP6 controller permissions.'
---

# 🔐 Migrate from Safe to Universal Profile

Moving custody from a Safe (Gnosis Safe) multisig to a [**Universal Profile**](../universal-profile/metadata/read-profile-data.md) maps the multisig-signer model onto [LSP6 Key Manager](../../standards/access-control/lsp6-key-manager.md) controllers. Each Safe owner becomes an LSP6 controller address with its own permission bitfield (`CALL`, `SETDATA`, `TRANSFERVALUE`, `ADDCONTROLLER`, `SIGN`, and more) — narrower and more expressive than a flat multisig owner list.

:::info Estimate
Roughly a day for a small Safe. Longer if the Safe custodies many distinct asset types, since each needs its own transfer step.
:::

## When to migrate

Migrate when per-controller permission scoping in a standard vocabulary — the LSP6 bitfield plus `AllowedCalls` and `AllowedERC725YDataKeys` — profile-native metadata storage, or [LSP25](../../standards/accounts/lsp25-execute-relay-call.md) relay execution through the Key Manager (no bundler or paymaster contract needed) is what you need. Stay on Safe when an m-of-n multisig threshold is the exact primitive your product encodes — that pattern doesn't map one-to-one onto LSP6 and needs an explicit recovery-contract layer to express instead.

## Step 1 — model the Safe as permissions

Safe's "3 of 5" threshold doesn't exist directly in LSP6 — controllers are individual, not aggregated by a vote. Two patterns work:

- **Recovery contract** — deploy a contract that enforces the threshold itself, and register that contract as a controller holding both `ADDCONTROLLER` and `EDITPERMISSIONS` on the profile. Both are required: `ADDCONTROLLER` lets it install a brand-new replacement controller (the one that's never held permissions before — the whole point of recovery), while `EDITPERMISSIONS` alone only lets it edit or remove a controller that already has some permission entry. Day-to-day controllers handle daily operations; the recovery contract handles ownership-level changes.
- **Single day-to-day controller + cold multisig** — flatten daily operations to one controller, and keep the Safe (or a new threshold contract) as the cold recovery layer behind it.

## Step 2 — deploy the Universal Profile

Deploy with the standard `lsp-factory.js` (or equivalent) deployment script. Set [LSP3](../../standards/metadata/lsp3-profile-metadata.md) profile metadata, then add controllers per the design from Step 1.

## Step 3 — transfer assets

For each asset class, the Safe is always the one initiating the transfer via `Safe.execTransaction`:

- **Native LYX** — `Safe.execTransaction(profileAddress, value, "0x", ...)`, sending value directly to the profile's address. There's no separate call needed on the profile side to receive a plain LYX transfer.
- **ERC20 tokens moving to LSP7** — the Safe calls `token.transfer(profile, balance)` on the old ERC20 contract (unchanged ERC20 syntax) if you're sweeping the legacy token, or the LSP7 contract's own `transfer(from, to, amount, force, data)` if you're moving an already-migrated LSP7 balance — LSP7's `transfer` is a 5-argument function, not ERC20's 2-argument one.
- **NFTs** — `token.safeTransferFrom(safe, profile, id)` for existing ERC721 assets works as-is between EOAs and contracts implementing `onERC721Received`, but a default Universal Profile does **not** implement `onERC721Received` natively. Before sending an ERC721 NFT with `safeTransferFrom`, register LSP17's [`OnERC721ReceivedExtension`](../../contracts/contracts/LSP17Extensions/OnERC721ReceivedExtension.md) on the profile for that selector, or use `transferFrom` (the non-safe variant) instead. NFTs already migrated to LSP8 move with `transferBatch(...)` or the LSP8 `transfer(from, to, tokenId, force, data)` call, which works against a Universal Profile with no extension needed.

If the Safe holds many distinct assets, write a sweep contract that batches the outbound transfers instead of sending them one by one.

## Step 4 — update integrations

Identify every protocol that references the Safe's address directly: vesting contracts, DAO memberships, subscriptions, allowance grants. Addresses don't migrate on their own — every external reference needs to be re-pointed to the new profile address.

## Step 5 — sunset the Safe

Once nothing material remains in the Safe, sweep out any remaining gas dust and treat it as historical. Leave the contract deployed — destroying multisig contracts is unsupported and risky.

## Gotchas

- Multisig threshold semantics don't map one-to-one to LSP6 — model it as a recovery-controller contract that enforces the threshold, then register that contract as an LSP6 controller with both `ADDCONTROLLER` and `EDITPERMISSIONS` (installing a never-before-permissioned replacement controller needs `ADDCONTROLLER`; `EDITPERMISSIONS` alone isn't enough).
- Asset transfer is many separate transactions unless the Safe owns assets through a sweep contract that batches outbound moves.
- Anything connected to the Safe's address — vesting schedules, allowance grants, on-chain memberships — needs to be re-pointed to the new profile address; addresses don't migrate, only the references you update do.
- Safe modules have their own permission shape; LSP6 controllers plus [LSP17](../../standards/accounts/lsp17-contract-extension.md) extensions are the closest LSP-side equivalents. Map each module to its closest LSP primitive deliberately rather than assuming a 1:1 translation.

## Verify the migration

- All assets transferred to the Universal Profile.
- Old Safe emptied of material assets — Safe has no built-in pause function, so "sunset" means the Safe holds nothing worth protecting anymore, not that it's disabled on-chain.
- Controllers and permissions configured on the new profile.
- Recovery policy in place, with at least one cold controller holding both `ADDCONTROLLER` and `EDITPERMISSIONS`.
- Off-chain integrations updated to the new address.

**Related reading:** [EOA vs Universal Profile](../why-lukso/compare/eoa-vs-universal-profile.md) · [Wallet permission scoping](../why-lukso/problems/wallet-permissions.md)
2 changes: 2 additions & 0 deletions docs/learn/why-lukso/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: '🆚 Why LUKSO'
collapsed: true
2 changes: 2 additions & 0 deletions docs/learn/why-lukso/architecture/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: '🏗️ Architecture Patterns'
collapsed: true
62 changes: 62 additions & 0 deletions docs/learn/why-lukso/architecture/consumer-crypto-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
sidebar_label: 'The Consumer Crypto Stack'
sidebar_position: 1
description: 'The architecture patterns a consumer crypto app needs — accounts, identity, permissions, gasless interaction, social primitives — chain by chain, compared.'
---

# The Consumer Crypto Stack

Every consumer crypto app is built from six architectural decisions: the account model, the identity layer, the permission model, the onboarding/gasless model, the metadata model, and the social-primitive layer. On Ethereum L1 and its L2s, each of those six is solved by a separate stack of contracts, services, and SDKs — an AA SDK plus a paymaster plus a bundler plus ENS plus EAS plus a per-protocol social layer. On Solana each is solved per-program. LUKSO is the only chain where all six are unified under one set of standards: [LSP0](../../../standards/accounts/lsp0-erc725account.md) for the account, [LSP6](../../../standards/access-control/lsp6-key-manager.md) for permissions, [LSP3](../../../standards/metadata/lsp3-profile-metadata.md) for identity, [LSP25](../../../standards/accounts/lsp25-execute-relay-call.md) for gasless execution, [LSP1](../../../standards/accounts/lsp1-universal-receiver.md) for reactive metadata, and [LSP26](../../../standards/accounts/lsp26-follower-system.md) for the social graph.

## The six decisions

Whether a team writes them down or not, every consumer crypto app makes these six choices:

1. **Account model** — an EOA, a smart account bolted on after the fact, or a smart account by design.
2. **Identity** — per-protocol, attestation-based, or a chain-native profile.
3. **Permissions** — per-contract approvals, session keys, or per-controller scopes.
4. **Onboarding/gasless** — the user pays, a paymaster pays via a bundler, a vendor pays via a hosted service, or a relayer pays via the account standard itself.
5. **Metadata** — an off-chain URI, a vendor metadata model, or on-chain typed key/value storage.
6. **Social primitives** — per-protocol, attestation-based, per-program, or chain-native.

The question is rarely "which chain." It's "which combination of these six layers, and how integrated should they be?" The more of a product's value depends on those layers talking to each other, the more a unified stack pays off — and the more a composed stack of separate vendors costs in integration tax.

## Implementation approaches

### Composed stack on Ethereum L1 / L2s

Pick an account-abstraction SDK, a paymaster, an indexer, a profile protocol, a permission system, and an attestation service. Wire them together.

- **Pros:** inherits Ethereum's protocol ecosystem and liquidity; best-of-breed tooling per layer; mature mainstream wallets.
- **Cons:** each layer is a separate vendor or protocol with its own SLA; identity and social data aren't portable without per-protocol integration; large operational surface area.
- **Chains:** Ethereum L1, Base, Arbitrum, Optimism, Polygon.

### Vendor stack on Base

Coinbase Smart Wallet, a Coinbase-hosted paymaster, Onchainkit, and Farcaster — tightly integrated, with one vendor owning most of the stack.

- **Pros:** lowest onboarding friction among EVM L2s; passkey-based recovery without seed phrases; Coinbase's distribution.
- **Cons:** vendor lock-in for paymaster and account services; social and identity data remain per-protocol.
- **Chains:** Base.

### Solana program-derived stack

Keypair accounts, program-derived addresses, the Solana Mobile Stack, and native fee delegation.

- **Pros:** highest sustained throughput; native fee delegation with no bundler; strong mobile SDK ecosystem.
- **Cons:** non-EVM toolchain with no Solidity portability; identity and social data remain per-program, with no shared standard.
- **Chains:** Solana.

### LUKSO LSP stack

One integrated set of standards on an EVM L1: a smart account plus permissions plus relayed execution plus a portable profile plus receiver hooks plus asset metadata plus a native follower system.

- **Pros:** all six layers standardized at the chain level; unmodified Solidity/EVM toolchain; no bundler infrastructure required for gasless UX; profile and social data portable across every app that reads the standard.
- **Cons:** smaller ecosystem maturity; less DeFi liquidity to inherit; full use of the LSP6 permission surface benefits from LSP-aware wallets and providers.
- **Chains:** LUKSO.

:::tip When LUKSO's unified stack is the right call
Reach for the composed Ethereum/L2 stack when the product is intrinsically protocol-composed — DeFi-shaped, or dependent on secondary NFT liquidity — and identity portability is a secondary concern. Reach for LUKSO when more than two of the six layers need to be standardized together: identity, permissions, gasless execution, and social data sharing without per-protocol integration work. That's the case LUKSO was built for — a single account object that already speaks all six layers, rather than six vendors a team has to keep in sync.
:::

**Related reading:** [Onchain identity patterns](./onchain-identity.md) · [Smart account permissions](./smart-account-permissions.md) · [Gasless onboarding patterns](./gasless-onboarding-patterns.md) · [Best blockchain for consumer apps](../best-blockchain-for/consumer-apps.md)
Loading
Loading