Why Credits Exist
Without credits, points distribution would be trivially gameable. Consider the naive alternative: At epoch end, distribute points proportional to EPT balance. The attack: Deposit 10,000 USDC one second before finalization. Claim the same points share as someone who deposited 10,000 USDC at epoch start. Zero effective cost (deposit, claim, redeem ST), infinite points per dollar. Credits prevent this by tracking balance x time x activity. Your share of points is proportional to your cumulative contribution over the epoch, not your balance at a single snapshot.The Credit Formula
Continuous Form
For a single user holding a constant balance over a time interval with a constant creditRate: In the general case, where both balance and creditRate change over time, credits are the integral: Your credits are the area under the curve of(your EPT balance) x (the credit rate) over time. Holding more EPT, for longer, at higher rates = more credits.
The bars represent Alice’s balance × creditRate over time. She held 100 EPT at creditRate=1 for weeks 1—4, then transferred 50 EPT, holding 50 for weeks 5—8. Her total credits are the sum of these values over time.
Points Distribution
At finalization, the Final Points Oracle reportstotalPoints earned during the epoch. Your share:
Where totalCredits is the sum of all individual credits across all holders, and alreadyClaimed tracks incremental claims.
This is a pro-rata system: you receive points in proportion to your credit share of the total.
The Constant Rate Proof
An important mathematical property: if creditRate is constant throughout the entire epoch, its exact value doesn’t matter. Proof: Let creditRate = k (constant) for all time. For user i holding b_i EPT for t_i seconds: The constant k cancels in the ratio. Any constant rate, whether 1, 100, or 1,000,000, produces identical point distribution. When does this matter? In the oracle-down fallback. If the Credits Oracle stops publishing, credits accrue at the last known rate. If that rate stays constant for the rest of the epoch, the exact constant chosen doesn’t affect distribution. The system degrades gracefully.Variable Rate: Why It Exists
The constant rate proof shows that constant rates are mathematically sufficient. So why use a variable rate?The Fairness Argument
A funding arb strategy opens $500K of OI at epoch start, then the market shifts and the strategy scales down to $50K OI mid-epoch. Exchange points are earned proportional to OI. Most of the epoch’s points came from the first half. Constant rate (rate = 1):- Alice (100 EPT, full epoch): 100 x 1 x 604,800 = 60.48M credits
- Bob (100 EPT, second half only): 100 x 1 x 259,200 = 25.92M credits
- Alice Phase 1 (first half, rate=500): 100 x 500 x 345,600 = 17.28B
- Alice Phase 2 (second half, rate=50): 100 x 50 x 259,200 = 1.296B
- Alice total: 18.576B
- Bob Phase 2 only (second half, rate=50): 100 x 50 x 259,200 = 1.296B
| Constant Rate | Variable Rate | |
|---|---|---|
| Alice’s share | 70.0% | 93.5% |
| Bob’s share | 30.0% | 6.5% |
The Pricing Argument
Variable creditRate makes EPT valuation more informationally efficient. When creditRate is high (strategy is active), EPT accrues credits faster, making it more valuable to hold right now. This information flows into flash loop economics: during high-activity periods, the implied cost of EPT through the flash loop better reflects its true value. With constant rate, EPT’s only value driver is time remaining. The market can’t distinguish “strategy has high OI” from “strategy is idle.”Why not just use a simple time-weighted average balance?
Why not just use a simple time-weighted average balance?
Time-weighted average balance is equivalent to the constant-rate credit system (creditRate = 1). It works, but it can’t distinguish high-activity from low-activity periods. Variable creditRate adds this dimension, making the distribution fairer when strategy activity fluctuates.
Can I game the system by depositing right before a creditRate increase?
Can I game the system by depositing right before a creditRate increase?
In theory, yes. If you knew creditRate was about to increase, depositing beforehand would earn you more credits during the high-rate period. In practice, creditRate reflects real-time strategy activity (OI, volume), which is partially observable on exchange dashboards. This is public information, and flash loop demand should already reflect expected future rates. You’re competing against the market’s collective information.
On-Chain Implementation
State Variables
The contract avoids computing the integral explicitly. Instead, it uses a globalCreditIndex pattern, a single accumulator that tracks cumulative credit-seconds per EPT unit:globalCreditIndex increases monotonically. The difference between two snapshots of globalCreditIndex tells you how many credit-seconds per EPT accrued in that interval. Multiply by the user’s balance, and that’s how many credits they earned.
Algorithm: Global Checkpoint
Called before any state-changing operation. Advances the global index by the time elapsed x current rate:Algorithm: User Checkpoint
Called lazily on every transfer, mint, or claim. Settles a user’s accrued credits:Why settle credits lazily instead of on every block?
Why settle credits lazily instead of on every block?
Gas efficiency. Starknet charges for computation. Settling credits on every block for every holder would be prohibitively expensive. Lazy settlement achieves mathematical equivalence: the result is identical to continuous tracking, but computation only happens when needed (on transfer, claim, or rate change).
Algorithm: Credit Rate Update
When the Credits Oracle publishes a new rate:Algorithm: Transfer
Every EPT transfer checkpoints both parties:Algorithm: Mint (On Deposit)
Algorithm: Claim Points (Post-Finalization)
Can I see my accrued credits before finalization?
Can I see my accrued credits before finalization?
Yes. A read-only function can compute
(globalCreditIndex + creditRate x timeSinceCheckpoint - userCreditIndex) x balance + userCredits. This is a view call, no gas cost, and shows your current credit balance at any time.Worked Examples
All examples use a 604,800-second epoch (7 days) for simplicity. The math works identically for longer epochs (8—12 weeks). Only the numbers scale.- Example 1: Single Holder
- Example 2: Two Holders
- Example 3: Transfer (Constant)
- Example 4: Transfer (Variable)
- Example 5: Smart Contract
- Example 6: Attack Defeated
Single Holder, Constant Rate
The simplest possible case. One user, no transfers, constant creditRate.The globalCreditIndex Walkthrough
The globalCreditIndex is the most important state variable. It’s how the contract avoids iterating over all holders. Here’s a step-by-step trace through Example 4:Edge Cases and Invariants
Edge Case: User Checkpoints Without Balance Change
If a user callsclaimPoints() or any other checkpoint-triggering function, their credits are settled even if no transfer occurs. This is by design. It ensures userCredits is always current before computing point claims.
Edge Case: Zero-Balance User
If a user transfers all their EPT and later receives some back, theiruserCreditIndex is updated at each interaction. When they receive EPT again, delta x 0 = 0 credits for the gap period. Correct. They shouldn’t earn credits while holding zero EPT.
Edge Case: Oracle Goes Down
If the Credits Oracle stops publishing,creditRate stays at its last known value. Credits accrue at a constant rate. Per the constant rate proof, if the rate remains constant for the rest of the epoch, the exact constant doesn’t affect the final distribution. The system degrades to time-weighted balance.
Edge Case: First Depositor
When the first user mints EPT,globalCreditIndex = 0 and userCreditIndex = 0. No credits are earned retroactively. The system starts fresh.
Invariant: Credit Conservation
Credits are never created or destroyed by transfers. When Alice sends EPT to Bob:- Alice’s accrued credits are settled (added to
totalCredits) - Bob’s accrued credits are settled (added to
totalCredits) - Only balances change, not the credit index or settlement logic
totalCredits at finalization correctly equals the sum of all individual credits, regardless of how many transfers occurred.
Invariant: No Double-Counting
ThealreadyClaimed tracker in claimPoints() prevents double-counting. Even if a user calls claimPoints() multiple times, gross - alreadyClaimed ensures they only receive the incremental amount.
What happens if creditRate is set to zero?
What happens if creditRate is set to zero?
No credits accrue. If creditRate is zero for the entire epoch,
totalCredits = 0 and the pointsPerCredit calculation would divide by zero. The oracle only sets creditRate = 0 if the strategy has zero activity, in which case totalPoints should also be zero. The contract guards against this edge case with a zero-totalCredits check.Comparison: Pendle vs ArcX Credit Systems
| Pendle (YT) | ArcX (EPT) | |
|---|---|---|
| What accrues | Yield (SY tokens) | Credits (abstract units converted to points at finalization) |
| Accrual mechanism | Yield streams continuously in SY tokens. Real tokens drip to YT holders | Credits are abstract accounting. No tokens move until finalization |
| Rate source | SY exchange rate (on-chain, autonomous) | creditRate from oracle (off-chain, ArcX-operated) |
| Variable rate? | Implicitly, SY rate changes with underlying protocol APY | Explicitly, oracle pushes rate updates |
| Maturity value | YT → $0 (all yield has streamed) | EPT → redeemable for PointsTokens (retains value) |
| Checkpoint pattern | Interest index (similar to Aave/Compound) | globalCreditIndex (same mathematical pattern) |
