delegation::delegation_state

Module 0xc0ded0c0::delegation_state

module delegation::delegation_state

We first give a brief overview of model of staking Aptos uses and then describe how this module implements a managed stake pool to enable delegation.

In Aptos ecosystem, funds stored in a stake pool (validator) could be distributed among:

  • active,

  • inactive,

  • pending_active,

  • pending_inactive.

Similarly, a validator's state could also be

  • active,

  • inactive,

  • pending_active,

  • pending_inactive.

New APT is only generated when the validator's state is active or pending_inactive, and funds in it are in state active or pending_inactive.

There are two kinds of shares represented in the state:

  • reserved shares which are stored in reserved_pool, and,

  • unreserved shares which are stored in unreserved_pool.

Reserved shares in are the ones that the owner or a delegator has requested to unstake and withdraw. Funds corresponding to reserved shares are either ALL in pending_inactive (if lockup has not expired) or they can be withdrawn and dispersed. The dispersal must happen before anyone else can reserve any more shares for withdrawal. So in a managed pool, funds can never be in both inactive and pending_inactive. Also, reserved shares and unreserved shares can be unequal in value (per unit share). Their values are calculated independently in this module.

If the funds corresponding to the reserved shares have been in inactive state for more than an epoch, then the value of reserved shares starts to depreciate compared to the value of unreserved shares, if the validator state is active or pending_inactive. Hence, before any delegation, any inactive funds are disbursed so that the depreciated shares do not affect the share calculation for the new delegator.

Also, before any new delegations, owner's commission must be paid by everyone so that the new delegator is not subject to any pending commissions.

Payment to the owner is a three step process:

  1. New unreserved shares are minted without adding any new funds to the pool. The resulting depreciation of unreserved shares is equal to the commission due on funds associated with the unreserved shares.

  2. New reserved shares are minted without adding any new funds to the pool at first. The resulting depreciation of reserved shares makes owner own a part of the fund that is currently inactive or pending_inactive.

  3. commission_exempt is adjusted to current total worth of the pool to reflect that there are no remaining owner commission at this moment.

Hence, paying commission to owner just manipulates shares, and does not move any coins.

use 0x1::account;
use 0x1::aptos_coin;
use 0x1::coin;
use 0x1::error;
use 0x1::event;
use 0x1::signer;
use 0x1::stake;
use 0x1::timestamp;
use 0xc0ded0c0::math;
use 0xc0ded0c0::pool;
use 0xc0ded0c0::stake_pool_helpers;

Resource SharesData

Created when a stake pool owner (validator) signs up via validator_router. It is stored with the pool owner, and destroyed when they leave the protocol. The stake pool can accept delegations from both the liquid-staking protocol as well as from other delegators.

struct SharesData has key
Fields

stake_pool_address: addressaddress of the stake poolcurrent_commission: u64commission which is paid to all non-protocol delegatorscommission_recipient_address: addressprotocol_delegator_address: addresscontract address of the protocol delegatorprotocol_commission: u64The commission that the protocol pays to the managed pool owner. protocol_commission cannot be more than current_commission.commission_set: boolWhether commission on the pool has been configured yet.reserved_pool: pool::PoolReserved shares poolunreserved_pool: pool::PoolUnreserved shares pool

Resource PendingCommissions

Created when the pool owner requests an update to commissions. Destroyed when the new commission takes effect.

struct PendingCommissions has key
Fields

update_timestamp: u64The timestamp of the the most recent owner request to update commissionscommission: u64The new default commissionprotocol_commission: u64The new protocol commission

Resource Payout

This is stored with a delegator's account and aggregates across all delegations made by them, potentially across multiple managed_stake_pools.

struct Payout has key
Fields

coins: coin::Coin<aptos_coin::AptosCoin>The total APT that the delegator can withdraw.withdraw_payout_events: event::EventHandle<delegation_state::WithdrawPayoutEvent>Handle to access all the WithdrawPayoutEvents

Struct WithdrawPayoutEvent

Event which is generated when a Payout takes place.

struct WithdrawPayoutEvent has drop, store
Fields

delegator: addressThe delegator who is receiving the payout.amount: u64The amount of the payout in AptosCoin.

Constants

Normalizer for the commission.

const COMMISSION_NORMALIZER: u64 = 1000000;

If the value of remaining unreserved shares is less than this when a delegator requests reservation of some of their shares, then all the shares of the delegator are reserved in order to not leave small amount of dust in the protocol. This mitigates a potential grieifing attack on the protocol.

const DUST_TOLERANCE: u64 = 1000000000;

When the total balance does not match the sum of reserved and unreserved balance.

const EBALANCE_ERROR: u64 = 13;

When the stake pool's funds are not in the active state at initialization.

const ECAN_ONLY_INIT_WITH_ACTIVE_STAKE: u64 = 11;

When the pool has no share holders or no shares to burn

const ECOULD_NOT_BURN_PAYOUT_SHARES: u64 = 2;

When there are funds in both active as well as inactive state at the same time.

const EDELAYED_PAYOUTS: u64 = 7;

When the commission being set is larger than 100%.

const EINVALID_COMMISSION: u64 = 6;

When the delegator tries to withdraw more unreserved shares than they have

const ENOT_ENOUGH_UNRESERVED_SHARES: u64 = 0;

When there are no reserved shares to withdraw.

const ENO_RESERVED_SHARE_SUPPLY: u64 = 10;

When there are no unreserved shares to unstake and withdraw.

const ENO_UNRESERVED_SHARE_SUPPLY: u64 = 9;

When the stake pool's funds do not meet the min_stake requirement.

const EOWNER_STAKE_TOO_LOW: u64 = 14;

When the funds from the inactive state have not been disbursed before doing an action which changes the delegation_state.

const EPAYOUTS_NOT_COMPLETE: u64 = 4;

When the pool has shareholders other than the owner during destruction.

const EPOOL_HAS_SHAREHOLDERS: u64 = 20;

When there are pending commissions to the owner which have not been paid.

const EUNPAID_COMMISSION_TO_OWNER: u64 = 5;

When there is a shareholder with zero shares during dispersal.

const EZERO_ENTRY: u64 = 21;

Initially 100 shares equals 1 OCTA (i.e. 10^-8 APT)

const INITIAL_SHARES_FACTOR: u64 = 100;

The precision for the commission change multiplier.

const MULTIPLIER_PRECISION: u64 = 10000;

Function is_owner_alone_in_the_pool

Returns whether the only shareholder of managed_pool_address is the owner of the pool.

public fun is_owner_alone_in_the_pool(managed_pool_address: address): bool
Implementation
public fun is_owner_alone_in_the_pool(
    managed_pool_address: address
): bool acquires SharesData {
    let shares_data = borrow_global<SharesData>(managed_pool_address);
    let unreserved_shares_length = pool::num_share_holders(
        &shares_data.unreserved_pool
    );
    let reserved_shares_length = pool::num_share_holders(
        &shares_data.reserved_pool
    );

    if (
        pool::is_shareholder(
            &shares_data.unreserved_pool,
            managed_pool_address
        )
    ) {
        (reserved_shares_length == 0 && unreserved_shares_length == 1)
    } else {
        // This case can only happen during testing as min owner stake for
        // mainnet will be positive.
        (reserved_shares_length == 0 && unreserved_shares_length == 0)
    }
}

Function is_an_active_delegator

Returns whether the delegator_address a shareholder in the managed_pool_address.

public fun is_an_active_delegator(managed_pool_address: address, delegator_address: address): bool
Implementation
public fun is_an_active_delegator(
    managed_pool_address: address,
    delegator_address: address
): bool acquires SharesData {
    let shares_data = borrow_global<SharesData>(managed_pool_address);
    pool::is_shareholder(&shares_data.unreserved_pool, delegator_address)
}

Function assert_no_pending_commissions

Assert that there is no pending commission to the owner.

Abort conditions

  • If there are unpaid commissions to the owner.

public fun assert_no_pending_commissions(managed_pool_address: address)
Implementation
public fun assert_no_pending_commissions(
    managed_pool_address: address
) acquires SharesData {
    let stake_pool_address = get_stake_pool_address(managed_pool_address);
    let total_worth = stake_pool_helpers::get_stake_pool_total(
        stake_pool_address
    );
    assert!(
        get_commission_exempt(managed_pool_address) == total_worth,
        error::invalid_state(EUNPAID_COMMISSION_TO_OWNER)
    );

    let unreserved_total =
        stake_pool_helpers::get_stake_pool_non_withdrawing_balance(
            stake_pool_address
        );

    assert!(
        get_commission_exempt_unreserved(managed_pool_address) ==
            unreserved_total,
        error::invalid_state(EUNPAID_COMMISSION_TO_OWNER)
    );
    assert!(
        get_commission_exempt_reserved(managed_pool_address) ==
            total_worth - unreserved_total,
        error::invalid_state(EUNPAID_COMMISSION_TO_OWNER)
    );
}

Function get_current_commission

Returns the current_commission from SharesData, which is the current commission rate that non-protocol delegators will have to pay.

public fun get_current_commission(managed_pool_address: address): u64
Implementation
public fun get_current_commission(
    managed_pool_address: address
): u64 acquires SharesData {
    borrow_global<SharesData>(managed_pool_address).current_commission
}

Function get_stake_pool_address

Returns the stake pool address for the managed pool at managed_pool_address.

public fun get_stake_pool_address(managed_pool_address: address): address
Implementation
public fun get_stake_pool_address(
    managed_pool_address: address
): address acquires SharesData {
    borrow_global<SharesData>(managed_pool_address).stake_pool_address
}

Function get_total_length_of_share_maps

Returns the number of shareholders in the managed pool at managed_pool_address.

public fun get_total_length_of_share_maps(managed_pool_address: address): u64
Implementation
public fun get_total_length_of_share_maps(
    managed_pool_address: address
): u64 acquires SharesData {
    let shares_data = borrow_global<SharesData>(managed_pool_address);
    pool::num_share_holders(&shares_data.unreserved_pool) +
        pool::num_share_holders(&shares_data.reserved_pool)
}

Function get_share_supply

Returns the number of shares in the managed pool at managed_pool_address.

public fun get_share_supply(managed_pool_address: address): u64
Implementation
public fun get_share_supply(
    managed_pool_address: address
): u64 acquires SharesData {
    let shares_data = borrow_global<SharesData>(
        managed_pool_address
    );
    (
        pool::share_supply(&shares_data.reserved_pool) +
            pool::share_supply(&shares_data.unreserved_pool)
    )
}

Function get_commission_exempt

Returns the commission_exempt amount in the pool, i.e., the total amount (in APT) on which commission has been paid. It is equal to total worth of the pool, if and only if there is no commission due for any delegator.

This gets reset to current total worth whenever commission is paid to the owner.

public fun get_commission_exempt(managed_pool_address: address): u64
Implementation
public fun get_commission_exempt(
    managed_pool_address: address
): u64 acquires SharesData {
    get_commission_exempt_reserved(managed_pool_address) +
        get_commission_exempt_unreserved(managed_pool_address)
}

Function get_commission_exempt_reserved

Returns the commission_exempt amount in the reserved pool.

public fun get_commission_exempt_reserved(managed_pool_address: address): u64
Implementation
public fun get_commission_exempt_reserved(
    managed_pool_address: address
): u64 acquires SharesData {
    pool::get_commission_exempt_amount(
        &borrow_global<SharesData>(managed_pool_address).reserved_pool
    )

}

Function get_commission_exempt_unreserved

Returns the commission_exempt amount in the unreserved pool.

public fun get_commission_exempt_unreserved(managed_pool_address: address): u64
Implementation
public fun get_commission_exempt_unreserved(
    managed_pool_address: address
): u64 acquires SharesData {
    pool::get_commission_exempt_amount(
        &borrow_global<SharesData>(managed_pool_address).unreserved_pool
    )
}

Function get_balance_reserved

Returns the current value of the shares in the reserved pool of managed pool at managed_pool_address for delegator.

public fun get_balance_reserved(managed_pool_address: address, delegator_address: address): u64
Implementation
public fun get_balance_reserved(
    managed_pool_address: address,
    delegator_address: address
): u64 acquires SharesData {
    let delegator_reserved_shares = get_reserved_shares_with_delegator(
        managed_pool_address,
        delegator_address
    );

    get_shares_to_value_reserved(
        managed_pool_address,
        delegator_reserved_shares
    )
}

Function get_balance_unreserved

Returns the current value of the shares in the unreserved pool of managed pool at managed_pool_address for delegator.

public fun get_balance_unreserved(managed_pool_address: address, delegator: address): u64
Implementation
public fun get_balance_unreserved(
    managed_pool_address: address,
    delegator: address
): u64 acquires SharesData {
    let delegator_unreserved_shares = get_unreserved_shares_with_delegator(
        managed_pool_address,
        delegator
    );

    get_shares_to_value_unreserved(
        managed_pool_address,
        delegator_unreserved_shares
    )
}

Function get_reserved_total_balance

Returns the total value of the shares in the reserved pool of managed pool at managed_pool_address.

public fun get_reserved_total_balance(managed_pool_address: address): u64
Implementation
public fun get_reserved_total_balance(
    managed_pool_address: address
): u64 acquires SharesData {
    let stake_pool_address = get_stake_pool_address(managed_pool_address);
    let (
        _active_stake,
        inactive_stake,
        _pending_active_stake,
        pending_inactive_stake
    ) = stake::get_stake(stake_pool_address);

    if (inactive_stake > 0) {
        inactive_stake
    } else {
        pending_inactive_stake
    }
}

Function get_reserved_share_supply

Returns the total number of shares in the reserved pool of managed pool at managed_pool_address.

fun get_reserved_share_supply(managed_pool_address: address): u64
Implementation
fun get_reserved_share_supply(
    managed_pool_address: address
): u64 acquires SharesData {
    pool::share_supply(
        &borrow_global<SharesData>(managed_pool_address).reserved_pool
    )
}

Function get_unreserved_share_supply

Returns the total number of shares in the unreserved pool of managed pool at managed_pool_address.

fun get_unreserved_share_supply(managed_pool_address: address): u64
Implementation
fun get_unreserved_share_supply(
    managed_pool_address: address
): u64 acquires SharesData {
    pool::share_supply(
        &borrow_global<SharesData>(managed_pool_address).unreserved_pool
    )
}

Function get_value_to_shares_unreserved

Converts a given APT amount to the number of shares in the unreserved pool of the managed pool at managed_pool_address.

public fun get_value_to_shares_unreserved(managed_pool_address: address, amount: u64): u64
Implementation
public fun get_value_to_shares_unreserved(
    managed_pool_address: address,
    amount: u64
): u64 acquires SharesData {
    let stake_pool_address = get_stake_pool_address(managed_pool_address);
    let shares_data = borrow_global<SharesData>(
        managed_pool_address
    );
    math::mul_div_with_init(
        amount,
        pool::share_supply(&shares_data.unreserved_pool),
        stake_pool_helpers::get_stake_pool_non_withdrawing_balance(
            stake_pool_address
        ),
        INITIAL_SHARES_FACTOR,
    )
}

Function get_shares_to_value_reserved

Converts a given num_shares shares in the reserved pool of the managed pool at managed_pool_address to their value in APT.

Abort condition

  • If there are no shares in the reserved pool.

fun get_shares_to_value_reserved(managed_pool_address: address, num_shares: u64): u64
Implementation
fun get_shares_to_value_reserved(
    managed_pool_address: address,
    num_shares: u64
): u64 acquires SharesData {
    math::safe_mul_div(
        num_shares,
        get_reserved_total_balance(managed_pool_address),
        get_reserved_share_supply(managed_pool_address),
        ENO_RESERVED_SHARE_SUPPLY,
    )
}

Function get_shares_to_value_unreserved

Converts a given num_shares shares in the unreserved pool of the managed pool at managed_pool_address to their value in APT.

Abort condition

  • If there are no shares in the unreserved pool.

public fun get_shares_to_value_unreserved(managed_pool_address: address, num_shares: u64): u64
Implementation
public fun get_shares_to_value_unreserved(
    managed_pool_address: address,
    num_shares: u64
): u64 acquires SharesData {
    let stake_pool_address = get_stake_pool_address(managed_pool_address);
    math::safe_mul_div(
        num_shares,
        stake_pool_helpers::get_stake_pool_non_withdrawing_balance(
            stake_pool_address
        ),
        get_unreserved_share_supply(managed_pool_address),
        ENO_UNRESERVED_SHARE_SUPPLY,
    )
}

Function get_unreserved_shares_with_delegator

Returns shares owned by a delegator delegator in the unreserved pool of the ManagesStakingPool at managed_pool_address.

public fun get_unreserved_shares_with_delegator(managed_pool_address: address, delegator: address): u64
Implementation
public fun get_unreserved_shares_with_delegator(
    managed_pool_address: address,
    delegator: address
): u64 acquires SharesData {
    let shares_data = borrow_global<SharesData>(managed_pool_address);
    pool::get_num_shares(&shares_data.unreserved_pool, delegator)
}

Function get_reserved_shares_with_delegator

Returns shares owned by a delegator delegator in the reserved pool of the managed pool at managed_pool_address.

public fun get_reserved_shares_with_delegator(managed_pool_address: address, delegator: address): u64
Implementation
public fun get_reserved_shares_with_delegator(
    managed_pool_address: address,
    delegator: address
): u64 acquires SharesData {
    let shares_data = borrow_global<SharesData>(managed_pool_address);
    pool::get_num_shares(&shares_data.reserved_pool, delegator)
}

Function get_protocol_delegator_address

Returns the address of the protocol. This is the address that protocol uses to delegate to an associated validator.

public fun get_protocol_delegator_address(managed_pool_address: address): address
Implementation
public fun get_protocol_delegator_address(
    managed_pool_address: address
): address acquires SharesData {
    borrow_global<SharesData>(
        managed_pool_address
    ).protocol_delegator_address
}

Function get_protocol_commission

Returns the protocol_commission of the managed pool at managed_pool_address.

public fun get_protocol_commission(managed_pool_address: address): u64
Implementation
public fun get_protocol_commission(
    managed_pool_address: address
): u64 acquires SharesData {
    borrow_global<SharesData>(managed_pool_address).protocol_commission
}

Function get_protocol_commission_change_multiplier

Returns the factor by which the protocol commission will be increased by the validator in future. If the protocol is not expecting any change, or the protocol commission is being lowered, this will return MULTIPLIER_PRECISION. Otherwise it will return a number less than MULTIPLIER_PRECISION, according to MULTIPLIER_PRECISION * (100 - new_commssion_pct) / (100 - old_commission_pct).

public fun get_protocol_commission_change_multiplier(managed_pool_address: address): u64
Implementation
public fun get_protocol_commission_change_multiplier(
    managed_pool_address: address,
): u64 acquires SharesData, PendingCommissions {
    let shares_data = borrow_global<SharesData>(managed_pool_address);

    if (
        !exists<PendingCommissions>(managed_pool_address)
    ) {
        return MULTIPLIER_PRECISION
    };

    let pending_commissions = borrow_global<PendingCommissions>(
        managed_pool_address
    );
    let current_protocol_commission
        = shares_data.protocol_commission;
    let pending_protocol_commission
        = pending_commissions.protocol_commission;

    if (
        pending_protocol_commission <= current_protocol_commission
    ) {
        MULTIPLIER_PRECISION
    } else {
        // Now pending_protocol_commision > current_protocol_commission,
        // So current_protocol_commission < COMMISSION_NORMALIZER.
        // This implies that there cannot be any division by zero errors here.
        // Moreover, the result is always < MULTIPLIER_PRECISION.
        math::mul_div(
            MULTIPLIER_PRECISION,
            COMMISSION_NORMALIZER - pending_protocol_commission,
            COMMISSION_NORMALIZER - current_protocol_commission,
        )
    }
}

Function get_protocol_payout_value

Returns the total amount of protocol's inactive and withdrawable funds with an associated validator.

public fun get_protocol_payout_value(managed_pool_address: address): u64
Implementation
public fun get_protocol_payout_value(
    managed_pool_address: address,
): u64 acquires SharesData, Payout {
    let protocol_delegator_address = get_protocol_delegator_address(
        managed_pool_address
    );
    if (!exists<Payout>(protocol_delegator_address)) {
        return 0
    };
    let payout = borrow_global<Payout>(protocol_delegator_address);
    coin::value<AptosCoin>(&payout.coins)
}

Function get_num_shares_to_reserve_rounded_up

Returns the shares which the delegator_address is allowed to withdraw from the unreserved pool of the managed pool at managed_pool_address, given that the delegator wants to withdraw amount APT. This ensures that the amount of APT withdrawn does not leave less than DUST_TOLERANCE APT in the unreserved pool for the delegator.

public fun get_num_shares_to_reserve_rounded_up(managed_pool_address: address, delegator_address: address, amount: u64): u64
Implementation
public fun get_num_shares_to_reserve_rounded_up(
    managed_pool_address: address,
    delegator_address: address,
    amount: u64,
): u64 acquires SharesData {
    let balance_unreserved_with_validator = get_balance_unreserved(
        managed_pool_address,
        delegator_address,
    );
    // Convert amount to unreserved shares.
    // Return all of their shares if remaining will be less than
    // DUST_TOLERANCE.
    if (amount + DUST_TOLERANCE >= balance_unreserved_with_validator) {
        return get_unreserved_shares_with_delegator(
            managed_pool_address,
            delegator_address
        )
    };
    get_value_to_shares_unreserved(managed_pool_address, amount)
}

Function payout_value

Returns the withdrawable amount of a delegator delegator_address.

public fun payout_value(delegator_address: address): u64
Implementation
public fun payout_value(delegator_address: address): u64 acquires Payout {
    let payout = borrow_global_mut<Payout>(delegator_address);
    coin::value<AptosCoin>(&payout.coins)
}

Function extract_payout

Extracts the payout which is currently available to the delegator.

public fun extract_payout(delegator: &signer): coin::Coin<aptos_coin::AptosCoin>
Implementation
public fun extract_payout(
    delegator: &signer,
): Coin<AptosCoin> acquires Payout {
    let payout = borrow_global_mut<Payout>(signer::address_of(delegator));
    let value = coin::value<AptosCoin>(&payout.coins);
    coin::extract(&mut payout.coins, value)
}

Function safe_create_payout_store

Creates and saves an empty payout struct for the user signaling they want to receive payouts

public fun safe_create_payout_store(creator: &signer)
Implementation
public entry fun safe_create_payout_store(
    creator: &signer,
) {
    if (!exists<Payout>(signer::address_of(creator))) {
        move_to(creator, Payout {
            coins: coin::zero<AptosCoin>(),
            withdraw_payout_events:
                account::new_event_handle<WithdrawPayoutEvent>(
                    creator
                ),
        });
    };
}

Function withdraw_payout

The delegator can request their unlocked stake that have accrued since their last withdrawal.

public fun withdraw_payout(delegator: &signer)
Implementation
public entry fun withdraw_payout(delegator: &signer) acquires Payout {
    let payout_coins = extract_payout(delegator);
    let amount = coin::value<AptosCoin>(&payout_coins);
    let delegator_address = signer::address_of(delegator);
    coin::deposit<AptosCoin>(delegator_address, payout_coins);

    let payout = borrow_global_mut<Payout>(delegator_address);
    event::emit_event<WithdrawPayoutEvent>(
        &mut payout.withdraw_payout_events,
        WithdrawPayoutEvent {
            delegator: delegator_address,
            amount: amount,
        }
    )
}

Function initialize_internal

Initializes a managed pool for the managed_pool_owner using the stake pool at stake_pool_address. The protocol's address is set to protocol_delegator_address.

Requirements

  • Stake pool at stake_pool_address must only have active stake.

  • There should not be any pending_active stake: wait an epoch for pending active stake to become active.

  • There should not be any inactive stake: either withdraw or restake inactive funds before initialization.

  • Stake pool must have more than min_stake in the pool as specified by aptos_framework

Restrictions

Abort conditions

  • If the owner's stake is too low.

  • If the owner's stake is not active.

public(friend) fun initialize_internal(managed_pool_owner: &signer, protocol_delegator_address: address, commission_recipient_address: address, stake_pool_address: address)
Implementation
public(friend) fun initialize_internal(
    managed_pool_owner: &signer,
    protocol_delegator_address: address,
    commission_recipient_address: address,
    stake_pool_address: address,
) {
    let managed_pool_address = signer::address_of(managed_pool_owner);

    let (
        active_stake,
        inactive_stake,
        pending_active_stake,
        pending_inactive_stake
    ) = stake::get_stake(stake_pool_address);

    assert!(
        active_stake >= stake_pool_helpers::get_framework_min_stake(),
        error::invalid_state(EOWNER_STAKE_TOO_LOW)
    );

    assert!(
        pending_active_stake == 0 &&
            pending_inactive_stake == 0 &&
            inactive_stake == 0,
        error::invalid_state(ECAN_ONLY_INIT_WITH_ACTIVE_STAKE)
    );

    let initial_shares = active_stake * INITIAL_SHARES_FACTOR;

    // Need to initialize owner payouts as all others are initialized during
    // delegation
    safe_create_payout_store(managed_pool_owner);

    let unreserved_pool = pool::create_pool();
    pool::buy_in(
        &mut unreserved_pool,
        managed_pool_address,
        initial_shares,
        active_stake
    );

    move_to(managed_pool_owner, SharesData {
        stake_pool_address: stake_pool_address,
        current_commission: 0,
        commission_recipient_address: commission_recipient_address,
        protocol_delegator_address: protocol_delegator_address,
        protocol_commission: 0,
        commission_set: false,
        unreserved_pool: unreserved_pool,
        reserved_pool: pool::create_pool(),
    });
}

Function change_commission_internal

Changes the commission of the managed pool. new_default_commission is the commission charged to non-protocol delegators while new_protocol_commission is the commission charged to the protocol delegator. If previous commission rates can be activated. Do so.

Assumes

  • new_protocol_commission is smaller than the new_default_commission.

Restrictions

Aborts conditions

public(friend) fun change_commission_internal(pool_owner: &signer, new_default_commission: u64, new_protocol_commission: u64)
Implementation
public(friend) fun change_commission_internal(
    pool_owner: &signer,
    new_default_commission: u64,
    new_protocol_commission: u64,
) acquires SharesData, PendingCommissions {
    let managed_pool_address = signer::address_of(pool_owner);
    let shares_data = borrow_global_mut<SharesData>(
        managed_pool_address
    );
    assert!(
        new_default_commission <= COMMISSION_NORMALIZER,
        error::invalid_argument(EINVALID_COMMISSION)
    );
    assert!(
        new_protocol_commission <= COMMISSION_NORMALIZER,
        error::invalid_argument(EINVALID_COMMISSION)
    );

    // If no commission had been set yet, activate it immediately.
    if (!shares_data.commission_set) {
        shares_data.protocol_commission = new_protocol_commission;
        shares_data.current_commission = new_default_commission;
        shares_data.commission_set = true;
        return
    };

    commission_rate_bookkeeping(managed_pool_address, shares_data);
    create_or_update_pending_commissions(pool_owner, new_default_commission, new_protocol_commission);
}

Function change_commission_recipient_internal

Changes the commission recipient of the managed pool.

Restrictions

public(friend) fun change_commission_recipient_internal(managed_pool_address: address, new_commission_recipient_address: address)
Implementation
public(friend) fun change_commission_recipient_internal(
    managed_pool_address: address,
    new_commission_recipient_address: address,
) acquires SharesData {
    let shares_data = borrow_global_mut<SharesData>(
        managed_pool_address
    );

    shares_data.commission_recipient_address = new_commission_recipient_address;
}

Function disperse_all_payouts

This function extracts withdrawable balance and disperses all shares in reserved pool of the managed_pool_address.

Restrictions

  • Only the friend module delegation_service can call this function.

  • Needs the OwnerCapability of the stake pool owner.

  • Must pay pending commissions before calling this.

public(friend) fun disperse_all_payouts(managed_pool_address: address, stake_pool_owner_cap: &stake::OwnerCapability)
Implementation
public(friend) fun disperse_all_payouts(
    managed_pool_address: address,
    stake_pool_owner_cap: &OwnerCapability,
) acquires SharesData, Payout {
    let stake_pool_address = get_stake_pool_address(managed_pool_address);
    let shares_data = borrow_global_mut<SharesData>(
        managed_pool_address
    );

    let total_withdrawable_amount =
        stake_pool_helpers::get_stake_pool_total_withdrawable_amount(
            stake_pool_address,
        );

    if (total_withdrawable_amount == 0) {
        // shares should never be worth zero
        // withdraw might be available at a later time, if the funds
        // corresponding to reserved shares are still locked
        return
    };

    let withdrawn_coins = stake::withdraw_with_cap(
        stake_pool_owner_cap,
        total_withdrawable_amount
    );

    let reserve_balance = coin::value<AptosCoin>(&withdrawn_coins);
    let reserved_share_supply = pool::share_supply(
        &shares_data.reserved_pool
    );
    let reserved_shares_to_burn = 0;
    let total_reserved_shares = 0;

    while (pool::num_share_holders(&shares_data.reserved_pool) > 0) {
        let (
            delegator_address,
            delegator_shares_for_payout
        ) = pool::remove_next_share_holder(&mut shares_data.reserved_pool);

        let payout_value = math::mul_div(
            delegator_shares_for_payout,
            reserve_balance,
            reserved_share_supply,
        );
        let payout_coins = coin::extract<AptosCoin>(
            &mut withdrawn_coins,
            payout_value
        );

        // If the delegator payout does not exist, this means it was a
        // commission recipient who did not create a payout store.
        // default to owner instead.
        let existing_delegator_payout = if (exists<Payout>(delegator_address)) {
            borrow_global_mut<Payout>(delegator_address)
        } else {
            borrow_global_mut<Payout>(managed_pool_address)
        };

        coin::merge<AptosCoin>(
            &mut existing_delegator_payout.coins,
            payout_coins
        );

        reserved_shares_to_burn = (
            reserved_shares_to_burn + delegator_shares_for_payout
        );
        total_reserved_shares = total_reserved_shares + payout_value;
    };

    pool::reset_pool(&mut shares_data.reserved_pool);
    // We let the owner take the floating point balances
    let existing_owner_payout = borrow_global_mut<Payout>(
        managed_pool_address
    );
    coin::merge<AptosCoin>(
        &mut existing_owner_payout.coins,
        withdrawn_coins
    );
}

Function pay_commission_to_owner

This mints new shares for the owner (and for the protocol as a commission rebate), so that owners new worth increases exactly by total commission due, and everyone else's worth decreases according to their due commission. Returns the total value of shares minted. If pending commission rates can be activated at the end. Do so.

This function does not move any coins, and can be called at any time.

Restrictions

public(friend) fun pay_commission_to_owner(managed_pool_address: address): u64
Implementation
public(friend) fun pay_commission_to_owner(
    managed_pool_address: address
): u64 acquires SharesData, PendingCommissions {
    // First, we make reserved share owners pay commission to the commission_recipient.
    // This mints new unreserved shares for the owner (and possibly for the
    // protocol as a commission rebate), so that owners new worth increases.
    let unreserved_commission = pay_commission_to_owner_unreserved(
        managed_pool_address
    );

    // Then we make reserved share owners pay commission to the commission_recipient.
    // This also mints new unreserved shares for the owner (and possibly for
    // the protocol).
    let reserved_commission = pay_commission_to_owner_reserved(
        managed_pool_address
    );

    commission_rate_bookkeeping(
        managed_pool_address,
        borrow_global_mut<SharesData>(managed_pool_address)
    );

    (unreserved_commission + reserved_commission)
}

Function delegate_internal

This function delegates coins_to_delegate on behalf of the delegator to the managed stake pool at managed_pool_address.

Restrictions

  • Only the friend module delegation_service can call this function.

  • The OwnerCapability for the underlying stake pool is required.

Assumes

  • max number of delegations is not violated

  • min delegation condition is not violated

  • commission has been paid and payouts dispersed

  • must pay pending commissions before calling this.

  • must disperse all payouts before calling this.

public(friend) fun delegate_internal(delegator: &signer, coins_to_delegate: coin::Coin<aptos_coin::AptosCoin>, managed_pool_address: address, stake_pool_owner_cap: &stake::OwnerCapability)
Implementation
public(friend) fun delegate_internal(
    delegator: &signer,
    coins_to_delegate: coin::Coin<AptosCoin>,
    managed_pool_address: address,
    stake_pool_owner_cap: &OwnerCapability
) acquires SharesData {
    let delegator_address = signer::address_of(delegator);
    let amount = coin::value<AptosCoin>(&coins_to_delegate);

    safe_create_payout_store(delegator);

    // We can now mint shares for the delegator and the owner
    let minted_shares = get_num_new_shares_to_mint(
        managed_pool_address,
        amount
    );

    let shares_data = borrow_global_mut<SharesData>(
        managed_pool_address
    );

    pool::buy_in(
        &mut shares_data.unreserved_pool,
        delegator_address,
        minted_shares,
        amount
    );

    stake::add_stake_with_cap(
        stake_pool_owner_cap,
        coins_to_delegate
    );
}

Function reserve_shares_for_withdraw_internal

Reserves at least shares num_shares to be withdrawn for delegator_address from the managed pool at managed_pool_address.

Restrictions

  • Only the friend module delegation_service can call this function.

  • The OwnerCapability for the underlying stake pool is required.

  • Must pay pending commissions before calling this.

  • Must disperse all payouts before calling this.

Aborts if

  • num_shares is greater than the number of shares owned by the delegator in the unreserved pool.

public(friend) fun reserve_shares_for_withdraw_internal(delegator_address: address, num_shares: u64, managed_pool_address: address, stake_pool_owner_cap: &stake::OwnerCapability)
Implementation
public(friend) fun reserve_shares_for_withdraw_internal(
    delegator_address: address,
    num_shares: u64,
    managed_pool_address: address,
    stake_pool_owner_cap: &OwnerCapability
) acquires SharesData {
    if (num_shares == 0) {
        return
    };

    let delegator_unreserved_shares = get_unreserved_shares_with_delegator(
        managed_pool_address,
        delegator_address
    );

    assert!(
        delegator_unreserved_shares >= num_shares,
        error::invalid_argument(ENOT_ENOUGH_UNRESERVED_SHARES)
    );

    // There are no pending payouts so unreserved shares have the same value
    // as reserved shares. We cannot check reserved here because there
    // might be no reserved shares.
    let shares_value = get_shares_to_value_unreserved(
        managed_pool_address,
        num_shares
    );

    let shares_data = borrow_global_mut<SharesData>(
        managed_pool_address
    );

    pool::buy_out(
        &mut shares_data.unreserved_pool,
        delegator_address,
        num_shares,
        shares_value
    );
    pool::buy_in(
        &mut shares_data.reserved_pool,
        delegator_address,
        num_shares,
        shares_value
    );

    stake::unlock_with_cap(
        shares_value,
        stake_pool_owner_cap
    );
}

Function self_destruct_internal

Sets the managed pool at managed_pool_address to be destroyed.

Restrictions

Assumes

  • The reserved and unreserved pools are empty.

Abort conditions

  • Owner is not the only delegator in either reserved or unreserved pool.

public(friend) fun self_destruct_internal(managed_pool_address: address)
Implementation
public(friend) fun self_destruct_internal(
    managed_pool_address: address
) acquires SharesData, PendingCommissions {
    assert!(
        is_owner_alone_in_the_pool(managed_pool_address),
        error::invalid_state(EPOOL_HAS_SHAREHOLDERS)
    );
    let SharesData {
        stake_pool_address: _a,
        current_commission: _c,
        commission_recipient_address: _cra,
        protocol_delegator_address: _p,
        protocol_commission: _pc,
        commission_set: _cs,
        reserved_pool,
        unreserved_pool,
    } = move_from<SharesData>(managed_pool_address);

    if(exists<PendingCommissions>(managed_pool_address)) {
        let PendingCommissions {
            update_timestamp: _ut,
            commission: _c,
            protocol_commission: _pc,
        } = move_from<PendingCommissions>(managed_pool_address);
    };

    pool::destroy_empty(reserved_pool, managed_pool_address, false);
    pool::destroy_empty(unreserved_pool, managed_pool_address, true);
}

Function create_or_update_pending_commissions

Create or update an existing PendingCommissions for the pool_owner Note: the update timestamp will reset for existing requests, thus restarting the clock for lockup duration commission update.

fun create_or_update_pending_commissions(pool_owner: &signer, new_default_commission: u64, new_protocol_commission: u64)
Implementation
fun create_or_update_pending_commissions(
    pool_owner: &signer,
    new_default_commission: u64,
    new_protocol_commission: u64,
) acquires PendingCommissions {
    let managed_pool_address = signer::address_of(pool_owner);
    if (!exists<PendingCommissions>(managed_pool_address)) {
        move_to(pool_owner, PendingCommissions {
            commission: 0,
            protocol_commission: 0,
            update_timestamp: 0,
        });
    };
    let pending_commissions = borrow_global_mut<PendingCommissions>(
        managed_pool_address
    );
    pending_commissions.commission = new_default_commission;
    pending_commissions.protocol_commission = new_protocol_commission;
    pending_commissions.update_timestamp = timestamp::now_seconds();
}

Function commission_rate_bookkeeping

If a StakingConfig lockup duration has passed since a requested commission update. Then we activate the pending commission rates.

fun commission_rate_bookkeeping(managed_pool_address: address, shares_data: &mut delegation_state::SharesData)
Implementation
fun commission_rate_bookkeeping(
    managed_pool_address: address,
    shares_data: &mut SharesData,
) acquires PendingCommissions {
    if (!exists<PendingCommissions>(managed_pool_address)) {
        return
    };
    let pending_commissions = borrow_global<PendingCommissions>(
        managed_pool_address
    );

    if (
        pending_commissions.update_timestamp > 0
        &&
        stake_pool_helpers::has_lockup_duration_passed(
            pending_commissions.update_timestamp
        )
    ) {

        shares_data.current_commission = pending_commissions.commission;
        shares_data.protocol_commission = pending_commissions.protocol_commission;
        // Remove pending commissions after updating
        let PendingCommissions {
            update_timestamp: _ut,
            commission: _c,
            protocol_commission: _pc,
        } = move_from<PendingCommissions>(managed_pool_address);
    }
}

Function pay_commission_to_owner_reserved

Mints new reserved shares to the owner based on how much commission was earned on reserved shares.

fun pay_commission_to_owner_reserved(managed_pool_address: address): u64
Implementation
fun pay_commission_to_owner_reserved(
    managed_pool_address: address
): u64 acquires SharesData {
    let reserved_total_balance = get_reserved_total_balance(
        managed_pool_address
    );
    let shares_data = borrow_global_mut<SharesData>(managed_pool_address);
    pool::pay_commission(
        &mut shares_data.reserved_pool,
        shares_data.commission_recipient_address,
        reserved_total_balance,
        shares_data.current_commission,
        shares_data.protocol_commission,
        COMMISSION_NORMALIZER,
        shares_data.protocol_delegator_address,
    )
}

Function pay_commission_to_owner_unreserved

Mints new reserved shares to the commission_recipient based on how much commission was earned on reserved shares. A new delegation is also subject stake generated on the epoch in which the delegation happens. See get_num_new_shares_to_mint() for more details.

Returns the total worth of the newly minted shares.

fun pay_commission_to_owner_unreserved(managed_pool_address: address): u64
Implementation
fun pay_commission_to_owner_unreserved(
    managed_pool_address: address
): u64 acquires SharesData {
    let stake_pool_address = get_stake_pool_address(managed_pool_address);
    let shares_data = borrow_global_mut<SharesData>(managed_pool_address);
    let unreserved_total_balance =
        stake_pool_helpers::get_stake_pool_non_withdrawing_balance(
            stake_pool_address
        );

    pool::pay_commission(
        &mut shares_data.unreserved_pool,
        shares_data.commission_recipient_address,
        unreserved_total_balance,
        shares_data.current_commission,
        shares_data.protocol_commission,
        COMMISSION_NORMALIZER,
        shares_data.protocol_delegator_address,
    )
}

Function get_num_new_shares_to_mint

Returns the number of shares the pool should mint given the current state of the reserved and unreserved pool for value_being_added.

Assumes

  • there is no restakable balance

  • the owner has been paid any pending commissions

  • the reserved shared have been dispersed

The last assumption is a simplifying one as shares corresponding to inactive balance depreciate in value. Making that assumption simplifies shares calculation.

fun get_num_new_shares_to_mint(managed_pool_address: address, value_being_added: u64): u64
Implementation
fun get_num_new_shares_to_mint(
    managed_pool_address: address,
    value_being_added: u64
): u64 acquires SharesData {
    // Here total_worth_next_epoch is needed so as to prevent last moment
    // stake attack from a delegator.  This assumes the
    // validator has 100% proposal success in the current epoch, and thus is
    // a small disadvantage to new stakers.
    let total_worth_next_epoch =
        stake_pool_helpers::get_stake_pool_total_after_current_epoch_generation(
            get_stake_pool_address(managed_pool_address)
        );

    // Newly minted shares below are still subject to some commission, at
    // the end of the current epoch, on stake earned by others during the
    // current epoch.  But after the next epoch the total value of their
    // share will be > value_being_added, given that validator is
    // performing, as their initial commission responsibility will be small.
    math::mul_div_with_init(
        value_being_added,
        get_share_supply(managed_pool_address),
        total_worth_next_epoch,
        INITIAL_SHARES_FACTOR,
    )
}

Last updated