tortuga::stake_router

Module 0xc0ded0c0::stake_router

This module is the main user facing module of the Tortuga protocol.

It handles:

  • APT -> tAPT transactions,

  • issuing of Tickets and claiming them for tAPT -> APT, and,

  • permissionless staking/withdrawal from validators according to their scores

The validators will interface with the protocol through the validator_router module.

use 0x1::account;
use 0x1::aptos_coin;
use 0x1::coin;
use 0x1::error;
use 0x1::event;
use 0x1::option;
use 0x1::signer;
use 0x1::string;
use 0x1::timestamp;
use 0x1::vector;
use 0xc0ded0c0::math;
use 0xc0ded0c0::validator_router;
use 0xc0ded0c0::validator_states;
use 0xc0ded0c1::permissions;
use 0xc0ded0c2::staked_aptos_coin;
use 0xc0ded0c2::tortuga_governance;

Resource StakingStatus

Struct which holds the current state of the protocol, including the storage for the protocol fee.

struct StakingStatus has key
Fields

protocol_fee: coin::Coin<staked_aptos_coin::StakedAptosCoin>Stores the protocol commission collected so farcommission: u64The rate at which protocol charges commission.community_rebate: u64When a user calls unstake, this much amount is taken from the user and distributed among all the current users of the protocol.total_claims_balance: u128Total balance of all the tickets issues since genesis.total_claims_balance_cleared: u128Total balance of all the redeemed tickets since genesis.cooldown_period: u64time that a user must wait between ticket issuance and ticket redemption, and time that must elapse after contract deployment before unstake is available.deployment_time: u64timestamp of the contract deployment.commission_exempt_amount: u64principal over which protocol's commission is calculated.min_transaction_amount: u64minimum amount to delegate. Also, used for clearing tAPT dust.increase_stake_in_validator_events: event::EventHandle<stake_router::IncreaseStakeInValidatorEvent>Event handle for increasing stake in a validator.unlock_from_validator_events: event::EventHandle<stake_router::UnlockFromValidatorEvent>Event handle for unlocking stake in a validator.

Resource EventStore

Struct which holds handles for events related to staking, unstaking, and claiming tickets. It is saved with the delegator's accounts.

struct EventStore has key
Fields

stake_events: event::EventHandle<stake_router::StakeEvent>Stores StakeEvents for the delegatorunstake_events: event::EventHandle<stake_router::UnstakeEvent>Stores UnstakeEvents for the delegatorclaim_events: event::EventHandle<stake_router::ClaimEvent>Stores ClaimEvents for the delegator

Resource StakedAptosCapabilities

Capabilities associated with tAPT coin

struct StakedAptosCapabilities has key
Fields

Struct Ticket

The ticket which is issued when a user unstakes.

struct Ticket has store
Fields

id: u128unique id, used to ensure first-in-first-out for claimsamount: u64amount in APT that will be redeemedtimestamp: u64timestamp when the ticket was issued

Resource TicketStore

A user can have several tickets which are stored in their ticket store

struct TicketStore has key
Fields

tickets: vector<stake_router::Ticket>List of all unclaimed tickets the user has.

Struct StakeEvent

Event emitted when a user stakes

struct StakeEvent has drop, store
Fields

delegator: addressaccount addressamount: u64amount staked in APTt_apt_coins: u64tAPT coins receivedtimestamp: u64timestamp when the event happened

Struct UnstakeEvent

Event emitted when a user unstakes

struct UnstakeEvent has drop, store
Fields

delegator: addressaccount addressamount: u64amount unstaked in APTt_apt_coins: u64tAPT coins burnedtimestamp: u64timestamp when the event happened

Struct ClaimEvent

Event emitted when a user claims a ticket

struct ClaimEvent has drop, store
Fields

delegator: addressAccount addressamount: u64Amount claimed in APTticket_index: u64Index of the ticket in the TicketStoretimestamp: u64timestamp when the event happened

Struct IncreaseStakeInValidatorEvent

Event emitted when a user increases stake in a validator

struct IncreaseStakeInValidatorEvent has drop, store
Fields

managed_pool_address: addressAddress of the validator's ManagedStakingPoolamount: u64The increase in stake in APTtimestamp: u64timestamp when the event happened

Struct UnlockFromValidatorEvent

Event emitted when a user unlocks funds from a validator

struct UnlockFromValidatorEvent has drop, store
Fields

managed_pool_address: addressAddress of the validator's ManagedStakingPoolamount: u64The amount of funds unlocked in APTtimestamp: u64timestamp when the event happened

Constants

When a non-protocol user tries to do any permissioned operation.

const EPERMISSION_DENIED: u64 = 6;

The normalizer for the commission rates.

const COMMISSION_NORMALIZER: u64 = 1000000;

When the amount in APT is too small for the operation.

const EAMOUNT_TOO_SMALL: u64 = 9;

When the commission being set is too high.

const ECOMMISSION_TOO_HIGH: u64 = 11;

When the new withdrawal fee is too high.

const ECOMMUNITY_REBATE_TOO_HIGH: u64 = 15;

When the cooldown period being set is too long.

const ECOOLDOWN_TOO_LONG: u64 = 12;

When a ticket cannot be claimed because the funds are not available.

const EFUND_NOT_AVAILABLE_YET: u64 = 5;

When a user tries to increase too much stake in a validator or unlock too much from a validator.

const EINVALID_AMOUNT: u64 = 10;

When a user tries to unstake a non-existent ticket.

const EINVALID_TICKET_INDEX: u64 = 8;

When the new min transaction amount is too high.

const EMIN_TRANSACTION_AMOUNT_TOO_HIGH: u64 = 14;

When a user tries to claim a ticket without ever having unstaked.

const ENO_REMAINING_CLAIMS: u64 = 4;

When the cooldown for a ticket has not elapsed.

const ETICKET_NOT_READY_YET: u64 = 3;

When the calculation of APT from shares fails because of incorrect arguments

const ETOO_MANY_SHARES: u64 = 2;

When the unstake operation is called before the cooldown period since contract deployment has elapsed.

const EUNSTAKE_NOT_READY_YET: u64 = 13;

The maximum withdrawal fee amount

const MAX_COMMUNITY_REBATE: u64 = 30000;

The maximum cooldown period in seconds.

const MAX_COOLDOWN_PERIOD: u64 = 1209600;

The maximum minimum transaction amount

const MAX_MIN_TRANSACTION_AMOUNT: u64 = 100000000000;

Function get_t_apt_supply

Returns the total tAPT minted.

public fun get_t_apt_supply(): u64
Implementation
public fun get_t_apt_supply(): u64 {
    (option::extract(&mut coin::supply<StakedAptosCoin>()) as u64)
}

Function get_total_value

Returns the total value locked with the protocol in APT (legacy name).

public fun get_total_value(): u64
Implementation
public fun get_total_value(): u64 acquires StakingStatus {
    let staking_status = borrow_global<StakingStatus>(@tortuga_governance);
    let unclaimed_balance =
        staking_status.total_claims_balance -
            staking_status.total_claims_balance_cleared;
    validator_router::get_total_balance() - (unclaimed_balance as u64)
}

Function current_total_stakable_amount

Returns how much APT can be staked with validators.

public fun current_total_stakable_amount(): u64
Implementation
public fun current_total_stakable_amount(): u64 acquires StakingStatus {
    let staking_status = borrow_global<StakingStatus>(@tortuga_governance);
    let unclaimed_balance = (
        (
            staking_status.total_claims_balance -
                staking_status.total_claims_balance_cleared
        ) as u64
    );
    let reserve_balance = validator_router::get_reserve_balance();
    let total_unlocking_balance =
        validator_states::get_total_unlocking_balance(@tortuga_governance);

    if (reserve_balance + total_unlocking_balance < unclaimed_balance) {
        0
    } else {
        reserve_balance + total_unlocking_balance - unclaimed_balance
    }
}

Function current_deficit

This function calculates how much needs to be unlocked from the validators, to be able to process unclaimed funds.

public fun current_deficit(): u64
Implementation
public fun current_deficit(): u64 acquires StakingStatus {
    let staking_status = borrow_global<StakingStatus>(@tortuga_governance);
    let unclaimed_balance = (
        (staking_status.total_claims_balance -
            staking_status.total_claims_balance_cleared)
        as u64
    );
    let reserve_balance = validator_router::get_reserve_balance();
    let total_unlocking_balance =
        validator_states::get_total_unlocking_balance(@tortuga_governance);

    if (reserve_balance + total_unlocking_balance > unclaimed_balance) {
        0
    } else {
        unclaimed_balance - (reserve_balance + total_unlocking_balance)
    }
}

Function get_num_tickets

Returns the total number of tickets that delegator has.

public fun get_num_tickets(delegator: address): u64
Implementation
public fun get_num_tickets(delegator: address): u64 acquires TicketStore {
    if (!exists<TicketStore>(delegator)) {
        0
    } else {
        let ticket_store = borrow_global<TicketStore>(delegator);
        vector::length(&ticket_store.tickets)
    }
}

Function get_ticket

Returns the ticket at position index for delegator as a 3-tuple of:

  • id: u128: unique id, used to ensure first-in-first-out for claims,

  • amount: u64, amount in APT that will be redeemed, and,

  • timestamp: u64: timestamp when the ticket was issued.

Abort condition

  • If delegator does not have a ticket at index.

public fun get_ticket(delegator: address, index: u64): (u128, u64, u64)
Implementation
public fun get_ticket(
    delegator: address,
    index: u64
): (u128, u64, u64) acquires TicketStore {
    assert!(
        exists<TicketStore>(delegator),
        error::invalid_argument(EINVALID_TICKET_INDEX)
    );
    let ticket_store = borrow_global<TicketStore>(delegator);

    assert!(
        index < vector::length(&ticket_store.tickets),
        error::invalid_argument(EINVALID_TICKET_INDEX)
    );

    let Ticket {
        id,
        amount,
        timestamp
    } = vector::borrow(&ticket_store.tickets, index);

    (*id, *amount, *timestamp)
}

Function initialize_tortuga_liquid_staking

This initializes the liquid staking protocol for tortuga with the given commission, cooldown_period and max_number_of_validators.

Restrictions

Abort conditions

public fun initialize_tortuga_liquid_staking(tortuga_governance_deployer: &signer, commission: u64, cooldown_period: u64, max_number_of_validators: u64)
Implementation
public entry fun initialize_tortuga_liquid_staking(
    tortuga_governance_deployer: &signer,
    commission: u64,
    cooldown_period: u64,
    max_number_of_validators: u64
) {
    // Since this function initializes a coin whose type is declared in
    // @tortuga_governance, this function can run successfully only if
    // called via the governance address. This assert is a reminder for
    // that.
    assert!(
        signer::address_of(tortuga_governance_deployer) == @tortuga_governance,
        error::unauthenticated(EPERMISSION_DENIED)
    );

    assert!(
        commission <= COMMISSION_NORMALIZER,
        error::invalid_argument(ECOMMISSION_TOO_HIGH)
    );

    // initialize the staked aptos coin first
    let (burn_cap, freeze_cap, mint_cap) =
        coin::initialize<StakedAptosCoin>(
            tortuga_governance_deployer,
            string::utf8(b"Tortuga Staked APT"),
            string::utf8(b"tAPT"),
            8,
            true,
        );

    move_to(tortuga_governance_deployer, StakedAptosCapabilities {
        mint_cap: mint_cap,
        freeze_cap: freeze_cap,
        burn_cap: burn_cap,
    });

    move_to(tortuga_governance_deployer, StakingStatus {
        protocol_fee: coin::zero<StakedAptosCoin>(),
        commission: commission,
        community_rebate: 0,
        total_claims_balance: 0,
        total_claims_balance_cleared: 0,
        cooldown_period: cooldown_period,
        deployment_time: timestamp::now_seconds(),
        commission_exempt_amount: 0,
        min_transaction_amount: 1000000, // 0.01 APT // tAPT
        increase_stake_in_validator_events:
            account::new_event_handle<IncreaseStakeInValidatorEvent>(
                tortuga_governance_deployer
            ),
        unlock_from_validator_events:
            account::new_event_handle<UnlockFromValidatorEvent>(
                tortuga_governance_deployer
            ),
    });

    // finally initialize the validator router
    validator_router::initialize(tortuga_governance_deployer, max_number_of_validators);

    // Initialize phase 0 of governance
    permissions::initialize_permissions<TortugaGovernance>(tortuga_governance_deployer);
}

Function set_commission

Set the commission that tortuga protocol charges to value.

Restrictions

Abort conditions

public fun set_commission(tortuga_governance: &signer, value: u64)
Implementation
public entry fun set_commission(
    tortuga_governance: &signer,
    value: u64
) acquires StakingStatus {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);

    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    assert!(
        value <= COMMISSION_NORMALIZER,
        error::invalid_argument(ECOMMISSION_TOO_HIGH)
    );
    staking_status.commission = value;
}

Function set_community_rebate

Set the community rebate that tortuga users get to value.

Restrictions

Abort conditions

  • If the value is greater than MAX_WITHDRAWAL_FEE, i.e. 0.05%.

public fun set_community_rebate(tortuga_governance: &signer, value: u64)
Implementation
public entry fun set_community_rebate(
    tortuga_governance: &signer,
    value: u64
) acquires StakingStatus {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);

    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    assert!(
        value <= MAX_COMMUNITY_REBATE,
        error::invalid_argument(ECOMMUNITY_REBATE_TOO_HIGH)
    );
    staking_status.community_rebate = value;
}

Function set_cooldown_period

Set wait period between ticket issuance and redemption.

Restrictions

Abort conditions

public fun set_cooldown_period(tortuga_governance: &signer, value: u64)
Implementation
public entry fun set_cooldown_period(
    tortuga_governance: &signer,
    value: u64
) acquires StakingStatus {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);

    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    assert!(
        value <= MAX_COOLDOWN_PERIOD,
        error::invalid_argument(ECOOLDOWN_TOO_LONG)
    );
    staking_status.cooldown_period = value;
}

Function set_min_transaction_amount

Set minimum delegation amount.

Restrictions

public fun set_min_transaction_amount(tortuga_governance: &signer, value: u64)
Implementation
public entry fun set_min_transaction_amount(
    tortuga_governance: &signer,
    value: u64
) acquires StakingStatus {
    permissions::assert_authority<TortugaGovernance>(tortuga_governance);

    assert!(
        value <= MAX_MIN_TRANSACTION_AMOUNT,
        error::invalid_argument(EMIN_TRANSACTION_AMOUNT_TOO_HIGH)
    );

    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    staking_status.min_transaction_amount = value;
}

Function stake

This is the endpoint for a user who wants to stake amount APT and get tAPT in return. The coins are directly deposited to the user's account.

Abort conditions

  • If the amount is less than min_transaction_amount.

public fun stake(delegator: &signer, amount: u64)
Implementation
public entry fun stake(
    delegator: &signer,
    amount: u64
) acquires StakingStatus, StakedAptosCapabilities, EventStore {
    let staking_status = borrow_global<StakingStatus>(@tortuga_governance);

    assert!(
        amount >= staking_status.min_transaction_amount,
        error::invalid_argument(EAMOUNT_TOO_SMALL)
    );

    let delegator_address = signer::address_of(delegator);
    let coins_to_stake = coin::withdraw<AptosCoin>(delegator, amount);
    register_for_t_apt(delegator);

    let minted_t_apt_coin = stake_coins(coins_to_stake);

    ensure_event_store(delegator);
    let event_store = borrow_global_mut<EventStore>(
        delegator_address
    );
    event::emit_event<StakeEvent>(
        &mut event_store.stake_events,
        StakeEvent {
            delegator: delegator_address,
            amount: amount,
            t_apt_coins: coin::value<StakedAptosCoin>(&minted_t_apt_coin),
            timestamp: timestamp::now_seconds(),
        }
    );

    // send shares to the delegator
    coin::deposit(delegator_address, minted_t_apt_coin);
}

Function unstake

Request to redeem APT by burn t_apt_amount tAPT.

Abort conditions

  • If the t_apt_amount is less than min_transaction_amount.

  • If the t_apt_amount is greater than the delegator's tAPT balance.

  • If cooldown_period has not passed since the contract's first deployment.

public fun unstake(delegator: &signer, t_apt_amount: u64)
Implementation
public entry fun unstake(
    delegator: &signer,
    t_apt_amount: u64
) acquires
    StakingStatus,
    StakedAptosCapabilities,
    TicketStore,
    EventStore
{
    let staking_status = borrow_global<StakingStatus>(@tortuga_governance);
    assert!(
        timestamp::now_seconds() >=
            staking_status.cooldown_period +
                staking_status.deployment_time,
        EUNSTAKE_NOT_READY_YET,
    );

    // Check against minimum transaction account
    let t_apt_supply = get_t_apt_supply();
    assert!(
        t_apt_amount >= staking_status.min_transaction_amount
        || t_apt_amount == t_apt_supply,
        error::invalid_argument(EAMOUNT_TOO_SMALL)
    );

    let t_apt_coins_to_unstake = coin::withdraw(delegator, t_apt_amount);
    let t_apt_remaining = coin::balance<StakedAptosCoin>(
        signer::address_of(delegator)
    );

    // must charge protocol fee to be able to calculate shares value
    // correctly
    charge_protocol_fee_internal();

    // calculate total worth
    let total_worth = get_total_value();
    // Need to borrow again because previous line also borrow this
    // resource.
    staking_status = borrow_global<StakingStatus>(@tortuga_governance);
    // If too little t_apt_coins remain in the wallet, then unstake all
    if (t_apt_remaining < staking_status.min_transaction_amount) {
        coin::merge<StakedAptosCoin>(
            &mut t_apt_coins_to_unstake,
            coin::withdraw(delegator, t_apt_remaining)
        );
        t_apt_amount = t_apt_amount + t_apt_remaining;
    };
    let shares_value = calc_shares_to_value(
        t_apt_amount,
        total_worth,
        t_apt_supply
    );

    // Distribute community rebate
    // to the remaining (t_apt_supply - t_apt_amount)
    // shares
    if (t_apt_supply > t_apt_amount) {
        let community_rebate = math::mul_div(
            shares_value,
            staking_status.community_rebate,
            COMMISSION_NORMALIZER
        );
        shares_value = shares_value - community_rebate;
    };

    // burn the tAPT coins
    let capabilities = borrow_global<StakedAptosCapabilities>(
        @tortuga_governance
    );
    coin::burn<StakedAptosCoin>(
        t_apt_coins_to_unstake,
        &capabilities.burn_cap
    );

    // modify total claims balance, and commission exempt amount
    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    staking_status.total_claims_balance = (
        staking_status.total_claims_balance + (shares_value as u128)
    );
    staking_status.commission_exempt_amount = (
        staking_status.commission_exempt_amount - shares_value
    );

    // issue claim ticket
    let delegator_address = signer::address_of(delegator);
    if (!exists<TicketStore>(delegator_address)) {
        move_to(delegator, TicketStore { tickets: vector::empty() });
    };
    let ticket_store = borrow_global_mut<TicketStore>(delegator_address);
    let ticket_id = staking_status.total_claims_balance;
    let now = timestamp::now_seconds();

    vector::push_back(
        &mut ticket_store.tickets,
        Ticket {
            id: ticket_id,
            amount: shares_value,
            timestamp: now
        }
    );

    ensure_event_store(delegator);
    let event_store = borrow_global_mut<EventStore>(
        delegator_address
    );
    event::emit_event<UnstakeEvent>(
        &mut event_store.unstake_events,
        UnstakeEvent {
            delegator: delegator_address,
            amount: shares_value,
            t_apt_coins: t_apt_amount,
            timestamp: now
        }
    );
}

Function claim

Users can claim their unstaked APTs using this function, after their funds have unlocked. A person who unstakes first will become eligible to claim first.

Note: The tickets in TicketStore are not guaranteed to be sorted by their timestamp field.

Abort conditions

  • If the delegator does not have a TicketStore.

  • If the ticket_index does not exist (anymore).

  • If the cooldown_period has not passed since the ticket's creation.

  • If the funds are still locked and not available to be claimed.

public fun claim(delegator: &signer, ticket_index: u64)
Implementation
public entry fun claim(
    delegator: &signer,
    ticket_index: u64
) acquires TicketStore, StakingStatus, EventStore {
    let delegator_address = signer::address_of(delegator);
    assert!(
        exists<TicketStore>(delegator_address),
        error::not_found(ENO_REMAINING_CLAIMS)
    );

    // get claim ticket
    let ticket_store = borrow_global_mut<TicketStore>(delegator_address);
    assert!(
        ticket_index < vector::length(&ticket_store.tickets),
        error::invalid_argument(EINVALID_TICKET_INDEX)
    );
    let ticket = vector::remove(
        &mut ticket_store.tickets,
        ticket_index
    );

    // assert that ticket has cooled down
    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    assert!(
        timestamp::now_seconds() - ticket.timestamp >
            staking_status.cooldown_period,
        error::invalid_argument(ETICKET_NOT_READY_YET)
    );

    // assert that the funds for this ticket has arrived
    let reserve_balance = validator_router::get_reserve_balance();
    assert!(
        ticket.id <=
            (reserve_balance as u128) +
                staking_status.total_claims_balance_cleared,
        error::unavailable(EFUND_NOT_AVAILABLE_YET)
    );

    // Pay aptos coins to the delegator
    let coins_to_repay = validator_router::extract_coins_from_reserve(
        ticket.amount
    );

    // The user may not have registered for aptos coins yet
    if (!coin::is_account_registered<AptosCoin>(delegator_address)) {
        coin::register<AptosCoin>(delegator);
    };

    coin::deposit<AptosCoin>(delegator_address, coins_to_repay);

    // update staking status
    staking_status.total_claims_balance_cleared = (
        staking_status.total_claims_balance_cleared + (
            ticket.amount as u128
        )
    );

    ensure_event_store(delegator);
    let event_store = borrow_global_mut<EventStore>(
        delegator_address
    );
    event::emit_event<ClaimEvent>(
        &mut event_store.claim_events,
        ClaimEvent {
            delegator: delegator_address,
            amount: ticket.amount,
            ticket_index: ticket_index,
            timestamp: timestamp::now_seconds()
        }
    );

    // burn the claim ticket
    burn_ticket(ticket);
}

Function increase_stake_in_validator

Increase the stake in the given managed_pool_address by amount if there are coins to stake in the coin reserve of the protocol and the current score and stake in the validator allows for it.

This allows permissionless staking into validators.

Abort conditions

public fun increase_stake_in_validator(managed_pool_address: address, amount: u64)
Implementation
public entry fun increase_stake_in_validator(
    managed_pool_address: address,
    amount: u64
) acquires StakingStatus {
    // Require the increase to be at least 1 APT
    assert!(
        amount >= 100000000,
        error::invalid_argument(EAMOUNT_TOO_SMALL)
    );

    let required_tvl = get_total_value();
    let target_delegation = validator_states::get_target_delegation(
        @tortuga_governance,
        managed_pool_address,
        required_tvl
    );
    let validator_balance = validator_states::get_balance_at_last_update(
        @tortuga_governance,
        managed_pool_address
    );

    assert!(
        target_delegation >= validator_balance + amount &&
            amount <= current_total_stakable_amount(),
        error::invalid_argument(EINVALID_AMOUNT)
    );
    validator_router::delegate(managed_pool_address, amount);

    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    event::emit_event<IncreaseStakeInValidatorEvent>(
        &mut staking_status.increase_stake_in_validator_events,
        IncreaseStakeInValidatorEvent {
            managed_pool_address: managed_pool_address,
            amount: amount,
            timestamp: timestamp::now_seconds()
        }
    );
}

Function unlock_from_validator

This function can be called if the total free balance is not enough to settle outstanding tickets. Anyone can call this function to unlock from the earliest unlocking validator, if current deficit is positive.

Abort conditions

  • If the amount is smaller than 1 APT.

  • If the amount is greater than the current_deficit().

public fun unlock_from_validator(managed_pool_address: address, amount: u64)
Implementation
public entry fun unlock_from_validator(
    managed_pool_address: address,
    amount: u64
) acquires StakingStatus {
    // If the validator is leaving us and we want to unlock more thant the
    // current deficit, we simply use remove_non_owner_delegator endpoint
    // provided in delegation_service
    assert!(
        amount <= current_deficit(),
        error::invalid_argument(EINVALID_AMOUNT)
    );

    // Require the unlock to be at least 1 APT
    assert!(
        amount >= 100000000,
        error::invalid_argument(EAMOUNT_TOO_SMALL)
    );

    validator_router::reserve_for_payout(managed_pool_address, amount);

    let staking_status = borrow_global_mut<StakingStatus>(
        @tortuga_governance
    );
    event::emit_event<UnlockFromValidatorEvent>(
        &mut staking_status.unlock_from_validator_events,
        UnlockFromValidatorEvent {
            managed_pool_address: managed_pool_address,
            amount: amount,
            timestamp: timestamp::now_seconds()
        }
    );
}

Function stake_coins

Stakes the given APT coins and returns tAPT coins.

fun stake_coins(coins_to_stake: coin::Coin<aptos_coin::AptosCoin>): coin::Coin<staked_aptos_coin::StakedAptosCoin>
Implementation
fun stake_coins(
    coins_to_stake: coin::Coin<AptosCoin>
): coin::Coin<StakedAptosCoin>
acquires
    StakingStatus,
    StakedAptosCapabilities
{
    // must charge protocol fee so that the new delegator is not subject to
    // pending commission
    charge_protocol_fee_internal();

    let t_apt_supply = get_t_apt_supply();
    let total_worth = get_total_value();
    let amount = coin::value(&coins_to_stake);
    let shares_to_mint = calc_shares_to_mint(
        amount,
        total_worth,
        t_apt_supply
    );

    // mint shares
    let capabilities = borrow_global<StakedAptosCapabilities>(@tortuga_governance);
    let minted_t_apt_coin = coin::mint<StakedAptosCoin>(
        shares_to_mint,
        &capabilities.mint_cap
    );

    // store aptos coin in the reserve
    let staking_status = borrow_global_mut<StakingStatus>(@tortuga_governance);
    validator_router::deposit_coins_to_reserve(coins_to_stake);

    // modify staking status
    staking_status.commission_exempt_amount =
        staking_status.commission_exempt_amount + amount;

    minted_t_apt_coin
}

Function ensure_event_store

Creates an EventStore for the given account if it does not exist.

fun ensure_event_store(account: &signer)
Implementation
fun ensure_event_store(
    account: &signer
) {
    let account_address = signer::address_of(account);
    if (!exists<EventStore>(account_address)) {
        move_to(account, EventStore {
            stake_events: account::new_event_handle<StakeEvent>(account),
            unstake_events:
                account::new_event_handle<UnstakeEvent>(account),
            claim_events: account::new_event_handle<ClaimEvent>(account),
        });
    };
}

Function charge_protocol_fee_internal

Charge the protocol fee based on the rewards earned over the commission_exempt_amount and the current reward_commission.

fun charge_protocol_fee_internal()
Implementation
fun charge_protocol_fee_internal()
acquires
    StakedAptosCapabilities,
    StakingStatus
{
    let total_worth = get_total_value();
    let t_apt_supply = get_t_apt_supply();
    let staking_status = borrow_global_mut<StakingStatus>(@tortuga_governance);

    // This potentially underflows because when you first stake with a
    // validator.
    // You pay for last epoch's commissions, leading to a lower total worth
    // than your original deposit.
    let total_surplus_since_last_payment = math::safe_sub(
        total_worth,
        staking_status.commission_exempt_amount,
    );

    if (total_surplus_since_last_payment == 0) {
        return
    };
    let commission_amount = mul_div(
        total_surplus_since_last_payment,
        staking_status.commission,
        COMMISSION_NORMALIZER,
    );
    let total_worth_after_commission = total_worth - commission_amount;
    let new_shares_for_protocol = calc_shares_to_mint(
        commission_amount,
        total_worth_after_commission,
        t_apt_supply
    );

    // mint shares
    let capabilities = borrow_global<StakedAptosCapabilities>(
        @tortuga_governance
    );
    let minted_t_apt_coin = coin::mint<StakedAptosCoin>(
        new_shares_for_protocol,
        &capabilities.mint_cap
    );

    // deposit shares in the protocol_fee
    coin::merge<StakedAptosCoin>(
        &mut staking_status.protocol_fee,
        minted_t_apt_coin
    );

    // reset commission exempt amount as the commission so far has been paid
    staking_status.commission_exempt_amount = total_worth;
}

Function burn_ticket

Burn the passed ticket.

fun burn_ticket(ticket: stake_router::Ticket)
Implementation
fun burn_ticket(ticket: Ticket) {
    let Ticket { id: _id, amount: _amount, timestamp: _timestamp } = ticket;
}

Function calc_shares_to_mint

Calculate the number of shares to mint based on the value_being_added in APT, the total_worth (i.e. the TVL) of the protocol in APT, and the t_apt_supply.

fun calc_shares_to_mint(value_being_added: u64, total_worth: u64, t_apt_supply: u64): u64
Implementation
fun calc_shares_to_mint(
    value_being_added: u64,
    total_worth: u64,
    t_apt_supply: u64
): u64 {
    // Note: `t_apt_supply` can actually be 0 here, and `total_worth > 0`.
    // This can happen when the only person with tAPT unstakes and more
    // value accrues during the last epoch.
    //
    // In this case, the next staker gets more value,
    // but we mint them shares such that 1 tAPT = 1 APT.
    if (t_apt_supply == 0 && total_worth > 0 && value_being_added > 0) {
        return total_worth + value_being_added
    };

    math::mul_div_with_init(
        value_being_added,
        t_apt_supply,
        total_worth,
        1,
    )
}

Function calc_shares_to_value

Calculate the value of num_shares if the total_worth (TVL) of the protocol in APT, and the t_apt_supply.

Abort conditions

  • If t_apt_supply < num_shares.

fun calc_shares_to_value(num_shares: u64, total_worth: u64, t_apt_supply: u64): u64
Implementation
fun calc_shares_to_value(
    num_shares: u64,
    total_worth: u64,
    t_apt_supply: u64
): u64 {
    assert!(
        t_apt_supply >= num_shares,
        error::invalid_state(ETOO_MANY_SHARES)
    );
    math::safe_mul_div(num_shares, total_worth, t_apt_supply, 0)
}

Last updated