Skip to content

Commit 3874e2e

Browse files
Merge pull request #1125 from serverlessdomain-hash/fix/issues-826-830-865-1031
feat: implement issues #826, #830, #865, and #1031
2 parents a9c17d2 + 16cb084 commit 3874e2e

427 files changed

Lines changed: 24109 additions & 60959 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

contract/src/events.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ pub struct DelegationPoolEvent {
160160
pub timestamp: u64,
161161
}
162162

163+
/// Event payload for user fee discount tier progression
164+
#[contracttype]
165+
#[derive(Clone, Debug)]
166+
pub struct FeeDiscountTierUpdatedEvent {
167+
pub creator: Address,
168+
pub old_tier: u32,
169+
pub new_tier: u32,
170+
pub total_executions: u64,
163171
/// Event payload for oracle volatility breaches
164172
#[contracttype]
165173
#[derive(Clone, Debug)]
@@ -182,6 +190,30 @@ pub struct VolatilityCircuitBreakerUnpausedEvent {
182190
pub struct EventLogger;
183191

184192
impl EventLogger {
193+
/// Logs a user fee discount tier update
194+
pub fn log_fee_discount_tier_updated(
195+
env: &Env,
196+
creator: Address,
197+
old_tier: u32,
198+
new_tier: u32,
199+
total_executions: u64,
200+
) {
201+
let timestamp = env.ledger().timestamp();
202+
let event_data = FeeDiscountTierUpdatedEvent {
203+
creator: creator.clone(),
204+
old_tier,
205+
new_tier,
206+
total_executions,
207+
timestamp,
208+
};
209+
210+
let topics = (
211+
Symbol::new(env, "sorotask"),
212+
Symbol::new(env, "fee_discount_tier"),
213+
creator,
214+
);
215+
env.events().publish(topics, event_data);
216+
}
185217
/// Logs a state change for off-chain indexers
186218
pub fn log_state_change(
187219
env: &Env,

contract/src/lib.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ const OPTIMISTIC_CHALLENGE_WINDOW_LEDGERS: u32 = 100;
188188
/// Minimum bond a keeper must post to submit an optimistic claim.
189189
const 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
192196
pub const PERM_CAN_PAUSE: u32 = 1;
193197
pub 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]
17131731
pub 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

Comments
 (0)