Разработка системы автоматической миграции с дедлайном
Миграция токенов нужна при обновлении токеномики, слиянии протоколов, или исправлении ошибки в исходном токен-контракте. Задача выглядит просто: держатели обменивают старый токен на новый в соотношении 1:1 (или по заданному ratio). Но без дедлайна и механизма сжигания система превращается в вечно висящее обязательство: протокол должен держать новые токены наготове бесконечно, а старые токены циркулируют параллельно с новыми, создавая confusion на рынке.
Дедлайн с автоматическим сжиганием немигрированных токенов решает обе проблемы.
Архитектура контракта миграции
Три участника системы:
OldToken — существующий ERC-20 контракт. Контракт миграции должен получить от держателей allowance или работать через transferFrom. Нельзя изменить существующий контракт — только взаимодействуем с ним.
NewToken — новый ERC-20 контракт. Должен поддерживать mint или предварительно пополнен достаточным количеством токенов для всех потенциальных мигрантов. Контракт миграции должен иметь MINTER_ROLE или холдинг новых токенов.
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 требует, чтобы новый owner принял права отдельной транзакцией. Для контракта, управляющего миграцией токенов с дедлайном, это критично.
Механизм сжигания после дедлайна
После истечения дедлайна все немигрированные старые токены, которые накопились на контракте, должны быть сожжены. Также нужно вернуть или сжечь неиспользованные новые токены.
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
Merkle Proof для snapshot-based миграции
Если миграция основана на снапшоте (балансы на конкретный блок, до деплоя нового контракта), пользователи не отдают токены — они доказывают право на получение новых через Merkle Proof:
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 дней даже для простых миграций.
Сроки разработки: 3-5 рабочих дней для базовой системы миграции, 7-10 дней для snapshot-based с Merkle Proof и subgraph. Стоимость рассчитывается индивидуально.







