The Pool contract handles the transfer of user funds into the LEEQUID protocol. Afterwards, it expects a transaction submitted by the Oracles, which are responsible for choosing the node operator and composing all the necessary data to register a new validator. This set of data is called the deposit data and it will be used to register the validator in the Proof of Stake protocol. The deposit data structure must contain:

    * @dev Structure for passing information about the validator deposit data.
    * @param operator - address of the operator.
    * @param withdrawalCredentials - withdrawal credentials used for generating the deposit data.
    * @param depositDataRoot - hash tree root of the deposit data, generated by the operator.
    * @param publicKey - BLS public key of the validator, generated by the operator.
    * @param signature - BLS signature of the validator, generated by the operator.
    struct DepositData {
        address operator;
        bytes32 withdrawalCredentials;
        bytes32 depositDataRoot;
        bytes publicKey;
        bytes signature;

Key features

Stake Management

The Pool contract provides functionality for staking. Users can send LYX to the contract using various methods (stake, stakeOnBehalf, stakeWithPartner, stakeWithPartnerOnBehalf, stakeWithReferrer, stakeWithReferrerOnBehalf). All these methods are non-reentrant, which means they prevent reentrancy attacks - a common smart contract vulnerability.

Validator Registration

The contract facilitates registering validators with the LUKSO Deposit Contract. It keeps track of the total number of activated validators and exited validators. It also manages the scheduling of activation and deactivation of validators.

Deposit Tracking

The Pool contract keeps track of the total pending validators and the minimum amount of LYX deposit that is considered for the activation period. It also maintains a limit on the pending validators percentage, a variable called pendingValidatorsLimit. If such limit is not exceeded, tokens can be minted immediately. This promotes efficient use of the staking pool and rewards early stakers.

Upgradeability and access control

The contract uses OwnablePausableUpgradeable to provide the ability to pause functions during contract upgrades or in case of any suspicious activity. This improves the security of the contract and provides a way to prevent malicious activities.

Secure execution

The contract employs OpenZeppelin's ReentrancyGuardUpgradeable contract to prevent re-entrancy attacks. This ensures that all internal calls are completed before the control flow is returned to the external caller.

The flow of registering validators

Once the oracles have noticed more than 32 LYX standing in the Pool contract, they will generate the data for a new validator, before passing it to the Pool contract in the deposit transaction. The Pool contract will register the validator on the LUKSO deposit contract, along with the LYX deposit.

    function registerValidator(IPoolValidators.DepositData calldata depositData) external override whenNotPaused {
        require(msg.sender == address(validators), "Pool: access denied");
        require(depositData.withdrawalCredentials == withdrawalCredentials, "Pool: invalid withdrawal credentials");

        // update number of pending validators
        pendingValidators = pendingValidators + 1;
        emit ValidatorRegistered(depositData.publicKey, depositData.operator);

        // register validator
        validatorRegistration.deposit{value : VALIDATOR_TOTAL_DEPOSIT}(

Notice that the variable validatorRegistration, used in the code block above, is defined in the beginning of the Pool contract as:

  // @dev Address of the ETH2 Deposit Contract (deployed by Lukso).
    IDepositContract public override validatorRegistration;

This variable represents the LUKSO deposit contract, and that's why the Pool contract calls the deposit function on it, passing as a value VALIDATOR_TOTAL_DEPOSIT, which is, as expected, defined as 32 LYX:

// @dev Validator deposit amount.
    uint256 public constant override VALIDATOR_TOTAL_DEPOSIT = 32 ether;

Notice that because LUKSO is a fork of Ethereum, it inherits the solidity keywords, which include those for units such as ether and wei.

The LYX pathway inside the Pool contract

LYX enters the Pool contract via the payable stake function:

function stake() external payable override nonReentrant {
    _stake(msg.sender, msg.value);

The transaction that the LEEQUID web app creates when you push the stake button is addressed to the Pool contract and to this specific part of the contract, triggering a cascade of conditions which determine different outcomes for the function call:

function _stake(address recipient, uint256 value) internal whenNotPaused {
    require(recipient != address(0), "Pool: invalid recipient");
    require(value > 0, "Pool: invalid deposit amount");

    uint256 unstakeMatchedAmount = 0;

    if (!stakedLyxToken.unstakeProcessing()) {
        // try to match unstake request
        unstakeMatchedAmount = stakedLyxToken.matchUnstake(value);
    if (unstakeMatchedAmount > 0) {
        address(rewards).call{value: unstakeMatchedAmount}("");

    uint256 _valueToDeposit = value - unstakeMatchedAmount;

    // mint tokens for small deposits immediately
    if (_valueToDeposit <= minActivatingDeposit) {
        stakedLyxToken.mint(recipient, value, true, "");

    // mint tokens if current pending validators limit is not exceeded
    uint256 _pendingValidators = pendingValidators + (address(this).balance / VALIDATOR_TOTAL_DEPOSIT);
    uint256 _activatedValidators = activatedValidators; // gas savings
    uint256 validatorIndex = _activatedValidators + _pendingValidators;
    if (validatorIndex * 1e4 <= _activatedValidators * 1e4 + effectiveValidators() * pendingValidatorsLimit) {
        stakedLyxToken.mint(recipient, value, true, "");
    } else {
        // lock deposit amount until validator activated
        if (unstakeMatchedAmount > 0) stakedLyxToken.mint(recipient, unstakeMatchedAmount, true, "");
        activations[recipient][validatorIndex] = activations[recipient][validatorIndex] + _valueToDeposit;
        emit ActivationScheduled(recipient, validatorIndex, _valueToDeposit);

The minActivatingDeposit and the pendingValidators variables control the mechanism that queues incoming deposits if there’s too much unactivated stake (LYX waiting in line to become part of an active validator). This formula in particular:

validatorIndexβˆ—1e4<=activatedValidatorsβˆ—1e4+effectiveValidators()βˆ—pendingValidatorsLimitvalidatorIndex * 1e4 <= activatedValidators * 1e4 + effectiveValidators() * pendingValidatorsLimit

...prevents what is called "socialization of rewards", or in other words, reward dilution due to big amounts of LYX flowing into the protocol. You can get an overview of this issue in the Staking section of this documentation.

Understanding multipliers as a workaround to floating point arithmetic

In the above formula, pendingValidatorsLimit acts as a multiplier, just as 1e4 does. This is due to solidity not handling floating point calculations (operations with numbers containing a decimal part). Thus, this math is necessary in order to obtain a fractional value of the total number of validators. In other words, we cannot multiply a number by 10% (0.1) in solidity. In order to do that, we must multiply all numbers in a formula by a base factor. The one you see in the code is 1e4, which corresponds to the number 10000 in decimal notation and allows us a precision of 2 decimals (0,01%). When we want to take a fraction of a number, instead of multiplying it by the base factor 10000, we multiply it, for example, by a number which is 10 times less, 10% of the base factor.

The variable pendingValidatorsLimit is set to 1000, 10% of 1e4. When applied as a multiplier to effectiveValidators, we get 10% of the effectiveValidators, i.e, 10% of the validators currently active and validating on-chain.

Last updated