Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pi/special cap #4

Merged
merged 8 commits into from
Oct 25, 2023
Merged
165 changes: 132 additions & 33 deletions calculation/calculation.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,22 +183,73 @@ func isPoolQualified(program types.Program, pool types.Pool, locked uint64) (boo
if !atLeastIntegerPercent(locked, pool.TotalLPTokens, program.MinLPIntegerPercent) {
return false, fmt.Sprintf("less than %v%% of LP tokens locked", program.MinLPIntegerPercent)
}
// We'll use a true/false striping to allow/disallow this pool
// if it's in the eligible pools, or has an asset that's in the eligible assets, we'll flip it true
// if it's in the disqualified pools, or disqualified assets, we'll flip it back false
// BUT, if eligible pools and eligible assets are both nil, then we'll assume it's true unless disqualified
qualified := program.EligiblePools == nil && program.EligibleAssets == nil && program.EligiblePairs == nil
Quantumplation marked this conversation as resolved.
Show resolved Hide resolved
reason := ""
if program.EligiblePools != nil {
found := false
for _, poolIdent := range program.EligiblePools {
if poolIdent == pool.PoolIdent {
return true, ""
qualified = true
found = true
Quantumplation marked this conversation as resolved.
Show resolved Hide resolved
}
}
return false, "Program lists eligible pools, but doesn't list this pool"
} else if program.DisqualifiedPools != nil {
if !found {
reason += "Program lists eligible pools, but doesn't list this pool; "
}
}
if program.EligibleAssets != nil {
found := false
for _, assetID := range program.EligibleAssets {
if assetID == pool.AssetA || assetID == pool.AssetB {
qualified = true
found = true
}
}
if !found {
reason += "Program lists eligible assets, but doesn't list either asset from this pool; "
}
}
if program.EligiblePairs != nil {
found := false
for _, pair := range program.EligiblePairs {
if pair.AssetA == pool.AssetA && pair.AssetB == pool.AssetB {
qualified = true
found = true
}
}
if !found {
reason += "Program lists eligible pairs, but doesn't list these two assets as an eligible pair; "
}
}
if program.DisqualifiedPools != nil {
for _, poolIdent := range program.DisqualifiedPools {
if poolIdent == pool.PoolIdent {
return false, "Pool is explicitly disqualified"
qualified = false
reason += "Pool is explicitly disqualified; "
}
}
return true, ""
}
return true, ""
if program.DisqualifiedAssets != nil {
for _, assetID := range program.DisqualifiedAssets {
if assetID == pool.AssetA || assetID == pool.AssetB {
qualified = false
reason += "One of the assets in this pool is explicitly disqualified; "
}
}
}
if program.DisqualifiedPairs != nil {
for _, pair := range program.DisqualifiedPairs {
if pair.AssetA == pool.AssetA && pair.AssetB == pool.AssetB {
qualified = false
reason += "Pair is explicitly disqualified; "
}
}
}
return qualified, reason
}

// Check which pools are disqualified and why, and return just the qualified delegation amounts
Expand Down Expand Up @@ -243,7 +294,7 @@ func SumDelegationWindow(program types.Program, qualifyingDelegationsPerPool map
}

// Select the top pools according to the program criteria
func SelectPoolsForEmission(
func SelectEligiblePoolsForEmission(
ctx context.Context,
program types.Program,
delegationsByPool map[string]uint64,
Expand Down Expand Up @@ -331,59 +382,107 @@ func SelectPoolsForEmission(
}

// Split the daily emissions of the program among a set of pools that have been chosen for emissions
func DistributeEmissionsToPools(program types.Program, poolsReceivingEmissionsByIdent map[string]uint64) map[string]uint64 {
func DistributeEmissionsToPools(program types.Program, poolsEligibleForEmissionsByIdent map[string]uint64) map[string]uint64 {
// We'll need to loop over pools round-robin by largest value; ordering of maps is non-deterministic
type Pairs struct {
PoolIdent string
Amount uint64
}
poolWeights := []Pairs{}
totalWeight := uint64(0)
for poolIdent, weight := range poolsReceivingEmissionsByIdent {
emissionsByPool := map[string]uint64{}

allocatedEmissions := uint64(0)
// First, add in any fixed-emissions pools
for poolIdent, amount := range program.FixedEmissions {
if program.DailyEmission < amount {
panic("program is misconfigured, fixed emissions exceed daily emissions")
}
emissionsByPool[poolIdent] = amount
allocatedEmissions += amount
}

// Now, sum up the various pool weights that are remaining
for poolIdent, weight := range poolsEligibleForEmissionsByIdent {
// Skip over pools that are receiving a fixed emission
if _, ok := program.FixedEmissions[poolIdent]; ok {
continue
}

totalWeight += weight
poolWeights = append(poolWeights, Pairs{PoolIdent: poolIdent, Amount: weight})
}

// No pool has received weight
// We then divide the daily emissions among these pools in proportion to their weight, rounding down
emissionsByPool := map[string]uint64{}
if totalWeight == 0 {
return emissionsByPool
}

allocatedAmount := uint64(0)
for poolIdent, weight := range poolsReceivingEmissionsByIdent {
frac := big.NewInt(0).SetUint64(program.DailyEmission)
// Then allocate the remainder according to the rules of the program
dynamicEmissions := program.DailyEmission - allocatedEmissions
for poolIdent, weight := range poolsEligibleForEmissionsByIdent {
// Skip over pools that receive a fixed emission
if _, ok := program.FixedEmissions[poolIdent]; ok {
continue
}
frac := big.NewInt(0).SetUint64(dynamicEmissions)
frac = frac.Mul(frac, big.NewInt(0).SetUint64(weight))
frac = frac.Div(frac, big.NewInt(0).SetUint64(totalWeight))
allocation := frac.Uint64()
allocatedAmount += allocation
if allocatedEmissions+allocation > program.DailyEmission {
panic("something went wrong: would allocate more than daily emissions")
}
emissionsByPool[poolIdent] += allocation
allocatedEmissions += allocation
}

// and distributing [diminutive tokens] among them until the daily emission is accounted for.
remainder := int(program.DailyEmission - allocatedAmount)
if remainder < 0 {
panic("emitted more to pools than the daily emissions, somehow")
} else if remainder > 0 {
if allocatedEmissions > program.DailyEmission {
panic("something went wrong with allocating emissions to pool; exceeded the daily emissions")
} else if allocatedEmissions != program.DailyEmission {
sort.Slice(poolWeights, func(i, j int) bool {
if poolWeights[i].Amount == poolWeights[j].Amount {
return poolWeights[i].PoolIdent < poolWeights[j].PoolIdent
}
return poolWeights[i].Amount > poolWeights[j].Amount
})
remainder := int(program.DailyEmission - allocatedEmissions)
for i := 0; i < remainder; i++ {
pool := poolWeights[i%len(poolWeights)]
emissionsByPool[pool.PoolIdent] += 1
allocatedAmount += 1
allocatedEmissions += 1
}
if allocatedAmount != program.DailyEmission {
if allocatedEmissions != program.DailyEmission {
// There's a bug in the round-robin distribution code, panic so we fix the bug
panic("round-robin distribution wasn't succesful")
}
}

// Now check to make sure none of these pools (other than the fixed emissions) exceed the cap on daily emissions per pool

return emissionsByPool
}

// Truncate the emissions to the maximum emission cap
func TruncateEmissions(program types.Program, emissionsByPool map[string]uint64) map[string]uint64 {
if program.EmissionCap == 0 {
return emissionsByPool
}

truncatedEmissions := map[string]uint64{}
for pool, amount := range emissionsByPool {
_, ok := program.FixedEmissions[pool]
if !ok && amount > program.EmissionCap {
truncatedEmissions[pool] = program.EmissionCap
} else {
truncatedEmissions[pool] = amount
}
}

return truncatedEmissions
}

// Compute the total LP token days that each owner has; We multiply the LP tokens by seconds they were locked, and then divide by 86400.
// This effectively divides the LP tokens by the fraction of the day they are locked, to prevent someone locking in the last minute of the day to receive rewards
func TotalLPDaysByOwnerAndAsset(positions []types.Position, poolLookup PoolLookup, minSlot uint64, maxSlot uint64) (map[string]map[chainsync.AssetID]uint64, map[chainsync.AssetID]uint64) {
Expand Down Expand Up @@ -605,16 +704,17 @@ type CalculationOutputs struct {
NumDelegationDays int
DelegationOverWindowByPool map[string]uint64

PoolsReceivingEmissions map[string]uint64
PoolsEligibleForEmissions map[string]uint64

LockedLPByPool map[string]uint64
TotalLPByPool map[string]uint64

EstimatedLockedLovelace uint64
EstimatedLockedLovelaceByPool map[string]uint64

TotalEmissions uint64
EmissionsByPool map[string]uint64
TotalEmissions uint64
UntruncatedEmissionsByPool map[string]uint64
EmissionsByPool map[string]uint64

EmissionsByOwner map[string]uint64

Expand All @@ -634,7 +734,7 @@ func CalculateEarnings(ctx context.Context, date types.Date, startSlot uint64, e
}

// To calculate the daily emissions, ... first take inventory of SUNDAE held at the Locking Contract
// and factory in the users delegation
// and factor in the users delegation
delegationByPool, totalDelegation, err := CalculateTotalDelegations(ctx, program, positions, poolLookup)
if err != nil {
return CalculationOutputs{}, fmt.Errorf("failed to calculate total delegations: %w", err)
Expand Down Expand Up @@ -676,13 +776,15 @@ func CalculateEarnings(ctx context.Context, date types.Date, startSlot uint64, e
}

// The top pools ... will be eligible for yield farming rewards that day.
poolsReceivingEmissions, err := SelectPoolsForEmission(ctx, program, delegationOverWindowByPool, poolLookup)
poolsEligibleForEmissions, err := SelectEligiblePoolsForEmission(ctx, program, delegationOverWindowByPool, poolLookup)
if err != nil {
return CalculationOutputs{}, fmt.Errorf("failed to select pools for emission: %w", err)
}

// We then divide the daily emissions among these pools ...
emissionsByAsset, err := RegroupByAsset(ctx, DistributeEmissionsToPools(program, poolsReceivingEmissions), poolLookup)
rawEmissionsByPool := DistributeEmissionsToPools(program, poolsEligibleForEmissions)
emissionsByPool := TruncateEmissions(program, rawEmissionsByPool)
emissionsByAsset, err := RegroupByAsset(ctx, emissionsByPool, poolLookup)
if err != nil {
return CalculationOutputs{}, fmt.Errorf("failed to regroup emissions by asset: %w", err)
}
Expand All @@ -698,10 +800,6 @@ func CalculateEarnings(ctx context.Context, date types.Date, startSlot uint64, e
}

// Find the pool that we should use for price reference, so we can estimate the ADA value of what was emitted
emissionsByPool, err := RegroupByPool(ctx, emissionsByAsset, poolLookup)
if err != nil {
return CalculationOutputs{}, fmt.Errorf("failed to regroup emissions by pool: %w", err)
}
var emittedLovelaceValue uint64
emittedLovelaceValueByPool := map[string]uint64{}
if program.ReferencePool != "" {
Expand Down Expand Up @@ -734,16 +832,17 @@ func CalculateEarnings(ctx context.Context, date types.Date, startSlot uint64, e
NumDelegationDays: program.ConsecutiveDelegationWindow,
DelegationOverWindowByPool: delegationOverWindowByPool,

PoolsReceivingEmissions: poolsReceivingEmissions,
PoolsEligibleForEmissions: poolsEligibleForEmissions,

LockedLPByPool: lockedLPByPool,
TotalLPByPool: totalLPByPool,

EstimatedLockedLovelace: totalEstimatedValue,
EstimatedLockedLovelaceByPool: estimatedValueByPool,

TotalEmissions: program.DailyEmission,
EmissionsByPool: emissionsByPool,
TotalEmissions: program.DailyEmission,
UntruncatedEmissionsByPool: rawEmissionsByPool,
EmissionsByPool: emissionsByPool,

EmissionsByOwner: perOwnerTotal,

Expand Down