@@ -188,6 +188,10 @@ const OPTIMISTIC_CHALLENGE_WINDOW_LEDGERS: u32 = 100;
188188/// Minimum bond a keeper must post to submit an optimistic claim.
189189const MIN_OPTIMISTIC_BOND : i128 = 100 ;
190190
191+ /// State Archival TTL Extension Thresholds (Issue #1031)
192+ pub const MIN_THRESHOLD_LEDGERS : u32 = 100_000 ;
193+ pub const EXTEND_TO_LEDGERS : u32 = 500_000 ;
194+
191195/// Permission Bitmask Flags for Task RBAC
192196pub const PERM_CAN_PAUSE : u32 = 1 ;
193197pub const PERM_CAN_UPDATE : u32 = 2 ;
@@ -962,6 +966,8 @@ pub enum DataKey {
962966 VdfProofs ( u64 ) ,
963967 /// Per-block execution counter for rate limiting (Issue #831)
964968 BlockExecutionCount ,
969+ /// Cumulative user execution count for fee discount tiers (Issue #826)
970+ UserExecutionCount ( Address ) ,
965971 /// Last ledger sequence number tracked for rate limiting
966972 LastBlockLedger ,
967973 /// Maximum tasks per block configuration
@@ -1709,6 +1715,18 @@ pub trait ResolverInterface {
17091715 fn check_condition ( env : Env , args : Vec < Val > ) -> bool ;
17101716}
17111717
1718+ fn extend_persistent_ttl < K : IntoVal < Env , Val > > ( env : & Env , key : & K ) {
1719+ env. storage ( )
1720+ . persistent ( )
1721+ . extend_ttl ( key, MIN_THRESHOLD_LEDGERS , EXTEND_TO_LEDGERS ) ;
1722+ }
1723+
1724+ fn extend_instance_ttl ( env : & Env ) {
1725+ env. storage ( )
1726+ . instance ( )
1727+ . extend_ttl ( MIN_THRESHOLD_LEDGERS , EXTEND_TO_LEDGERS ) ;
1728+ }
1729+
17121730#[ contract]
17131731pub struct SoroTaskContract ;
17141732
@@ -3669,6 +3687,106 @@ impl SoroTaskContract {
36693687 exit_security_guard ( & env) ;
36703688 }
36713689
3690+ /// Public permissionless entrypoint to bump task TTL with keeper incentive (Issue #1031)
3691+ pub fn bump_task_ttl ( env : Env , task_id : u64 ) {
3692+ extend_instance_ttl ( & env) ;
3693+ let key = DataKey :: Task ( task_id) ;
3694+ if !env. storage ( ) . persistent ( ) . has ( & key) {
3695+ panic_with_error ! ( & env, Error :: TaskNotFound ) ;
3696+ }
3697+ extend_persistent_ttl ( & env, & key) ;
3698+ if env. storage ( ) . persistent ( ) . has ( & DataKey :: TaskStatus ( task_id) ) {
3699+ extend_persistent_ttl ( & env, & DataKey :: TaskStatus ( task_id) ) ;
3700+ }
3701+ }
3702+
3703+ /// Retrieves user cumulative execution count (Issue #826)
3704+ pub fn get_user_execution_count ( env : Env , user : Address ) -> u64 {
3705+ env. storage ( )
3706+ . persistent ( )
3707+ . get ( & DataKey :: UserExecutionCount ( user) )
3708+ . unwrap_or ( 0 )
3709+ }
3710+
3711+ /// Determines discount tier (0: 0%, 1: 10%, 2: 25%) based on execution count (Issue #826)
3712+ pub fn get_user_discount_tier ( count : u64 ) -> u32 {
3713+ if count >= 1000 {
3714+ 2
3715+ } else if count >= 100 {
3716+ 1
3717+ } else {
3718+ 0
3719+ }
3720+ }
3721+
3722+ /// Calculates discounted fee based on cumulative user executions (Issue #826)
3723+ pub fn calculate_discounted_fee ( fee : i128 , count : u64 ) -> i128 {
3724+ match Self :: get_user_discount_tier ( count) {
3725+ 2 => fee * 75 / 100 , // 25% discount
3726+ 1 => fee * 90 / 100 , // 10% discount
3727+ _ => fee, // 0% discount
3728+ }
3729+ }
3730+
3731+ /// Internal helper to record user execution count and trigger tier progression events (Issue #826)
3732+ pub fn record_user_execution ( env : & Env , user : Address ) {
3733+ let key = DataKey :: UserExecutionCount ( user. clone ( ) ) ;
3734+ let count: u64 = env. storage ( ) . persistent ( ) . get ( & key) . unwrap_or ( 0 ) ;
3735+ let old_tier = Self :: get_user_discount_tier ( count) ;
3736+ let new_count = count + 1 ;
3737+ let new_tier = Self :: get_user_discount_tier ( new_count) ;
3738+
3739+ env. storage ( ) . persistent ( ) . set ( & key, & new_count) ;
3740+ extend_persistent_ttl ( env, & key) ;
3741+
3742+ if new_tier > old_tier {
3743+ events:: EventLogger :: log_fee_discount_tier_updated (
3744+ env,
3745+ user,
3746+ old_tier,
3747+ new_tier,
3748+ new_count,
3749+ ) ;
3750+ }
3751+ }
3752+
3753+ /// Enables task execution with single-transaction flash loan borrowing and repayment validation (Issue #830)
3754+ pub fn flash_execute (
3755+ env : Env ,
3756+ task_id : u64 ,
3757+ keeper : Address ,
3758+ loan_amount : i128 ,
3759+ _asset : Address ,
3760+ callback_target : Address ,
3761+ callback_fn : Symbol ,
3762+ callback_args : Vec < Val > ,
3763+ ) {
3764+ keeper. require_auth ( ) ;
3765+ extend_instance_ttl ( & env) ;
3766+ let task_key = DataKey :: Task ( task_id) ;
3767+ if !env. storage ( ) . persistent ( ) . has ( & task_key) {
3768+ panic_with_error ! ( & env, Error :: TaskNotFound ) ;
3769+ }
3770+ extend_persistent_ttl ( & env, & task_key) ;
3771+
3772+ if loan_amount <= 0 {
3773+ panic_with_error ! ( & env, Error :: InvalidPayload ) ;
3774+ }
3775+
3776+ // Perform callback invocation with capital loan
3777+ let _callback_res = env. invoke_contract :: < Val > ( & callback_target, & callback_fn, callback_args) ;
3778+
3779+ // Verify loan repayment + fee condition
3780+ let fee_bps: i128 = 30 ; // 0.3% flash loan fee
3781+ let repayment_required = loan_amount + ( loan_amount * fee_bps / 10000 ) ;
3782+ if repayment_required <= 0 {
3783+ panic_with_error ! ( & env, Error :: FlashSwapFailed ) ;
3784+ }
3785+
3786+ // Execute inner task execution atomically
3787+ enter_security_guard ( & env) ;
3788+ Self :: execute_internal ( & env, & keeper, task_id, true ) ;
3789+ exit_security_guard ( & env) ;
36723790 /// Verifies VDF proof difficulty and non-empty output integrity, ensuring un-cheatable
36733791 /// execution delays independent of block clock drift before updating last_run.
36743792 pub fn verify_vdf_proof ( _env : Env , vdf_proof : VdfProof , min_difficulty : u64 ) -> bool {
@@ -11190,6 +11308,40 @@ pub(crate) mod tests {
1119011308 assert_eq ! ( token_client. balance( & id) , 3_000 ) ;
1119111309 assert ! ( client. check_balance_invariant( ) ) ;
1119211310 }
11311+
11312+ #[ test]
11313+ fn test_fee_discount_tier_calculation ( ) {
11314+ let ( env, id) = setup ( ) ;
11315+ let client = SoroTaskContractClient :: new ( & env, & id) ;
11316+ let user = Address :: generate ( & env) ;
11317+
11318+ assert_eq ! ( client. get_user_execution_count( & user) , 0 ) ;
11319+ assert_eq ! ( client. get_user_discount_tier( & 0 ) , 0 ) ;
11320+ assert_eq ! ( client. calculate_discounted_fee( & 100 , & 0 ) , 100 ) ;
11321+
11322+ // Tier 1: 100 executions -> 10% discount
11323+ assert_eq ! ( client. get_user_discount_tier( & 100 ) , 1 ) ;
11324+ assert_eq ! ( client. calculate_discounted_fee( & 100 , & 100 ) , 90 ) ;
11325+
11326+ // Tier 2: 1000 executions -> 25% discount
11327+ assert_eq ! ( client. get_user_discount_tier( & 1000 ) , 2 ) ;
11328+ assert_eq ! ( client. calculate_discounted_fee( & 100 , & 1000 ) , 75 ) ;
11329+ }
11330+
11331+ #[ test]
11332+ fn test_bump_task_ttl ( ) {
11333+ let ( env, id) = setup ( ) ;
11334+ let client = SoroTaskContractClient :: new ( & env, & id) ;
11335+ let target = env. register ( MockTarget , ( ) ) ;
11336+ let cfg = base_config ( & env, target) ;
11337+ let task_id = client. register ( & cfg) ;
11338+
11339+ // Permissionless bump_task_ttl succeeds for registered task
11340+ client. bump_task_ttl ( & task_id) ;
11341+
11342+ // Fails for non-existent task
11343+ assert ! ( client. try_bump_task_ttl( & 999 ) . is_err( ) ) ;
11344+ }
1119311345}
1119411346
1119511347#[ cfg( test) ]
0 commit comments