tortuga::validator_router

Module 0xc0ded0c0::validator_router

module tortuga::validator_router

This module handles interaction between the liquid staking protocol and the validators. It is responsible for:

  • delegations and withdrawals from validators,

  • validator signup and removal.

The delegators to the protocol will interact usually with tortuga::stake_router module.

This module uses oracle::validator_states module, which keeps track of changes in the states of the validators and calculates their performance.

It provides an endpoint for validators to be able to charge commissions. Charging commission often will compound the commissions, and will also keep update the state of this contract.

It also manages the AptosCoinReserve for the protocol, which holds the APT that the delegators have requested for withdrawal (but not claimed yet) or deposited to the protocol.

Validators will receive delegations from the protocol after they call validator_signup. They also will be able to receive outside delegations.

This module only manages the stake that the protocol has with the validators.

use 0x1::account;
use 0x1::aptos_coin;
use 0x1::bcs;
use 0x1::coin;
use 0x1::error;
use 0x1::event;
use 0x1::option;
use 0x1::signer;
use 0x1::simple_map;
use 0x1::stake;
use 0x1::timestamp;
use 0xc0ded0c0::delegation_service;
use 0xc0ded0c0::delegation_state;
use 0xc0ded0c0::iterable_table_custom;
use 0xc0ded0c0::stake_pool_helpers;
use 0xc0ded0c0::validator_states;
use 0xc0ded0c1::permissions;
use 0xc0ded0c2::tortuga_governance;

Resource ValidatorContract

This struct is used to allow permissioned validator signup.

struct ValidatorContract has store, key
Fields

dummy_field: bool

Resource Status

This struct stores validator signup details.

struct Status has key
Fields

vetted_validators: simple_map::SimpleMap<address, validator_router::ValidatorContract>Vetted validators, if self-signup is disabledallow_self_signup: boolWhether to allow self-signup for validators.max_number_of_validators: u64Maximum number of validator.max_max_commission: u64Maximum number of validator.unclaimed_stake_pool_owner_caps: iterable_table_custom::IterableTableCustom<address, stake::OwnerCapability>Unclaimed stake_pool_owner_cap from forcibly removed validators.validate_states_update_cap: validator_states::UpdateCapabilityStores the permission to update oracle::validator_states::ValidatorSystem.validator_signup_events: event::EventHandle<validator_router::ValidatorSignupEvent>Event handle for ValidatorSignupEvent.validator_offboard_events: event::EventHandle<validator_router::ValidatorOffboardEvent>Event handle for ValidatorOffboardEvent.withdraw_signature_set_events: event::EventHandle<validator_router::WithdrawSignatureSetEvent>Event handle for WithdrawSignatureSetEvent.

Resource WithdrawSignature

Withdrawal to distribute claims should be from earliest validator to maximize the liquidity of the protocol. This struct helps with that mechanism as follows:

  • protocol sets a withdraw signature which is valid as long as timestamp::now_seconds() < unlocking_at and managed_pool_address is still a validator in our set.

  • In case of a deficit, anyone can then request a withdraw for any validator as long as that validator's unlocking_at is not later than the one in withdraw signature.

  • If the withdraw signature has expired (i.e. the managed_pool_address has either left the validator set of the liquid staking contract or has unlocked already), then a withdrawal can be requested from any validator.

This strikes a balance between keeping the protocol liquid and not allowing early withdraw their stake before they are supposed to.

struct WithdrawSignature has drop, key
Fields

managed_pool_address: addressAddress where the ManagedStakePool for the validator is stored.unlocking_at: u64The timestamp when the validator's stake will be unlocked.

Resource AptosCoinReserve

This reserve store coins which can be either delegated to validators, or withdrawn from them to clear Tickets in stake_router module.

struct AptosCoinReserve has key
Fields

coin: coin::Coin<aptos_coin::AptosCoin>Store for AptosCoin.

Resource DelegationAccounts

These resource accounts delegate to the individual validators

struct DelegationAccounts has store, key
Fields

accounts: iterable_table_custom::IterableTableCustom<address, validator_router::DelegationAccount>Map from address of an associated validator to their DelegationAccount.pay_commission_events: event::EventHandle<validator_router::PayCommissionEvent>Event handle for PayCommissionEvent.withdraw_to_reserve_events: event::EventHandle<validator_router::WithdrawToReserveEvent>Event handle for DelegationAccountCreatedEvent.

Struct DelegationAccount

Delegation account for a validator, stores the address and capabilities.

struct DelegationAccount has store
Fields

managed_pool_address: addressAddress where the ManagedStakePool for the validator is stored.signer_cap: account::SignerCapabilityThe SignerCapability for the resource account that delegates to the validator.delegation_service_manage_cap: delegation_service::ManageCapabilityThe ManageCapability for the validator's ManagedStakePool.

Struct ValidatorSignupEvent

Event generated when a validator successfully signs up.

struct ValidatorSignupEvent has drop, store
Fields

managed_pool_address: addressAddress where the ManagedStakePool for the validator is stored.

Struct ValidatorOffboardEvent

Event generated when a validator is off-boarded.

struct ValidatorOffboardEvent has drop, store
Fields

managed_pool_address: addressAddress where the ManagedStakePool for the validator is stored.

Struct PayCommissionEvent

Event generated when a validator is paid commission.

struct PayCommissionEvent has drop, store
Fields

managed_pool_address: addressAddress where the ManagedStakePool for the validator is stored.commission: u64Commission paid in APT.

Struct WithdrawToReserveEvent

Event generated when a validator withdraws to the reserve.

struct WithdrawToReserveEvent has drop, store
Fields

managed_pool_address: addressAddress where the ManagedStakePool for the validator is stored.amount: u64Amount withdrawn in APT.

Struct WithdrawSignatureSetEvent

Event generated when the contract sets a WithdrawSignature.

struct WithdrawSignatureSetEvent has drop, store
Fields

managed_pool_address: addressAddress of the validator unlocking at unlocking_at.unlocking_at: u64The timestamp when the validator's stake will be unlocked.

Constants

Validator state active.

const VALIDATOR_STATUS_ACTIVE: u64 = 2;

Validator state inactive.

const VALIDATOR_STATUS_INACTIVE: u64 = 4;

The following constants must match those of stake.move Validator state pending_active.

const VALIDATOR_STATUS_PENDING_ACTIVE: u64 = 1;

Validator state pending_inactive.

const VALIDATOR_STATUS_PENDING_INACTIVE: u64 = 3;

When an unauthorized action is performed.

const EPERMISSION_DENIED: u64 = 3;

The maximum minimum transaction amount

const DEFAULT_MAX_MAX_COMMISSION: u64 = 1000000;

When a validator is already associated with the protocol.

const EALREADY_AN_ASSOCIATED_VALIDATOR: u64 = 10;

When a validator tries to rejoin without claiming the owner capability first.

const ECANNOT_REJOIN_WITH_UNCLAIMED_OWNER_CAP: u64 = 13;

When a validator already has a ValidatorContract.

const ECONTRACT_ALREADY_ISSUED: u64 = 4;

When a validator is not in ACTIVE state.

const EINACTIVE_VALIDATOR: u64 = 7;

When the max_commission is too high.

const EMAX_COMMISSION_TOO_HIGH: u64 = 16;

When the protocol has a withdrawable amount with a validator.

const ENONZERO_WITHDRAWABLE_AMOUNT: u64 = 9;

When the module is not initialized.

const ENOT_INITIALIZED: u64 = 11;

When there is no OwnerCapability to claim with the protocol.

const ENO_OWNER_CAP_TO_CLAIM: u64 = 12;

When self-signup is not allowed.

const ESELF_SIGNUP_NOT_ALLOWED: u64 = 15;

When more than max_number_of_validators validators are already associated with the protocol.

const ETOO_MANY_VALIDATORS: u64 = 8;

When the validator is not associated with the protocol.

const EVALIDATOR_NOT_FOUND: u64 = 2;

When an unlock is requested from a validator but is not allowed by the current WithdrawSignature.

const EVALIDATOR_UNLOCK_IS_TOO_FAR: u64 = 14;

Function initialize

Initializes the module with a given max_number_of_validators. It is initialized in the module tortuga::stake_router.

public(friend) fun initialize(tortuga_governance_deployer: &signer, max_number_of_validators: u64)
Implementation
public(friend) fun initialize(
    tortuga_governance_deployer: &signer,
    max_number_of_validators: u64,
) {
    move_to(tortuga_governance_deployer, AptosCoinReserve { coin: coin::zero<AptosCoin>() });

    let validator_signup_events =
        account::new_event_handle<ValidatorSignupEvent>(tortuga_governance_deployer);
    let validator_offboard_events =
        account::new_event_handle<ValidatorOffboardEvent>(tortuga_governance_deployer);
    let withdraw_signature_set_events =
        account::new_event_handle<WithdrawSignatureSetEvent>(tortuga_governance_deployer);

    let cap = validator_states::initialize_validator_states(tortuga_governance_deployer);

    move_to(tortuga_governance_deployer, Status {
        vetted_validators: simple_map::create<address, ValidatorContract>(),
        allow_self_signup: false,
        max_number_of_validators: max_number_of_validators,
        max_max_commission: DEFAULT_MAX_MAX_COMMISSION,
        unclaimed_stake_pool_owner_caps:
            iterable_table_custom::new<address, OwnerCapability>(),
        validator_signup_events,
        validator_offboard_events,
        withdraw_signature_set_events,
        validate_states_update_cap: cap,
    });

    move_to(tortuga_governance_deployer, DelegationAccounts {
        accounts: iterable_table_custom::new<address, DelegationAccount>(),
        pay_commission_events:
            account::new_event_handle<PayCommissionEvent>(tortuga_governance_deployer),
        withdraw_to_reserve_events:
            account::new_event_handle<WithdrawToReserveEvent>(tortuga_governance_deployer),
    });

    // Set a dummy withdraw signature to start with
    move_to(tortuga_governance_deployer, WithdrawSignature {
        managed_pool_address: @aptos_framework,
        unlocking_at: 0
    });
}

Function assert_no_withdrawable_amount

Ensures that the protocol does not have any currently withdrawable amount in the validator at managed_pool_address.

Abort conditions

  • If is_withdrawable_amount_nonzero() returns true.

public fun assert_no_withdrawable_amount(managed_pool_address: address)
Implementation
public fun assert_no_withdrawable_amount(
    managed_pool_address: address
) acquires DelegationAccounts {
    assert!(
        !is_withdrawable_amount_positive(managed_pool_address),
        error::invalid_state(ENONZERO_WITHDRAWABLE_AMOUNT)
    );
}

Function get_total_balance

Returns the total balance in APT staked with the validators and in the AptosCoinReserve. This may will only be 100% accurate if the oracle was updated in the current epoch.

public fun get_total_balance(): u64
Implementation
public fun get_total_balance(): u64 acquires AptosCoinReserve {
    get_reserve_balance() +
        validator_states::get_total_balance_with_validators(
            @tortuga_governance
        )
}

Function get_reserve_balance

Returns the balance in the AptosCoinReserve.

public fun get_reserve_balance(): u64
Implementation
public fun get_reserve_balance(): u64 acquires AptosCoinReserve {
    coin::value<AptosCoin>(
        &borrow_global<AptosCoinReserve>(@tortuga_governance).coin
    )
}

Function get_current_number_of_validators

Returns the number of associated validators.

public fun get_current_number_of_validators(): u64
Implementation
public fun get_current_number_of_validators(): u64
acquires DelegationAccounts {
    let accounts_ref = &borrow_global<DelegationAccounts>(
        @tortuga_governance
    ).accounts;
    iterable_table_custom::length<address, DelegationAccount>(accounts_ref)
}

Function get_max_number_of_validators

Returns the current maximum number of validators.

public fun get_max_number_of_validators(): u64
Implementation
public fun get_max_number_of_validators(): u64 acquires Status {
    borrow_global<Status>(@tortuga_governance).max_number_of_validators
}

Function get_allow_self_signup

Returns true if self-signup is allowed, false otherwise.

public fun get_allow_self_signup(): bool
Implementation
public fun get_allow_self_signup(): bool acquires Status {
    borrow_global<Status>(@tortuga_governance).allow_self_signup
}

Function is_withdrawable_amount_positive

Return true if there is any withdrawable balance in the validator at managed_pool_address.

public fun is_withdrawable_amount_positive(managed_pool_address: address): bool
Implementation
public fun is_withdrawable_amount_positive(
    managed_pool_address: address
): bool acquires DelegationAccounts {
    if (
        delegation_state::get_protocol_payout_value(
            managed_pool_address
        ) > 0
    ) {
        true
    } else if (
        delegation_state::get_reserved_shares_with_delegator(
            managed_pool_address,
            get_delegation_account_address(managed_pool_address)
        ) > 0
    ) {
        let stake_pool_address =
            delegation_state::get_stake_pool_address(
                managed_pool_address
            );
        let total_withdrawable_amount =
            stake_pool_helpers::get_stake_pool_total_withdrawable_amount(
                stake_pool_address
            );
        total_withdrawable_amount > 0
    } else {
        false
    }
}

Function is_an_associated_validator

Returns if the validator at managed_pool_address is associated with the protocol.

public fun is_an_associated_validator(managed_pool_address: address): bool
Implementation
public fun is_an_associated_validator(
    managed_pool_address: address
): bool acquires DelegationAccounts {
    let accounts_ref = &borrow_global<DelegationAccounts>(
        @tortuga_governance
    ).accounts;

    iterable_table_custom::contains<address, DelegationAccount>(
        accounts_ref,
        managed_pool_address
    )
}

Function set_max_number_of_validators

Set the maximum number of validators that can be associated with the protocol to value.

Restrictions

public fun set_max_number_of_validators(tortuga_governance: &signer, value: u64)
Implementation
public entry fun set_max_number_of_validators(
    tortuga_governance: &signer,
    value: u64
) acquires Status {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    let signup_status = borrow_global_mut<Status>(@tortuga_governance);
    signup_status.max_number_of_validators = value;
}

Function set_max_max_commission

Set the maximum max_commission for validators to value This does not affect existing validators.

Restrictions

public fun set_max_max_commission(tortuga_governance: &signer, value: u64)
Implementation
public entry fun set_max_max_commission(
    tortuga_governance: &signer,
    value: u64
) acquires Status {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    let signup_status = borrow_global_mut<Status>(@tortuga_governance);
    signup_status.max_max_commission = value;
}

Function set_allow_self_signup

Set the allow_self_signup flag.

Restrictions

public fun set_allow_self_signup(tortuga_governance: &signer, value: bool)
Implementation
public entry fun set_allow_self_signup(
    tortuga_governance: &signer,
    value: bool
) acquires Status {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    let signup_status = borrow_global_mut<Status>(@tortuga_governance);
    signup_status.allow_self_signup = value;
}

Function set_withdraw_signature

Set the WithdrawSigner for the protocol to the validator managed_pool_address such that it expires when managed_pool_address unlocks or if managed_pool_address is unassociated with the protocol.

It allows permissionless unstaking while still maintaining liquidity.

Restrictions

Abort conditions

  • If managed_pool_address is not associated with the protocol.

public fun set_withdraw_signature(tortuga_governance: &signer, managed_pool_address: address)
Implementation
public entry fun set_withdraw_signature(
    tortuga_governance: &signer,
    managed_pool_address: address,
) acquires WithdrawSignature, DelegationAccounts, Status {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    assert!(
        is_an_associated_validator(managed_pool_address),
        error::not_found(EVALIDATOR_NOT_FOUND)
    );

    let stake_pool_address = delegation_state::get_stake_pool_address(
        managed_pool_address
    );

    let unlocking_at = stake::get_lockup_secs(stake_pool_address);

    let withdraw_signature = borrow_global_mut<WithdrawSignature>(
        @tortuga_governance
    );

    withdraw_signature.managed_pool_address = managed_pool_address;
    withdraw_signature.unlocking_at = unlocking_at;

    event::emit_event<WithdrawSignatureSetEvent>(
        &mut borrow_global_mut<Status>(@tortuga_governance)
            .withdraw_signature_set_events,
        WithdrawSignatureSetEvent {
            managed_pool_address,
            unlocking_at,
        }
    );
}

Function reset_validator_states_config

Update the configs of the oracle::validator_state module.

Restrictions

public fun reset_validator_states_config(tortuga_governance: &signer, initial_time_averaged_effective_reward_rate: u128, min_span_between_observations_sec: u64, max_number_of_observations: u64, initial_delegation_target: u64, rate_normalizer: u128, time_normalizer: u128, max_time_averaged_effective_reward_rate: u128, allow_permissionless_scoring: bool, ramp_up_duration: u64)
Implementation
public entry fun reset_validator_states_config(
    tortuga_governance: &signer,
    initial_time_averaged_effective_reward_rate: u128,
    min_span_between_observations_sec: u64,
    max_number_of_observations: u64,
    initial_delegation_target: u64,
    rate_normalizer: u128,
    time_normalizer: u128,
    max_time_averaged_effective_reward_rate: u128,
    allow_permissionless_scoring: bool,
    ramp_up_duration: u64,
) acquires Status {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    validator_states::reset_config(
        &borrow_global<Status>(
            @tortuga_governance
        ).validate_states_update_cap,
        initial_time_averaged_effective_reward_rate,
        min_span_between_observations_sec,
        max_number_of_observations,
        initial_delegation_target,
        rate_normalizer,
        time_normalizer,
        max_time_averaged_effective_reward_rate,
        allow_permissionless_scoring,
        ramp_up_duration,
    );
}

Function set_validator_state_permissioned_score_for_validator

Set the score for validator at managed_pool_address to value. It would not affect target_delegations for the validator unless oracle::validator_states::StatsConfig.allow_permissionless_scoring is set to false.

See oracle::validator_states module for details of the scoring mechanisms.

Restrictions

public fun set_validator_state_permissioned_score_for_validator(tortuga_governance: &signer, managed_pool_address: address, value: u128)
Implementation
public entry fun set_validator_state_permissioned_score_for_validator(
    tortuga_governance: &signer,
    managed_pool_address: address,
    value: u128
) acquires Status {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    validator_states::set_permissioned_score(
        &borrow_global<Status>(
            @tortuga_governance
        ).validate_states_update_cap,
        managed_pool_address,
        value
    );
}

Function vet_a_validator

Vets a validator for becoming associated with the protocol.

If Status.allow_self_signup is set to false, then a validator can only join after it has been vetted.

Restrictions

Abort conditions

public fun vet_a_validator(tortuga_governance: &signer, potential_managed_pool_owner: address)
Implementation
public entry fun vet_a_validator(
    tortuga_governance: &signer,
    potential_managed_pool_owner: address,
) acquires Status {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    assert_initialized();
    let signup_status = borrow_global_mut<Status>(@tortuga_governance);
    let vetted_validators = &mut signup_status.vetted_validators;
    assert!(
        !simple_map::contains_key(
            vetted_validators,
            &potential_managed_pool_owner
        ),
        error::already_exists(ECONTRACT_ALREADY_ISSUED)
    );

    simple_map::add(
        vetted_validators,
        potential_managed_pool_owner,
        ValidatorContract {}
    );
}

Function validator_begin_leaving_force

Forcibly remove a validator from the protocol.

The protocol may have to remove non-owner delegators via delegation::delegation_service module after calling this function, and before calling validator_offboard_force.

The validator can then claim owner_cap via claim_stake_pool_owner_cap().

public fun validator_begin_leaving_force(tortuga_governance: &signer, managed_pool_address: address)
Implementation
public entry fun validator_begin_leaving_force(
    tortuga_governance: &signer,
    managed_pool_address: address
) acquires DelegationAccounts {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);
    validator_begin_leaving_protocol_internal(managed_pool_address);
}

Function validator_offboard_force

This function is called to forcible remove the validator at managed_pool_address. This can only be called after the validator has begin leaving, i.e., the ManagedStakePool is in self-destruct state.

This function call is permissionless.

Abort conditions

  • If the validator has non-zero balance.

public fun validator_offboard_force(managed_pool_address: address)
Implementation
public entry fun validator_offboard_force(
    managed_pool_address: address,
) acquires DelegationAccounts, Status  {
    let stake_pool_owner_cap =
        validator_offboard_internal(managed_pool_address);

    let status = borrow_global_mut<Status>(@tortuga_governance);
    iterable_table_custom::add(
        &mut status.unclaimed_stake_pool_owner_caps,
        managed_pool_address,
        stake_pool_owner_cap
    );
}

Function withdraw_to_reserve

Request withdrawal of the amount in Payout for the validator who is the owner of managed_pool_address.

The function call is permissionless.

Abort conditions

  • If the validator is not in the protocol.

public fun withdraw_to_reserve(managed_pool_address: address)
Implementation
public entry fun withdraw_to_reserve(
    managed_pool_address: address
) acquires AptosCoinReserve, DelegationAccounts, Status {
    assert!(
        is_an_associated_validator(managed_pool_address),
        error::not_found(EVALIDATOR_NOT_FOUND)
    );

    if (!is_withdrawable_amount_positive(managed_pool_address)) {
        return
    };
    // Before modifying the state we pay commission to the validator,
    // so that our calculation is as fresh as possible.
    pay_commission_to_validator_internal(managed_pool_address);

    // Try to withdraw coins
    delegation_service::trigger_payout_dispersal(managed_pool_address);

    // Extract the coins
    let delegation_account_signer = get_delegation_account_signer(
        managed_pool_address
    );
    let extracted_coins = delegation_state::extract_payout(
        &delegation_account_signer
    );
    let withdrawn_amount = coin::value<AptosCoin>(&extracted_coins);

    // send the money to the reserve
    deposit_coins_to_reserve(extracted_coins);

    // modify the state of the validator
    update_validator_states(managed_pool_address, 0, withdrawn_amount, 0);

    let delegation_accounts = borrow_global_mut<DelegationAccounts>(
        @tortuga_governance
    );
    event::emit_event<WithdrawToReserveEvent>(
        &mut delegation_accounts.withdraw_to_reserve_events,
        WithdrawToReserveEvent {
            managed_pool_address,
            amount: withdrawn_amount,
        }
    );
}

Function pay_commission_to_the_validator

Pays commission to the validator at managed_pool_address.

Validator can call this function to make sure that their commission is compounded.

This function is permissionless.

Abort conditions

  • If the validator at address managed_pool_address is not an associated validator.

public fun pay_commission_to_the_validator(managed_pool_address: address)
Implementation
public entry fun pay_commission_to_the_validator(
    managed_pool_address: address
) acquires DelegationAccounts, Status {
    assert!(
        is_an_associated_validator(managed_pool_address),
        error::not_found(EVALIDATOR_NOT_FOUND)
    );

    // This assert keeps a validator's score to dip if the balance with them
    // is all inactive.
    // The validator's can still get their commission by calling
    // `withdraw_to_reserve`, if this assert fails.

    assert_no_withdrawable_amount(managed_pool_address);
    let owner_commissions = pay_commission_to_validator_internal(
        managed_pool_address
    );

    // modify states
    update_validator_states(managed_pool_address, 0, 0, 0);

    let delegation_accounts = borrow_global_mut<DelegationAccounts>(
        @tortuga_governance
    );
    event::emit_event<PayCommissionEvent>(
        &mut delegation_accounts.pay_commission_events,
        PayCommissionEvent {
            managed_pool_address,
            commission: owner_commissions
        }
    );
}

Function validator_signup

A validator can sign up to become an associated validator. max_commission is the commission that validator charges outside delegators. protocol_commission is the commission that validator charges the protocol. commission_recipient_address is the address where the APT generated by the validator are sent.

Both commissions are provided with 6 decimal precision, i.e., 1000000 is 100%.

Abort conditions

  • If the validator is already in the protocol.

  • If the validator has not claimed their owner_cap after leaving the protocol previously.

  • If the validator's state is not ACTIVE.

  • If self-signup is not allowed.

  • If the maximum number of validators has already been reached.

public fun validator_signup(pool_owner: &signer, commission_recipient_address: address, max_commission: u64, protocol_commission: u64)
Implementation
public entry fun validator_signup(
    pool_owner: &signer,
    commission_recipient_address: address,
    max_commission: u64,
    protocol_commission: u64
) acquires Status, DelegationAccounts {
    assert_initialized();
    let new_pool_owner_address = signer::address_of(pool_owner);
    let status = borrow_global_mut<Status>(@tortuga_governance);

    assert!(
        !delegation_account_exists(new_pool_owner_address),
        error::already_exists(EALREADY_AN_ASSOCIATED_VALIDATOR)
    );
    assert!(
        !iterable_table_custom::contains(
            &status.unclaimed_stake_pool_owner_caps,
            new_pool_owner_address
        ),
        error::unauthenticated(ECANNOT_REJOIN_WITH_UNCLAIMED_OWNER_CAP),
    );
    assert!(
        max_commission <= status.max_max_commission,
        error::invalid_argument(EMAX_COMMISSION_TOO_HIGH),
    );

    let current_number_of_validators = get_current_number_of_validators();
    // check if the pool owner has special permissions
    let vetted_validators = &mut status.vetted_validators;

    if (
        simple_map::contains_key(
            vetted_validators,
            &new_pool_owner_address
        )
    ) {
        let (_k, ValidatorContract {} ) =
            simple_map::remove<address, ValidatorContract>(
                vetted_validators,
                &new_pool_owner_address
            );
    }
    else {
        assert!(
            current_number_of_validators < status.max_number_of_validators,
            error::invalid_state(ETOO_MANY_VALIDATORS)
        );
        assert!(
            status.allow_self_signup,
            error::unauthenticated(ESELF_SIGNUP_NOT_ALLOWED)
        );
    };

    let delegation_accounts = borrow_global_mut<DelegationAccounts>(
        @tortuga_governance
    );
    let (
        delegation_signer,
        delegation_signer_cap
    ) = create_delegation_account(pool_owner);

    let manage_cap = delegation_service::initialize(
        pool_owner,
        max_commission,
        commission_recipient_address,
        signer::address_of(&delegation_signer),
        protocol_commission,
    );

    iterable_table_custom::add(
        &mut delegation_accounts.accounts,
        new_pool_owner_address,
        DelegationAccount {
            managed_pool_address: new_pool_owner_address,
            signer_cap: delegation_signer_cap,
            delegation_service_manage_cap: manage_cap,
        },
    );

    let stake_pool_address =
        delegation_state::get_stake_pool_address(new_pool_owner_address);

    assert!(
        stake::get_validator_state(stake_pool_address) ==
            VALIDATOR_STATUS_ACTIVE,
        error::invalid_state(EINACTIVE_VALIDATOR)
    );

    validator_states::validator_signup_internal(
        new_pool_owner_address,
        &status.validate_states_update_cap,
    );
    event::emit_event<ValidatorSignupEvent>(
        &mut status.validator_signup_events,
        ValidatorSignupEvent {
            managed_pool_address: new_pool_owner_address,
        }
    );
}

Function validator_begin_leaving

A validator calls this to begin off-boarding and get their stake_pool back. This initiates self-destruct of the ManagedStakePool. A validator may have to remove non-owner delegators via delegation::delegation_service module after calling this function and before calling validator_offboard().

public fun validator_begin_leaving(pool_owner: &signer)
Implementation
public entry fun validator_begin_leaving(
    pool_owner: &signer
) acquires DelegationAccounts {
    validator_begin_leaving_protocol_internal(
        signer::address_of(pool_owner)
    );
}

Function claim_stake_pool_owner_cap

Validators can claim their OwnerCapability if they have been removed by the protocol (forced-offboarding).

Abort conditions

  • If the validator is not in the protocol.

public fun claim_stake_pool_owner_cap(old_pool_owner: &signer)
Implementation
public entry fun claim_stake_pool_owner_cap(
    old_pool_owner: &signer
) acquires Status {
    let status = borrow_global_mut<Status>(@tortuga_governance);
    let old_pool_owner_address = signer::address_of(old_pool_owner);
    assert!(
        iterable_table_custom::contains(
            &status.unclaimed_stake_pool_owner_caps,
            old_pool_owner_address
        ),
        error::unauthenticated(ENO_OWNER_CAP_TO_CLAIM)
    );

    let stake_pool_owner_cap = iterable_table_custom::remove(
        &mut status.unclaimed_stake_pool_owner_caps,
        old_pool_owner_address,
    );
    stake::deposit_owner_cap(old_pool_owner, stake_pool_owner_cap);
}

Function validator_offboard

A validator can call this to retrieve owner_cap. It must be called after the validator has called validator_begins_leaving(), and after removing all the non-owner delegators from the ManagedStakePool.

public fun validator_offboard(pool_owner: &signer)
Implementation
public entry fun validator_offboard(
    pool_owner: &signer
) acquires DelegationAccounts, Status {
    let stake_pool_owner_cap = validator_offboard_internal(
        signer::address_of(pool_owner)
    );
    stake::deposit_owner_cap(pool_owner, stake_pool_owner_cap);
}

Function get_delegation_account_address

Get the delegation account that protocol uses for an associated validator at managed_pool_address.

public(friend) fun get_delegation_account_address(managed_pool_address: address): address
Implementation
public(friend) fun get_delegation_account_address(
    managed_pool_address: address
): address acquires DelegationAccounts {
    signer::address_of(&get_delegation_account_signer(managed_pool_address))
}

Function delegate

Delegates to a validator at managed_pool_address with amount APT. We allow delegations even if there is nonzero unlocking balance for simplicity.

Abort conditions

  • If there is any withdrawable amount.

  • If the validator is not in the protocol.

  • If the validator is in self-destruct state.

  • If the validator is not ACTIVE.

public(friend) fun delegate(managed_pool_address: address, amount: u64)
Implementation
public(friend) fun delegate(
     managed_pool_address: address,
     amount: u64,
 ) acquires DelegationAccounts, AptosCoinReserve, Status {
     assert!(
         is_an_associated_validator(managed_pool_address),
         error::not_found(EVALIDATOR_NOT_FOUND)
     );

     // We must withdraw first, if there is a withdrawable balance
     assert_no_withdrawable_amount(managed_pool_address);

     // If the validator is in self-destruct the delegation will fail
     // automatically, so we do not check for that condition here.

     let stake_pool_address = delegation_state::get_stake_pool_address(
         managed_pool_address
     );
     assert!(
         stake::get_validator_state(stake_pool_address) ==
             VALIDATOR_STATUS_ACTIVE,
         error::invalid_state(EINACTIVE_VALIDATOR)
     );

     // Need to move coins to the delegation account to delegate
     let coins_to_delegate = extract_coins_from_reserve(amount);
     let delegation_account_signer_ref = &get_delegation_account_signer(
         managed_pool_address
     );
     let delegation_account_address = signer::address_of(
         delegation_account_signer_ref
     );
     coin::deposit<AptosCoin>(delegation_account_address, coins_to_delegate);

     delegation_service::delegate(
         delegation_account_signer_ref,
         managed_pool_address,
         amount,
     );

     // modify states and stats
     update_validator_states(managed_pool_address, amount, 0, 0);
 }

Function reserve_for_payout

Reserve amount to unlock from the validator at managed_pool_address at its next unlock.

Abort conditions

  • If the validator is not in the protocol.

  • If the validator's unlock period is too far. See WithdrawSignature.

public(friend) fun reserve_for_payout(managed_pool_address: address, amount: u64)
Implementation
public(friend) fun reserve_for_payout(
    managed_pool_address: address,
    amount: u64
) acquires DelegationAccounts, WithdrawSignature, Status {
    if (amount == 0) {
        return
    };
    assert!(
        is_an_associated_validator(managed_pool_address),
        error::not_found(EVALIDATOR_NOT_FOUND)
    );

    assert!(
        verify_withdraw_signature_for_validator(managed_pool_address),
        error::invalid_state(EVALIDATOR_UNLOCK_IS_TOO_FAR)
    );

    // We must withdraw first, if there is a withdrawable balance
    assert_no_withdrawable_amount(managed_pool_address);

    // We first pay commission to the owner,
    // so that our balance and value to shares is calculated correctly
    pay_commission_to_validator_internal(managed_pool_address);

    let delegation_account_address =
        get_delegation_account_address(managed_pool_address);

    let num_shares_to_reserve =
        delegation_state::get_num_shares_to_reserve_rounded_up(
            managed_pool_address,
            delegation_account_address,
            amount,
        );

    // reserve shares for payout
    delegation_service::reserve_shares_for_withdraw(
        &get_delegation_account_signer(managed_pool_address),
        managed_pool_address,
        num_shares_to_reserve,
    );

    // modify the states
    update_validator_states(managed_pool_address, 0, 0, amount);
}

Function extract_coins_from_reserve

Extract amount APT from the AptosCoinReserve of the protocol.

public(friend) fun extract_coins_from_reserve(amount: u64): coin::Coin<aptos_coin::AptosCoin>
Implementation
public(friend) fun extract_coins_from_reserve(
    amount: u64
): coin::Coin<AptosCoin> acquires AptosCoinReserve {
    let aptos_coin_reserve = borrow_global_mut<AptosCoinReserve>(
        @tortuga_governance
    );
    return coin::extract<AptosCoin>(&mut aptos_coin_reserve.coin, amount)
}

Function deposit_coins_to_reserve

Deposit amount APT to the AptosCoinReserve of the protocol.

public(friend) fun deposit_coins_to_reserve(coins_to_deposit: coin::Coin<aptos_coin::AptosCoin>)
Implementation
public(friend) fun deposit_coins_to_reserve(
    coins_to_deposit: coin::Coin<AptosCoin>
) acquires AptosCoinReserve {
    let aptos_coin_reserve = borrow_global_mut<AptosCoinReserve>(
        @tortuga_governance
    );
    coin::merge<AptosCoin>(&mut aptos_coin_reserve.coin, coins_to_deposit);
}

Function assert_initialized

Assert that the protocol has a DelegationAccounts resource.

Abort conditions

fun assert_initialized()
Implementation
fun assert_initialized() {
    assert!(
        exists<DelegationAccounts>(@tortuga_governance),
        error::invalid_state(ENOT_INITIALIZED)
    );
}

Function update_validator_states

Update the oracle in module oracle::validator_state for the validator at managed_pool_address.

fun update_validator_states(managed_pool_address: address, amount_in: u64, amount_out: u64, amount_reserved: u64)
Implementation
fun update_validator_states(
    managed_pool_address: address,
    amount_in: u64,
    amount_out: u64,
    amount_reserved: u64,
) acquires Status, DelegationAccounts {

    let (
        current_balance_reserved,
        current_balance_unreserved,
    ) = get_current_validator_balances(
        managed_pool_address,
    );

    let twar_multiplier =
        delegation_state::get_protocol_commission_change_multiplier(
            managed_pool_address,
        );

    let shift_amount = get_shift_amount(managed_pool_address);

    validator_states::update_validator_and_total_internal(
        managed_pool_address,
        &borrow_global<Status>(
            @tortuga_governance
        ).validate_states_update_cap,
        current_balance_reserved,
        current_balance_unreserved,
        amount_in,
        amount_out,
        amount_reserved,
        twar_multiplier,
        shift_amount,
    );
}

Function pay_commission_to_validator_internal

Pay commission to the owner of the validator at managed_pool_address, and return the amount in APT.

fun pay_commission_to_validator_internal(managed_pool_address: address): u64
Implementation
fun pay_commission_to_validator_internal(
    managed_pool_address: address
): u64 acquires DelegationAccounts {
    let delegation_accounts = borrow_global<DelegationAccounts>(
        @tortuga_governance
    );
    let account = iterable_table_custom::borrow<address, DelegationAccount>(
        &delegation_accounts.accounts,
        managed_pool_address,
    );

    delegation_service::pay_commission_to_owner(
        &account.delegation_service_manage_cap,
    )
}

Function get_current_validator_balances

Returns the current balances in reserved and unreserved pool, in APT and number of shares for the protocol in the validator at managed_pool_address.

Restrictions

  • There should be no pending commissions to be paid out by managed_pool_address.

fun get_current_validator_balances(managed_pool_address: address): (u64, u64)
Implementation
fun get_current_validator_balances(
    managed_pool_address: address,
): (u64, u64) acquires DelegationAccounts {
    let delegation_account_address = get_delegation_account_address(
        managed_pool_address
    );

    let current_balance_reserved = (
        delegation_state::get_balance_reserved(
            managed_pool_address,
            delegation_account_address
        ) +
            delegation_state::get_protocol_payout_value(
                managed_pool_address
            )
    );

    let current_balance_unreserved =
        delegation_state::get_balance_unreserved(
            managed_pool_address,
            delegation_account_address
        );

    (
        current_balance_reserved,
        current_balance_unreserved,
    )
}

Function verify_withdraw_signature_for_validator

Returns true if the managed_pool_address can be withdrawn from. If the WithdrawSignature has not been set, the validator in it is no longer associated with the protocol, or it has expired, returns true. This allows the withdrawals to go through even if the signatures are not updated on time. Otherwise, it returns true if the lockup period for the validator at the address managed_pool_address is earlier than WithdrawSignature.unlock_at.

Abort conditions

  • If the managed_pool_address is not associated with a validator.

fun verify_withdraw_signature_for_validator(managed_pool_address: address): bool
Implementation
fun verify_withdraw_signature_for_validator(
    managed_pool_address: address
): bool acquires WithdrawSignature, DelegationAccounts {
    assert!(
        is_an_associated_validator(managed_pool_address),
        error::not_found(EVALIDATOR_NOT_FOUND)
    );

    let withdraw_signature = borrow_global<WithdrawSignature>(
        @tortuga_governance
    );
    let signature_managed_pool_address =
        withdraw_signature.managed_pool_address;

    // If the withdraw signature is no longer a valid validator in our set,
    // then the signature is expired.
    if (!is_an_associated_validator(signature_managed_pool_address)) {
        return true
    };

    let signature_stake_pool_address =
        delegation_state::get_stake_pool_address(
            signature_managed_pool_address
        );

    if (
        stake::get_lockup_secs(
            signature_stake_pool_address
        ) != withdraw_signature.unlocking_at
    ) {
        // signature has expired
        return true
    };

    let stake_pool_address = delegation_state::get_stake_pool_address(
        managed_pool_address
    );
    let unlocking_at = stake::get_lockup_secs(stake_pool_address);

    return withdraw_signature.unlocking_at >= unlocking_at
}

Function validator_begin_leaving_protocol_internal

Starts the process of the validator leaving the protocol. This function will self-destruct the ManagedStakePool which the validator had created before joining the protocol.

Abort conditions

  • If the validator is not associated with the protocol.

fun validator_begin_leaving_protocol_internal(managed_pool_address: address)
Implementation
fun validator_begin_leaving_protocol_internal(
    managed_pool_address: address
) acquires DelegationAccounts {
    assert!(
        is_an_associated_validator(managed_pool_address),
        error::not_found(EVALIDATOR_NOT_FOUND)
    );
    let delegation_accounts = borrow_global<DelegationAccounts>(
        @tortuga_governance
    );
    let account = iterable_table_custom::borrow<address, DelegationAccount>(
        &delegation_accounts.accounts,
        managed_pool_address,
    );
    delegation_service::begin_self_destruct(
        &account.delegation_service_manage_cap
    );
}

Function validator_offboard_internal

Function to offboard an associated validator at address managed_pool_address from the protocol after the reserved and unreserved balance with the protocol is zero.

Returns the OwnerCapability after a successful off-boarding.

Abort conditions

  • If the validator is not associated with the protocol.

  • If the validator has a non-zero balance of the protocol.

fun validator_offboard_internal(managed_pool_address: address): stake::OwnerCapability
Implementation
fun validator_offboard_internal(
    managed_pool_address: address
): OwnerCapability acquires DelegationAccounts, Status {
    let status = borrow_global_mut<Status>(@tortuga_governance);

    assert!(
        is_an_associated_validator(managed_pool_address),
        error::unauthenticated(EVALIDATOR_NOT_FOUND)
    );
    validator_states::validator_removal_internal(
        managed_pool_address,
        &status.validate_states_update_cap,
    );

    let delegation_accounts = borrow_global_mut<DelegationAccounts>(
        @tortuga_governance
    );

    let DelegationAccount {
        managed_pool_address,
        signer_cap: _sc,
        delegation_service_manage_cap: manage_cap,
    } = iterable_table_custom::remove(
        &mut delegation_accounts.accounts,
        managed_pool_address
    );

    let stake_pool_owner_cap =
        delegation_service::finish_self_destruct(manage_cap);

    event::emit_event<ValidatorOffboardEvent>(
        &mut status.validator_offboard_events,
        ValidatorOffboardEvent {
            managed_pool_address,
        }
    );

    stake_pool_owner_cap
}

Function create_delegation_account

Creates a new resource account for the pool_owner, register it to receive AptosCoin and returns the resource_signer and the SignerCapability for the resource_signer.

It uses timestamp::now_seconds() as the seed for the resource account.

fun create_delegation_account(pool_owner: &signer): (signer, account::SignerCapability)
Implementation
fun create_delegation_account(
    pool_owner: &signer
): (signer, SignerCapability) {
    let seed: vector<u8> = bcs::to_bytes(&timestamp::now_seconds());
    let (
        resource_signer,
        resource_signer_cap
    ) = account::create_resource_account(pool_owner, seed);

    if (
        !coin::is_account_registered<AptosCoin>(
            signer::address_of(&resource_signer)
        )
    ) {
        coin::register<AptosCoin>(&resource_signer);
    };

    (resource_signer, resource_signer_cap)
}

Function get_delegation_account_signer

Returns a signer for the validator at managed_pool_address.

fun get_delegation_account_signer(managed_pool_address: address): signer
Implementation
fun get_delegation_account_signer(
    managed_pool_address: address
): signer acquires DelegationAccounts {
    let accounts_ref = &borrow_global<DelegationAccounts>(
        @tortuga_governance
    ).accounts;
    let account = iterable_table_custom::borrow(
        accounts_ref,
        managed_pool_address,
    );

    account::create_signer_with_capability(&account.signer_cap)
}

Function delegation_account_exists

Returns true if the managed_pool_address has a DelegationAccount from the Tortuga protocol.

fun delegation_account_exists(managed_pool_address: address): bool
Implementation
fun delegation_account_exists(
    managed_pool_address: address
): bool acquires DelegationAccounts {
    let delegation_accounts = borrow_global<DelegationAccounts>(
        @tortuga_governance
    );
    iterable_table_custom::contains(
        &delegation_accounts.accounts,
        managed_pool_address
    )
}

Function get_shift_amount

Get shift amount. At genesis this will be set to 0.

fun get_shift_amount(managed_pool_address: address): u64
Implementation
fun get_shift_amount(managed_pool_address: address): u64 acquires DelegationAccounts {
    assert!(
        is_an_associated_validator(managed_pool_address),
        error::not_found(EVALIDATOR_NOT_FOUND)
    );

    0
}

Last updated