// Contract: MonoMigrator
// Target function: function migrateMonoPair(IMonoPair monoPair)
pragma solidity 0.8.18;

/**************************************************
 *                    Interfaces
 **************************************************/
interface IERC20 {
    function name() external view returns (string memory);

    function symbol() external view returns (string memory);

    function decimals() external view returns (uint8);

    function approve(address, uint256) external;

    function balanceOf(address) external view returns (uint256);

    function allowance(address, address) external view returns (uint256);

    function transfer(address, uint256) external returns (bool);

    function transferFrom(
        address,
        address,
        uint256
    ) external returns (bool);
}

interface IRouter {
    struct route {
        address from;
        address to;
        bool stable;
    }

    function swapExactTokensForTokens(
        uint256,
        uint256,
        route[] calldata,
        address,
        uint256
    ) external returns (uint256[] memory);
}

interface IElmoSolid {
    function redeem(
        uint256,
        address,
        address
    ) external returns (uint256);

    function balanceOf(address) external view returns (uint256);

    function transferFrom(
        address,
        address,
        uint256
    ) external returns (bool);
}

interface IVe {
    struct LockedBalance {
        int128 amount;
        uint256 end;
    }

    function safeTransferFrom(
        address,
        address,
        uint256
    ) external;

    function locked(uint256) external view returns (LockedBalance memory);

    function balanceOf(address) external view returns (uint256);

    function tokenOfOwnerByIndex(address, uint256)
        external
        view
        returns (uint256);

    function attachments(uint256) external view returns (uint256);

    function voted(uint256) external view returns (bool);

    function isApprovedOrOwner(address, uint256) external view returns (bool);
}

interface IFactory {
    function isPair(address) external returns (bool);
}

interface IMonoDepositor {
    function withdraw(address, uint256) external;
}

interface IPair {
    function burn(address) external returns (uint256, uint256);

    function balanceOf(address) external returns (uint256);

    function transfer(address, uint256) external returns (bool);

    function transferFrom(
        address,
        address,
        uint256
    ) external returns (bool);

    function stable() external view returns (bool);

    function token0() external view returns (address);

    function token1() external view returns (address);
}

interface IMonoPair {
    function pool() external view returns (address);

    function balanceOf(address) external view returns (uint256);

    function transferFrom(
        address,
        address,
        uint256
    ) external;
}

/**************************************************
 *                    Libraries
 **************************************************/

library SafeCast {
    /**
     * @dev Converts a signed int256 into an unsigned uint256.
     *
     * Requirements:
     *
     * - input must be greater than or equal to 0.
     */
    function toUint256(int256 value) internal pure returns (uint256) {
        require(value >= 0, "SafeCast: value must be positive");
        return uint256(value);
    }
}

/**************************************************
 *               Migration logic
 **************************************************/

contract MonoMigrator {
    /**************************************************
     *                   Configuration
     **************************************************/
    IFactory constant factory =
        IFactory(0x777de5Fe8117cAAA7B44f396E93a401Cf5c9D4d6);
    IVe public constant veNft = IVe(0x77730ed992D286c53F3A0838232c3957dAeaaF73);
    IMonoDepositor public constant monoDepositor =
        IMonoDepositor(0x822EF744C568466D40Ba28b0f9e4A4961837a46a);
    IRouter public constant router =
        IRouter(0x77784f96C936042A3ADB1dD29C91a55EB2A4219f);
    IElmoSolid elmoSolid =
        IElmoSolid(0x7c90784d6144967bF7B8C89Ca06A3629d8abe642);
    address public constant solid = 0x777172D858dC1599914a1C4c6c9fC48c99a60990;
    address public constant moSolid =
        0x848578e351D25B6Ec0d486E42677891521c3d743;
    address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    uint256 public immutable migrateStartTime;

    // Storage slots start here
    address public owner;
    uint256 internal _unlocked = 1; // For simple reentrancy check

    uint256 public deadline; // No tokens can be migrated after this date

    mapping(address => bool) public tokenIsEcosystemToken; // Mapping to keep track whether a specific token is migratable
    mapping(address => mapping(address => uint256))
        public tokensMigratedByAccount; // tokensMigratedByAccount[tokenAddress][accountAddress]
    mapping(address => uint256) public tokensMigratedByToken; // Total tokens migrated by token address

    mapping(address => bool) tokenMigrated; // Check if individual token is migrated already via LP
    address[] public migratedTokenAddresses; // List of tokens migrated

    mapping(address => mapping(uint256 => uint256))
        public veNftMigratedIndexById; // tokenId index in array of user migrated veNFTs
    mapping(address => uint256[]) public veNftMigratedIdByIndex; // array of veNFT tokenIds a user has migrated
    mapping(address => uint256) public veNftMigratedAmountByAccount; // total SOLID equivalent migrated via veNFT of a user
    uint256 public veNftMigratedAmountTotal; // total SOLID equivalent migrated via veNFTs

    /**************************************************
     *                    Events
     **************************************************/
    event NftMigrated(address indexed from, uint256 tokenId, uint256 amount);

    event EcosystemTokenMigrated(
        address indexed from,
        address indexed token,
        uint256 amount
    );

    event LpMigrated(
        address indexed from,
        address pair,
        bool stable,
        address token0,
        address token1,
        uint256 amount0,
        uint256 amount1
    );

    /**************************************************
     *                    Structs
     **************************************************/
    struct Token {
        address id; // Token address
        uint256 balance; // Token balance
        uint256 migrated; // Tokens migrated
        bool approved; // Did user approve tokens to be migrated
    }

    struct VeNft {
        uint256 id; // NFT ID
        uint256 migrated; // Amount migrated
    }

    /**************************************************
     *                   Modifiers
     **************************************************/

    // Simple reentrancy check
    modifier lock() {
        require(_unlocked == 1);
        _unlocked = 2;
        _;
        _unlocked = 1;
    }

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    // Only allow migrating for predefined period
    modifier onlyMigratePeriod() {
        require(block.timestamp < deadline, "Migrate period over");
        _;
    }

    // Only allow after predefined period
    modifier onlyAfterMigratePeriod() {
        require(block.timestamp >= deadline, "Migrate period not over");
        _;
    }

    /**************************************************
     *                   Initialization
     **************************************************/

    /**
     * @notice Initialization
     */
    constructor() {
        owner = msg.sender;
        migrateStartTime = block.timestamp;
        deadline = migrateStartTime + 7 days; // Set deadline
    }

    /**
     * @notice Transfers ownership
     * @param newOwner new owner, or address(0) to renounce ownership
     */
    function transferOwnership(address newOwner) external onlyOwner {
        owner = newOwner;
    }

    /**
     * @notice Extends deadline
     * @param _newDeadline new deadline
     * @dev _newDealine must be longer than existing deadline
     */
    function extendDeadline(uint256 _newDeadline) external onlyOwner {
        require(
            _newDeadline > deadline,
            "New dealdine must be longer than existing deadline"
        );
        deadline = _newDeadline;
    }

    function setEcosystemTokens(address[] memory _validEcosystemTokenAddresses)
        external
        onlyOwner
    {
        for (uint256 i = 0; i < _validEcosystemTokenAddresses.length; i++) {
            address token = _validEcosystemTokenAddresses[i];
            tokenIsEcosystemToken[token] = true;
            tokenMigrated[token] = true;
            migratedTokenAddresses.push(token);
        }
    }

    /**************************************************
     *                  ERC20 migrate logic
     **************************************************/

    /**
     * @notice Primary migrate method for ERC20 ecosystem tokens
     * @param tokenAddress Address of the token to migrate
     * @dev Only allow migrating entire user balance. YOLO
     * @dev Method can only be called during migrate period
     */
    function migrate(address tokenAddress) external lock onlyMigratePeriod {
        _migrate(tokenAddress, msg.sender);
    }

    function migrateFor(address tokenAddress, address recipient)
        external
        lock
        onlyMigratePeriod
    {
        _migrate(tokenAddress, recipient);
    }

    function _migrate(address tokenAddress, address recipientAddress) internal {
        require(tokenIsEcosystemToken[tokenAddress], "Invalid ecosystem token");
        uint256 amountToMigrate = IERC20(tokenAddress).balanceOf(msg.sender);
        IERC20(tokenAddress).transferFrom(
            msg.sender,
            address(this),
            amountToMigrate
        );
        _recordMigration(tokenAddress, recipientAddress, amountToMigrate);
    }

    /**
     * @notice Dump SOLID for moSOLID and migrate
     */
    function migrateSolid(uint256 minimumAmountOut)
        external
        lock
        onlyMigratePeriod
    {
        uint256 amountIn = IERC20(solid).balanceOf(msg.sender);
        IERC20(solid).transferFrom(msg.sender, address(this), amountIn);
        IRouter.route[] memory routes = new IRouter.route[](2);
        routes[0] = IRouter.route({from: solid, to: weth, stable: false});
        routes[1] = IRouter.route({from: weth, to: moSolid, stable: false});
        IERC20(solid).approve(address(router), amountIn);
        uint256[] memory amounts = router.swapExactTokensForTokens(
            amountIn,
            minimumAmountOut,
            routes,
            address(this),
            block.timestamp
        );
        uint256 amountOut = amounts[amounts.length - 1];
        _recordMigration(moSolid, msg.sender, amountOut);
    }

    function _recordMigration(
        address tokenAddress,
        address recipientAddress,
        uint256 amountToMigrate
    ) internal {
        // Record migrated token and amount
        tokensMigratedByAccount[tokenAddress][
            recipientAddress
        ] += amountToMigrate;

        // Increment global token amount migrated for this token
        tokensMigratedByToken[tokenAddress] += amountToMigrate;
        emit EcosystemTokenMigrated(
            recipientAddress,
            tokenAddress,
            amountToMigrate
        );
    }

    /**************************************************
     *              elmoSOLID migrate logic
     **************************************************/
    function migrateElmoSolid() external {
        uint256 shares = elmoSolid.balanceOf(msg.sender);
        uint256 migratedAmount = elmoSolid.redeem(
            shares,
            address(this),
            msg.sender
        );
        _recordMigration(moSolid, msg.sender, migratedAmount);
    }

    /**************************************************
     *                veNFT migrate logic
     **************************************************/

    /**
     * @notice Migrate veNFT
     * @param tokenId Token ID to migrate
     * @dev veNFT is converted to moSOLID first
     * @dev Method can only be called during migrate period
     */
    function migrateVeNft(uint256 tokenId) external lock onlyMigratePeriod {
        _migrateVeNft(tokenId, msg.sender);
    }

    function migrateVeNftFor(uint256 tokenId, address recipient)
        external
        lock
        onlyMigratePeriod
    {
        _migrateVeNft(tokenId, recipient);
    }

    function _migrateVeNft(uint256 tokenId, address recipient) internal {
        veNft.safeTransferFrom(msg.sender, address(this), tokenId); // Transfer veNFT to this contract
        uint256 moSolidBalanceBefore = IERC20(moSolid).balanceOf(address(this));
        veNft.safeTransferFrom(address(this), moSolid, tokenId); // Convert veNFT to moSOLID
        uint256 moSolidBalanceAfter = IERC20(moSolid).balanceOf(address(this));
        uint256 moSolidDelta = moSolidBalanceAfter - moSolidBalanceBefore;
        _recordMigration(moSolid, recipient, moSolidDelta);
    }