¿Qué es ERC-20 en EmpoorioChain?

Si vienes del ecosistema EVM, seguramente conoces bien ERC-20 — suele ser el primer contrato que se aprende a desplegar. EmpoorioChain no tiene un equivalente a ERC-20 en el sentido de "un estándar de contrato que se despliega". En su lugar, los tokens fungibles son una funcionalidad nativa del runtime. Así es como funciona en realidad.

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):

solidity
// 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.

PasoEthereum (ERC-20)EmpoorioChain (pallet-emp-assets)
1. Preparar el código del tokenEscribir/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 desplegarCompilar y desplegar con Hardhat/Foundry.Registrar un nuevo ID de activo mediante un extrínseco — sin paso de despliegue independiente.
3. Acuñación inicialLlamar 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 destinatarioUna 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 resultadosVer en Etherscan por dirección de contrato.Consultar vía EmpooScan o api.query.assets.account por ID de activo.
6. Actualizaciones de códigoPatró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íaCada 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

bash
function name() public view returns (string) // Returns the name of the token

El nombre y el símbolo se leen del elemento de almacenamiento de metadatos del activo, mediante api.query.assets.metadata(assetId).

jsx
// 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

bash
function symbol() public view returns (string) // Returns the symbol of the token

El equivalente en Ethereum es una simple función de vista (view) en el contrato ERC-20.

jsx
// 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

bash
function decimals() public view returns (uint8) // Returns the number of decimals the token use

Los decimales forman parte del mismo elemento de almacenamiento de metadatos/información del activo.

jsx
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

bash
function balanceOf(address _owner) public view returns (uint256 balance) // Returns the number of tokens in owner's account
jsx
import { 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

bash
function totalSupply() public view returns (uint256) // Returns the total issuance of tokens

El suministro total forma parte de la información on-chain del activo, mediante api.query.assets.asset(assetId).

jsx
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

solidity
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.

jsx
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.

jsx
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.

Nodo que almacena todo el historial y participa en el consenso

  • Ethereum: Archive Node
  • EmpoorioChain: Nodo completo con poda de archivo habilitada

Nodo que produce/finaliza bloques

  • Ethereum: Full Node (cliente de ejecución + consenso)
  • EmpoorioChain: Nodo validador (producción de bloques con Aura + finalidad con GRANDPA, binario único en Rust)

Nodo que sirve RPC sin validar

  • Ethereum: Light Node
  • EmpoorioChain: Nodo RPC (no validador, estado completo)

EVM A EMPOORIOCHAIN

Empieza a construir en EmpoorioChain

Introducción al desarrollo en EmpoorioChain

Leer

Referencia de pallets de EmpoorioChain

Leer

Guía rápida de emp-cli

Leer

Más herramientas para desarrolladores de EmpoorioChain

Leer
¿Qué es ERC-20 en EmpoorioChain? | empoorio