Links
Comment on page

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. 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. 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. 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
),