Skip to content

Commit e47fcfb

Browse files
committed
fix Auto-Claim via Keeper Bot
1 parent 2d6ff80 commit e47fcfb

2 files changed

Lines changed: 128 additions & 59 deletions

File tree

contracts/vesting_contracts/src/lib.rs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const INITIAL_SUPPLY: Symbol = Symbol::new(&"INITIAL_SUPPLY");
1818
const ADMIN_BALANCE: Symbol = Symbol::new(&"ADMIN_BALANCE");
1919
const ADMIN_ADDRESS: Symbol = Symbol::new(&"ADMIN_ADDRESS");
2020
const PROPOSED_ADMIN: Symbol = Symbol::new(&"PROPOSED_ADMIN");
21+
const KEEPER_FEES: Symbol = Symbol::new(&"KEEPER_FEES");
2122

2223
// Vault structure with lazy initialization
2324
#[contracttype]
@@ -28,6 +29,7 @@ pub struct Vault {
2829
pub released_amount: i128,
2930
pub start_time: u64,
3031
pub end_time: u64,
32+
pub keeper_fee: i128, // Fee paid to anyone who triggers auto_claim
3133
pub is_initialized: bool, // Lazy initialization flag
3234
pub is_irrevocable: bool, // Security flag to prevent admin withdrawal
3335
}
@@ -38,6 +40,7 @@ pub struct BatchCreateData {
3840
pub amounts: Vec<i128>,
3941
pub start_times: Vec<u64>,
4042
pub end_times: Vec<u64>,
43+
pub keeper_fees: Vec<i128>,
4144
}
4245

4346
#[contracttype]
@@ -116,7 +119,7 @@ impl VestingContract {
116119
}
117120

118121
// Full initialization - writes all metadata immediately
119-
pub fn create_vault_full(env: Env, owner: Address, amount: i128, start_time: u64, end_time: u64) -> u64 {
122+
pub fn create_vault_full(env: Env, owner: Address, amount: i128, start_time: u64, end_time: u64, keeper_fee: i128) -> u64 {
120123
Self::require_admin(&env);
121124

122125
// Get next vault ID
@@ -137,6 +140,7 @@ impl VestingContract {
137140
released_amount: 0,
138141
start_time,
139142
end_time,
143+
keeper_fee,
140144
is_initialized: true, // Mark as fully initialized
141145
is_irrevocable: false, // Default to revocable
142146
};
@@ -170,7 +174,7 @@ impl VestingContract {
170174
}
171175

172176
// Lazy initialization - writes minimal data initially
173-
pub fn create_vault_lazy(env: Env, owner: Address, amount: i128, start_time: u64, end_time: u64) -> u64 {
177+
pub fn create_vault_lazy(env: Env, owner: Address, amount: i128, start_time: u64, end_time: u64, keeper_fee: i128) -> u64 {
174178
Self::require_admin(&env);
175179

176180
// Get next vault ID
@@ -191,6 +195,7 @@ impl VestingContract {
191195
released_amount: 0,
192196
start_time,
193197
end_time,
198+
keeper_fee,
194199
is_initialized: false, // Mark as lazy initialized
195200
is_irrevocable: false, // Default to revocable
196201
};
@@ -232,6 +237,7 @@ impl VestingContract {
232237
released_amount: 0,
233238
start_time: 0,
234239
end_time: 0,
240+
keeper_fee: 0,
235241
is_initialized: false,
236242
is_irrevocable: false,
237243
}
@@ -395,6 +401,7 @@ impl VestingContract {
395401
released_amount: 0,
396402
start_time: batch_data.start_times.get(i).unwrap(),
397403
end_time: batch_data.end_times.get(i).unwrap(),
404+
keeper_fee: batch_data.keeper_fees.get(i).unwrap(),
398405
is_initialized: false, // Lazy initialization
399406
is_irrevocable: false, // Default to revocable
400407
};
@@ -448,6 +455,7 @@ impl VestingContract {
448455
released_amount: 0,
449456
start_time: batch_data.start_times.get(i).unwrap(),
450457
end_time: batch_data.end_times.get(i).unwrap(),
458+
keeper_fee: batch_data.keeper_fees.get(i).unwrap(),
451459
is_initialized: true, // Full initialization
452460
is_irrevocable: false, // Default to revocable
453461
};
@@ -496,6 +504,7 @@ impl VestingContract {
496504
released_amount: 0,
497505
start_time: 0,
498506
end_time: 0,
507+
keeper_fee: 0,
499508
is_initialized: false,
500509
is_irrevocable: false,
501510
}
@@ -529,6 +538,7 @@ impl VestingContract {
529538
released_amount: 0,
530539
start_time: 0,
531540
end_time: 0,
541+
keeper_fee: 0,
532542
is_initialized: false,
533543
is_irrevocable: false,
534544
}
@@ -683,4 +693,79 @@ impl VestingContract {
683693
let sum = total_locked + total_claimed + admin_balance;
684694
sum == initial_supply
685695
}
696+
697+
// --- New Auto-Claim Logic ---
698+
699+
// Calculate currently claimable tokens based on linear vesting
700+
pub fn get_claimable_amount(env: Env, vault_id: u64) -> i128 {
701+
let vault: Vault = env.storage().instance()
702+
.get(&VAULT_DATA, &vault_id)
703+
.unwrap_or_else(|| panic!("Vault not found"));
704+
705+
let now = env.ledger().timestamp();
706+
707+
if now <= vault.start_time {
708+
return 0;
709+
}
710+
711+
let elapsed = if now >= vault.end_time {
712+
vault.end_time - vault.start_time
713+
} else {
714+
now - vault.start_time
715+
};
716+
717+
let total_duration = vault.end_time - vault.start_time;
718+
719+
let vested = if total_duration > 0 {
720+
// Use i128 for calculation to prevent overflow then back to i128
721+
(vault.total_amount * elapsed as i128) / total_duration as i128
722+
} else {
723+
vault.total_amount
724+
};
725+
726+
if vested > vault.released_amount {
727+
vested - vault.released_amount
728+
} else {
729+
0
730+
}
731+
}
732+
733+
// Auto-claim function that anyone can call.
734+
// Tokens go to beneficiary, but keeper can get a tip.
735+
pub fn auto_claim(env: Env, vault_id: u64, keeper: Address) {
736+
let mut vault: Vault = env.storage().instance()
737+
.get(&VAULT_DATA, &vault_id)
738+
.unwrap_or_else(|| panic!("Vault not found"));
739+
740+
require!(vault.is_initialized, "Vault not initialized");
741+
742+
let claimable = Self::get_claimable_amount(env.clone(), vault_id);
743+
744+
// Ensure there's enough to cover the fee and something left for beneficiary
745+
require!(claimable > vault.keeper_fee, "Insufficient claimable tokens to cover fee");
746+
747+
let beneficiary_amount = claimable - vault.keeper_fee;
748+
749+
// Update vault
750+
vault.released_amount += claimable;
751+
env.storage().instance().set(&VAULT_DATA, &vault_id, &vault);
752+
753+
// Update keeper fees
754+
let mut fees: Map<Address, i128> = env.storage().instance().get(&KEEPER_FEES).unwrap_or(Map::new(&env));
755+
let current_fees = fees.get(keeper.clone()).unwrap_or(0);
756+
fees.set(keeper.clone(), current_fees + vault.keeper_fee);
757+
env.storage().instance().set(&KEEPER_FEES, &fees);
758+
759+
// Emit KeeperClaim event
760+
env.events().publish(
761+
(Symbol::new(&env, "KeeperClaim"), vault_id, keeper),
762+
(beneficiary_amount, vault.keeper_fee)
763+
);
764+
}
765+
766+
// Get accumulated fees for a keeper
767+
pub fn get_keeper_fee(env: Env, keeper: Address) -> i128 {
768+
let fees: Map<Address, i128> = env.storage().instance().get(&KEEPER_FEES).unwrap_or(Map::new(&env));
769+
fees.get(keeper).unwrap_or(0)
770+
}
686771
}

contracts/vesting_contracts/src/test.rs

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@ fn test_admin_access_control() {
104104
});
105105

106106
let result = std::panic::catch_unwind(|| {
107-
client.create_vault_full(&vault_owner, &1000i128, &100u64, &200u64);
107+
client.create_vault_full(&vault_owner, &1000i128, &100u64, &200u64, &0i128);
108108
});
109109
assert!(result.is_err());
110110

111111
let result = std::panic::catch_unwind(|| {
112-
client.create_vault_lazy(&vault_owner, &1000i128, &100u64, &200u64);
112+
client.create_vault_lazy(&vault_owner, &1000i128, &100u64, &200u64, &0i128);
113113
});
114114
assert!(result.is_err());
115115

@@ -118,10 +118,10 @@ fn test_admin_access_control() {
118118
env.current_contract_address().set(&admin);
119119
});
120120

121-
let vault_id = client.create_vault_full(&vault_owner, &1000i128, &100u64, &200u64);
121+
let vault_id = client.create_vault_full(&vault_owner, &1000i128, &100u64, &200u64, &0i128);
122122
assert_eq!(vault_id, 1);
123123

124-
let vault_id2 = client.create_vault_lazy(&vault_owner, &500i128, &150u64, &250u64);
124+
let vault_id2 = client.create_vault_lazy(&vault_owner, &500i128, &150u64, &250u64, &0i128);
125125
assert_eq!(vault_id2, 2);
126126
}
127127

@@ -147,6 +147,7 @@ fn test_batch_operations_admin_control() {
147147
amounts: vec![&env, 1000i128, 2000i128],
148148
start_times: vec![&env, 100u64, 150u64],
149149
end_times: vec![&env, 200u64, 250u64],
150+
keeper_fees: vec![&env, 0i128, 0i128],
150151
};
151152

152153
// Test: Unauthorized user cannot create batch vaults
@@ -176,79 +177,62 @@ fn test_batch_operations_admin_control() {
176177
}
177178

178179
#[test]
179-
180+
fn test_auto_claim_functionality() {
180181
let env = Env::default();
181182
let contract_id = env.register(VestingContract, ());
182183
let client = VestingContractClient::new(&env, &contract_id);
183184

184-
// Create addresses for testing
185185
let admin = Address::generate(&env);
186-
let vault_owner = Address::generate(&env);
187-
188-
let unauthorized_user = Address::generate(&env);
186+
let beneficiary = Address::generate(&env);
187+
let keeper = Address::generate(&env);
189188

190-
// Initialize contract with admin
191189
let initial_supply = 1000000i128;
192190
client.initialize(&admin, &initial_supply);
193191

194-
192+
let amount = 1000i128;
193+
let keeper_fee = 10i128;
194+
let start_time = env.ledger().timestamp();
195+
let end_time = start_time + 100; // 100 seconds duration
196+
197+
// Create vault
195198
env.as_contract(&contract_id, || {
196199
env.current_contract_address().set(&admin);
197200
});
201+
let vault_id = client.create_vault_full(&beneficiary, &amount, &start_time, &end_time, &keeper_fee);
198202

199-
200-
env.as_contract(&contract_id, || {
201-
env.current_contract_address().set(&unauthorized_user);
202-
});
203+
// Advance time by 50 seconds (halfway)
204+
env.ledger().set_timestamp(start_time + 50);
203205

204-
let result = std::panic::catch_unwind(|| {
205-
206-
});
207-
assert!(result.is_err());
208-
}
209-
210-
#[test]
211-
212-
let env = Env::default();
213-
let contract_id = env.register(VestingContract, ());
214-
let client = VestingContractClient::new(&env, &contract_id);
206+
// Check claimable amount
207+
let claimable = client.get_claimable_amount(&vault_id);
208+
assert!(claimable >= 500); // Linear vesting should be ~500
215209

216-
// Create addresses for testing
217-
let admin = Address::generate(&env);
218-
let vault_owner = Address::generate(&env);
219-
210+
// Perform auto-claim
211+
client.auto_claim(&vault_id, &keeper);
220212

221-
// Initialize contract with admin
222-
let initial_supply = 1000000i128;
223-
client.initialize(&admin, &initial_supply);
213+
// Verify vault state
214+
let vault = client.get_vault(&vault_id);
215+
assert_eq!(vault.released_amount, claimable);
224216

225-
226-
env.as_contract(&contract_id, || {
227-
env.current_contract_address().set(&admin);
228-
});
217+
// Verify keeper received fee
218+
let accumulated_fee = client.get_keeper_fee(&keeper);
219+
assert_eq!(accumulated_fee, keeper_fee);
229220

230-
221+
// Test: Calling auto_claim again immediately should fail (no tokens to cover fee)
222+
let result = std::panic::catch_unwind(|| {
223+
client.auto_claim(&vault_id, &keeper);
231224
});
232225
assert!(result.is_err());
233-
}
234-
235-
#[test]
236-
237-
let env = Env::default();
238-
let contract_id = env.register(VestingContract, ());
239-
let client = VestingContractClient::new(&env, &contract_id);
240226

241-
// Create addresses for testing
242-
let admin = Address::generate(&env);
243-
let vault_owner = Address::generate(&env);
244-
245-
246-
// Initialize contract with admin
247-
let initial_supply = 1000000i128;
248-
client.initialize(&admin, &initial_supply);
227+
// Advance time to end
228+
env.ledger().set_timestamp(end_time);
249229

250-
251-
env.as_contract(&contract_id, || {
252-
env.current_contract_address().set(&admin);
253-
});
230+
// Perform another auto-claim
231+
let current_released = client.get_vault(&vault_id).released_amount;
232+
let second_claimable = client.get_claimable_amount(&vault_id);
233+
client.auto_claim(&vault_id, &keeper);
254234

235+
let vault_final = client.get_vault(&vault_id);
236+
assert_eq!(vault_final.released_amount, current_released + second_claimable);
237+
assert_eq!(client.get_keeper_fee(&keeper), keeper_fee * 2);
238+
}

0 commit comments

Comments
 (0)