Wrapped ETH: WETH deposit и withdraw
Как обернуть ETH в WETH через deposit, проверить ERC-20 баланс и развернуть обратно через withdraw в ethers v6.
WETH — это ERC-20 версия ETH
ETH сам по себе не является ERC-20 токеном.
WETH решает эту проблему: deposit() принимает ETH и выпускает столько же WETH, а withdraw(amount) сжигает WETH и возвращает ETH.
Оборачиваем и разворачиваем ETH
Создайте проект:
mkdir weth-deposit-withdraw-example
cd weth-deposit-withdraw-example
npm init -y
npm pkg set type="module"
npm install ethers solc tsx
npm install --save-dev hardhat typescript @types/node
mkdir contracts scriptsСоздайте файл hardhat.config.ts:
import { defineConfig } from "hardhat/config";
export default defineConfig({
solidity: {
version: "0.8.20",
},
});Создайте файл contracts/WETH9Demo.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract WETH9Demo {
string public name = "Wrapped Ether";
string public symbol = "WETH";
uint8 public decimals = 18;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Deposit(address indexed dst, uint256 wad);
event Withdrawal(address indexed src, uint256 wad);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
receive() external payable {
deposit();
}
function deposit() public payable {
require(msg.value > 0, "zero deposit");
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
emit Transfer(address(0), msg.sender, msg.value);
}
function withdraw(uint256 wad) external {
require(balanceOf[msg.sender] >= wad, "insufficient WETH");
balanceOf[msg.sender] -= wad;
emit Withdrawal(msg.sender, wad);
emit Transfer(msg.sender, address(0), wad);
payable(msg.sender).transfer(wad);
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
require(to != address(0), "transfer to zero address");
require(balanceOf[msg.sender] >= amount, "insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}Создайте файл scripts/wrap-unwrap.ts:
import { readFileSync } from "node:fs";
import { ethers } from "ethers";
import solc from "solc";
const source = readFileSync("contracts/WETH9Demo.sol", "utf8");
const input = {
language: "Solidity",
sources: {
"WETH9Demo.sol": {
content: source,
},
},
settings: {
outputSelection: {
"*": {
"*": ["abi", "evm.bytecode.object"],
},
},
},
};
const output = JSON.parse(solc.compile(JSON.stringify(input)));
const contract = output.contracts["WETH9Demo.sol"].WETH9Demo;
const abi = contract.abi;
const bytecode = `0x${contract.evm.bytecode.object}`;
const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
// Первый тестовый аккаунт Hardhat. Не используйте этот ключ в mainnet.
const user = new ethers.Wallet(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
provider,
);
const factory = new ethers.ContractFactory(abi, bytecode, user);
const weth = await factory.deploy();
await weth.waitForDeployment();
const wethAddress = await weth.getAddress();
const wrapAmount = ethers.parseEther("1.5");
const unwrapAmount = ethers.parseEther("0.4");
console.log("WETH address:", wethAddress);
console.log("ETH before:", ethers.formatEther(await provider.getBalance(user.address)));
console.log("WETH before:", ethers.formatEther(await weth.balanceOf(user.address)));
const depositTx = await weth.deposit({
value: wrapAmount,
});
console.log("Deposit hash:", depositTx.hash);
const depositReceipt = await depositTx.wait();
if (!depositReceipt || depositReceipt.status !== 1) {
throw new Error("Транзакция deposit() не была успешно выполнена");
}
console.log("Deposit block:", depositReceipt.blockNumber);
console.log("WETH after deposit:", ethers.formatEther(await weth.balanceOf(user.address)));
const withdrawTx = await weth.withdraw(unwrapAmount);
console.log("Withdraw hash:", withdrawTx.hash);
const withdrawReceipt = await withdrawTx.wait();
if (!withdrawReceipt || withdrawReceipt.status !== 1) {
throw new Error("Транзакция withdraw() не была успешно выполнена");
}
console.log("Withdraw block:", withdrawReceipt.blockNumber);
console.log("WETH after withdraw:", ethers.formatEther(await weth.balanceOf(user.address)));
console.log("ETH after:", ethers.formatEther(await provider.getBalance(user.address)));Запустите локальную сеть Hardhat в отдельном терминале:
npx hardhat nodeВо втором терминале выполните скрипт:
npx tsx scripts/wrap-unwrap.tsСкрипт задеплоит локальный WETH-контракт, обернёт 1.5 ETH в 1.5 WETH, затем развернёт 0.4 WETH обратно в ETH.
Как это работает
weth.deposit({ value: wrapAmount }) — отправляет ETH в контракт и вызывает payable функцию deposit.
msg.value — количество ETH, которое пришло вместе с транзакцией.
balanceOf[msg.sender] += msg.value — начисляет WETH в том же количестве, что и отправленный ETH.
weth.balanceOf(user.address) — читает ERC-20 баланс WETH.
weth.withdraw(unwrapAmount) — сжигает WETH пользователя и отправляет ему ETH обратно.
ethers.parseEther("1.5") — переводит ETH в wei. WETH использует те же 18 decimals.
ETH баланс и WETH баланс
После deposit ETH баланс пользователя уменьшается:
ETH balance -= deposit amount + gas
WETH balance += deposit amountПосле withdraw WETH баланс уменьшается, а ETH баланс растёт:
WETH balance -= withdraw amount
ETH balance += withdraw amount - gasGas всё равно платится в ETH, потому что deposit и withdraw — это транзакции.
WETH в mainnet
В Ethereum mainnet canonical WETH9 находится по адресу:
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2Минимальный ABI для работы с ним:
const wethAbi = [
"function deposit() payable",
"function withdraw(uint256 wad)",
"function balanceOf(address account) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
];На публичной сети используйте реальный signer с ETH на gas и проверяйте адрес контракта для конкретной сети.
Частые ошибки
Думать, что WETH появляется без ETH → deposit должен получить ETH через { value }.
Путать ETH и WETH баланс → ETH читается через provider.getBalance(address), WETH — через weth.balanceOf(address).
Забывать gas на withdraw → чтобы развернуть WETH обратно в ETH, у кошелька всё равно должен быть ETH для комиссии.
Что дальше
- ERC-20: balanceOf и decimals — читать и форматировать WETH как обычный ERC-20
- ERC-20: approve и allowance — разрешить DeFi-контракту использовать WETH
- Отправка ETH — сравнить обычный ETH transfer с WETH deposit