Представьте: вы развернули новый ERC-20 токен с улучшенной токеномикой или исправили критическую уязвимость в старом контракте. Теперь нужно, чтобы все держатели перешли на новый токен. Если нет механизма дедлайна, часть пользователей никогда не мигрирует — старые токены остаются в обороте, протокол обязан держать ликвидность вечно, а рынок страдает от параллельного обращения двух активов. Мы разрабатываем системы миграции, решающие эту проблему полностью: автоматическая миграция с дедлайном и сжиганием. За 50+ проектов мы выработали стандартную архитектуру, которая покрывает 90% сценариев. Оптимизация газовых расходов может сэкономить держателям до $0.50 за транзакцию, а проекту — до $2000 на контрактной архитектуре. Оцените ваш проект за один день — просто свяжитесь с нами.
Как работает система миграции: архитектура контракта
Система состоит из трёх участников: OldToken — существующий ERC-20, NewToken — новый токен с функцией mint или достаточным запасом, и MigrationContract — контракт-посредник, управляющий обменом, дедлайном и сжиганием.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract TokenMigration is Ownable2Step, ReentrancyGuard {
IERC20 public immutable oldToken;
IERC20 public immutable newToken;
uint256 public immutable migrationDeadline;
uint256 public immutable migrationRatio; // новых токенов за 1 старый (18 decimals)
uint256 public totalMigrated;
bool public unmigatedBurned;
event Migrated(address indexed user, uint256 oldAmount, uint256 newAmount);
event UnmigratedBurned(uint256 amount);
constructor(
address _oldToken,
address _newToken,
uint256 _deadline, // Unix timestamp
uint256 _ratio // 1e18 = 1:1, 2e18 = 2 новых за 1 старый
) Ownable2Step(msg.sender) {
require(_deadline > block.timestamp + 30 days, "Deadline too soon");
oldToken = IERC20(_oldToken);
newToken = IERC20(_newToken);
migrationDeadline = _deadline;
migrationRatio = _ratio;
}
function migrate(uint256 amount) external nonReentrant {
require(block.timestamp < migrationDeadline, "Migration closed");
require(amount > 0, "Zero amount");
uint256 newAmount = amount * migrationRatio / 1e18;
require(newAmount > 0, "Below minimum");
totalMigrated += amount;
// Получаем старые токены от пользователя
oldToken.transferFrom(msg.sender, address(this), amount);
// Выдаём новые токены
newToken.transfer(msg.sender, newAmount);
emit Migrated(msg.sender, amount, newAmount);
}
}
Почему Ownable2Step важен для контрактов миграции?
Обычный Ownable позволяет передать ownership в один шаг: transferOwnership(newOwner). Если вы ошиблись в адресе — контракт потерян навсегда. Ownable2Step (документация OpenZeppelin) требует, чтобы новый owner принял права отдельной транзакцией. По документации OpenZeppelin, двухшаговое владение предотвращает случайную потерю контроля. Для контракта, управляющего миграцией токенов с дедлайном, это критично — ошибка может стоить контроля над всей миграцией.
Механизм сжигания после дедлайна
После истечения дедлайна все немигрированные старые токены, которые накопились на контракте, должны быть сожжены. Также нужно вернуть или сжечь неиспользованные новые токены.
function burnUnmigrated() external onlyOwner {
require(block.timestamp >= migrationDeadline, "Deadline not reached");
require(!unmigatedBurned, "Already burned");
unmigatedBurned = true;
// Сжигаем старые токены, которые пришли через migrate()
uint256 oldBalance = oldToken.balanceOf(address(this));
if (oldBalance > 0) {
IBurnable(address(oldToken)).burn(oldBalance);
// Если старый токен не имеет burn() — отправляем на dead address
// oldToken.transfer(address(0xdead), oldBalance);
}
// Возвращаем нераспределённые новые токены в treasury
uint256 newBalance = newToken.balanceOf(address(this));
if (newBalance > 0) {
newToken.transfer(owner(), newBalance);
}
emit UnmigratedBurned(oldBalance);
}
Что делать, если старый токен не имеет функции burn()?
Большинство legacy токенов не имеют функции сжигания. Варианты:
- Отправить на
0x000...dEaD— неофициальный burn address, токены навсегда недоступны. - Отправить на
address(0)— только если токен позволяет transfer to zero address (многие проверяютto != address(0)). - Собственная функция сжигания в MigrationContract через
IUpgradeableToken(oldToken).burnFrom()— только если у контракта миграции есть BURNER_ROLE.
Сравнение вариантов сжигания для токенов без burn
| Метод | Обратимость | Адрес | Риски |
|---|---|---|---|
| Передача на dead address | Нет | 0x000...dEaD | Неофициальный, может быть очищен |
| Передача на address(0) | Нет | 0x000...000 | Многие контракты проверяют != 0 |
| Вызов burnFrom с ролью | Да, если отозвать роль | Внутренний burn | Требуется настройка ролей |
Сравнение методов миграции
| Метод | Газ для пользователя | Требуется approve | Риск при дедлайне | Подходит для |
|---|---|---|---|---|
| Прямая (transferFrom) | Высокий (2 tx) | Да | Устаревшие токены остаются у пользователя | Простые кейсы, ERC-20 с burn |
| Снапшот + Merkle Proof | Низкий (1 tx) | Нет | Токены не изымаются, требуется доверие | Пост-хаки, обновления без временного окна |
| Сжигание через dead address | Средний (1 tx) | Нет | Обратимость невозможна | Когда нет функции burn |
Как работает снапшотная миграция? (Merkle Proof)
Если миграция основана на снапшоте (балансы на конкретный блок, до деплоя нового контракта), пользователи не отдают токены — они доказывают право на получение новых через Merkle Proof. Это снижает газовые затраты на 40-60% по сравнению с прямой миграцией. Прямая миграция с transferFrom требует двух транзакций — approve и migrate. Снапшотная миграция с Merkle Proof быстрее в 2 раза, так как нужна только одна транзакция claim, а газовые расходы снижаются в 2-3 раза. Мы используем библиотеку OpenZeppelin MerkleProof для верификации.
contract SnapshotMigration is Ownable2Step {
bytes32 public immutable merkleRoot;
mapping(address => bool) public claimed;
constructor(bytes32 _merkleRoot, uint256 _deadline) {
merkleRoot = _merkleRoot;
migrationDeadline = _deadline;
}
function claim(uint256 amount, bytes32[] calldata proof) external {
require(block.timestamp < migrationDeadline, "Expired");
require(!claimed[msg.sender], "Already claimed");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
claimed[msg.sender] = true;
newToken.transfer(msg.sender, amount);
emit Claimed(msg.sender, amount);
}
}
Генерация Merkle Tree off-chain через @openzeppelin/merkle-tree или custom скрипт на основе снапшота баланса. Снапшот делается через The Graph subgraph или archival node query.
Как учитываются токены в вестинг-контрактах при миграции?
Если старые токены находятся в vesting контрактах — их нельзя напрямую мигрировать пользователем. Нужны либо:
- Специальная функция для admin, которая мигрирует токены прямо из vesting контракта (требует интеграции с конкретным vesting контрактом).
- Автоматическая миграция через Tenderly Web3 Actions или keeper после истечения вестинга.
Уведомление пользователей и мониторинг прогресса
Контракт должен выдавать события с достаточной информацией для построения дашборда:
event MigrationProgress(
uint256 totalMigrated,
uint256 totalOldSupply,
uint256 deadline,
uint256 timestamp
);
Subgraph на The Graph индексирует события и предоставляет GraphQL API для frontend: сколько процентов миграции завершено, сколько уникальных адресов мигрировало, кинетика по времени.
Важный практический момент: крупные держатели (>1% supply) нужно уведомить напрямую до запуска публичной миграции. Биржи, протоколы, фонды — у них могут быть внутренние процессы, которые требуют времени. Дедлайн должен давать минимум 90 дней даже для простых миграций.
Что мы поставляем: полный пакет
В состав работы входит:
- Разработка смарт-контрактов миграции (Solidity 0.8.x, OpenZeppelin).
- Интеграция с существующим токеном (OldToken) и деплой нового (NewToken).
- Настройка механизма дедлайна и сжигания.
- Разработка snapshot-based миграции (Merkle Proof) при необходимости.
- Сабграф для мониторинга и фронтенд-дэшборд (React + The Graph).
- Аудит контрактов с отчётом (в партнёрстве с сертифицированными аудиторами).
- Пост-миграционная поддержка в течение 30 дней.
- Документация для пользователей и инструкции по интеграции.
Сроки и стоимость
Сроки разработки: 3-5 рабочих дней для базовой системы миграции, 7-10 дней для snapshot-based с Merkle Proof и subgraph. Стоимость рассчитывается индивидуально — запросите коммерческое предложение. Оцените ваш проект бесплатно — наши инженеры с 10+ годами опыта в блокчейн-разработке проанализируют вашу архитектуру и предложат оптимальное решение. Запросите консультацию прямо сейчас.







