diff --git a/aptos-move/aptos-release-builder/src/components/feature_flags.rs b/aptos-move/aptos-release-builder/src/components/feature_flags.rs index 9ece764a9a88c..9a98fdda624de 100644 --- a/aptos-move/aptos-release-builder/src/components/feature_flags.rs +++ b/aptos-move/aptos-release-builder/src/components/feature_flags.rs @@ -89,6 +89,7 @@ pub enum FeatureFlag { AggregatorV2DelayedFields, ConcurrentAssets, LimitMaxIdentifierLength, + OperatorBeneficiaryChange, } fn generate_features_blob(writer: &CodeWriter, data: &[u64]) { @@ -232,6 +233,7 @@ impl From for AptosFeatureFlag { }, FeatureFlag::ConcurrentAssets => AptosFeatureFlag::CONCURRENT_ASSETS, FeatureFlag::LimitMaxIdentifierLength => AptosFeatureFlag::LIMIT_MAX_IDENTIFIER_LENGTH, + FeatureFlag::OperatorBeneficiaryChange => AptosFeatureFlag::OPERATOR_BENEFICIARY_CHANGE, } } } @@ -298,6 +300,7 @@ impl From for FeatureFlag { }, AptosFeatureFlag::CONCURRENT_ASSETS => FeatureFlag::ConcurrentAssets, AptosFeatureFlag::LIMIT_MAX_IDENTIFIER_LENGTH => FeatureFlag::LimitMaxIdentifierLength, + AptosFeatureFlag::OPERATOR_BENEFICIARY_CHANGE => FeatureFlag::OperatorBeneficiaryChange, } } } diff --git a/aptos-move/framework/aptos-framework/doc/delegation_pool.md b/aptos-move/framework/aptos-framework/doc/delegation_pool.md index b589f21b346ee..9aa3f6032724c 100644 --- a/aptos-move/framework/aptos-framework/doc/delegation_pool.md +++ b/aptos-move/framework/aptos-framework/doc/delegation_pool.md @@ -121,14 +121,17 @@ transferred to A - [Struct `VoteDelegation`](#0x1_delegation_pool_VoteDelegation) - [Struct `DelegatedVotes`](#0x1_delegation_pool_DelegatedVotes) - [Resource `GovernanceRecords`](#0x1_delegation_pool_GovernanceRecords) +- [Resource `BeneficiaryForOperator`](#0x1_delegation_pool_BeneficiaryForOperator) - [Struct `AddStakeEvent`](#0x1_delegation_pool_AddStakeEvent) - [Struct `ReactivateStakeEvent`](#0x1_delegation_pool_ReactivateStakeEvent) - [Struct `UnlockStakeEvent`](#0x1_delegation_pool_UnlockStakeEvent) - [Struct `WithdrawStakeEvent`](#0x1_delegation_pool_WithdrawStakeEvent) - [Struct `DistributeCommissionEvent`](#0x1_delegation_pool_DistributeCommissionEvent) +- [Struct `DistributeCommission`](#0x1_delegation_pool_DistributeCommission) - [Struct `VoteEvent`](#0x1_delegation_pool_VoteEvent) - [Struct `CreateProposalEvent`](#0x1_delegation_pool_CreateProposalEvent) - [Struct `DelegateVotingPowerEvent`](#0x1_delegation_pool_DelegateVotingPowerEvent) +- [Struct `SetBeneficiaryForOperator`](#0x1_delegation_pool_SetBeneficiaryForOperator) - [Constants](#@Constants_0) - [Function `owner_cap_exists`](#0x1_delegation_pool_owner_cap_exists) - [Function `get_owned_pool_address`](#0x1_delegation_pool_get_owned_pool_address) @@ -147,6 +150,7 @@ transferred to A - [Function `calculate_and_update_delegator_voter`](#0x1_delegation_pool_calculate_and_update_delegator_voter) - [Function `get_expected_stake_pool_address`](#0x1_delegation_pool_get_expected_stake_pool_address) - [Function `initialize_delegation_pool`](#0x1_delegation_pool_initialize_delegation_pool) +- [Function `beneficiary_for_operator`](#0x1_delegation_pool_beneficiary_for_operator) - [Function `enable_partial_governance_voting`](#0x1_delegation_pool_enable_partial_governance_voting) - [Function `vote`](#0x1_delegation_pool_vote) - [Function `create_proposal`](#0x1_delegation_pool_create_proposal) @@ -171,6 +175,7 @@ transferred to A - [Function `calculate_and_update_delegator_voter_internal`](#0x1_delegation_pool_calculate_and_update_delegator_voter_internal) - [Function `calculate_and_update_delegated_votes`](#0x1_delegation_pool_calculate_and_update_delegated_votes) - [Function `set_operator`](#0x1_delegation_pool_set_operator) +- [Function `set_beneficiary_for_operator`](#0x1_delegation_pool_set_beneficiary_for_operator) - [Function `set_delegated_voter`](#0x1_delegation_pool_set_delegated_voter) - [Function `delegate_voting_power`](#0x1_delegation_pool_delegate_voting_power) - [Function `add_stake`](#0x1_delegation_pool_add_stake) @@ -199,6 +204,7 @@ transferred to A
use 0x1::account;
+use 0x1::aptos_account;
 use 0x1::aptos_coin;
 use 0x1::aptos_governance;
 use 0x1::coin;
@@ -547,6 +553,33 @@ This struct should be stored in the delegation pool resource account.
 
 
 
+
+
+
+
+## Resource `BeneficiaryForOperator`
+
+
+
+
struct BeneficiaryForOperator has key
+
+ + + +
+Fields + + +
+
+beneficiary_for_operator: address +
+
+ +
+
+ +
@@ -754,6 +787,58 @@ This struct should be stored in the delegation pool resource account. + + + + +## Struct `DistributeCommission` + + + +
#[event]
+struct DistributeCommission has drop, store
+
+ + + +
+Fields + + +
+
+pool_address: address +
+
+ +
+
+operator: address +
+
+ +
+
+beneficiary: address +
+
+ +
+
+commission_active: u64 +
+
+ +
+
+commission_pending_inactive: u64 +
+
+ +
+
+ +
@@ -883,6 +968,46 @@ This struct should be stored in the delegation pool resource account. + + + + +## Struct `SetBeneficiaryForOperator` + + + +
#[event]
+struct SetBeneficiaryForOperator has drop, store
+
+ + + +
+Fields + + +
+
+operator: address +
+
+ +
+
+old_beneficiary: address +
+
+ +
+
+new_beneficiary: address +
+
+ +
+
+ +
@@ -919,6 +1044,16 @@ The function is disabled or hasn't been enabled. + + +The account is not the operator of the stake pool. + + +
const ENOT_OPERATOR: u64 = 18;
+
+ + + Account is already owning a delegation pool. @@ -1038,6 +1173,16 @@ There is not enough active stake on the stake pool to unlock< + + +Chaning beneficiaries for operators is not supported. + + +
const EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED: u64 = 19;
+
+ + + Partial governance voting hasn't been enabled on this delegation pool. @@ -1430,7 +1575,7 @@ in each of its individual states: (active,inactive,Implementation -
public fun get_stake(pool_address: address, delegator_address: address): (u64, u64, u64) acquires DelegationPool {
+
public fun get_stake(pool_address: address, delegator_address: address): (u64, u64, u64) acquires DelegationPool, BeneficiaryForOperator {
     assert_delegation_pool_exists(pool_address);
     let pool = borrow_global<DelegationPool>(pool_address);
     let (
@@ -1470,7 +1615,7 @@ in each of its individual states: (active,inactive,to buy shares which is introducing
     // some imprecision (received stake would be slightly less)
     // but adding rewards onto the existing stake is still a good approximation
-    if (delegator_address == stake::get_operator(pool_address)) {
+    if (delegator_address == beneficiary_for_operator(get_operator(pool_address))) {
         active = active + commission_active;
         // in-flight pending_inactive commission can coexist with already inactive withdrawal
         if (lockup_cycle_ended) {
@@ -1578,7 +1723,7 @@ latest state.
 Implementation
 
 
-
public fun calculate_and_update_voter_total_voting_power(pool_address: address, voter: address): u64 acquires DelegationPool, GovernanceRecords {
+
public fun calculate_and_update_voter_total_voting_power(pool_address: address, voter: address): u64 acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert_partial_governance_voting_enabled(pool_address);
     // Delegation pool need to be synced to explain rewards(which could change the coin amount) and
     // commission(which could cause share transfer).
@@ -1612,7 +1757,7 @@ latest state.
 Implementation
 
 
-
public fun calculate_and_update_remaining_voting_power(pool_address: address, voter_address: address, proposal_id: u64): u64 acquires DelegationPool, GovernanceRecords {
+
public fun calculate_and_update_remaining_voting_power(pool_address: address, voter_address: address, proposal_id: u64): u64 acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert_partial_governance_voting_enabled(pool_address);
     // If the whole stake pool has no voting power(e.g. it has already voted before partial
     // governance voting flag is enabled), the delegator also has no voting power.
@@ -1713,7 +1858,7 @@ Ownership over setting the operator/voter is granted to owner who h
     owner: &signer,
     operator_commission_percentage: u64,
     delegation_pool_creation_seed: vector<u8>,
-) acquires DelegationPool, GovernanceRecords {
+) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert!(features::delegation_pools_enabled(), error::invalid_state(EDELEGATION_POOLS_DISABLED));
     let owner_address = signer::address_of(owner);
     assert!(!owner_cap_exists(owner_address), error::already_exists(EOWNER_CAP_ALREADY_EXISTS));
@@ -1763,6 +1908,36 @@ Ownership over setting the operator/voter is granted to owner who h
 
 
 
+
+
+
+
+## Function `beneficiary_for_operator`
+
+Return the beneficiary address of the operator.
+
+
+
#[view]
+public fun beneficiary_for_operator(operator: address): address
+
+ + + +
+Implementation + + +
public fun beneficiary_for_operator(operator: address): address acquires BeneficiaryForOperator {
+    if (exists<BeneficiaryForOperator>(operator)) {
+        return borrow_global<BeneficiaryForOperator>(operator).beneficiary_for_operator
+    } else {
+        operator
+    }
+}
+
+ + +
@@ -1784,7 +1959,7 @@ THe existing voter will be replaced. The function is permissionless.
public entry fun enable_partial_governance_voting(
     pool_address: address,
-) acquires DelegationPool, GovernanceRecords {
+) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert!(features::partial_governance_voting_enabled(), error::invalid_state(EDISABLED_FUNCTION));
     assert!(features::delegation_pool_partial_governance_voting_enabled(), error::invalid_state(EDISABLED_FUNCTION));
     assert_delegation_pool_exists(pool_address);
@@ -1833,7 +2008,7 @@ Vote on a proposal with a voter's voting power. To successfully vote, the follow
 Implementation
 
 
-
public entry fun vote(voter: &signer, pool_address: address, proposal_id: u64, voting_power: u64, should_pass: bool) acquires DelegationPool, GovernanceRecords {
+
public entry fun vote(voter: &signer, pool_address: address, proposal_id: u64, voting_power: u64, should_pass: bool) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert_partial_governance_voting_enabled(pool_address);
     // synchronize delegation and stake pools before any user operation.
     synchronize_delegation_pool(pool_address);
@@ -1896,7 +2071,7 @@ voting power in THIS delegation pool must be not less than the minimum required
     metadata_location: vector<u8>,
     metadata_hash: vector<u8>,
     is_multi_step_proposal: bool,
-) acquires DelegationPool, GovernanceRecords {
+) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert_partial_governance_voting_enabled(pool_address);
 
     // synchronize delegation and stake pools before any user operation
@@ -2562,7 +2737,7 @@ Allows an owner to change the operator of the underlying stake pool.
 
public entry fun set_operator(
     owner: &signer,
     new_operator: address
-) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords {
+) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     let pool_address = get_owned_pool_address(signer::address_of(owner));
     // synchronize delegation and stake pools before any user operation
     // ensure the old operator is paid its uncommitted commission rewards
@@ -2573,6 +2748,51 @@ Allows an owner to change the operator of the underlying stake pool.
 
 
 
+
+
+
+
+## Function `set_beneficiary_for_operator`
+
+Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new
+beneficiary. To ensures payment to the current beneficiary, one should first call synchronize_delegation_pool
+before switching the beneficiary. An operator can set one beneficiary for delegation pools, not a separate
+one for each pool.
+
+
+
public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address)
+
+ + + +
+Implementation + + +
public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address) acquires BeneficiaryForOperator {
+    assert!(features::operator_beneficiary_change_enabled(), std::error::invalid_state(
+        EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED
+    ));
+    // The beneficiay address of an operator is stored under the operator's address.
+    // So, the operator does not need to be validated with respect to a staking pool.
+    let operator_addr = signer::address_of(operator);
+    let old_beneficiary = beneficiary_for_operator(operator_addr);
+    if (exists<BeneficiaryForOperator>(operator_addr)) {
+        borrow_global_mut<BeneficiaryForOperator>(operator_addr).beneficiary_for_operator = new_beneficiary;
+    } else {
+        move_to(operator, BeneficiaryForOperator { beneficiary_for_operator: new_beneficiary });
+    };
+
+    emit(SetBeneficiaryForOperator {
+        operator: operator_addr,
+        old_beneficiary,
+        new_beneficiary,
+    });
+}
+
+ + +
@@ -2594,7 +2814,7 @@ Allows an owner to change the delegated voter of the underlying stake pool.
public entry fun set_delegated_voter(
     owner: &signer,
     new_voter: address
-) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords {
+) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     // No one can change delegated_voter once the partial governance voting feature is enabled.
     assert!(!features::delegation_pool_partial_governance_voting_enabled(), error::invalid_state(EDEPRECATED_FUNCTION));
     let pool_address = get_owned_pool_address(signer::address_of(owner));
@@ -2629,7 +2849,7 @@ this change won't take effects until the next lockup period.
     delegator: &signer,
     pool_address: address,
     new_voter: address
-) acquires DelegationPool, GovernanceRecords {
+) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert_partial_governance_voting_enabled(pool_address);
 
     // synchronize delegation and stake pools before any user operation
@@ -2696,7 +2916,7 @@ Add amount of coins to the delegation pool pool_addressImplementation
 
 
-
public entry fun add_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords {
+
public entry fun add_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     // short-circuit if amount to add is 0 so no event is emitted
     if (amount == 0) { return };
     // synchronize delegation and stake pools before any user operation
@@ -2709,7 +2929,7 @@ Add amount of coins to the delegation pool pool_addresslet delegator_address = signer::address_of(delegator);
 
     // stake the entire amount to the stake pool
-    coin::transfer<AptosCoin>(delegator, pool_address, amount);
+    aptos_account::transfer(delegator, pool_address, amount);
     stake::add_stake(&retrieve_stake_pool_owner(pool), amount);
 
     // but buy shares for delegator just for the remaining amount after fee
@@ -2755,7 +2975,7 @@ at most how much active stake there is on the stake pool.
 Implementation
 
 
-
public entry fun unlock(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords {
+
public entry fun unlock(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     // short-circuit if amount to unlock is 0 so no event is emitted
     if (amount == 0) { return };
 
@@ -2813,7 +3033,7 @@ Move amount of coins from pending_inactive to active.
 Implementation
 
 
-
public entry fun reactivate_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords {
+
public entry fun reactivate_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     // short-circuit if amount to reactivate is 0 so no event is emitted
     if (amount == 0) { return };
     // synchronize delegation and stake pools before any user operation
@@ -2867,7 +3087,7 @@ Withdraw amount of owned inactive stake from the delegation pool at
 Implementation
 
 
-
public entry fun withdraw(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords {
+
public entry fun withdraw(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert!(amount > 0, error::invalid_argument(EWITHDRAW_ZERO_STAKE));
     // synchronize delegation and stake pools before any user operation
     synchronize_delegation_pool(pool_address);
@@ -2936,7 +3156,7 @@ Withdraw amount of owned inactive stake from the delegation pool at
         // no excess stake if `stake::withdraw` does not inactivate at all
         stake::withdraw(stake_pool_owner, amount);
     };
-    coin::transfer<AptosCoin>(stake_pool_owner, delegator_address, amount);
+    aptos_account::transfer(stake_pool_owner, delegator_address, amount);
 
     // commit withdrawal of possibly inactive stake to the `total_coins_inactive`
     // known by the delegation pool in order to not mistake it for slashing at next synchronization
@@ -3402,7 +3622,7 @@ shares pools, assign commission to operator and eventually prepare delegation po
 Implementation
 
 
-
public entry fun synchronize_delegation_pool(pool_address: address) acquires DelegationPool, GovernanceRecords {
+
public entry fun synchronize_delegation_pool(pool_address: address) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator {
     assert_delegation_pool_exists(pool_address);
     let pool = borrow_global_mut<DelegationPool>(pool_address);
     let (
@@ -3436,9 +3656,9 @@ shares pools, assign commission to operator and eventually prepare delegation po
     );
 
     // reward operator its commission out of uncommitted active rewards (`add_stake` fees already excluded)
-    buy_in_active_shares(pool, stake::get_operator(pool_address), commission_active);
+    buy_in_active_shares(pool, beneficiary_for_operator(stake::get_operator(pool_address)), commission_active);
     // reward operator its commission out of uncommitted pending_inactive rewards
-    buy_in_pending_inactive_shares(pool, stake::get_operator(pool_address), commission_pending_inactive);
+    buy_in_pending_inactive_shares(pool, beneficiary_for_operator(stake::get_operator(pool_address)), commission_pending_inactive);
 
     event::emit_event(
         &mut pool.distribute_commission_events,
@@ -3450,6 +3670,16 @@ shares pools, assign commission to operator and eventually prepare delegation po
         },
     );
 
+    if (features::operator_beneficiary_change_enabled()) {
+        emit(DistributeCommission {
+            pool_address,
+            operator: stake::get_operator(pool_address),
+            beneficiary: beneficiary_for_operator(stake::get_operator(pool_address)),
+            commission_active,
+            commission_pending_inactive,
+        })
+    };
+
     // advance lockup cycle on delegation pool if already ended on stake pool (AND stake explicitly inactivated)
     if (lockup_cycle_ended) {
         // capture inactive coins over all ended lockup cycles (including this ending one)
diff --git a/aptos-move/framework/aptos-framework/doc/staking_contract.md b/aptos-move/framework/aptos-framework/doc/staking_contract.md
index 963caff5b0db1..27a3c1ec5152c 100644
--- a/aptos-move/framework/aptos-framework/doc/staking_contract.md
+++ b/aptos-move/framework/aptos-framework/doc/staking_contract.md
@@ -34,6 +34,7 @@ pool.
 -  [Struct `StakingGroupContainer`](#0x1_staking_contract_StakingGroupContainer)
 -  [Struct `StakingContract`](#0x1_staking_contract_StakingContract)
 -  [Resource `Store`](#0x1_staking_contract_Store)
+-  [Resource `BeneficiaryForOperator`](#0x1_staking_contract_BeneficiaryForOperator)
 -  [Struct `UpdateCommissionEvent`](#0x1_staking_contract_UpdateCommissionEvent)
 -  [Resource `StakingGroupUpdateCommissionEvent`](#0x1_staking_contract_StakingGroupUpdateCommissionEvent)
 -  [Struct `CreateStakingContractEvent`](#0x1_staking_contract_CreateStakingContractEvent)
@@ -45,6 +46,7 @@ pool.
 -  [Struct `SwitchOperatorEvent`](#0x1_staking_contract_SwitchOperatorEvent)
 -  [Struct `AddDistributionEvent`](#0x1_staking_contract_AddDistributionEvent)
 -  [Struct `DistributeEvent`](#0x1_staking_contract_DistributeEvent)
+-  [Struct `SetBeneficiaryForOperator`](#0x1_staking_contract_SetBeneficiaryForOperator)
 -  [Constants](#@Constants_0)
 -  [Function `stake_pool_address`](#0x1_staking_contract_stake_pool_address)
 -  [Function `last_recorded_principal`](#0x1_staking_contract_last_recorded_principal)
@@ -52,6 +54,7 @@ pool.
 -  [Function `staking_contract_amounts`](#0x1_staking_contract_staking_contract_amounts)
 -  [Function `pending_distribution_counts`](#0x1_staking_contract_pending_distribution_counts)
 -  [Function `staking_contract_exists`](#0x1_staking_contract_staking_contract_exists)
+-  [Function `beneficiary_for_operator`](#0x1_staking_contract_beneficiary_for_operator)
 -  [Function `get_expected_stake_pool_address`](#0x1_staking_contract_get_expected_stake_pool_address)
 -  [Function `create_staking_contract`](#0x1_staking_contract_create_staking_contract)
 -  [Function `create_staking_contract_with_coins`](#0x1_staking_contract_create_staking_contract_with_coins)
@@ -65,6 +68,7 @@ pool.
 -  [Function `unlock_rewards`](#0x1_staking_contract_unlock_rewards)
 -  [Function `switch_operator_with_same_commission`](#0x1_staking_contract_switch_operator_with_same_commission)
 -  [Function `switch_operator`](#0x1_staking_contract_switch_operator)
+-  [Function `set_beneficiary_for_operator`](#0x1_staking_contract_set_beneficiary_for_operator)
 -  [Function `distribute`](#0x1_staking_contract_distribute)
 -  [Function `distribute_internal`](#0x1_staking_contract_distribute_internal)
 -  [Function `assert_staking_contract_exists`](#0x1_staking_contract_assert_staking_contract_exists)
@@ -81,6 +85,7 @@ pool.
     -  [Function `staking_contract_amounts`](#@Specification_1_staking_contract_amounts)
     -  [Function `pending_distribution_counts`](#@Specification_1_pending_distribution_counts)
     -  [Function `staking_contract_exists`](#@Specification_1_staking_contract_exists)
+    -  [Function `beneficiary_for_operator`](#@Specification_1_beneficiary_for_operator)
     -  [Function `create_staking_contract`](#@Specification_1_create_staking_contract)
     -  [Function `create_staking_contract_with_coins`](#@Specification_1_create_staking_contract_with_coins)
     -  [Function `add_stake`](#@Specification_1_add_stake)
@@ -93,6 +98,7 @@ pool.
     -  [Function `unlock_rewards`](#@Specification_1_unlock_rewards)
     -  [Function `switch_operator_with_same_commission`](#@Specification_1_switch_operator_with_same_commission)
     -  [Function `switch_operator`](#@Specification_1_switch_operator)
+    -  [Function `set_beneficiary_for_operator`](#@Specification_1_set_beneficiary_for_operator)
     -  [Function `distribute`](#@Specification_1_distribute)
     -  [Function `distribute_internal`](#@Specification_1_distribute_internal)
     -  [Function `assert_staking_contract_exists`](#@Specification_1_assert_staking_contract_exists)
@@ -104,11 +110,13 @@ pool.
 
 
 
use 0x1::account;
+use 0x1::aptos_account;
 use 0x1::aptos_coin;
 use 0x1::bcs;
 use 0x1::coin;
 use 0x1::error;
 use 0x1::event;
+use 0x1::features;
 use 0x1::pool_u64;
 use 0x1::signer;
 use 0x1::simple_map;
@@ -283,6 +291,33 @@ pool.
 
 
 
+
+
+
+
+## Resource `BeneficiaryForOperator`
+
+
+
+
struct BeneficiaryForOperator has key
+
+ + + +
+Fields + + +
+
+beneficiary_for_operator: address +
+
+ +
+
+ +
@@ -737,6 +772,46 @@ pool. + + + + +## Struct `SetBeneficiaryForOperator` + + + +
#[event]
+struct SetBeneficiaryForOperator has drop, store
+
+ + + +
+Fields + + +
+
+operator: address +
+
+ +
+
+old_beneficiary: address +
+
+ +
+
+new_beneficiary: address +
+
+ +
+
+ +
@@ -754,6 +829,16 @@ Commission percentage has to be between 0 and 100. + + +Chaning beneficiaries for operators is not supported. + + +
const EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED: u64 = 9;
+
+ + + Staking contracts can't be merged. @@ -784,12 +869,12 @@ Store amount must be at least the min stake required for a stake pool to join th - + -Caller must be either the staker or operator. +Caller must be either the staker, operator, or beneficiary. -
const ENOT_STAKER_OR_OPERATOR: u64 = 8;
+
const ENOT_STAKER_OR_OPERATOR_OR_BENEFICIARY: u64 = 8;
 
@@ -1029,6 +1114,36 @@ Return true if the staking contract between the provided staker and operator exi + + + + +## Function `beneficiary_for_operator` + +Return the beneficiary address of the operator. + + +
#[view]
+public fun beneficiary_for_operator(operator: address): address
+
+ + + +
+Implementation + + +
public fun beneficiary_for_operator(operator: address): address acquires BeneficiaryForOperator {
+    if (exists<BeneficiaryForOperator>(operator)) {
+        return borrow_global<BeneficiaryForOperator>(operator).beneficiary_for_operator
+    } else {
+        operator
+    }
+}
+
+ + +
@@ -1307,7 +1422,7 @@ TODO: fix the typo in function name. commision -> commission Implementation -
public entry fun update_commision(staker: &signer, operator: address, new_commission_percentage: u64) acquires Store, StakingGroupUpdateCommissionEvent {
+
public entry fun update_commision(staker: &signer, operator: address, new_commission_percentage: u64) acquires Store, StakingGroupUpdateCommissionEvent, BeneficiaryForOperator {
     assert!(
         new_commission_percentage >= 0 && new_commission_percentage <= 100,
         error::invalid_argument(EINVALID_COMMISSION_PERCENTAGE),
@@ -1348,7 +1463,7 @@ TODO: fix the typo in function name. commision -> commission
 Unlock commission amount from the stake pool. Operator needs to wait for the amount to become withdrawable
 at the end of the stake pool's lockup period before they can actually can withdraw_commission.
 
-Only staker or operator can call this.
+Only staker, operator or beneficiary can call this.
 
 
 
public entry fun request_commission(account: &signer, staker: address, operator: address)
@@ -1360,9 +1475,12 @@ Only staker or operator can call this.
 Implementation
 
 
-
public entry fun request_commission(account: &signer, staker: address, operator: address) acquires Store {
+
public entry fun request_commission(account: &signer, staker: address, operator: address) acquires Store, BeneficiaryForOperator {
     let account_addr = signer::address_of(account);
-    assert!(account_addr == staker || account_addr == operator, error::unauthenticated(ENOT_STAKER_OR_OPERATOR));
+    assert!(
+        account_addr == staker || account_addr == operator || account_addr == beneficiary_for_operator(operator),
+        error::unauthenticated(ENOT_STAKER_OR_OPERATOR_OR_BENEFICIARY)
+    );
     assert_staking_contract_exists(staker, operator);
 
     let store = borrow_global_mut<Store>(staker);
@@ -1457,7 +1575,7 @@ This also triggers paying commission to the operator for accounting simplicity.
 Implementation
 
 
-
public entry fun unlock_stake(staker: &signer, operator: address, amount: u64) acquires Store {
+
public entry fun unlock_stake(staker: &signer, operator: address, amount: u64) acquires Store, BeneficiaryForOperator {
     // Short-circuit if amount is 0.
     if (amount == 0) return;
 
@@ -1523,7 +1641,7 @@ Unlock all accumulated rewards since the last recorded principals.
 Implementation
 
 
-
public entry fun unlock_rewards(staker: &signer, operator: address) acquires Store {
+
public entry fun unlock_rewards(staker: &signer, operator: address) acquires Store, BeneficiaryForOperator {
     let staker_address = signer::address_of(staker);
     assert_staking_contract_exists(staker_address, operator);
 
@@ -1558,7 +1676,7 @@ Allows staker to switch operator without going through the lenghthy process to u
     staker: &signer,
     old_operator: address,
     new_operator: address,
-) acquires Store {
+) acquires Store, BeneficiaryForOperator {
     let staker_address = signer::address_of(staker);
     assert_staking_contract_exists(staker_address, old_operator);
 
@@ -1592,7 +1710,7 @@ Allows staker to switch operator without going through the lenghthy process to u
     old_operator: address,
     new_operator: address,
     new_commission_percentage: u64,
-) acquires Store {
+) acquires Store, BeneficiaryForOperator {
     let staker_address = signer::address_of(staker);
     assert_staking_contract_exists(staker_address, old_operator);
 
@@ -1632,6 +1750,50 @@ Allows staker to switch operator without going through the lenghthy process to u
 
 
 
+
+
+
+
+## Function `set_beneficiary_for_operator`
+
+Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new
+beneficiary. To ensures payment to the current beneficiary, one should first call distribute before switching
+the beneficiary. An operator can set one beneficiary for staking contract pools, not a separate one for each pool.
+
+
+
public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address)
+
+ + + +
+Implementation + + +
public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address) acquires BeneficiaryForOperator {
+    assert!(features::operator_beneficiary_change_enabled(), std::error::invalid_state(
+        EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED
+    ));
+    // The beneficiay address of an operator is stored under the operator's address.
+    // So, the operator does not need to be validated with respect to a staking pool.
+    let operator_addr = signer::address_of(operator);
+    let old_beneficiary = beneficiary_for_operator(operator_addr);
+    if (exists<BeneficiaryForOperator>(operator_addr)) {
+        borrow_global_mut<BeneficiaryForOperator>(operator_addr).beneficiary_for_operator = new_beneficiary;
+    } else {
+        move_to(operator, BeneficiaryForOperator { beneficiary_for_operator: new_beneficiary });
+    };
+
+    emit(SetBeneficiaryForOperator {
+        operator: operator_addr,
+        old_beneficiary,
+        new_beneficiary,
+    });
+}
+
+ + +
@@ -1651,7 +1813,7 @@ not need to be restricted to just the staker or operator. Implementation -
public entry fun distribute(staker: address, operator: address) acquires Store {
+
public entry fun distribute(staker: address, operator: address) acquires Store, BeneficiaryForOperator {
     assert_staking_contract_exists(staker, operator);
     let store = borrow_global_mut<Store>(staker);
     let staking_contract = simple_map::borrow_mut(&mut store.staking_contracts, &operator);
@@ -1684,7 +1846,7 @@ Distribute all unlocked (inactive) funds according to distribution shares.
     operator: address,
     staking_contract: &mut StakingContract,
     distribute_events: &mut EventHandle<DistributeEvent>,
-) {
+) acquires BeneficiaryForOperator {
     let pool_address = staking_contract.pool_address;
     let (_, inactive, _, pending_inactive) = stake::get_stake(pool_address);
     let total_potential_withdrawable = inactive + pending_inactive;
@@ -1705,7 +1867,11 @@ Distribute all unlocked (inactive) funds according to distribution shares.
         let recipient = *vector::borrow(&mut recipients, 0);
         let current_shares = pool_u64::shares(distribution_pool, recipient);
         let amount_to_distribute = pool_u64::redeem_shares(distribution_pool, recipient, current_shares);
-        coin::deposit(recipient, coin::extract(&mut coins, amount_to_distribute));
+        // If the recipient is the operator, send the commission to the beneficiary instead.
+        if (recipient == operator) {
+            recipient = beneficiary_for_operator(operator);
+        };
+        aptos_account::deposit_coins(recipient, coin::extract(&mut coins, amount_to_distribute));
 
         emit_event(
             distribute_events,
@@ -1715,7 +1881,7 @@ Distribute all unlocked (inactive) funds according to distribution shares.
 
     // In case there's any dust left, send them all to the staker.
     if (coin::value(&coins) > 0) {
-        coin::deposit(staker, coins);
+        aptos_account::deposit_coins(staker, coins);
         pool_u64::update_total_coins(distribution_pool, 0);
     } else {
         coin::destroy_zero(coins);
@@ -2157,6 +2323,23 @@ Staking_contract exists the stacker/operator pair.
 
 
 
+
+
+### Function `beneficiary_for_operator`
+
+
+
#[view]
+public fun beneficiary_for_operator(operator: address): address
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `create_staking_contract` @@ -2419,6 +2602,22 @@ Staking_contract exists the stacker/operator pair. + + +### Function `set_beneficiary_for_operator` + + +
public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address)
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `distribute` @@ -2605,4 +2804,182 @@ The guid_creation_num of the ccount resource is up to MAX_U64.
+The Store exists under the staker. +a staking_contract exists for the staker/operator pair. + + + + + +
schema ContractExistsAbortsIf {
+    staker: address;
+    operator: address;
+    aborts_if !exists<Store>(staker);
+    let staking_contracts = global<Store>(staker).staking_contracts;
+    aborts_if !simple_map::spec_contains_key(staking_contracts, operator);
+}
+
+ + + + + + + +
schema UpdateVoterSchema {
+    staker: address;
+    operator: address;
+    let store = global<Store>(staker);
+    let staking_contract = simple_map::spec_get(store.staking_contracts, operator);
+    let pool_address = staking_contract.pool_address;
+    aborts_if !exists<stake::StakePool>(pool_address);
+    aborts_if !exists<stake::StakePool>(staking_contract.owner_cap.pool_address);
+    include ContractExistsAbortsIf;
+}
+
+ + + + + + + +
schema WithdrawAbortsIf<CoinType> {
+    account: signer;
+    amount: u64;
+    let account_addr = signer::address_of(account);
+    let coin_store = global<coin::CoinStore<CoinType>>(account_addr);
+    let balance = coin_store.coin.value;
+    aborts_if !exists<coin::CoinStore<CoinType>>(account_addr);
+    aborts_if coin_store.frozen;
+    aborts_if balance < amount;
+}
+
+ + + + + + + +
schema GetStakingContractAmountsAbortsIf {
+    staking_contract: StakingContract;
+    let pool_address = staking_contract.pool_address;
+    let stake_pool = global<stake::StakePool>(pool_address);
+    let active = coin::value(stake_pool.active);
+    let pending_active = coin::value(stake_pool.pending_active);
+    let total_active_stake = active + pending_active;
+    let accumulated_rewards = total_active_stake - staking_contract.principal;
+    aborts_if !exists<stake::StakePool>(pool_address);
+    aborts_if active + pending_active > MAX_U64;
+    aborts_if total_active_stake < staking_contract.principal;
+    aborts_if accumulated_rewards * staking_contract.commission_percentage > MAX_U64;
+}
+
+ + + + + + + +
schema IncreaseLockupWithCapAbortsIf {
+    staker: address;
+    operator: address;
+    let store = global<Store>(staker);
+    let staking_contract = simple_map::spec_get(store.staking_contracts, operator);
+    let pool_address = staking_contract.owner_cap.pool_address;
+    aborts_if !stake::stake_pool_exists(pool_address);
+    aborts_if !exists<staking_config::StakingConfig>(@aptos_framework);
+    let config = global<staking_config::StakingConfig>(@aptos_framework);
+    let stake_pool = global<stake::StakePool>(pool_address);
+    let old_locked_until_secs = stake_pool.locked_until_secs;
+    let seconds = global<timestamp::CurrentTimeMicroseconds>(@aptos_framework).microseconds / timestamp::MICRO_CONVERSION_FACTOR;
+    let new_locked_until_secs = seconds + config.recurring_lockup_duration_secs;
+    aborts_if seconds + config.recurring_lockup_duration_secs > MAX_U64;
+    aborts_if old_locked_until_secs > new_locked_until_secs || old_locked_until_secs == new_locked_until_secs;
+    aborts_if !exists<timestamp::CurrentTimeMicroseconds>(@aptos_framework);
+    let post post_store = global<Store>(staker);
+    let post post_staking_contract = simple_map::spec_get(post_store.staking_contracts, operator);
+    let post post_stake_pool = global<stake::StakePool>(post_staking_contract.owner_cap.pool_address);
+    ensures post_stake_pool.locked_until_secs == new_locked_until_secs;
+}
+
+ + + + + + + +
schema CreateStakingContractWithCoinsAbortsIfAndEnsures {
+    staker: signer;
+    operator: address;
+    voter: address;
+    amount: u64;
+    commission_percentage: u64;
+    contract_creation_seed: vector<u8>;
+    aborts_if commission_percentage < 0 || commission_percentage > 100;
+    aborts_if !exists<staking_config::StakingConfig>(@aptos_framework);
+    let config = global<staking_config::StakingConfig>(@aptos_framework);
+    let min_stake_required = config.minimum_stake;
+    aborts_if amount < min_stake_required;
+    let staker_address = signer::address_of(staker);
+    let account = global<account::Account>(staker_address);
+    aborts_if !exists<Store>(staker_address) && !exists<account::Account>(staker_address);
+    aborts_if !exists<Store>(staker_address) && account.guid_creation_num + 9 >= account::MAX_GUID_CREATION_NUM;
+    ensures exists<Store>(staker_address);
+    let store = global<Store>(staker_address);
+    let staking_contracts = store.staking_contracts;
+    let seed_0 = bcs::to_bytes(staker_address);
+    let seed_1 = concat(concat(concat(seed_0, bcs::to_bytes(operator)), SALT), contract_creation_seed);
+    let resource_addr = account::spec_create_resource_address(staker_address, seed_1);
+    include CreateStakePoolAbortsIf {resource_addr};
+    let owner_cap = simple_map::spec_get(store.staking_contracts, operator).owner_cap;
+    let post post_store = global<Store>(staker_address);
+    let post post_staking_contracts = post_store.staking_contracts;
+}
+
+ + + + + + + +
schema PreconditionsInCreateContract {
+    requires exists<stake::ValidatorPerformance>(@aptos_framework);
+    requires exists<stake::ValidatorSet>(@aptos_framework);
+    requires exists<staking_config::StakingRewardsConfig>(@aptos_framework) || !std::features::spec_periodical_reward_rate_decrease_enabled();
+    requires exists<stake::ValidatorFees>(@aptos_framework);
+    requires exists<aptos_framework::timestamp::CurrentTimeMicroseconds>(@aptos_framework);
+    requires exists<stake::AptosCoinCapabilities>(@aptos_framework);
+}
+
+ + + + + + + +
schema CreateStakePoolAbortsIf {
+    resource_addr: address;
+    operator: address;
+    voter: address;
+    contract_creation_seed: vector<u8>;
+    let acc = global<account::Account>(resource_addr);
+    aborts_if exists<account::Account>(resource_addr) && (len(acc.signer_capability_offer.for.vec) != 0 || acc.sequence_number != 0);
+    aborts_if !exists<account::Account>(resource_addr) && len(bcs::to_bytes(resource_addr)) != 32;
+    aborts_if len(account::ZERO_AUTH_KEY) != 32;
+    aborts_if exists<stake::ValidatorConfig>(resource_addr);
+    let allowed = global<stake::AllowedValidators>(@aptos_framework);
+    aborts_if exists<stake::AllowedValidators>(@aptos_framework) && !contains(allowed.accounts, resource_addr);
+    aborts_if exists<stake::StakePool>(resource_addr);
+    aborts_if exists<stake::OwnerCapability>(resource_addr);
+    aborts_if exists<account::Account>(resource_addr) && acc.guid_creation_num + 12 >= account::MAX_GUID_CREATION_NUM;
+}
+
+ + [move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/doc/vesting.md b/aptos-move/framework/aptos-framework/doc/vesting.md index a07f0552649d4..7e7ea0a2514a4 100644 --- a/aptos-move/framework/aptos-framework/doc/vesting.md +++ b/aptos-move/framework/aptos-framework/doc/vesting.md @@ -89,6 +89,7 @@ withdrawable, admin can call admin_withdraw to withdraw all funds to the vesting - [Function `reset_beneficiary`](#0x1_vesting_reset_beneficiary) - [Function `set_management_role`](#0x1_vesting_set_management_role) - [Function `set_beneficiary_resetter`](#0x1_vesting_set_beneficiary_resetter) +- [Function `set_beneficiary_for_operator`](#0x1_vesting_set_beneficiary_for_operator) - [Function `get_role_holder`](#0x1_vesting_get_role_holder) - [Function `get_vesting_account_signer`](#0x1_vesting_get_vesting_account_signer) - [Function `get_vesting_account_signer_internal`](#0x1_vesting_get_vesting_account_signer_internal) @@ -133,6 +134,7 @@ withdrawable, admin can call admin_withdraw to withdraw all funds to the vesting - [Function `reset_beneficiary`](#@Specification_1_reset_beneficiary) - [Function `set_management_role`](#@Specification_1_set_management_role) - [Function `set_beneficiary_resetter`](#@Specification_1_set_beneficiary_resetter) + - [Function `set_beneficiary_for_operator`](#@Specification_1_set_beneficiary_for_operator) - [Function `get_role_holder`](#@Specification_1_get_role_holder) - [Function `get_vesting_account_signer`](#@Specification_1_get_vesting_account_signer) - [Function `get_vesting_account_signer_internal`](#@Specification_1_get_vesting_account_signer_internal) @@ -2460,6 +2462,34 @@ account. + + + + +## Function `set_beneficiary_for_operator` + +Set the beneficiary for the operator. + + +
public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address)
+
+ + + +
+Implementation + + +
public entry fun set_beneficiary_for_operator(
+    operator: &signer,
+    new_beneficiary: address,
+) {
+    staking_contract::set_beneficiary_for_operator(operator, new_beneficiary);
+}
+
+ + +
@@ -3465,6 +3495,22 @@ This address should be deterministic for the same admin and vesting contract cre + + +### Function `set_beneficiary_for_operator` + + +
public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address)
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `get_role_holder` diff --git a/aptos-move/framework/aptos-framework/sources/delegation_pool.move b/aptos-move/framework/aptos-framework/sources/delegation_pool.move index cb1d2e8de9451..30d1788eb8c4b 100644 --- a/aptos-move/framework/aptos-framework/sources/delegation_pool.move +++ b/aptos-move/framework/aptos-framework/sources/delegation_pool.move @@ -119,11 +119,13 @@ module aptos_framework::delegation_pool { use aptos_std::smart_table::{Self, SmartTable}; use aptos_framework::account; + use aptos_framework::aptos_account; use aptos_framework::aptos_coin::AptosCoin; use aptos_framework::aptos_governance; use aptos_framework::coin; - use aptos_framework::event::{Self, EventHandle}; + use aptos_framework::event::{Self, EventHandle, emit}; use aptos_framework::stake; + use aptos_framework::stake::get_operator; use aptos_framework::staking_config; use aptos_framework::timestamp; @@ -183,6 +185,12 @@ module aptos_framework::delegation_pool { /// The stake pool has already voted on the proposal before enabling partial governance voting on this delegation pool. const EALREADY_VOTED_BEFORE_ENABLE_PARTIAL_VOTING: u64 = 17; + /// The account is not the operator of the stake pool. + const ENOT_OPERATOR: u64 = 18; + + /// Chaning beneficiaries for operators is not supported. + const EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED: u64 = 19; + const MAX_U64: u64 = 18446744073709551615; /// Maximum operator percentage fee(of double digit precision): 22.85% is represented as 2285 @@ -295,6 +303,10 @@ module aptos_framework::delegation_pool { delegate_voting_power_events: EventHandle, } + struct BeneficiaryForOperator has key { + beneficiary_for_operator: address, + } + struct AddStakeEvent has drop, store { pool_address: address, delegator_address: address, @@ -327,6 +339,15 @@ module aptos_framework::delegation_pool { commission_pending_inactive: u64, } + #[event] + struct DistributeCommission has drop, store { + pool_address: address, + operator: address, + beneficiary: address, + commission_active: u64, + commission_pending_inactive: u64, + } + struct VoteEvent has drop, store { voter: address, proposal_id: u64, @@ -347,6 +368,13 @@ module aptos_framework::delegation_pool { voter: address, } + #[event] + struct SetBeneficiaryForOperator has drop, store { + operator: address, + old_beneficiary: address, + new_beneficiary: address, + } + #[view] /// Return whether supplied address `addr` is owner of a delegation pool. public fun owner_cap_exists(addr: address): bool { @@ -444,7 +472,7 @@ module aptos_framework::delegation_pool { #[view] /// Return total stake owned by `delegator_address` within delegation pool `pool_address` /// in each of its individual states: (`active`,`inactive`,`pending_inactive`) - public fun get_stake(pool_address: address, delegator_address: address): (u64, u64, u64) acquires DelegationPool { + public fun get_stake(pool_address: address, delegator_address: address): (u64, u64, u64) acquires DelegationPool, BeneficiaryForOperator { assert_delegation_pool_exists(pool_address); let pool = borrow_global(pool_address); let ( @@ -484,7 +512,7 @@ module aptos_framework::delegation_pool { // operator rewards are actually used to buy shares which is introducing // some imprecision (received stake would be slightly less) // but adding rewards onto the existing stake is still a good approximation - if (delegator_address == stake::get_operator(pool_address)) { + if (delegator_address == beneficiary_for_operator(get_operator(pool_address))) { active = active + commission_active; // in-flight pending_inactive commission can coexist with already inactive withdrawal if (lockup_cycle_ended) { @@ -532,7 +560,7 @@ module aptos_framework::delegation_pool { #[view] /// Return the total voting power of a delegator in a delegation pool. This function syncs DelegationPool to the /// latest state. - public fun calculate_and_update_voter_total_voting_power(pool_address: address, voter: address): u64 acquires DelegationPool, GovernanceRecords { + public fun calculate_and_update_voter_total_voting_power(pool_address: address, voter: address): u64 acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert_partial_governance_voting_enabled(pool_address); // Delegation pool need to be synced to explain rewards(which could change the coin amount) and // commission(which could cause share transfer). @@ -546,7 +574,7 @@ module aptos_framework::delegation_pool { #[view] /// Return the remaining voting power of a delegator in a delegation pool on a proposal. This function syncs DelegationPool to the /// latest state. - public fun calculate_and_update_remaining_voting_power(pool_address: address, voter_address: address, proposal_id: u64): u64 acquires DelegationPool, GovernanceRecords { + public fun calculate_and_update_remaining_voting_power(pool_address: address, voter_address: address, proposal_id: u64): u64 acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert_partial_governance_voting_enabled(pool_address); // If the whole stake pool has no voting power(e.g. it has already voted before partial // governance voting flag is enabled), the delegator also has no voting power. @@ -587,7 +615,7 @@ module aptos_framework::delegation_pool { owner: &signer, operator_commission_percentage: u64, delegation_pool_creation_seed: vector, - ) acquires DelegationPool, GovernanceRecords { + ) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert!(features::delegation_pools_enabled(), error::invalid_state(EDELEGATION_POOLS_DISABLED)); let owner_address = signer::address_of(owner); assert!(!owner_cap_exists(owner_address), error::already_exists(EOWNER_CAP_ALREADY_EXISTS)); @@ -634,11 +662,21 @@ module aptos_framework::delegation_pool { } } + #[view] + /// Return the beneficiary address of the operator. + public fun beneficiary_for_operator(operator: address): address acquires BeneficiaryForOperator { + if (exists(operator)) { + return borrow_global(operator).beneficiary_for_operator + } else { + operator + } + } + /// Enable partial governance voting on a stake pool. The voter of this stake pool will be managed by this module. /// THe existing voter will be replaced. The function is permissionless. public entry fun enable_partial_governance_voting( pool_address: address, - ) acquires DelegationPool, GovernanceRecords { + ) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert!(features::partial_governance_voting_enabled(), error::invalid_state(EDISABLED_FUNCTION)); assert!(features::delegation_pool_partial_governance_voting_enabled(), error::invalid_state(EDISABLED_FUNCTION)); assert_delegation_pool_exists(pool_address); @@ -667,7 +705,7 @@ module aptos_framework::delegation_pool { /// 2. The delegation pool's lockup period ends after the voting period of the proposal. /// 3. The voter still has spare voting power on this proposal. /// 4. The delegation pool never votes on the proposal before enabling partial governance voting. - public entry fun vote(voter: &signer, pool_address: address, proposal_id: u64, voting_power: u64, should_pass: bool) acquires DelegationPool, GovernanceRecords { + public entry fun vote(voter: &signer, pool_address: address, proposal_id: u64, voting_power: u64, should_pass: bool) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert_partial_governance_voting_enabled(pool_address); // synchronize delegation and stake pools before any user operation. synchronize_delegation_pool(pool_address); @@ -710,7 +748,7 @@ module aptos_framework::delegation_pool { metadata_location: vector, metadata_hash: vector, is_multi_step_proposal: bool, - ) acquires DelegationPool, GovernanceRecords { + ) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert_partial_governance_voting_enabled(pool_address); // synchronize delegation and stake pools before any user operation @@ -956,7 +994,7 @@ module aptos_framework::delegation_pool { public entry fun set_operator( owner: &signer, new_operator: address - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { let pool_address = get_owned_pool_address(signer::address_of(owner)); // synchronize delegation and stake pools before any user operation // ensure the old operator is paid its uncommitted commission rewards @@ -964,11 +1002,36 @@ module aptos_framework::delegation_pool { stake::set_operator(&retrieve_stake_pool_owner(borrow_global(pool_address)), new_operator); } + /// Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new + /// beneficiary. To ensures payment to the current beneficiary, one should first call `synchronize_delegation_pool` + /// before switching the beneficiary. An operator can set one beneficiary for delegation pools, not a separate + /// one for each pool. + public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address) acquires BeneficiaryForOperator { + assert!(features::operator_beneficiary_change_enabled(), std::error::invalid_state( + EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED + )); + // The beneficiay address of an operator is stored under the operator's address. + // So, the operator does not need to be validated with respect to a staking pool. + let operator_addr = signer::address_of(operator); + let old_beneficiary = beneficiary_for_operator(operator_addr); + if (exists(operator_addr)) { + borrow_global_mut(operator_addr).beneficiary_for_operator = new_beneficiary; + } else { + move_to(operator, BeneficiaryForOperator { beneficiary_for_operator: new_beneficiary }); + }; + + emit(SetBeneficiaryForOperator { + operator: operator_addr, + old_beneficiary, + new_beneficiary, + }); + } + /// Allows an owner to change the delegated voter of the underlying stake pool. public entry fun set_delegated_voter( owner: &signer, new_voter: address - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { // No one can change delegated_voter once the partial governance voting feature is enabled. assert!(!features::delegation_pool_partial_governance_voting_enabled(), error::invalid_state(EDEPRECATED_FUNCTION)); let pool_address = get_owned_pool_address(signer::address_of(owner)); @@ -983,7 +1046,7 @@ module aptos_framework::delegation_pool { delegator: &signer, pool_address: address, new_voter: address - ) acquires DelegationPool, GovernanceRecords { + ) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert_partial_governance_voting_enabled(pool_address); // synchronize delegation and stake pools before any user operation @@ -1030,7 +1093,7 @@ module aptos_framework::delegation_pool { } /// Add `amount` of coins to the delegation pool `pool_address`. - public entry fun add_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords { + public entry fun add_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { // short-circuit if amount to add is 0 so no event is emitted if (amount == 0) { return }; // synchronize delegation and stake pools before any user operation @@ -1043,7 +1106,7 @@ module aptos_framework::delegation_pool { let delegator_address = signer::address_of(delegator); // stake the entire amount to the stake pool - coin::transfer(delegator, pool_address, amount); + aptos_account::transfer(delegator, pool_address, amount); stake::add_stake(&retrieve_stake_pool_owner(pool), amount); // but buy shares for delegator just for the remaining amount after fee @@ -1069,7 +1132,7 @@ module aptos_framework::delegation_pool { /// Unlock `amount` from the active + pending_active stake of `delegator` or /// at most how much active stake there is on the stake pool. - public entry fun unlock(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords { + public entry fun unlock(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { // short-circuit if amount to unlock is 0 so no event is emitted if (amount == 0) { return }; @@ -1107,7 +1170,7 @@ module aptos_framework::delegation_pool { } /// Move `amount` of coins from pending_inactive to active. - public entry fun reactivate_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords { + public entry fun reactivate_stake(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { // short-circuit if amount to reactivate is 0 so no event is emitted if (amount == 0) { return }; // synchronize delegation and stake pools before any user operation @@ -1141,7 +1204,7 @@ module aptos_framework::delegation_pool { } /// Withdraw `amount` of owned inactive stake from the delegation pool at `pool_address`. - public entry fun withdraw(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords { + public entry fun withdraw(delegator: &signer, pool_address: address, amount: u64) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert!(amount > 0, error::invalid_argument(EWITHDRAW_ZERO_STAKE)); // synchronize delegation and stake pools before any user operation synchronize_delegation_pool(pool_address); @@ -1190,7 +1253,7 @@ module aptos_framework::delegation_pool { // no excess stake if `stake::withdraw` does not inactivate at all stake::withdraw(stake_pool_owner, amount); }; - coin::transfer(stake_pool_owner, delegator_address, amount); + aptos_account::transfer(stake_pool_owner, delegator_address, amount); // commit withdrawal of possibly inactive stake to the `total_coins_inactive` // known by the delegation pool in order to not mistake it for slashing at next synchronization @@ -1436,7 +1499,7 @@ module aptos_framework::delegation_pool { /// Synchronize delegation and stake pools: distribute yet-undetected rewards to the corresponding internal /// shares pools, assign commission to operator and eventually prepare delegation pool for a new lockup cycle. - public entry fun synchronize_delegation_pool(pool_address: address) acquires DelegationPool, GovernanceRecords { + public entry fun synchronize_delegation_pool(pool_address: address) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { assert_delegation_pool_exists(pool_address); let pool = borrow_global_mut(pool_address); let ( @@ -1470,9 +1533,9 @@ module aptos_framework::delegation_pool { ); // reward operator its commission out of uncommitted active rewards (`add_stake` fees already excluded) - buy_in_active_shares(pool, stake::get_operator(pool_address), commission_active); + buy_in_active_shares(pool, beneficiary_for_operator(stake::get_operator(pool_address)), commission_active); // reward operator its commission out of uncommitted pending_inactive rewards - buy_in_pending_inactive_shares(pool, stake::get_operator(pool_address), commission_pending_inactive); + buy_in_pending_inactive_shares(pool, beneficiary_for_operator(stake::get_operator(pool_address)), commission_pending_inactive); event::emit_event( &mut pool.distribute_commission_events, @@ -1484,6 +1547,16 @@ module aptos_framework::delegation_pool { }, ); + if (features::operator_beneficiary_change_enabled()) { + emit(DistributeCommission { + pool_address, + operator: stake::get_operator(pool_address), + beneficiary: beneficiary_for_operator(stake::get_operator(pool_address)), + commission_active, + commission_pending_inactive, + }) + }; + // advance lockup cycle on delegation pool if already ended on stake pool (AND stake explicitly inactivated) if (lockup_cycle_ended) { // capture inactive coins over all ended lockup cycles (including this ending one) @@ -1627,6 +1700,12 @@ module aptos_framework::delegation_pool { #[test_only] const DELEGATION_POOLS: u64 = 11; + #[test_only] + const MODULE_EVENT: u64 = 26; + + #[test_only] + const OPERATOR_BENEFICIARY_CHANGE: u64 = 39; + #[test_only] public fun end_aptos_epoch() { stake::end_epoch(); // additionally forwards EPOCH_DURATION seconds @@ -1638,7 +1717,7 @@ module aptos_framework::delegation_pool { initialize_for_test_custom( aptos_framework, 100 * ONE_APT, - 10000 * ONE_APT, + 10000000 * ONE_APT, LOCKUP_CYCLE_SECONDS, true, 1, @@ -1652,7 +1731,7 @@ module aptos_framework::delegation_pool { initialize_for_test_custom( aptos_framework, 100 * ONE_APT, - 10000 * ONE_APT, + 10000000 * ONE_APT, LOCKUP_CYCLE_SECONDS, true, 0, @@ -1684,7 +1763,7 @@ module aptos_framework::delegation_pool { voting_power_increase_limit, ); reconfiguration::initialize_for_test(aptos_framework); - features::change_feature_flags(aptos_framework, vector[DELEGATION_POOLS], vector[]); + features::change_feature_flags(aptos_framework, vector[DELEGATION_POOLS, MODULE_EVENT, OPERATOR_BENEFICIARY_CHANGE], vector[]); } #[test_only] @@ -1693,7 +1772,7 @@ module aptos_framework::delegation_pool { amount: u64, should_join_validator_set: bool, should_end_epoch: bool, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_test_validator_custom(validator, amount, should_join_validator_set, should_end_epoch, 0); } @@ -1704,7 +1783,7 @@ module aptos_framework::delegation_pool { should_join_validator_set: bool, should_end_epoch: bool, commission_percentage: u64, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { let validator_address = signer::address_of(validator); if (!account::exists_at(validator_address)) { account::create_account_for_test(validator_address); @@ -1734,7 +1813,7 @@ module aptos_framework::delegation_pool { delegator: &signer, pool_address: address, amount: u64 - ) acquires DelegationPool, GovernanceRecords { + ) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { synchronize_delegation_pool(pool_address); let pool = borrow_global_mut(pool_address); @@ -1750,7 +1829,7 @@ module aptos_framework::delegation_pool { public entry fun test_delegation_pools_disabled( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPool, GovernanceRecords { + ) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); features::change_feature_flags(aptos_framework, vector[], vector[DELEGATION_POOLS]); @@ -1761,7 +1840,7 @@ module aptos_framework::delegation_pool { public entry fun test_set_operator_and_delegated_voter( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); let validator_address = signer::address_of(validator); @@ -1783,7 +1862,7 @@ module aptos_framework::delegation_pool { public entry fun test_cannot_set_operator( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); // account does not own any delegation pool set_operator(validator, @0x111); @@ -1794,7 +1873,7 @@ module aptos_framework::delegation_pool { public entry fun test_cannot_set_delegated_voter( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); // account does not own any delegation pool set_delegated_voter(validator, @0x112); @@ -1805,7 +1884,7 @@ module aptos_framework::delegation_pool { public entry fun test_already_owns_delegation_pool( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPool, GovernanceRecords { + ) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_delegation_pool(validator, 0, x"00"); initialize_delegation_pool(validator, 0, x"01"); @@ -1816,7 +1895,7 @@ module aptos_framework::delegation_pool { public entry fun test_cannot_withdraw_zero_stake( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_delegation_pool(validator, 0, x"00"); withdraw(validator, get_owned_pool_address(signer::address_of(validator)), 0); @@ -1826,7 +1905,7 @@ module aptos_framework::delegation_pool { public entry fun test_initialize_delegation_pool( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); let validator_address = signer::address_of(validator); @@ -1853,7 +1932,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, delegator2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test_custom( aptos_framework, 100 * ONE_APT, @@ -1996,7 +2075,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1000 * ONE_APT, true, false); @@ -2059,7 +2138,7 @@ module aptos_framework::delegation_pool { public entry fun test_add_stake_min_amount( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, MIN_COINS_ON_SHARES_POOL - 1, false, false); } @@ -2068,7 +2147,7 @@ module aptos_framework::delegation_pool { public entry fun test_add_stake_single( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1000 * ONE_APT, false, false); @@ -2158,7 +2237,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1000 * ONE_APT, true, true); @@ -2220,7 +2299,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 100 * ONE_APT, true, true); @@ -2390,7 +2469,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, delegator2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 200 * ONE_APT, true, true); @@ -2497,7 +2576,7 @@ module aptos_framework::delegation_pool { public entry fun test_reactivate_stake_single( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 200 * ONE_APT, true, true); @@ -2565,7 +2644,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1000 * ONE_APT, true, true); @@ -2641,7 +2720,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1200 * ONE_APT, true, true); @@ -2747,7 +2826,7 @@ module aptos_framework::delegation_pool { public entry fun test_active_stake_rewards( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1000 * ONE_APT, true, true); @@ -2820,7 +2899,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 200 * ONE_APT, true, true); @@ -2880,7 +2959,7 @@ module aptos_framework::delegation_pool { public entry fun test_pending_inactive_stake_rewards( aptos_framework: &signer, validator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1000 * ONE_APT, true, true); @@ -2927,7 +3006,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, delegator2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 1000 * ONE_APT, true, true); @@ -3006,7 +3085,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, delegator2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); let validator_address = signer::address_of(validator); @@ -3155,7 +3234,7 @@ module aptos_framework::delegation_pool { old_operator: &signer, delegator: &signer, new_operator: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); let old_operator_address = signer::address_of(old_operator); @@ -3211,13 +3290,87 @@ module aptos_framework::delegation_pool { assert_delegation(new_operator_address, pool_address, 26050290, 0, 26050290); } + #[test(aptos_framework = @aptos_framework, operator1 = @0x123, delegator = @0x010, beneficiary = @0x020, operator2 = @0x030)] + public entry fun test_set_beneficiary_for_operator( + aptos_framework: &signer, + operator1: &signer, + delegator: &signer, + beneficiary: &signer, + operator2: &signer, + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { + initialize_for_test(aptos_framework); + + let operator1_address = signer::address_of(operator1); + aptos_account::create_account(operator1_address); + + let operator2_address = signer::address_of(operator2); + aptos_account::create_account(operator2_address); + + let beneficiary_address = signer::address_of(beneficiary); + aptos_account::create_account(beneficiary_address); + + // create delegation pool of commission fee 12.65% + initialize_delegation_pool(operator1, 1265, vector::empty()); + let pool_address = get_owned_pool_address(operator1_address); + assert!(stake::get_operator(pool_address) == operator1_address, 0); + assert!(beneficiary_for_operator(operator1_address) == operator1_address, 0); + + let delegator_address = signer::address_of(delegator); + account::create_account_for_test(delegator_address); + + stake::mint(delegator, 2000000 * ONE_APT); + add_stake(delegator, pool_address, 2000000 * ONE_APT); + unlock(delegator, pool_address, 1000000 * ONE_APT); + + // activate validator + stake::rotate_consensus_key(operator1, pool_address, CONSENSUS_KEY_1, CONSENSUS_POP_1); + stake::join_validator_set(operator1, pool_address); + end_aptos_epoch(); + + // produce active and pending_inactive rewards + end_aptos_epoch(); + stake::assert_stake_pool(pool_address, 101000000000000, 0, 0, 101000000000000); + assert_delegation(operator1_address, pool_address, 126500000000, 0, 126500000000); + end_aptos_epoch(); + stake::assert_stake_pool(pool_address, 102010000000000, 0, 0, 102010000000000); + assert_delegation(operator1_address, pool_address, 254265000000, 0, 254265000000); + timestamp::fast_forward_seconds(LOCKUP_CYCLE_SECONDS); + end_aptos_epoch(); + + withdraw(operator1, pool_address, ONE_APT); + assert!(coin::balance(operator1_address) == ONE_APT - 1, 0); + + set_beneficiary_for_operator(operator1, beneficiary_address); + assert!(beneficiary_for_operator(operator1_address) == beneficiary_address, 0); + end_aptos_epoch(); + + unlock(beneficiary, pool_address, ONE_APT); + timestamp::fast_forward_seconds(LOCKUP_CYCLE_SECONDS); + end_aptos_epoch(); + + withdraw(beneficiary, pool_address, ONE_APT); + assert!(coin::balance(beneficiary_address) == ONE_APT - 1, 0); + assert!(coin::balance(operator1_address) == ONE_APT - 1, 0); + + // switch operator to operator2. The rewards should go to operator2 not to the beneficiay of operator1. + set_operator(operator1, operator2_address); + end_aptos_epoch(); + unlock(operator2, pool_address, ONE_APT); + timestamp::fast_forward_seconds(LOCKUP_CYCLE_SECONDS); + end_aptos_epoch(); + + withdraw(operator2, pool_address, ONE_APT); + assert!(coin::balance(beneficiary_address) == ONE_APT - 1, 0); + assert!(coin::balance(operator2_address) == ONE_APT - 1, 0); + } + #[test(aptos_framework = @aptos_framework, validator = @0x123, delegator1 = @0x010, delegator2 = @0x020)] public entry fun test_min_stake_is_preserved( aptos_framework: &signer, validator: &signer, delegator1: &signer, delegator2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); initialize_test_validator(validator, 100 * ONE_APT, true, false); @@ -3324,7 +3477,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, // delegator2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); aptos_governance::initialize_for_test( aptos_framework, @@ -3369,7 +3522,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator1: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test(aptos_framework); aptos_governance::initialize_for_test( aptos_framework, @@ -3417,7 +3570,7 @@ module aptos_framework::delegation_pool { delegator2: &signer, voter1: &signer, voter2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test_no_reward(aptos_framework); aptos_governance::initialize_for_test( aptos_framework, @@ -3567,7 +3720,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, voter1: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test_no_reward(aptos_framework); aptos_governance::initialize_for_test( aptos_framework, @@ -3632,7 +3785,7 @@ module aptos_framework::delegation_pool { delegator2: &signer, voter1: &signer, voter2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test_custom( aptos_framework, 100 * ONE_APT, @@ -3722,7 +3875,7 @@ module aptos_framework::delegation_pool { delegator2: &signer, voter1: &signer, voter2: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { // partial voing hasn't been enabled yet. A proposal has been created by the validator. let proposal1_id = setup_vote(aptos_framework, validator, false); @@ -3799,7 +3952,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, voter1: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { // partial voing hasn't been enabled yet. A proposal has been created by the validator. let proposal1_id = setup_vote(aptos_framework, validator, false); @@ -3835,7 +3988,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, voter1: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { // partial voing hasn't been enabled yet. A proposal has been created by the validator. let proposal1_id = setup_vote(aptos_framework, validator, false); @@ -3873,7 +4026,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, delegator1: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { // partial voing hasn't been enabled yet. A proposal has been created by the validator. let proposal1_id = setup_vote(aptos_framework, validator, true); @@ -3892,7 +4045,7 @@ module aptos_framework::delegation_pool { validator: &signer, delegator1: &signer, voter1: &signer, - ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { // partial voing hasn't been enabled yet. A proposal has been created by the validator. setup_vote(aptos_framework, validator, true); @@ -3918,7 +4071,7 @@ module aptos_framework::delegation_pool { active_stake: u64, inactive_stake: u64, pending_inactive_stake: u64, - ) acquires DelegationPool { + ) acquires DelegationPool, BeneficiaryForOperator { let (actual_active, actual_inactive, actual_pending_inactive) = get_stake(pool_address, delegator_address); assert!(actual_active == active_stake, actual_active); assert!(actual_inactive == inactive_stake, actual_inactive); @@ -3967,7 +4120,7 @@ module aptos_framework::delegation_pool { aptos_framework: &signer, validator: &signer, enable_partial_voting: bool, - ): u64 acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + ): u64 acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator { initialize_for_test_no_reward(aptos_framework); aptos_governance::initialize_for_test( aptos_framework, diff --git a/aptos-move/framework/aptos-framework/sources/staking_contract.move b/aptos-move/framework/aptos-framework/sources/staking_contract.move index 84a061939f4c1..1b28d37f1c634 100644 --- a/aptos-move/framework/aptos-framework/sources/staking_contract.move +++ b/aptos-move/framework/aptos-framework/sources/staking_contract.move @@ -27,6 +27,7 @@ module aptos_framework::staking_contract { use std::bcs; use std::error; + use std::features; use std::signer; use std::vector; @@ -37,7 +38,7 @@ module aptos_framework::staking_contract { use aptos_framework::aptos_account; use aptos_framework::aptos_coin::AptosCoin; use aptos_framework::coin::{Self, Coin}; - use aptos_framework::event::{EventHandle, emit_event}; + use aptos_framework::event::{EventHandle, emit, emit_event}; use aptos_framework::stake::{Self, OwnerCapability}; use aptos_framework::staking_config; @@ -57,8 +58,10 @@ module aptos_framework::staking_contract { const ESTAKING_CONTRACT_ALREADY_EXISTS: u64 = 6; /// Not enough active stake to withdraw. Some stake might still pending and will be active in the next epoch. const EINSUFFICIENT_ACTIVE_STAKE_TO_WITHDRAW: u64 = 7; - /// Caller must be either the staker or operator. - const ENOT_STAKER_OR_OPERATOR: u64 = 8; + /// Caller must be either the staker, operator, or beneficiary. + const ENOT_STAKER_OR_OPERATOR_OR_BENEFICIARY: u64 = 8; + /// Chaning beneficiaries for operators is not supported. + const EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED: u64 = 9; /// Maximum number of distributions a stake pool can support. const MAXIMUM_PENDING_DISTRIBUTIONS: u64 = 20; @@ -95,6 +98,10 @@ module aptos_framework::staking_contract { distribute_events: EventHandle, } + struct BeneficiaryForOperator has key { + beneficiary_for_operator: address, + } + struct UpdateCommissionEvent has drop, store { staker: address, operator: address, @@ -166,6 +173,13 @@ module aptos_framework::staking_contract { amount: u64, } + #[event] + struct SetBeneficiaryForOperator has drop, store { + operator: address, + old_beneficiary: address, + new_beneficiary: address, + } + #[view] /// Return the address of the underlying stake pool for the staking contract between the provided staker and /// operator. @@ -234,6 +248,16 @@ module aptos_framework::staking_contract { simple_map::contains_key(&store.staking_contracts, &operator) } + #[view] + /// Return the beneficiary address of the operator. + public fun beneficiary_for_operator(operator: address): address acquires BeneficiaryForOperator { + if (exists(operator)) { + return borrow_global(operator).beneficiary_for_operator + } else { + operator + } + } + #[view] /// Return the address of the stake pool to be created with the provided staker, operator and seed. public fun get_expected_stake_pool_address( @@ -375,7 +399,7 @@ module aptos_framework::staking_contract { /// Convenience function to allow a staker to update the commission percentage paid to the operator. /// TODO: fix the typo in function name. commision -> commission - public entry fun update_commision(staker: &signer, operator: address, new_commission_percentage: u64) acquires Store, StakingGroupUpdateCommissionEvent { + public entry fun update_commision(staker: &signer, operator: address, new_commission_percentage: u64) acquires Store, StakingGroupUpdateCommissionEvent, BeneficiaryForOperator { assert!( new_commission_percentage >= 0 && new_commission_percentage <= 100, error::invalid_argument(EINVALID_COMMISSION_PERCENTAGE), @@ -407,10 +431,13 @@ module aptos_framework::staking_contract { /// Unlock commission amount from the stake pool. Operator needs to wait for the amount to become withdrawable /// at the end of the stake pool's lockup period before they can actually can withdraw_commission. /// - /// Only staker or operator can call this. - public entry fun request_commission(account: &signer, staker: address, operator: address) acquires Store { + /// Only staker, operator or beneficiary can call this. + public entry fun request_commission(account: &signer, staker: address, operator: address) acquires Store, BeneficiaryForOperator { let account_addr = signer::address_of(account); - assert!(account_addr == staker || account_addr == operator, error::unauthenticated(ENOT_STAKER_OR_OPERATOR)); + assert!( + account_addr == staker || account_addr == operator || account_addr == beneficiary_for_operator(operator), + error::unauthenticated(ENOT_STAKER_OR_OPERATOR_OR_BENEFICIARY) + ); assert_staking_contract_exists(staker, operator); let store = borrow_global_mut(staker); @@ -465,7 +492,7 @@ module aptos_framework::staking_contract { /// Staker can call this to request withdrawal of part or all of their staking_contract. /// This also triggers paying commission to the operator for accounting simplicity. - public entry fun unlock_stake(staker: &signer, operator: address, amount: u64) acquires Store { + public entry fun unlock_stake(staker: &signer, operator: address, amount: u64) acquires Store, BeneficiaryForOperator { // Short-circuit if amount is 0. if (amount == 0) return; @@ -511,7 +538,7 @@ module aptos_framework::staking_contract { } /// Unlock all accumulated rewards since the last recorded principals. - public entry fun unlock_rewards(staker: &signer, operator: address) acquires Store { + public entry fun unlock_rewards(staker: &signer, operator: address) acquires Store, BeneficiaryForOperator { let staker_address = signer::address_of(staker); assert_staking_contract_exists(staker_address, operator); @@ -526,7 +553,7 @@ module aptos_framework::staking_contract { staker: &signer, old_operator: address, new_operator: address, - ) acquires Store { + ) acquires Store, BeneficiaryForOperator { let staker_address = signer::address_of(staker); assert_staking_contract_exists(staker_address, old_operator); @@ -540,7 +567,7 @@ module aptos_framework::staking_contract { old_operator: address, new_operator: address, new_commission_percentage: u64, - ) acquires Store { + ) acquires Store, BeneficiaryForOperator { let staker_address = signer::address_of(staker); assert_staking_contract_exists(staker_address, old_operator); @@ -577,9 +604,33 @@ module aptos_framework::staking_contract { ); } + /// Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new + /// beneficiary. To ensures payment to the current beneficiary, one should first call `distribute` before switching + /// the beneficiary. An operator can set one beneficiary for staking contract pools, not a separate one for each pool. + public entry fun set_beneficiary_for_operator(operator: &signer, new_beneficiary: address) acquires BeneficiaryForOperator { + assert!(features::operator_beneficiary_change_enabled(), std::error::invalid_state( + EOPERATOR_BENEFICIARY_CHANGE_NOT_SUPPORTED + )); + // The beneficiay address of an operator is stored under the operator's address. + // So, the operator does not need to be validated with respect to a staking pool. + let operator_addr = signer::address_of(operator); + let old_beneficiary = beneficiary_for_operator(operator_addr); + if (exists(operator_addr)) { + borrow_global_mut(operator_addr).beneficiary_for_operator = new_beneficiary; + } else { + move_to(operator, BeneficiaryForOperator { beneficiary_for_operator: new_beneficiary }); + }; + + emit(SetBeneficiaryForOperator { + operator: operator_addr, + old_beneficiary, + new_beneficiary, + }); + } + /// Allow anyone to distribute already unlocked funds. This does not affect reward compounding and therefore does /// not need to be restricted to just the staker or operator. - public entry fun distribute(staker: address, operator: address) acquires Store { + public entry fun distribute(staker: address, operator: address) acquires Store, BeneficiaryForOperator { assert_staking_contract_exists(staker, operator); let store = borrow_global_mut(staker); let staking_contract = simple_map::borrow_mut(&mut store.staking_contracts, &operator); @@ -592,7 +643,7 @@ module aptos_framework::staking_contract { operator: address, staking_contract: &mut StakingContract, distribute_events: &mut EventHandle, - ) { + ) acquires BeneficiaryForOperator { let pool_address = staking_contract.pool_address; let (_, inactive, _, pending_inactive) = stake::get_stake(pool_address); let total_potential_withdrawable = inactive + pending_inactive; @@ -613,6 +664,10 @@ module aptos_framework::staking_contract { let recipient = *vector::borrow(&mut recipients, 0); let current_shares = pool_u64::shares(distribution_pool, recipient); let amount_to_distribute = pool_u64::redeem_shares(distribution_pool, recipient, current_shares); + // If the recipient is the operator, send the commission to the beneficiary instead. + if (recipient == operator) { + recipient = beneficiary_for_operator(operator); + }; aptos_account::deposit_coins(recipient, coin::extract(&mut coins, amount_to_distribute)); emit_event( @@ -776,6 +831,12 @@ module aptos_framework::staking_contract { #[test_only] const MAXIMUM_STAKE: u64 = 100000000000000000; // 1B APT coins with 8 decimals. + #[test_only] + const MODULE_EVENT: u64 = 26; + + #[test_only] + const OPERATOR_BENEFICIARY_CHANGE: u64 = 39; + #[test_only] public fun setup(aptos_framework: &signer, staker: &signer, operator: &signer, initial_balance: u64) { // Reward rate of 0.1% per epoch. @@ -806,10 +867,11 @@ module aptos_framework::staking_contract { // Voter is initially set to operator but then updated to be staker. create_staking_contract(staker, operator_address, operator_address, amount, commission, vector::empty()); + std::features::change_feature_flags(aptos_framework, vector[MODULE_EVENT, OPERATOR_BENEFICIARY_CHANGE], vector[]); } #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234)] - public entry fun test_end_to_end(aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store { + public entry fun test_end_to_end(aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, BeneficiaryForOperator { setup_staking_contract(aptos_framework, staker, operator, INITIAL_BALANCE, 10); let staker_address = signer::address_of(staker); let operator_address = signer::address_of(operator); @@ -912,7 +974,7 @@ module aptos_framework::staking_contract { #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234)] public entry fun test_operator_cannot_request_same_commission_multiple_times( - aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store { + aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, BeneficiaryForOperator { setup_staking_contract(aptos_framework, staker, operator, INITIAL_BALANCE, 10); let staker_address = signer::address_of(staker); let operator_address = signer::address_of(operator); @@ -940,7 +1002,7 @@ module aptos_framework::staking_contract { #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234)] public entry fun test_unlock_rewards( - aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store { + aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, BeneficiaryForOperator { setup_staking_contract(aptos_framework, staker, operator, INITIAL_BALANCE, 10); let staker_address = signer::address_of(staker); let operator_address = signer::address_of(operator); @@ -1037,7 +1099,7 @@ module aptos_framework::staking_contract { staker: &signer, operator_1: &signer, operator_2: &signer, - ) acquires Store { + ) acquires Store, BeneficiaryForOperator { setup_staking_contract(aptos_framework, staker, operator_1, INITIAL_BALANCE, 10); account::create_account_for_test(signer::address_of(operator_2)); stake::mint(operator_2, INITIAL_BALANCE); @@ -1114,7 +1176,7 @@ module aptos_framework::staking_contract { staker: &signer, operator_1: &signer, operator_2: &signer, - ) acquires Store { + ) acquires Store, BeneficiaryForOperator { setup_staking_contract(aptos_framework, staker, operator_1, INITIAL_BALANCE, 10); let staker_address = signer::address_of(staker); let operator_1_address = signer::address_of(operator_1); @@ -1128,9 +1190,95 @@ module aptos_framework::staking_contract { assert!(commission_percentage(staker_address, operator_2_address) == 10, 2); } + #[test(aptos_framework = @0x1, staker = @0x123, operator1 = @0x234, beneficiary = @0x345, operator2 = @0x456)] + public entry fun test_operator_can_set_beneficiary( + aptos_framework: &signer, + staker: &signer, + operator1: &signer, + beneficiary: &signer, + operator2: &signer, + ) acquires Store, BeneficiaryForOperator { + setup_staking_contract(aptos_framework, staker, operator1, INITIAL_BALANCE, 10); + let staker_address = signer::address_of(staker); + let operator1_address = signer::address_of(operator1); + let operator2_address = signer::address_of(operator2); + let beneficiary_address = signer::address_of(beneficiary); + + // account::create_account_for_test(beneficiary_address); + aptos_framework::aptos_account::create_account(beneficiary_address); + assert_staking_contract_exists(staker_address, operator1_address); + assert_staking_contract(staker_address, operator1_address, INITIAL_BALANCE, 10); + + // Verify that the stake pool has been set up properly. + let pool_address = stake_pool_address(staker_address, operator1_address); + stake::assert_stake_pool(pool_address, INITIAL_BALANCE, 0, 0, 0); + assert!(last_recorded_principal(staker_address, operator1_address) == INITIAL_BALANCE, 0); + assert!(stake::get_operator(pool_address) == operator1_address, 0); + assert!(beneficiary_for_operator(operator1_address) == operator1_address, 0); + + // Operator joins the validator set. + let (_sk, pk, pop) = stake::generate_identity(); + stake::join_validator_set_for_test(&pk, &pop, operator1, pool_address, true); + assert!(stake::get_validator_state(pool_address) == VALIDATOR_STATUS_ACTIVE, 1); + + // Set beneficiary. + set_beneficiary_for_operator(operator1, beneficiary_address); + assert!(beneficiary_for_operator(operator1_address) == beneficiary_address, 0); + + // Fast forward to generate rewards. + stake::end_epoch(); + let new_balance = with_rewards(INITIAL_BALANCE); + stake::assert_stake_pool(pool_address, new_balance, 0, 0, 0); + + // Operator claims 10% of rewards so far as commissions. + let expected_commission_1 = (new_balance - last_recorded_principal(staker_address, operator1_address)) / 10; + new_balance = new_balance - expected_commission_1; + request_commission(operator1, staker_address, operator1_address); + stake::assert_stake_pool(pool_address, new_balance, 0, 0, expected_commission_1); + assert!(last_recorded_principal(staker_address, operator1_address) == new_balance, 0); + assert_distribution(staker_address, operator1_address, operator1_address, expected_commission_1); + stake::fast_forward_to_unlock(pool_address); + + // Both original stake and operator commissions have received rewards. + expected_commission_1 = with_rewards(expected_commission_1); + new_balance = with_rewards(new_balance); + stake::assert_stake_pool(pool_address, new_balance, expected_commission_1, 0, 0); + distribute(staker_address, operator1_address); + let operator_balance = coin::balance(operator1_address); + let beneficiary_balance = coin::balance(beneficiary_address); + let expected_operator_balance = INITIAL_BALANCE; + let expected_beneficiary_balance = expected_commission_1; + assert!(operator_balance == expected_operator_balance, operator_balance); + assert!(beneficiary_balance == expected_beneficiary_balance, beneficiary_balance); + stake::assert_stake_pool(pool_address, new_balance, 0, 0, 0); + assert_no_pending_distributions(staker_address, operator1_address); + + // switch operator to operator2. The rewards should go to operator2 not to the beneficiay of operator1. + let old_beneficiay_balance = beneficiary_balance; + switch_operator(staker, operator1_address, operator2_address, 10); + + stake::end_epoch(); + let (_, accumulated_rewards, _) = staking_contract_amounts(staker_address, operator2_address); + + let expected_commission = accumulated_rewards / 10; + + // Request commission. + request_commission(operator2, staker_address, operator2_address); + // Unlocks the commission. + stake::fast_forward_to_unlock(pool_address); + expected_commission = with_rewards(expected_commission); + + // Distribute the commission to the operator. + distribute(staker_address, operator2_address); + + // Assert that the rewards go to operator2, and the balance of the operator1's beneficiay remains the same. + assert!(coin::balance(operator2_address) >= expected_commission, 1); + assert!(coin::balance(beneficiary_address) == old_beneficiay_balance, 1); + } + #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234)] public entry fun test_staker_can_withdraw_partial_stake( - aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store { + aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, BeneficiaryForOperator { let initial_balance = INITIAL_BALANCE * 2; setup_staking_contract(aptos_framework, staker, operator, initial_balance, 10); let staker_address = signer::address_of(staker); @@ -1180,7 +1328,7 @@ module aptos_framework::staking_contract { #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234)] public entry fun test_staker_can_withdraw_partial_stake_if_operator_never_joined_validator_set( - aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store { + aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, BeneficiaryForOperator { let initial_balance = INITIAL_BALANCE * 2; setup_staking_contract(aptos_framework, staker, operator, initial_balance, 10); let staker_address = signer::address_of(staker); @@ -1213,7 +1361,7 @@ module aptos_framework::staking_contract { #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234)] public entry fun test_multiple_distributions_added_before_distribute( - aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store { + aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, BeneficiaryForOperator { let initial_balance = INITIAL_BALANCE * 2; setup_staking_contract(aptos_framework, staker, operator, initial_balance, 10); let staker_address = signer::address_of(staker); @@ -1258,7 +1406,7 @@ module aptos_framework::staking_contract { assert!(last_recorded_principal(staker_address, operator_address) == new_balance, 0); } #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234)] - public entry fun test_update_commission(aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, StakingGroupUpdateCommissionEvent { + public entry fun test_update_commission(aptos_framework: &signer, staker: &signer, operator: &signer) acquires Store, StakingGroupUpdateCommissionEvent, BeneficiaryForOperator { let initial_balance = INITIAL_BALANCE * 2; setup_staking_contract(aptos_framework, staker, operator, initial_balance, 10); let staker_address = signer::address_of(staker); diff --git a/aptos-move/framework/aptos-framework/sources/staking_contract.spec.move b/aptos-move/framework/aptos-framework/sources/staking_contract.spec.move index a09e2ca017287..4fb35fa2e965b 100644 --- a/aptos-move/framework/aptos-framework/sources/staking_contract.spec.move +++ b/aptos-move/framework/aptos-framework/sources/staking_contract.spec.move @@ -243,6 +243,16 @@ spec aptos_framework::staking_contract { aborts_if simple_map::spec_contains_key(staking_contracts, new_operator); } + spec set_beneficiary_for_operator(operator: &signer, new_beneficiary: address) { + // TODO: temporary mockup + pragma verify = false; + } + + spec beneficiary_for_operator(operator: address): address { + // TODO: temporary mockup + pragma verify = false; + } + /// Staking_contract exists the stacker/operator pair. spec distribute(staker: address, operator: address) { // TODO: Call `distribute_internal` and could not verify `update_distribution_pool`. diff --git a/aptos-move/framework/aptos-framework/sources/vesting.move b/aptos-move/framework/aptos-framework/sources/vesting.move index 2266c2fdda703..440f0650c67e1 100644 --- a/aptos-move/framework/aptos-framework/sources/vesting.move +++ b/aptos-move/framework/aptos-framework/sources/vesting.move @@ -905,6 +905,14 @@ module aptos_framework::vesting { set_management_role(admin, contract_address, utf8(ROLE_BENEFICIARY_RESETTER), beneficiary_resetter); } + /// Set the beneficiary for the operator. + public entry fun set_beneficiary_for_operator( + operator: &signer, + new_beneficiary: address, + ) { + staking_contract::set_beneficiary_for_operator(operator, new_beneficiary); + } + public fun get_role_holder(contract_address: address, role: String): address acquires VestingAccountManagement { assert!(exists(contract_address), error::not_found(EVESTING_ACCOUNT_HAS_NO_ROLES)); let roles = &borrow_global(contract_address).roles; @@ -1011,6 +1019,12 @@ module aptos_framework::vesting { #[test_only] const VALIDATOR_STATUS_INACTIVE: u64 = 4; + #[test_only] + const MODULE_EVENT: u64 = 26; + + #[test_only] + const OPERATOR_BENEFICIARY_CHANGE: u64 = 39; + #[test_only] public fun setup(aptos_framework: &signer, accounts: &vector
) { use aptos_framework::aptos_account::create_account; @@ -1023,6 +1037,8 @@ module aptos_framework::vesting { create_account(addr); }; }); + + std::features::change_feature_flags(aptos_framework, vector[MODULE_EVENT, OPERATOR_BENEFICIARY_CHANGE], vector[]); } #[test_only] @@ -1534,6 +1550,85 @@ module aptos_framework::vesting { assert!(coin::balance(operator_address) == expected_commission, 1); } + #[test(aptos_framework = @0x1, admin = @0x123, shareholder = @0x234, operator1 = @0x345, beneficiary = @0x456, operator2 = @0x567)] + public entry fun test_set_beneficiary_for_operator( + aptos_framework: &signer, + admin: &signer, + shareholder: &signer, + operator1: &signer, + beneficiary: &signer, + operator2: &signer, + ) acquires AdminStore, VestingContract { + let admin_address = signer::address_of(admin); + let operator_address1 = signer::address_of(operator1); + let operator_address2 = signer::address_of(operator2); + let shareholder_address = signer::address_of(shareholder); + let beneficiary_address = signer::address_of(beneficiary); + setup(aptos_framework, &vector[admin_address, shareholder_address, operator_address1, beneficiary_address]); + let contract_address = setup_vesting_contract( + admin, &vector[shareholder_address], &vector[GRANT_AMOUNT], admin_address, 0); + assert!(operator_commission_percentage(contract_address) == 0, 0); + let stake_pool_address = stake_pool_address(contract_address); + // 10% commission will be paid to the operator. + update_operator(admin, contract_address, operator_address1, 10); + assert!(staking_contract::beneficiary_for_operator(operator_address1) == operator_address1, 0); + set_beneficiary_for_operator(operator1, beneficiary_address); + assert!(staking_contract::beneficiary_for_operator(operator_address1) == beneficiary_address, 0); + + // Operator needs to join the validator set for the stake pool to earn rewards. + let (_sk, pk, pop) = stake::generate_identity(); + stake::join_validator_set_for_test(&pk, &pop, operator1, stake_pool_address, true); + stake::assert_stake_pool(stake_pool_address, GRANT_AMOUNT, 0, 0, 0); + assert!(get_accumulated_rewards(contract_address) == 0, 0); + assert!(remaining_grant(contract_address) == GRANT_AMOUNT, 0); + + // Stake pool earns some rewards. + stake::end_epoch(); + let (_, accumulated_rewards, _) = staking_contract::staking_contract_amounts(contract_address, + operator_address1 + ); + // Commission is calculated using the previous commission percentage which is 10%. + let expected_commission = accumulated_rewards / 10; + + // Request commission. + staking_contract::request_commission(operator1, contract_address, operator_address1); + // Unlocks the commission. + stake::fast_forward_to_unlock(stake_pool_address); + expected_commission = with_rewards(expected_commission); + + // Distribute the commission to the operator. + distribute(contract_address); + + // Assert that the beneficiary receives the expected commission. + assert!(coin::balance(operator_address1) == 0, 1); + assert!(coin::balance(beneficiary_address) == expected_commission, 1); + let old_beneficiay_balance = coin::balance(beneficiary_address); + + // switch operator to operator2. The rewards should go to operator2 not to the beneficiay of operator1. + update_operator(admin, contract_address, operator_address2, 10); + + stake::end_epoch(); + let (_, accumulated_rewards, _) = staking_contract::staking_contract_amounts(contract_address, + operator_address2 + ); + + let expected_commission = accumulated_rewards / 10; + + // Request commission. + staking_contract::request_commission(operator2, contract_address, operator_address2); + // Unlocks the commission. + stake::fast_forward_to_unlock(stake_pool_address); + expected_commission = with_rewards(expected_commission); + + // Distribute the commission to the operator. + distribute(contract_address); + + // Assert that the rewards go to operator2, and the balance of the operator1's beneficiay remains the same. + assert!(coin::balance(operator_address2) >= expected_commission, 1); + assert!(coin::balance(beneficiary_address) == old_beneficiay_balance, 1); + + } + #[test(aptos_framework = @0x1, admin = @0x123, shareholder = @0x234)] #[expected_failure(abort_code = 0x30008, location = Self)] public entry fun test_cannot_unlock_rewards_after_contract_is_terminated( diff --git a/aptos-move/framework/aptos-framework/sources/vesting.spec.move b/aptos-move/framework/aptos-framework/sources/vesting.spec.move index c4b60b662f666..0099b54a32cf9 100644 --- a/aptos-move/framework/aptos-framework/sources/vesting.spec.move +++ b/aptos-move/framework/aptos-framework/sources/vesting.spec.move @@ -365,6 +365,14 @@ spec aptos_framework::vesting { include SetManagementRoleAbortsIf; } + spec set_beneficiary_for_operator( + operator: &signer, + new_beneficiary: address, + ) { + // TODO: temporary mockup + pragma verify = false; + } + spec get_role_holder(contract_address: address, role: String): address { aborts_if !exists(contract_address); let roles = global(contract_address).roles; diff --git a/aptos-move/framework/aptos-framework/tests/delegation_pool_integration_tests.move b/aptos-move/framework/aptos-framework/tests/delegation_pool_integration_tests.move index 5e7906840f4d5..ebc5b1223b763 100644 --- a/aptos-move/framework/aptos-framework/tests/delegation_pool_integration_tests.move +++ b/aptos-move/framework/aptos-framework/tests/delegation_pool_integration_tests.move @@ -32,6 +32,9 @@ module aptos_framework::delegation_pool_integration_tests { #[test_only] const DELEGATION_POOLS: u64 = 11; + #[test_only] + const MODULE_EVENT: u64 = 26; + #[test_only] public fun initialize_for_test(aptos_framework: &signer) { initialize_for_test_custom( @@ -76,7 +79,7 @@ module aptos_framework::delegation_pool_integration_tests { voting_power_increase_limit ); reconfiguration::initialize_for_test(aptos_framework); - features::change_feature_flags(aptos_framework, vector[DELEGATION_POOLS], vector[]); + features::change_feature_flags(aptos_framework, vector[DELEGATION_POOLS, MODULE_EVENT], vector[]); } #[test_only] diff --git a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs index e76c4b215a106..ae8e5d9b79d04 100644 --- a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs +++ b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs @@ -297,6 +297,14 @@ pub enum EntryFunctionCall { amount: u64, }, + /// Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new + /// beneficiary. To ensures payment to the current beneficiary, one should first call `synchronize_delegation_pool` + /// before switching the beneficiary. An operator can set one beneficiary for delegation pools, not a separate + /// one for each pool. + DelegationPoolSetBeneficiaryForOperator { + new_beneficiary: AccountAddress, + }, + /// Allows an owner to change the delegated voter of the underlying stake pool. DelegationPoolSetDelegatedVoter { new_voter: AccountAddress, @@ -688,7 +696,7 @@ pub enum EntryFunctionCall { /// Unlock commission amount from the stake pool. Operator needs to wait for the amount to become withdrawable /// at the end of the stake pool's lockup period before they can actually can withdraw_commission. /// - /// Only staker or operator can call this. + /// Only staker, operator or beneficiary can call this. StakingContractRequestCommission { staker: AccountAddress, operator: AccountAddress, @@ -699,6 +707,13 @@ pub enum EntryFunctionCall { operator: AccountAddress, }, + /// Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new + /// beneficiary. To ensures payment to the current beneficiary, one should first call `distribute` before switching + /// the beneficiary. An operator can set one beneficiary for staking contract pools, not a separate one for each pool. + StakingContractSetBeneficiaryForOperator { + new_beneficiary: AccountAddress, + }, + /// Allows staker to switch operator without going through the lenghthy process to unstake. StakingContractSwitchOperator { old_operator: AccountAddress, @@ -814,6 +829,11 @@ pub enum EntryFunctionCall { new_beneficiary: AccountAddress, }, + /// Set the beneficiary for the operator. + VestingSetBeneficiaryForOperator { + new_beneficiary: AccountAddress, + }, + VestingSetBeneficiaryResetter { contract_address: AccountAddress, beneficiary_resetter: AccountAddress, @@ -1038,6 +1058,9 @@ impl EntryFunctionCall { pool_address, amount, } => delegation_pool_reactivate_stake(pool_address, amount), + DelegationPoolSetBeneficiaryForOperator { new_beneficiary } => { + delegation_pool_set_beneficiary_for_operator(new_beneficiary) + }, DelegationPoolSetDelegatedVoter { new_voter } => { delegation_pool_set_delegated_voter(new_voter) }, @@ -1289,6 +1312,9 @@ impl EntryFunctionCall { staking_contract_request_commission(staker, operator) }, StakingContractResetLockup { operator } => staking_contract_reset_lockup(operator), + StakingContractSetBeneficiaryForOperator { new_beneficiary } => { + staking_contract_set_beneficiary_for_operator(new_beneficiary) + }, StakingContractSwitchOperator { old_operator, new_operator, @@ -1360,6 +1386,9 @@ impl EntryFunctionCall { shareholder, new_beneficiary, } => vesting_set_beneficiary(contract_address, shareholder, new_beneficiary), + VestingSetBeneficiaryForOperator { new_beneficiary } => { + vesting_set_beneficiary_for_operator(new_beneficiary) + }, VestingSetBeneficiaryResetter { contract_address, beneficiary_resetter, @@ -2128,6 +2157,27 @@ pub fn delegation_pool_reactivate_stake( )) } +/// Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new +/// beneficiary. To ensures payment to the current beneficiary, one should first call `synchronize_delegation_pool` +/// before switching the beneficiary. An operator can set one beneficiary for delegation pools, not a separate +/// one for each pool. +pub fn delegation_pool_set_beneficiary_for_operator( + new_beneficiary: AccountAddress, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("delegation_pool").to_owned(), + ), + ident_str!("set_beneficiary_for_operator").to_owned(), + vec![], + vec![bcs::to_bytes(&new_beneficiary).unwrap()], + )) +} + /// Allows an owner to change the delegated voter of the underlying stake pool. pub fn delegation_pool_set_delegated_voter(new_voter: AccountAddress) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( @@ -3260,7 +3310,7 @@ pub fn staking_contract_distribute( /// Unlock commission amount from the stake pool. Operator needs to wait for the amount to become withdrawable /// at the end of the stake pool's lockup period before they can actually can withdraw_commission. /// -/// Only staker or operator can call this. +/// Only staker, operator or beneficiary can call this. pub fn staking_contract_request_commission( staker: AccountAddress, operator: AccountAddress, @@ -3298,6 +3348,26 @@ pub fn staking_contract_reset_lockup(operator: AccountAddress) -> TransactionPay )) } +/// Allows an operator to change its beneficiary. Any existing unpaid commission rewards will be paid to the new +/// beneficiary. To ensures payment to the current beneficiary, one should first call `distribute` before switching +/// the beneficiary. An operator can set one beneficiary for staking contract pools, not a separate one for each pool. +pub fn staking_contract_set_beneficiary_for_operator( + new_beneficiary: AccountAddress, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("staking_contract").to_owned(), + ), + ident_str!("set_beneficiary_for_operator").to_owned(), + vec![], + vec![bcs::to_bytes(&new_beneficiary).unwrap()], + )) +} + /// Allows staker to switch operator without going through the lenghthy process to unstake. pub fn staking_contract_switch_operator( old_operator: AccountAddress, @@ -3708,6 +3778,22 @@ pub fn vesting_set_beneficiary( )) } +/// Set the beneficiary for the operator. +pub fn vesting_set_beneficiary_for_operator(new_beneficiary: AccountAddress) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("vesting").to_owned(), + ), + ident_str!("set_beneficiary_for_operator").to_owned(), + vec![], + vec![bcs::to_bytes(&new_beneficiary).unwrap()], + )) +} + pub fn vesting_set_beneficiary_resetter( contract_address: AccountAddress, beneficiary_resetter: AccountAddress, @@ -4315,6 +4401,18 @@ mod decoder { } } + pub fn delegation_pool_set_beneficiary_for_operator( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::DelegationPoolSetBeneficiaryForOperator { + new_beneficiary: bcs::from_bytes(script.args().get(0)?).ok()?, + }) + } else { + None + } + } + pub fn delegation_pool_set_delegated_voter( payload: &TransactionPayload, ) -> Option { @@ -4981,6 +5079,20 @@ mod decoder { } } + pub fn staking_contract_set_beneficiary_for_operator( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::StakingContractSetBeneficiaryForOperator { + new_beneficiary: bcs::from_bytes(script.args().get(0)?).ok()?, + }, + ) + } else { + None + } + } + pub fn staking_contract_switch_operator( payload: &TransactionPayload, ) -> Option { @@ -5232,6 +5344,18 @@ mod decoder { } } + pub fn vesting_set_beneficiary_for_operator( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::VestingSetBeneficiaryForOperator { + new_beneficiary: bcs::from_bytes(script.args().get(0)?).ok()?, + }) + } else { + None + } + } + pub fn vesting_set_beneficiary_resetter( payload: &TransactionPayload, ) -> Option { @@ -5495,6 +5619,10 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy + + + +
const LIMIT_MAX_IDENTIFIER_LENGTH: u64 = 38;
+
+ + + Whether emit function in event.move are enabled for module events. @@ -370,6 +381,17 @@ Lifetime: transient + + +Whether allow changing beneficiaries for operators. +Lifetime: transient + + +
const OPERATOR_BENEFICIARY_CHANGE: u64 = 39;
+
+ + + Whether enable paritial governance voting on aptos_governance. @@ -421,15 +443,6 @@ This is needed because of new attributes for structs and a change in storage rep - - - - -
const SECP256K1_ECDSA_AUTHENTICATOR: u64 = 33;
-
- - - Whether the new SHA2-512, SHA3-512 and RIPEMD-160 hash function natives are enabled. @@ -464,6 +477,15 @@ Lifetime: transient + + + + +
const SINGLE_SENDER_AUTHENTICATOR: u64 = 33;
+
+ + + Whether the automatic creation of accounts is enabled for sponsored transactions. @@ -1592,6 +1614,52 @@ Lifetime: transient + + + + +## Function `get_operator_beneficiary_change_feature` + + + +
public fun get_operator_beneficiary_change_feature(): u64
+
+ + + +
+Implementation + + +
public fun get_operator_beneficiary_change_feature(): u64 { OPERATOR_BENEFICIARY_CHANGE }
+
+ + + +
+ + + +## Function `operator_beneficiary_change_enabled` + + + +
public fun operator_beneficiary_change_enabled(): bool
+
+ + + +
+Implementation + + +
public fun operator_beneficiary_change_enabled(): bool acquires Features {
+    is_enabled(OPERATOR_BENEFICIARY_CHANGE)
+}
+
+ + +
diff --git a/aptos-move/framework/move-stdlib/sources/configs/features.move b/aptos-move/framework/move-stdlib/sources/configs/features.move index 287d3890b42f6..a3abb43ba4562 100644 --- a/aptos-move/framework/move-stdlib/sources/configs/features.move +++ b/aptos-move/framework/move-stdlib/sources/configs/features.move @@ -264,7 +264,7 @@ module std::features { const SAFER_METADATA: u64 = 32; - const SECP256K1_ECDSA_AUTHENTICATOR: u64 = 33; + const SINGLE_SENDER_AUTHENTICATOR: u64 = 33; /// Whether the automatic creation of accounts is enabled for sponsored transactions. /// Lifetime: transient @@ -294,6 +294,18 @@ module std::features { is_enabled(CONCURRENT_ASSETS) } + const LIMIT_MAX_IDENTIFIER_LENGTH: u64 = 38; + + /// Whether allow changing beneficiaries for operators. + /// Lifetime: transient + const OPERATOR_BENEFICIARY_CHANGE: u64 = 39; + + public fun get_operator_beneficiary_change_feature(): u64 { OPERATOR_BENEFICIARY_CHANGE } + + public fun operator_beneficiary_change_enabled(): bool acquires Features { + is_enabled(OPERATOR_BENEFICIARY_CHANGE) + } + // ============================================================================================ // Feature Flag Implementation diff --git a/types/src/on_chain_config/aptos_features.rs b/types/src/on_chain_config/aptos_features.rs index a1866c5e834b5..1400b337a96fe 100644 --- a/types/src/on_chain_config/aptos_features.rs +++ b/types/src/on_chain_config/aptos_features.rs @@ -46,6 +46,7 @@ pub enum FeatureFlag { AGGREGATOR_V2_DELAYED_FIELDS = 36, CONCURRENT_ASSETS = 37, LIMIT_MAX_IDENTIFIER_LENGTH = 38, + OPERATOR_BENEFICIARY_CHANGE = 39, } /// Representation of features on chain as a bitset.