Snapshot System

Deep dive into the multi-period snapshot system for fair yield distribution in YRT tokens.


The Problem: Distribution Bypass

Without snapshots, users could manipulate yield distribution through strategic token transfers.

Attack Scenario (Without Snapshots)

Problem: Last-minute token transfers can bypass fair distribution!


The Solution: Snapshot Holders

The snapshot system records both balances AND holder addresses at maturity date, preventing manipulation.

Fair Distribution (With Snapshots)

Solution: Distribution uses frozen snapshot, not current balances!


Implementation Architecture

Key Innovation: Holder List

The periodSnapshotHolders mapping stores the list of addresses that held tokens at snapshot time.

Why this matters:

  • Distribution can iterate through snapshot holders only

  • No need to check entire blockchain for past holders

  • Prevents distribution bypass via post-snapshot transfers


Snapshot Lifecycle

1. Holder Tracking (Continuous)

Holders are tracked automatically on every transfer:

Example:

Initial: allHolders = []

Alice buys 100 YRT:
→ allHolders = [Alice]

Bob buys 50 YRT:
→ allHolders = [Alice, Bob]

Charlie buys 25 YRT:
→ allHolders = [Alice, Bob, Charlie]

Bob sells all 50 YRT:
→ allHolders = [Alice, Charlie]

2. Snapshot Trigger (At Maturity)

Snapshot is triggered automatically by Chainlink Automation when maturity date is reached

What gets frozen:

  1. Total supply at snapshot time

  2. Balance of each holder

  3. List of holder addresses ← Critical for fair distribution

  4. Timestamp of snapshot

3. Distribution (Post-Snapshot)

Distribution uses snapshot holders list, not current holders


Multi-Period Independence

Each period maintains completely independent snapshots.

Example: 3 Periods

Period 1 (Q1 2024):
Snapshot taken: Jan 31, 2024
Holders: [Alice: 100, Bob: 50, Charlie: 25]
Total: 175 YRT
Yield: 10,000 USDC

Period 2 (Q2 2024):
Snapshot taken: Apr 30, 2024
Holders: [Alice: 150, David: 100] ← Bob & Charlie sold
Total: 250 YRT
Yield: 15,000 USDC

Period 3 (Q3 2024):
Snapshot taken: Jul 31, 2024
Holders: [Alice: 200, David: 50, Eve: 50] ← Independent from P1 & P2
Total: 300 YRT
Yield: 18,000 USDC

Key Point: Each period's snapshot is frozen independently. P2 snapshot doesn't affect P1 distribution.


Yield Calculation Formula

Formula

userYield = (snapshotBalance / snapshotTotalSupply) × totalYieldDeposited

Example Calculation

Period 1 Data:

Total Yield Deposited: 100,000 USDC
Snapshot Total Supply: 1000 YRT

Snapshot Holders:
- Alice: 250 YRT (25%)
- Bob: 200 YRT (20%)
- Charlie: 150 YRT (15%)
- Property Owner: 400 YRT (40%)

Distribution Calculation:

Holder
Snapshot Balance
Share
Yield Received

Alice

250 YRT

25%

25,000 USDC

Bob

200 YRT

20%

20,000 USDC

Charlie

150 YRT

15%

15,000 USDC

Owner

400 YRT

40%

40,000 USDC

Total

1000 YRT

100%

100,000 USDC


Edge Cases Handled

1. Post-Snapshot Transfers

Scenario: Alice sells tokens after snapshot but before distribution.

Timeline:
1. Snapshot taken: Alice has 100 YRT
2. Alice sells 100 YRT to Bob
3. Distribution happens

Result:
✅ Alice receives yield (she held at snapshot)
✅ Bob receives nothing this period (will get P2 yield if he holds)

Why it works: Distribution uses periodSnapshotHolders[periodId] which is frozen.

2. Pre-Snapshot Trading

Scenario: Alice buys YRT 1 day before snapshot.

Timeline:
1. Alice buys 100 YRT (1 day before maturity)
2. Snapshot taken: Alice has 100 YRT
3. Distribution: Alice receives full proportional yield

Result:
✅ Fair - Alice owned tokens at snapshot time

Design Choice: Snapshot captures ownership at specific timestamp. All holders at that moment receive yield proportionally.

3. Zero Balance Holders

Scenario: Bob had tokens but sold them all before snapshot.

// In takeSnapshotForPeriod():
uint256 balance = balanceOf(holder);
if (balance > 0) {  // ← Skip zero balance holders
    periodSnapshotBalances[_periodId][holder] = balance;
    periodSnapshotHolders[_periodId].push(holder);
}

Result: Bob is not included in snapshot holders list, saves gas during distribution.

4. Multiple Periods Same Holder

Scenario: Alice holds through P1, P2, P3.

Period 1: Alice receives 25% of P1 yield
Period 2: Alice receives 40% of P2 yield (increased holdings)
Period 3: Alice receives 30% of P3 yield (decreased holdings)

Each period is independent. Alice's P1 distribution doesn't affect P2 or P3.


Gas Optimization

Efficient Holder Tracking

Instead of scanning all addresses, we maintain allHolders array:

// Add holder: O(1)
function _addHolder(address holder) private {
    isHolder[holder] = true;
    holderIndex[holder] = allHolders.length;
    allHolders.push(holder);
}

// Remove holder: O(1) with swap-and-pop
function _removeHolder(address holder) private {
    uint256 indexToRemove = holderIndex[holder];
    uint256 lastIndex = allHolders.length - 1;

    if (indexToRemove != lastIndex) {
        address lastHolder = allHolders[lastIndex];
        allHolders[indexToRemove] = lastHolder;
        holderIndex[lastHolder] = indexToRemove;
    }

    allHolders.pop();
    delete holderIndex[holder];
}

Benefit: Snapshot only loops through current holders (e.g., 100 iterations), not all historical addresses (e.g., 10,000).

Batch Distribution

For large holder counts, distribution can be batched:

function batchDistribute(
    uint256 _seriesId,
    uint256 _periodId,
    address[] calldata _holders,
    uint256 _batchSize
) external {
    // Process in batches to avoid gas limit
    for (uint256 i = 0; i < _batchSize && i < _holders.length; i++) {
        distributeYieldToHolder(_seriesId, _periodId, _holders[i]);
    }
}

Security Considerations

Attack Vector: Front-Running Distribution

Attempted Attack:

  1. Attacker sees distribution transaction in mempool

  2. Attacker front-runs with token purchase

  3. Hopes to receive yield

Mitigation:

// Distribution uses SNAPSHOT holders, not current
address[] memory holders = token.getSnapshotHoldersForPeriod(_periodId);

Attacker's address is not in snapshot, so they receive nothing. ✅

Attack Vector: Snapshot Manipulation

Attempted Attack:

  1. Attacker accumulates large position

  2. Attacker front-runs snapshot trigger

  3. Sells immediately after snapshot

Reality: This is not an attack - it's expected behavior! Snapshot captures ownership at specific moment. If attacker held tokens at snapshot time, they deserve proportional yield.

Why it's fair:

  • Attacker took price risk during holding period

  • Attacker paid for tokens at market price

  • Other holders can do the same

  • Market efficiency ensures fair pricing


Comparison: With vs. Without Snapshot Holders

Scenario
Without Snapshot Holders
With Snapshot Holders

Alice holds 90 days, sells before distribution

Alice gets 0 yield ❌

Alice gets yield ✅

Bob buys after snapshot

Bob gets yield ❌

Bob gets 0 yield ✅

Gas cost

High (check all addresses)

Low (only snapshot holders)

Manipulation risk

High

None

Fairness

Unfair

Fair


API Reference

Query Snapshot Data

// Check if snapshot taken
const snapshotTaken = await token.isSnapshotTakenForPeriod(periodId);

// Get snapshot holders list
const holders = await token.getSnapshotHoldersForPeriod(periodId);
// Returns: ["0xAlice...", "0xBob...", "0xCharlie..."]

// Get specific holder's snapshot balance
const balance = await token.getSnapshotBalanceForPeriod(periodId, userAddress);

// Get snapshot total supply
const totalSupply = await token.getSnapshotTotalSupplyForPeriod(periodId);

// Calculate claimable yield
const yield = await factory.calculateClaimableYield(seriesId, periodId, userAddress);

Trigger Snapshot (Admin Only)

// Manual trigger (usually done by Chainlink)
await factory.triggerSnapshotForPeriod(seriesId, periodId);


Key Takeaways:

  1. Snapshot captures BOTH balances AND holder addresses

  2. Each period has independent snapshot

  3. Distribution uses snapshot holders, not current holders

  4. Prevents manipulation through post-snapshot transfers

  5. Gas-efficient through active holder tracking


Last Updated: october 2025

Last updated