// File: AllocationVesting.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import { IERC20 } from "IERC20.sol";
import { DelegatedOps } from "DelegatedOps.sol";
import { ITokenLocker } from "ITokenLocker.sol";

/**
 * @title Vesting contract for team and investors
 * @author PrismaFi
 * @notice Vesting contract which allows transfer of future vesting claims
 */
contract AllocationVesting is DelegatedOps {
    error NothingToClaim();
    error CannotLock();
    error WrongMaxTotalPreclaimPct();
    error PreclaimTooLarge();
    error AllocationsMismatch();
    error ZeroTotalAllocation();
    error ZeroAllocation();
    error ZeroNumberOfWeeks();
    error DuplicateAllocation();
    error InsufficientPoints();
    error LockedAllocation();
    error SelfTransfer();
    error IncompatibleVestingPeriod(uint256 numberOfWeeksFrom, uint256 numberOfWeeksTo);

    struct AllocationSplit {
        address recipient;
        uint24 points;
        uint8 numberOfWeeks;
    }

    struct AllocationState {
        uint24 points;
        uint8 numberOfWeeks;
        uint128 claimed;
        uint96 preclaimed;
    }

    // This number should allow a good precision in allocation fractions
    uint256 private immutable totalPoints;
    // Users allocations
    mapping(address => AllocationState) public allocations;
    // max percentage of one's vest that can be preclaimed in total
    uint256 public immutable maxTotalPreclaimPct;
    // Total allocation expressed in tokens
    uint256 public immutable totalAllocation;
    IERC20 public immutable vestingToken;
    address public immutable vault;
    ITokenLocker public immutable tokenLocker;
    uint256 public immutable lockToTokenRatio;
    // Vesting timeline starting timestamp
    uint256 public immutable vestingStart;

    constructor(
        IERC20 vestingToken_,
        ITokenLocker tokenLocker_,
        uint256 totalAllocation_,
        address vault_,
        uint256 maxTotalPreclaimPct_,
        uint256 vestingStart_,
        AllocationSplit[] memory allocationSplits
    ) {
        if (totalAllocation_ == 0) revert ZeroTotalAllocation();
        if (maxTotalPreclaimPct_ > 20) revert WrongMaxTotalPreclaimPct();
        vault = vault_;
        tokenLocker = tokenLocker_;
        vestingToken = vestingToken_;
        totalAllocation = totalAllocation_;
        lockToTokenRatio = tokenLocker_.lockToTokenRatio();
        maxTotalPreclaimPct = maxTotalPreclaimPct_;

        vestingStart = vestingStart_;
        uint256 loopEnd = allocationSplits.length;
        uint256 total;
        for (uint256 i; i < loopEnd; ) {
            address recipient = allocationSplits[i].recipient;
            uint8 numberOfWeeks = allocationSplits[i].numberOfWeeks;
            uint256 points = allocationSplits[i].points;
            if (points == 0) revert ZeroAllocation();
            if (numberOfWeeks == 0) revert ZeroNumberOfWeeks();
            if (allocations[recipient].numberOfWeeks > 0) revert DuplicateAllocation();
            total += points;
            allocations[recipient].points = uint24(points);
            allocations[recipient].numberOfWeeks = numberOfWeeks;
            unchecked {
                ++i;
            }
        }
        totalPoints = total;
    }

    /**
     * @notice Claims accrued tokens for initiator and transfers a number of allocation points to a recipient
     * @dev Can be delegated
     * @param from Initiator
     * @param to Recipient
     * @param points Number of points to transfer
     */
    function transferPoints(address from, address to, uint256 points) external callerOrDelegated(from) {
        if (from == to) revert SelfTransfer();
        AllocationState memory fromAllocation = allocations[from];
        AllocationState memory toAllocation = allocations[to];
        uint8 numberOfWeeksFrom = fromAllocation.numberOfWeeks;
        uint8 numberOfWeeksTo = toAllocation.numberOfWeeks;
        uint256 pointsFrom = fromAllocation.points;
        if (numberOfWeeksTo != 0 && numberOfWeeksTo != numberOfWeeksFrom)
            revert IncompatibleVestingPeriod(numberOfWeeksFrom, numberOfWeeksTo);
        uint256 totalVested = _vestedAt(block.timestamp, pointsFrom, numberOfWeeksFrom);
        if (totalVested < fromAllocation.claimed) revert LockedAllocation();
        if (points == 0) revert ZeroAllocation();
        if (pointsFrom < points) revert InsufficientPoints();
        // We claim one last time before transfer
        uint256 claimed = _claim(from, pointsFrom, fromAllocation.claimed, numberOfWeeksFrom);
        // Passive balance to transfer
        uint128 claimedAdjustment = uint128((claimed * points) / fromAllocation.points);
        allocations[from].points = uint24(pointsFrom - points);
        // we can't use fromAllocation.claimed since the storage value was modified by the _claim() call
        allocations[from].claimed = allocations[from].claimed - claimedAdjustment;
        allocations[to].points = toAllocation.points + uint24(points);
        allocations[to].claimed = toAllocation.claimed + claimedAdjustment;
        // Transfer preclaimed pro-rata to avoid limit gaming
        uint256 preclaimedToTransfer = (fromAllocation.preclaimed * points) / pointsFrom;
        allocations[to].preclaimed = uint96(toAllocation.preclaimed + preclaimedToTransfer);
        allocations[from].preclaimed = uint96(fromAllocation.preclaimed - preclaimedToTransfer);
        if (numberOfWeeksTo == 0) {
            allocations[to].numberOfWeeks = numberOfWeeksFrom;
        }
    }

    /**
     * @notice Lock future claimable tokens tokens
     * @dev Can be delegated
     * @param account Account to lock for
     * @param amount Amount to preclaim
     */
    function lockFutureClaims(address account, uint256 amount) external callerOrDelegated(account) {
        lockFutureClaimsWithReceiver(account, account, amount);
    }

    /**
     * @notice Lock future claimable tokens tokens
     * @dev Can be delegated
     * @param account Account to lock for
     * @param receiver Receiver of the lock
     * @param amount Amount to preclaim. If 0 the maximum allowed will be locked
     */
    function lockFutureClaimsWithReceiver(
        address account,
        address receiver,
        uint256 amount
    ) public callerOrDelegated(account) {
        AllocationState memory allocation = allocations[account];
        if (allocation.points == 0 || vestingStart == 0) revert CannotLock();
        uint256 claimedUpdated = allocation.claimed;
        if (_claimableAt(block.timestamp, allocation.points, allocation.claimed, allocation.numberOfWeeks) > 0) {
            claimedUpdated = _claim(account, allocation.points, allocation.claimed, allocation.numberOfWeeks);
        }
        uint256 userAllocation = (allocation.points * totalAllocation) / totalPoints;
        uint256 _unclaimed = userAllocation - claimedUpdated;
        uint256 preclaimed = allocation.preclaimed;
        uint256 maxTotalPreclaim = (maxTotalPreclaimPct * userAllocation) / 100;
        uint256 leftToPreclaim = maxTotalPreclaim - preclaimed;
        if (amount == 0) amount = leftToPreclaim > _unclaimed ? _unclaimed : leftToPreclaim;
        else if (preclaimed + amount > maxTotalPreclaim || amount > _unclaimed) revert PreclaimTooLarge();
        amount = (amount / lockToTokenRatio) * lockToTokenRatio; // truncating the dust
        allocations[account].claimed = uint128(claimedUpdated + amount);
        allocations[account].preclaimed = uint96(preclaimed + amount);
        vestingToken.transferFrom(vault, address(this), amount);
        tokenLocker.lock(receiver, amount / lockToTokenRatio, 52);
    }

    /**
     *
     * @notice Claims accrued tokens
     * @dev Can be delegated
     * @param account Account to claim for
     */
    function claim(address account) external callerOrDelegated(account) {
        AllocationState memory allocation = allocations[account];
        _claim(account, allocation.points, allocation.claimed, allocation.numberOfWeeks);
    }

    // This function exists to avoid reloading the AllocationState struct in memory
    function _claim(
        address account,
        uint256 points,
        uint256 claimed,
        uint256 numberOfWeeks
    ) private returns (uint256 claimedUpdated) {
        if (points == 0) revert NothingToClaim();
        uint256 claimable = _claimableAt(block.timestamp, points, claimed, numberOfWeeks);
        if (claimable == 0) revert NothingToClaim();
        claimedUpdated = claimed + claimable;
        allocations[account].claimed = uint128(claimedUpdated);
        // We send to delegate for possible zaps
        vestingToken.transferFrom(vault, msg.sender, claimable);
    }

    /**
     * @notice Calculates number of tokens claimable by the user at the current block
     * @param account Account to calculate for
     * @return claimable Accrued tokens
     */
    function claimableNow(address account) external view returns (uint256 claimable) {
        AllocationState memory allocation = allocations[account];
        claimable = _claimableAt(block.timestamp, allocation.points, allocation.claimed, allocation.numberOfWeeks);
    }

    function _claimableAt(
        uint256 when,
        uint256 points,
        uint256 claimed,
        uint256 numberOfWeeks
    ) private view returns (uint256) {
        uint256 totalVested = _vestedAt(when, points, numberOfWeeks);
        return totalVested > claimed ? totalVested - claimed : 0;
    }

    function _vestedAt(uint256 when, uint256 points, uint256 numberOfWeeks) private view returns (uint256 vested) {
        if (vestingStart == 0 || numberOfWeeks == 0) return 0;
        uint256 vestingWeeks = numberOfWeeks * 1 weeks;
        uint256 vestingEnd = vestingStart + vestingWeeks;
        uint256 endTime = when >= vestingEnd ? vestingEnd : when;
        uint256 timeSinceStart = endTime - vestingStart;
        vested = (totalAllocation * timeSinceStart * points) / (totalPoints * vestingWeeks);
    }

    /**
     * @notice Calculates the total number of tokens left unclaimed by the user including unvested ones
     * @param account Account to calculate for
     * @return Unclaimed tokens
     */
    function unclaimed(address account) external view returns (uint256) {
        AllocationState memory allocation = allocations[account];
        uint256 accountAllocation = (totalAllocation * allocation.points) / totalPoints;
        return accountAllocation - allocation.claimed;
    }

    /**
     * @notice Calculates the total number of tokens left to preclaim
     * @param account Account to calculate for
     * @return Preclaimable tokens
     */
    function preclaimable(address account) external view returns (uint256) {
        AllocationState memory allocation = allocations[account];
        if (allocation.points == 0 || vestingStart == 0) return 0;
        uint256 userAllocation = (allocation.points * totalAllocation) / totalPoints;
        uint256 preclaimed = allocation.preclaimed;
        uint256 maxTotalPreclaim = (maxTotalPreclaimPct * userAllocation) / 100;
        return maxTotalPreclaim - preclaimed;
    }
}

// File: IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

// File: DelegatedOps.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

/**
    @title Prisma Delegated Operations
    @notice Allows delegation to specific contract functionality. Useful for creating
            wrapper contracts to bundle multiple interactions into a single call.

            Functions that supports delegation should include an `account` input allowing
            the delegated caller to indicate who they are calling on behalf of. In executing
            the call, all internal state updates should be applied for `account` and all
            value transfers should occur to or from the caller.

            For example: a delegated call to `openTrove` should transfer collateral
            from the caller, create the debt position for `account`, and send newly
            minted tokens to the caller.
 */
contract DelegatedOps {
    mapping(address owner => mapping(address caller => bool isApproved)) public isApprovedDelegate;

    modifier callerOrDelegated(address _account) {
        require(msg.sender == _account || isApprovedDelegate[_account][msg.sender], "Delegate not approved");
        _;
    }

    function setDelegateApproval(address _delegate, bool _isApproved) external {
        isApprovedDelegate[msg.sender][_delegate] = _isApproved;
    }
}

// File: ITokenLocker.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface ITokenLocker {
    struct LockData {
        uint256 amount;
        uint256 weeksToUnlock;
    }
    struct ExtendLockData {
        uint256 amount;
        uint256 currentWeeks;
        uint256 newWeeks;
    }

    event LockCreated(address indexed account, uint256 amount, uint256 _weeks);
    event LockExtended(address indexed account, uint256 amount, uint256 _weeks, uint256 newWeeks);
    event LocksCreated(address indexed account, LockData[] newLocks);
    event LocksExtended(address indexed account, ExtendLockData[] locks);
    event LocksFrozen(address indexed account, uint256 amount);
    event LocksUnfrozen(address indexed account, uint256 amount);
    event LocksWithdrawn(address indexed account, uint256 withdrawn, uint256 penalty);

    function extendLock(uint256 _amount, uint256 _weeks, uint256 _newWeeks) external returns (bool);

    function extendMany(ExtendLockData[] calldata newExtendLocks) external returns (bool);

    function freeze() external;

    function getAccountWeightWrite(address account) external returns (uint256);

    function getTotalWeightWrite() external returns (uint256);

    function lock(address _account, uint256 _amount, uint256 _weeks) external returns (bool);

    function lockMany(address _account, LockData[] calldata newLocks) external returns (bool);

    function setPenaltyWithdrawalsEnabled(bool _enabled) external returns (bool);

    function unfreeze(bool keepIncentivesVote) external;

    function withdrawExpiredLocks(uint256 _weeks) external returns (bool);

    function withdrawWithPenalty(uint256 amountToWithdraw) external returns (uint256);

    function MAX_LOCK_WEEKS() external view returns (uint256);

    function PRISMA_CORE() external view returns (address);

    function getAccountActiveLocks(
        address account,
        uint256 minWeeks
    ) external view returns (LockData[] memory lockData, uint256 frozenAmount);

    function getAccountBalances(address account) external view returns (uint256 locked, uint256 unlocked);

    function getAccountWeight(address account) external view returns (uint256);

    function getAccountWeightAt(address account, uint256 week) external view returns (uint256);

    function getTotalWeight() external view returns (uint256);

    function getTotalWeightAt(uint256 week) external view returns (uint256);

    function getWeek() external view returns (uint256 week);

    function getWithdrawWithPenaltyAmounts(
        address account,
        uint256 amountToWithdraw
    ) external view returns (uint256 amountWithdrawn, uint256 penaltyAmountPaid);

    function guardian() external view returns (address);

    function incentiveVoter() external view returns (address);

    function lockToTokenRatio() external view returns (uint256);

    function lockToken() external view returns (address);

    function owner() external view returns (address);

    function penaltyWithdrawalsEnabled() external view returns (bool);

    function prismaCore() external view returns (address);

    function totalDecayRate() external view returns (uint32);

    function totalUpdatedWeek() external view returns (uint16);
}