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;
This struct is used to allow permissioned validator signup.
struct ValidatorContract has store, key
This struct stores validator signup details.
struct Status has key
vetted_validators:
simple_map::SimpleMap
<
address
,
validator_router::ValidatorContract
>
Vetted validators, if self-signup is disabledallow_self_signup: bool
Whether to allow self-signup for validators.max_number_of_validators: u64
Maximum number of validator.max_max_commission: u64
Maximum 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::UpdateCapability
Stores 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
.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
andmanaged_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
This reserve store coins which can be either delegated to validators, or withdrawn from them to clear
Ticket
s in stake_router
module.struct AptosCoinReserve has key
These resource accounts delegate to the individual validators
struct DelegationAccounts has store, key
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
.Delegation account for a validator, stores the address and capabilities.
struct DelegationAccount has store
managed_pool_address:
address
Address where the ManagedStakePool
for the validator is stored.signer_cap:
account::SignerCapability
The SignerCapability
for the resource account that delegates to the validator.delegation_service_manage_cap:
delegation_service::ManageCapability
The ManageCapability
for the validator's ManagedStakePool
.Event generated when a validator successfully signs up.
struct ValidatorSignupEvent has drop, store
Event generated when a validator is off-boarded.
struct ValidatorOffboardEvent has drop, store
Event generated when a validator is paid commission.
struct PayCommissionEvent has drop, store
Event generated when a validator withdraws to the reserve.
struct WithdrawToReserveEvent has drop, store
struct WithdrawSignatureSetEvent has drop, store
Validator state
active
.const VALIDATOR_STATUS_ACTIVE: u64 = 2;
Validator state
inactive
.const VALIDATOR_STATUS_INACTIVE: u64 = 4;
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;
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;
const EVALIDATOR_UNLOCK_IS_TOO_FAR: u64 = 14;
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)
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
});
}
Ensures that the protocol does not have any currently withdrawable amount in the validator at
managed_pool_address
.- If
is_withdrawable_amount_nonzero()
returnstrue
.
public fun assert_no_withdrawable_amount(managed_pool_address: address)
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
public fun get_reserve_balance(): u64
Returns the number of associated validators.
public fun get_current_number_of_validators(): u64
Returns the current maximum number of validators.
public fun get_max_number_of_validators(): u64
Returns
true
if self-signup is allowed, false
otherwise.public fun get_allow_self_signup(): bool
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
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
}
}
Returns if the validator at
managed_pool_address
is associated with the protocol.public fun is_an_associated_validator(managed_pool_address: address): bool
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
)
}
Set the maximum number of validators that can be associated with the protocol to
value
.public fun set_max_number_of_validators(tortuga_governance: &signer, value: u64)
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;
}
Set the maximum max_commission for validators to
value
This does not affect existing validators.public fun set_max_max_commission(tortuga_governance: &signer, value: u64)
Set the
allow_self_signup
flag.public fun set_allow_self_signup(tortuga_governance: &signer, value: bool)
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.
- If
managed_pool_address
is not associated with the protocol.
public fun set_withdraw_signature(tortuga_governance: &signer, managed_pool_address: address)
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,
}
);
}
Update the configs of the
oracle::validator_state
module.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)
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,
);
}
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.public fun set_validator_state_permissioned_score_for_validator(tortuga_governance: &signer, managed_pool_address: address, value: u128)
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
);
}
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.public fun vet_a_validator(tortuga_governance: &signer, potential_managed_pool_owner: address)
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 {}
);
}
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
.public fun validator_begin_leaving_force(tortuga_governance: &signer, managed_pool_address: address)
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.
- If the validator has non-zero balance.
public fun validator_offboard_force(managed_pool_address: address)
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
);
}
Request withdrawal of the amount in
Payout
for the validator who is the owner of managed_pool_address
.The function call is permissionless.
- If the validator is not in the protocol.
public fun withdraw_to_reserve(managed_pool_address: address)