Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions contracts/governance-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,72 @@ impl GovernanceTokenContract {
.get(&DataKey::Delegation(delegator))
}

/// Take a voting snapshot for a voter at a given ledger sequence.
/// Stores the voter's own balance plus any delegated power they hold
/// at that point in time. Governance-dao should use this snapshot
/// balance when a proposal is being voted on to prevent flash-loan attacks.
pub fn take_snapshot(env: Env, voter: Address, ledger_sequence: u32) {
env.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
voter.require_auth();

// Only allow snapshotting at or before the current ledger
if ledger_sequence > env.ledger().sequence() {
panic!("cannot snapshot a future ledger");
}

let own_balance: i128 = env
.storage()
.persistent()
.get(&DataKey::Balance(voter.clone()))
.unwrap_or(0);
let delegated_power: i128 = env
.storage()
.persistent()
.get(&DataKey::DelegatedPower(voter.clone()))
.unwrap_or(0);

// If the voter has delegated their power away, snapshot as 0
let delegation = env
.storage()
.persistent()
.get::<DataKey, Delegation>(&DataKey::Delegation(voter.clone()));
let snapshot_power = if delegation.is_some() {
0i128
} else {
own_balance + delegated_power
};

let key = DataKey::VotingSnapshot(voter.clone(), ledger_sequence);
env.storage().persistent().set(&key, &snapshot_power);
env.storage().persistent().extend_ttl(
&key,
PERSISTENT_LIFETIME_THRESHOLD,
PERSISTENT_BUMP_AMOUNT,
);

env.events().publish(
(symbol_short!("snapshot"),),
(voter, ledger_sequence, snapshot_power),
);
}

/// Retrieve a previously taken voting snapshot.
/// Returns None if no snapshot exists for this voter at the given ledger.
pub fn get_voting_snapshot(
env: Env,
voter: Address,
ledger_sequence: u32,
) -> Option<i128> {
env.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
env.storage()
.persistent()
.get(&DataKey::VotingSnapshot(voter, ledger_sequence))
}

pub fn propose_admin(env: Env, current_admin: Address, new_admin: Address) {
pulsar_common_admin::propose_admin(
&env,
Expand Down
6 changes: 6 additions & 0 deletions contracts/liquidity-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ impl LiquidityPoolContract {
panic!("duration_secs must be greater than zero");
}

// Reject unreasonably long loan durations (max 1 year = 31,557,600 seconds)
const MAX_DURATION_SECS: u64 = 31_557_600;
if duration_secs > MAX_DURATION_SECS {
panic!("duration_secs exceeds maximum allowed loan duration");
}

if env
.storage()
.persistent()
Expand Down
35 changes: 35 additions & 0 deletions contracts/milestone-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,41 @@ impl MilestoneTrackerContract {
.get(&DataKey::Milestone(milestone_id))
}

/// Permissionless function to mark an expired milestone as Missed.
/// Anyone can call this once the deadline has passed and the milestone
/// is not already finalized, unblocking downstream logic when the oracle
/// is offline or throttled.
pub fn mark_missed(env: Env, milestone_id: u64) {
env.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

let mut milestone: Milestone = env
.storage()
.persistent()
.get(&DataKey::Milestone(milestone_id))
.expect("not found");

if milestone.status == MilestoneStatus::Achieved
|| milestone.status == MilestoneStatus::Missed
{
panic!("already finalized");
}

if env.ledger().timestamp() <= milestone.deadline {
panic!("deadline has not passed");
}

milestone.status = MilestoneStatus::Missed;
let _ttl_key = DataKey::Milestone(milestone_id);
env.storage().persistent().set(&_ttl_key, &milestone);
env.storage().persistent().extend_ttl(
&_ttl_key,
PERSISTENT_LIFETIME_THRESHOLD,
PERSISTENT_BUMP_AMOUNT,
);
}

pub fn get_campaign_milestone_count(env: Env, campaign_id: u64) -> u64 {
env.storage()
.instance()
Expand Down
9 changes: 7 additions & 2 deletions contracts/rewards-distributor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,13 @@ impl RewardsDistributorContract {
let vested_total = if rewards.vesting_duration == 0 {
rewards.total_earned
} else {
(rewards.total_earned as u128 * vesting_fraction as u128
/ rewards.vesting_duration as u128) as i128
// Use a 10_000 bps scaling factor to preserve precision for small rewards
// with long vesting durations, preventing integer division truncation to 0.
let vested_bps = rewards.total_earned as u128
* vesting_fraction as u128
* 10_000
/ rewards.vesting_duration as u128;
(vested_bps / 10_000) as i128
};
let claimable = vested_total.saturating_sub(rewards.total_claimed);

Expand Down
Loading