Links

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,