From 35af21c068cace3c4b55da68fbe9e2b4c2ad270d Mon Sep 17 00:00:00 2001 From: Aregnaz Harutyunyan <> Date: Fri, 30 Jan 2026 15:23:10 +0400 Subject: [PATCH 1/2] [AN-Issue-2637] Unlocked valid cycle fee and updated registry state properly when tasks are stopped --- .../doc/automation_registry.md | 40 ++++++++++++++----- .../sources/automation_registry.move | 40 ++++++++++++++----- .../tests/automation_registry_tests.move | 17 +++++--- 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/aptos-move/framework/supra-framework/doc/automation_registry.md b/aptos-move/framework/supra-framework/doc/automation_registry.md index 13823a4dab14d..0a6f738600c45 100644 --- a/aptos-move/framework/supra-framework/doc/automation_registry.md +++ b/aptos-move/framework/supra-framework/doc/automation_registry.md @@ -2668,6 +2668,16 @@ Invalid number of auxiliary data. + + +The refund fee for remaining cycle cycle time is greater than total cycle fee for the task. + + +
const EINVALID_CYCLE_REFUND_FEE: u64 = 48;
+
+ + + Invalid expiry time: it cannot be earlier than the current time @@ -4663,7 +4673,7 @@ by the max gas amount of the stopped task. Half of the remaining task fee is ref let stopped_task_details = vector[]; let total_refund_fee = 0; - let epoch_locked_fees = automation_registry.epoch_locked_fees; + let total_cycle_locked_fees = automation_registry.epoch_locked_fees; // Calculate refundable fee for this remaining time task in current epoch let current_time = timestamp::now_seconds(); @@ -4700,8 +4710,14 @@ by the max gas amount of the stopped task. Half of the remaining task fee is ref automation_registry.gas_committed_for_next_epoch = automation_registry.gas_committed_for_next_epoch - task.max_gas_amount; }; - let (epoch_fee_refund, deposit_refund) = if (task.state != PENDING) { - let task_fee = calculate_task_fee( + let (cycle_locked_fee_for_task, cycle_fee_refund, deposit_refund) = if (task.state != PENDING) { + let task_fee_for_full_cycle = + calculate_automation_fee_for_interval( + cycle_info.duration_secs, + task.max_gas_amount, + automation_fee_per_sec, + arc.registry_max_gas_cap); + let task_fee_for_residual_time = calculate_task_fee( &arc, &task, residual_interval, @@ -4709,27 +4725,28 @@ by the max gas amount of the stopped task. Half of the remaining task fee is ref automation_fee_per_sec ); // Refund full deposit and the half of the remaining run-time fee when task is active or cancelled stage - (task_fee / REFUND_FRACTION, task.locked_fee_for_next_epoch) + (task_fee_for_full_cycle, task_fee_for_residual_time / REFUND_FRACTION, task.locked_fee_for_next_epoch) } else { - (0, (task.locked_fee_for_next_epoch / REFUND_FRACTION)) + (0, 0, (task.locked_fee_for_next_epoch / REFUND_FRACTION)) }; let result = safe_unlock_locked_deposit( refund_bookkeeping, task.locked_fee_for_next_epoch, task.task_index); assert!(result, EDEPOSIT_REFUND); - let (result, remaining_epoch_locked_fees) = safe_unlock_locked_epoch_fee( - epoch_locked_fees, - epoch_fee_refund, + assert!(cycle_locked_fee_for_task >= cycle_fee_refund, EINVALID_CYCLE_REFUND_FEE); + let (result, remaining_cycle_locked_fees) = safe_unlock_locked_epoch_fee( + total_cycle_locked_fees, + cycle_locked_fee_for_task, task.task_index); assert!(result, EEPOCH_FEE_REFUND); - epoch_locked_fees = remaining_epoch_locked_fees; + total_cycle_locked_fees = remaining_cycle_locked_fees; - total_refund_fee = total_refund_fee + (epoch_fee_refund + deposit_refund); + total_refund_fee = total_refund_fee + (cycle_fee_refund + deposit_refund); vector::push_back( &mut stopped_task_details, - TaskStoppedV2 { task_index, deposit_refund, epoch_fee_refund, registration_hash: task.tx_hash } + TaskStoppedV2 { task_index, deposit_refund, epoch_fee_refund: cycle_fee_refund, registration_hash: task.tx_hash } ); } }); @@ -4743,6 +4760,7 @@ by the max gas amount of the stopped task. Half of the remaining task fee is ref let resource_account_balance = coin::balance<SupraCoin>(automation_registry.registry_fee_address); assert!(resource_account_balance >= total_refund_fee, EINSUFFICIENT_BALANCE_FOR_REFUND); coin::transfer<SupraCoin>(&resource_signer, owner, total_refund_fee); + automation_registry.epoch_locked_fees = total_cycle_locked_fees; // Emit task stopped event event::emit(TasksStoppedV2 { diff --git a/aptos-move/framework/supra-framework/sources/automation_registry.move b/aptos-move/framework/supra-framework/sources/automation_registry.move index 5c162aaa74c47..e6466b6ec7ad1 100644 --- a/aptos-move/framework/supra-framework/sources/automation_registry.move +++ b/aptos-move/framework/supra-framework/sources/automation_registry.move @@ -135,6 +135,8 @@ module supra_framework::automation_registry { const EREGISTRY_SYSTEM_MAX_GAS_CAP_NON_ZERO: u64 = 46; /// The input address is not identified as multisig account. const EUNKNOWN_MULTISIG_ADDRESS: u64 = 47; + /// The refund fee for remaining cycle cycle time is greater than total cycle fee for the task. + const EINVALID_CYCLE_REFUND_FEE: u64 = 48; /// The length of the transaction hash. const TXN_HASH_LENGTH: u64 = 32; @@ -1286,7 +1288,7 @@ module supra_framework::automation_registry { let stopped_task_details = vector[]; let total_refund_fee = 0; - let epoch_locked_fees = automation_registry.epoch_locked_fees; + let total_cycle_locked_fees = automation_registry.epoch_locked_fees; // Calculate refundable fee for this remaining time task in current epoch let current_time = timestamp::now_seconds(); @@ -1323,8 +1325,14 @@ module supra_framework::automation_registry { automation_registry.gas_committed_for_next_epoch = automation_registry.gas_committed_for_next_epoch - task.max_gas_amount; }; - let (epoch_fee_refund, deposit_refund) = if (task.state != PENDING) { - let task_fee = calculate_task_fee( + let (cycle_locked_fee_for_task, cycle_fee_refund, deposit_refund) = if (task.state != PENDING) { + let task_fee_for_full_cycle = + calculate_automation_fee_for_interval( + cycle_info.duration_secs, + task.max_gas_amount, + automation_fee_per_sec, + arc.registry_max_gas_cap); + let task_fee_for_residual_time = calculate_task_fee( &arc, &task, residual_interval, @@ -1332,27 +1340,28 @@ module supra_framework::automation_registry { automation_fee_per_sec ); // Refund full deposit and the half of the remaining run-time fee when task is active or cancelled stage - (task_fee / REFUND_FRACTION, task.locked_fee_for_next_epoch) + (task_fee_for_full_cycle, task_fee_for_residual_time / REFUND_FRACTION, task.locked_fee_for_next_epoch) } else { - (0, (task.locked_fee_for_next_epoch / REFUND_FRACTION)) + (0, 0, (task.locked_fee_for_next_epoch / REFUND_FRACTION)) }; let result = safe_unlock_locked_deposit( refund_bookkeeping, task.locked_fee_for_next_epoch, task.task_index); assert!(result, EDEPOSIT_REFUND); - let (result, remaining_epoch_locked_fees) = safe_unlock_locked_epoch_fee( - epoch_locked_fees, - epoch_fee_refund, + assert!(cycle_locked_fee_for_task >= cycle_fee_refund, EINVALID_CYCLE_REFUND_FEE); + let (result, remaining_cycle_locked_fees) = safe_unlock_locked_epoch_fee( + total_cycle_locked_fees, + cycle_locked_fee_for_task, task.task_index); assert!(result, EEPOCH_FEE_REFUND); - epoch_locked_fees = remaining_epoch_locked_fees; + total_cycle_locked_fees = remaining_cycle_locked_fees; - total_refund_fee = total_refund_fee + (epoch_fee_refund + deposit_refund); + total_refund_fee = total_refund_fee + (cycle_fee_refund + deposit_refund); vector::push_back( &mut stopped_task_details, - TaskStoppedV2 { task_index, deposit_refund, epoch_fee_refund, registration_hash: task.tx_hash } + TaskStoppedV2 { task_index, deposit_refund, epoch_fee_refund: cycle_fee_refund, registration_hash: task.tx_hash } ); } }); @@ -1366,6 +1375,7 @@ module supra_framework::automation_registry { let resource_account_balance = coin::balance(automation_registry.registry_fee_address); assert!(resource_account_balance >= total_refund_fee, EINSUFFICIENT_BALANCE_FOR_REFUND); coin::transfer(&resource_signer, owner, total_refund_fee); + automation_registry.epoch_locked_fees = total_cycle_locked_fees; // Emit task stopped event event::emit(TasksStoppedV2 { @@ -3334,6 +3344,14 @@ module supra_framework::automation_registry { automation_registry.main.epoch_locked_fees = locked_fee; } + #[test_only] + public(friend) fun check_locked_fee( + locked_fee: u64, + ) acquires AutomationRegistryV2 { + let automation_registry = borrow_global_mut(@supra_framework); + assert!(automation_registry.main.epoch_locked_fees == locked_fee, locked_fee); + } + #[test_only] public(friend) fun set_total_deposited_automation_fee( fee: u64, diff --git a/aptos-move/framework/supra-framework/tests/automation_registry_tests.move b/aptos-move/framework/supra-framework/tests/automation_registry_tests.move index 379b60cd54f3b..5ffd5f4e1142f 100644 --- a/aptos-move/framework/supra-framework/tests/automation_registry_tests.move +++ b/aptos-move/framework/supra-framework/tests/automation_registry_tests.move @@ -12,7 +12,7 @@ module std::automation_registry_tests { use supra_framework::multisig_account; use supra_framework::automation_registry::{ check_task_priority, check_cycle_state_and_duration, check_next_task_index_to_be_processed, - calculate_automation_fee_multiplier_for_committed_occupancy, AutomationRegistryConfigV2, + calculate_automation_fee_multiplier_for_committed_occupancy, AutomationRegistryConfigV2, check_locked_fee, }; use supra_framework::coin; use supra_framework::config_buffer; @@ -2841,11 +2841,12 @@ module std::automation_registry_tests { }); // 0.002 (*4) - automation_epoch_fee_per_second, 7200 epoch duration - let expected_automation_fee = 4 * (max_gas_amount * EPOCH_INTERVAL_FOR_TEST_IN_SECS / 100000); - expected_current_balance = expected_current_balance - expected_automation_fee; - expected_registry_balance = expected_registry_balance + expected_automation_fee; - check_account_balance(user_account, expected_current_balance ); - check_account_balance( registry_fee_address, expected_registry_balance ); + let expected_automation_fees = 4 * (max_gas_amount * EPOCH_INTERVAL_FOR_TEST_IN_SECS / 100000); + expected_current_balance = expected_current_balance - expected_automation_fees; + expected_registry_balance = expected_registry_balance + expected_automation_fees; + check_account_balance(user_account, expected_current_balance); + check_account_balance(registry_fee_address, expected_registry_balance); + check_locked_fee(expected_automation_fees); timestamp::update_global_time_for_test_secs( EPOCH_INTERVAL_FOR_TEST_IN_SECS + (EPOCH_INTERVAL_FOR_TEST_IN_SECS / 2) @@ -2869,6 +2870,8 @@ module std::automation_registry_tests { expected_registry_balance = expected_registry_balance - expected_refund; check_account_balance(user_account, expected_current_balance); check_account_balance(registry_fee_address, expected_registry_balance); + // locked cycle fees are deducted by the full cycle fee amount for the user + check_locked_fee((expected_automation_fees * 3) / 4); // Add and stop the task in the same epoch. Task index will be 4 assert!(automation_registry::get_next_task_index() == 4, 1); @@ -2903,6 +2906,8 @@ module std::automation_registry_tests { expected_refund = automation_fee_cap / REFUND_FACTOR; check_account_balance(user_account, expected_current_balance + expected_refund); check_account_balance(registry_fee_address, expected_registry_balance - expected_refund); + // As long as the task was not active, no locked fee for it will be unlocked + check_locked_fee((expected_automation_fees * 3) / 4); } #[test(framework = @supra_framework, user = @0x1cafe, user2 = @0x1cafa)] From e356740001058adee50271d570bf85501d805b7f Mon Sep 17 00:00:00 2001 From: Aregnaz Harutyunyan <> Date: Mon, 2 Feb 2026 12:11:41 +0400 Subject: [PATCH 2/2] Fixed comment --- aptos-move/framework/supra-framework/doc/automation_registry.md | 2 +- .../framework/supra-framework/sources/automation_registry.move | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aptos-move/framework/supra-framework/doc/automation_registry.md b/aptos-move/framework/supra-framework/doc/automation_registry.md index 0a6f738600c45..4ebcdd03c97d0 100644 --- a/aptos-move/framework/supra-framework/doc/automation_registry.md +++ b/aptos-move/framework/supra-framework/doc/automation_registry.md @@ -2670,7 +2670,7 @@ Invalid number of auxiliary data. -The refund fee for remaining cycle cycle time is greater than total cycle fee for the task. +The refund fee for remaining cycle time is greater than total cycle fee for the task.
const EINVALID_CYCLE_REFUND_FEE: u64 = 48;
diff --git a/aptos-move/framework/supra-framework/sources/automation_registry.move b/aptos-move/framework/supra-framework/sources/automation_registry.move
index e6466b6ec7ad1..15965aec1d9ae 100644
--- a/aptos-move/framework/supra-framework/sources/automation_registry.move
+++ b/aptos-move/framework/supra-framework/sources/automation_registry.move
@@ -135,7 +135,7 @@ module supra_framework::automation_registry {
     const EREGISTRY_SYSTEM_MAX_GAS_CAP_NON_ZERO: u64 = 46;
     /// The input address is not identified as multisig account.
     const EUNKNOWN_MULTISIG_ADDRESS: u64 = 47;
-    /// The refund fee for remaining cycle cycle time is greater than total cycle fee for the task.
+    /// The refund fee for remaining cycle time is greater than total cycle fee for the task.
     const EINVALID_CYCLE_REFUND_FEE: u64 = 48;
 
     /// The length of the transaction hash.