Automatic Token Migration System with Deadline

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Automatic Token Migration System with Deadline
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1260
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    874
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1092
  • image_logo-advance_0.png
    B2B Advance company logo design
    563
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    830

Automated Token Migration System with Deadline

Token migration is necessary when updating tokenomics, merging protocols, or fixing errors in the original token contract. The task seems simple: holders exchange old tokens for new ones at a 1:1 ratio (or a specified ratio). But without a deadline and burn mechanism, the system becomes an eternal hanging obligation: the protocol must keep new tokens ready indefinitely, and old tokens circulate parallel to new ones, creating market confusion.

A deadline with automatic unmigrated token burning solves both problems.

Migration Contract Architecture

Three participants in the system:

OldToken — existing ERC-20 contract. The migration contract must get allowance from holders or work via transferFrom. You can't modify the existing contract — only interact with it.

NewToken — new ERC-20 contract. Must support mint or be pre-funded with enough tokens for all potential migrants. The migration contract must have MINTER_ROLE or hold new tokens.

MigrationContract — exchange logic, deadline management, burn mechanism.

// 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; // new tokens per 1 old (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 new per 1 old
    ) 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;
        
        // Get old tokens from user
        oldToken.transferFrom(msg.sender, address(this), amount);
        
        // Issue new tokens
        newToken.transfer(msg.sender, newAmount);
        
        emit Migrated(msg.sender, amount, newAmount);
    }
}

Why Ownable2Step is Important

Standard Ownable lets you transfer ownership in one step: transferOwnership(newOwner). Mistype the address — the contract is lost. Ownable2Step requires the new owner to accept ownership in a separate transaction. For a contract managing token migration with a deadline, this is critical.

Burn Mechanism After Deadline

After the deadline expires, all unmigrated old tokens accumulated on the contract should be burned. Also return or burn unused new tokens.

function burnUnmigrated() external onlyOwner {
    require(block.timestamp >= migrationDeadline, "Deadline not reached");
    require(!unmigatedBurned, "Already burned");
    
    unmigatedBurned = true;
    
    // Burn old tokens that came through migrate()
    uint256 oldBalance = oldToken.balanceOf(address(this));
    if (oldBalance > 0) {
        IBurnable(address(oldToken)).burn(oldBalance);
        // If old token doesn't have burn() — send to dead address
        // oldToken.transfer(address(0xdead), oldBalance);
    }
    
    // Return undistributed new tokens to treasury
    uint256 newBalance = newToken.balanceOf(address(this));
    if (newBalance > 0) {
        newToken.transfer(owner(), newBalance);
    }
    
    emit UnmigratedBurned(oldBalance);
}

When Old Token Doesn't Have burn()

Most legacy tokens don't have a burn function. Options:

  1. Send to 0x000...dEaD — unofficial burn address, tokens permanently inaccessible
  2. Send to address(0) — only if the token allows transfer to zero address (many check to != address(0))
  3. Custom burn function in MigrationContract via IUpgradeableToken(oldToken).burnFrom() — only if the migration contract has BURNER_ROLE

Merkle Proof for Snapshot-Based Migration

If migration is based on a snapshot (balances at a specific block, before new contract deployment), users don't give tokens — they prove their right to receive new ones via 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);
    }
}

Generate Merkle Tree off-chain via @openzeppelin/merkle-tree or a custom script based on balance snapshot. Snapshot via The Graph subgraph or archival node query.

Handling Vesting During Migration

If old tokens are in vesting contracts — they can't be directly migrated by the user. You need either:

  1. A special admin function that migrates tokens directly from the vesting contract (requires integration with the specific vesting contract)
  2. Automatic migration via Tenderly Web3 Actions or keeper

User Notification and Progress Monitoring

The contract should emit events with sufficient information to build a dashboard:

event MigrationProgress(
    uint256 totalMigrated,
    uint256 totalOldSupply,
    uint256 deadline,
    uint256 timestamp
);

A Graph subgraph indexes events and provides GraphQL API for frontend: what % of migration is complete, how many unique addresses migrated, momentum over time.

Important practical note: large holders (>1% supply) should be notified directly before public migration launch. Exchanges, protocols, funds — they may have internal processes requiring time. Deadline should provide minimum 90 days even for simple migrations.

Development timeline: 3-5 working days for basic migration system, 7-10 days for snapshot-based with Merkle Proof and subgraph. Cost calculated individually.