Históricamente, Ethereum ha tenido una alta barrera de entrada con interfaces difíciles de entener y usar. En mayo de 2025 se lanzó la mejora que en mi opinión es la más significativa en experiencia de usuario en Ethereum, el EIP‑7702. Gracias a ella, una EOA, o sea una cuenta tradicional de Ethereum, puede convertirse temporalmente en un contrato inteligente.
Históricamente, obtener un préstamo en Aave toma 3 transacciones, en este artículo veremos como realizarlo en una sola transacción usando el EIP-7702
En este artículo mostraremos cómo usar EIP‑7702 para obtener un préstamo en Aave con un solo click. Aave es la plataforma de préstamos colateralizados más grande de Ethereum, que maneja más de 50 billones de dólares. Antes de esta mejora, tomar un préstamo implicaba al menos tres transacciones, mientras que con EIP‑7702, agrupamos esas tres acciones en una sola.
Empecemos por entender cómo funciona Aave y luego construiremos paso a paso un frontend que realiza todo el proceso con un único botón.
Cómo Obtener un Préstamo en Aave
Supongamos que quieres obtener $300 en WBTC dejando $1000 en DAI como garantía, lo que también llamamos colateral. Para hacerlo primero debes aprobar que el contrato de Aave sea capaz de depositar tu DAI mediante la función approve, luego depoitas tu DAI como colateral llamando la función suuply y finalmente obtienes el préstamo ejecutando la función borrow. Desde el punto de vista del usuario esto no es evidente y antes no teníamos una manera de expresarlo con una sola transacción.
Ejemplo de una posición en Aave con un préstamo de $300 en WBTC con $1000 en DAI como garantía
En nuestro ejemplo veremos paso por paso cómo realizar el mismo proceso con un solo botón. Veremos cómo hablitar el EIP-7702 en tu wallet para que sea capaz de agrupar transacciones y luego construiremos un frontend para interactuar con Aave desde tu smart contract wallet.
Convierte tu Wallet Temporalmente en un Smart Contract
Con EIP-7702 puedes inyectar cualquier codigo a tu wallet. Otra opción, para ahorrar gas, es hacer que tu wallet apunte a cualquier otro contrato de manera que "copia el código" de esa dirección, a esto le llamamos delegación de wallet. En este ejemplo vamos a lanzar y delegar hacia un contrato Multicall7702
basado en Multicall
que es un contrato que agrupa varias transacciónes pero adapaté para EIP-7702 pues solo permite al creador del contrato ejecutarlo. De no hacerlo, tu wallet quedaría expuesta.
Primero lanzaremos el contrato Multicall7702
y luego haremos que nuestra wallet apunte hacia ese contrato.
Las interfaces de wallet aún no soportan EIP-7702, así que por ahora usaremos foundry en su lugar. Además, como desarrolladores de dApps no deberíamos de preocuparnos por realizar delegación en nuestras interfaces. Por razones de seguridad esto debería implementarse a nivel de Wallet, no de dApp.
Lanza el Contrato Multicall7702
Lanza el siguiente contrato en la red que deses probar este ejemplo y guarda el address. Yo lo lanzaré en Scroll Testnet, donde puedes conectarte desde chainlist y obtener fondos en el faucet envíando un mensaje a @scroll_up_sepolia_bot
y luego escribir /drop 0xTU_WALLET_ADDRESS
en el chat de faucet.
Si lanzas en otra chain explicaré durante el proceso qué cambios debes hacer.
El único cambio que debes realizar antes de lanzar este contrato es reemplazar 0xTU_DIRECCION_EOA
con la wallet que deseas convertir en EOA.
// SPDX-License-Identifier: MIT
// Derivado del Multicall3 de Makerdao
// NO HA SIDO AUDITADO, NO USAR EN PRODUCCIÓN
pragma solidity 0.8.12;
contract Multicall7702 {
struct Call3Value { address target; bool allowFailure; uint256 value; bytes callData; }
struct Result { bool success; bytes returnData; }
address immutable public owner = 0xTU_DIRECCION_EOA;
function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData) {
require(owner == msg.sender, "Only owner"); // Added this to prevent others from executing transactions on your behalf
uint256 valAccumulator;
uint256 length = calls.length;
returnData = new Result[](length);
Call3Value calldata calli;
for (uint256 i = 0; i < length;) {
Result memory result = returnData[i];
calli = calls[i];
uint256 val = calli.value;
unchecked { valAccumulator += val; }
(result.success, result.returnData) = calli.target.call{value: val}(calli.callData);
assembly {
if iszero(or(calldataload(add(calli, 0x20)), mload(result))) {
mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020)
mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017)
mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000)
revert(0x00, 0x84)
}
}
unchecked { ++i; }
}
require(msg.value == valAccumulator, "Multicall3: valor incorrecto");
}
}
Delega tu Wallet a Multicall7702
Instala foundry si no lo tienes.
curl -L https://foundry.paradigm.xyz | bash
foundryup
Para delegar tu wallet ejecuta el siguiente comando reemplazando los valores de TU_LLAVE_PRIVADA
y 0xDIRECCION_DE_TU_CONTRATO
. Se deseas lanzar en otra chain, cambia la --rpc-url
que puedes obtener desde Chainlist.
cast send \
--rpc-url https://scroll-public.scroll-testnet.quiknode.pro \
--private-key TU_LLAVE_PRIVADA \
--auth 0xDIRECCION_DE_TU_CONTRATO $(cast az)
Puedes verificar que la delegación fue exitosa con este comando.
cast code \
--rpc-url https://scroll-public.scroll-testnet.quiknode.pro \
0xTU_DIRECCION_EOA
Donde deberías la dirección de Multicall7702 en el siguiente formato.
0xef0100TU_DIRECCION_EOA
Esto es el prefijo de delegación EIP-7702 0xef0100
más la dirección del contrato. Esto significa que tu wallet apunta hacia el codigo de tu contrato Multicall7702.
Construye el Fontend
A continación realizaremos un frontend con el código mínimo para tener un demo funcional del EIP-7702. Con todo el código comentado para que puedas entender qué se necesita para ser compatible con este nuevo estándar.
En nuestra dApp, el usuario será capáz de seleccionar una cantidad de WBTC que desea obtener como préstamo respaldado por DAI. Si deseas usar otras monedas también indicaré los cambios necesarios que debes realizar.
Vamos a construir una implementación muy simple de una dApp habilitada con EIP-7702
Vamos a ocupar dos archivos, index.html
para describir toda la UI y blockchain_stuff.js
para toda la lógica.
Crea una carpeta nueva y colócalos uno al lado del otro.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<input id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: block"></input>
<p id="account_address" style="display: none"></p>
<p id="web3_message"></p>
<p id="contract_state"></p>
<label for="lending_token_amount">DAI supply amount:</label>
<input type="input" value="" id="lending_token_amount" placeholder="e.g. 1000"></input>
<br>
<label for="borrowed_token_amount">WBTC borrow amount:</label>
<input type="input" value="" id="borrowed_token_amount" placeholder="e.g. 0.005"></input>
<br>
<input type="button" value="One click loan" onclick="_oneClickLoan()"></input>
<br>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
<script type="text/javascript" src="blockchain_stuff.js"></script>
</body>
</html>
<script>
function _oneClickLoan()
{
lendingTokenAmount = document.getElementById("lending_token_amount").value
borrowedTokenAmount = document.getElementById("borrowed_token_amount").value
oneClickLoan(lendingTokenAmount, borrowedTokenAmount)
}
</script>
En cuanto a javascript el siguiente archivo contiene toda la lógica necesaria para conectar tu wallet e interactuar con tu wallet como si fuera un contrato.
Si deseas lanzar en una chain que no sea Scroll Sepolia deberás editar el NETWORK_ID
y también edita LENDING_ADDRESS
y BORROWED_TOKEN
para seleccionar los tokens que quieras como préstamo y colateral. Finalmente edita la AAVE_POOL_ADDRESS
en base a los deployments oficiales de Aave.
blockchain_stuff.js
// Si deseas lanzarlo en otra red edita esto
const NETWORK_ID = 534351 // Scroll Sepolia
// Edita los siguientes datos
const LENDING_ADDRESS = "0x7984e363c38b590bb4ca35aed5133ef2c6619c40" //Aave Testnet DAI en Scroll Sepolia
const BORROWED_TOKEN = "0x5ea79f3190ff37418d42f9b2618688494dbd9693" //Aave Testnet WBTC en Scroll Sepolia
const AAVE_POOL_ADDRESS = "0x48914C788295b5db23aF2b5F0B3BE775C4eA9440" // Aave Testnet Pool en Scroll Sepolia
// ABIs que usaremos, para mantener el código más limpio podrías crear archivos json con cada ABI
const MULTICALL_ABI = [
{
"inputs": [
{
"components": [
{"internalType": "address", "name": "target", "type": "address"},
{"internalType": "bool", "name": "allowFailure", "type": "bool"},
{"internalType": "uint256", "name": "value", "type": "uint256"},
{"internalType": "bytes", "name": "callData", "type": "bytes"}
],
"internalType": "struct Multicall3.Call3Value[]",
"name": "calls",
"type": "tuple[]"
}
],
"name": "aggregate3Value",
"outputs": [
{
"components": [
{"internalType": "bool", "name": "success", "type": "bool"},
{"internalType": "bytes", "name": "returnData", "type": "bytes"}
],
"internalType": "struct Multicall3.Result[]",
"name": "returnData",
"type": "tuple[]"
}
],
"stateMutability": "payable",
"type": "function"
}
];
const ERC20_ABI = [
{
"inputs": [
{"internalType": "address", "name": "spender", "type": "address"},
{"internalType": "uint256", "name": "value", "type": "uint256"}
],
"name": "approve",
"outputs": [
{"internalType": "bool", "name": "", "type": "bool"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{"internalType": "uint8", "name": "", "type": "uint8"}
],
"stateMutability": "view",
"type": "function"
}
];
const AAVE_POOL_ABI = [
{
"inputs": [
{"internalType": "address", "name": "asset", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"},
{"internalType": "address", "name": "onBehalfOf", "type": "address"},
{"internalType": "uint16", "name": "referralCode", "type": "uint16"}
],
"name": "supply",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "asset", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"},
{"internalType": "uint256", "name": "interestRateMode", "type": "uint256"},
{"internalType": "uint16", "name": "referralCode", "type": "uint16"},
{"internalType": "address", "name": "onBehalfOf", "type": "address"}
],
"name": "borrow",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
// Variables que usaremos
var lendingTokenContract
var borrowedTokenContract
var aavePoolContract
var myERC7702Account
var accounts
var web3
// Cuando el usuario cambie de cuenta o red, se refresca la página
function metamaskReloadCallback() {
window.ethereum.on('accountsChanged', (accounts) => {
document.getElementById("web3_message").textContent="Se cambió el account, refrescando...";
window.location.reload()
})
window.ethereum.on('networkChanged', (accounts) => {
document.getElementById("web3_message").textContent="Se el network, refrescando...";
window.location.reload()
})
}
// Función para inicializar web3
const getWeb3 = async () => {
return new Promise((resolve, reject) => {
if(document.readyState=="complete")
{
if (window.ethereum) {
const web3 = new Web3(window.ethereum)
window.location.reload()
resolve(web3)
} else {
reject("must install MetaMask")
document.getElementById("web3_message").textContent="Error: Porfavor conéctate a Metamask";
}
}else
{
window.addEventListener("load", async () => {
if (window.ethereum) {
const web3 = new Web3(window.ethereum)
resolve(web3)
} else {
reject("must install MetaMask")
document.getElementById("web3_message").textContent="Error: Please install Metamask";
}
});
}
});
};
// Función para iniciar la app, lee los contractos y conecta la wallet
async function loadDapp() {
metamaskReloadCallback()
document.getElementById("web3_message").textContent="Please connect to Metamask"
var awaitWeb3 = async function () {
web3 = await getWeb3()
web3.eth.net.getId((err, netId) => {
if (netId == NETWORK_ID) {
var awaitContract = async function () {
lendingTokenContract = new web3.eth.Contract(ERC20_ABI, LENDING_ADDRESS);
borrowedTokenContract = new web3.eth.Contract(ERC20_ABI, BORROWED_TOKEN);
aavePoolContract = new web3.eth.Contract(AAVE_POOL_ABI, AAVE_POOL_ADDRESS);
document.getElementById("web3_message").textContent="You are connected to Metamask"
onContractsInitCallback()
web3.eth.getAccounts(function(err, _accounts){
accounts = _accounts
if (err != null)
{
console.error("An error occurred: "+err)
} else if (accounts.length > 0)
{
onWalletConnectedCallback()
document.getElementById("account_address").style.display = "block"
} else
{
document.getElementById("connect_button").style.display = "block"
}
});
};
awaitContract();
} else {
document.getElementById("web3_message").textContent="Please connect to Goerli";
}
});
};
awaitWeb3();
}
async function connectWallet() {
await window.ethereum.request({ method: "eth_requestAccounts" })
accounts = await web3.eth.getAccounts()
onWalletConnectedCallback()
}
loadDapp()
// Este callback se ejecuta cuando se han cargado los contractos, usualmente aquí puedes leer los datos de los contractos
const onContractsInitCallback = async () => {
}
// Este callback se ejecuta cuando se ha conectado la wallet, en este caso asignamos el contracto de multicall a nuestra wallet 7702
const onWalletConnectedCallback = async () => {
myERC7702Account = new web3.eth.Contract(MULTICALL_ABI, accounts[0]);
}
// Función que se llama desde el frontend que recibe las cantidades del préstamo
const oneClickLoan = async (lendingTokenAmount, borrowedTokenAmount) => {
// Convierte de formato ether a wei dependiendo de la cantidad de decimales de cada token
const scrDecimals = await lendingTokenContract.methods.decimals().call();
const borrowedDecimals = await borrowedTokenContract.methods.decimals().call();
const supplyAmount = Web3.utils.toBN(
(parseFloat(lendingTokenAmount) * Math.pow(10, parseInt(scrDecimals))).toLocaleString('fullwide', {useGrouping:false})
);
const borrowAmount = Web3.utils.toBN(
(parseFloat(borrowedTokenAmount) * Math.pow(10, parseInt(borrowedDecimals))).toLocaleString('fullwide', {useGrouping:false})
);
// Generamos las llamadas de los contratos y las encodeamos de manera que sea compatible con nuestro contrato de multicall
const approveCall = lendingTokenContract.methods.approve(AAVE_POOL_ADDRESS, supplyAmount).encodeABI();
const supplyCall = aavePoolContract.methods.supply(LENDING_ADDRESS, supplyAmount, accounts[0], 0).encodeABI();
const interestRateMode = 2;
const referralCode = 0;
const borrowCall = aavePoolContract.methods.borrow(
BORROWED_TOKEN,
borrowAmount.toString(),
interestRateMode,
referralCode,
accounts[0]
).encodeABI();
const calls = [
{
target: LENDING_ADDRESS,
allowFailure: false,
value: 0,
callData: approveCall
},
{
target: AAVE_POOL_ADDRESS,
allowFailure: false,
value: 0,
callData: supplyCall
},
{
target: AAVE_POOL_ADDRESS,
allowFailure: false,
value: 0,
callData: borrowCall
}
];
// Ejecutamos la llamada dirigida a nuestra wallet 7702
const result = await myERC7702Account.methods.aggregate3Value(calls)
.send({ from: accounts[0], gas: 0, value: 0 })
.on('transactionHash', function(hash){
document.getElementById("web3_message").textContent="Executing...";
})
.on('receipt', function(receipt){
document.getElementById("web3_message").textContent="Success."; })
.catch((revertReason) => {
console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash)
});
}
Prueba tu dApp
Instala Node si aún no lo tienes. Te recomiendo instalarlo a través de NVM.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
nvm install 22
# Reinicia o recarga tu terminal
Instala un servidor web.
npm i -g lite-server
I levanta el frontend.
lite-server
Tu webapp debería estár en http://localhost:3000
.
Una vez obtengas tu préstamo puedes ver el detalle de lo ocurrido en Etherscan y en el frontend de Aave.
En Etherscan podrás ver el detalle de todas las transacciones agrupadas en una sola
Para seguir aprendiendo sobre esta y otras nuevas funcionalidades que hace posible este cambio puedes ver la Especificación EIP-7702.
¡Gracias por ver este artículo!
Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.
Top comments (0)