diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index 322e49d..1def9ba 100644 --- a/contracts/claims-processor/src/lib.rs +++ b/contracts/claims-processor/src/lib.rs @@ -359,6 +359,35 @@ impl ClaimsProcessor { claim_id } + /// Submit multiple claims in a single transaction up to MAX_BATCH_SIZE. + pub fn batch_submit_claims(env: Env, claimant: Address, policy_ids: Vec) -> Vec { + claimant.require_auth(); + Self::require_not_paused(&env); + + let mut claim_ids = Vec::new(&env); + let count = if policy_ids.len() > MAX_BATCH_SIZE { + MAX_BATCH_SIZE + } else { + policy_ids.len() + }; + + for i in 0..count { + let pid = policy_ids.get_unchecked(i); + let cid = Self::submit_claim(env.clone(), claimant.clone(), pid); + claim_ids.push_back(cid); + } + + env.events().publish( + (Symbol::new(&env, "batch_claims_submitted"),), + BatchClaimsSubmitted { + claimant, + count, + }, + ); + + claim_ids + } + /// Process an existing pending claim. Reads oracle data and pays out or rejects. /// /// `partial_payout_bps` is an optional payout ratio in basis points (0-10000). @@ -389,6 +418,41 @@ impl ClaimsProcessor { Self::evaluate_and_settle(&env, &mut claim, &policy, partial_payout_bps) } + /// Process multiple existing claims in a single transaction up to MAX_BATCH_SIZE. + pub fn batch_process_claims( + env: Env, + keeper: Address, + claim_ids: Vec, + partial_payout_bps: Option, + ) -> Vec<(u128, ClaimResult)> { + Self::require_keeper(&env, &keeper); + Self::require_not_paused(&env); + + let mut results = Vec::new(&env); + let count = if claim_ids.len() > MAX_BATCH_SIZE { + MAX_BATCH_SIZE + } else { + claim_ids.len() + }; + + for i in 0..count { + let cid = claim_ids.get_unchecked(i); + let res = Self::process_claim(env.clone(), keeper.clone(), cid, partial_payout_bps); + results.push_back((cid, res)); + } + + env.events().publish( + (Symbol::new(&env, "batch_claims_processed"),), + BatchClaimsProcessed { + keeper, + count, + }, + ); + + results + } + + /// Keeper-triggered automatic processing — no prior `submit_claim` needed. /// This is the primary flow for parametric insurance. /// Returns AlreadyClaimed / Expired idempotently if policy is already settled. diff --git a/contracts/claims-processor/src/test.rs b/contracts/claims-processor/src/test.rs index 27556d0..ed363d5 100644 --- a/contracts/claims-processor/src/test.rs +++ b/contracts/claims-processor/src/test.rs @@ -1278,3 +1278,36 @@ fn test_resolve_dispute_paid_claim_fails() { cp.resolve_dispute(&w.admin, &claim_id); } + +// ── Batch Claim Processing (Issue #427) ────────────────────────────────────── + +#[test] +fn test_batch_submit_and_process_claims() { + let w = deploy(); + let pid = create_crop_product(&w); + let buyer = Address::generate(&w.env); + let pol_id1 = buy_crop_policy(&w, &buyer, pid); + let pol_id2 = buy_crop_policy(&w, &buyer, pid); + + submit_rainfall(&w, 20_000_000); // 20mm < 50mm threshold + + let cp = ClaimsProcessorClient::new(&w.env, &w.claims_id); + + let mut policy_ids = soroban_sdk::Vec::new(&w.env); + policy_ids.push_back(pol_id1); + policy_ids.push_back(pol_id2); + + let claim_ids = cp.batch_submit_claims(&buyer, &policy_ids); + assert_eq!(claim_ids.len(), 2); + + let pending = cp.get_pending_claims(); + assert_eq!(pending.len(), 2); + + let results = cp.batch_process_claims(&w.keeper, &claim_ids, &None); + assert_eq!(results.len(), 2); + assert_eq!(results.get_unchecked(0).1, ClaimResult::Paid); + assert_eq!(results.get_unchecked(1).1, ClaimResult::Paid); + + assert_eq!(cp.get_pending_claims().len(), 0); +} + diff --git a/contracts/claims-processor/src/types.rs b/contracts/claims-processor/src/types.rs index 86ef6ee..ac63522 100644 --- a/contracts/claims-processor/src/types.rs +++ b/contracts/claims-processor/src/types.rs @@ -119,6 +119,21 @@ pub struct ClaimSubmitted { pub coverage_amount: i128, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BatchClaimsSubmitted { + pub claimant: Address, + pub count: u32, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BatchClaimsProcessed { + pub keeper: Address, + pub count: u32, +} + + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClaimProcessed { diff --git a/contracts/governance-dao/src/lib.rs b/contracts/governance-dao/src/lib.rs index 01c99b8..9d3ad6e 100644 --- a/contracts/governance-dao/src/lib.rs +++ b/contracts/governance-dao/src/lib.rs @@ -108,8 +108,11 @@ enum StorageKey { TemplateList, /// Risk pool contract address for querying LP vote delegation. RiskPool, + /// On-chain audit trail record for executed proposal — proposal_id -> ExecutionAuditRecord. + ExecutionAudit(u64), } + #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] @@ -1099,12 +1102,41 @@ impl GovernanceDao { .persistent() .set(&StorageKey::Proposal(proposal_id), &proposal); + let executed_at = env.ledger().timestamp(); + let audit = ExecutionAuditRecord { + proposal_id, + executor: proposal.proposer.clone(), + target: proposal.target.clone(), + function: proposal.function.clone(), + executed_at, + votes_for: proposal.votes_for, + votes_against: proposal.votes_against, + }; + let audit_key = StorageKey::ExecutionAudit(proposal_id); + env.storage().persistent().set(&audit_key, &audit); + env.storage().persistent().extend_ttl(&audit_key, TTL_THRESHOLD, TTL_EXTEND_TO); + env.events().publish( (Symbol::new(&env, "proposal_executed"),), - ProposalExecuted { proposal_id }, + ProposalExecuted { + proposal_id, + executor: proposal.proposer, + target: proposal.target, + function: proposal.function, + executed_at, + }, ); } + /// Return the execution audit trail record for an executed proposal. + pub fn get_execution_audit(env: Env, proposal_id: u64) -> ExecutionAuditRecord { + env.storage() + .persistent() + .get(&StorageKey::ExecutionAudit(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)) + } + + /// Admin-only: cancel an Active proposal before voting closes. /// /// Refunds the proposer's deposit (the exact amount locked at diff --git a/contracts/governance-dao/src/test.rs b/contracts/governance-dao/src/test.rs index 4ba314b..db67e8f 100644 --- a/contracts/governance-dao/src/test.rs +++ b/contracts/governance-dao/src/test.rs @@ -1293,3 +1293,49 @@ fn deactivated_template_cannot_be_used() { ); assert!(result.is_err()); } + +// ── Governance Execution Audit Trail (Issue #428) ───────────────────────────── + +#[test] +fn test_get_execution_audit_records_data() { + let (env, dao, _, voter1, voter2, target) = setup(); + let args: Vec = Vec::new(&env); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Execute and audit me"), + &target, + &Symbol::new(&env, "update"), + &args, + ); + dao.vote(&voter1, &pid, &VoteChoice::For); + dao.vote(&voter2, &pid, &VoteChoice::For); + + env.ledger() + .with_mut(|l| l.timestamp += VOTING_PERIOD + (24 * 3600) + 1); + + dao.finalize(&pid); + dao.execute(&pid); + + let audit = dao.get_execution_audit(&pid); + assert_eq!(audit.proposal_id, pid); + assert_eq!(audit.target, target); + assert_eq!(audit.function, Symbol::new(&env, "update")); + assert_eq!(audit.executed_at, env.ledger().timestamp()); + assert!(audit.votes_for > 0); +} + +#[test] +#[should_panic(expected = "Error(Contract, #5)")] +fn test_get_execution_audit_unexecuted_panics() { + let (env, dao, _, voter1, _, target) = setup(); + let args: Vec = Vec::new(&env); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Unexecuted"), + &target, + &Symbol::new(&env, "update"), + &args, + ); + dao.get_execution_audit(&pid); +} + diff --git a/contracts/governance-dao/src/types.rs b/contracts/governance-dao/src/types.rs index 3a34a18..0e85481 100644 --- a/contracts/governance-dao/src/types.rs +++ b/contracts/governance-dao/src/types.rs @@ -245,12 +245,29 @@ pub struct ProposalFinalized { pub status: ProposalStatus, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExecutionAuditRecord { + pub proposal_id: u64, + pub executor: Address, + pub target: Address, + pub function: Symbol, + pub executed_at: u64, + pub votes_for: i128, + pub votes_against: i128, +} + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct ProposalExecuted { pub proposal_id: u64, + pub executor: Address, + pub target: Address, + pub function: Symbol, + pub executed_at: u64, } + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct ProposalCancelled { diff --git a/contracts/oracle-verifier/src/lib.rs b/contracts/oracle-verifier/src/lib.rs index 58fcda0..38710b9 100644 --- a/contracts/oracle-verifier/src/lib.rs +++ b/contracts/oracle-verifier/src/lib.rs @@ -1171,6 +1171,145 @@ impl OracleVerifier { .unwrap_or(0) } + // ── Geographic Weighting (Issue #426) ─────────────────────────────────── + + /// Admin-only: Set geographic weighting multiplier for an oracle in basis points (10000 = 1.0x). + pub fn set_oracle_geo_weight( + env: Env, + admin: Address, + oracle: Address, + data_type: Symbol, + region: Symbol, + geo_weight_bps: u32, + ) { + Self::require_admin(&env, &admin); + if geo_weight_bps == 0 { + panic_with_error!(&env, Error::InvalidConfidence); + } + let key = StorageKey::GeoWeight(data_type.clone(), oracle.clone(), region.clone()); + env.storage().persistent().set(&key, &geo_weight_bps); + env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); + + env.events().publish( + (Symbol::new(&env, "geo_weight_updated"),), + GeoWeightUpdated { + oracle, + data_type, + region, + geo_weight_bps, + }, + ); + } + + /// Return the geographic weighting multiplier in basis points for (data_type, oracle, region). + /// Defaults to 10,000 (1.0x baseline weight) if unconfigured. + pub fn get_oracle_geo_weight( + env: Env, + oracle: Address, + data_type: Symbol, + region: Symbol, + ) -> u32 { + env.storage() + .persistent() + .get(&StorageKey::GeoWeight(data_type, oracle, region)) + .unwrap_or(10_000) + } + + /// Return aggregated data for a specific geographic region, applying geographic weighting to active oracles. + pub fn get_aggregated_for_region( + env: Env, + data_type: Symbol, + key: Symbol, + max_age_seconds: u64, + target_region: Symbol, + ) -> AggregatedData { + let points: Vec = env + .storage() + .persistent() + .get(&StorageKey::DataPoints(data_type.clone(), key.clone())) + .unwrap_or_else(|| panic_with_error!(&env, Error::NoDataAvailable)); + if points.is_empty() { + panic_with_error!(&env, Error::NoDataAvailable); + } + + let now = env.ledger().timestamp(); + let min_confidence: u32 = env + .storage() + .instance() + .get(&StorageKey::MinConfidence) + .unwrap_or(0); + + let mut values = [(0i128, 0u32); 100]; + let mut count: usize = 0; + let mut total_effective_weight: u32 = 0; + let mut weighted_confidence_sum: u64 = 0; + let mut min_conf: u32 = 100; + let mut newest_timestamp: u64 = 0; + + for i in 0..points.len() { + let p = points.get_unchecked(i); + let age = now.saturating_sub(p.timestamp); + if age <= max_age_seconds && p.confidence >= min_confidence { + let oracle_key = StorageKey::Oracle(data_type.clone(), p.oracle.clone()); + let base_weight = match env.storage().persistent().get::<_, OracleEntry>(&oracle_key) { + Some(entry) if entry.active => entry.weight, + _ => continue, + }; + + let geo_multiplier = Self::get_oracle_geo_weight(env.clone(), p.oracle.clone(), data_type.clone(), target_region.clone()); + let effective_weight = ((base_weight as u64 * geo_multiplier as u64) / 10_000) as u32; + let effective_weight = if effective_weight == 0 { 1 } else { effective_weight }; + + if count < 100 { + values[count] = (p.value, effective_weight); + count += 1; + } + + total_effective_weight += effective_weight; + weighted_confidence_sum += p.confidence as u64 * effective_weight as u64; + if p.confidence < min_conf { + min_conf = p.confidence; + } + if p.timestamp > newest_timestamp { + newest_timestamp = p.timestamp; + } + } + } + + if count == 0 { + panic_with_error!(&env, Error::NoDataAvailable); + } + + let slice = &mut values[..count]; + slice.sort_by(|a, b| a.0.cmp(&b.0)); + + let half_weight = total_effective_weight / 2; + let mut accum: u32 = 0; + let mut median: i128 = slice[0].0; + for &(val, weight) in slice.iter() { + accum += weight; + if accum >= half_weight { + median = val; + break; + } + } + + let avg_confidence = if total_effective_weight > 0 { + (weighted_confidence_sum / total_effective_weight as u64) as u32 + } else { + 0 + }; + + AggregatedData { + median_value: median, + oracle_count: count as u32, + active_oracle_count: count as u32, + confidence: avg_confidence, + min_confidence: min_conf, + last_updated: newest_timestamp, + } + } + /// Withdraw the caller's full stake for `data_type`. Only permitted once /// the oracle is not an active registration for that data_type (never /// registered, or previously removed via `remove_oracle`) — an active diff --git a/contracts/oracle-verifier/src/test.rs b/contracts/oracle-verifier/src/test.rs index 0056803..3cdc396 100644 --- a/contracts/oracle-verifier/src/test.rs +++ b/contracts/oracle-verifier/src/test.rs @@ -1188,3 +1188,54 @@ fn test_invalidate_data_scoped_to_key() { let data = client.get_data(&weather(), &key2); assert_eq!(data.value, 45_000_000i128); } + +// ── Geographic Weighting (Issue #426) ────────────────────────────────────────── + +#[test] +fn test_geo_weight_set_and_get() { + let (env, admin, contract_id) = setup(); + let client = OracleVerifierClient::new(&env, &contract_id); + let oracle = Address::generate(&env); + let region = symbol_short!("AFR"); + + client.add_oracle(&admin, &oracle, &weather(), &50u32); + assert_eq!(client.get_oracle_geo_weight(&oracle, &weather(), ®ion), 10_000); + + client.set_oracle_geo_weight(&admin, &oracle, &weather(), ®ion, &20_000u32); + assert_eq!(client.get_oracle_geo_weight(&oracle, &weather(), ®ion), 20_000); +} + +#[test] +fn test_geo_weight_aggregation_prioritizes_local_oracle() { + let (env, admin, contract_id) = setup(); + let client = OracleVerifierClient::new(&env, &contract_id); + let oracle1 = Address::generate(&env); + let oracle2 = Address::generate(&env); + let region = symbol_short!("AFR"); + + client.add_oracle(&admin, &oracle1, &weather(), &50u32); + client.add_oracle(&admin, &oracle2, &weather(), &50u32); + + let ts = env.ledger().timestamp(); + client.submit_data(&oracle1, &weather(), &kisumu_key(), &10_000_000i128, &90u32, &ts); + client.submit_data(&oracle2, &weather(), &kisumu_key(), &30_000_000i128, &90u32, &ts); + + // Give oracle2 a 3x geographic weighting in AFR region + client.set_oracle_geo_weight(&admin, &oracle2, &weather(), ®ion, &30_000u32); + + let agg = client.get_aggregated_for_region(&weather(), &kisumu_key(), &3600u64, ®ion); + assert_eq!(agg.median_value, 30_000_000i128); +} + +#[test] +#[should_panic(expected = "Error(Contract, #3)")] +fn test_non_admin_cannot_set_geo_weight() { + let (env, _admin, contract_id) = setup(); + let client = OracleVerifierClient::new(&env, &contract_id); + let oracle = Address::generate(&env); + let impostor = Address::generate(&env); + let region = symbol_short!("AFR"); + + client.set_oracle_geo_weight(&impostor, &oracle, &weather(), ®ion, &20_000u32); +} + diff --git a/contracts/oracle-verifier/src/types.rs b/contracts/oracle-verifier/src/types.rs index 51823d1..3cc273d 100644 --- a/contracts/oracle-verifier/src/types.rs +++ b/contracts/oracle-verifier/src/types.rs @@ -246,6 +246,15 @@ pub struct MinOracleCountUpdated { pub min_count: u32, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GeoWeightUpdated { + pub oracle: Address, + pub data_type: Symbol, + pub region: Symbol, + pub geo_weight_bps: u32, +} + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct DataTypeMinOracleCountUpdated { diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index 16de772..03e07fb 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -473,8 +473,99 @@ impl RiskPool { amount } + /// Transfer `shares` from `from` address to `to` address. + /// Returns the proportional USDC deposit amount transferred. + pub fn transfer_position(env: Env, from: Address, to: Address, shares: i128) -> i128 { + from.require_auth(); + if shares <= 0 { panic_with_error!(&env, Error::ZeroAmount); } + if from == to { panic_with_error!(&env, Error::InvalidAddress); } + Self::assert_active(&env); + + let from_key = StorageKey::LpPosition(from.clone()); + let mut from_pos: LpPosition = env.storage().persistent() + .get(&from_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::NoShares)); + + if from_pos.shares < shares { + panic_with_error!(&env, Error::InsufficientShares); + } + + let amount = shares.checked_mul(from_pos.deposited) + .and_then(|v| v.checked_div(from_pos.shares)) + .unwrap_or_else(|| panic_with_error!(&env, Error::Overflow)); + + let pending_yield_from = Self::settle_yield(&env, &mut from_pos); + from_pos.deposited = from_pos.deposited.saturating_sub(amount); + from_pos.shares -= shares; + let acc_per_share: i128 = env.storage().instance().get(&StorageKey::AccumulatedPerShare).unwrap_or(0); + from_pos.yield_debt = (acc_per_share * from_pos.shares) / 1_000_000_000_000; + env.storage().persistent().set(&from_key, &from_pos); + Self::extend_to_max(&env, &from_key); + + Self::update_lp_nft(&env, &from, &from_pos); + Self::pay_out_yield(&env, &from, pending_yield_from); + + let now = env.ledger().timestamp(); + let to_key = StorageKey::LpPosition(to.clone()); + let mut is_new_lp = false; + let mut to_pos: LpPosition = match env.storage().persistent().get::<_, LpPosition>(&to_key) { + Some(mut pos) => { + let pending_yield_to = Self::settle_yield(&env, &mut pos); + pos.deposited += amount; + pos.shares += shares; + pos.yield_debt = (acc_per_share * pos.shares) / 1_000_000_000_000; + env.storage().persistent().set(&to_key, &pos); + Self::extend_to_max(&env, &to_key); + Self::pay_out_yield(&env, &to, pending_yield_to); + pos + } + None => { + is_new_lp = true; + let count: u32 = env.storage().instance() + .get(&StorageKey::LpCount).unwrap_or(0); + let lp_address_key = StorageKey::LpAddress(count); + env.storage().persistent().set(&lp_address_key, &to); + Self::extend_to_max(&env, &lp_address_key); + env.storage().instance().set(&StorageKey::LpCount, &(count + 1)); + let pos = LpPosition { + provider: to.clone(), + deposited: amount, + shares, + yield_claimed: 0, + yield_debt: (acc_per_share * shares) / 1_000_000_000_000, + deposited_at: now, + last_yield_claim: now, + compound_enabled: false, + }; + env.storage().persistent().set(&to_key, &pos); + Self::extend_to_max(&env, &to_key); + pos + } + }; + + let category: Symbol = env.storage().instance().get(&StorageKey::Category).unwrap(); + if is_new_lp { + Self::mint_lp_nft(&env, &to, &category, now, &to_pos); + } else { + Self::update_lp_nft(&env, &to, &to_pos); + } + + env.events().publish( + (Symbol::new(&env, "lp_position_transferred"),), + LpPositionTransferred { + from, + to, + shares, + amount, + }, + ); + + amount + } + // ── Premium and yield ───────────────────────────────────────────────────── + /// Pull `amount` USDC from `caller` and split it among LPs, treasury, and backstop /// according to the protocol fee schedule. No-op if `amount` is zero or negative. pub fn receive_premium(env: Env, caller: Address, amount: i128) { diff --git a/contracts/risk-pool/src/test.rs b/contracts/risk-pool/src/test.rs index 5eb9556..ca0d536 100644 --- a/contracts/risk-pool/src/test.rs +++ b/contracts/risk-pool/src/test.rs @@ -1107,3 +1107,73 @@ fn direct_withdraw_still_works_when_no_delay_is_set() { assert_eq!(amount, 10_000_0000000i128); } + +// ── position transfer (Issue #425) ────────────────────────────────────────────── + +#[test] +fn test_transfer_position_full() { + let (env, pool, _usdc, _admin, _t, lp1) = setup(); + let lp2 = Address::generate(&env); + + let amount = 1000_0000000i128; + let shares = pool.deposit(&lp1, &amount, &0i128, &false); + + let transferred_amount = pool.transfer_position(&lp1, &lp2, &shares); + assert_eq!(transferred_amount, amount); + + let pos1 = pool.get_position(&lp1).unwrap(); + assert_eq!(pos1.shares, 0); + assert_eq!(pos1.deposited, 0); + + let pos2 = pool.get_position(&lp2).unwrap(); + assert_eq!(pos2.shares, shares); + assert_eq!(pos2.deposited, amount); +} + +#[test] +fn test_transfer_position_partial() { + let (env, pool, _usdc, _admin, _t, lp1) = setup(); + let lp2 = Address::generate(&env); + + let amount = 1000_0000000i128; + let shares = pool.deposit(&lp1, &amount, &0i128, &false); + + let half_shares = shares / 2; + let transferred_amount = pool.transfer_position(&lp1, &lp2, &half_shares); + assert_eq!(transferred_amount, amount / 2); + + let pos1 = pool.get_position(&lp1).unwrap(); + assert_eq!(pos1.shares, shares - half_shares); + assert_eq!(pos1.deposited, amount - transferred_amount); + + let pos2 = pool.get_position(&lp2).unwrap(); + assert_eq!(pos2.shares, half_shares); + assert_eq!(pos2.deposited, transferred_amount); +} + +#[test] +#[should_panic(expected = "Error(Contract, #13)")] +fn test_transfer_position_self_fails() { + let (_env, pool, _usdc, _admin, _t, lp1) = setup(); + let shares = pool.deposit(&lp1, &1000_0000000i128, &0i128, &false); + pool.transfer_position(&lp1, &lp1, &shares); +} + +#[test] +#[should_panic(expected = "Error(Contract, #5)")] +fn test_transfer_position_zero_shares_fails() { + let (env, pool, _usdc, _admin, _t, lp1) = setup(); + let lp2 = Address::generate(&env); + pool.deposit(&lp1, &1000_0000000i128, &0i128, &false); + pool.transfer_position(&lp1, &lp2, &0i128); +} + +#[test] +#[should_panic(expected = "Error(Contract, #4)")] +fn test_transfer_position_insufficient_shares_fails() { + let (env, pool, _usdc, _admin, _t, lp1) = setup(); + let lp2 = Address::generate(&env); + let shares = pool.deposit(&lp1, &1000_0000000i128, &0i128, &false); + pool.transfer_position(&lp1, &lp2, &(shares + 1)); +} + diff --git a/contracts/risk-pool/src/types.rs b/contracts/risk-pool/src/types.rs index 9581be3..ea86c4e 100644 --- a/contracts/risk-pool/src/types.rs +++ b/contracts/risk-pool/src/types.rs @@ -418,6 +418,15 @@ pub struct CompoundYieldToggled { pub enabled: bool, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LpPositionTransferred { + pub from: Address, + pub to: Address, + pub shares: i128, + pub amount: i128, +} + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct PoolCapacityUpdated {