-
Notifications
You must be signed in to change notification settings - Fork 2
single debt and collateral per position #184
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
Draft
nialexsan
wants to merge
32
commits into
main
Choose a base branch
from
nialexsan/multi-debt
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.
Draft
Changes from 14 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
f8e0c64
multi debt
nialexsan 921efff
Merge remote-tracking branch 'origin/main' into nialexsan/multi-debt
nialexsan cfbcd1a
Apply suggestion from @nialexsan
nialexsan 7b1a144
fix refs
nialexsan c492f20
fix balance
nialexsan a7f0828
revert MOET collateral restriction
nialexsan 4c9e452
fix test
nialexsan a916839
fix testing transactions
nialexsan aa091eb
Merge branch 'main' into nialexsan/multi-debt
nialexsan 2b1ed97
add tests
nialexsan 7187d1e
fix test
nialexsan 1a6b4ec
method names for multi types
nialexsan 093d823
reserve handler
nialexsan e3dcb84
Apply suggestion from @jordanschalm
nialexsan 8153647
Fix phantom debt/collateral types after exact zero balances
liobrasil 368dbaa
trim tests
nialexsan a414472
fix test
nialexsan 385387f
Merge branch 'nialexsan/multi-debt' into lionel/fix-phantom-balance-t…
nialexsan 3641469
Merge pull request #199 from onflow/lionel/fix-phantom-balance-types
nialexsan ebffab0
move around
nialexsan 5d9b84c
address comments
nialexsan 6e95bab
Merge branch 'main' into nialexsan/multi-debt
nialexsan af6b9d7
fix test
nialexsan 5058488
skip multi collateral tests
nialexsan 9fbd813
skip test
nialexsan b65f0ff
fix zero balance check
nialexsan c0713a7
Merge branch 'main' into nialexsan/multi-debt
liobrasil 416a9d2
clean-up
nialexsan 23603b4
simplify handler
nialexsan ee601c1
tweak handler
nialexsan 08fd6fd
switch tests to use dummy token
nialexsan 8d48045
revert unnecessary changes
nialexsan 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 |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import "FungibleToken" | |
| import "DeFiActions" | ||
| import "DeFiActionsUtils" | ||
| import "MOET" | ||
| import "Burner" | ||
| import "FlowALPMath" | ||
| import "FlowALPInterestRates" | ||
| import "FlowALPEvents" | ||
|
|
@@ -244,6 +245,147 @@ access(all) contract FlowALPModels { | |
| } | ||
| } | ||
|
|
||
| /// TokenReserveHandler | ||
| /// | ||
| /// Interface for handling token reserve operations. Different token types may require | ||
| /// different handling for deposits and withdrawals. For example, MOET tokens are minted | ||
| /// on debt withdrawals and burned on repayments, while standard tokens use reserve vaults. | ||
| access(all) struct interface TokenReserveHandler { | ||
| /// Returns the token type this handler manages | ||
| access(all) view fun getTokenType(): Type | ||
|
|
||
| /// Deposits collateral (always to reserves, including MOET) | ||
| access(all) fun depositCollateral( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| from: @{FungibleToken.Vault} | ||
| ): UFix64 | ||
|
|
||
| /// Deposits repayment (to reserves for most tokens, BURNS for MOET) | ||
| access(all) fun depositRepayment( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| from: @{FungibleToken.Vault} | ||
| ): UFix64 | ||
|
|
||
| /// Withdraws debt (from reserves for most tokens, MINTS for MOET) | ||
| access(all) fun withdrawDebt( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| amount: UFix64, | ||
| minterRef: &MOET.Minter? | ||
| ): @{FungibleToken.Vault} | ||
|
|
||
| /// Withdraws collateral (always from reserves, including MOET) | ||
| access(all) fun withdrawCollateral( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| amount: UFix64 | ||
| ): @{FungibleToken.Vault} | ||
| } | ||
|
|
||
| /// StandardTokenReserveHandler | ||
| /// | ||
| /// Standard implementation of TokenReserveHandler that interacts with reserve vaults | ||
| /// for all four operations (deposit/withdraw collateral and debt). | ||
| access(all) struct StandardTokenReserveHandler: TokenReserveHandler { | ||
| access(self) let tokenType: Type | ||
|
|
||
| init(tokenType: Type) { | ||
| self.tokenType = tokenType | ||
| } | ||
|
|
||
| access(all) view fun getTokenType(): Type { | ||
| return self.tokenType | ||
| } | ||
|
|
||
| access(all) fun depositCollateral( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| from: @{FungibleToken.Vault} | ||
| ): UFix64 { | ||
| let amount = from.balance | ||
| let reserveVault = state.borrowOrCreateReserve(self.tokenType) | ||
| reserveVault.deposit(from: <-from) | ||
| return amount | ||
| } | ||
|
|
||
| access(all) fun depositRepayment( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| from: @{FungibleToken.Vault} | ||
| ): UFix64 { | ||
| let amount = from.balance | ||
| let reserveVault = state.borrowOrCreateReserve(self.tokenType) | ||
| reserveVault.deposit(from: <-from) | ||
| return amount | ||
| } | ||
|
|
||
| access(all) fun withdrawDebt( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| amount: UFix64, | ||
| minterRef: &MOET.Minter? | ||
| ): @{FungibleToken.Vault} { | ||
| let reserveVault = state.borrowOrCreateReserve(self.tokenType) | ||
| return <- reserveVault.withdraw(amount: amount) | ||
| } | ||
|
|
||
| access(all) fun withdrawCollateral( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| amount: UFix64 | ||
| ): @{FungibleToken.Vault} { | ||
| let reserveVault = state.borrowOrCreateReserve(self.tokenType) | ||
| return <- reserveVault.withdraw(amount: amount) | ||
| } | ||
| } | ||
|
|
||
| /// MoetTokenReserveHandler | ||
| /// | ||
| /// Special implementation of TokenReserveHandler for MOET tokens. | ||
| /// - Collateral deposits/withdrawals use reserve vaults (standard behavior) | ||
| /// - Debt repayments BURN the MOET tokens (reducing supply) | ||
| /// - Debt withdrawals MINT new MOET tokens (increasing supply) | ||
| access(all) struct MoetTokenReserveHandler: TokenReserveHandler { | ||
|
|
||
| init() {} | ||
|
|
||
| access(all) view fun getTokenType(): Type { | ||
| return Type<@MOET.Vault>() | ||
| } | ||
|
|
||
| access(all) fun depositCollateral( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| from: @{FungibleToken.Vault} | ||
| ): UFix64 { | ||
| let amount = from.balance | ||
| let reserveVault = state.borrowOrCreateReserve(Type<@MOET.Vault>()) | ||
| reserveVault.deposit(from: <-from) | ||
| return amount | ||
| } | ||
|
|
||
| access(all) fun depositRepayment( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| from: @{FungibleToken.Vault} | ||
| ): UFix64 { | ||
| // Repayments burn MOET tokens to reduce supply | ||
| let amount = from.balance | ||
| Burner.burn(<-from) | ||
| return amount | ||
| } | ||
|
|
||
| access(all) fun withdrawDebt( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| amount: UFix64, | ||
| minterRef: &MOET.Minter? | ||
| ): @{FungibleToken.Vault} { | ||
| // Debt withdrawals mint new MOET tokens | ||
| assert(minterRef != nil, message: "MOET Minter reference required for debt withdrawal") | ||
| return <- minterRef!.mintTokens(amount: amount) | ||
| } | ||
|
|
||
| access(all) fun withdrawCollateral( | ||
| state: auth(EImplementation) &{PoolState}, | ||
| amount: UFix64 | ||
| ): @{FungibleToken.Vault} { | ||
| let reserveVault = state.borrowOrCreateReserve(Type<@MOET.Vault>()) | ||
| return <- reserveVault.withdraw(amount: amount) | ||
| } | ||
| } | ||
|
|
||
| /// Risk parameters for a token used in effective collateral/debt computations. | ||
| /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. | ||
| /// The size of this discount indicates a subjective assessment of risk for the token. | ||
|
|
@@ -1070,6 +1212,10 @@ access(all) contract FlowALPModels { | |
| access(EImplementation) fun increaseDebitBalance(by amount: UFix128) | ||
| /// Decreases total debit balance (floored at 0) and recalculates interest rates. | ||
| access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) | ||
|
|
||
| /// Returns the reserve operations handler for this token type. | ||
| /// Different tokens may have different reserve behaviors (e.g., MOET burns on repayment, others use reserves). | ||
| access(all) view fun getReserveOperations(): {TokenReserveHandler} | ||
| } | ||
|
|
||
| /// TokenStateImplv1 is the concrete implementation of TokenState. | ||
|
|
@@ -1141,6 +1287,8 @@ access(all) contract FlowALPModels { | |
| /// - A credit balance greater than or equal to M | ||
| /// - A debit balance greater than or equal to M | ||
| access(self) var minimumTokenBalancePerPosition: UFix64 | ||
| /// The reserve operations handler for this token type | ||
| access(self) let reserveHandler: {TokenReserveHandler} | ||
|
|
||
| init( | ||
| tokenType: Type, | ||
|
|
@@ -1169,6 +1317,12 @@ access(all) contract FlowALPModels { | |
| self.depositUsage = {} | ||
| self.lastDepositCapacityUpdate = getCurrentBlock().timestamp | ||
| self.minimumTokenBalancePerPosition = 1.0 | ||
| // Initialize reserve handler based on token type | ||
| if tokenType == Type<@MOET.Vault>() { | ||
| self.reserveHandler = MoetTokenReserveHandler() | ||
| } else { | ||
| self.reserveHandler = StandardTokenReserveHandler(tokenType: tokenType) | ||
| } | ||
| } | ||
|
|
||
| // --- Getters --- | ||
|
|
@@ -1178,6 +1332,11 @@ access(all) contract FlowALPModels { | |
| return self.tokenType | ||
| } | ||
|
|
||
| /// Returns the reserve operations handler for this token type. | ||
| access(all) view fun getReserveOperations(): {TokenReserveHandler} { | ||
| return self.reserveHandler | ||
| } | ||
|
|
||
| /// Returns the timestamp at which the TokenState was last updated. | ||
| access(all) view fun getLastUpdate(): UFix64 { | ||
| return self.lastUpdate | ||
|
|
@@ -1903,6 +2062,22 @@ access(all) contract FlowALPModels { | |
| /// Sets the top-up source. See borrowTopUpSource for additional details. | ||
| /// If nil, the Pool will not pull underflown value, and liquidation may occur. | ||
| access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) | ||
|
|
||
| /// Returns the current collateral types for this position based on existing Credit balances | ||
| /// Returns empty array if no Credit balance exists yet (allows any collateral type for first deposit) | ||
| access(all) fun getCollateralTypes(): [Type] | ||
|
|
||
| /// Returns the current debt types for this position based on existing Debit balances | ||
| /// Returns empty array if no Debit balance exists yet (allows any debt type for first borrow) | ||
| access(all) fun getDebtTypes(): [Type] | ||
|
|
||
| /// Validates that the given token type can be used as collateral for this position | ||
| /// Panics if position already has a different collateral type | ||
| access(EImplementation) fun validateCollateralType(_ type: Type) | ||
|
|
||
| /// Validates that the given token type can be used as debt for this position | ||
| /// Panics if position already has a different debt type | ||
| access(EImplementation) fun validateDebtType(_ type: Type) | ||
| } | ||
|
|
||
| /// InternalPositionImplv1 is the concrete implementation of InternalPosition. | ||
|
|
@@ -2071,6 +2246,78 @@ access(all) contract FlowALPModels { | |
| access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) { | ||
| self.topUpSource = source | ||
| } | ||
|
|
||
| /// Returns the current collateral types for this position based on existing Credit balances | ||
| /// Returns empty array if no Credit balance exists yet (allows any collateral type for first deposit) | ||
| access(all) fun getCollateralTypes(): [Type] { | ||
| let types: [Type] = [] | ||
| for type in self.balances.keys { | ||
| if self.balances[type]!.direction == BalanceDirection.Credit { | ||
|
Contributor
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. Follow-up fix: this type discovery now needs to ignore zero balances. Otherwise exact repay/full withdraw can leave a phantom type that still trips |
||
| types.append(type) | ||
| } | ||
| } | ||
| return types | ||
| } | ||
|
|
||
| /// Returns the current debt types for this position based on existing Debit balances | ||
| /// Returns empty array if no Debit balance exists yet (allows any debt type for first borrow) | ||
| access(all) fun getDebtTypes(): [Type] { | ||
| let types: [Type] = [] | ||
| for type in self.balances.keys { | ||
| if self.balances[type]!.direction == BalanceDirection.Debit { | ||
| types.append(type) | ||
| } | ||
| } | ||
| return types | ||
| } | ||
|
|
||
| /// Validates that the given token type can be used as collateral for this position | ||
| /// Panics if position already has a different collateral type | ||
| access(EImplementation) fun validateCollateralType(_ type: Type) { | ||
| let existingTypes = self.getCollateralTypes() | ||
|
|
||
| // Constraint: For now, only one collateral type is allowed per position | ||
| // This assertion ensures the invariant is maintained | ||
| assert(existingTypes.length <= 1, message: "Internal error: Position has multiple collateral types") | ||
|
|
||
| if existingTypes.length == 0 { | ||
| // No collateral yet, allow any type | ||
| return | ||
| } | ||
|
|
||
| // Check if type already exists (idempotent) | ||
| if existingTypes.contains(type) { | ||
| return | ||
| } | ||
|
|
||
| // For now, only one collateral type is allowed per position | ||
| // This restriction can be removed in the future to support multiple collateral types | ||
| panic("Position already has collateral type ".concat(existingTypes[0].identifier).concat(". Cannot deposit ").concat(type.identifier).concat(". Only one collateral type allowed per position.")) | ||
| } | ||
|
|
||
| /// Validates that the given token type can be used as debt for this position | ||
| /// Panics if position already has a different debt type | ||
| access(EImplementation) fun validateDebtType(_ type: Type) { | ||
| let existingTypes = self.getDebtTypes() | ||
|
|
||
| // Constraint: For now, only one debt type is allowed per position | ||
| // This assertion ensures the invariant is maintained | ||
| assert(existingTypes.length <= 1, message: "Internal error: Position has multiple debt types") | ||
|
|
||
| if existingTypes.length == 0 { | ||
| // No debt yet, allow any type | ||
| return | ||
| } | ||
|
|
||
| // Check if type already exists (idempotent) | ||
| if existingTypes.contains(type) { | ||
| return | ||
| } | ||
|
|
||
| // For now, only one debt type is allowed per position | ||
| // This restriction can be removed in the future to support multiple debt types | ||
| panic("Position already has debt type ".concat(existingTypes[0].identifier).concat(". Cannot borrow ").concat(type.identifier).concat(". Only one debt type allowed per position.")) | ||
| } | ||
| } | ||
|
|
||
| /// Factory function to create a new InternalPositionImplv1 resource. | ||
|
|
||
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.
The current implementation injects checks in all places where we might add/remove a debt/collateral balance, and uses the
validate*functions below to guard against adding more balances than we want to support.I am wondering if we can use post-conditions and these
getCollateralTypesfunctions to simplify this new logic and localize the temporary balance constraint to a smaller cross-section of the code. For example, if we added something like the following post-conditions to all functions which are able to modify position balances:This way, we don't need to make any modifications to the business logic of affected functions, we just let them execute as normal and revert if their end state conflicts with the constraint.