ERC-20 estandariza los tokens fungibles en Ethereum: cada unidad de un token dado es intercambiable, y cada tipo de token es su propio contrato desplegado.
pallet-emp-assets
pallet-emp-assets es el módulo nativo del runtime que EmpoorioChain usa para los tokens fungibles. En lugar de desplegar un nuevo contrato por cada token, registras un nuevo ID de activo en el almacenamiento de este pallet. Esto es distinto de Ethereum, donde cada token es su propio contrato desplegado y auditado de forma independiente.
¿Por qué un único pallet en lugar de un contrato por token? Porque el modelo de cuentas de EmpoorioChain separa la lógica del pallet (compilada dentro del runtime) de los datos de la aplicación (almacenados en los mapas de almacenamiento propios de cada pallet) — consulta las páginas de Cuentas y Contratos inteligentes para ver el panorama completo.
Los tokens no tienen dirección de contrato, solo un ID de activo
En Ethereum, cada token se identifica de forma única por la dirección de su contrato. En EmpoorioChain, cada token se identifica mediante un ID de activo registrado en pallet-emp-assets — no hay una dirección de contrato independiente que desplegar, porque la lógica ya reside en el pallet.
Sin flujo de aprobación independiente para transferencias básicas
En Ethereum, transferir tokens ERC-20 en nombre de otra persona a menudo requiere approve + transferFrom. En EmpoorioChain, cada cuenta ya mantiene su saldo directamente bajo su propio AccountId32 en el almacenamiento del pallet — un único extrínseco puede mover tokens sin un paso previo de aprobación para una transferencia directa. (El gasto delegado, cuando se necesita, se gestiona mediante las propias llamadas de tipo aprobación del pallet, no con un patrón específico por token.)
¿Cómo sería un equivalente a ERC-20 en Solidity?
A modo de comparación, así sería aproximadamente el mismo comportamiento de token fungible escrito como un único contrato reutilizable en Solidity (esto es ilustrativo, no es código literal de EmpoorioChain — la lógica real reside en pallet-emp-assets, escrito en Rust):
// SPDX-License-Identifier: MIT license
pragma solidity =0.8.28;
struct Mint {
uint8 decimals;
uint256 supply;
address mintAuthority;
address freezeAuthority;
address mintAddress;
}
struct TokenAccount {
address mintAddress;
address owner;
uint256 balance;
bool isFrozen;
}
contract Spl20 {
mapping(address => Mint) public mints;
mapping(address => TokenAccount) public tokenAccounts;
mapping(address => bool) public mintAddresses;
mapping(address => bool) public tokenAddresses;
function initializeMint(uint8 decimals, address mintAuthority, address freezeAuthority, address mintAddress)
public
returns (Mint memory)
{
require(mintAddresses[mintAddress] == false, "Mint already exists");
mints[mintAddress] = Mint(decimals, 0, mintAuthority, freezeAuthority, mintAddress);
mintAddresses[mintAddress] = true;
return Mint(decimals, 0, mintAuthority, freezeAuthority, mintAddress);
}
function mintTokens(address toMintTokens, address mintAddress, uint256 amount) public {
require(mints[mintAddress].mintAuthority == msg.sender, "Only the mint authority can mint tokens");
require(mints[mintAddress].mintAddress != address(0), "Token does not exist");
require(mints[mintAddress].supply + amount <= type(uint256).max, "Supply overflow");
mints[mintAddress].supply += amount;
address tokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(toMintTokens, mintAddress)))));
if (tokenAccounts[tokenAddress].mintAddress == address(0)) {
tokenAccounts[tokenAddress] = TokenAccount(mintAddress, toMintTokens, 0, false);
tokenAddresses[tokenAddress] = true;
}
tokenAccounts[tokenAddress].balance += amount;
tokenAccounts[tokenAddress].owner = toMintTokens;
}
function transfer(address to, address mintAddress, uint256 amount) public {
address toTokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(to, mintAddress)))));
address fromTokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, mintAddress)))));
require(tokenAccounts[fromTokenAddress].balance >= amount, "Insufficient balance");
require(tokenAccounts[toTokenAddress].balance + amount <= type(uint256).max, "Supply overflow");
require(tokenAccounts[fromTokenAddress].owner == msg.sender, "fromToken owner is not msg.sender");
require(tokenAccounts[fromTokenAddress].isFrozen == false, "fromToken is frozen");
require(tokenAccounts[toTokenAddress].isFrozen == false, "toToken is frozen");
if (tokenAccounts[toTokenAddress].mintAddress == address(0)) {
tokenAccounts[toTokenAddress] = TokenAccount(mintAddress, to, 0, false);
tokenAddresses[toTokenAddress] = true;
}
tokenAccounts[fromTokenAddress].balance -= amount;
tokenAccounts[toTokenAddress].balance += amount;
}
function getMint(address token) public view returns (Mint memory) {
return mints[token];
}
function getTokenAccount(address owner, address token) public view returns (TokenAccount memory) {
return tokenAccounts[address(uint160(uint256(keccak256(abi.encodePacked(owner, token)))))];
}
}Como se muestra, un único contrato reutilizable puede expresar las mismas operaciones básicas (acuñación, transferencia, consulta de saldo) que pallet-emp-assets expone como extrínsecos — la diferencia es que en EmpoorioChain esta lógica se distribuye una sola vez con el runtime, en lugar de una vez por cada contrato de token desplegado.
¿Dónde están los metadatos del token?
pallet-emp-assets almacena los metadatos básicos (nombre, símbolo, decimales) directamente en su propio elemento de almacenamiento de metadatos por cada ID de activo, establecido mediante un extrínseco dedicado en el momento del registro — no hay un contrato de metadatos independiente que desplegar o mantener.
Compensaciones de cada enfoque
ERC-20
- Altamente personalizable por token (lógica arbitraria en el contrato)
- Requiere el despliegue de un nuevo contrato auditado por cada token
- Requiere un flujo de aprobación para las transferencias delegadas
- La indexación requiere rastrear cada dirección de contrato por separado
pallet-emp-assets
- Un único pallet auditado cubre todos los activos
- Los nuevos tokens se crean registrando un ID de activo, no desplegando código
- La indexación es más simple — un pallet, un conjunto de mapas de almacenamiento
- La lógica personalizada por token requiere una extensión del pallet o un cambio de gobernanza, no un redespliegue rápido
Comparación general
A continuación se muestra una comparación de los pasos básicos para emitir un token fungible en Ethereum frente a EmpoorioChain.
| Paso | Ethereum (ERC-20) | EmpoorioChain (pallet-emp-assets) |
|---|---|---|
| 1. Preparar el código del token | Escribir/importar un contrato en Solidity (p. ej. mediante OpenZeppelin). | No se necesita código personalizado — el pallet ya forma parte del runtime. |
| 2. Compilar y desplegar | Compilar y desplegar con Hardhat/Foundry. | Registrar un nuevo ID de activo mediante un extrínseco — sin paso de despliegue independiente. |
| 3. Acuñación inicial | Llamar al constructor del contrato o a mint(). | Llamar al extrínseco de acuñación del pallet para el ID de activo registrado. |
| 4. Configuración del destinatario | Una dirección de Ethereum simple; las wallets a menudo necesitan añadir la dirección del contrato manualmente. | Cualquier AccountId32 puede mantener el activo directamente; no hay derivación de cuenta independiente. |
| 5. Comprobar resultados | Ver en Etherscan por dirección de contrato. | Consultar vía EmpooScan o api.query.assets.account por ID de activo. |
| 6. Actualizaciones de código | Patrón proxy o redespliegue. | La lógica del pallet queda fija a la granularidad de una actualización de runtime, gobernada on-chain. |
| 7. Requisitos de auditoría | Cada contrato normalmente necesita su propia auditoría. | El pallet compartido se audita una vez; los parámetros por activo conllevan mucho menos riesgo de código personalizado. |
¿Quieres registrar tu propio activo en EmpoorioChain? Consulta la referencia de pallets y la documentación de emp-cli enlazadas más abajo.
Cómo interactuar con pallet-emp-assets
Los ejemplos siguientes usan @polkadot/api, la librería cliente estándar de JavaScript/TypeScript para cadenas Substrate, apuntando al RPC de la testnet de EmpoorioChain. Los nombres exactos de extrínsecos y elementos de almacenamiento deben verificarse contra el SDK/referencia de pallets actual de EmpoorioChain, ya que pueden evolucionar.
Leer nombre/símbolo
function name() public view returns (string) // Returns the name of the tokenEl nombre y el símbolo se leen del elemento de almacenamiento de metadatos del activo, mediante api.query.assets.metadata(assetId).
// EmpoorioChain: query on-chain asset metadata via @polkadot/api
// against the pallet_emp_assets metadata storage for the given assetId.
// See the EmpoorioChain SDK docs for the exact metadata query shape.
import { ApiPromise, WsProvider } from "@polkadot/api";
async function name(assetId) {
const api = await ApiPromise.create({ provider: new WsProvider("wss://rpc.testnet.empooriochain.org") });
const metadata = await api.query.assets.metadata(assetId);
return metadata.toHuman();
}
name().then(name => {
console.log("token Name:", name);
})
.catch(error => {
console.error("Error:", error);
});
Cómo se compara con la firma de la interfaz ERC-20
function symbol() public view returns (string) // Returns the symbol of the tokenEl equivalente en Ethereum es una simple función de vista (view) en el contrato ERC-20.
// EmpoorioChain: query on-chain asset metadata via @polkadot/api
// against the pallet_emp_assets metadata storage for the given assetId.
// See the EmpoorioChain SDK docs for the exact metadata query shape.
import { ApiPromise, WsProvider } from "@polkadot/api";
async function symbol(assetId) {
const api = await ApiPromise.create({ provider: new WsProvider("wss://rpc.testnet.empooriochain.org") });
const metadata = await api.query.assets.metadata(assetId);
return metadata.toHuman();
}
symbol().then(symbol => {
console.log("Symbol:", symbol);
}).catch(error => {
console.error("Error:", error);
});
Leer los decimales
function decimals() public view returns (uint8) // Returns the number of decimals the token useLos decimales forman parte del mismo elemento de almacenamiento de metadatos/información del activo.
import { ApiPromise, WsProvider } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
async function decimals(assetId) {
const asset = await api.query.assets.asset(assetId);
return asset.unwrap().decimals?.toNumber?.() ?? asset.toHuman();
}
decimals(1).then(console.log).catch(console.error);Leer un saldo
function balanceOf(address _owner) public view returns (uint256 balance) // Returns the number of tokens in owner's accountimport { ApiPromise, WsProvider } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
async function balanceOf(assetId, accountId) {
const account = await api.query.assets.account(assetId, accountId);
return account.isSome ? account.unwrap().balance.toString() : "0";
}
balanceOf(1, "5Grwv...").then(console.log).catch(console.error);El saldo de una cuenta para un activo dado se lee mediante api.query.assets.account(assetId, accountId).
Leer el suministro total
function totalSupply() public view returns (uint256) // Returns the total issuance of tokensEl suministro total forma parte de la información on-chain del activo, mediante api.query.assets.asset(assetId).
import { ApiPromise, WsProvider } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
async function totalSupply(assetId) {
const asset = await api.query.assets.asset(assetId);
return asset.unwrap().supply.toString();
}
totalSupply(1).then(console.log).catch(console.error);Transferir
function transfer(address _to, uint256 _value) public returns (bool success) // Moves a value amount of tokens from the caller’s account to _to En EmpoorioChain, cada cuenta mantiene su saldo directamente bajo su AccountId32 — no hay que calcular primero una dirección de cuenta de token derivada. Un único extrínseco assets.transfer, firmado con @polkadot/api, mueve el activo.
import { Keypair, Transaction, Connection, PublicKey } from "@polkadot/api";
import { createTransferCheckedInstruction } from "@polkadot/api";
const connection = new Connection("https://rpc.testnet.empooriochain.org", "confirmed");
// Must contain your private key as a Uint8Array
const ownerSecretkey = [];
const ownerPrivatekeypair = Keypair.fromSecretKey(new Uint8Array(ownerSecretkey));
const receiverAddress = new PublicKey("Receiver's Wallet Address");
const mintAddress = new PublicKey("Token Address");
const ownerTokenAccount = new PublicKey("Your Associated Token Account Address");
const receiverTokenAccount = new PublicKey("Receiver's Associated Token Account Address");
// For a token with 9 decimals, transferring 1 => 1 * 10^9
const amount = 1;
async function transfer(_to, _value) {
try {
// Create a transaction with the transfer instruction
const tx = new Transaction().add(
createTransferCheckedInstruction(
ownerTokenAccount,
mintAddress,
receiverTokenAccount,
ownerPrivatekeypair.publicKey,
_value * Math.pow(10, 9), // Decimal correction
9 // decimals
)
);
// Send the transaction (simplified, no explicit blockhash or feePayer set)
await connection.sendTransaction(tx, [ownerPrivatekeypair]);
return true;
} catch (error) {
console.error("Error in transfer:", error);
return false;
}
}
transfer(receiverAddress, amount)
.then(result => {
console.log(result);
})
.catch(error => {
console.error("Error:", error);
});
Las transferencias con comisión patrocinada (pagadas por un relayer) están soportadas de forma nativa mediante el pallet paymaster de EmpoorioChain, en lugar de requerir un patrón de meta-transacción específico por token.
import { Keypair, Transaction, Connection, PublicKey } from "@polkadot/api";
import { createTransferCheckedInstruction, getOrCreateAssociatedTokenAccount } from "@polkadot/api";
const connection = new Connection("https://rpc.testnet.empooriochain.org", "confirmed");
// Must contain your private key as a Uint8Array
const ownerSecretkey = [];
const ownerPrivatekeypair = Keypair.fromSecretKey(new Uint8Array(ownerSecretkey));
const receiverAddress = new PublicKey("Receiver's Wallet Address");
const mintAddress = new PublicKey("Token Address");
const amount = 1; // Amount to transfer
async function transfer(_to, _value) {
try {
// Get or create the sender's ATA
const ownerTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
ownerPrivatekeypair, // Fee payer
mintAddress,
ownerPrivatekeypair.publicKey
);
// Get or create the receiver's ATA
const receiverTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
ownerPrivatekeypair, // Fee payer
mintAddress,
_to
);
// Build the transaction
const tx = new Transaction().add(
createTransferCheckedInstruction(
ownerTokenAccount.address,
mintAddress,
receiverTokenAccount.address,
ownerPrivatekeypair.publicKey,
_value * Math.pow(10, 9), // Decimal correction (9 decimals)
9 // decimals
)
);
// Send the transaction (simple version)
await connection.sendTransaction(tx, [ownerPrivatekeypair]);
return true;
} catch (error) {
console.error("Error in transfer:", error);
return false;
}
}
// Execute the transfer function
transfer(receiverAddress, amount)
.then(result => {
console.log("Transaction result:", result);
})
.catch(error => {
console.error("Error:", error);
});
¿Quieres explorar más? Consulta la referencia de pallets de EmpoorioChain para ver la lista completa de extrínsecos y elementos de almacenamiento de pallet-emp-assets.


