oracle::validator_states

Module 0xc0ded0c0::validator_states

This module updates the state (and stats) for a validator when the protocol interacts with the validator viz.:

  • creates a new delegation,

  • pays commission,

  • requests a withdrawal, or,

  • when it reserves shares for payout.

This module also calculates the performance statistics of a validator. These statistics are used to calculate how much to delegate in a validator. Tortuga Protocol may start out with permissioned scoring and switch to permissionless scoring after sufficient data has been collected. Hence, this module may remain upgradable for longer than the rest of the protocol while the permissionless scoring is being fine-tuned.

This modules handles both permissioned and permissionless scoring.

use 0x1::error;
use 0x1::option;
use 0x1::reconfiguration;
use 0x1::signer;
use 0x1::timestamp;
use 0xc0ded0c0::circular_buffer;
use 0xc0ded0c0::iterable_table_custom;
use 0xc0ded0c0::math;

Struct Observation

An observation of on online stats oracle. Effective reward rate for a validator is calculated after they have charged their commission, and after accounting for their performance degradations, like missed proposals.

struct Observation has copy, drop, store
Fields

timestamp: u64The time the observation was made.effective_reward_rate: u128The effective reward rate of the validator at the time of the observation.

Struct AssociatedValidator

This struct stores the balances and stats for an associated validator.

struct AssociatedValidator has store
Fields

last_update_timestamp: u64Timestamp when the last checkpoint was made.last_reconfiguration_timestamp: u64The last recorded reconfiguration timestamp from the aptos_framework module.balance_at_last_update_unreserved: u64Unreserved balance when the last update was made.balance_at_last_update_reserved: u64Reserved balance when the last update was made.time_averaged_effective_reward_rate: u128The time-averaged effective reward rate of the validator.observations: circular_buffer::CircularBuffer<validator_states::Observation>Observations which will be used to calculate the effective reward.observation_begin_timestamp: u64The earliest timestamp when an observation was made in the current buffer.score: u128Score of the validatorpermissioned_score: u128Permissioned score of the validator. It is used only when allow_permissionless_scoring is set to false.ramp_up_start_at: u64Scoring may in future be ramped up starting from this timestamp.

Resource ValidatorSystem

This struct stores the aggregate values across all validators associated with the protocol.

struct ValidatorSystem has key
Fields

total_balance_with_validators: u64Total amount in APT in stakes with validatorstotal_unlocking_balance: u64Total balance which has been reserved for payoutsassociated_validators: iterable_table_custom::IterableTableCustom<address, validator_states::AssociatedValidator>Table of all associated validatorstotal_score: u128Sum of scores of all the validators

Resource StatsConfig

This stores the on-chain oracle's config information. Careful choices for these values will be needed at genesis.

struct StatsConfig has key
Fields

initial_time_averaged_effective_reward_rate: u128Initial time averaged effective reward rate to assign to a new validator.min_span_between_observations_sec: u64Minimum time difference between reward rate checks.max_number_of_observations: u64Max number of observations to keep track of.initial_delegation_target: u64The initial amount of delegate to give to a validator. A nonzero value ensure that stats start to populate. *Note*: Should be around 10 APT.rate_normalizer: u128The normalizer for the rate of yield on the chain. If the chain reward rate is going to be ~ 10% per annum, then effective_reward_rate would be about 0.000011 per hour, or if we check validators every day and use the following normalize of 10^6, reward rate would be at most a four digit number.time_normalizer: u128The constant used to normalize the time. We will normalize observations to about 5 hours: 5 * 3600 = 18000.max_time_averaged_effective_reward_rate: u128Maximum possible time averaged effective reward rate. this should equal:

reward_rate * rate_normalizer * time_normalizer /
epoch_length

allow_permissionless_scoring: boolramp_up_duration: u64

Struct UpdateCapability

This struct is generated when someone initializes this module. All the resources above are stored in the address specified here.

struct UpdateCapability has store
Fields

state_storage_address: addressThe address where the validator state will be stored.

Constants

When the pool already exists on an address.

const EALREADY_EXISTS: u64 = 4;

The precision of the performance score.

const CLIFF_PRECISION: u128 = 10000;

When a request to reduce the number of observations the validator requires on-chain is made.

const ECANNOT_DECREASE_OBSERVATIONS: u64 = 3;

When max_time_averaged_effective_reward_rate is set to zero.

const EMAX_TWAR_ZERO: u64 = 5;

When trying to remove a validator which had non-zero balance the last time it was updated.

const ENONZERO_BALANCE_AT_LAST_UPDATE: u64 = 2;

The precision for the score multiplier.

const MULTIPLIER_PRECISION: u64 = 10000;

This constant will act as total score if permissionless scoring is disabled

const PERMISSIONED_SCORING_NORMALIZER: u128 = 10000000000;

The bottom-performing validator cut-off.

const SCORE_BUCKET_BOT: u128 = 1000000;

The mid-performing validator cut-off.

const SCORE_BUCKET_MID: u128 = 100000000;

The top-performing validator cut-off.

const SCORE_BUCKET_TOP: u128 = 10000000000;

Function initialize_validator_states

Initializes the module. Returns an UpdateCapability with the address of the sender.

Abort conditions

public fun initialize_validator_states(sender: &signer): validator_states::UpdateCapability
Implementation
public fun initialize_validator_states(sender: &signer): UpdateCapability {
    assert!(
        !exists<ValidatorSystem>(signer::address_of(sender)),
        error::already_exists(EALREADY_EXISTS)
    );

    move_to(sender, ValidatorSystem {
        total_balance_with_validators: 0,
        total_unlocking_balance: 0,
        associated_validators:
            iterable_table_custom::new<address, AssociatedValidator>(),
        total_score: 0,
    });

    // The Aptos' reward rate will be about 0.0011 per hour
    // `max_time_averaged_effective_reward_rate` should be calculated using
    // this rate at genesis.
    move_to(sender, StatsConfig {

        // Validators start with 0 score.
        initial_time_averaged_effective_reward_rate: 0,

        // 5 hours
        min_span_between_observations_sec: 5 * 3600,

        // We keep track of reward rate for 500 hours (right now we can only
        // increase this number after init)
        max_number_of_observations: 10,

        // Set the initial delegation target to 10 APT to kickoff stats
        // calculations.
        initial_delegation_target: 1000000000,

        // Larger the `*_normalizer`, better the precision of the scores.
        rate_normalizer: 1000000000000000, // large for better precision
        time_normalizer: 3600 * 5, // 5*3600 five hours ; ideal genesis value

        // TODO: May update.
        // 0.0011 * 1000000000000000 * 3600*5 / 3600
        // This must be > 0.
        max_time_averaged_effective_reward_rate: 5500000000000,

        // We start permissionless scoring sometime after the launch
        allow_permissionless_scoring: false,

        // We start with no ramp up
        ramp_up_duration: 0,
    });

    UpdateCapability {
        state_storage_address: signer::address_of(sender),
    }
}

Function get_total_unlocking_balance

Returns the ValidatorSystem struct stored at the address of state_storage_address.

public fun get_total_unlocking_balance(state_storage_address: address): u64
Implementation
public fun get_total_unlocking_balance(
    state_storage_address: address
): u64 acquires ValidatorSystem {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    validator_system.total_unlocking_balance
}

Function get_total_balance_with_validators

Returns validator_system.total_balance_with_validators stored at the address state_storage_address.

public fun get_total_balance_with_validators(state_storage_address: address): u64
Implementation
public fun get_total_balance_with_validators(
    state_storage_address: address
): u64 acquires ValidatorSystem {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    validator_system.total_balance_with_validators
}

Function get_balance_at_last_update

Returns the total balance with the managed staking pool at managed_pool_address.

public fun get_balance_at_last_update(state_storage_address: address, managed_pool_address: address): u64
Implementation
public fun get_balance_at_last_update(
    state_storage_address: address,
    managed_pool_address: address
): u64 acquires ValidatorSystem {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    let validator =
        iterable_table_custom::borrow<address, AssociatedValidator>(
            &validator_system.associated_validators,
            managed_pool_address
        );

    validator.balance_at_last_update_reserved +
        validator.balance_at_last_update_unreserved
}

Function get_target_delegation

This returns the target delegation that the managed staking pool at managed_pool_address should receive from the protocol, given the performance of the validator.

This is used in module stake_router to allow permissionless staking. If allow_permissionless_scoring is set to false, the protocol determines the score that target_delegation depends on.

Abort conditions

  • If the managed_pool_address is not an associated validator.

public fun get_target_delegation(state_storage_address: address, managed_pool_address: address, required_tvl: u64): u64
Implementation
public fun get_target_delegation(
    state_storage_address: address,
    managed_pool_address: address,
    required_tvl: u64,
): u64 acquires ValidatorSystem, StatsConfig {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    let stats_config = borrow_global<StatsConfig>(state_storage_address);
    let validator =
        iterable_table_custom::borrow<address, AssociatedValidator>(
            &validator_system.associated_validators,
            managed_pool_address
        );

    if (stats_config.allow_permissionless_scoring) {
        if (validator_system.total_score == 0) {
            stats_config.initial_delegation_target
        } else {
            (
                (
                    validator.score *
                        (required_tvl as u128) /
                        validator_system.total_score
                ) as u64
            ) + stats_config.initial_delegation_target
        }
    } else {
        (
            (
                validator.permissioned_score *
                    (required_tvl as u128) /
                    PERMISSIONED_SCORING_NORMALIZER
            ) as u64
        )
    }
}

Function get_score

Get the score of the validator at managed_pool_address.

Abort conditions

  • If the managed_pool_address is not an associated validator.

public fun get_score(state_storage_address: address, managed_pool_address: address): u128
Implementation
public fun get_score(
    state_storage_address: address,
    managed_pool_address: address
): u128 acquires ValidatorSystem {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    let validator =
        iterable_table_custom::borrow<address, AssociatedValidator>(
            &validator_system.associated_validators,
            managed_pool_address
        );
    validator.score
}

Function get_permissioned_score

Get permissioned score of managed_pool_address from ValidatorSystem stored at state_storage_address.

This score will be used if StatsConfig.allow_permissionless_scoring is false.

public fun get_permissioned_score(state_storage_address: address, managed_pool_address: address): u128
Implementation
public fun get_permissioned_score(
    state_storage_address: address,
    managed_pool_address: address
): u128 acquires ValidatorSystem {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    let validator =
        iterable_table_custom::borrow<address, AssociatedValidator>(
            &validator_system.associated_validators,
            managed_pool_address
        );
    validator.permissioned_score
}

Function get_time_averaged_effective_reward_rate

Returns the time averaged effective reward rate of the validator at managed_pool_address from the ValidatorSystem stored at the address state_storage_address.

public fun get_time_averaged_effective_reward_rate(state_storage_address: address, managed_pool_address: address): u128
Implementation
public fun get_time_averaged_effective_reward_rate(
    state_storage_address: address,
    managed_pool_address: address
): u128 acquires ValidatorSystem {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    let validator =
        iterable_table_custom::borrow<address, AssociatedValidator>(
            &validator_system.associated_validators,
            managed_pool_address
        );
    validator.time_averaged_effective_reward_rate
}

Function get_last_update_timestamp

Returns the last time the validator at managed_pool_address was updated from the ValidatorSystem stored at the address state_storage_address.

public fun get_last_update_timestamp(state_storage_address: address, managed_pool_address: address): u64
Implementation
public fun get_last_update_timestamp(
    state_storage_address: address,
    managed_pool_address: address
): u64 acquires ValidatorSystem {
    let validator_system = borrow_global<ValidatorSystem>(
        state_storage_address
    );
    let validator =
        iterable_table_custom::borrow<address, AssociatedValidator>(
            &validator_system.associated_validators,
            managed_pool_address
        );
    validator.last_update_timestamp
}

Function reset_config

This function can be used to reset the StatsConfig configuration stored at the address update_cap.state_storage_address post initialization.

StatsConfig may be reset initially at launch to fine tune parameters. Config should not be changed after it has been fine tuned for a network.

Restrictions

  • Can only be called with the UpdateCapability for the the address state_storage_address.

Abort conditions

  • If the max_number_of_observations is less than the currently set value.

public fun reset_config(update_cap: &validator_states::UpdateCapability, 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 fun reset_config(
     update_cap: &UpdateCapability,
     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 StatsConfig {
     let stats_config = borrow_global_mut<StatsConfig>(
         update_cap.state_storage_address
     );
     stats_config.initial_time_averaged_effective_reward_rate =
         initial_time_averaged_effective_reward_rate;

     stats_config.min_span_between_observations_sec =
         min_span_between_observations_sec;

     assert!(
         max_number_of_observations >=
             stats_config.max_number_of_observations,
         error::invalid_argument(ECANNOT_DECREASE_OBSERVATIONS)
     );
     stats_config.max_number_of_observations = max_number_of_observations;
     stats_config.initial_delegation_target = initial_delegation_target;
     stats_config.rate_normalizer = rate_normalizer;
     stats_config.time_normalizer = time_normalizer;
     stats_config.allow_permissionless_scoring =
         allow_permissionless_scoring;

     // This is needed for `cliff` to not abort
     assert!(
         max_time_averaged_effective_reward_rate > 0,
         error::invalid_argument(EMAX_TWAR_ZERO)
     );

     stats_config.max_time_averaged_effective_reward_rate =
         max_time_averaged_effective_reward_rate;
     stats_config.ramp_up_duration = ramp_up_duration;
 }

Function set_permissioned_score

Set score for the validator at managed_pool_address in the ValidatorSystem stored at the address update_cap.state_storage_address to value.

It would not affect target_delegations unless StatsConfig.allow_permissionless_scoring is set to false.

Restrictions

public fun set_permissioned_score(update_cap: &validator_states::UpdateCapability, managed_pool_address: address, value: u128)
Implementation
public fun set_permissioned_score(
    update_cap: &UpdateCapability,
    managed_pool_address: address,
    value: u128,
) acquires ValidatorSystem {
    let validator_system = borrow_global_mut<ValidatorSystem>(
        update_cap.state_storage_address
    );
    let validator =
        iterable_table_custom::borrow_mut<address, AssociatedValidator>(
            &mut validator_system.associated_validators,
            managed_pool_address
        );

    validator.permissioned_score = value;
}

Function validator_signup_internal

Add a new associated validator at managed_pool_address to the ValidatorSystem stored at the address update_cap.state_storage_address.

Restrictions

public fun validator_signup_internal(managed_pool_address: address, update_cap: &validator_states::UpdateCapability)
Implementation
public fun validator_signup_internal(
    managed_pool_address: address,
    update_cap: &UpdateCapability,
) acquires ValidatorSystem, StatsConfig {
    let validator_system = borrow_global_mut<ValidatorSystem>(
        update_cap.state_storage_address
    );
    let stats_config = borrow_global<StatsConfig>(
        update_cap.state_storage_address
    );
    let current_time = timestamp::now_seconds();
    // last reconfiguration time in seconds
    let reconfiguration_time =
        reconfiguration::last_reconfiguration_time() / 1000000;

    let validator = AssociatedValidator {
        last_update_timestamp: current_time,
        last_reconfiguration_timestamp: reconfiguration_time,
        balance_at_last_update_unreserved: 0,
        balance_at_last_update_reserved: 0,
        time_averaged_effective_reward_rate:
            stats_config.initial_time_averaged_effective_reward_rate,
        observations: circular_buffer::empty<Observation>(),
        observation_begin_timestamp: current_time,
        score: 0,
        permissioned_score: PERMISSIONED_SCORING_NORMALIZER,
        ramp_up_start_at: current_time,
    };

    iterable_table_custom::add<address, AssociatedValidator>(
        &mut validator_system.associated_validators,
        managed_pool_address, validator
    );
}

Function validator_removal_internal

Remove an associated validator at managed_pool_address from the ValidatorSystem stored at the address update_cap.state_storage_address.

Restrictions

Abort conditions

  • If either the reserved or unreserved balance with the validator is non-zero.

public fun validator_removal_internal(managed_pool_address: address, update_cap: &validator_states::UpdateCapability)
Implementation
public fun validator_removal_internal(
    managed_pool_address: address,
    update_cap: &UpdateCapability,
) acquires ValidatorSystem {
    let validator_system = borrow_global_mut<ValidatorSystem>(
        update_cap.state_storage_address
    );
    let validator =
        iterable_table_custom::remove<address, AssociatedValidator>(
            &mut validator_system.associated_validators,
            managed_pool_address
        );

    // The balances should be able to be reset permissionlessly via
    // `validator_router::pay_commission_to_the_validator`.
    assert!(
        validator.balance_at_last_update_unreserved == 0 &&
            validator.balance_at_last_update_reserved == 0,
        error::invalid_state(ENONZERO_BALANCE_AT_LAST_UPDATE)
    );
    burn_associated_validator(validator);
}

Function update_validator_and_total_internal

This function is called at every interaction with an associated validator, to update balances and stats. Commissions must be paid to the associated validator before calling this function.

Restrictions

public fun update_validator_and_total_internal(managed_pool_address: address, update_cap: &validator_states::UpdateCapability, current_balance_reserved: u64, current_balance_unreserved: u64, amount_in: u64, amount_out: u64, amount_reserved: u64, validator_specific_scoring_multiplier: u64, shift_amount: u64): option::Option<u128>
Implementation
public fun update_validator_and_total_internal(
    managed_pool_address: address,
    update_cap: &UpdateCapability,
    current_balance_reserved: u64,
    current_balance_unreserved: u64,
    amount_in: u64,
    amount_out: u64,
    amount_reserved: u64,
    validator_specific_scoring_multiplier: u64,
    shift_amount: u64,
): Option<u128> acquires ValidatorSystem, StatsConfig {
    // MODIFY THE BALANCES FIRST
    // only at most one of the amounts field could be nonzero:
    // 1. `amount_in > 0` if the money just went into the
    //    `managed_stake_pool` just before state modification via a delegate
    //    call,
    // 2. `amount_out > 0` if the money just went out of the
    //    `managed_stake_pool` just before state modification via a
    //    `withdraw_to_reserve` call,
    // 3. `amount_reserved > 0` if an amount was reserved for withdrawal in
    //    the `managed_stake_pool` just before state modification via a
    //    `reserve_shares_for_payout` call, or
    // 4. all three amount fields are zero, if only
    //    `pay_commission_to_owner` was called

    // We calculate current balances
    let validator_system = borrow_global_mut<ValidatorSystem>(
        update_cap.state_storage_address
    );

    let validator =
        iterable_table_custom::borrow_mut<address, AssociatedValidator>(
            &mut validator_system.associated_validators,
            managed_pool_address
        );

    let current_balance =
        current_balance_reserved + current_balance_unreserved;

    let balance_at_last_update =
        validator.balance_at_last_update_reserved +
            validator.balance_at_last_update_unreserved;

    let (
        total_rewards,
        total_negative_rewards,
        reserved_rewards,
        reserved_negative_rewards,
    ) = calculate_rewards(
        current_balance,
        balance_at_last_update,
        current_balance_reserved,
        validator.balance_at_last_update_reserved,
        amount_in,
        amount_out,
        amount_reserved,
    );

    if (amount_in > 0) {
        // we now calculate the rewards on balance which is unlocking
        // We use if else to avoid any floating point catastrophe
        validator_system.total_balance_with_validators =
            math::add_possibly_negative(
                validator_system.total_balance_with_validators + amount_in,
                total_rewards,
                total_negative_rewards
            );

        validator_system.total_unlocking_balance =
            math::add_possibly_negative(
                validator_system.total_unlocking_balance,
                reserved_rewards,
                reserved_negative_rewards
            );
    } else if (amount_out > 0) {
        // The order of `safe_sub` and `add_possibly_negative` is important
        // here.  Reversing it can result in a nonzero entry for
        // `total_balance_with_validators`, while it should actually be
        // zero.
        validator_system.total_balance_with_validators = math::safe_sub(
            math::add_possibly_negative(
                validator_system.total_balance_with_validators,
                total_rewards,
                total_negative_rewards
            ),
            amount_out
        );

        // It is important to not subtract `amount_out` as the old state
        // did not know about amount_out
        validator_system.total_unlocking_balance = math::safe_sub(
            validator_system.total_unlocking_balance,
            validator.balance_at_last_update_reserved
        );
    } else {
        validator_system.total_balance_with_validators =
            math::add_possibly_negative(
                validator_system.total_balance_with_validators,
                total_rewards,
                total_negative_rewards
            );
        validator_system.total_unlocking_balance =
            math::add_possibly_negative(
                validator_system.total_unlocking_balance + amount_reserved,
                reserved_rewards,
                reserved_negative_rewards
            );
    };

    validator.balance_at_last_update_reserved = current_balance_reserved;
    validator.balance_at_last_update_unreserved =
        current_balance_unreserved;

    // Finally, we update the validator stats
    let stats_config = borrow_global<StatsConfig>(
        update_cap.state_storage_address
    );

    let observation_option = modify_validator_score(
        stats_config,
        validator_system,
        managed_pool_address,
        balance_at_last_update,
        total_rewards,
        validator_specific_scoring_multiplier,
        shift_amount,
    );

    // Return a value
    if (option::is_none(&observation_option)) {
        option::destroy_none(observation_option);
        option::none()
    } else {
        let a = option::destroy_some(observation_option);
        option::some(a.effective_reward_rate)
    }
}

Function calculate_rewards

Returns the total_rewards, total_negative_rewards, reserved_rewards, reserved_negative_rewards given the current and previous balances and the amount of tokens that were added or removed.

fun calculate_rewards(current_balance: u64, balance_at_last_update: u64, current_balance_reserved: u64, balance_at_last_update_reserved: u64, amount_in: u64, amount_out: u64, amount_reserved: u64): (u64, u64, u64, u64)
Implementation
fun calculate_rewards(
    current_balance: u64,
    balance_at_last_update: u64,
    current_balance_reserved: u64,
    balance_at_last_update_reserved: u64,
    amount_in: u64,
    amount_out: u64,
    amount_reserved: u64,
): (u64, u64, u64, u64) {
    let total_rewards = math::safe_sub(
        current_balance + amount_out,
        balance_at_last_update + amount_in
    );
    let total_negative_rewards = math::safe_sub(
        balance_at_last_update + amount_in,
        current_balance + amount_out
    );
    let reserved_rewards = math::safe_sub(
        current_balance_reserved,
        balance_at_last_update_reserved + amount_reserved
    );
    let reserved_negative_rewards = math::safe_sub(
        balance_at_last_update_reserved + amount_reserved,
        current_balance_reserved
    );
    (
        total_rewards,
        total_negative_rewards,
        reserved_rewards,
        reserved_negative_rewards
    )
}

Function effective_reward_rate

Calculates effective reward rate based on, new rewards, balance_at_last_update and time_delta between the updates.

The stats_config is needed to determine the normalizers.

fun effective_reward_rate(stats_config: &validator_states::StatsConfig, rewards: u128, balance_at_last_update: u128, time_delta: u128): u128
Implementation
fun effective_reward_rate(
    stats_config: &StatsConfig,
    rewards: u128,
    balance_at_last_update: u128,
    time_delta: u128,
): u128 {
    // The order of operations is important to ensure highest precision.
    (rewards * stats_config.rate_normalizer / balance_at_last_update) *
        stats_config.time_normalizer / time_delta
}

Function time_averaged_effective_reward_rate

Returns the time averaged effective reward rate given the tail_sum, the new head and the total_time span the observations cover.

fun time_averaged_effective_reward_rate(tail_sum: u128, head: u128, total_time: u128): u128
Implementation
fun time_averaged_effective_reward_rate(
    tail_sum: u128,
    head: u128,
    total_time: u128,
): u128 {
    (tail_sum + head) / total_time
}

Function calc_ramp_up_multiplier

Calculate scoring multiplier for the validator.

fun calc_ramp_up_multiplier(ramp_up_duration: u64, ramp_up_start_at: u64, current_timestamp: u64): u64
Implementation
fun calc_ramp_up_multiplier(
    ramp_up_duration: u64,
    ramp_up_start_at: u64,
    current_timestamp: u64,
): u64 {
    if (
        current_timestamp >= ramp_up_start_at + ramp_up_duration
        || ramp_up_duration == 0
    ) {
        MULTIPLIER_PRECISION
    } else {
        math::mul_div(
            math::safe_sub(
                current_timestamp,
                ramp_up_start_at
            ),
            MULTIPLIER_PRECISION,
            ramp_up_duration
        )
    }
}

Function modify_validator_score

Modifies the stats for the validator at managed_pool_address given the balance_at_last_update, and rewards.

This also updates the validator_system with the new score for the validator.

fun modify_validator_score(stats_config: &validator_states::StatsConfig, validator_system: &mut validator_states::ValidatorSystem, managed_pool_address: address, balance_at_last_update: u64, rewards: u64, twar_multiplier: u64, shift_amount: u64): option::Option<validator_states::Observation>
Implementation
fun modify_validator_score(
    stats_config: &StatsConfig,
    validator_system: &mut ValidatorSystem,
    managed_pool_address: address,
    balance_at_last_update: u64,
    rewards: u64,
    twar_multiplier: u64,
    shift_amount: u64,
): Option<Observation> {
    let current_timestamp = timestamp::now_seconds();

    // Modify the stats
    let reconfiguration_timestamp =
        reconfiguration::last_reconfiguration_time() / 1000000;

    let validator =
        iterable_table_custom::borrow_mut<address, AssociatedValidator>(
            &mut validator_system.associated_validators,
            managed_pool_address
        );

    let observations_length = circular_buffer::length<Observation>(
        &validator.observations
    );
    let last_observation_timestamp = if (observations_length > 0) {
        circular_buffer::borrow_newest<Observation>(
            &validator.observations
        ).timestamp
    } else {
        0
    };

    let time_since_last_observation =
        reconfiguration_timestamp - last_observation_timestamp;

    let reconfiguration_delta =
        reconfiguration_timestamp -
            validator.last_reconfiguration_timestamp;

    if (
        reconfiguration_delta == 0 ||
        time_since_last_observation <
            stats_config.min_span_between_observations_sec ||
        balance_at_last_update == 0
    ) {
        // We don't modify stats if the balance was zero in the validator,
        // so as to not punish them for not having any balance there are no
        // rewards in this case, so we can short cut early
        validator.last_update_timestamp = current_timestamp;
        validator.last_reconfiguration_timestamp =
            reconfiguration_timestamp;

        if (
            balance_at_last_update == 0 &&
                circular_buffer::length<Observation>(
                    &validator.observations
                ) == 0
        ) {
            // validator.observation_begin_timestamp = current_timestamp;
            validator.observation_begin_timestamp =
                reconfiguration_timestamp;
        };
        return option::none<Observation>()
    };

    // calculate the time_averaged_effective_reward_rate
    let effective_reward_rate = effective_reward_rate(
        stats_config,
        (rewards as u128),
        (balance_at_last_update as u128),
        (reconfiguration_delta as u128)
    );

    let current_observation = Observation {
        timestamp: reconfiguration_timestamp,
        effective_reward_rate: effective_reward_rate,
    };

    let time_span = if (observations_length > 0) {
        (
            (
                last_observation_timestamp -
                    validator.observation_begin_timestamp
            ) as u128
        )
    } else {
        (0 as u128)
    };

    let time_delta_u128 = if (observations_length > 0) {
        ((reconfiguration_timestamp - last_observation_timestamp) as u128)
    } else {
        (
            (
                reconfiguration_timestamp -
                    validator.observation_begin_timestamp
            ) as u128
        )
    };

    let total_time = time_span + time_delta_u128;

    let head = effective_reward_rate * time_delta_u128;
    let tail_sum =
        validator.time_averaged_effective_reward_rate * time_span;

    if (observations_length < stats_config.max_number_of_observations) {
        validator.time_averaged_effective_reward_rate =
            time_averaged_effective_reward_rate(
                tail_sum,
                head,
                total_time,
            );
    } else {
        let oldest_observation =
            circular_buffer::borrow_oldest<Observation>(
                &validator.observations
            );
        let oldest_time_delta = (
            (
                oldest_observation.timestamp -
                    validator.observation_begin_timestamp
            ) as u128
        );

        validator.observation_begin_timestamp =
            oldest_observation.timestamp;

        let new_tail_sum = math::safe_sub_u128(
            tail_sum,
            oldest_observation.effective_reward_rate * oldest_time_delta
        );
        let new_total_time = total_time - oldest_time_delta;
        validator.time_averaged_effective_reward_rate =
            time_averaged_effective_reward_rate(
                new_tail_sum,
                head,
                new_total_time,
            );
    };

    // add the new observation
    circular_buffer::push<Observation>(
        &mut validator.observations,
        copy current_observation,
        stats_config.max_number_of_observations
    );

    // update scores
    let old_score = validator.score;
    let ramp_up_multiplier = calc_ramp_up_multiplier(
        stats_config.ramp_up_duration,
        validator.ramp_up_start_at,
        current_timestamp
    );
    validator.score = cliff(
        validator.time_averaged_effective_reward_rate,
        stats_config.max_time_averaged_effective_reward_rate,
        (twar_multiplier as u128),
        (ramp_up_multiplier as u128),
        (shift_amount as u128),
    );
    validator_system.total_score = math::safe_sub_u128(
        validator_system.total_score + validator.score,
        old_score
    );

    // Finally, modify timestamps
    validator.last_update_timestamp = current_timestamp;
    validator.last_reconfiguration_timestamp = reconfiguration_timestamp;

    option::some<Observation>(current_observation)
}

Function burn_associated_validator

Used to destroy the AssociatedValidator object.

fun burn_associated_validator(validator: validator_states::AssociatedValidator)
Implementation
fun burn_associated_validator(validator: AssociatedValidator) {
    let AssociatedValidator {
        last_update_timestamp: _c,
        last_reconfiguration_timestamp: _l,
        balance_at_last_update_unreserved: _d,
        balance_at_last_update_reserved: _e,
        time_averaged_effective_reward_rate: _g,
        observations: observations,
        observation_begin_timestamp: _i,
        score: _j,
        permissioned_score: _k,
        ramp_up_start_at: _m,
    } = validator;

    circular_buffer::burn_all(observations);
}

Function cliff

This function calculates the score of a validator based on the the time_averaged_effective_reward_rate (a) and the maximum possible time_averaged_effective_reward_rate on the Aptos chain (b).

As the name suggests, this function behaves like a cliff, with a steep drop as a diverges from b.

fun cliff(a: u128, b: u128, twar_multiplier: u128, ramp_up_multiplier: u128, shift_amount: u128): u128
Implementation
fun cliff(
    a: u128,
    b: u128,
    twar_multiplier: u128,
    ramp_up_multiplier: u128,
    shift_amount: u128,
): u128 {
    let multiplier_precision_u128 = (MULTIPLIER_PRECISION as u128);
    a = a * twar_multiplier / multiplier_precision_u128;
    let pct = a * CLIFF_PRECISION / b;
    let cutoff_top = 96 * CLIFF_PRECISION / 100;
    let cutoff_mid = 92 * CLIFF_PRECISION / 100;
    let cutoff_bot = 88 * CLIFF_PRECISION / 100;
    let raw_score: u128;
    if (a >= b) {
        raw_score = SCORE_BUCKET_TOP;
    } else if (pct >= cutoff_top) {
        // if a is equal to b, then the return value is SCORE_BUCKET_TOP
        // if a = 0.96 b, then the return value is SCORE_BUCKET_MID
        let den = math::max_u128(
            math::safe_sub_u128(2476 * b, 2475 * a),
            1
        );
        raw_score = b * SCORE_BUCKET_TOP / den;
    } else if (pct >= cutoff_mid) {
        // if a = 0.96 b, then the return value is SCORE_BUCKET_MID
        // if a = 0.92 b, then the return value us is SCORE_BUCKET_BOT
        let den = math::max_u128(
            math::safe_sub_u128(2377 * b, 2475 * a),
            1
        );
        raw_score = b * SCORE_BUCKET_MID / den;
    } else if (pct >= cutoff_bot) {
        // if a = 0.92 b, then the return is SCORE_BUCKET_BOT
        // if a = 0.88 b, then the return value is is SCORE_BUCKET_BOT / 100
        let den = math::max_u128(
            math::safe_sub_u128(2278 * b, 2475 * a),
            1
        );
        raw_score = b * SCORE_BUCKET_BOT / den;
    } else {
        raw_score = 0;
    };

    // We get raw score.
    // mul_div not needed here as everything is u128 anyway.
    let raw_score = ramp_up_multiplier * raw_score / multiplier_precision_u128;

    // return the score after shifting
    apply_shift(
        raw_score,
        shift_amount
    )
}

Function apply_shift

fun apply_shift(raw_score: u128, shift_amount: u128): u128
Implementation
fun apply_shift(
    raw_score: u128,
    shift_amount: u128,
): u128 {
    raw_score * (shift_amount + 1)
}

Last updated