ERC-4626 es un estándar de bóvedas tokenizadas: una interfaz canónica para bóvedas que aceptan un único activo ERC-20 subyacente y emiten tokens de participación que representan la propiedad proporcional, con un ciclo de vida estándar de depósito/acuñación/retiro/canje.
Características clave
- Custodia de un único activo: La bóveda mantiene un único activo subyacente; toda la contabilidad se expresa en ese activo.
- Contabilidad de participaciones: El total de activos y el total de participaciones se mantienen en una proporción predecible y convertible.
- Interfaz componible: Los DEX, agregadores y front-ends pueden integrar cualquier bóveda conforme al estándar sin adaptadores personalizados.
- Independiente de la estrategia: La estrategia de rendimiento (préstamo, staking, creación de mercado) vive detrás de la misma fachada estándar.
El equivalente en EmpoorioChain: pallet-yield-vault
¿Cómo es una bóveda en EmpoorioChain?
pallet-yield-vault es un pallet nativo del runtime: mantiene el saldo subyacente de pallet-emp-assets y rastrea la emisión de participaciones directamente en su propio almacenamiento. No hay un contrato de bóveda independiente que escribir, auditar y desplegar por estrategia — una bóveda se registra como una entrada en el pallet, del mismo modo que un activo se registra en pallet-emp-assets.
Program ──┐
└─> handles instructions
Accounts ───┘ (carry all mutable state)Sensación de despliegue: en lugar de desplegar un nuevo contrato ERC-4626 por cada estrategia, como harías al desplegar un nuevo contrato de pool al estilo de Uniswap v2, registras un nuevo ID de bóveda contra el pallet-yield-vault compartido — el propio pallet nunca cambia, solo la configuración.
Modelo de contabilidad
Como pallet-yield-vault es un pallet nativo, puede leer y mover saldos en pallet-emp-assets directamente mediante una llamada interna entre pallets, en lugar de una llamada externa entre contratos con su propio sobrecoste de gas. Los saldos de participaciones se rastrean igual que los saldos de activos — como entradas indexadas por AccountId32 en el almacenamiento propio del pallet de la bóveda.
Custodia de la bóveda
En Ethereum, un contrato de bóveda custodia él mismo el activo subyacente, controlado por su propia lógica de contrato. En EmpoorioChain, pallet-yield-vault custodia el saldo subyacente de pallet-emp-assets directamente en su almacenamiento controlado por el runtime — no hay una dirección de custodia derivada ni una clave privada independiente involucrada; la lógica del pallet es lo único autorizado para moverlo.
Llamadas entre pallets
Mientras que una bóveda ERC-4626 en Ethereum realiza una llamada externa al contrato ERC-20 subyacente (p. ej. `IERC20.transferFrom`) para mover activos, pallet-yield-vault llama directamente a pallet-emp-assets dentro del runtime. El runtime aplica el mismo tipo de atomicidad que Ethereum obtiene de una única transacción — o todo el extrínseco tiene éxito, o ninguna parte de él lo tiene.
Flujo de depósito/retiro
Depósito
- El titular llama al extrínseco de depósito del pallet de la bóveda con una cantidad de activo.
- El pallet mueve el activo subyacente hacia dentro y acuña participaciones proporcionales para el depositante, en un único extrínseco atómico.
Retiro / canje
- Orden inverso: se queman las participaciones y después se transfiere de vuelta la cantidad correspondiente del activo subyacente.
La actualización del estado ocurre dentro del almacenamiento propio del pallet (total de activos, total de participaciones) como parte del mismo extrínseco.
Como toda la operación es un único extrínseco contra un almacenamiento de pallet conocido, las wallets y los front-ends pueden simular las cantidades resultantes de participaciones/activos antes de enviarla.
¿Cómo se compara con una bóveda personalizada en Solidity?
Una bóveda ERC-4626 escrita a mano en Ethereum rastrea el total de activos, el total de participaciones y los saldos de participaciones por titular en el almacenamiento propio del contrato, convirtiendo entre ambos mediante las proporciones `totalAssets()`/`totalSupply()` en cada depósito y canje.
pallet-yield-vault realiza la misma lógica de conversión, pero como código de pallet compartido y auditado una sola vez, en lugar de un contrato específico por bóveda. Los depósitos traen el activo subyacente mediante una llamada entre pallets y acuñan participaciones proporcionales; los canjes queman participaciones y pagan el activo subyacente en el mismo extrínseco atómico.
Como la lógica de la bóveda no se redespliega por proyecto, no hay una superficie de reentrancia independiente que auditar por cada bóveda, como sí ocurre con los contratos ERC-4626 escritos de forma independiente — las garantías del pallet compartido se aplican de manera uniforme a todas las bóvedas registradas.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IEmpAssets {
function mintTokens(address to, address mintAddr, uint256 amount) external;
function transfer(address to, address mintAddr, uint256 amount) external;
function getMint(address mintAddr) external view returns (uint8, uint256, address, address, address);
function getTokenAccount(address owner, address mintAddr) external view returns (address, address, uint256, bool);
}
contract EmpYieldVault {
IEmpAssets public immutable empAssets;
address public immutable mintAddr;
uint8 public immutable assetDecimals;
uint256 public totalShareSupply;
mapping(address => uint256) public shareBalance;
mapping(address => mapping(address => uint256)) public shareAllowance;
bool private locked;
event Approval(address indexed owner, address indexed spender, uint256 value);
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);
modifier nonReentrant() {
require(!locked, "REENTRANCY");
locked = true;
_;
locked = false;
}
constructor(IEmpAssets _empAssets, address _mintAddr) {
empAssets = _empAssets;
mintAddr = _mintAddr;
(uint8 dec,, , ,) = _empAssets.getMint(_mintAddr);
assetDecimals = dec;
}
function totalAssets() public view returns (uint256 assets) {
(, , assets, ) = empAssets.getTokenAccount(address(this), mintAddr);
}
function convertToShares(uint256 assets) public view returns (uint256) {
return totalShareSupply == 0 ? assets : (assets * totalShareSupply) / totalAssets();
}
function convertToAssets(uint256 shares) public view returns (uint256) {
return totalShareSupply == 0 ? shares : (shares * totalAssets()) / totalShareSupply;
}
function _mint(address to, uint256 amount) internal {
totalShareSupply += amount;
shareBalance[to] += amount;
}
function _burn(address from, uint256 amount) internal {
shareBalance[from] -= amount;
totalShareSupply -= amount;
}
function approve(address spender, uint256 amount) external returns (bool) {
shareAllowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function deposit(uint256 assets, address receiver) external nonReentrant returns (uint256 shares) {
require(assets > 0, "zero assets");
empAssets.transfer(address(this), mintAddr, assets);
shares = convertToShares(assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
function redeem(uint256 shares, address receiver, address owner) external nonReentrant returns (uint256 assets) {
require(shares > 0, "zero shares");
if (msg.sender != owner) {
uint256 allowed = shareAllowance[owner][msg.sender];
require(allowed >= shares, "allowance too low");
if (allowed != type(uint256).max) {
shareAllowance[owner][msg.sender] = allowed - shares;
}
}
assets = convertToAssets(shares);
_burn(owner, shares);
empAssets.transfer(receiver, mintAddr, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
}
Cómo usar pallet-yield-vault
1. Mapa conceptual
| Elemento | Objeto on-chain | Propósito |
|---|---|---|
| Activo subyacente | ID de activo existente en pallet-emp-assets | El activo equivalente a ERC-20 que se deposita (p. ej. DUSD) |
| Contabilidad de participaciones | Almacenamiento de pallet-yield-vault | Rastrea la propiedad proporcional de cada depositante en la bóveda |
| Custodia de la bóveda | Saldo de activo controlado por el pallet | Mantiene los activos subyacentes en nombre de todos los depositantes |
| Estado de la bóveda | Entrada de almacenamiento de pallet-yield-vault | Almacena share_mintel total de activos, el total de participacionespda_bump, y las comisiones configuradas |
| Administrador de la bóveda | AccountId32 u origen de gobernanza | Autoridad sobre los parámetros de la bóveda (cuando son configurables) |
Un extrínseco siempre mueve exactamente una cantidad del activo subyacente y, si es necesario, acuña/quema el número exacto y proporcional de participaciones en la misma llamada atómica.
2. Flujo de extremo a extremo (simplificado)
1) depósito (activos -> participaciones)
2) el cliente (wallet o dApp) construye el extrínseco
- El cliente llama al extrínseco de depósito del pallet de la bóveda con (vaultId, amount)
- Firma y envía mediante @polkadot/api
3) pallet-yield-vault
- El pallet transfiere el activo subyacente del depositante a la custodia de la bóveda
- Calcula shares = deposit_amount * total_shares / total_assets
- Acuña participaciones para el depositante en el almacenamiento propio del pallet de la bóveda
- Actualiza vault_state.total_assets
- Emite un evento Deposited
4) previsualización en la wallet: como el estado de la bóveda se conoce on-chain, un cliente puede estimar -X activos y +Y participaciones antes de firmar
5) el canje/retiro (participaciones -> activos) es la misma secuencia a la inversa
3. Lo que realmente escribes
A diferencia de Ethereum, donde lanzar una nueva bóveda implica escribir y auditar un nuevo contrato ERC-4626, usar pallet-yield-vault en EmpoorioChain es una integración del lado del cliente: registra un ID de bóveda (o usa uno existente) y después llama a los extrínsecos de depósito/canje mediante @polkadot/api. No hay ningún programa en Rust que escribir, compilar o desplegar por bóveda — la lógica de contabilidad de participaciones (tasa 1:1 para el primer depositante, depósitos posteriores a supply*assets/totalAssets) vive una sola vez dentro del pallet auditado.
// EmpoorioChain implements the deposit/redeem lifecycle natively via
// pallet_yield_vault (index 77) — there is no separate on-chain "vault
// program" for an app team to write, build, and deploy. The pallet holds
// the underlying pallet_emp_assets balance and tracks share issuance in
// its own storage; client code just calls its extrinsics.
import { ApiPromise, WsProvider, Keyring } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
const keyring = new Keyring({ type: "sr25519" });
const depositor = keyring.addFromUri("//your-seed-here");
const vaultId = 1;
async function deposit(assets) {
const hash = await api.tx.yieldVault
.deposit(vaultId, assets)
.signAndSend(depositor);
return hash.toString();
}
async function redeem(shares) {
const hash = await api.tx.yieldVault
.redeem(vaultId, shares)
.signAndSend(depositor);
return hash.toString();
}


