Vaults V2 Migration Technical Info

Technical Aspects of the Vaults Update

How have you tested the migration process, especially for large volumes?

We performed testing of the migration contract with 100% test coverage. To minimize risks, we created a batch of accounts with vault share tokens and tested the migration process on a local fork of the mainnet, using real vaults and accounts.

How are we ensuring the secure transfer of funds between the old and new contracts?

Migration smart contract guarantees secure transfers.

In case of a security breach, what measures are in place to recover or reimburse lost funds?

Emergency functions across all contracts allow us to swiftly respond to any unexpected situations, ensuring the safety of your assets.

Migration Smart Contract

Migration Contracts Addresses:

for Locus Yield ETH: 0xd25d0de43579223429c28f2d64183a47a79078C7

for DeFi Core Index: 0xf42402303BCA9d5575A8aC7b90CB18026c80354D

for Arbitrum Yield Index: 0xC7469254416Ad546A5F07e5530a0c3eA468F1FCE

The process works as follows:

1. You need to approve the funds transfer to our migration contract. 2. We will then execute a withdraw function that moves your approved tokens from the vault to the migration contract 3. Afterward, we execute a deposit function, transferring the funds to the new vault. From this point on, the migration contract will manage all share tokens and safeguard your portion of these tokens.

Migration Smart Contract

Please note that the final smart contract may differ slightly; before announcing requests, we will attach the final version

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../contracts/interfaces/IBaseVault.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Migration is Ownable, ReentrancyGuard {
    IBaseVault public vaultV1;
    IBaseVault public vaultV2;

    address public treasury;

    address[] public users;

    IERC20 public token;

    mapping(address user => uint256 balance) public userToBalance;

    address[] public notWithdrawnUsers;

    constructor(address _vaultV1, address[] memory _users, address _treasury) {
        vaultV1 = IBaseVault(_vaultV1);
        users = _users;
        treasury = _treasury;
        token = vaultV1.token();
        vaultV1.token().approve(treasury, type(uint256).max);
    }

    function setVaultV2(address _vaultV2) external onlyOwner {
        vaultV2 = IBaseVault(_vaultV2);
        vaultV1.token().approve(address(vaultV2), type(uint256).max);
    }

    function addUsers(address[] memory _newUsers) external onlyOwner {
        for (uint256 i = 0; i < _newUsers.length; i++) {
            users.push(_newUsers[i]);
        }
    }

    function withdraw() external nonReentrant {
        for (uint256 i = 0; i < users.length; i++) {
            uint256 userBalance = IERC20(address(vaultV1)).balanceOf(users[i]);
            if (userBalance == 0) {
                continue;
            }
            if (
                IERC20(address(vaultV1)).allowance(users[i], address(this)) <
                userBalance
            ) {
                if (!checkUserExistence(users[i])) {
                    notWithdrawnUsers.push(users[i]);
                }
                continue;
            }
            IERC20(address(vaultV1)).transferFrom(
                users[i],
                address(this),
                userBalance
            );

            userToBalance[users[i]] += userBalance;
        }
        if (IERC20(address(vaultV1)).balanceOf(address(this)) > 0) {
            vaultV1.withdraw();
        }
    }

    function withdrawUsersWithDetectedError() external nonReentrant {
        for (uint256 i = 0; i < notWithdrawnUsers.length; i++) {
            if (notWithdrawnUsers[i] == address(0)) {
                continue;
            }
            uint256 userBalance = IERC20(address(vaultV1)).balanceOf(
                notWithdrawnUsers[i]
            );
            if (
                userBalance == 0 ||
                IERC20(address(vaultV1)).allowance(
                    notWithdrawnUsers[i],
                    address(this)
                ) <
                userBalance
            ) {
                continue;
            }
            IERC20(address(vaultV1)).transferFrom(
                notWithdrawnUsers[i],
                address(this),
                userBalance
            );

            userToBalance[notWithdrawnUsers[i]] += userBalance;

            notWithdrawnUsers[i] = address(0);
        }
        vaultV1.withdraw();
    }

    function deposit() external nonReentrant {
        vaultV2.deposit(token.balanceOf(address(this)), address(this));
    }

    function emergencyExit() external onlyOwner {
        vaultV1.token().transfer(treasury, token.balanceOf(address(this)));
        IERC20(address(vaultV2)).transfer(
            treasury,
            IERC20(address(vaultV2)).balanceOf(address(this))
        );
        IERC20(address(vaultV1)).transfer(
            treasury,
            IERC20(address(vaultV1)).balanceOf(address(this))
        );
    }

    function checkUserExistence(address _user) internal view returns (bool) {
        for (uint256 i = 0; i < notWithdrawnUsers.length; i++) {
            if (notWithdrawnUsers[i] == _user) {
                return true;
            }
        }
        return false;
    }
}

Valuable points

1. Before approval you can check yourself in array users, that you are in the list of migrating users 2. We made such architecture, that everyone can easily do this process yourself. We did that to exclude the case in your mind, that we can withdraw but not deposit. 3. There's an emergency withdraw function that moves all funds to the treasury address. You can check this address before giving your approval

4. We've also included a function to add users in case we unintentionally miss someone. Importantly, we can only add new addresses and cannot remove old ones.

5. There's a single onlyOwner modifier, which means only we can initiate specific functions such as adding users and initiating emergency withdrawals 6. This contract is not upgradeable, so we can’t change logic after deploy

Variables

vaultV1 - Address of the old vault.

vaultV2 - Address of the new vault.

token - Address of the want token (USDC/USDT/etc).

treasury - Address of the treasury.

users - List of users who are intended to be migrated.

userToBalance - Mapping that tracks users' balances. After calling the withdraw function, you can easily verify your migration status by checking your balance. If it's greater than 0, your money was successfully withdrawn.

notWithdrawnUsers - List of users who haven't approved their tokens for migration. This list is used to track users who will be withdrawn later using the `withdrawUsersWithDetectedError` function.

Initialization

In this step, we set up all the important addresses: treasury, old vault (vaultV1), new vault (vaultV2), the token we're working with, and the list of users who want to migrate their assets. We also handle approvals to save on transaction fees. If there's an emergency, we have extra approval from the treasury.

Functions

addUsers - This function enables us to add more users to the list of migrating addresses.

withdraw - This core function allows us to withdraw funds from the old vault. Our vault's security is robust, preventing us from withdrawing funds directly. Instead, we require all users to exit the old vault by providing their share tokens for migration to the new vault. We iterate over all users on the list, verifying if they have a balance. We also check if the user has given us approval. If not, we add them to the list of addresses without approval for later migration when they are approved. Afterward, we transfer the user's share tokens from their account to the migration contract and update the user's balance. Once we've processed all users, we initiate a withdrawal from vault V1 to the migration contract.

withdrawUsersWithDetectedError - This function allows us to migrate users who didn't provide approval for the initial withdrawal. The logic is similar to the regular withdrawal, but here we iterate over a separate list of users. If a user still hasn't given approval, we skip them for the next round of migration (in waves). This approach helps us optimize the function call's cost, as iterating over the entire list of users can be more expensive, especially on Ethereum.

deposit - Simply deposit all assets from the migration contract into vault V2. We don't need to make multiple deposit function calls with specific recipients because we're tracking balances and can do this in waves.

emergencyExit - This function is used when we decide to transfer all assets on the contract to the treasury. This can occur in an emergency situation or in response to an attack attempt.

Last updated