китаев.tech

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

Создайте проект:

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

typescript
import { defineConfig } from "hardhat/config";

export default defineConfig({
  solidity: {
    version: "0.8.20",
  },
});

Создайте файл contracts/WETH9Demo.sol:

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

typescript
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 в отдельном терминале:

bash
npx hardhat node

Во втором терминале выполните скрипт:

bash
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 баланс пользователя уменьшается:

text
ETH balance  -= deposit amount + gas
WETH balance += deposit amount

После withdraw WETH баланс уменьшается, а ETH баланс растёт:

text
WETH balance -= withdraw amount
ETH balance  += withdraw amount - gas

Gas всё равно платится в ETH, потому что deposit и withdraw — это транзакции.


WETH в mainnet

В Ethereum mainnet canonical WETH9 находится по адресу:

text
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

Минимальный ABI для работы с ним:

typescript
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 появляется без ETHdeposit должен получить ETH через { value }.

Путать ETH и WETH баланс → ETH читается через provider.getBalance(address), WETH — через weth.balanceOf(address).

Забывать gas на withdraw → чтобы развернуть WETH обратно в ETH, у кошелька всё равно должен быть ETH для комиссии.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.