diff --git a/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs b/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs index c3366ca452cea..94b0b942c6a53 100644 --- a/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs +++ b/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::tests::automation_registration::AutomationRegistrationTestContext; -use aptos_cached_packages::{aptos_framework_sdk_builder}; +use aptos_cached_packages::aptos_framework_sdk_builder; use aptos_crypto::HashValue; -use aptos_types::transaction::automated_transaction::AutomatedTransaction; +use aptos_types::chain_id::ChainId; +use aptos_types::transaction::automated_transaction::{ + AutomatedTransaction, AutomatedTransactionBuilder, BuilderResult, +}; use aptos_types::transaction::{ExecutionStatus, Transaction, TransactionStatus}; -use aptos_vm::transaction_metadata::TransactionMetadata; use move_core_types::vm_status::StatusCode; #[test] @@ -65,7 +67,8 @@ fn check_expired_automated_transaction() { fn check_automated_transaction_with_insufficient_balance() { let mut test_context = AutomationRegistrationTestContext::new(); let dest_account = test_context.new_account_data(0, 0); - let payload = aptos_framework_sdk_builder::supra_account_transfer(dest_account.address().clone(), 100); + let payload = + aptos_framework_sdk_builder::supra_account_transfer(dest_account.address().clone(), 100); let sequence_number = 0; let raw_transaction = test_context @@ -93,7 +96,8 @@ fn check_automated_transaction_with_insufficient_balance() { fn check_automated_transaction_successful_execution() { let mut test_context = AutomationRegistrationTestContext::new(); let dest_account = test_context.new_account_data(1_000_000, 0); - let payload = aptos_framework_sdk_builder::supra_account_transfer(dest_account.address().clone(), 100); + let payload = + aptos_framework_sdk_builder::supra_account_transfer(dest_account.address().clone(), 100); let gas_price = 100; let max_gas_amount = 100; @@ -114,24 +118,23 @@ fn check_automated_transaction_successful_execution() { &TransactionStatus::Keep(ExecutionStatus::Success), "{output:?}" ); - - // For now no active transaction available, so this task execution will fail on prologue. - let sequence_number = 0; - let raw_transaction = test_context - .sender_account_data() - .account() - .transaction() - .payload(payload.clone()) - .sequence_number(sequence_number) - .gas_unit_price(gas_price) - .max_gas_amount(max_gas_amount) - .ttl(expiration_time) - .raw(); - - let parent_hash = HashValue::new([42; HashValue::LENGTH]); - let automated_txn = AutomatedTransaction::new(raw_transaction.clone(), parent_hash, 1); - let result = - test_context.execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); + let next_task_index = test_context.get_next_task_index_from_registry(); + assert_eq!(next_task_index, 1); + let automated_task_details = test_context.get_task_details(next_task_index - 1); + let automated_txn_builder = AutomatedTransactionBuilder::try_from(automated_task_details) + .expect("Successful builder creation"); + let maybe_automated_txn = automated_txn_builder + .clone() + .with_chain_id(ChainId::test()) + .with_block_height(1) + .with_gas_unit_price(gas_price) + .build(); + let BuilderResult::Success(automated_txn) = maybe_automated_txn else { + panic!("Automated transaction should successfully build") + }; + + let result = test_context + .execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); AutomationRegistrationTestContext::check_discarded_output( result, StatusCode::NO_ACTIVE_AUTOMATED_TASK, @@ -143,8 +146,8 @@ fn check_automated_transaction_successful_execution() { // Execute automated transaction one more time which should be success, as task is already become active after epoch change let sender_address = test_context.sender_account_address(); let sender_seq_num = test_context.account_sequence_number(sender_address); - let output = - test_context.execute_and_apply_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); + let output = test_context + .execute_and_apply_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); assert_eq!( output.status(), &TransactionStatus::Keep(ExecutionStatus::Success), @@ -153,23 +156,23 @@ fn check_automated_transaction_successful_execution() { let dest_account_balance = test_context.account_balance(dest_account.address().clone()); assert_eq!(dest_account_balance, 1_000_100); // check that sequence number is not updated. - assert_eq!(sender_seq_num, test_context.account_sequence_number(sender_address)); + assert_eq!( + sender_seq_num, + test_context.account_sequence_number(sender_address) + ); // try to submit automated transaction with incorrect sender - let raw_transaction = dest_account - .account() - .transaction() - .payload(payload) - .sequence_number(sequence_number) - .gas_unit_price(gas_price) - .max_gas_amount(max_gas_amount) - .ttl(expiration_time) - .raw(); - - let parent_hash = HashValue::new([42; HashValue::LENGTH]); - let automated_txn = AutomatedTransaction::new(raw_transaction, parent_hash, 1); - let result = - test_context.execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); + let maybe_automated_txn = automated_txn_builder + .with_sender(*dest_account.address()) + .with_chain_id(ChainId::test()) + .with_block_height(1) + .with_gas_unit_price(gas_price) + .build(); + let BuilderResult::Success(automated_txn) = maybe_automated_txn else { + panic!("Automated transaction should successfully build") + }; + let result = test_context + .execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); AutomationRegistrationTestContext::check_discarded_output( result, StatusCode::NO_ACTIVE_AUTOMATED_TASK, diff --git a/aptos-move/e2e-testsuite/src/tests/automation_registration.rs b/aptos-move/e2e-testsuite/src/tests/automation_registration.rs index 9024c1ea7afdd..91fe2b35df030 100644 --- a/aptos-move/e2e-testsuite/src/tests/automation_registration.rs +++ b/aptos-move/e2e-testsuite/src/tests/automation_registration.rs @@ -4,20 +4,22 @@ use aptos_cached_packages::aptos_framework_sdk_builder; use aptos_language_e2e_tests::account::{Account, AccountData}; use aptos_language_e2e_tests::executor::FakeExecutor; -use aptos_types::transaction::automation::RegistrationParams; +use aptos_types::transaction::automation::{AutomationTaskMetaData, RegistrationParams}; use aptos_types::transaction::{ EntryFunction, ExecutionStatus, SignedTransaction, TransactionOutput, TransactionPayload, TransactionStatus, }; +use move_core_types::account_address::AccountAddress; +use move_core_types::value::MoveValue; use move_core_types::vm_status::StatusCode; use std::ops::{Deref, DerefMut}; -use move_core_types::account_address::AccountAddress; const TIMESTAMP_NOW_SECONDS: &str = "0x1::timestamp::now_seconds"; const ACCOUNT_BALANCE: &str = "0x1::coin::balance"; const SUPRA_COIN: &str = "0x1::supra_coin::SupraCoin"; const ACCOUNT_SEQ_NUM: &str = "0x1::account::get_sequence_number"; const AUTOMATION_NEXT_TASK_ID: &str = "0x1::automation_registry::get_next_task_index"; +const AUTOMATION_TASK_DETAILS: &str = "0x1::automation_registry::get_task_details"; pub(crate) struct AutomationRegistrationTestContext { executor: FakeExecutor, @@ -75,7 +77,10 @@ impl AutomationRegistrationTestContext { .sign() } - pub(crate) fn check_miscellaneous_output(output: TransactionOutput, expected_status_code: StatusCode) { + pub(crate) fn check_miscellaneous_output( + output: TransactionOutput, + expected_status_code: StatusCode, + ) { match output.status() { TransactionStatus::Keep(ExecutionStatus::MiscellaneousError(maybe_status_code)) => { assert_eq!( @@ -87,31 +92,27 @@ impl AutomationRegistrationTestContext { _ => panic!("Unexpected transaction status: {output:?}"), } } - pub(crate) fn check_discarded_output(output: TransactionOutput, expected_status_code: StatusCode) { + pub(crate) fn check_discarded_output( + output: TransactionOutput, + expected_status_code: StatusCode, + ) { match output.status() { - TransactionStatus::Discard(status_code ) => { - assert_eq!( - status_code, - &expected_status_code, - "{output:?}" - ); + TransactionStatus::Discard(status_code) => { + assert_eq!(status_code, &expected_status_code, "{output:?}"); }, _ => panic!("Unexpected transaction status: {output:?}"), } } - pub (crate) fn chain_time_now(&mut self) -> u64 { - let view_output = self.execute_view_function( - str::parse(TIMESTAMP_NOW_SECONDS).unwrap(), - vec![], - vec![], - ); + pub(crate) fn chain_time_now(&mut self) -> u64 { + let view_output = + self.execute_view_function(str::parse(TIMESTAMP_NOW_SECONDS).unwrap(), vec![], vec![]); let result = view_output.values.expect("Valid result"); assert_eq!(result.len(), 1); bcs::from_bytes::(&result[0]).unwrap() } - pub(crate) fn advance_chain_time_in_secs(&mut self, secs: u64) { + pub(crate) fn advance_chain_time_in_secs(&mut self, secs: u64) { self.set_block_time(secs * 1_000_000); self.new_block() } @@ -125,8 +126,8 @@ impl AutomationRegistrationTestContext { let result = view_output.values.expect("Valid result"); assert_eq!(result.len(), 1); bcs::from_bytes::(&result[0]).unwrap() - } + pub(crate) fn account_sequence_number(&mut self, account_address: AccountAddress) -> u64 { let view_output = self.execute_view_function( str::parse(ACCOUNT_SEQ_NUM).unwrap(), @@ -136,7 +137,31 @@ impl AutomationRegistrationTestContext { let result = view_output.values.expect("Valid result"); assert_eq!(result.len(), 1); bcs::from_bytes::(&result[0]).unwrap() + } + pub(crate) fn get_next_task_index_from_registry(&mut self) -> u64 { + let view_output = self.execute_view_function( + str::parse(AUTOMATION_NEXT_TASK_ID).unwrap(), + vec![], + vec![], + ); + let result = view_output.values.expect("Valid result"); + assert_eq!(result.len(), 1); + bcs::from_bytes::(&result[0]).unwrap() + } + + pub(crate) fn get_task_details(&mut self, index: u64) -> AutomationTaskMetaData { + let view_output = self.execute_view_function( + str::parse(AUTOMATION_TASK_DETAILS).unwrap(), + vec![], + vec![MoveValue::U64(index) + .simple_serialize() + .expect("Successful serialization")], + ); + let result = view_output.values.expect("Valid result"); + assert!(!result.is_empty()); + bcs::from_bytes::(&result[0]) + .expect("Successful deserialization of AutomationTaskMetaData") } } @@ -182,14 +207,7 @@ fn check_successful_registration() { ); // Check automation registry state. - let view_output = test_context.execute_view_function( - str::parse(AUTOMATION_NEXT_TASK_ID).unwrap(), - vec![], - vec![], - ); - let result = view_output.values.expect("Valid result"); - assert_eq!(result.len(), 1); - let next_task_id = bcs::from_bytes::(&result[0]).unwrap(); + let next_task_id = test_context.get_next_task_index_from_registry(); assert_eq!(next_task_id, 1); let sender_seq_num = test_context.account_sequence_number(sender_address); assert_eq!(sender_seq_num, sender_seq_num_old + 1); diff --git a/aptos-move/framework/aptos-stdlib/doc/fixed_point64.md b/aptos-move/framework/aptos-stdlib/doc/fixed_point64.md index 22571462f6c30..642601dc34a1b 100644 --- a/aptos-move/framework/aptos-stdlib/doc/fixed_point64.md +++ b/aptos-move/framework/aptos-stdlib/doc/fixed_point64.md @@ -12,6 +12,7 @@ a 64-bit fractional part. - [Function `sub`](#0x1_fixed_point64_sub) - [Function `add`](#0x1_fixed_point64_add) - [Function `multiply_u128`](#0x1_fixed_point64_multiply_u128) +- [Function `multiply_u128_return_fixpoint64`](#0x1_fixed_point64_multiply_u128_return_fixpoint64) - [Function `divide_u128`](#0x1_fixed_point64_divide_u128) - [Function `create_from_rational`](#0x1_fixed_point64_create_from_rational) - [Function `create_from_raw_value`](#0x1_fixed_point64_create_from_raw_value) @@ -255,6 +256,36 @@ overflows. + + + + +## Function `multiply_u128_return_fixpoint64` + + + +
public fun multiply_u128_return_fixpoint64(val: u128, multiplier: fixed_point64::FixedPoint64): fixed_point64::FixedPoint64
+
+ + + +
+Implementation + + +
public fun multiply_u128_return_fixpoint64(val: u128, multiplier: FixedPoint64): FixedPoint64 {
+    // The product of two 128 bit values has 256 bits, so perform the
+    // multiplication with u256 types and keep the full 256 bit product
+    // to avoid losing accuracy.
+    let unscaled_product = (val as u256) * (multiplier.value as u256);
+    // Check whether the value is too large.
+    assert!(unscaled_product <= MAX_U128, EMULTIPLICATION);
+    create_from_raw_value((unscaled_product as u128))
+}
+
+ + +
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 dcedd7739d765..aeeca7f3e7502 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 @@ -572,6 +572,33 @@ pub enum EntryFunctionCall { stakes: Vec, }, + /// Initialize a delegation pool without actual coin but withdraw from the owner's account. + PboDelegationPoolInitializeDelegationPoolWithAmount { + multisig_admin: AccountAddress, + amount: u64, + operator_commission_percentage: u64, + delegation_pool_creation_seed: Vec, + delegator_address: Vec, + principle_stake: Vec, + unlock_numerators: Vec, + unlock_denominator: u64, + unlock_start_time: u64, + unlock_duration: u64, + }, + + /// Initialize a delegation pool without actual coin but withdraw from the owner's account. + PboDelegationPoolInitializeDelegationPoolWithAmountWithoutMultisigAdmin { + amount: u64, + operator_commission_percentage: u64, + delegation_pool_creation_seed: Vec, + delegator_address: Vec, + principle_stake: Vec, + unlock_numerators: Vec, + unlock_denominator: u64, + unlock_start_time: u64, + unlock_duration: u64, + }, + /// Updates the `principle_stake` of each `delegator` in `delegators` according to the amount specified /// at the corresponding index of `new_principle_stakes`. Also ensures that the `delegator`'s `active` stake /// is as close to the specified amount as possible. The locked amount is subject to the vesting schedule @@ -644,6 +671,20 @@ pub enum EntryFunctionCall { new_commission_percentage: u64, }, + /// Pre-condition: `cumulative_unlocked_fraction` should be zero, which would indicate that even + /// though there are principle stake holders, none of those have yet called `unlock` on the pool + /// thus it is ``safe'' to change the schedule + /// This is a temporary measure to allow Supra Foundation to change the schedule for those pools + /// there were initialized with ``dummy/default'' schedule. This method must be disabled + /// before external validators are allowed to join the validator set. + PboDelegationPoolUpdateUnlockingSchedule { + pool_address: AccountAddress, + unlock_numerators: Vec, + unlock_denominator: u64, + unlock_start_time: u64, + unlock_duration: u64, + }, + /// Withdraw `amount` of owned inactive stake from the delegation pool at `pool_address`. PboDelegationPoolWithdraw { pool_address: AccountAddress, @@ -1559,6 +1600,50 @@ impl EntryFunctionCall { delegators, stakes, } => pbo_delegation_pool_fund_delegators_with_stake(pool_address, delegators, stakes), + PboDelegationPoolInitializeDelegationPoolWithAmount { + multisig_admin, + amount, + operator_commission_percentage, + delegation_pool_creation_seed, + delegator_address, + principle_stake, + unlock_numerators, + unlock_denominator, + unlock_start_time, + unlock_duration, + } => pbo_delegation_pool_initialize_delegation_pool_with_amount( + multisig_admin, + amount, + operator_commission_percentage, + delegation_pool_creation_seed, + delegator_address, + principle_stake, + unlock_numerators, + unlock_denominator, + unlock_start_time, + unlock_duration, + ), + PboDelegationPoolInitializeDelegationPoolWithAmountWithoutMultisigAdmin { + amount, + operator_commission_percentage, + delegation_pool_creation_seed, + delegator_address, + principle_stake, + unlock_numerators, + unlock_denominator, + unlock_start_time, + unlock_duration, + } => pbo_delegation_pool_initialize_delegation_pool_with_amount_without_multisig_admin( + amount, + operator_commission_percentage, + delegation_pool_creation_seed, + delegator_address, + principle_stake, + unlock_numerators, + unlock_denominator, + unlock_start_time, + unlock_duration, + ), PboDelegationPoolLockDelegatorsStakes { pool_address, delegators, @@ -1596,6 +1681,19 @@ impl EntryFunctionCall { PboDelegationPoolUpdateCommissionPercentage { new_commission_percentage, } => pbo_delegation_pool_update_commission_percentage(new_commission_percentage), + PboDelegationPoolUpdateUnlockingSchedule { + pool_address, + unlock_numerators, + unlock_denominator, + unlock_start_time, + unlock_duration, + } => pbo_delegation_pool_update_unlocking_schedule( + pool_address, + unlock_numerators, + unlock_denominator, + unlock_start_time, + unlock_duration, + ), PboDelegationPoolWithdraw { pool_address, amount, @@ -3424,6 +3522,80 @@ pub fn pbo_delegation_pool_fund_delegators_with_stake( )) } +/// Initialize a delegation pool without actual coin but withdraw from the owner's account. +pub fn pbo_delegation_pool_initialize_delegation_pool_with_amount( + multisig_admin: AccountAddress, + amount: u64, + operator_commission_percentage: u64, + delegation_pool_creation_seed: Vec, + delegator_address: Vec, + principle_stake: Vec, + unlock_numerators: Vec, + unlock_denominator: u64, + unlock_start_time: u64, + unlock_duration: u64, +) -> 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!("pbo_delegation_pool").to_owned(), + ), + ident_str!("initialize_delegation_pool_with_amount").to_owned(), + vec![], + vec![ + bcs::to_bytes(&multisig_admin).unwrap(), + bcs::to_bytes(&amount).unwrap(), + bcs::to_bytes(&operator_commission_percentage).unwrap(), + bcs::to_bytes(&delegation_pool_creation_seed).unwrap(), + bcs::to_bytes(&delegator_address).unwrap(), + bcs::to_bytes(&principle_stake).unwrap(), + bcs::to_bytes(&unlock_numerators).unwrap(), + bcs::to_bytes(&unlock_denominator).unwrap(), + bcs::to_bytes(&unlock_start_time).unwrap(), + bcs::to_bytes(&unlock_duration).unwrap(), + ], + )) +} + +/// Initialize a delegation pool without actual coin but withdraw from the owner's account. +pub fn pbo_delegation_pool_initialize_delegation_pool_with_amount_without_multisig_admin( + amount: u64, + operator_commission_percentage: u64, + delegation_pool_creation_seed: Vec, + delegator_address: Vec, + principle_stake: Vec, + unlock_numerators: Vec, + unlock_denominator: u64, + unlock_start_time: u64, + unlock_duration: u64, +) -> 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!("pbo_delegation_pool").to_owned(), + ), + ident_str!("initialize_delegation_pool_with_amount_without_multisig_admin").to_owned(), + vec![], + vec![ + bcs::to_bytes(&amount).unwrap(), + bcs::to_bytes(&operator_commission_percentage).unwrap(), + bcs::to_bytes(&delegation_pool_creation_seed).unwrap(), + bcs::to_bytes(&delegator_address).unwrap(), + bcs::to_bytes(&principle_stake).unwrap(), + bcs::to_bytes(&unlock_numerators).unwrap(), + bcs::to_bytes(&unlock_denominator).unwrap(), + bcs::to_bytes(&unlock_start_time).unwrap(), + bcs::to_bytes(&unlock_duration).unwrap(), + ], + )) +} + /// Updates the `principle_stake` of each `delegator` in `delegators` according to the amount specified /// at the corresponding index of `new_principle_stakes`. Also ensures that the `delegator`'s `active` stake /// is as close to the specified amount as possible. The locked amount is subject to the vesting schedule @@ -3620,6 +3792,39 @@ pub fn pbo_delegation_pool_update_commission_percentage( )) } +/// Pre-condition: `cumulative_unlocked_fraction` should be zero, which would indicate that even +/// though there are principle stake holders, none of those have yet called `unlock` on the pool +/// thus it is ``safe'' to change the schedule +/// This is a temporary measure to allow Supra Foundation to change the schedule for those pools +/// there were initialized with ``dummy/default'' schedule. This method must be disabled +/// before external validators are allowed to join the validator set. +pub fn pbo_delegation_pool_update_unlocking_schedule( + pool_address: AccountAddress, + unlock_numerators: Vec, + unlock_denominator: u64, + unlock_start_time: u64, + unlock_duration: u64, +) -> 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!("pbo_delegation_pool").to_owned(), + ), + ident_str!("update_unlocking_schedule").to_owned(), + vec![], + vec![ + bcs::to_bytes(&pool_address).unwrap(), + bcs::to_bytes(&unlock_numerators).unwrap(), + bcs::to_bytes(&unlock_denominator).unwrap(), + bcs::to_bytes(&unlock_start_time).unwrap(), + bcs::to_bytes(&unlock_duration).unwrap(), + ], + )) +} + /// Withdraw `amount` of owned inactive stake from the delegation pool at `pool_address`. pub fn pbo_delegation_pool_withdraw( pool_address: AccountAddress, @@ -6168,6 +6373,49 @@ mod decoder { } } + pub fn pbo_delegation_pool_initialize_delegation_pool_with_amount( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::PboDelegationPoolInitializeDelegationPoolWithAmount { + multisig_admin: bcs::from_bytes(script.args().get(0)?).ok()?, + amount: bcs::from_bytes(script.args().get(1)?).ok()?, + operator_commission_percentage: bcs::from_bytes(script.args().get(2)?).ok()?, + delegation_pool_creation_seed: bcs::from_bytes(script.args().get(3)?).ok()?, + delegator_address: bcs::from_bytes(script.args().get(4)?).ok()?, + principle_stake: bcs::from_bytes(script.args().get(5)?).ok()?, + unlock_numerators: bcs::from_bytes(script.args().get(6)?).ok()?, + unlock_denominator: bcs::from_bytes(script.args().get(7)?).ok()?, + unlock_start_time: bcs::from_bytes(script.args().get(8)?).ok()?, + unlock_duration: bcs::from_bytes(script.args().get(9)?).ok()?, + }, + ) + } else { + None + } + } + + pub fn pbo_delegation_pool_initialize_delegation_pool_with_amount_without_multisig_admin( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::PboDelegationPoolInitializeDelegationPoolWithAmountWithoutMultisigAdmin { + amount : bcs::from_bytes(script.args().get(0)?).ok()?, + operator_commission_percentage : bcs::from_bytes(script.args().get(1)?).ok()?, + delegation_pool_creation_seed : bcs::from_bytes(script.args().get(2)?).ok()?, + delegator_address : bcs::from_bytes(script.args().get(3)?).ok()?, + principle_stake : bcs::from_bytes(script.args().get(4)?).ok()?, + unlock_numerators : bcs::from_bytes(script.args().get(5)?).ok()?, + unlock_denominator : bcs::from_bytes(script.args().get(6)?).ok()?, + unlock_start_time : bcs::from_bytes(script.args().get(7)?).ok()?, + unlock_duration : bcs::from_bytes(script.args().get(8)?).ok()?, + }) + } else { + None + } + } + pub fn pbo_delegation_pool_lock_delegators_stakes( payload: &TransactionPayload, ) -> Option { @@ -6286,6 +6534,24 @@ mod decoder { } } + pub fn pbo_delegation_pool_update_unlocking_schedule( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::PboDelegationPoolUpdateUnlockingSchedule { + pool_address: bcs::from_bytes(script.args().get(0)?).ok()?, + unlock_numerators: bcs::from_bytes(script.args().get(1)?).ok()?, + unlock_denominator: bcs::from_bytes(script.args().get(2)?).ok()?, + unlock_start_time: bcs::from_bytes(script.args().get(3)?).ok()?, + unlock_duration: bcs::from_bytes(script.args().get(4)?).ok()?, + }, + ) + } else { + None + } + } + pub fn pbo_delegation_pool_withdraw(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::PboDelegationPoolWithdraw { @@ -7550,6 +7816,11 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy + + + +## Function `multiply_u64_return_fixpoint32` + + + +
public fun multiply_u64_return_fixpoint32(val: u64, multiplier: fixed_point32::FixedPoint32): fixed_point32::FixedPoint32
+
+ + + +
+Implementation + + +
public fun multiply_u64_return_fixpoint32(val: u64, multiplier: FixedPoint32): FixedPoint32 {
+    // The product of two 64 bit values has 128 bits, so perform the
+    // multiplication with u128 types and keep the full 128 bit product
+    // to avoid losing accuracy.
+    let unscaled_product = (val as u128) * (multiplier.value as u128);
+    // Check whether the value is too large.
+    assert!(unscaled_product <= MAX_U64, EMULTIPLICATION);
+    create_from_raw_value((unscaled_product as u64))
+}
+
+ + +
diff --git a/aptos-move/framework/supra-framework/doc/pbo_delegation_pool.md b/aptos-move/framework/supra-framework/doc/pbo_delegation_pool.md index 0cec80a28086d..4e17e991cc979 100644 --- a/aptos-move/framework/supra-framework/doc/pbo_delegation_pool.md +++ b/aptos-move/framework/supra-framework/doc/pbo_delegation_pool.md @@ -131,6 +131,7 @@ transferred to A - [Struct `UnlockStakeEvent`](#0x1_pbo_delegation_pool_UnlockStakeEvent) - [Struct `WithdrawStakeEvent`](#0x1_pbo_delegation_pool_WithdrawStakeEvent) - [Struct `DistributeCommissionEvent`](#0x1_pbo_delegation_pool_DistributeCommissionEvent) +- [Struct `UnlockScheduleUpdated`](#0x1_pbo_delegation_pool_UnlockScheduleUpdated) - [Struct `DistributeCommission`](#0x1_pbo_delegation_pool_DistributeCommission) - [Struct `DelegatorReplacemendEvent`](#0x1_pbo_delegation_pool_DelegatorReplacemendEvent) - [Struct `VoteEvent`](#0x1_pbo_delegation_pool_VoteEvent) @@ -159,6 +160,11 @@ transferred to A - [Function `get_expected_stake_pool_address`](#0x1_pbo_delegation_pool_get_expected_stake_pool_address) - [Function `min_remaining_secs_for_commission_change`](#0x1_pbo_delegation_pool_min_remaining_secs_for_commission_change) - [Function `initialize_delegation_pool_with_amount`](#0x1_pbo_delegation_pool_initialize_delegation_pool_with_amount) +- [Function `initialize_delegation_pool_with_amount_without_multisig_admin`](#0x1_pbo_delegation_pool_initialize_delegation_pool_with_amount_without_multisig_admin) +- [Function `get_unlock_schedule`](#0x1_pbo_delegation_pool_get_unlock_schedule) +- [Function `create_schedule_fractions`](#0x1_pbo_delegation_pool_create_schedule_fractions) +- [Function `update_unlocking_schedule`](#0x1_pbo_delegation_pool_update_unlocking_schedule) +- [Function `validate_unlock_schedule_params`](#0x1_pbo_delegation_pool_validate_unlock_schedule_params) - [Function `initialize_delegation_pool`](#0x1_pbo_delegation_pool_initialize_delegation_pool) - [Function `fund_delegators_with_locked_stake`](#0x1_pbo_delegation_pool_fund_delegators_with_locked_stake) - [Function `fund_delegators_with_stake`](#0x1_pbo_delegation_pool_fund_delegators_with_stake) @@ -918,6 +924,58 @@ This struct should be stored in the delegation pool resource account. + + + + +## Struct `UnlockScheduleUpdated` + + + +
#[event]
+struct UnlockScheduleUpdated has drop, store
+
+ + + +
+Fields + + +
+
+pool_address: address +
+
+ +
+
+unlock_numerators: vector<u64> +
+
+ +
+
+unlock_denominator: u64 +
+
+ +
+
+unlock_start_time: u64 +
+
+ +
+
+unlock_duration: u64 +
+
+ +
+
+ +
@@ -1660,6 +1718,15 @@ Commission percentage change is too late in this lockup period, and should be do + + + + +
const EUNLOCKING_ALREADY_STARTED: u64 = 41;
+
+ + + Vector length is not the same. @@ -2112,6 +2179,7 @@ in each of its individual states: (active,inactive,assert_delegation_pool_exists(pool_address); let pool = borrow_global<DelegationPool>(pool_address); let (lockup_cycle_ended, active, _, commission_active, commission_pending_inactive) = + calculate_stake_pool_drift(pool); let total_active_shares = pool_u64::total_shares(&pool.active_shares); @@ -2378,7 +2446,7 @@ Return the minimum remaining time in seconds for commission change, which is one Initialize a delegation pool without actual coin but withdraw from the owner's account. -
public fun initialize_delegation_pool_with_amount(owner: &signer, multisig_admin: option::Option<address>, amount: u64, operator_commission_percentage: u64, delegation_pool_creation_seed: vector<u8>, delegator_address: vector<address>, principle_stake: vector<u64>, unlock_numerators: vector<u64>, unlock_denominator: u64, unlock_start_time: u64, unlock_duration: u64)
+
public entry fun initialize_delegation_pool_with_amount(owner: &signer, multisig_admin: address, amount: u64, operator_commission_percentage: u64, delegation_pool_creation_seed: vector<u8>, delegator_address: vector<address>, principle_stake: vector<u64>, unlock_numerators: vector<u64>, unlock_denominator: u64, unlock_start_time: u64, unlock_duration: u64)
 
@@ -2387,9 +2455,63 @@ Initialize a delegation pool without actual coin but withdraw from the owner's a Implementation -
public fun initialize_delegation_pool_with_amount(
+
public entry fun initialize_delegation_pool_with_amount(
+    owner: &signer,
+    multisig_admin: address,
+    amount: u64,
+    operator_commission_percentage: u64,
+    delegation_pool_creation_seed: vector<u8>,
+    delegator_address: vector<address>,
+    principle_stake: vector<u64>,
+    unlock_numerators: vector<u64>,
+    unlock_denominator: u64,
+    unlock_start_time: u64,
+    unlock_duration: u64
+) acquires DelegationPool, GovernanceRecords, BeneficiaryForOperator, NextCommissionPercentage {
+    assert!(
+        coin::balance<SupraCoin>(signer::address_of(owner)) >= amount,
+        error::invalid_argument(EBALANCE_NOT_SUFFICIENT)
+    );
+    let coin = coin::withdraw<SupraCoin>(owner, amount);
+
+    initialize_delegation_pool(
+        owner,
+        option::some(multisig_admin),
+        operator_commission_percentage,
+        delegation_pool_creation_seed,
+        delegator_address,
+        principle_stake,
+        coin,
+        unlock_numerators,
+        unlock_denominator,
+        unlock_start_time,
+        unlock_duration
+    )
+}
+
+ + + + + + + +## Function `initialize_delegation_pool_with_amount_without_multisig_admin` + +Initialize a delegation pool without actual coin but withdraw from the owner's account. + + +
public entry fun initialize_delegation_pool_with_amount_without_multisig_admin(owner: &signer, amount: u64, operator_commission_percentage: u64, delegation_pool_creation_seed: vector<u8>, delegator_address: vector<address>, principle_stake: vector<u64>, unlock_numerators: vector<u64>, unlock_denominator: u64, unlock_start_time: u64, unlock_duration: u64)
+
+ + + +
+Implementation + + +
public entry fun initialize_delegation_pool_with_amount_without_multisig_admin(
     owner: &signer,
-    multisig_admin: option::Option<address>,
     amount: u64,
     operator_commission_percentage: u64,
     delegation_pool_creation_seed: vector<u8>,
@@ -2408,7 +2530,7 @@ Initialize a delegation pool without actual coin but withdraw from the owner's a
 
     initialize_delegation_pool(
         owner,
-        multisig_admin,
+        option::none<address>(),
         operator_commission_percentage,
         delegation_pool_creation_seed,
         delegator_address,
@@ -2424,6 +2546,212 @@ Initialize a delegation pool without actual coin but withdraw from the owner's a
 
 
 
+
+ + + +## Function `get_unlock_schedule` + +Return the unlock schedule of the pool as (schedule, start_time, period_duration, last_unlock_period, cumulative_unlocked_fraction) + + +
#[view]
+public fun get_unlock_schedule(pool_address: address): (vector<fixed_point64::FixedPoint64>, u64, u64, u64, fixed_point64::FixedPoint64)
+
+ + + +
+Implementation + + +
public fun get_unlock_schedule(
+    pool_address: address
+): (vector<FixedPoint64>, u64, u64, u64, FixedPoint64) acquires DelegationPool {
+    let uschedule =
+        borrow_global<DelegationPool>(pool_address).principle_unlock_schedule;
+    (
+        uschedule.schedule,
+        uschedule.start_timestamp_secs,
+        uschedule.period_duration,
+        uschedule.last_unlock_period,
+        uschedule.cumulative_unlocked_fraction
+    )
+
+}
+
+ + + +
+ + + +## Function `create_schedule_fractions` + + + +
fun create_schedule_fractions(unlock_numerators: &vector<u64>, unlock_denominator: u64): vector<fixed_point64::FixedPoint64>
+
+ + + +
+Implementation + + +
fun create_schedule_fractions(unlock_numerators: &vector<u64>, unlock_denominator: u64) : vector<FixedPoint64> {
+
+//Create unlock schedule
+    let schedule = vector::empty();
+    vector::for_each_ref(
+        unlock_numerators,
+        |e| {
+            let fraction =
+                fixed_point64::create_from_rational(
+                    (*e as u128), (unlock_denominator as u128)
+                );
+            vector::push_back(&mut schedule, fraction);
+        }
+    );
+
+    schedule
+
+}
+
+ + + +
+ + + +## Function `update_unlocking_schedule` + +Pre-condition: cumulative_unlocked_fraction should be zero, which would indicate that even +though there are principle stake holders, none of those have yet called unlock on the pool +thus it is ```safe'' to change the schedule +This is a temporary measure to allow Supra Foundation to change the schedule for those pools +there were initialized with ```dummy/default'' schedule. This method must be disabled +before external validators are allowed to join the validator set. + + +
public entry fun update_unlocking_schedule(multisig_admin: &signer, pool_address: address, unlock_numerators: vector<u64>, unlock_denominator: u64, unlock_start_time: u64, unlock_duration: u64)
+
+ + + +
+Implementation + + +
public entry fun update_unlocking_schedule(
+    multisig_admin: &signer,
+    pool_address: address,
+    unlock_numerators: vector<u64>,
+    unlock_denominator: u64,
+    unlock_start_time: u64,
+    unlock_duration: u64
+) acquires DelegationPool {
+    assert!(
+        is_admin(signer::address_of(multisig_admin), pool_address),
+        error::permission_denied(ENOT_AUTHORIZED)
+    );
+    let pool = borrow_global_mut<DelegationPool>(pool_address);
+    assert!(
+        fixed_point64::is_zero(
+            pool.principle_unlock_schedule.cumulative_unlocked_fraction
+        ),
+        error::invalid_state(EUNLOCKING_ALREADY_STARTED)
+    );
+
+    validate_unlock_schedule_params(
+        &unlock_numerators,
+        unlock_denominator,
+        unlock_start_time,
+        unlock_duration
+    );
+
+    //Create unlock schedule fractions
+    let schedule = create_schedule_fractions(&unlock_numerators,unlock_denominator);
+
+    pool.principle_unlock_schedule = UnlockSchedule {
+        schedule: schedule,
+        start_timestamp_secs: unlock_start_time,
+        period_duration: unlock_duration,
+        last_unlock_period: 0,
+        cumulative_unlocked_fraction: fixed_point64::create_from_rational(0, 1)
+    };
+    event::emit(
+        UnlockScheduleUpdated {
+            pool_address,
+            unlock_numerators,
+            unlock_denominator,
+            unlock_start_time,
+            unlock_duration
+        }
+    );
+
+}
+
+ + + +
+ + + +## Function `validate_unlock_schedule_params` + + + +
fun validate_unlock_schedule_params(unlock_numerators: &vector<u64>, unlock_denominator: u64, _unlock_start_time: u64, unlock_duration: u64)
+
+ + + +
+Implementation + + +
fun validate_unlock_schedule_params(
+    unlock_numerators: &vector<u64>,
+    unlock_denominator: u64,
+    _unlock_start_time: u64,
+    unlock_duration: u64
+) {
+    //Unlock duration can not be zero
+    assert!(unlock_duration > 0, error::invalid_argument(EPERIOD_DURATION_IS_ZERO));
+    //Fraction denominator can not be zero
+    assert!(unlock_denominator != 0, error::invalid_argument(EDENOMINATOR_IS_ZERO));
+    let numerator_length = vector::length(unlock_numerators);
+    //Fraction numerators can not be empty
+    assert!(
+        numerator_length > 0,
+        error::invalid_argument(EEMPTY_UNLOCK_SCHEDULE)
+    );
+    //First and last numerator can not be zero
+    assert!(
+        *vector::borrow(unlock_numerators, 0) != 0,
+        error::invalid_argument(ESCHEDULE_WITH_ZERO_FRACTION)
+    );
+    assert!(
+        *vector::borrow(unlock_numerators, numerator_length - 1) != 0,
+        error::invalid_argument(ESCHEDULE_WITH_ZERO_FRACTION)
+    );
+
+    let sum = vector::foldr(*unlock_numerators, 0, |e, a| { e + a });
+    //Sum of numerators can not be greater than denominators
+    assert!(
+        sum <= unlock_denominator,
+        error::invalid_argument(ENUMERATORS_GRATER_THAN_DENOMINATOR)
+    );
+
+}
+
+ + +
@@ -2482,31 +2810,13 @@ Ownership over setting the operator/voter is granted to owner who h features::delegation_pools_enabled(), error::invalid_state(EDELEGATION_POOLS_DISABLED) ); - //Unlock start time can not be in the past - assert!( - unlock_start_time >= timestamp::now_seconds(), - error::invalid_argument(ESTARTUP_TIME_IN_PAST) - ); - //Unlock duration can not be zero - assert!(unlock_duration > 0, error::invalid_argument(EPERIOD_DURATION_IS_ZERO)); - //Fraction denominator can not be zero - assert!(unlock_denominator != 0, error::invalid_argument(EDENOMINATOR_IS_ZERO)); - //Fraction numerators can not be empty - assert!( - vector::length(&unlock_numerators) > 0, - error::invalid_argument(EEMPTY_UNLOCK_SCHEDULE) - ); - //Fraction numerators can not be zero - assert!( - !vector::any(&unlock_numerators, |e| { *e == 0 }), - error::invalid_argument(ESCHEDULE_WITH_ZERO_FRACTION) - ); - let sum = vector::foldr(unlock_numerators, 0, |e, a| { e + a }); - //Sum of numerators can not be greater than denominators - assert!( - sum <= unlock_denominator, - error::invalid_argument(ENUMERATORS_GRATER_THAN_DENOMINATOR) + + validate_unlock_schedule_params( + &unlock_numerators, + unlock_denominator, + unlock_start_time, + unlock_duration ); let owner_address = signer::address_of(owner); @@ -2561,17 +2871,7 @@ Ownership over setting the operator/voter is granted to owner who h }; //Create unlock schedule - let schedule = vector::empty(); - vector::for_each_ref( - &unlock_numerators, - |e| { - let fraction = - fixed_point64::create_from_rational( - (*e as u128), (unlock_denominator as u128) - ); - vector::push_back(&mut schedule, fraction); - } - ); + let schedule = create_schedule_fractions(&unlock_numerators,unlock_denominator); move_to( &stake_pool_signer, @@ -2690,11 +2990,7 @@ Ownership over setting the operator/voter is granted to owner who h // Record the details of the lockup event. Note that only the newly locked // amount is reported and not the total locked amount. event::emit( - UnlockScheduleApplied { - pool_address, - delegator, - amount: stake - } + UnlockScheduleApplied { pool_address, delegator, amount: stake } ); } } @@ -3873,7 +4169,10 @@ Add amount of coins to the delegation pool pool_addressif amount to add is 0 so no event is emitted if (amount == 0) { return }; // fail unlock of less than `MIN_COINS_ON_SHARES_POOL` - assert!(amount >= MIN_COINS_ON_SHARES_POOL, error::invalid_argument(EMINIMUM_UNLOCK_AMOUNT)); + assert!( + amount >= MIN_COINS_ON_SHARES_POOL, + error::invalid_argument(EMINIMUM_UNLOCK_AMOUNT) + ); // synchronize delegation and stake pools before any user operation synchronize_delegation_pool(pool_address); @@ -4008,7 +4307,10 @@ authorized to reactivate their own stake. // short-circuit if amount to reactivate is 0 so no event is emitted if (amount == 0) { return }; // fail unlock of less than `MIN_COINS_ON_SHARES_POOL` - assert!(amount >= MIN_COINS_ON_SHARES_POOL, error::invalid_argument(EMINIMUM_UNLOCK_AMOUNT)); + assert!( + amount >= MIN_COINS_ON_SHARES_POOL, + error::invalid_argument(EMINIMUM_UNLOCK_AMOUNT) + ); // synchronize delegation and stake pools before any user operation synchronize_delegation_pool(pool_address); @@ -4021,12 +4323,7 @@ authorized to reactivate their own stake. amount ); let observed_lockup_cycle = pool.observed_lockup_cycle; - amount = redeem_inactive_shares( - pool, - delegator, - amount, - observed_lockup_cycle - ); + amount = redeem_inactive_shares(pool, delegator, amount, observed_lockup_cycle); stake::reactivate_stake(&retrieve_stake_pool_owner(pool), amount); @@ -4071,7 +4368,10 @@ validator-owners to prevent it from being abused.
fun admin_withdraw(
-    multisig_admin: &signer, pool_address: address, delegator_address: address, amount: u64
+    multisig_admin: &signer,
+    pool_address: address,
+    delegator_address: address,
+    amount: u64
 ) acquires DelegationPool, GovernanceRecords {
     // Ensure that the caller is the admin of the delegation pool.
     {
@@ -4085,7 +4385,7 @@ validator-owners to prevent it from being abused.
         borrow_global_mut<DelegationPool>(pool_address),
         delegator_address,
         amount,
-        signer::address_of(multisig_admin),
+        signer::address_of(multisig_admin)
     );
 }
 
@@ -4141,7 +4441,8 @@ validator-owners to prevent it from being abused. delegators, new_principle_stakes, |delegator, principle_stake| { - let (active, inactive, pending_inactive) = get_stake(pool_address, delegator); + let (active, inactive, pending_inactive) = + get_stake(pool_address, delegator); // Ensure that all stake to be locked is made `active`. if (active < principle_stake) { @@ -4159,14 +4460,17 @@ validator-owners to prevent it from being abused. if (amount_to_reactivate > MIN_COINS_ON_SHARES_POOL) { // Reactivate the required amount of `pending_inactive` stake first. - authorized_reactivate_stake(delegator, pool_address, amount_to_reactivate); + authorized_reactivate_stake( + delegator, pool_address, amount_to_reactivate + ); }; let active_and_pending_inactive = active + pending_inactive; if (active_and_pending_inactive < principle_stake) { // Need to reactivate some of the `inactive` stake. - let amount_to_withdraw = principle_stake - active_and_pending_inactive; + let amount_to_withdraw = + principle_stake - active_and_pending_inactive; // Ensure that we do not try to withdraw more stake than the `inactive` stake. if (amount_to_withdraw > inactive) { @@ -4182,7 +4486,12 @@ validator-owners to prevent it from being abused. amount_to_withdraw ); // Then allocate it to the delegator again. - fund_delegator_stake(multisig_admin, pool_address, delegator, amount_to_withdraw); + fund_delegator_stake( + multisig_admin, + pool_address, + delegator, + amount_to_withdraw + ); } } }; @@ -4520,16 +4829,26 @@ Note: this does not synchronize with stake pool, therefore the answer may be con let last_unlocked_period = unlock_schedule.last_unlock_period; let schedule_length = vector::length(&unlock_schedule.schedule); let cfraction = unlock_schedule.cumulative_unlocked_fraction; - while (last_unlocked_period < unlock_periods_passed && fixed_point64::less(cfraction, one) - && last_unlocked_period < schedule_length) { - let next_fraction = *vector::borrow(&unlock_schedule.schedule, last_unlocked_period); + while (last_unlocked_period < unlock_periods_passed + && fixed_point64::less(cfraction, one) + && last_unlocked_period < schedule_length) { + let next_fraction = + *vector::borrow(&unlock_schedule.schedule, last_unlocked_period); cfraction = fixed_point64::add(cfraction, next_fraction); last_unlocked_period = last_unlocked_period + 1; }; - if (last_unlocked_period < unlock_periods_passed && fixed_point64::less(cfraction, one)) { - let final_fraction= *vector::borrow(&unlock_schedule.schedule, schedule_length - 1); + if (last_unlocked_period < unlock_periods_passed + && fixed_point64::less(cfraction, one)) { + let final_fraction = + *vector::borrow(&unlock_schedule.schedule, schedule_length - 1); // Acclerate calculation to current period and don't update last_unlocked_period since it is not used anymore - cfraction = fixed_point64::add(cfraction, fixed_point64::multiply_u128_return_fixpoint64((unlock_periods_passed - last_unlocked_period as u128), final_fraction)); + cfraction = fixed_point64::add( + cfraction, + fixed_point64::multiply_u128_return_fixpoint64( + (unlock_periods_passed - last_unlocked_period as u128), + final_fraction + ) + ); cfraction = fixed_point64::min(cfraction, one); }; unlock_schedule.cumulative_unlocked_fraction = cfraction; @@ -4566,7 +4885,10 @@ at most how much active stake there is on the stake pool. // short-circuit if amount to unlock is 0 so no event is emitted if (amount == 0) { return }; // fail unlock of less than `MIN_COINS_ON_SHARES_POOL` - assert!(amount >= MIN_COINS_ON_SHARES_POOL, error::invalid_argument(EMINIMUM_UNLOCK_AMOUNT)); + assert!( + amount >= MIN_COINS_ON_SHARES_POOL, + error::invalid_argument(EMINIMUM_UNLOCK_AMOUNT) + ); // fail unlock of more stake than `active` on the stake pool let (active, _, _, _) = stake::get_stake(pool_address); assert!( @@ -4669,7 +4991,7 @@ Withdraw amount of owned inactive stake from the delegation pool at borrow_global_mut<DelegationPool>(pool_address), delegator_address, amount, - delegator_address, + delegator_address ); }
@@ -4694,7 +5016,10 @@ Withdraw amount of owned inactive stake from the delegation pool at
fun withdraw_internal(
-    pool: &mut DelegationPool, delegator_address: address, amount: u64, recipient_address: address
+    pool: &mut DelegationPool,
+    delegator_address: address,
+    amount: u64,
+    recipient_address: address
 ) acquires GovernanceRecords {
     // TODO: recycle storage when a delegator fully exits the delegation pool.
     // short-circuit if amount to withdraw is 0 so no event is emitted
@@ -4874,7 +5199,12 @@ be explicitly withdrawn by delegator
         pending_withdrawal_exists(pool, delegator_address);
     if (withdrawal_exists
         && withdrawal_olc.index < pool.observed_lockup_cycle.index) {
-        withdraw_internal(pool, delegator_address, MAX_U64, delegator_address);
+        withdraw_internal(
+            pool,
+            delegator_address,
+            MAX_U64,
+            delegator_address
+        );
     }
 }
 
diff --git a/aptos-move/framework/supra-framework/doc/vesting_without_staking.md b/aptos-move/framework/supra-framework/doc/vesting_without_staking.md index f0280f356953c..c334e70ebaf48 100644 --- a/aptos-move/framework/supra-framework/doc/vesting_without_staking.md +++ b/aptos-move/framework/supra-framework/doc/vesting_without_staking.md @@ -21,6 +21,9 @@ Vesting without staking contract - [Constants](#@Constants_0) - [Function `vesting_start_secs`](#0x1_vesting_without_staking_vesting_start_secs) - [Function `period_duration_secs`](#0x1_vesting_without_staking_period_duration_secs) +- [Function `get_withdrawal_addr`](#0x1_vesting_without_staking_get_withdrawal_addr) +- [Function `get_contract_admin`](#0x1_vesting_without_staking_get_contract_admin) +- [Function `get_vesting_record`](#0x1_vesting_without_staking_get_vesting_record) - [Function `remaining_grant`](#0x1_vesting_without_staking_remaining_grant) - [Function `beneficiary`](#0x1_vesting_without_staking_beneficiary) - [Function `vesting_contracts`](#0x1_vesting_without_staking_vesting_contracts) @@ -863,6 +866,93 @@ This errors out if the vesting contract with the provided address doesn't exist. + + + + +## Function `get_withdrawal_addr` + + + +
#[view]
+public fun get_withdrawal_addr(vesting_contract_addr: address): address
+
+ + + +
+Implementation + + +
public fun get_withdrawal_addr(vesting_contract_addr: address): address acquires VestingContract {
+    borrow_global<VestingContract>(vesting_contract_addr).withdrawal_address
+}
+
+ + + +
+ + + +## Function `get_contract_admin` + + + +
#[view]
+public fun get_contract_admin(vesting_contract_addr: address): address
+
+ + + +
+Implementation + + +
public fun get_contract_admin(vesting_contract_addr: address): address acquires VestingContract {
+    borrow_global<VestingContract>(vesting_contract_addr).admin
+}
+
+ + + +
+ + + +## Function `get_vesting_record` + + + +
#[view]
+public fun get_vesting_record(vesting_contract_address: address, shareholder_address: address): (u64, u64, u64)
+
+ + + +
+Implementation + + +
public fun get_vesting_record(
+    vesting_contract_address: address, shareholder_address: address
+): (u64, u64, u64) acquires VestingContract {
+    assert_vesting_contract_exists(vesting_contract_address);
+    let vesting_record =
+        simple_map::borrow(
+            &borrow_global<VestingContract>(vesting_contract_address).shareholders,
+            &shareholder_address
+        );
+    (
+        vesting_record.init_amount,
+        vesting_record.left_amount,
+        vesting_record.last_vested_period
+    )
+}
+
+ + +
@@ -882,11 +972,14 @@ Return the remaining grant of shareholder Implementation -
public fun remaining_grant(vesting_contract_address: address, shareholder_address: address)
-    : u64 acquires VestingContract {
+
public fun remaining_grant(
+    vesting_contract_address: address, shareholder_address: address
+): u64 acquires VestingContract {
     assert_vesting_contract_exists(vesting_contract_address);
-    simple_map::borrow(&borrow_global<VestingContract>(vesting_contract_address).shareholders,
-        &shareholder_address).left_amount
+    simple_map::borrow(
+        &borrow_global<VestingContract>(vesting_contract_address).shareholders,
+        &shareholder_address
+    ).left_amount
 }
 
@@ -914,10 +1007,13 @@ This errors out if the vesting contract with the provided address doesn't exist. Implementation -
public fun beneficiary(vesting_contract_address: address, shareholder: address): address acquires VestingContract {
+
public fun beneficiary(
+    vesting_contract_address: address, shareholder: address
+): address acquires VestingContract {
     assert_vesting_contract_exists(vesting_contract_address);
-    get_beneficiary(borrow_global<VestingContract>(vesting_contract_address),
-        shareholder)
+    get_beneficiary(
+        borrow_global<VestingContract>(vesting_contract_address), shareholder
+    )
 }
 
@@ -980,7 +1076,9 @@ This errors out if the vesting contract with the provided address doesn't exist. Implementation -
public fun vesting_schedule(vesting_contract_address: address): VestingSchedule acquires VestingContract {
+
public fun vesting_schedule(
+    vesting_contract_address: address
+): VestingSchedule acquires VestingContract {
     assert_vesting_contract_exists(vesting_contract_address);
     borrow_global<VestingContract>(vesting_contract_address).vesting_schedule
 }
@@ -1007,7 +1105,9 @@ Return the list of all shareholders in the vesting contract.
 Implementation
 
 
-
public fun shareholders(vesting_contract_address: address): vector<address> acquires VestingContract {
+
public fun shareholders(
+    vesting_contract_address: address
+): vector<address> acquires VestingContract {
     assert_active_vesting_contract(vesting_contract_address);
 
     let vesting_contract = borrow_global<VestingContract>(vesting_contract_address);
@@ -1041,8 +1141,9 @@ This returns 0x0 if no shareholder is found for the given beneficiary / the addr
 Implementation
 
 
-
public fun shareholder(vesting_contract_address: address, shareholder_or_beneficiary: address)
-    : address acquires VestingContract {
+
public fun shareholder(
+    vesting_contract_address: address, shareholder_or_beneficiary: address
+): address acquires VestingContract {
     assert_active_vesting_contract(vesting_contract_address);
 
     let shareholders = &shareholders(vesting_contract_address);
@@ -1052,7 +1153,9 @@ This returns 0x0 if no shareholder is found for the given beneficiary / the addr
     let vesting_contract = borrow_global<VestingContract>(vesting_contract_address);
     let result = @0x0;
     let (sh_vec, ben_vec) = simple_map::to_vec_pair(vesting_contract.beneficiaries);
-    let (found, found_index) = vector::index_of(&ben_vec, &shareholder_or_beneficiary);
+    let (found, found_index) = vector::index_of(
+        &ben_vec, &shareholder_or_beneficiary
+    );
     if (found) {
         result = *vector::borrow(&sh_vec, found_index);
     };
@@ -1081,23 +1184,31 @@ Create a vesting schedule with the given schedule of distributions, a vesting st
 
 
 
public fun create_vesting_schedule(
-    schedule: vector<FixedPoint32>, start_timestamp_secs: u64, period_duration: u64,
+    schedule: vector<FixedPoint32>,
+    start_timestamp_secs: u64,
+    period_duration: u64
 ): VestingSchedule {
     let schedule_len = vector::length(&schedule);
     assert!(schedule_len > 0, error::invalid_argument(EEMPTY_VESTING_SCHEDULE));
     // If the first vesting fraction is zero, we can replace it with nonzero by increasing start time
-    assert!(fixed_point32::get_raw_value(*vector::borrow(&schedule, 0)) != 0,
-        error::invalid_argument(EEMPTY_VESTING_SCHEDULE));
+    assert!(
+        fixed_point32::get_raw_value(*vector::borrow(&schedule, 0)) != 0,
+        error::invalid_argument(EEMPTY_VESTING_SCHEDULE)
+    );
     // last vesting fraction must be non zero to ensure that no amount remains unvested forever.
-    assert!(fixed_point32::get_raw_value(*vector::borrow(&schedule, schedule_len - 1))
-        != 0,
-        error::invalid_argument(EEMPTY_VESTING_SCHEDULE));
-    assert!(period_duration > 0, error::invalid_argument(EZERO_VESTING_SCHEDULE_PERIOD));
+    assert!(
+        fixed_point32::get_raw_value(*vector::borrow(&schedule, schedule_len - 1))
+            != 0,
+        error::invalid_argument(EEMPTY_VESTING_SCHEDULE)
+    );
+    assert!(
+        period_duration > 0, error::invalid_argument(EZERO_VESTING_SCHEDULE_PERIOD)
+    );
     VestingSchedule {
         schedule,
         start_timestamp_secs,
         period_duration,
-        last_vested_period: 0,
+        last_vested_period: 0
     }
 }
 
@@ -1121,7 +1232,7 @@ Create a vesting schedule with the given schedule of distributions, a vesting st Implementation -
public entry fun create_vesting_contract_with_amounts (
+
public entry fun create_vesting_contract_with_amounts(
     admin: &signer,
     shareholders: vector<address>,
     shares: vector<u64>,
@@ -1130,82 +1241,106 @@ Create a vesting schedule with the given schedule of distributions, a vesting st
     start_timestamp_secs: u64,
     period_duration: u64,
     withdrawal_address: address,
-    contract_creation_seed: vector<u8>,
+    contract_creation_seed: vector<u8>
 ) acquires AdminStore {
-    assert!(!system_addresses::is_reserved_address(withdrawal_address),
-        error::invalid_argument(EINVALID_WITHDRAWAL_ADDRESS),);
+    assert!(
+        !system_addresses::is_reserved_address(withdrawal_address),
+        error::invalid_argument(EINVALID_WITHDRAWAL_ADDRESS)
+    );
     assert_account_is_registered_for_supra(withdrawal_address);
-    assert!(vector::length(&shareholders) > 0,
-        error::invalid_argument(ENO_SHAREHOLDERS));
+    assert!(
+        vector::length(&shareholders) > 0,
+        error::invalid_argument(ENO_SHAREHOLDERS)
+    );
     assert!(
         vector::length(&shareholders) == vector::length(&shares),
-        error::invalid_argument(ESHARES_LENGTH_MISMATCH),
+        error::invalid_argument(ESHARES_LENGTH_MISMATCH)
     );
 
     // If this is the first time this admin account has created a vesting contract, initialize the admin store.
     let admin_address = signer::address_of(admin);
     if (!exists<AdminStore>(admin_address)) {
-        move_to(admin,
+        move_to(
+            admin,
             AdminStore {
                 vesting_contracts: vector::empty<address>(),
                 nonce: 0,
-                create_events: new_event_handle<CreateVestingContractEvent>(admin),
-            });
+                create_events: new_event_handle<CreateVestingContractEvent>(admin)
+            }
+        );
     };
 
     // Initialize the vesting contract in a new resource account. This allows the same admin to create multiple
     // pools.
-    let (contract_signer, contract_signer_cap) = create_vesting_contract_account(admin,
-        contract_creation_seed);
+    let (contract_signer, contract_signer_cap) =
+        create_vesting_contract_account(admin, contract_creation_seed);
     let contract_signer_address = signer::address_of(&contract_signer);
-    let schedule = vector::map_ref(&vesting_numerators, |numerator| {
-        let event = fixed_point32::create_from_rational(*numerator, vesting_denominator);
-        event
-    });
+    let schedule = vector::map_ref(
+        &vesting_numerators,
+        |numerator| {
+            let event =
+                fixed_point32::create_from_rational(*numerator, vesting_denominator);
+            event
+        }
+    );
 
-    let vesting_schedule = create_vesting_schedule(schedule, start_timestamp_secs, period_duration);
+    let vesting_schedule =
+        create_vesting_schedule(schedule, start_timestamp_secs, period_duration);
     let shareholders_map = simple_map::create<address, VestingRecord>();
     let grant_amount = 0;
-    vector::for_each_reverse(shares, |amount| {
-        let shareholder = vector::pop_back(&mut shareholders);
-        simple_map::add(&mut shareholders_map,
-            shareholder,
-            VestingRecord {
-                init_amount: amount,
-                left_amount: amount,
-                last_vested_period: vesting_schedule.last_vested_period,
-            }
-        );
-        grant_amount = grant_amount + amount;
-    });
+    vector::for_each_reverse(
+        shares,
+        |amount| {
+            let shareholder = vector::pop_back(&mut shareholders);
+            simple_map::add(
+                &mut shareholders_map,
+                shareholder,
+                VestingRecord {
+                    init_amount: amount,
+                    left_amount: amount,
+                    last_vested_period: vesting_schedule.last_vested_period
+                }
+            );
+            grant_amount = grant_amount + amount;
+        }
+    );
     assert!(grant_amount > 0, error::invalid_argument(EZERO_GRANT));
     coin::transfer<SupraCoin>(admin, contract_signer_address, grant_amount);
 
     let admin_store = borrow_global_mut<AdminStore>(admin_address);
     vector::push_back(&mut admin_store.vesting_contracts, contract_signer_address);
-    emit_event(&mut admin_store.create_events,
+    emit_event(
+        &mut admin_store.create_events,
         CreateVestingContractEvent {
             withdrawal_address,
             grant_amount,
-            vesting_contract_address: contract_signer_address,
-        },
+            vesting_contract_address: contract_signer_address
+        }
     );
 
-    move_to(&contract_signer,
+    move_to(
+        &contract_signer,
         VestingContract {
             state: VESTING_POOL_ACTIVE,
             admin: admin_address,
-            shareholders:shareholders_map,
+            shareholders: shareholders_map,
             beneficiaries: simple_map::create<address, address>(),
             vesting_schedule,
             withdrawal_address,
             signer_cap: contract_signer_cap,
-            set_beneficiary_events: new_event_handle<SetBeneficiaryEvent>(&contract_signer),
+            set_beneficiary_events: new_event_handle<SetBeneficiaryEvent>(
+                &contract_signer
+            ),
             vest_events: new_event_handle<VestEvent>(&contract_signer),
             terminate_events: new_event_handle<TerminateEvent>(&contract_signer),
-            admin_withdraw_events: new_event_handle<AdminWithdrawEvent>(&contract_signer),
-            shareholder_removed_events: new_event_handle<ShareHolderRemovedEvent>(&contract_signer),
-        });
+            admin_withdraw_events: new_event_handle<AdminWithdrawEvent>(
+                &contract_signer
+            ),
+            shareholder_removed_events: new_event_handle<ShareHolderRemovedEvent>(
+                &contract_signer
+            )
+        }
+    );
 }
 
@@ -1234,14 +1369,18 @@ Create a vesting contract with a given configurations. buy_ins: SimpleMap<address, Coin<SupraCoin>>, vesting_schedule: VestingSchedule, withdrawal_address: address, - contract_creation_seed: vector<u8>, + contract_creation_seed: vector<u8> ): address acquires AdminStore { - assert!(!system_addresses::is_reserved_address(withdrawal_address), - error::invalid_argument(EINVALID_WITHDRAWAL_ADDRESS),); + assert!( + !system_addresses::is_reserved_address(withdrawal_address), + error::invalid_argument(EINVALID_WITHDRAWAL_ADDRESS) + ); assert_account_is_registered_for_supra(withdrawal_address); let shareholders_address = &simple_map::keys(&buy_ins); - assert!(vector::length(shareholders_address) > 0, - error::invalid_argument(ENO_SHAREHOLDERS)); + assert!( + vector::length(shareholders_address) > 0, + error::invalid_argument(ENO_SHAREHOLDERS) + ); let shareholders = simple_map::create<address, VestingRecord>(); let grant = coin::zero<SupraCoin>(); @@ -1252,12 +1391,13 @@ Create a vesting contract with a given configurations. let buy_in = vector::pop_back(&mut buy_ins); let init = coin::value(&buy_in); coin::merge(&mut grant, buy_in); - simple_map::add(&mut shareholders, + simple_map::add( + &mut shareholders, shareholder, VestingRecord { init_amount: init, left_amount: init, - last_vested_period: vesting_schedule.last_vested_period, + last_vested_period: vesting_schedule.last_vested_period } ); grant_amount = grant_amount + init; @@ -1267,32 +1407,36 @@ Create a vesting contract with a given configurations. // If this is the first time this admin account has created a vesting contract, initialize the admin store. let admin_address = signer::address_of(admin); if (!exists<AdminStore>(admin_address)) { - move_to(admin, + move_to( + admin, AdminStore { vesting_contracts: vector::empty<address>(), nonce: 0, - create_events: new_event_handle<CreateVestingContractEvent>(admin), - }); + create_events: new_event_handle<CreateVestingContractEvent>(admin) + } + ); }; // Initialize the vesting contract in a new resource account. This allows the same admin to create multiple // pools. - let (contract_signer, contract_signer_cap) = create_vesting_contract_account(admin, - contract_creation_seed); + let (contract_signer, contract_signer_cap) = + create_vesting_contract_account(admin, contract_creation_seed); let contract_signer_address = signer::address_of(&contract_signer); coin::deposit(contract_signer_address, grant); let admin_store = borrow_global_mut<AdminStore>(admin_address); vector::push_back(&mut admin_store.vesting_contracts, contract_signer_address); - emit_event(&mut admin_store.create_events, + emit_event( + &mut admin_store.create_events, CreateVestingContractEvent { withdrawal_address, grant_amount, - vesting_contract_address: contract_signer_address, - }, + vesting_contract_address: contract_signer_address + } ); - move_to(&contract_signer, + move_to( + &contract_signer, VestingContract { state: VESTING_POOL_ACTIVE, admin: admin_address, @@ -1301,12 +1445,19 @@ Create a vesting contract with a given configurations. vesting_schedule, withdrawal_address, signer_cap: contract_signer_cap, - set_beneficiary_events: new_event_handle<SetBeneficiaryEvent>(&contract_signer), + set_beneficiary_events: new_event_handle<SetBeneficiaryEvent>( + &contract_signer + ), vest_events: new_event_handle<VestEvent>(&contract_signer), terminate_events: new_event_handle<TerminateEvent>(&contract_signer), - admin_withdraw_events: new_event_handle<AdminWithdrawEvent>(&contract_signer), - shareholder_removed_events: new_event_handle<ShareHolderRemovedEvent>(&contract_signer), - }); + admin_withdraw_events: new_event_handle<AdminWithdrawEvent>( + &contract_signer + ), + shareholder_removed_events: new_event_handle<ShareHolderRemovedEvent>( + &contract_signer + ) + } + ); vector::destroy_empty(buy_ins); contract_signer_address @@ -1337,7 +1488,8 @@ Unlock any vested portion of the grant. assert_active_vesting_contract(contract_address); let vesting_contract = borrow_global_mut<VestingContract>(contract_address); // Short-circuit if vesting hasn't started yet. - if (vesting_contract.vesting_schedule.start_timestamp_secs > timestamp::now_seconds()) { return }; + if (vesting_contract.vesting_schedule.start_timestamp_secs + > timestamp::now_seconds()) { return }; let shareholders = simple_map::keys(&vesting_contract.shareholders); while (vector::length(&shareholders) > 0) { @@ -1370,16 +1522,22 @@ Unlock any vested portion of the grant. Implementation -
public entry fun vest_individual(contract_address: address, shareholder_address: address) acquires VestingContract {
+
public entry fun vest_individual(
+    contract_address: address, shareholder_address: address
+) acquires VestingContract {
     //check if contract exist, active and shareholder is a member of the contract
     assert_shareholder_exists(contract_address, shareholder_address);
 
     let vesting_contract = borrow_global_mut<VestingContract>(contract_address);
     let beneficiary = get_beneficiary(vesting_contract, shareholder_address);
     // Short-circuit if vesting hasn't started yet.
-    if (vesting_contract.vesting_schedule.start_timestamp_secs > timestamp::now_seconds()) { return };
+    if (vesting_contract.vesting_schedule.start_timestamp_secs
+        > timestamp::now_seconds()) { return };
 
-    let vesting_record = simple_map::borrow_mut(&mut vesting_contract.shareholders, &shareholder_address);
+    let vesting_record =
+        simple_map::borrow_mut(
+            &mut vesting_contract.shareholders, &shareholder_address
+        );
     let signer_cap = &vesting_contract.signer_cap;
 
     // Check if the next vested period has already passed. If not, short-circuit since there's nothing to vest.
@@ -1387,29 +1545,58 @@ Unlock any vested portion of the grant.
     let schedule = &vesting_schedule.schedule;
     let last_vested_period = vesting_record.last_vested_period;
     let next_period_to_vest = last_vested_period + 1;
-    let last_completed_period = (timestamp::now_seconds() - vesting_schedule.start_timestamp_secs)
-        / vesting_schedule.period_duration;
+    let last_completed_period =
+        (timestamp::now_seconds() - vesting_schedule.start_timestamp_secs)
+            / vesting_schedule.period_duration;
 
     // Index is 0-based while period is 1-based so we need to subtract 1.
-    while (last_completed_period >= next_period_to_vest && vesting_record.left_amount > 0) {
+
+    while (last_completed_period >= next_period_to_vest && vesting_record.left_amount > 0 && next_period_to_vest <= vector::length(schedule)) {
         let schedule_index = next_period_to_vest - 1;
-        let vesting_fraction = if (schedule_index < vector::length(schedule)) {
-            *vector::borrow(schedule, schedule_index)
+        let vesting_fraction = *vector::borrow(schedule, schedule_index);
+        vest_transfer(vesting_record, signer_cap, beneficiary, vesting_fraction);
+        emit_event(&mut vesting_contract.vest_events,
+            VestEvent {
+                admin: vesting_contract.admin,
+                shareholder_address,
+                vesting_contract_address: contract_address,
+                period_vested: next_period_to_vest
+            }
+        );
+        next_period_to_vest = next_period_to_vest + 1;
+    };
+
+    if(last_completed_period >= next_period_to_vest && vesting_record.left_amount > 0) {
+        let final_fraction = *vector::borrow(schedule, vector::length(schedule) - 1);
+        let final_fraction_amount = fixed_point32::multiply_u64(vesting_record.init_amount, final_fraction);
+        // Determine how many periods is needed based on the left_amount
+        let added_fraction = fixed_point32::multiply_u64_return_fixpoint32(last_completed_period - next_period_to_vest + 1, final_fraction);
+        // If the added_fraction is greater than or equal to the left_amount, then we can vest all the left_amount
+        let periods_need =
+            if (fixed_point32::multiply_u64(vesting_record.init_amount, added_fraction) >= vesting_record.left_amount){
+            let result =  vesting_record.left_amount / final_fraction_amount;
+                // check if `left_amount` is perfectly divisible by `final_fraction_amount`
+                  if (vesting_record.left_amount == final_fraction_amount*result) {
+                   result
+                } else {
+                   result + 1
+                }
         } else {
-            // Last vesting schedule fraction will repeat until the grant runs out.
-            *vector::borrow(schedule, vector::length(schedule) - 1)
+            last_completed_period - next_period_to_vest + 1
         };
-        vest_transfer(vesting_record, signer_cap, beneficiary, vesting_fraction);
 
+        let total_fraction = fixed_point32::multiply_u64_return_fixpoint32(periods_need, final_fraction);
+        // We don't need to check vesting_record.left_amount > 0 because vest_transfer will handle that.
+        vest_transfer(vesting_record, signer_cap, beneficiary, total_fraction);
+        next_period_to_vest = next_period_to_vest + periods_need;
         emit_event(&mut vesting_contract.vest_events,
             VestEvent {
                 admin: vesting_contract.admin,
-                shareholder_address: shareholder_address,
+                shareholder_address,
                 vesting_contract_address: contract_address,
                 period_vested: next_period_to_vest,
             },
         );
-        next_period_to_vest = next_period_to_vest + 1;
     };
 
     //update last_vested_period for the shareholder
@@ -1445,8 +1632,13 @@ Unlock any vested portion of the grant.
     let vesting_signer = account::create_signer_with_capability(signer_cap);
 
     //amount to be transfer is minimum of what is left and vesting fraction due of init_amount
-    let amount = min(vesting_record.left_amount,
-        fixed_point32::multiply_u64(vesting_record.init_amount, vesting_fraction));
+    let amount =
+        min(
+            vesting_record.left_amount,
+            fixed_point32::multiply_u64(
+                vesting_record.init_amount, vesting_fraction
+            )
+        );
     //update left_amount for the shareholder
     vesting_record.left_amount = vesting_record.left_amount - amount;
     coin::transfer<SupraCoin>(&vesting_signer, beneficiary, amount);
@@ -1481,38 +1673,43 @@ Example usage: If admin find shareholder suspicious, admin can remove it.
     let vesting_contract = borrow_global_mut<VestingContract>(contract_address);
     verify_admin(admin, vesting_contract);
     let vesting_signer = get_vesting_account_signer_internal(vesting_contract);
-    let shareholder_amount = simple_map::borrow(&vesting_contract.shareholders, &shareholder_address)
-        .left_amount;
-    coin::transfer<SupraCoin>(&vesting_signer, vesting_contract.withdrawal_address,
-        shareholder_amount);
-    emit_event(&mut vesting_contract.admin_withdraw_events,
+    let shareholder_amount =
+        simple_map::borrow(&vesting_contract.shareholders, &shareholder_address).left_amount;
+    coin::transfer<SupraCoin>(
+        &vesting_signer, vesting_contract.withdrawal_address, shareholder_amount
+    );
+    emit_event(
+        &mut vesting_contract.admin_withdraw_events,
         AdminWithdrawEvent {
             admin: vesting_contract.admin,
             vesting_contract_address: contract_address,
-            amount: shareholder_amount,
-        },
+            amount: shareholder_amount
+        }
     );
 
     // remove `shareholder_address`` from `vesting_contract.shareholders`
     let shareholders = &mut vesting_contract.shareholders;
-    let (_, shareholders_vesting) = simple_map::remove(shareholders, &shareholder_address);
+    let (_, shareholders_vesting) =
+        simple_map::remove(shareholders, &shareholder_address);
 
     // remove `shareholder_address` from `vesting_contract.beneficiaries`
     let beneficiary = option::none();
     let shareholder_beneficiaries = &mut vesting_contract.beneficiaries;
     // Not all shareholders have their beneficiaries, so before removing them, we need to check if the beneficiary exists
     if (simple_map::contains_key(shareholder_beneficiaries, &shareholder_address)) {
-        let (_, shareholder_baneficiary) = simple_map::remove(shareholder_beneficiaries, &shareholder_address);
+        let (_, shareholder_baneficiary) =
+            simple_map::remove(shareholder_beneficiaries, &shareholder_address);
         beneficiary = option::some(shareholder_baneficiary);
     };
 
     // Emit ShareHolderRemovedEvent
-    emit_event(&mut vesting_contract.shareholder_removed_events,
+    emit_event(
+        &mut vesting_contract.shareholder_removed_events,
         ShareHolderRemovedEvent {
             shareholder: shareholder_address,
             beneficiary,
-            amount: shareholders_vesting.left_amount,
-        },
+            amount: shareholders_vesting.left_amount
+        }
     );
 }
 
@@ -1537,7 +1734,9 @@ Terminate the vesting contract and send all funds back to the withdrawal address Implementation -
public entry fun terminate_vesting_contract(admin: &signer, contract_address: address) acquires VestingContract {
+
public entry fun terminate_vesting_contract(
+    admin: &signer, contract_address: address
+) acquires VestingContract {
     assert_active_vesting_contract(contract_address);
 
     vest(contract_address);
@@ -1547,12 +1746,16 @@ Terminate the vesting contract and send all funds back to the withdrawal address
 
     // Distribute remaining coins to withdrawal address of vesting contract.
     let shareholders_address = simple_map::keys(&vesting_contract.shareholders);
-    vector::for_each_ref(&shareholders_address,
+    vector::for_each_ref(
+        &shareholders_address,
         |shareholder| {
-            let shareholder_amount = simple_map::borrow_mut(&mut vesting_contract.shareholders,
-                shareholder);
+            let shareholder_amount =
+                simple_map::borrow_mut(
+                    &mut vesting_contract.shareholders, shareholder
+                );
             shareholder_amount.left_amount = 0;
-        });
+        }
+    );
     set_terminate_vesting_contract(contract_address);
 }
 
@@ -1578,24 +1781,30 @@ has already been terminated. Implementation -
public entry fun admin_withdraw(admin: &signer, contract_address: address) acquires VestingContract {
+
public entry fun admin_withdraw(
+    admin: &signer, contract_address: address
+) acquires VestingContract {
     let vesting_contract = borrow_global<VestingContract>(contract_address);
-    assert!(vesting_contract.state == VESTING_POOL_TERMINATED,
-        error::invalid_state(EVESTING_CONTRACT_STILL_ACTIVE));
+    assert!(
+        vesting_contract.state == VESTING_POOL_TERMINATED,
+        error::invalid_state(EVESTING_CONTRACT_STILL_ACTIVE)
+    );
 
     let vesting_contract = borrow_global_mut<VestingContract>(contract_address);
     verify_admin(admin, vesting_contract);
     let total_balance = coin::balance<SupraCoin>(contract_address);
     let vesting_signer = get_vesting_account_signer_internal(vesting_contract);
-    coin::transfer<SupraCoin>(&vesting_signer, vesting_contract.withdrawal_address,
-        total_balance);
+    coin::transfer<SupraCoin>(
+        &vesting_signer, vesting_contract.withdrawal_address, total_balance
+    );
 
-    emit_event(&mut vesting_contract.admin_withdraw_events,
+    emit_event(
+        &mut vesting_contract.admin_withdraw_events,
         AdminWithdrawEvent {
             admin: vesting_contract.admin,
             vesting_contract_address: contract_address,
-            amount: total_balance,
-        },
+            amount: total_balance
+        }
     );
 }
 
@@ -1623,7 +1832,7 @@ has already been terminated. admin: &signer, contract_address: address, shareholder: address, - new_beneficiary: address, + new_beneficiary: address ) acquires VestingContract { // Verify that the beneficiary account is set up to receive SUPRA. This is a requirement so distribute() wouldn't // fail and block all other accounts from receiving SUPRA if one beneficiary is not registered. @@ -1636,14 +1845,15 @@ has already been terminated. let beneficiaries = &mut vesting_contract.beneficiaries; simple_map::upsert(beneficiaries, shareholder, new_beneficiary); - emit_event(&mut vesting_contract.set_beneficiary_events, + emit_event( + &mut vesting_contract.set_beneficiary_events, SetBeneficiaryEvent { admin: vesting_contract.admin, vesting_contract_address: contract_address, shareholder, old_beneficiary, - new_beneficiary, - }, + new_beneficiary + } ); }
@@ -1670,13 +1880,20 @@ account.
public entry fun reset_beneficiary(
-    account: &signer, contract_address: address, shareholder: address,
+    account: &signer,
+    contract_address: address,
+    shareholder: address
 ) acquires VestingAccountManagement, VestingContract {
     let vesting_contract = borrow_global_mut<VestingContract>(contract_address);
     let addr = signer::address_of(account);
-    assert!(addr == vesting_contract.admin || addr == get_role_holder(contract_address,
-            utf8(ROLE_BENEFICIARY_RESETTER)),
-        error::permission_denied(EPERMISSION_DENIED),);
+    assert!(
+        addr == vesting_contract.admin
+            || addr
+                == get_role_holder(
+                    contract_address, utf8(ROLE_BENEFICIARY_RESETTER)
+                ),
+        error::permission_denied(EPERMISSION_DENIED)
+    );
 
     let beneficiaries = &mut vesting_contract.beneficiaries;
     if (simple_map::contains_key(beneficiaries, &shareholder)) {
@@ -1708,17 +1925,22 @@ account.
     admin: &signer,
     contract_address: address,
     role: String,
-    role_holder: address,
+    role_holder: address
 ) acquires VestingAccountManagement, VestingContract {
     let vesting_contract = borrow_global_mut<VestingContract>(contract_address);
     verify_admin(admin, vesting_contract);
 
     if (!exists<VestingAccountManagement>(contract_address)) {
         let contract_signer = &get_vesting_account_signer_internal(vesting_contract);
-        move_to(contract_signer,
-            VestingAccountManagement { roles: simple_map::create<String, address>(), })
+        move_to(
+            contract_signer,
+            VestingAccountManagement {
+                roles: simple_map::create<String, address>()
+            }
+        )
     };
-    let roles = &mut borrow_global_mut<VestingAccountManagement>(contract_address).roles;
+    let roles =
+        &mut borrow_global_mut<VestingAccountManagement>(contract_address).roles;
     simple_map::upsert(roles, role, role_holder);
 }
 
@@ -1743,10 +1965,16 @@ account.
public entry fun set_beneficiary_resetter(
-    admin: &signer, contract_address: address, beneficiary_resetter: address,
+    admin: &signer,
+    contract_address: address,
+    beneficiary_resetter: address
 ) acquires VestingAccountManagement, VestingContract {
-    set_management_role(admin, contract_address, utf8(ROLE_BENEFICIARY_RESETTER),
-        beneficiary_resetter);
+    set_management_role(
+        admin,
+        contract_address,
+        utf8(ROLE_BENEFICIARY_RESETTER),
+        beneficiary_resetter
+    );
 }
 
@@ -1769,11 +1997,17 @@ account. Implementation -
public fun get_role_holder(contract_address: address, role: String): address acquires VestingAccountManagement {
-    assert!(exists<VestingAccountManagement>(contract_address),
-        error::not_found(EVESTING_ACCOUNT_HAS_NO_ROLES));
+
public fun get_role_holder(
+    contract_address: address, role: String
+): address acquires VestingAccountManagement {
+    assert!(
+        exists<VestingAccountManagement>(contract_address),
+        error::not_found(EVESTING_ACCOUNT_HAS_NO_ROLES)
+    );
     let roles = &borrow_global<VestingAccountManagement>(contract_address).roles;
-    assert!(simple_map::contains_key(roles, &role), error::not_found(EROLE_NOT_FOUND));
+    assert!(
+        simple_map::contains_key(roles, &role), error::not_found(EROLE_NOT_FOUND)
+    );
     *simple_map::borrow(roles, &role)
 }
 
@@ -1798,7 +2032,9 @@ For emergency use in case the admin needs emergency control of vesting contract Implementation -
public fun get_vesting_account_signer(admin: &signer, contract_address: address): signer acquires VestingContract {
+
public fun get_vesting_account_signer(
+    admin: &signer, contract_address: address
+): signer acquires VestingContract {
     let vesting_contract = borrow_global_mut<VestingContract>(contract_address);
     verify_admin(admin, vesting_contract);
     get_vesting_account_signer_internal(vesting_contract)
@@ -1824,7 +2060,9 @@ For emergency use in case the admin needs emergency control of vesting contract
 Implementation
 
 
-
fun get_vesting_account_signer_internal(vesting_contract: &VestingContract): signer {
+
fun get_vesting_account_signer_internal(
+    vesting_contract: &VestingContract
+): signer {
     account::create_signer_with_capability(&vesting_contract.signer_cap)
 }
 
@@ -1850,8 +2088,9 @@ This address should be deterministic for the same admin and vesting contract cre Implementation -
fun create_vesting_contract_account(admin: &signer, contract_creation_seed: vector<u8>,)
-    : (signer, SignerCapability) acquires AdminStore {
+
fun create_vesting_contract_account(
+    admin: &signer, contract_creation_seed: vector<u8>
+): (signer, SignerCapability) acquires AdminStore {
     let admin_store = borrow_global_mut<AdminStore>(signer::address_of(admin));
     let seed = bcs::to_bytes(&signer::address_of(admin));
     vector::append(&mut seed, bcs::to_bytes(&admin_store.nonce));
@@ -1890,8 +2129,10 @@ This address should be deterministic for the same admin and vesting contract cre
 
 
 
fun verify_admin(admin: &signer, vesting_contract: &VestingContract) {
-    assert!(signer::address_of(admin) == vesting_contract.admin,
-        error::unauthenticated(ENOT_ADMIN));
+    assert!(
+        signer::address_of(admin) == vesting_contract.admin,
+        error::unauthenticated(ENOT_ADMIN)
+    );
 }
 
@@ -1915,8 +2156,10 @@ This address should be deterministic for the same admin and vesting contract cre
fun assert_vesting_contract_exists(contract_address: address) {
-    assert!(exists<VestingContract>(contract_address),
-        error::not_found(EVESTING_CONTRACT_NOT_FOUND));
+    assert!(
+        exists<VestingContract>(contract_address),
+        error::not_found(EVESTING_CONTRACT_NOT_FOUND)
+    );
 }
 
@@ -1939,11 +2182,17 @@ This address should be deterministic for the same admin and vesting contract cre Implementation -
fun assert_shareholder_exists(contract_address: address, shareholder_address: address) acquires VestingContract {
+
fun assert_shareholder_exists(
+    contract_address: address, shareholder_address: address
+) acquires VestingContract {
     assert_active_vesting_contract(contract_address);
-    assert!(simple_map::contains_key(&borrow_global<VestingContract>(contract_address)
-                .shareholders, &shareholder_address),
-        error::not_found(ESHAREHOLDER_NOT_EXIST));
+    assert!(
+        simple_map::contains_key(
+            &borrow_global<VestingContract>(contract_address).shareholders,
+            &shareholder_address
+        ),
+        error::not_found(ESHAREHOLDER_NOT_EXIST)
+    );
 }
 
@@ -1969,8 +2218,10 @@ This address should be deterministic for the same admin and vesting contract cre
fun assert_active_vesting_contract(contract_address: address) acquires VestingContract {
     assert_vesting_contract_exists(contract_address);
     let vesting_contract = borrow_global<VestingContract>(contract_address);
-    assert!(vesting_contract.state == VESTING_POOL_ACTIVE,
-        error::invalid_state(EVESTING_CONTRACT_NOT_ACTIVE));
+    assert!(
+        vesting_contract.state == VESTING_POOL_ACTIVE,
+        error::invalid_state(EVESTING_CONTRACT_NOT_ACTIVE)
+    );
 }
 
@@ -1996,7 +2247,9 @@ This address should be deterministic for the same admin and vesting contract cre
fun get_beneficiary(contract: &VestingContract, shareholder: address): address {
     if (simple_map::contains_key(&contract.beneficiaries, &shareholder)) {
         *simple_map::borrow(&contract.beneficiaries, &shareholder)
-    } else { shareholder }
+    } else {
+        shareholder
+    }
 }
 
@@ -2022,11 +2275,12 @@ This address should be deterministic for the same admin and vesting contract cre
fun set_terminate_vesting_contract(contract_address: address) acquires VestingContract {
     let vesting_contract = borrow_global_mut<VestingContract>(contract_address);
     vesting_contract.state = VESTING_POOL_TERMINATED;
-    emit_event(&mut vesting_contract.terminate_events,
+    emit_event(
+        &mut vesting_contract.terminate_events,
         TerminateEvent {
             admin: vesting_contract.admin,
-            vesting_contract_address: contract_address,
-        },
+            vesting_contract_address: contract_address
+        }
     );
 }
 
diff --git a/types/src/transaction/automated_transaction.rs b/types/src/transaction/automated_transaction.rs index 9a6d02985dba4..bbc012da543d2 100644 --- a/types/src/transaction/automated_transaction.rs +++ b/types/src/transaction/automated_transaction.rs @@ -4,13 +4,13 @@ use crate::chain_id::ChainId; use crate::transaction::automation::AutomationTaskMetaData; use crate::transaction::{EntryFunction, RawTransaction, Transaction, TransactionPayload}; +use anyhow::anyhow; use aptos_crypto::HashValue; use move_core_types::account_address::AccountAddress; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; use std::fmt; use std::fmt::Debug; -use anyhow::anyhow; /// A transaction that has been created based on the automation-task in automation registry. /// @@ -143,7 +143,6 @@ impl AutomatedTransaction { /// otherwise None. pub fn duration_since(&self, base_timestamp: u64) -> Option { self.expiration_timestamp_secs().checked_sub(base_timestamp) - } } @@ -164,7 +163,16 @@ macro_rules! value_or_missing { #[derive(Clone, Debug)] pub enum BuilderResult { Success(AutomatedTransaction), - GasPriceThresholdExceeded { task_index: u64, threshold: u64, value: u64 }, + GasPriceThresholdExceeded { + task_index: u64, + threshold: u64, + value: u64, + }, + ExpiryThresholdExceeded { + task_index: u64, + threshold: u64, + value: u64, + }, MissingValue(&'static str), } @@ -173,8 +181,23 @@ impl BuilderResult { Self::Success(txn) } - pub fn gas_price_threshold_exceeded(task_index: u64, threshold: u64, value: u64) -> BuilderResult { - Self::GasPriceThresholdExceeded { task_index, threshold, value } + pub fn gas_price_threshold_exceeded( + task_index: u64, + threshold: u64, + value: u64, + ) -> BuilderResult { + Self::GasPriceThresholdExceeded { + task_index, + threshold, + value, + } + } + pub fn expiry_threshold_exceeded(task_index: u64, threshold: u64, value: u64) -> BuilderResult { + Self::ExpiryThresholdExceeded { + task_index, + threshold, + value, + } } pub fn missing_value(missing: &'static str) -> BuilderResult { Self::MissingValue(missing) @@ -187,6 +210,9 @@ pub struct AutomatedTransactionBuilder { /// Gas unit price threshold. Default to 0. pub(crate) gas_price_cap: u64, + /// Expiration time threshold. Default to 0. + pub(crate) expiry_threshold_secs: u64, + /// Sender's address. pub(crate) sender: Option, @@ -220,6 +246,51 @@ pub struct AutomatedTransactionBuilder { pub(crate) block_height: Option, } +/// Getter interfaces of the builder +impl AutomatedTransactionBuilder { + pub fn gas_price_cap(&self) -> &u64 { + &self.gas_price_cap + } + + pub fn sender(&self) -> &Option { + &self.sender + } + + pub fn sequence_number(&self) -> &Option { + &self.sequence_number + } + + pub fn payload(&self) -> &Option { + &self.payload + } + + pub fn max_gas_amount(&self) -> &Option { + &self.max_gas_amount + } + + pub fn gas_unit_price(&self) -> &Option { + &self.gas_unit_price + } + pub fn expiration_timestamp_secs(&self) -> &Option { + &self.expiration_timestamp_secs + } + + pub fn chain_id(&self) -> &Option { + &self.chain_id + } + pub fn authenticator(&self) -> &Option { + &self.authenticator + } + + pub fn block_height(&self) -> &Option { + &self.block_height + } + + pub fn expiry_threshold(&self) -> &u64 { + &self.expiry_threshold_secs + } +} + impl AutomatedTransactionBuilder { pub fn new() -> Self { Self::default() @@ -271,8 +342,9 @@ impl AutomatedTransactionBuilder { self } - pub fn expiration_timestamp_secs(&self) -> u64 { - self.expiration_timestamp_secs.unwrap_or(0) + pub fn with_expiry_threshold_secs(mut self, secs: u64) -> Self { + self.expiry_threshold_secs = secs; + self } /// Build an [AutomatedTransaction] instance. @@ -282,6 +354,7 @@ impl AutomatedTransactionBuilder { pub fn build(self) -> BuilderResult { let AutomatedTransactionBuilder { gas_price_cap, + expiry_threshold_secs, sender, sequence_number, payload, @@ -303,7 +376,18 @@ impl AutomatedTransactionBuilder { let expiration_timestamp_secs = value_or_missing!(expiration_timestamp_secs, "expiration_timestamp_secs"); if gas_price_cap < gas_unit_price { - return BuilderResult::gas_price_threshold_exceeded(sequence_number, gas_price_cap, gas_unit_price); + return BuilderResult::gas_price_threshold_exceeded( + sequence_number, + gas_price_cap, + gas_unit_price, + ); + } + if expiry_threshold_secs > expiration_timestamp_secs { + return BuilderResult::expiry_threshold_exceeded( + sequence_number, + expiry_threshold_secs, + expiration_timestamp_secs, + ); } let raw_transaction = RawTransaction::new( sender, @@ -314,7 +398,11 @@ impl AutomatedTransactionBuilder { expiration_timestamp_secs, chain_id, ); - BuilderResult::Success(AutomatedTransaction::new(raw_transaction, authenticator, block_height)) + BuilderResult::Success(AutomatedTransaction::new( + raw_transaction, + authenticator, + block_height, + )) } } diff --git a/types/src/transaction/automation.rs b/types/src/transaction/automation.rs index 9ba066878adce..8074f0ac53dc5 100644 --- a/types/src/transaction/automation.rs +++ b/types/src/transaction/automation.rs @@ -1,4 +1,5 @@ -// Copyright (c) 2024 Supra. +// Copyright (c) 2025 Supra. +// SPDX-License-Identifier: Apache-2.0 use crate::transaction::EntryFunction; use move_core_types::account_address::AccountAddress; @@ -112,8 +113,6 @@ pub struct AutomationTaskMetaData { pub(crate) max_gas_amount: u64, /// Maximum gas price cap for the task pub(crate) gas_price_cap: u64, - /// Registration epoch number - pub(crate) registration_epoch: u64, /// Registration epoch time pub(crate) registration_time: u64, /// Flag indicating whether the task is active. @@ -130,7 +129,6 @@ impl AutomationTaskMetaData { tx_hash: Vec, max_gas_amount: u64, gas_price_cap: u64, - registration_epoch: u64, registration_time: u64, is_active: bool, ) -> Self { @@ -142,7 +140,6 @@ impl AutomationTaskMetaData { tx_hash, max_gas_amount, gas_price_cap, - registration_epoch, registration_time, is_active, } @@ -155,4 +152,32 @@ impl AutomationTaskMetaData { pub fn gas_price_cap(&self) -> u64 { self.gas_price_cap } + + pub fn payload_tx(&self) -> &[u8] { + &self.payload_tx + } + + pub fn expiry_time(&self) -> u64 { + self.expiry_time + } + + pub fn tx_hash(&self) -> &[u8] { + &self.tx_hash + } + + pub fn max_gas_amount(&self) -> u64 { + self.max_gas_amount + } + + pub fn registration_time(&self) -> u64 { + self.registration_time + } + + pub fn owner(&self) -> AccountAddress { + self.owner + } + + pub fn id(&self) -> u64 { + self.id + } } diff --git a/types/src/unit_tests/automation.rs b/types/src/unit_tests/automation.rs index d70377da054c4..5376ed93f9671 100644 --- a/types/src/unit_tests/automation.rs +++ b/types/src/unit_tests/automation.rs @@ -65,7 +65,6 @@ fn automated_txn_builder_from_task_meta() { tx_hash: vec![42; 32], max_gas_amount: 10, gas_price_cap: 20, - registration_epoch: 1, registration_time: 3600, is_active: false, }; @@ -125,15 +124,15 @@ fn automated_txn_build() { let address = AccountAddress::random(); let parent_hash = HashValue::random(); let chain_id = ChainId::new(1); + let expiry_time = 7200; let task_meta = AutomationTaskMetaData { id: 0, owner: address, payload_tx: bcs::to_bytes(&entry_function).unwrap(), - expiry_time: 7200, + expiry_time, tx_hash: parent_hash.to_vec(), max_gas_amount: 10, gas_price_cap: 20, - registration_epoch: 1, registration_time: 3600, is_active: false, }; @@ -180,14 +179,21 @@ fn automated_txn_build() { } // Gas unit price cap is greater than gas-price-cap - let builder_with_higher_gas_unit_price = builder_valid.with_gas_unit_price(30); + let builder_with_higher_gas_unit_price = builder_valid.clone().with_gas_unit_price(30); assert!(matches!( builder_with_higher_gas_unit_price.clone().build(), BuilderResult::GasPriceThresholdExceeded { .. } )); + // Expired transaction + let builder_with_expired_timestamp = builder_valid.clone().with_expiry_threshold_secs(2 * expiry_time); + assert!(matches!( + builder_with_expired_timestamp.clone().build(), + BuilderResult::ExpiryThresholdExceeded { .. } + )); + // Any other field if missing build will fail - let mut builder_with_no_expiry_time = builder_with_higher_gas_unit_price.clone(); + let mut builder_with_no_expiry_time = builder_valid.clone(); builder_with_no_expiry_time.expiration_timestamp_secs = None; assert!(matches!( builder_with_no_expiry_time.clone().build(),