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:
Total supply at snapshot time
Balance of each holder
List of holder addresses ← Critical for fair distribution
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 USDCKey Point: Each period's snapshot is frozen independently. P2 snapshot doesn't affect P1 distribution.
Yield Calculation Formula
Formula
userYield = (snapshotBalance / snapshotTotalSupply) × totalYieldDepositedExample 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:
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 timeDesign 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:
Attacker sees distribution transaction in mempool
Attacker front-runs with token purchase
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:
Attacker accumulates large position
Attacker front-runs snapshot trigger
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
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);Related Documentation
YRT Factory Overview - Multi-period system
Chainlink Automation - Automatic snapshot triggering
Property Onboarding - Period management
Glossary - Term definitions
Key Takeaways:
Snapshot captures BOTH balances AND holder addresses
Each period has independent snapshot
Distribution uses snapshot holders, not current holders
Prevents manipulation through post-snapshot transfers
Gas-efficient through active holder tracking
Last Updated: october 2025
Last updated
