delegation::delegation_service

Module 0xc0ded0c0::delegation_service

This module provides a delegation service for staking on Aptos.

It:

  1. builds on top of delegation_state module,

  2. consumes the owner_capability of a StakePool owner and uses it to delegate to the pool,

  3. makes the liquid staking protocol just one of many delegators,

  4. requires the owner to join with the minimum stake as specified by the aptos_framework, and cannot go below that amount until they self-destruct, and,

  5. lets the pool owner can retrieve the owner_capability of their StakePool after a successful self-destruct.

use 0x1::aptos_coin;
use 0x1::coin;
use 0x1::error;
use 0x1::signer;
use 0x1::stake;
use 0xc0ded0c0::delegation_state;
use 0xc0ded0c0::stake_pool_helpers;

Resource ManagedStakePool

The struct which holds the OwnerCapability of the StakePool and stores config parameters.

struct ManagedStakePool has key
Fields

stake_pool_owner_cap: stake::OwnerCapabilitystores the OwnerCapability of the StakePool owner.in_self_destruct: boolstores whether self-destruct has been called by the owner. The ManagedStakingPool does not accept new delegations in self-destruct mode.min_delegation_amount: u64max_commission: u64

Struct ManageCapability

Holders of ManageCapability can call permissioned functions. It is returned during initialization.

struct ManageCapability has store
Fields

managed_pool_address: address

Constants

The minimum stake that can be delegated to a managed stake pool.

const DEFAULT_MINIMUM_DELEGATION_AMOUNT: u64 = 10000000000000;

When trying to remove the stake pool owner's delegation during self-destruct.

const ECANNOT_FORCE_OWNER_TO_WITHDRAW: u64 = 12;

When the commission being set is higher than the max_commission.

const ECOMMISSION_EXCEEDS_MAX: u64 = 2;

When the delegation amount is below the threshold.

const EDELEGATION_AMOUNT_TOO_SMALL: u64 = 10;

When the managed stake pool already exists at the address during initialization.

const EMANAGED_POOL_ALREADY_EXISTS: u64 = 5;

When the stake of the owner is below the minimum stake, and the pool is not in self-destruct.

const EMINIMUM_VIOLATION: u64 = 7;

When the the pool is not in self-destruct state.

const ENOT_SELF_DESTRUCTING: u64 = 8;

When the managed stake pool does not exist.

const EPOOL_DOES_NOT_EXIST: u64 = 11;

When the commission being charged for the protocol delegator is higher than the commission being charged to outside delegators.

const EPROTOCOL_COMMISSION_EXCEEDS_DEFAULT: u64 = 3;

When the pool is in the self-destruct state.

const ESELF_DESTRUCTING: u64 = 1;

When the maximum number of delegators are already staked.

const ETOO_MANY_DELEGATIONS: u64 = 9;

The maximum number of delegators that can delegate to a managed pool.

const MAX_NUMBER_OF_DELEGATIONS: u64 = 100;

Function assert_is_self_destructing

Assert the pool is in self-destruct mode

Abort conditions

  • If the managed stake pool is not in self-destruct state.

public fun assert_is_self_destructing(managed_pool_address: address)
Implementation
public fun assert_is_self_destructing(
    managed_pool_address: address
) acquires ManagedStakePool {
    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );
    assert!(
        managed_stake_pool.in_self_destruct,
        error::invalid_state(ENOT_SELF_DESTRUCTING)
    );
}

Function begin_self_destruct

Move the Managed Stake Pool into self-destruct mode. Cannot be reversed.

Restrictions

Abort conditions

  • If the pool does not exist.

public fun begin_self_destruct(manage_cap: &delegation_service::ManageCapability)
Implementation
public fun begin_self_destruct(
    manage_cap: &ManageCapability,
) acquires ManagedStakePool {
    let managed_pool_address = manage_cap.managed_pool_address;
    assert_pool_exists(managed_pool_address);
    let managed_stake_pool = borrow_global_mut<ManagedStakePool>(
        managed_pool_address
    );
    managed_stake_pool.in_self_destruct = true;
}

Function finish_self_destruct

Destroys the ManageStakePool and returns the StakePool's OwnerCapability.

Restrictions

Abort conditions

  • If the pool is not in self-destruct state.

public fun finish_self_destruct(manage_cap: delegation_service::ManageCapability): stake::OwnerCapability
Implementation
public fun finish_self_destruct(
    manage_cap: ManageCapability,
): OwnerCapability acquires ManagedStakePool {
    let managed_pool_address = manage_cap.managed_pool_address;
    assert_is_self_destructing(managed_pool_address);

    let ManagedStakePool {
        stake_pool_owner_cap: owner_cap,
        in_self_destruct: _b,
        min_delegation_amount: _c,
        max_commission: _m,
    } = move_from<ManagedStakePool>(managed_pool_address);

    delegation_state::pay_commission_to_owner(managed_pool_address);
    delegation_state::disperse_all_payouts(
        managed_pool_address,
        &owner_cap
    );
    delegation_state::self_destruct_internal(managed_pool_address);

    let ManageCapability {
        managed_pool_address: _mpa,
    } = manage_cap;

    owner_cap
}

Function pay_commission_to_owner

Compound ManagedStakePool's commission. Returns the amount of commission paid to the owner in APT.

Restrictions

public fun pay_commission_to_owner(manage_cap: &delegation_service::ManageCapability): u64
Implementation
public fun pay_commission_to_owner(
    manage_cap: &ManageCapability,
): u64 {
    assert_pool_exists(manage_cap.managed_pool_address);
    delegation_state::pay_commission_to_owner(
        manage_cap.managed_pool_address
    )
}

Function initialize

Allows a StakePool owner managed_pool_owner to start a ManagedStakePool that can accept delegations from outside delegators. The maximum commission that can be charged is given by max_commission. The address of the Tortuga protocol delegator is given by protocol_delegator_address and the commission charged to the protocol delegator is given by protocol_commission.

Returns the ManageCapability of the newly created ManagedStakePool.

Note: Commissions are specified in 6 decimal precision, following the value of COMMISSION_NORMALIZER in delegation_state module (i.e. commission of 1000000 = 100%).

Restrictions

  • To be associated with Tortuga protocol, this function should be called from validator_router module.

Abort conditions

  • If the ManagedStakePool already exists on the given address.

  • protocol_commission must be lower than max_commission.

public fun initialize(managed_pool_owner: &signer, max_commission: u64, commission_recipient_address: address, protocol_delegator_address: address, protocol_commission: u64): delegation_service::ManageCapability
Implementation
public fun initialize(
    managed_pool_owner: &signer,
    max_commission: u64,
    commission_recipient_address: address,
    protocol_delegator_address: address,
    protocol_commission: u64,
): ManageCapability acquires ManagedStakePool {
    let managed_pool_address = signer::address_of(managed_pool_owner);
    assert!(
        !exists<ManagedStakePool>(managed_pool_address),
        error::invalid_argument(EMANAGED_POOL_ALREADY_EXISTS)
    );
    // extract `owner_capability` from the pool_owner
    let stake_pool_owner_cap = stake::extract_owner_cap(managed_pool_owner);
    let stake_pool_address = stake::get_owned_pool_address(
        &stake_pool_owner_cap
    );

    // store the ManagedStakePool
    let managed_stake_pool = ManagedStakePool {
        stake_pool_owner_cap: stake_pool_owner_cap,
        in_self_destruct: false,
        min_delegation_amount: DEFAULT_MINIMUM_DELEGATION_AMOUNT,
        max_commission: max_commission,
    };
    move_to(managed_pool_owner, managed_stake_pool);

    delegation_state::initialize_internal(
        managed_pool_owner,
        protocol_delegator_address,
        commission_recipient_address,
        stake_pool_address
    );

    change_commission(
        managed_pool_owner,
        max_commission,
        protocol_commission
    );

    ManageCapability {
        managed_pool_address: managed_pool_address,
    }
}

Function set_min_delegation_amount

Set minimum acceptable delegations from outside delegators. Does not affect pool owner and the protocol.

Restrictions

  • Can only be called by the pool_owner.

public fun set_min_delegation_amount(pool_owner: &signer, value: u64)
Implementation
public entry fun set_min_delegation_amount(
    pool_owner: &signer,
    value: u64
) acquires ManagedStakePool {
    let managed_pool_address = signer::address_of(pool_owner);
    let managed_stake_pool = borrow_global_mut<ManagedStakePool>(
        managed_pool_address
    );
    managed_stake_pool.min_delegation_amount = value;
}

Function delegate

Lets a delegator to delegate amount APT with a ManagedStakePool at address managed_pool_address.

Abort conditions

public fun delegate(delegator: &signer, managed_pool_address: address, amount: u64)
Implementation
public entry fun delegate(
    delegator: &signer,
    managed_pool_address: address,
    amount: u64,
) acquires ManagedStakePool {
    assert_not_self_destructing(managed_pool_address);

    let delegator_address = signer::address_of(delegator);
    certify_delegation(managed_pool_address, delegator_address, amount);

    let coins_to_delegate = coin::withdraw<AptosCoin>(delegator, amount);
    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );

    // make sure that newly minted shares for the delegator are not subject
    // to any commissions by paying outstanding commissions first
    delegation_state::pay_commission_to_owner(managed_pool_address);
    delegation_state::disperse_all_payouts(
        managed_pool_address,
        &managed_stake_pool.stake_pool_owner_cap
    );

    delegation_state::delegate_internal(
        delegator,
        coins_to_delegate,
        managed_pool_address,
        &managed_stake_pool.stake_pool_owner_cap
    );
}

Function change_commission

Change the default and protocol commissions.

Restrictions

  • Only the pool_owner can call this function.

Abort condition

  • If the ManagedStakePool does not exist.

  • If the new_default_commission is greater than max_commission.

  • If the new_protocol_commission is greater than new_default_commission.

public fun change_commission(pool_owner: &signer, new_default_commission: u64, new_protocol_commission: u64)
Implementation
public entry fun change_commission(
    pool_owner: &signer,
    new_default_commission: u64,
    new_protocol_commission: u64,
) acquires ManagedStakePool {
    let managed_pool_address = signer::address_of(pool_owner);
    assert_pool_exists(managed_pool_address);

    let managed_stake_pool = borrow_global_mut<ManagedStakePool>(
        managed_pool_address
    );
    assert!(
        new_default_commission <= managed_stake_pool.max_commission,
        error::invalid_argument(ECOMMISSION_EXCEEDS_MAX)
    );
    assert!(
        new_protocol_commission <= new_default_commission,
        error::invalid_argument(EPROTOCOL_COMMISSION_EXCEEDS_DEFAULT)
    );

    // pay outstanding commission on original commission rate
    delegation_state::pay_commission_to_owner(managed_pool_address);

    delegation_state::change_commission_internal(
        pool_owner,
        new_default_commission,
        new_protocol_commission,
    );
}

Function change_commission_recipient

Change the commission recipient of the ManagedStakePool to new_commission_recipient_address.

Restrictions

  • Only the pool_owner can call this function.

Abort conditions

public fun change_commission_recipient(pool_owner: &signer, new_commission_recipient_address: address)
Implementation
public entry fun change_commission_recipient(
    pool_owner: &signer,
    new_commission_recipient_address: address,
) {
    let managed_pool_address = signer::address_of(pool_owner);
    assert_pool_exists(managed_pool_address);

    // pay outstanding commission to the old recipient
    delegation_state::pay_commission_to_owner(managed_pool_address);

    delegation_state::change_commission_recipient_internal(
        managed_pool_address,
        new_commission_recipient_address,
    );
}

Function reserve_shares_for_withdraw

delegator can call this function to withdraw num_shares from the ManagedStakePool at address managed_pool_address. This will move delegator shares from unreserved_pool to reserved_pool and move corresponding StakePool funds from 'active' to 'pending_inactive' state.. May have to call trigger_payout_dispersal when the stake unlocks (i.e., changes state from 'pending_inactive' to 'inactive').

Restrictions

  • Only the delegator can call this function for themselves.

Abort conditions

  • If the withdrawal amount will leave less than min_delegation_amount in the delegator's account. However, it allows all the funds to be atomically withdrawn.

public fun reserve_shares_for_withdraw(delegator: &signer, managed_pool_address: address, num_shares: u64)
Implementation
public entry fun reserve_shares_for_withdraw(
    delegator: &signer,
    managed_pool_address: address,
    num_shares: u64,
) acquires ManagedStakePool {
    assert_pool_exists(managed_pool_address);
    let delegator_address = signer::address_of(delegator);

    // Check for min violation
    ensure_minimum_delegation_remaining(
        managed_pool_address,
        delegator_address,
        num_shares
    );

    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );

    // make sure there is no outstanding commission in either pool before
    // transferring
    delegation_state::pay_commission_to_owner(managed_pool_address);
    delegation_state::disperse_all_payouts(
        managed_pool_address,
        &managed_stake_pool.stake_pool_owner_cap
    );

    delegation_state::reserve_shares_for_withdraw_internal(
        delegator_address,
        num_shares,
        managed_pool_address,
        &managed_stake_pool.stake_pool_owner_cap
    );
}

Function trigger_payout_dispersal

Disperses all APT if there is inactive stake in the StakePool. Can be called permissionlessly.

Abort conditions

public fun trigger_payout_dispersal(managed_pool_address: address)
Implementation
public entry fun trigger_payout_dispersal(
    managed_pool_address: address
) acquires ManagedStakePool {
    assert_pool_exists(managed_pool_address);
    let stake_pool_address = delegation_state::get_stake_pool_address(
        managed_pool_address
    );

    // nothing to disperse
    if (
        stake_pool_helpers::get_stake_pool_total_withdrawable_amount(
            stake_pool_address
        ) == 0
    ) {
        return
    };

    // first pay commission to the owner
    delegation_state::pay_commission_to_owner(managed_pool_address);

    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );
    delegation_state::disperse_all_payouts(
        managed_pool_address,
        &managed_stake_pool.stake_pool_owner_cap
    );
}

Function remove_non_owner_delegator

Reserve all shares for withdrawal for delegator_address. Can be called by anyone but only when pool is in self-destruct state. This ensures the owner can remove all delegators and retrieve their owner_capability.

Abort conditions

public fun remove_non_owner_delegator(managed_pool_address: address, delegator_address: address)
Implementation
public entry fun remove_non_owner_delegator(
    managed_pool_address: address,
    delegator_address: address,
) acquires ManagedStakePool {
    assert_pool_exists(managed_pool_address);
    assert_is_self_destructing(managed_pool_address);
    assert!(
        delegator_address != managed_pool_address,
        error::invalid_argument(ECANNOT_FORCE_OWNER_TO_WITHDRAW),
    );

    let managed_stake_pool = borrow_global<ManagedStakePool>(managed_pool_address);
    delegation_state::pay_commission_to_owner(managed_pool_address);
    delegation_state::disperse_all_payouts(
        managed_pool_address,
        &managed_stake_pool.stake_pool_owner_cap
    );

    let delegator_unreserved_shares =
        delegation_state::get_unreserved_shares_with_delegator(
            managed_pool_address,
            delegator_address
        );

    delegation_state::reserve_shares_for_withdraw_internal(
        delegator_address,
        delegator_unreserved_shares,
        managed_pool_address,
        &managed_stake_pool.stake_pool_owner_cap,
    );
}

Function set_operator

Allow the stake_pool owner to set new_operator as the operator of the pool. See 0x1::stake::set_operator_with_cap for details.

public fun set_operator(pool_owner: &signer, new_operator: address)
Implementation
public entry fun set_operator(
    pool_owner: &signer,
    new_operator: address
) acquires ManagedStakePool {
    let managed_pool_address = signer::address_of(pool_owner);
    assert_pool_exists(managed_pool_address);
    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );
    stake::set_operator_with_cap(
        &managed_stake_pool.stake_pool_owner_cap,
        new_operator
    );
}

Function set_delegated_voter

Allow the stake_pool owner to set new_voter as the delegated voter of the pool. See 0x1::stake::set_delegated_voter_with_cap for details.

public fun set_delegated_voter(pool_owner: &signer, new_voter: address)
Implementation
public entry fun set_delegated_voter(
    pool_owner: &signer,
    new_voter: address
) acquires ManagedStakePool {
    let managed_pool_address = signer::address_of(pool_owner);
    assert_pool_exists(managed_pool_address);
    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );
    stake::set_delegated_voter_with_cap(
        &managed_stake_pool.stake_pool_owner_cap,
        new_voter
    );
}

Function assert_not_self_destructing

Assert the pool is not in self-destruct mode

Abort conditions

  • If the pool is in self-destruct mode.

fun assert_not_self_destructing(managed_pool_address: address)
Implementation
fun assert_not_self_destructing(
    managed_pool_address: address
) acquires ManagedStakePool {
    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );
    assert!(
        !managed_stake_pool.in_self_destruct,
        error::invalid_state(ESELF_DESTRUCTING)
    );
}

Function assert_pool_exists

Assert the ManagedStakePool has been initialized at the provided address.

Abort conditions

fun assert_pool_exists(managed_pool_address: address)
Implementation
fun assert_pool_exists(managed_pool_address: address) {
    assert!(
        exists<ManagedStakePool>(managed_pool_address),
        error::invalid_argument(EPOOL_DOES_NOT_EXIST)
    );
}

Function certify_delegation

Certifies that a delegation with amount from delegator_address can be made. Aborts if criteria are not met. The protocol_delegator and the pool_owner, and existing delegators are not subject to these conditions.

Abort conditions

fun certify_delegation(managed_pool_address: address, delegator_address: address, amount: u64)
Implementation
fun certify_delegation(
    managed_pool_address: address,
    delegator_address: address,
    amount: u64
) acquires ManagedStakePool {
    if (delegator_address == managed_pool_address) {
        return
    };
    if (
        delegator_address ==
            delegation_state::get_protocol_delegator_address(
                managed_pool_address
            )
    ) {
        return
    };

    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );
    let is_current_delegator = delegation_state::is_an_active_delegator(
        managed_pool_address,
        delegator_address
    );
    if (is_current_delegator) {
        return
    };

    assert!(
        amount >= managed_stake_pool.min_delegation_amount,
        error::invalid_argument(EDELEGATION_AMOUNT_TOO_SMALL)
    );

    let current_length = delegation_state::get_total_length_of_share_maps(
        managed_pool_address
    );
    assert!(
        // this actually allows (MAX_NUMBER_OF_DELEGATIONS + 1) delegators,
        // including the owner
        current_length <= MAX_NUMBER_OF_DELEGATIONS,
        error::out_of_range(ETOO_MANY_DELEGATIONS)
    );
}

Function ensure_minimum_delegation_remaining

Ensures withdrawal does not change the delegator's stake below the minimum, unless a non-stake-pool-owner delegator is withdrawing all of their stake.

Abort conditions

  • If the non-owner delegator's stake would be non-zero but below the minimum after the withdrawal.

  • If the owner delegator's stake would be below the min_stake required by the aptos_framework after the withdrawal.

fun ensure_minimum_delegation_remaining(managed_pool_address: address, delegator_address: address, num_shares_to_withdraw: u64)
Implementation
fun ensure_minimum_delegation_remaining(
    managed_pool_address: address,
    delegator_address: address,
    num_shares_to_withdraw: u64
) acquires ManagedStakePool {
    // protocol is not subject to minimums
    if (
        delegator_address ==
            delegation_state::get_protocol_delegator_address(
                managed_pool_address
            )
    ) {
        return
    };
    // only called from `reserve_shares_to_withdraw`, pool existence is
    // ensured
    let managed_stake_pool = borrow_global<ManagedStakePool>(
        managed_pool_address
    );
    let current_unreserved_shares =
        delegation_state::get_unreserved_shares_with_delegator(
            managed_pool_address,
            delegator_address
        );

    if (
        delegator_address != managed_pool_address &&
            current_unreserved_shares == num_shares_to_withdraw
    ) {
        // Non-owner delegator can always withdraw all of their stake
        return
    };

    let remaining_balance_after_withdrawal =
        delegation_state::get_shares_to_value_unreserved(
            managed_pool_address,
            current_unreserved_shares - num_shares_to_withdraw
        );

    let minimum = if (delegator_address == managed_pool_address) {
        stake_pool_helpers::get_framework_min_stake()
    } else {
        managed_stake_pool.min_delegation_amount
    };

    assert!(
        remaining_balance_after_withdrawal >= minimum ||
            managed_stake_pool.in_self_destruct,
        error::invalid_argument(EMINIMUM_VIOLATION)
    );
}

Last updated