Lock-Ups

For smart contracts that represent securities, it is beneficial to provide an option for an "early-sale" or "pre-sale" of security tokens. This means that tokens can be purchased by investors at a lower price, but cannot be sold or transferred for a specified period of time. To facilitate this, lock-ups have been added to the smart contract.

  • Tokens can be locked for a predetermined period.

  • Investors can see the locked tokens in their balance but cannot transfer them until the lock-up period ends.

  • Lock-ups are an optional feature that can be enabled or disabled. The authority to toggle Lock-ups is assigned to the Super Admin role.

/// @notice Flag to indicate whether Lock-ups are enabled (true) or disabled (false)
    bool public _isEnabledLockUps;

When Lock-ups are disabled, the information regarding locked tokens remains intact, but the contract no longer checks this parameter during token transfers.

function getAmountOfLockedTokens(
        address _account
    ) public view returns (uint256) {
        uint256 result = 0;

        **if (!(_isEnabledLockUps)) {
            return result;**
        } else {...

Information about locked tokens is stored in the PersonalInfo struct for each address. This is a dynamic array consisting of pairs of data: 1 - the timestamp when tokens can be unlocked, and 2 - the amount of tokens locked until that timestamp.

/// @notice Struct that contains all information about a user's address.
    struct PersonalInfo {
        // User's wallet address, assigned when the user is whitelisted.
        // Default value - address(0)
        address userAddress;
        // True if the address is whitelisted, otherwise false.
        bool whitelisted;
        // True if the address has an individual secondary limit, otherwise false.
        bool hasOwnSecondaryLimit;
        // True if the address has an individual transaction count limit, otherwise false.
        bool hasOwnTransactionCountLimit;
        // Value of the individual secondary trading limit (if exists), default - 0
        uint256 individualSecondaryTradingLimit;
        // Value of the individual transaction count limit (if exists), default - 0
        uint256 individualTransactionCountLimit;
        // The total amount of all tokens ever sent by the user.
        uint256 outputAmount;
        // The total number of all transfers ever made by the user.
        uint256 transactionCount;
        **// Dynamic array containing pairs of [0]: timestamp, [1]: blocked amount**
        uint256[2][] personalLockUps;
    }

Tokens can only be locked by the Financial Manager role and will only be unlocked after the period specified during the locking process. There is no option to unlock them earlier.


The Process of Locking Tokens

Technically, locking tokens involves writing a pair (timestamp, amount) to the PersonalInfo of the account. The smart contract checks this array whenever the address attempts to transfer tokens, and if tokens are locked, they cannot be moved.

  • Tokens can be locked at the time of their distribution. When the Financial Manager sends tokens to the investor’s address from the Corporate Treasury using the transferFromTreasuryLockedTokens() function, the specified amount of tokens will be sent and locked simultaneously. (This function cannot be called if Lock-ups are disabled)

  • Tokens can also be locked when they are already in the user’s balance. In this case, the Financial Manager uses the lockUpTokensOnAddress() function. (This function cannot be called if Lock-ups are disabled)

    This function has a special feature! It checks the available balance of the address (total token balance minus already locked tokens), compares it with the amount to be locked, and if the available balance is less than the amount to lock, only the available tokens will be locked.

/// @dev Checks the amount to lock on `_account`:
    ///      compares the available balance of `_account` and `_amountToLock`
    ///      and returns the lesser value.
    ///      This function does not allow locking tokens that `_account` does not have
    ///      in its balance, preventing a "negative balance" for `_account`.
    function _checkAmountToLock(address _account, uint256 _amountToLock)
        internal
        view
        returns (uint256 resultedAmount)
    {
        _availableBalance(_account) > _amountToLock
            ? resultedAmount = _amountToLock
            : resultedAmount = _availableBalance(_account);
    }

Information About Locked Tokens of an Address

Using the standard balanceOf() function will not show if there are locked tokens in an account. It will only display the total token balance.

To find out information about locked tokens, you can use:

  • getListOfLockUps(address _account) to see the complete list of (timestamp, amount) pairs.

  • getAmountOfLockedTokens(address _account) to see the total amount of locked tokens (this information is also included in getUserData(address _account)).


How Do Tokens Unlock?

Any function that includes the internal _transfer() function (such as transfer, transferFrom, transferFromTreasuryLockedTokens, replacementOfCorporateTreasury) uses the _beforeTokenTransfer(_from, _amount) hook to check the amount of locked tokens associated with an address. If locked tokens exist, the transfer will be prevented.

function _beforeTokenTransfer(address _from, uint256 _amount) internal {
...        
        require(
            _availableBalance(_from) >= _amount,
            "STokenV1: transfer amount exceeds balance or you are trying to transfer locked tokens."
        );
...
    }

After the transfer, the _afterTokenTransfer(_from) hook updates the array of locked tokens.

/// @dev This hook is called after any token transfer (except minting & burning).
    ///      It updates the PersonalInfo.personalLockUps of the `_from` account.
    ///      See {updateDataOfLockedTokensOf}.
    function _afterTokenTransfer(address _from) internal {
        updateDataOfLockedTokensOf(_from);
    }

The updateDataOfLockedTokensOf() function removes the lock-up entry if the unlocking timestamp has passed, thus freeing the locked tokens and allowing them to be transferred.

/// @notice Updates the lock-ups of the `_account` based on timestamps.
    ///         If the unlock time for certain tokens has arrived,
    ///         those tokens will be released.
    /// @dev This function iterates through the array of locked token pairs:
    ///      {PersonalInfo.personalLockUps}.
    ///      If the [0] indexed parameter (unlock timestamp) is less than the current
    ///      timestamp - {block.timestamp} => it will be deleted from the array
    ///      and the [1] indexed amount of tokens will be unlocked, triggering the
    ///      UnlockTokens event.
    ///      Otherwise, the pair is retained, and the tokens remain locked.
    /// @param _account The address for which to update Lock-ups.
    /// @return true if the function executes successfully.
    function updateDataOfLockedTokensOf(
        address _account
    ) public returns (bool) {
        if (userData[_account].personalLockUps.length == 0) {
            return true;
        } else {
            uint count = 0;
            uint256[2][] memory memoryArray = new uint256[2][](
                userData[_account].personalLockUps.length
            );
            for (uint256 i = 0; i < memoryArray.length; i++) {
                if (
                    userData[_account].personalLockUps[i][0] <= block.timestamp
                ) {
                    emit UnlockTokens(
                        _account,
                        block.timestamp,
                        userData[_account].personalLockUps[i][1]
                    );
                } else {
                    memoryArray[i] = userData[_account].personalLockUps[i];
                    count++;
                }
            }

            uint256[2][] memory finalArray = new uint256[2][](count);
            uint k = 0;

            for (uint256 i = 0; i < memoryArray.length; i++) {
                if (memoryArray[i][0] > 0) {
                    finalArray[k] = memoryArray[i];
                    k++;
                }
            }

            userData[_account].personalLockUps = finalArray;
            return true;
        }
    }

Additionally, note that the updateDataOfLockedTokensOf() function is public and can be called at any time by any user without restrictions. This will update the list of locked pairs and unlock tokens as their respective timestamps are reached.


Last updated