From b98bf94e4e39b187761151289da8fc2657e3d51d Mon Sep 17 00:00:00 2001 From: Junkil Park Date: Tue, 10 Oct 2023 03:07:28 -0700 Subject: [PATCH] Allow to set the beneficiary for operator --- .../aptos-framework/doc/delegation_pool.md | 80 ++++- .../framework/aptos-framework/doc/stake.md | 293 ++++++++++-------- .../aptos-framework/doc/staking_contract.md | 142 ++++++++- .../framework/aptos-framework/doc/vesting.md | 47 +++ .../sources/delegation_pool.move | 102 +++++- .../aptos-framework/sources/stake.move | 29 ++ .../sources/staking_contract.move | 103 +++++- .../sources/staking_contract.spec.move | 15 + .../aptos-framework/sources/vesting.move | 73 +++++ .../aptos-framework/sources/vesting.spec.move | 9 + .../src/aptos_framework_sdk_builder.rs | 151 ++++++++- 11 files changed, 901 insertions(+), 143 deletions(-) diff --git a/aptos-move/framework/aptos-framework/doc/delegation_pool.md b/aptos-move/framework/aptos-framework/doc/delegation_pool.md index b589f21b346ee..5ff80668466fa 100644 --- a/aptos-move/framework/aptos-framework/doc/delegation_pool.md +++ b/aptos-move/framework/aptos-framework/doc/delegation_pool.md @@ -171,6 +171,8 @@ 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_beneficiary_for_operator_internal`](#0x1_delegation_pool_set_beneficiary_for_operator_internal) - [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) @@ -919,6 +921,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. @@ -2568,6 +2580,69 @@ Allows an owner to change the operator of the underlying stake pool. // ensure the old operator is paid its uncommitted commission rewards synchronize_delegation_pool(pool_address); stake::set_operator(&retrieve_stake_pool_owner(borrow_global<DelegationPool>(pool_address)), new_operator); + // set the beneficiary + set_beneficiary_for_operator_internal(pool_address, new_operator); +} + + + + + + + + +## Function `set_beneficiary_for_operator` + +Allows an operator to change the beneficiary of the underlying stake pool. + + +
public entry fun set_beneficiary_for_operator(operator: &signer, owner_address: address, new_beneficiary: address)
+
+ + + +
+Implementation + + +
public entry fun set_beneficiary_for_operator(
+    operator: &signer,
+    owner_address: address,
+    new_beneficiary: address,
+) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords {
+    let pool_address = get_owned_pool_address(owner_address);
+    assert(stake::get_operator(pool_address) == signer::address_of(operator), error::unauthenticated(ENOT_OPERATOR));
+    synchronize_delegation_pool(pool_address);
+    set_beneficiary_for_operator_internal(pool_address, new_beneficiary);
+}
+
+ + + +
+ + + +## Function `set_beneficiary_for_operator_internal` + + + +
fun set_beneficiary_for_operator_internal(pool_address: address, new_beneficiary: address)
+
+ + + +
+Implementation + + +
fun set_beneficiary_for_operator_internal(
+    pool_address: address,
+    new_beneficiary: address,
+) acquires DelegationPool {
+    let delegation_pool = borrow_global<DelegationPool>(pool_address);
+    let stake_pool_signer = retrieve_stake_pool_owner(delegation_pool);
+    stake::set_beneficiary_for_operator(&stake_pool_signer, new_beneficiary);
 }
 
@@ -3436,10 +3511,11 @@ 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, stake::get_beneficiary_for_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, stake::get_beneficiary_for_operator(pool_address), commission_pending_inactive); + // TODO: beneficiary? event::emit_event( &mut pool.distribute_commission_events, DistributeCommissionEvent { diff --git a/aptos-move/framework/aptos-framework/doc/stake.md b/aptos-move/framework/aptos-framework/doc/stake.md index 44f7b4b86a780..de8ee1b0c91e8 100644 --- a/aptos-move/framework/aptos-framework/doc/stake.md +++ b/aptos-move/framework/aptos-framework/doc/stake.md @@ -32,6 +32,7 @@ or if their stake drops below the min required, they would get removed at the en - [Resource `AptosCoinCapabilities`](#0x1_stake_AptosCoinCapabilities) - [Struct `IndividualValidatorPerformance`](#0x1_stake_IndividualValidatorPerformance) - [Resource `ValidatorPerformance`](#0x1_stake_ValidatorPerformance) +- [Resource `Beneficiary`](#0x1_stake_Beneficiary) - [Struct `RegisterValidatorCandidateEvent`](#0x1_stake_RegisterValidatorCandidateEvent) - [Struct `SetOperatorEvent`](#0x1_stake_SetOperatorEvent) - [Struct `AddStakeEvent`](#0x1_stake_AddStakeEvent) @@ -58,6 +59,7 @@ or if their stake drops below the min required, they would get removed at the en - [Function `get_current_epoch_voting_power`](#0x1_stake_get_current_epoch_voting_power) - [Function `get_delegated_voter`](#0x1_stake_get_delegated_voter) - [Function `get_operator`](#0x1_stake_get_operator) +- [Function `get_beneficiary_for_operator`](#0x1_stake_get_beneficiary_for_operator) - [Function `get_owned_pool_address`](#0x1_stake_get_owned_pool_address) - [Function `get_validator_index`](#0x1_stake_get_validator_index) - [Function `get_current_epoch_proposal_counts`](#0x1_stake_get_current_epoch_proposal_counts) @@ -74,6 +76,7 @@ or if their stake drops below the min required, they would get removed at the en - [Function `destroy_owner_cap`](#0x1_stake_destroy_owner_cap) - [Function `set_operator`](#0x1_stake_set_operator) - [Function `set_operator_with_cap`](#0x1_stake_set_operator_with_cap) +- [Function `set_beneficiary_for_operator`](#0x1_stake_set_beneficiary_for_operator) - [Function `set_delegated_voter`](#0x1_stake_set_delegated_voter) - [Function `set_delegated_voter_with_cap`](#0x1_stake_set_delegated_voter_with_cap) - [Function `add_stake`](#0x1_stake_add_stake) @@ -570,6 +573,33 @@ This allows the Stake module to mint rewards to stakers. +
+ + + +## Resource `Beneficiary` + + + +
struct Beneficiary has key
+
+ + + +
+Fields + + +
+
+beneficiary: address +
+
+ +
+
+ +
@@ -1641,6 +1671,37 @@ Return the operator of the validator at pool_address. + + + + +## Function `get_beneficiary_for_operator` + +Return the operator of the validator at pool_address. + + +
#[view]
+public fun get_beneficiary_for_operator(pool_address: address): address
+
+ + + +
+Implementation + + +
public fun get_beneficiary_for_operator(pool_address: address): address acquires StakePool, Beneficiary {
+    assert_stake_pool_exists(pool_address);
+    if(exists<Beneficiary>(pool_address)) {
+        borrow_global<Beneficiary>(pool_address).beneficiary
+    } else {
+        borrow_global<StakePool>(pool_address).operator_address
+    }
+}
+
+ + +
@@ -2183,6 +2244,39 @@ Allows an account with ownership capability to change the operator of the stake + + + + +## Function `set_beneficiary_for_operator` + + + +
public fun set_beneficiary_for_operator(pool_signer: &signer, new_beneficiary: address)
+
+ + + +
+Implementation + + +
public fun set_beneficiary_for_operator(pool_signer: &signer, new_beneficiary: address) acquires Beneficiary {
+    let pool_address = signer::address_of(pool_signer);
+    assert_stake_pool_exists(pool_address);
+    if (!exists<Beneficiary>(pool_address)) {
+        move_to(pool_signer, Beneficiary { beneficiary: new_beneficiary });
+    }
+    else {
+        let beneficiary = borrow_global_mut<Beneficiary>(pool_address);
+        beneficiary.beneficiary = new_beneficiary;
+    }
+    // TODO: emit an event
+}
+
+ + +
@@ -3630,132 +3724,6 @@ Returns validator's next epoch voting power, including pending_active, active, a - - - - -
fun spec_validator_index_upper_bound(): u64 {
-   len(global<ValidatorPerformance>(@aptos_framework).validators)
-}
-
- - - - - - - -
fun spec_has_stake_pool(a: address): bool {
-   exists<StakePool>(a)
-}
-
- - - - - - - -
fun spec_has_validator_config(a: address): bool {
-   exists<ValidatorConfig>(a)
-}
-
- - - - - - - -
fun spec_rewards_amount(
-   stake_amount: u64,
-   num_successful_proposals: u64,
-   num_total_proposals: u64,
-   rewards_rate: u64,
-   rewards_rate_denominator: u64,
-): u64;
-
- - - - - - - -
fun spec_contains(validators: vector<ValidatorInfo>, addr: address): bool {
-   exists i in 0..len(validators): validators[i].addr == addr
-}
-
- - - - - - - -
fun spec_is_current_epoch_validator(pool_address: address): bool {
-   let validator_set = global<ValidatorSet>(@aptos_framework);
-   !spec_contains(validator_set.pending_active, pool_address)
-       && (spec_contains(validator_set.active_validators, pool_address)
-       || spec_contains(validator_set.pending_inactive, pool_address))
-}
-
- - - - - - - -
fun spec_get_reward_rate_1(config: StakingConfig): num {
-   if (features::spec_periodical_reward_rate_decrease_enabled()) {
-       let epoch_rewards_rate = global<staking_config::StakingRewardsConfig>(@aptos_framework).rewards_rate;
-       if (epoch_rewards_rate.value == 0) {
-           0
-       } else {
-           let denominator_0 = aptos_std::fixed_point64::spec_divide_u128(staking_config::MAX_REWARDS_RATE, epoch_rewards_rate);
-           let denominator = if (denominator_0 > MAX_U64) {
-               MAX_U64
-           } else {
-               denominator_0
-           };
-           let nominator = aptos_std::fixed_point64::spec_multiply_u128(denominator, epoch_rewards_rate);
-           nominator
-       }
-   } else {
-           config.rewards_rate
-   }
-}
-
- - - - - - - -
fun spec_get_reward_rate_2(config: StakingConfig): num {
-   if (features::spec_periodical_reward_rate_decrease_enabled()) {
-       let epoch_rewards_rate = global<staking_config::StakingRewardsConfig>(@aptos_framework).rewards_rate;
-       if (epoch_rewards_rate.value == 0) {
-           1
-       } else {
-           let denominator_0 = aptos_std::fixed_point64::spec_divide_u128(staking_config::MAX_REWARDS_RATE, epoch_rewards_rate);
-           let denominator = if (denominator_0 > MAX_U64) {
-               MAX_U64
-           } else {
-               denominator_0
-           };
-           denominator
-       }
-   } else {
-           config.rewards_rate_denominator
-   }
-}
-
- - - ### Resource `ValidatorSet` @@ -4638,4 +4606,77 @@ Returns validator's next epoch voting power, including pending_active, active, a + + + + + +
fun spec_validator_index_upper_bound(): u64 {
+   len(global<ValidatorPerformance>(@aptos_framework).validators)
+}
+
+ + + + + + + +
fun spec_has_stake_pool(a: address): bool {
+   exists<StakePool>(a)
+}
+
+ + + + + + + +
fun spec_has_validator_config(a: address): bool {
+   exists<ValidatorConfig>(a)
+}
+
+ + + + + + + +
fun spec_rewards_amount(
+   stake_amount: u64,
+   num_successful_proposals: u64,
+   num_total_proposals: u64,
+   rewards_rate: u64,
+   rewards_rate_denominator: u64,
+): u64;
+
+ + + + + + + +
fun spec_contains(validators: vector<ValidatorInfo>, addr: address): bool {
+   exists i in 0..len(validators): validators[i].addr == addr
+}
+
+ + + + + + + +
fun spec_is_current_epoch_validator(pool_address: address): bool {
+   let validator_set = global<ValidatorSet>(@aptos_framework);
+   !spec_contains(validator_set.pending_active, pool_address)
+       && (spec_contains(validator_set.active_validators, pool_address)
+       || spec_contains(validator_set.pending_inactive, pool_address))
+}
+
+ + [move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/doc/staking_contract.md b/aptos-move/framework/aptos-framework/doc/staking_contract.md index c0a7aa461a184..770a3ee4f88a4 100644 --- a/aptos-move/framework/aptos-framework/doc/staking_contract.md +++ b/aptos-move/framework/aptos-framework/doc/staking_contract.md @@ -52,6 +52,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 +66,8 @@ 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 `set_beneficiary_for_operator_internal`](#0x1_staking_contract_set_beneficiary_for_operator_internal) - [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 +84,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 +97,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,6 +109,7 @@ pool.
use 0x1::account;
+use 0x1::aptos_account;
 use 0x1::aptos_coin;
 use 0x1::bcs;
 use 0x1::coin;
@@ -784,12 +790,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 +1035,34 @@ 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(staker: address, operator: address): address
+
+ + + +
+Implementation + + +
public fun beneficiary_for_operator(staker: address, operator: address): address acquires Store {
+    assert_staking_contract_exists(staker, operator);
+    let pool_address = stake_pool_address(staker, operator);
+    stake::get_beneficiary_for_operator(pool_address)
+}
+
+ + +
@@ -1348,7 +1382,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)
@@ -1362,7 +1396,10 @@ Only staker or operator can call this.
 
 
public entry fun request_commission(account: &signer, staker: address, operator: address) acquires Store {
     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(staker, operator),
+        error::unauthenticated(ENOT_STAKER_OR_OPERATOR_OR_BENEFICIARY)
+    );
     assert_staking_contract_exists(staker, operator);
 
     let store = borrow_global_mut<Store>(staker);
@@ -1627,6 +1664,64 @@ Allows staker to switch operator without going through the lenghthy process to u
         &mut store.switch_operator_events,
         SwitchOperatorEvent { pool_address, old_operator, new_operator }
     );
+
+    // Set the beneficiary address to the new operator by default.
+    set_beneficiary_for_operator_internal(new_operator, staker_address, new_operator);
+}
+
+ + + + + + + +## Function `set_beneficiary_for_operator` + + + +
public entry fun set_beneficiary_for_operator(operator: &signer, staker: address, new_beneficiary: address)
+
+ + + +
+Implementation + + +
public entry fun set_beneficiary_for_operator(operator: &signer, staker: address, new_beneficiary: address) acquires Store {
+    let operator_addr = signer::address_of(operator);
+    assert_staking_contract_exists(staker, operator_addr);
+    assert_account_is_registered_for_apt(new_beneficiary);
+    set_beneficiary_for_operator_internal(operator_addr, staker, new_beneficiary);
+}
+
+ + + +
+ + + +## Function `set_beneficiary_for_operator_internal` + + + +
fun set_beneficiary_for_operator_internal(operator: address, staker: address, new_beneficiary: address)
+
+ + + +
+Implementation + + +
fun set_beneficiary_for_operator_internal(operator: address, staker: address, new_beneficiary: address) acquires Store {
+    assert_staking_contract_exists(staker, operator);
+    let staking_contracts = &borrow_global<Store>(staker).staking_contracts;
+    let pool_signer_cap = &simple_map::borrow(staking_contracts, &operator).signer_cap;
+    let pool_signer = account::create_signer_with_capability(pool_signer_cap);
+    stake::set_beneficiary_for_operator(&pool_signer, new_beneficiary);
 }
 
@@ -1705,6 +1800,10 @@ 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); + // If the recipient is the operator, send the commission to the beneficiary instead. + if (recipient == operator) { + recipient = stake::get_beneficiary_for_operator(staking_contract.pool_address); + }; coin::deposit(recipient, coin::extract(&mut coins, amount_to_distribute)); emit_event( @@ -2130,6 +2229,23 @@ Staking_contract exists the stacker/operator pair. + + +### Function `beneficiary_for_operator` + + +
#[view]
+public fun beneficiary_for_operator(staker: address, operator: address): address
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `create_staking_contract` @@ -2374,6 +2490,22 @@ Staking_contract exists the stacker/operator pair. + + +### Function `set_beneficiary_for_operator` + + +
public entry fun set_beneficiary_for_operator(operator: &signer, staker: address, new_beneficiary: address)
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `distribute` diff --git a/aptos-move/framework/aptos-framework/doc/vesting.md b/aptos-move/framework/aptos-framework/doc/vesting.md index a07f0552649d4..afbb3a05a9630 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,35 @@ account. +
+ + + +## Function `set_beneficiary_for_operator` + +Set the beneficiary for the operator. + + +
public entry fun set_beneficiary_for_operator(operator: &signer, contract_address: address, new_beneficiary: address)
+
+ + + +
+Implementation + + +
public entry fun set_beneficiary_for_operator(
+    operator: &signer,
+    contract_address: address,
+    new_beneficiary: address,
+) {
+    staking_contract::set_beneficiary_for_operator(operator, contract_address, new_beneficiary);
+}
+
+ + +
@@ -3465,6 +3496,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, contract_address: address, 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..6f4d78879d538 100644 --- a/aptos-move/framework/aptos-framework/sources/delegation_pool.move +++ b/aptos-move/framework/aptos-framework/sources/delegation_pool.move @@ -183,6 +183,9 @@ 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; + const MAX_U64: u64 = 18446744073709551615; /// Maximum operator percentage fee(of double digit precision): 22.85% is represented as 2285 @@ -962,6 +965,29 @@ module aptos_framework::delegation_pool { // ensure the old operator is paid its uncommitted commission rewards synchronize_delegation_pool(pool_address); stake::set_operator(&retrieve_stake_pool_owner(borrow_global(pool_address)), new_operator); + // set the beneficiary + set_beneficiary_for_operator_internal(pool_address, new_operator); + } + + /// Allows an operator to change the beneficiary of the underlying stake pool. + public entry fun set_beneficiary_for_operator( + operator: &signer, + owner_address: address, + new_beneficiary: address, + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + let pool_address = get_owned_pool_address(owner_address); + assert!(stake::get_operator(pool_address) == signer::address_of(operator), error::unauthenticated(ENOT_OPERATOR)); + synchronize_delegation_pool(pool_address); + set_beneficiary_for_operator_internal(pool_address, new_beneficiary); + } + + fun set_beneficiary_for_operator_internal( + pool_address: address, + new_beneficiary: address, + ) acquires DelegationPool { + let delegation_pool = borrow_global(pool_address); + let stake_pool_signer = retrieve_stake_pool_owner(delegation_pool); + stake::set_beneficiary_for_operator(&stake_pool_signer, new_beneficiary); } /// Allows an owner to change the delegated voter of the underlying stake pool. @@ -1470,10 +1496,11 @@ 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, stake::get_beneficiary_for_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, stake::get_beneficiary_for_operator(pool_address), commission_pending_inactive); + // TODO: beneficiary? event::emit_event( &mut pool.distribute_commission_events, DistributeCommissionEvent { @@ -1638,7 +1665,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 +1679,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, @@ -3211,6 +3238,73 @@ module aptos_framework::delegation_pool { assert_delegation(new_operator_address, pool_address, 26050290, 0, 26050290); } + #[test(aptos_framework = @aptos_framework, operator = @0x123, delegator = @0x010, beneficiary = @0x020)] + public entry fun test_set_beneficiary_for_operator( + aptos_framework: &signer, + operator: &signer, + delegator: &signer, + beneficiary: &signer, + ) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords { + initialize_for_test(aptos_framework); + + let operator_address = signer::address_of(operator); + //account::create_account_for_test(operator_address); + aptos_framework::aptos_account::create_account(operator_address); + + let beneficiary_address = signer::address_of(beneficiary); + //account::create_account_for_test(beneficiary_address); + aptos_framework::aptos_account::create_account(beneficiary_address); + + // create delegation pool of commission fee 12.65% + initialize_delegation_pool(operator, 1265, vector::empty()); + let pool_address = get_owned_pool_address(operator_address); + assert!(stake::get_operator(pool_address) == operator_address, 0); + assert!(stake::get_beneficiary_for_operator(pool_address) == operator_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(operator, pool_address, CONSENSUS_KEY_1, CONSENSUS_POP_1); + stake::join_validator_set(operator, 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(operator_address, pool_address, 126500000000, 0, 126500000000); + end_aptos_epoch(); + stake::assert_stake_pool(pool_address, 102010000000000, 0, 0, 102010000000000); + assert_delegation(operator_address, pool_address, 254265000000, 0, 254265000000); + timestamp::fast_forward_seconds(LOCKUP_CYCLE_SECONDS); + end_aptos_epoch(); + + withdraw(operator, pool_address, ONE_APT); + assert!(coin::balance(operator_address) == ONE_APT - 1, 0); + + set_beneficiary_for_operator(operator, operator_address, beneficiary_address); + assert!(stake::get_beneficiary_for_operator(pool_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(operator_address) == ONE_APT - 1, 0); + + let (actual_active, actual_inactive, actual_pending_inactive) = get_stake(pool_address, beneficiary_address); + aptos_std::debug::print(&actual_active); + aptos_std::debug::print(&actual_inactive); + aptos_std::debug::print(&actual_pending_inactive); + + } + #[test(aptos_framework = @aptos_framework, validator = @0x123, delegator1 = @0x010, delegator2 = @0x020)] public entry fun test_min_stake_is_preserved( aptos_framework: &signer, diff --git a/aptos-move/framework/aptos-framework/sources/stake.move b/aptos-move/framework/aptos-framework/sources/stake.move index 2368472f33b40..eadbe23edaa29 100644 --- a/aptos-move/framework/aptos-framework/sources/stake.move +++ b/aptos-move/framework/aptos-framework/sources/stake.move @@ -197,6 +197,11 @@ module aptos_framework::stake { validators: vector, } + // The beneficiary address for the operator. + struct Beneficiary has key { + beneficiary: address + } + struct RegisterValidatorCandidateEvent has drop, store { pool_address: address, } @@ -368,6 +373,16 @@ module aptos_framework::stake { borrow_global(pool_address).operator_address } + #[view] + /// Return the operator of the validator at `pool_address`. + public fun get_beneficiary_for_operator(pool_address: address): address acquires StakePool, Beneficiary { + assert_stake_pool_exists(pool_address); + if(exists(pool_address)) { + borrow_global(pool_address).beneficiary + } else { + borrow_global(pool_address).operator_address + } + } /// Return the pool address in `owner_cap`. public fun get_owned_pool_address(owner_cap: &OwnerCapability): address { owner_cap.pool_address @@ -590,6 +605,20 @@ module aptos_framework::stake { ); } + /// Allows to change the beneficiary for the oeprator of the stake pool. + public fun set_beneficiary_for_operator(pool_signer: &signer, new_beneficiary: address) acquires Beneficiary { + let pool_address = signer::address_of(pool_signer); + assert_stake_pool_exists(pool_address); + if (!exists(pool_address)) { + move_to(pool_signer, Beneficiary { beneficiary: new_beneficiary }); + } + else { + let beneficiary = borrow_global_mut(pool_address); + beneficiary.beneficiary = new_beneficiary; + } + // TODO: emit an event + } + /// Allows an owner to change the delegated voter of the stake pool. public entry fun set_delegated_voter(owner: &signer, new_voter: address) acquires OwnerCapability, StakePool { let owner_address = signer::address_of(owner); diff --git a/aptos-move/framework/aptos-framework/sources/staking_contract.move b/aptos-move/framework/aptos-framework/sources/staking_contract.move index 1291d63213947..a534351767023 100644 --- a/aptos-move/framework/aptos-framework/sources/staking_contract.move +++ b/aptos-move/framework/aptos-framework/sources/staking_contract.move @@ -34,6 +34,7 @@ module aptos_framework::staking_contract { use aptos_std::simple_map::{Self, SimpleMap}; use aptos_framework::account::{Self, SignerCapability}; + use aptos_framework::aptos_account::{assert_account_is_registered_for_apt}; use aptos_framework::aptos_coin::AptosCoin; use aptos_framework::coin::{Self, Coin}; use aptos_framework::event::{EventHandle, emit_event}; @@ -56,8 +57,8 @@ 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; /// Maximum number of distributions a stake pool can support. const MAXIMUM_PENDING_DISTRIBUTIONS: u64 = 20; @@ -233,6 +234,14 @@ 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(staker: address, operator: address): address acquires Store { + assert_staking_contract_exists(staker, operator); + let pool_address = stake_pool_address(staker, operator); + stake::get_beneficiary_for_operator(pool_address) + } + #[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( @@ -406,10 +415,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. + /// Only staker, operator or beneficiary can call this. public entry fun request_commission(account: &signer, staker: address, operator: address) acquires Store { 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(staker, operator), + error::unauthenticated(ENOT_STAKER_OR_OPERATOR_OR_BENEFICIARY) + ); assert_staking_contract_exists(staker, operator); let store = borrow_global_mut(staker); @@ -574,6 +586,24 @@ module aptos_framework::staking_contract { &mut store.switch_operator_events, SwitchOperatorEvent { pool_address, old_operator, new_operator } ); + + // Set the beneficiary address to the new operator by default. + set_beneficiary_for_operator_internal(new_operator, staker_address, new_operator); + } + + public entry fun set_beneficiary_for_operator(operator: &signer, staker: address, new_beneficiary: address) acquires Store { + let operator_addr = signer::address_of(operator); + assert_staking_contract_exists(staker, operator_addr); + assert_account_is_registered_for_apt(new_beneficiary); + set_beneficiary_for_operator_internal(operator_addr, staker, new_beneficiary); + } + + fun set_beneficiary_for_operator_internal(operator: address, staker: address, new_beneficiary: address) acquires Store { + assert_staking_contract_exists(staker, operator); + let staking_contracts = &borrow_global(staker).staking_contracts; + let pool_signer_cap = &simple_map::borrow(staking_contracts, &operator).signer_cap; + let pool_signer = account::create_signer_with_capability(pool_signer_cap); + stake::set_beneficiary_for_operator(&pool_signer, new_beneficiary); } /// Allow anyone to distribute already unlocked funds. This does not affect reward compounding and therefore does @@ -612,6 +642,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 = stake::get_beneficiary_for_operator(staking_contract.pool_address); + }; coin::deposit(recipient, coin::extract(&mut coins, amount_to_distribute)); emit_event( @@ -1127,6 +1161,67 @@ module aptos_framework::staking_contract { assert!(commission_percentage(staker_address, operator_2_address) == 10, 2); } + #[test(aptos_framework = @0x1, staker = @0x123, operator = @0x234, beneficiary = @0x345)] + public entry fun test_operator_can_set_beneficiary( + aptos_framework: &signer, + staker: &signer, + operator: &signer, + beneficiary: &signer, + ) acquires Store { + setup_staking_contract(aptos_framework, staker, operator, INITIAL_BALANCE, 10); + let staker_address = signer::address_of(staker); + let operator_address = signer::address_of(operator); + 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, operator_address); + assert_staking_contract(staker_address, operator_address, INITIAL_BALANCE, 10); + + // Verify that the stake pool has been set up properly. + let pool_address = stake_pool_address(staker_address, operator_address); + stake::assert_stake_pool(pool_address, INITIAL_BALANCE, 0, 0, 0); + assert!(last_recorded_principal(staker_address, operator_address) == INITIAL_BALANCE, 0); + assert!(stake::get_operator(pool_address) == operator_address, 0); + assert!(stake::get_beneficiary_for_operator(pool_address) == operator_address, 0); + + // Operator joins the validator set. + let (_sk, pk, pop) = stake::generate_identity(); + stake::join_validator_set_for_test(&pk, &pop, operator, pool_address, true); + assert!(stake::get_validator_state(pool_address) == VALIDATOR_STATUS_ACTIVE, 1); + + // Set beneficiary. + set_beneficiary_for_operator(operator, staker_address, beneficiary_address); + assert!(stake::get_beneficiary_for_operator(pool_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, operator_address)) / 10; + new_balance = new_balance - expected_commission_1; + request_commission(operator, staker_address, operator_address); + stake::assert_stake_pool(pool_address, new_balance, 0, 0, expected_commission_1); + assert!(last_recorded_principal(staker_address, operator_address) == new_balance, 0); + assert_distribution(staker_address, operator_address, operator_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, operator_address); + let operator_balance = coin::balance(operator_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, operator_address); + } + #[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 { 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 6eb1d33f95739..5773d501bb054 100644 --- a/aptos-move/framework/aptos-framework/sources/staking_contract.spec.move +++ b/aptos-move/framework/aptos-framework/sources/staking_contract.spec.move @@ -192,6 +192,21 @@ spec aptos_framework::staking_contract { aborts_if simple_map::spec_contains_key(staking_contracts, new_operator); } + spec set_beneficiary_for_operator(operator: &signer, staker: address, new_beneficiary: address) { + // TODO: temporary mockup + pragma verify = false; + } + + spec set_beneficiary_for_operator_internal(operator: address, staker: address, new_beneficiary: address) { + // TODO: temporary mockup + pragma verify = false; + } + + spec beneficiary_for_operator(staker: address, 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..c4169d2556b5e 100644 --- a/aptos-move/framework/aptos-framework/sources/vesting.move +++ b/aptos-move/framework/aptos-framework/sources/vesting.move @@ -905,6 +905,15 @@ 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, + contract_address: address, + new_beneficiary: address, + ) { + staking_contract::set_beneficiary_for_operator(operator, contract_address, 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; @@ -1534,6 +1543,70 @@ module aptos_framework::vesting { assert!(coin::balance(operator_address) == expected_commission, 1); } + #[test(aptos_framework = @0x1, admin = @0x123, shareholder = @0x234, operator = @0x345, beneficiary = @0x456)] + public entry fun test_set_beneficiary_for_operator( + aptos_framework: &signer, + admin: &signer, + shareholder: &signer, + operator: &signer, + beneficiary: &signer, + ) acquires AdminStore, VestingContract { + let admin_address = signer::address_of(admin); + let operator_address = signer::address_of(operator); + let shareholder_address = signer::address_of(shareholder); + let beneficiary_address = signer::address_of(beneficiary); + setup(aptos_framework, &vector[admin_address, shareholder_address, operator_address, 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_address, 10); + assert!(stake::get_beneficiary_for_operator(stake_pool_address) == operator_address, 0); + set_beneficiary_for_operator(operator, contract_address, beneficiary_address); + assert!(stake::get_beneficiary_for_operator(stake_pool_address) == 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, operator, 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_address); + + // Update commission percentage to 20%. This also immediately requests commission. + update_commission_percentage(admin, contract_address, 20); + // Assert that the operator is still the same, and the commission percentage is updated to 20%. + assert!(operator(contract_address) == operator_address, 0); + assert!(operator_commission_percentage(contract_address) == 20, 0); + + // Commission is calculated using the previous commission percentage which is 10%. + let expected_commission = accumulated_rewards / 10; + + // Stake pool earns some more rewards. + stake::end_epoch(); + let (_, accumulated_rewards, _) = staking_contract::staking_contract_amounts(contract_address, operator_address); + + // Request commission again. + staking_contract::request_commission(operator, contract_address, operator_address); + // The commission is calculated using the current commission percentage which is 20%. + expected_commission = with_rewards(expected_commission) + (accumulated_rewards / 5); + + // 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_address) == 0, 1); + assert!(coin::balance(beneficiary_address) == expected_commission, 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..c91a30d745169 100644 --- a/aptos-move/framework/aptos-framework/sources/vesting.spec.move +++ b/aptos-move/framework/aptos-framework/sources/vesting.spec.move @@ -365,6 +365,15 @@ spec aptos_framework::vesting { include SetManagementRoleAbortsIf; } + spec set_beneficiary_for_operator( + operator: &signer, + contract_address: address, + 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/cached-packages/src/aptos_framework_sdk_builder.rs b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs index e76c4b215a106..dd98a3332a905 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,12 @@ pub enum EntryFunctionCall { amount: u64, }, + /// Allows an operator to change the beneficiary of the underlying stake pool. + DelegationPoolSetBeneficiaryForOperator { + owner_address: AccountAddress, + new_beneficiary: AccountAddress, + }, + /// Allows an owner to change the delegated voter of the underlying stake pool. DelegationPoolSetDelegatedVoter { new_voter: AccountAddress, @@ -688,7 +694,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 +705,11 @@ pub enum EntryFunctionCall { operator: AccountAddress, }, + StakingContractSetBeneficiaryForOperator { + staker: AccountAddress, + new_beneficiary: AccountAddress, + }, + /// Allows staker to switch operator without going through the lenghthy process to unstake. StakingContractSwitchOperator { old_operator: AccountAddress, @@ -814,6 +825,12 @@ pub enum EntryFunctionCall { new_beneficiary: AccountAddress, }, + /// Set the beneficiary for the operator. + VestingSetBeneficiaryForOperator { + contract_address: AccountAddress, + new_beneficiary: AccountAddress, + }, + VestingSetBeneficiaryResetter { contract_address: AccountAddress, beneficiary_resetter: AccountAddress, @@ -1038,6 +1055,10 @@ impl EntryFunctionCall { pool_address, amount, } => delegation_pool_reactivate_stake(pool_address, amount), + DelegationPoolSetBeneficiaryForOperator { + owner_address, + new_beneficiary, + } => delegation_pool_set_beneficiary_for_operator(owner_address, new_beneficiary), DelegationPoolSetDelegatedVoter { new_voter } => { delegation_pool_set_delegated_voter(new_voter) }, @@ -1289,6 +1310,10 @@ impl EntryFunctionCall { staking_contract_request_commission(staker, operator) }, StakingContractResetLockup { operator } => staking_contract_reset_lockup(operator), + StakingContractSetBeneficiaryForOperator { + staker, + new_beneficiary, + } => staking_contract_set_beneficiary_for_operator(staker, new_beneficiary), StakingContractSwitchOperator { old_operator, new_operator, @@ -1360,6 +1385,10 @@ impl EntryFunctionCall { shareholder, new_beneficiary, } => vesting_set_beneficiary(contract_address, shareholder, new_beneficiary), + VestingSetBeneficiaryForOperator { + contract_address, + new_beneficiary, + } => vesting_set_beneficiary_for_operator(contract_address, new_beneficiary), VestingSetBeneficiaryResetter { contract_address, beneficiary_resetter, @@ -2128,6 +2157,28 @@ pub fn delegation_pool_reactivate_stake( )) } +/// Allows an operator to change the beneficiary of the underlying stake pool. +pub fn delegation_pool_set_beneficiary_for_operator( + owner_address: AccountAddress, + 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(&owner_address).unwrap(), + 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 +3311,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 +3349,27 @@ pub fn staking_contract_reset_lockup(operator: AccountAddress) -> TransactionPay )) } +pub fn staking_contract_set_beneficiary_for_operator( + staker: AccountAddress, + 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(&staker).unwrap(), + 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 +3780,28 @@ pub fn vesting_set_beneficiary( )) } +/// Set the beneficiary for the operator. +pub fn vesting_set_beneficiary_for_operator( + contract_address: AccountAddress, + 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(&contract_address).unwrap(), + bcs::to_bytes(&new_beneficiary).unwrap(), + ], + )) +} + pub fn vesting_set_beneficiary_resetter( contract_address: AccountAddress, beneficiary_resetter: AccountAddress, @@ -4315,6 +4409,19 @@ mod decoder { } } + pub fn delegation_pool_set_beneficiary_for_operator( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::DelegationPoolSetBeneficiaryForOperator { + owner_address: bcs::from_bytes(script.args().get(0)?).ok()?, + new_beneficiary: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + pub fn delegation_pool_set_delegated_voter( payload: &TransactionPayload, ) -> Option { @@ -4981,6 +5088,21 @@ mod decoder { } } + pub fn staking_contract_set_beneficiary_for_operator( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::StakingContractSetBeneficiaryForOperator { + staker: bcs::from_bytes(script.args().get(0)?).ok()?, + new_beneficiary: bcs::from_bytes(script.args().get(1)?).ok()?, + }, + ) + } else { + None + } + } + pub fn staking_contract_switch_operator( payload: &TransactionPayload, ) -> Option { @@ -5232,6 +5354,19 @@ mod decoder { } } + pub fn vesting_set_beneficiary_for_operator( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::VestingSetBeneficiaryForOperator { + contract_address: bcs::from_bytes(script.args().get(0)?).ok()?, + new_beneficiary: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + pub fn vesting_set_beneficiary_resetter( payload: &TransactionPayload, ) -> Option { @@ -5495,6 +5630,10 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy