AgentSkillsCN

universal-profile

通过直接交易或免 Gas 中继交易,管理 LUKSO 通用身份档案——涵盖身份认证、权限管理、代币发行与区块链操作等核心功能。

SKILL.md
--- frontmatter
name: universal-profile
description: Manage LUKSO Universal Profiles — identity, permissions, tokens, and blockchain operations via direct or gasless relay transactions
version: 1.0.0
author: LUKSO

Universal Profile Skill

Interact with LUKSO Universal Profiles from OpenClaw. Provides CLI commands and a programmatic API for profile management, permission handling, token operations, and transaction execution — including gasless relay transactions via LSP25.


Table of Contents

  1. Overview
  2. Setup & Authorization
  3. CLI Commands
  4. Direct Transactions
  5. Execute Relay Call (Gasless Transactions)
  6. LSP Standards Reference
  7. Smart Contract Interfaces
  8. Permission System
  9. ERC725Y Data Keys Reference
  10. Security Best Practices
  11. Error Handling
  12. Network Configuration

1. Overview

What Are Universal Profiles?

A Universal Profile (UP) is a smart contract–based blockchain account on LUKSO built on LSP0 (ERC725Account). Unlike EOAs, Universal Profiles provide:

  • On-chain data storage — Profile metadata, asset lists, permissions (ERC725Y key-value store)
  • Granular access control — Multiple controller keys with fine-grained permissions (LSP6 Key Manager)
  • Gasless transactions — Third parties execute transactions on your behalf via relay calls (LSP25)
  • Notification hooks — React to incoming tokens, ownership changes (LSP1 Universal Receiver)
  • Extensibility — Add new functionality without redeploying (LSP17 Contract Extension)
  • Secure ownership transfer — Two-step process prevents accidental loss (LSP14)

Architecture

code
┌──────────────────────────────────────────────────┐
│              Universal Profile (LSP0)             │
│  ERC725X (execute) + ERC725Y (data) + LSP1 +     │
│  LSP14 (ownership) + LSP17 (extensions) + LSP20  │
└───────────────────────┬──────────────────────────┘
                        │ owner
           ┌────────────▼────────────┐
           │    Key Manager (LSP6)    │
           │  Permissions stored in   │
           │  UP's ERC725Y storage    │
           └─────────────┬───────────┘
                         │
        ┌────────────────┼────────────────┐
   Controller A     Controller B     Controller C
   (CALL, SIGN)    (SETDATA)        (TRANSFERVALUE)

The LSP Ecosystem

StandardNamePurpose
LSP0ERC725AccountSmart contract account (Universal Profile)
LSP1UniversalReceiverNotification hooks for incoming interactions
LSP2ERC725Y JSON SchemaStandardized key encoding for on-chain data
LSP3Profile MetadataName, description, avatar, links, tags
LSP4Digital Asset MetadataToken name, symbol, type, creators
LSP5ReceivedAssetsTracks tokens/NFTs owned by a profile
LSP6KeyManagerPermission-based access control
LSP7DigitalAssetFungible token standard (like ERC20 + notifications)
LSP8IdentifiableDigitalAssetNFT standard (bytes32 token IDs + notifications)
LSP9VaultSub-account for asset segregation
LSP10ReceivedVaultsTracks vaults owned by a profile
LSP12IssuedAssetsTracks assets created by a profile
LSP14Ownable2StepTwo-step ownership transfer
LSP16UniversalFactoryDeterministic CREATE2 deployment
LSP17ContractExtensionAdd functions without redeploying
LSP20CallVerificationPermission checking between UP and Key Manager
LSP23LinkedContractsFactoryDeploy UP + Key Manager together
LSP25ExecuteRelayCallMeta-transaction / gasless execution
LSP26FollowerSystemOn-chain follow/unfollow

2. Setup & Authorization

Step 1: Generate a Controller Key

bash
up key generate --save --password <your-password>

Creates an encrypted keypair (AES-256-GCM, PBKDF2 100k iterations). Never share the private key.

Step 2: Authorize the Controller

Visit the authorization UI: https://lukso-network.github.io/openclaw-universalprofile-skill/

  1. Connect your Universal Profile (UP Browser Extension)
  2. Paste the controller address from Step 1
  3. Select a permission preset or customize
  4. Submit the transaction

Or generate a URL from CLI: up authorize url --permissions defi-trader

Step 3: Configure

bash
up profile configure 0xYourUPAddress --chain lukso

Step 4: Verify

bash
up status

Configuration File

Stored at ~/.clawdbot/universal-profile/config.json:

json
{
  "version": "1.0.0",
  "network": "lukso-mainnet",
  "rpc": {
    "lukso-mainnet": "https://42.rpc.thirdweb.com",
    "lukso-testnet": "https://rpc.testnet.lukso.network"
  },
  "universalProfile": {
    "address": "0xYourUPAddress",
    "keyManager": "0xAutoDetectedKMAddress"
  },
  "controllerKey": {
    "address": "0xControllerAddress",
    "label": "default",
    "encrypted": true,
    "path": "/path/to/keystore.json"
  },
  "relay": {
    "enabled": true,
    "url": "https://relayer.lukso.network",
    "fallbackToDirect": true
  }
}

3. CLI Commands

All commands use the up prefix.

Key Management

bash
up key generate [--save] [--password <pw>]   # Generate a new controller keypair
up key list                                   # List stored keys

Status & Profile

bash
up status [--chain <chain>]                                            # Config, keys, connectivity
up profile info [<address>] [--chain <chain>]                          # Profile details
up profile configure <address> [--key-manager <km>] [--chain <chain>]  # Save UP for use

Permissions

bash
up permissions encode <perm1> [<perm2> ...]   # Encode to bytes32 hex
up permissions decode <hex>                    # Decode to permission names
up permissions presets                         # List presets with risk levels
up permissions validate <hex>                  # Security audit

Authorization

bash
up authorize url [--permissions <preset|hex>] [--chain <chain>]

Presets: read-only (🟢) | token-operator (🟡) | nft-trader (🟡) | defi-trader (🟠) | profile-manager (🟡) | full-access (🔴)

Configuration

bash
up config show                # Full JSON config
up config set <key> <value>   # Set defaultChain, keystorePath, etc.
up help                       # All commands

4. Direct Transactions

When the controller has LYX, call the Key Manager's execute() directly. The controller pays gas.

code
Controller EOA → KeyManager.execute(payload) → UP.execute(...) → Target

ethers.js v6 Examples

javascript
import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('https://42.rpc.thirdweb.com');
const signer = new ethers.Wallet('0xCONTROLLER_PRIVATE_KEY', provider);

const UP_ABI = [
  'function execute(uint256 operationType, address target, uint256 value, bytes data) payable returns (bytes)',
  'function setData(bytes32 dataKey, bytes dataValue)',
];
const KM_ABI = ['function execute(bytes calldata payload) payable returns (bytes)'];

const up = new ethers.Contract('0xUPAddress', UP_ABI, signer);
const km = new ethers.Contract('0xKMAddress', KM_ABI, signer);

// Transfer LYX
const payload = up.interface.encodeFunctionData('execute', [
  0, '0xRecipient', ethers.parseEther('1.5'), '0x',
]);
await (await km.execute(payload)).wait();

// Transfer LSP7 Token
const tokenIface = new ethers.Interface([
  'function transfer(address from, address to, uint256 amount, bool force, bytes data)',
]);
const calldata = tokenIface.encodeFunctionData('transfer', [
  '0xUPAddress', '0xRecipient', ethers.parseEther('100'), false, '0x',
]);
const payload2 = up.interface.encodeFunctionData('execute', [0, '0xTokenContract', 0n, calldata]);
await (await km.execute(payload2)).wait();

Skill API

javascript
import { createUniversalProfileSkill } from 'openclaw-universalprofile-skill';
const skill = createUniversalProfileSkill({ network: 'lukso-mainnet', privateKey: '0x...' });

await skill.execute({ operationType: 0, target: '0xRecipient', value: ethers.parseEther('1.5'), data: '0x' });
await skill.transferToken('0xTokenAddr', '0xRecipient', ethers.parseEther('100'));
await skill.transferNFT('0xCollectionAddr', '0xRecipient', '0xTokenId');

5. Execute Relay Call (Gasless Transactions)

The executeRelayCall mechanism (LSP25) lets a third party submit transactions on behalf of a controller — the controller signs but pays no gas.

When to Use

  • Controller has no LYX for gas
  • UP registered with a relay service with available quota
  • You want gasless UX

Users who created their UP via universalprofile.cloud have a monthly gas quota paid by LUKSO.

How It Works

  1. Controller signs a message off-chain (payload + nonce + validity + chainId)
  2. Signature + params sent to relayer or submitted on-chain by another account
  3. Relayer calls KeyManager.executeRelayCall(signature, nonce, validityTimestamps, payload)
  4. Key Manager verifies signature, checks permissions, forwards to UP
  5. Relayer pays gas

Nonce Channels

Each controller has nonces per channel (uint128). Same channel = sequential; different channels = parallel.

javascript
const nonce = await keyManager.getNonce(controllerAddress, 0);  // channel 0

The nonce uint256: upper 128 bits = channel ID, lower 128 bits = sequential nonce.

Validity Timestamps

Packed uint256: (startTimestamp << 128) | endTimestamp. Use 0 for no restriction.

javascript
function createValidityTimestamps(duration = 3600) {
  const now = Math.floor(Date.now() / 1000);
  return (BigInt(now) << 128n) | BigInt(now + duration);
}

LSP25 Signature Format

javascript
const encoded = ethers.solidityPacked(
  ['uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'bytes'],
  [25, chainId, nonce, validityTimestamps, msgValue, abiPayload]
  // 25 = LSP25_VERSION (always 25)
);
const hash = ethers.keccak256(encoded);
const signature = await signer.signMessage(ethers.getBytes(hash));

Complete Example: Gasless LYX Transfer

javascript
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://42.rpc.thirdweb.com');
const controller = new ethers.Wallet('0xCONTROLLER_KEY');

const upIface = new ethers.Interface([
  'function execute(uint256, address, uint256, bytes) payable returns (bytes)',
]);
const km = new ethers.Contract('0xKMAddress', [
  'function executeRelayCall(bytes, uint256, uint256, bytes) payable returns (bytes)',
  'function getNonce(address, uint128) view returns (uint256)',
], provider);

// 1. Encode payload
const payload = upIface.encodeFunctionData('execute', [
  0, '0xRecipient', ethers.parseEther('3'), '0x',
]);

// 2. Get nonce
const nonce = await km.getNonce(controller.address, 0);
const { chainId } = await provider.getNetwork();

// 3. Sign (LSP25)
const encoded = ethers.solidityPacked(
  ['uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'bytes'],
  [25, chainId, nonce, 0n, 0n, payload]
);
const signature = await controller.signMessage(
  ethers.getBytes(ethers.keccak256(encoded))
);

// 4a. Send to relay service
await fetch('https://relayer.lukso.network/execute', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    keyManagerAddress: '0xKMAddress', signature,
    nonce: nonce.toString(), validityTimestamps: '0', payload, value: '0',
  }),
});

// 4b. Or execute on-chain (funded account pays gas)
const relayer = new ethers.Wallet('0xRELAYER_KEY', provider);
await (await km.connect(relayer).executeRelayCall(signature, nonce, 0n, payload)).wait();

Skill Relay API

javascript
import { executeViaRelay, setDataViaRelay, checkRelayQuota } from 'openclaw-universalprofile-skill';

// High-level: handles nonce, signing, execution automatically
const result = await executeViaRelay(signer, upAddr, kmAddr,
  { operationType: 0, target: '0xRecipient', value: ethers.parseEther('1'), data: '0x' },
  { relayerUrl: 'https://relayer.lukso.network', validityDuration: 3600 }
);
console.log('TX:', result.transactionHash);

// Set data via relay
await setDataViaRelay(signer, upAddr, kmAddr, dataKey, dataValue, { relayerUrl: '...' });

// Check quota
const quota = await checkRelayQuota('https://relayer.lukso.network', upAddr);

Relay Service Endpoints

EndpointMethodDescription
/executePOSTSubmit signed relay transaction
/quota/<address>GETCheck remaining quota

URLs: Mainnet https://relayer.lukso.network · Testnet https://relayer.testnet.lukso.network

Direct vs. Relay

ScenarioDirectRelay
Controller has LYXOptional
Controller has no LYX
UP registered with relayEither
Need immediate executionDepends

6. LSP Standards Reference

LSP0 — ERC725Account (Universal Profile)

Interface ID: 0x24871b3d — Smart contract account combining ERC725X (execute), ERC725Y (data), LSP1 (notifications), LSP14 (ownership), LSP17 (extensions), LSP20 (verification). Operation types: CALL=0, CREATE=1, CREATE2=2, STATICCALL=3, DELEGATECALL=4.

LSP1 — UniversalReceiver

Interface IDs: 0x6bb56a14 / 0xa245bbda (delegate) — Notification hook called when UP receives tokens/value. URD contract auto-registers assets in LSP5.

LSP2 — ERC725Y JSON Schema

Key encoding: Singleton keccak256(name), Array base key (length) + bytes16(base)+bytes16(index), Mapping bytes10(keccak256(first))+0000+bytes20(second).

LSP3 — Profile Metadata

Profile JSON (name, description, links, tags, profileImage, backgroundImage, avatar) stored as VerifiableURI.

LSP4 — Digital Asset Metadata

Token metadata (name, symbol, type, creators). Token types: 0=Fungible, 1=Single NFT, 2=Collection.

LSP5 — ReceivedAssets

On-chain array tracking tokens/NFTs held by a profile. Auto-managed by URD.

LSP6 — KeyManager

Interface ID: 0x23f34c62 — Permission engine. Functions: execute(), executeRelayCall(), getNonce(). See Section 8.

LSP7 — DigitalAsset (Fungible)

Interface ID: 0xc52d6008 — Fungible tokens with LSP1 notifications, force parameter (false=safe transfer), rich metadata, batch transfers.

LSP8 — IdentifiableDigitalAsset (NFT)

Interface ID: 0x3a271706 — NFTs with bytes32 token IDs (formats: uint256/string/address/hash), per-token metadata, LSP1 hooks.

LSP9 — Vault

Interface ID: 0x28af17e6 — Sub-account with own ERC725X/Y storage for asset segregation.

LSP14 — Ownable2Step

Interface ID: 0x94be5999transferOwnership()acceptOwnership().

LSP25 — ExecuteRelayCall

Interface ID: 0x5ac79908 — Version 25. See Section 5.

LSP26 — FollowerSystem

Interface ID: 0x2b299cea — On-chain follow/unfollow with LSP1 notifications.


7. Smart Contract Interfaces

Universal Profile (LSP0) ABI

javascript
const LSP0_ABI = [
  'function execute(uint256 operationType, address target, uint256 value, bytes data) payable returns (bytes)',
  'function executeBatch(uint256[] operationTypes, address[] targets, uint256[] values, bytes[] datas) payable returns (bytes[])',
  'function getData(bytes32 dataKey) view returns (bytes)',
  'function getDataBatch(bytes32[] dataKeys) view returns (bytes[])',
  'function setData(bytes32 dataKey, bytes dataValue)',
  'function setDataBatch(bytes32[] dataKeys, bytes[] dataValues)',
  'function universalReceiver(bytes32 typeId, bytes data) payable returns (bytes)',
  'function owner() view returns (address)',
  'function transferOwnership(address newOwner)',
  'function acceptOwnership()',
  'function supportsInterface(bytes4 interfaceId) view returns (bool)',
  'function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)',
];

Key Manager (LSP6) ABI

javascript
const LSP6_ABI = [
  'function execute(bytes calldata payload) payable returns (bytes)',
  'function executeBatch(uint256[] values, bytes[] payloads) payable returns (bytes[])',
  'function executeRelayCall(bytes signature, uint256 nonce, uint256 validityTimestamps, bytes payload) payable returns (bytes)',
  'function executeRelayCallBatch(bytes[] signatures, uint256[] nonces, uint256[] validityTimestamps, uint256[] values, bytes[] payloads) payable returns (bytes[])',
  'function getNonce(address from, uint128 channelId) view returns (uint256)',
  'function target() view returns (address)',
  'function isValidSignature(bytes32 dataHash, bytes signature) view returns (bytes4)',
];

LSP7 Digital Asset ABI

javascript
const LSP7_ABI = [
  'function name() view returns (string)',
  'function symbol() view returns (string)',
  'function decimals() view returns (uint8)',
  'function totalSupply() view returns (uint256)',
  'function balanceOf(address tokenOwner) view returns (uint256)',
  'function transfer(address from, address to, uint256 amount, bool force, bytes data)',
  'function transferBatch(address[] from, address[] to, uint256[] amounts, bool[] force, bytes[] data)',
  'function authorizeOperator(address operator, uint256 amount, bytes operatorNotificationData)',
  'function revokeOperator(address operator, address tokenOwner, bool notify, bytes operatorNotificationData)',
  'function authorizedAmountFor(address operator, address tokenOwner) view returns (uint256)',
  'function getOperatorsOf(address tokenOwner) view returns (address[])',
  'function owner() view returns (address)',
  'function supportsInterface(bytes4 interfaceId) view returns (bool)',
];

LSP8 Identifiable Digital Asset ABI

javascript
const LSP8_ABI = [
  'function name() view returns (string)',
  'function symbol() view returns (string)',
  'function totalSupply() view returns (uint256)',
  'function balanceOf(address tokenOwner) view returns (uint256)',
  'function tokenOwnerOf(bytes32 tokenId) view returns (address)',
  'function tokenIdsOf(address tokenOwner) view returns (bytes32[])',
  'function transfer(address from, address to, bytes32 tokenId, bool force, bytes data)',
  'function authorizeOperator(address operator, bytes32 tokenId, bytes operatorNotificationData)',
  'function revokeOperator(address operator, bytes32 tokenId, bool notify, bytes operatorNotificationData)',
  'function isOperatorFor(address operator, bytes32 tokenId) view returns (bool)',
  'function getDataForTokenId(bytes32 tokenId, bytes32 dataKey) view returns (bytes)',
  'function owner() view returns (address)',
  'function supportsInterface(bytes4 interfaceId) view returns (bool)',
];

Interface IDs

javascript
const INTERFACE_IDS = {
  ERC165: '0x01ffc9a7',       ERC725X: '0x7545acac',       ERC725Y: '0x629aa694',
  ERC1271: '0x1626ba7e',      LSP0: '0x24871b3d',          LSP1: '0x6bb56a14',
  LSP1Delegate: '0xa245bbda', LSP6: '0x23f34c62',          LSP7: '0xc52d6008',
  LSP8: '0x3a271706',         LSP9: '0x28af17e6',          LSP14: '0x94be5999',
  LSP17Extendable: '0xa918fa6b', LSP17Extension: '0xcee78b40',
  LSP25: '0x5ac79908',        LSP26: '0x2b299cea',
};

8. Permission System

Permissions are a bytes32 BitArray stored at AddressPermissions:Permissions:<address> in the UP's ERC725Y storage.

Permission Values

PermissionBitHexRisk
CHANGEOWNER00x...01🔴 Critical
ADDCONTROLLER10x...02🟠 High
EDITPERMISSIONS20x...04🟠 High
ADDEXTENSIONS30x...08🟡 Medium
CHANGEEXTENSIONS40x...10🟡 Medium
ADDUNIVERSALRECEIVERDELEGATE50x...20🟡 Medium
CHANGEUNIVERSALRECEIVERDELEGATE60x...40🟡 Medium
REENTRANCY70x...80🟡 Medium
SUPER_TRANSFERVALUE80x...0100🟠 High
TRANSFERVALUE90x...0200🟡 Medium
SUPER_CALL100x...0400🟠 High
CALL110x...0800🟡 Medium
SUPER_STATICCALL120x...1000🟢 Low
STATICCALL130x...2000🟢 Low
SUPER_DELEGATECALL140x...4000🔴 Critical
DELEGATECALL150x...8000🔴 Critical
DEPLOY160x...010000🟡 Medium
SUPER_SETDATA170x...020000🟠 High
SETDATA180x...040000🟡 Medium
ENCRYPT190x...080000🟢 Low
DECRYPT200x...100000🟢 Low
SIGN210x...200000🟢 Low
EXECUTE_RELAY_CALL220x...400000🟢 Low

ALL_PERMISSIONS = 0x00000000000000000000000000000000000000000000000000000000007f3f7f

Combining Permissions

Bitwise OR:

javascript
// CALL + TRANSFERVALUE + SIGN = 0x200a00
const perm = BigInt(0x800) | BigInt(0x200) | BigInt(0x200000);
const hex = '0x' + perm.toString(16).padStart(64, '0');

SUPER vs. Regular

  • SUPER_CALL — Call ANY contract; CALL — Only addresses in AllowedCalls
  • SUPER_SETDATA — Set ANY key; SETDATA — Only keys in AllowedERC725YDataKeys
  • Always prefer restricted + AllowedCalls/AllowedDataKeys for security.

AllowedCalls Format

CompactBytesArray at AddressPermissions:AllowedCalls:<address>. Each 32-byte entry:

code
<callTypes(4)><address(20)><interfaceId(4)><functionSelector(4)>

callTypes bits: 0=TRANSFERVALUE, 1=CALL, 2=STATICCALL, 3=DELEGATECALL. Use 0xffffffff... for wildcards.


9. ERC725Y Data Keys Reference

Profile & Metadata

KeyHex
LSP3Profile0x5ef83ad9559033e6e941db7d7c495acdce616347d28e90c7ce47cbfcfcad3bc5
SupportedStandards:LSP3Profile0xeafec4d89fa9619884b600005ef83ad9559033e6e941db7d7c495acdce616347

Token Metadata (LSP4)

KeyHex
LSP4TokenName0xdeba1e292f8ba88238e10ab3c7f88bd4be4fac56cad5194b6ecceaf653468af1
LSP4TokenSymbol0x2f0a68ab07768e01943a599e73362a0e17a63a72e94dd2e384d2c1d4db932756
LSP4TokenType0xe0261fa95db2eb3b5439bd033cda66d56b96f92f243a8228fd87550ed7bdfdb3
LSP4Metadata0x9afb95cacc9f95858ec44aa8c3b685511002e30ae54415823f406128b85b238e
LSP4Creators[]0x114bd03b3a46d48759680d81ebb2b414fda7d030a7105a851867accf1c2352e7

Received Assets (LSP5)

KeyHex
LSP5ReceivedAssets[]0x6460ee3c0aac563ccbf76d6e1d07bada78e3a9514e6382b736ed3f478ab7b90b
LSP5ReceivedAssetsMap:<address>0x812c4334633eb816c80d0000 + address

Permissions (LSP6)

KeyHex
AddressPermissions[]0xdf30dba06db6a30e65354d9a64c609861f089545ca58c6b4dbe31a5f338cb0e3
AddressPermissions:Permissions:<addr>0x4b80742de2bf82acb3630000 + address
AddressPermissions:AllowedCalls:<addr>0x4b80742de2bf393a64c70000 + address
AddressPermissions:AllowedERC725YDataKeys:<addr>0x4b80742de2bf866c29110000 + address

Other Keys

KeyHex
LSP1UniversalReceiverDelegate0x0cfc51aec37c55a4d0b1a65c6255c4bf2fbdf6277f3cc0730c45b828b6db8b47
LSP10Vaults[]0x55482936e01da86729a45d2b87a6b1d3bc582bea0ec00e38bdb340e3af6f9f06
LSP12IssuedAssets[]0x7c8c3416d6cda87cd42c71ea1843df28ac4850354f988d55ee2eaa47b6dc05cd
LSP8TokenIdFormat0xf675e9361af1c1664c1868cfa3eb97672d6b1a513aa5b81dec34c9ee330e818d

Array Key Encoding

For array types, the base key stores the length. Element keys use the first 16 bytes of the base key + 16 bytes for the index:

javascript
function getArrayElementKey(baseKey, index) {
  const prefix = baseKey.slice(0, 34); // 0x + 32 hex chars = 16 bytes
  return prefix + index.toString(16).padStart(32, '0');
}

10. Security Best Practices

  1. Principle of Least Privilege — Grant minimum necessary permissions. Prefer CALL over SUPER_CALL, SETDATA over SUPER_SETDATA.
  2. Use AllowedCalls — When granting CALL, always restrict to specific contracts, interfaces, and functions via AllowedCalls.
  3. Use AllowedERC725YDataKeys — When granting SETDATA, restrict to specific data keys.
  4. Avoid DELEGATECALL — Can execute arbitrary code in the UP's context. Only use for trusted upgrade mechanisms.
  5. Avoid CHANGEOWNER — Only grant to recovery addresses. Allows transferring profile ownership.
  6. Encrypt keys at rest — Use the skill's encrypted keystore (AES-256-GCM).
  7. Never log private keys — The skill never exposes private keys in output.
  8. Validate permissions before granting — Use up permissions validate <hex> to check for risks.
  9. Use validity timestamps for relay calls — Limit the window during which a signed relay call can be executed.
  10. Test on testnet first — Always verify operations on LUKSO Testnet (chain 4201) before mainnet.
  11. Monitor relay quota — Check quota before relying on gasless execution.
  12. Review AllowedCalls entries — Ensure wildcards (0xffffffff) are intentional.

11. Error Handling

Error Codes

CodeNameDescription
UP_KEY_NOT_FOUNDKey not foundController key not in keystore
UP_KEY_DECRYPT_FAILEDDecrypt failedWrong password for keystore
UP_PERMISSION_DENIEDPermission deniedController lacks required permission
UP_DEPLOYMENT_FAILEDDeployment failedContract deployment error
UP_RELAY_FAILEDRelay failedRelay call execution error
UP_INVALID_SIGNATUREInvalid signatureLSP25 signature verification failed
UP_QUOTA_EXCEEDEDQuota exceededRelay service quota exhausted
UP_NETWORK_ERRORNetwork errorRPC connection failure
UP_INVALID_ADDRESSInvalid addressNot a valid Ethereum address or not a UP
UP_INSUFFICIENT_BALANCEInsufficient balanceNot enough LYX for transaction
UP_TRANSACTION_FAILEDTransaction failedOn-chain transaction reverted
UP_CONFIG_NOT_FOUNDConfig not foundMissing configuration
UP_NOT_AUTHORIZEDNot authorizedAddress is not a controller

Common Issues

"Not authorized" — The controller address hasn't been added to the UP's permissions. Visit the authorization UI.

"Relay failed" — Check relay quota with checkRelayQuota(). Ensure the UP is registered with the relay service. Fall back to direct execution if needed.

"Invalid signature" — Ensure you're using the correct chainId (42 mainnet, 4201 testnet), the nonce hasn't been used, and validity timestamps haven't expired.

"Permission denied" — Check which permissions the controller has with up profile info. The controller may need additional permissions for the action.


12. Network Configuration

LUKSO Mainnet

PropertyValue
Chain ID42 (0x2a)
RPC URLhttps://42.rpc.thirdweb.com
Explorerhttps://explorer.lukso.network
Relay Servicehttps://relayer.lukso.network
Native TokenLYX (18 decimals)

LUKSO Testnet

PropertyValue
Chain ID4201 (0x1069)
RPC URLhttps://rpc.testnet.lukso.network
Explorerhttps://explorer.testnet.lukso.network
Relay Servicehttps://relayer.testnet.lukso.network
Native TokenLYXt (18 decimals)

Factory Contracts (Deterministic, Same on Both Networks)

ContractAddress
LSP16 Universal Factory0x1600016e23e25D20CA8759338BfB8A8d11563C4e
LSP23 Linked Contracts Factory0x2300000A84D25dF63081feAa37ba6b62C4c89a30

NPM Packages

bash
npm install @lukso/lsp-smart-contracts  # All LSP contract artifacts
npm install @erc725/erc725.js           # ERC725Y data encoding/decoding
npm install ethers                       # Ethereum library (v6)

Reading Profile Data with ERC725.js

javascript
import ERC725 from '@erc725/erc725.js';
import LSP3Schema from '@erc725/erc725.js/schemas/LSP3ProfileMetadata.json';

const erc725 = new ERC725(LSP3Schema, '0xUPAddress', 'https://42.rpc.thirdweb.com', {
  ipfsGateway: 'https://api.universalprofile.cloud/ipfs',
});

// Fetch profile (resolves IPFS automatically)
const profile = await erc725.fetchData('LSP3Profile');
console.log(profile.value); // { LSP3Profile: { name: '...', ... } }

// Available schemas: LSP3, LSP4, LSP5, LSP6, LSP8, LSP9, LSP10, LSP12

Checking Interface Support

javascript
const up = new ethers.Contract(address, ['function supportsInterface(bytes4) view returns (bool)'], provider);
const isUP = await up.supportsInterface('0x24871b3d');  // LSP0
const isLSP7 = await up.supportsInterface('0xc52d6008'); // LSP7
const isLSP8 = await up.supportsInterface('0x3a271706'); // LSP8

Dependencies

  • Node.js 18+
  • ethers.js v6
  • Network access to LUKSO RPC

Links