-
Notifications
You must be signed in to change notification settings - Fork 2
FLO-12: Fee Calculation Diverges From Rate Allocation Formula #288
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
Open
mts1715
wants to merge
11
commits into
main
Choose a base branch
from
taras/221-FLO-12-fee-calculation-diverges-from-rate-allocation-formula
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
6000a1a
changes at fees calculation formula and per seconds rates
mts1715 e5c3929
unify insurance and stability into single collectProtocolFees accumul…
mts1715 0045ff9
Merge remote-tracking branch 'origin/main' into taras/221-FLO-12-fee-…
mts1715 c5cf4db
fix failures test_collectStability_multipleTokens and test_collectSta…
mts1715 e6c6b42
documentation fix
mts1715 a379178
minor fixes
mts1715 edd56a9
fix tests
mts1715 41828a5
Apply suggestion from @zhangchiqing
mts1715 aafbae2
Apply suggestions from code review
mts1715 0ff2f42
review fix
mts1715 66a478b
Merge branch 'main' into taras/221-FLO-12-fee-calculation-diverges-fr…
mts1715 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -947,17 +947,30 @@ access(all) contract FlowALPModels { | |
| /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) | ||
| access(all) view fun getInsuranceRate(): UFix64 | ||
|
|
||
| /// Timestamp of the last insurance collection for this token. | ||
| access(all) view fun getLastInsuranceCollectionTime(): UFix64 | ||
|
|
||
| /// Swapper used to convert this token to MOET for insurance collection. | ||
| access(all) view fun getInsuranceSwapper(): {DeFiActions.Swapper}? | ||
|
|
||
| /// The stability fee rate to calculate stability (default 0.05, 5%). | ||
| access(all) view fun getStabilityFeeRate(): UFix64 | ||
|
|
||
| /// Timestamp of the last stability collection for this token. | ||
| access(all) view fun getLastStabilityFeeCollectionTime(): UFix64 | ||
| /// Timestamp of the last protocol fee collection for this token. | ||
| access(all) view fun getLastProtocolFeeCollectionTime(): UFix64 | ||
|
|
||
| /// Returns the accumulated insurance fee income as UFix64, ready for collection. | ||
| access(all) view fun getCollectInsuranceAmount(): UFix64 | ||
|
|
||
| /// Resets the accumulated insurance fee income to zero after successful collection. | ||
| access(EImplementation) fun resetCollectInsuranceAmount() | ||
|
|
||
| /// Returns the accumulated stability fee income as UFix64, ready for collection. | ||
| access(all) view fun getCollectStabilityAmount(): UFix64 | ||
|
|
||
| /// Resets the accumulated stability fee income to zero after successful collection. | ||
| access(EImplementation) fun resetCollectStabilityAmount() | ||
|
|
||
| /// Accumulates protocol fees (insurance + stability) for elapsed time since last collection. | ||
| /// Called before any balance or rate change to capture fees at the current rates and balances. | ||
| access(EImplementation) fun collectProtocolFees() | ||
|
|
||
| /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) | ||
| access(all) view fun getDepositLimitFraction(): UFix64 | ||
|
|
@@ -993,9 +1006,6 @@ access(all) contract FlowALPModels { | |
| /// Sets the insurance rate. See getInsuranceRate for additional details. | ||
| access(EImplementation) fun setInsuranceRate(_ rate: UFix64) | ||
|
|
||
| /// Sets the last insurance collection timestamp. See getLastInsuranceCollectionTime for additional details. | ||
| access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) | ||
|
|
||
| /// Sets the insurance swapper. See getInsuranceSwapper for additional details. | ||
| /// If non-nil, the swapper must accept this token type as input and output MOET. | ||
| access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) | ||
|
|
@@ -1018,9 +1028,6 @@ access(all) contract FlowALPModels { | |
| /// Sets the stability fee rate. See getStabilityFeeRate for additional details. | ||
| access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) | ||
|
|
||
| /// Sets the last stability fee collection timestamp. See getLastStabilityFeeCollectionTime for additional details. | ||
| access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) | ||
|
|
||
| /// Sets the deposit capacity. See getDepositCapacity for additional details. | ||
| access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) | ||
|
|
||
|
|
@@ -1112,14 +1119,16 @@ access(all) contract FlowALPModels { | |
| access(self) var interestCurve: {FlowALPInterestRates.InterestCurve} | ||
| /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) | ||
| access(self) var insuranceRate: UFix64 | ||
| /// Timestamp of the last insurance collection for this token. | ||
| access(self) var lastInsuranceCollectionTime: UFix64 | ||
| /// Swapper used to convert this token to MOET for insurance collection. | ||
| access(self) var insuranceSwapper: {DeFiActions.Swapper}? | ||
| /// The stability fee rate to calculate stability (default 0.05, 5%). | ||
| access(self) var stabilityFeeRate: UFix64 | ||
| /// Timestamp of the last stability collection for this token. | ||
| access(self) var lastStabilityFeeCollectionTime: UFix64 | ||
| /// Timestamp of the last protocol fee accumulation (shared by insurance and stability). | ||
| access(self) var lastProtocolFeeCollectionTime: UFix64 | ||
| /// Accrued stability fee income not yet withdrawn from reserves. | ||
| access(self) var accumulatedStabilityFeeIncome: UFix128 | ||
| /// Accrued insurance fee income not yet withdrawn from reserves. | ||
| access(self) var accumulatedInsuranceFeeIncome: UFix128 | ||
| /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) | ||
| access(self) var depositLimitFraction: UFix64 | ||
| /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate, | ||
|
|
@@ -1158,10 +1167,11 @@ access(all) contract FlowALPModels { | |
| self.currentDebitRate = 1.0 | ||
| self.interestCurve = interestCurve | ||
| self.insuranceRate = 0.0 | ||
| self.lastInsuranceCollectionTime = getCurrentBlock().timestamp | ||
| self.insuranceSwapper = nil | ||
| self.stabilityFeeRate = 0.05 | ||
| self.lastStabilityFeeCollectionTime = getCurrentBlock().timestamp | ||
| self.lastProtocolFeeCollectionTime = getCurrentBlock().timestamp | ||
| self.accumulatedStabilityFeeIncome = 0.0 | ||
| self.accumulatedInsuranceFeeIncome = 0.0 | ||
| self.depositLimitFraction = 0.05 | ||
| self.depositRate = depositRate | ||
| self.depositCapacity = depositCapacityCap | ||
|
|
@@ -1223,11 +1233,6 @@ access(all) contract FlowALPModels { | |
| return self.insuranceRate | ||
| } | ||
|
|
||
| /// Returns the timestamp of the last insurance collection for this token. | ||
| access(all) view fun getLastInsuranceCollectionTime(): UFix64 { | ||
| return self.lastInsuranceCollectionTime | ||
| } | ||
|
|
||
| /// Returns the swapper used to convert this token to MOET for insurance collection. | ||
| access(all) view fun getInsuranceSwapper(): {DeFiActions.Swapper}? { | ||
| return self.insuranceSwapper | ||
|
|
@@ -1238,9 +1243,29 @@ access(all) contract FlowALPModels { | |
| return self.stabilityFeeRate | ||
| } | ||
|
|
||
| /// Returns the timestamp of the last stability fee collection for this token. | ||
| access(all) view fun getLastStabilityFeeCollectionTime(): UFix64 { | ||
| return self.lastStabilityFeeCollectionTime | ||
| /// Returns the timestamp of the last protocol fee collection for this token. | ||
| access(all) view fun getLastProtocolFeeCollectionTime(): UFix64 { | ||
| return self.lastProtocolFeeCollectionTime | ||
| } | ||
|
|
||
| /// Returns the accumulated insurance fee income as UFix64, ready for collection. | ||
| access(all) view fun getCollectInsuranceAmount(): UFix64 { | ||
| return FlowALPMath.toUFix64RoundDown(self.accumulatedInsuranceFeeIncome) | ||
| } | ||
|
|
||
| /// Resets the accumulated insurance fee income to zero after successful collection. | ||
| access(EImplementation) fun resetCollectInsuranceAmount() { | ||
| self.accumulatedInsuranceFeeIncome = 0.0 | ||
| } | ||
|
|
||
| /// Returns the accumulated stability fee income as UFix64, ready for collection. | ||
| access(all) view fun getCollectStabilityAmount(): UFix64 { | ||
| return FlowALPMath.toUFix64RoundDown(self.accumulatedStabilityFeeIncome) | ||
| } | ||
|
|
||
| /// Resets the accumulated stability fee income to zero after successful collection. | ||
| access(EImplementation) fun resetCollectStabilityAmount() { | ||
| self.accumulatedStabilityFeeIncome = 0.0 | ||
| } | ||
|
|
||
| /// Returns the per-position limit fraction of capacity (default 0.05 i.e., 5%). | ||
|
|
@@ -1282,12 +1307,9 @@ access(all) contract FlowALPModels { | |
|
|
||
| /// Sets the insurance rate. See TokenState.setInsuranceRate. | ||
| access(EImplementation) fun setInsuranceRate(_ rate: UFix64) { | ||
| self.collectProtocolFees() | ||
| self.insuranceRate = rate | ||
| } | ||
|
|
||
| /// Sets the last insurance collection timestamp. See TokenState.setLastInsuranceCollectionTime. | ||
| access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) { | ||
| self.lastInsuranceCollectionTime = lastInsuranceCollectionTime | ||
| self.updateForUtilizationChange() | ||
| } | ||
|
|
||
| /// Sets the insurance swapper. See TokenState.setInsuranceSwapper. | ||
|
|
@@ -1329,12 +1351,9 @@ access(all) contract FlowALPModels { | |
|
|
||
| /// Sets the stability fee rate. See TokenState.setStabilityFeeRate. | ||
| access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) { | ||
| self.collectProtocolFees() | ||
| self.stabilityFeeRate = rate | ||
| } | ||
|
|
||
| /// Sets the last stability fee collection timestamp. See TokenState.setLastStabilityFeeCollectionTime. | ||
| access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) { | ||
| self.lastStabilityFeeCollectionTime = lastStabilityFeeCollectionTime | ||
| self.updateForUtilizationChange() | ||
| } | ||
|
|
||
| /// Sets the deposit capacity. See TokenState.setDepositCapacity. | ||
|
|
@@ -1344,6 +1363,7 @@ access(all) contract FlowALPModels { | |
|
|
||
| /// Sets the interest curve. Recalculates interest rates immediately. See TokenState.setInterestCurve. | ||
| access(EImplementation) fun setInterestCurve(_ curve: {FlowALPInterestRates.InterestCurve}) { | ||
| self.collectProtocolFees() | ||
| self.interestCurve = curve | ||
| // Update rates immediately to reflect the new curve | ||
| self.updateInterestRates() | ||
|
|
@@ -1401,33 +1421,28 @@ access(all) contract FlowALPModels { | |
| let insuranceRate = UFix128(self.insuranceRate) | ||
| let stabilityFeeRate = UFix128(self.stabilityFeeRate) | ||
|
|
||
| var creditRate: UFix128 = 0.0 | ||
| // Total protocol cut as a percentage of debit interest income | ||
| let protocolFeeRate = insuranceRate + stabilityFeeRate | ||
| self.currentDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) | ||
| let debitRatePerSecond = self.currentDebitRate - 1.0 | ||
|
|
||
| // Two calculation paths based on curve type: | ||
| // 1. FixedCurve: simple spread model (creditRate = debitRate * (1 - protocolFeeRate)) | ||
| // 1. FixedCurve: simple spread model | ||
| // Used for stable assets like MOET where rates are governance-controlled | ||
| // 2. KinkCurve (and others): reserve factor model | ||
| // Insurance and stability are percentages of interest income, not a fixed spread | ||
| if self.interestCurve.getType() == Type<FlowALPInterestRates.FixedCurve>() { | ||
| // FixedRate path: creditRate = debitRate * (1 - protocolFeeRate)) | ||
| // This provides a fixed, predictable spread between borrower and lender rates | ||
| creditRate = debitRate * (1.0 - protocolFeeRate) | ||
| // FixedRate path: creditRatePerSec = debitRatePerSec * (1 - protocolFeeRate) | ||
| self.currentCreditRate = 1.0 + debitRatePerSecond * (1.0 - protocolFeeRate) | ||
| } else { | ||
| // KinkCurve path (and any other curves): reserve factor model | ||
| // protocolFeeAmount = debitIncome * protocolFeeRate (percentage of income) | ||
| // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance | ||
| let debitIncome = self.totalDebitBalance * debitRate | ||
| let protocolFeeAmount = debitIncome * protocolFeeRate | ||
|
|
||
| // creditRatePerSec = debitRatePerSec * (1 - protocolFeeRate) * totalDebit / totalCredit | ||
| if self.totalCreditBalance > 0.0 { | ||
| creditRate = (debitIncome - protocolFeeAmount) / self.totalCreditBalance | ||
| self.currentCreditRate = 1.0 + debitRatePerSecond * (1.0 - protocolFeeRate) * self.totalDebitBalance / self.totalCreditBalance | ||
| } else { | ||
| self.currentCreditRate = 1.0 | ||
| } | ||
| } | ||
|
|
||
| self.currentCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: creditRate) | ||
| self.currentDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) | ||
| } | ||
|
|
||
| /// Updates the credit and debit interest indices for elapsed time since last update. | ||
|
|
@@ -1486,12 +1501,14 @@ access(all) contract FlowALPModels { | |
|
|
||
| /// Increases total credit balance by the given amount and recalculates interest rates. | ||
| access(EImplementation) fun increaseCreditBalance(by amount: UFix128) { | ||
| self.collectProtocolFees() | ||
| self.totalCreditBalance = self.totalCreditBalance + amount | ||
| self.updateForUtilizationChange() | ||
| } | ||
|
|
||
| /// Decreases total credit balance by the given amount (floored at 0) and recalculates interest rates. | ||
| access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) { | ||
| self.collectProtocolFees() | ||
| if amount >= self.totalCreditBalance { | ||
| self.totalCreditBalance = 0.0 | ||
| } else { | ||
|
|
@@ -1502,19 +1519,50 @@ access(all) contract FlowALPModels { | |
|
|
||
| /// Increases total debit balance by the given amount and recalculates interest rates. | ||
| access(EImplementation) fun increaseDebitBalance(by amount: UFix128) { | ||
| self.collectProtocolFees() | ||
| self.totalDebitBalance = self.totalDebitBalance + amount | ||
| self.updateForUtilizationChange() | ||
| } | ||
|
|
||
| /// Decreases total debit balance by the given amount (floored at 0) and recalculates interest rates. | ||
| access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) { | ||
| self.collectProtocolFees() | ||
| if amount >= self.totalDebitBalance { | ||
| self.totalDebitBalance = 0.0 | ||
| } else { | ||
| self.totalDebitBalance = self.totalDebitBalance - amount | ||
| } | ||
| self.updateForUtilizationChange() | ||
| } | ||
|
|
||
| /// Accumulates insurance and stability fee income for the elapsed time since the last call. | ||
| /// Updates lastProtocolFeeCollectionTime to the current block timestamp. | ||
| /// Must be called before any balance or rate change to settle fees at current rates. | ||
|
mts1715 marked this conversation as resolved.
|
||
| access(EImplementation) fun collectProtocolFees() { | ||
|
mts1715 marked this conversation as resolved.
Outdated
|
||
| let currentTime = getCurrentBlock().timestamp | ||
|
|
||
| let totalProtocolFeeRate = self.insuranceRate + self.stabilityFeeRate | ||
| if totalProtocolFeeRate == 0.0 { | ||
| self.lastProtocolFeeCollectionTime = currentTime | ||
| return | ||
| } | ||
|
|
||
| let timeElapsed = currentTime - self.lastProtocolFeeCollectionTime | ||
| if timeElapsed <= 0.0 { | ||
| return | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| let debitIncome = self.totalDebitBalance * (FlowALPMath.powUFix128(self.currentDebitRate, timeElapsed) - 1.0) | ||
| let creditIncome = self.totalCreditBalance * (FlowALPMath.powUFix128(self.currentCreditRate, timeElapsed) - 1.0) | ||
| let protocolFeeIncome: UFix128 = debitIncome > creditIncome ? debitIncome - creditIncome : 0.0 | ||
|
|
||
| let insuranceFeeAmount = protocolFeeIncome * UFix128(self.insuranceRate) / UFix128(totalProtocolFeeRate) | ||
| let stabilityFeeAmount = protocolFeeIncome - insuranceFeeAmount | ||
|
|
||
| self.accumulatedInsuranceFeeIncome = self.accumulatedInsuranceFeeIncome + insuranceFeeAmount | ||
| self.accumulatedStabilityFeeIncome = self.accumulatedStabilityFeeIncome + stabilityFeeAmount | ||
| self.lastProtocolFeeCollectionTime = currentTime | ||
| } | ||
| } | ||
|
|
||
| /* --- POOL STATE --- */ | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest adding
setLastProtocolFeeCollectionTimeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setLastProtocolFeeCollectionTime won't be used as part of interface
changes of
lastProtocolFeeCollectionTimeonly insidecollectProtocolFeesmethod.so there is no need to add setter to interface