From 8e99b524b45a386df16a8abfa225fb4e2bb714bc Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 14 May 2025 14:03:15 -0600 Subject: [PATCH 01/80] add initial Tidal contract & supporting scripts & transactions --- cadence/contracts/Tidal.cdc | 188 ++++++++++++++++++ cadence/scripts/tidal/get_tide_ids.cdc | 12 ++ cadence/scripts/tokens/get_balance.cdc | 15 ++ .../transactions/flow-token/transfer_flow.cdc | 24 +++ cadence/transactions/tidal/close_tide.cdc | 51 +++++ .../transactions/tidal/deposit_to_tide.cdc | 41 ++++ cadence/transactions/tidal/open_tide.cdc | 51 +++++ cadence/transactions/tidal/setup.cdc | 28 +++ .../transactions/tidal/withdraw_from_tide.cdc | 52 +++++ 9 files changed, 462 insertions(+) create mode 100644 cadence/contracts/Tidal.cdc create mode 100644 cadence/scripts/tidal/get_tide_ids.cdc create mode 100644 cadence/scripts/tokens/get_balance.cdc create mode 100644 cadence/transactions/flow-token/transfer_flow.cdc create mode 100644 cadence/transactions/tidal/close_tide.cdc create mode 100644 cadence/transactions/tidal/deposit_to_tide.cdc create mode 100644 cadence/transactions/tidal/open_tide.cdc create mode 100644 cadence/transactions/tidal/setup.cdc create mode 100644 cadence/transactions/tidal/withdraw_from_tide.cdc diff --git a/cadence/contracts/Tidal.cdc b/cadence/contracts/Tidal.cdc new file mode 100644 index 00000000..789401de --- /dev/null +++ b/cadence/contracts/Tidal.cdc @@ -0,0 +1,188 @@ +import "FungibleToken" +import "Burner" +import "ViewResolver" + +import "DFB" + +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +access(all) contract Tidal { + + access(all) let TideManagerStoragePath: StoragePath + access(all) let TideManagerPublicPath: PublicPath + + access(all) event CreatedTide(id: UInt64, initialAmount: UFix64, creator: Address?) + access(all) event DepositedToTide(id: UInt64, amount: UFix64, owner: Address?, fromUUID: UInt64) + access(all) event WithdrawnFromTide(id: UInt64, amount: UFix64, owner: Address?, toUUID: UInt64) + access(all) event AddedToManager(id: UInt64, owner: Address?, managerUUID: UInt64) + access(all) event BurnedTide(id: UInt64, remainingBalance: UFix64) + + access(self) var currentIdentifier: UInt64 + + access(all) fun createTideManager(): @TideManager { + return <-create TideManager() + } + + /* --- CONSTRUCTS --- */ + + access(all) struct UniqueID : DFB.UniqueIdentifier { + access(all) let id: UInt64 + init() { + self.id = Tidal.currentIdentifier + Tidal.currentIdentifier = Tidal.currentIdentifier + 1 + } + } + + access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, FungibleToken.Provider, ViewResolver.Resolver { + access(contract) let uniqueID: {DFB.UniqueIdentifier} + access(self) let vault: @{FungibleToken.Vault} + + init(_ vault: @{FungibleToken.Vault}) { + self.vault <- vault + self.uniqueID = UniqueID() + } + + access(all) view fun id(): UInt64 { + return self.uniqueID.id + } + + access(all) view fun getTideBalance(): UFix64 { + return self.vault.balance + } + + access(contract) fun burnCallback() { + emit BurnedTide(id: self.uniqueID.id, remainingBalance: self.vault.balance) + } + + access(all) view fun getViews(): [Type] { + return [] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + return nil + } + + access(all) fun deposit(from: @{FungibleToken.Vault}) { + pre { + self.isSupportedVaultType(type: from.getType()): + "Deposited vault of type \(from.getType().identifier) is not supported by this Tide" + } + emit DepositedToTide(id: self.uniqueID.id, amount: from.balance, owner: self.owner?.address, fromUUID: from.uuid) + self.vault.deposit(from: <-from) + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + return { self.vault.getType() : true } + } + + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return self.getSupportedVaultTypes()[type] ?? false + } + + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return amount <= self.vault.balance + } + + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { + pre { + self.isAvailableToWithdraw(amount: amount): + "Requested amount \(amount) is greater than withdrawable balance of \(self.vault.balance)" + } + let res <- self.vault.withdraw(amount: amount) + emit WithdrawnFromTide(id: self.uniqueID.id, amount: amount, owner: self.owner?.address, toUUID: res.uuid) + return <- res + } + } + + access(all) entitlement Owner + + access(all) resource TideManager : ViewResolver.ResolverCollection { + access(self) let tides: @{UInt64: Tide} + + init() { + self.tides <- {} + } + + access(all) view fun borrowTide(id: UInt64): &Tide? { + return &self.tides[id] + } + + access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? { + return &self.tides[id] + } + + access(all) view fun getIDs(): [UInt64] { + return self.tides.keys + } + + access(all) view fun getNumberOfTides(): Int { + return self.tides.length + } + + access(all) fun createTide(withVault: @{FungibleToken.Vault}) { + let balance = withVault.balance + let tide <-create Tide(<-withVault) + + emit CreatedTide(id: tide.uniqueID.id, initialAmount: balance, creator: self.owner?.address) + + self.addTide(<-tide) + } + + access(all) fun addTide(_ tide: @Tide) { + pre { + self.tides[tide.uniqueID.id] == nil: + "Collision with Tide ID \(tide.uniqueID.id) - a Tide with this ID already exists" + } + emit AddedToManager(id: tide.uniqueID.id, owner: self.owner?.address, managerUUID: self.uuid) + self.tides[tide.uniqueID.id] <-! tide + } + + access(all) fun depositToTide(_ id: UInt64, from: @{FungibleToken.Vault}) { + pre { + self.tides[id] != nil: + "No Tide with ID \(id) found" + } + let tide = (&self.tides[id] as &Tide?)! + tide.deposit(from: <-from) + } + + access(Owner) fun withdrawTide(id: UInt64): @Tide { + pre { + self.tides[id] != nil: + "No Tide with ID \(id) found" + } + return <- self.tides.remove(key: id)! + } + + access(Owner) fun withdrawFromTide(_ id: UInt64, amount: UFix64): @{FungibleToken.Vault} { + pre { + self.tides[id] != nil: + "No Tide with ID \(id) found" + } + let tide = (&self.tides[id] as auth(FungibleToken.Withdraw) &Tide?)! + return <- tide.withdraw(amount: amount) + } + + access(Owner) fun closeTide(_ id: UInt64): @{FungibleToken.Vault} { + pre { + self.tides[id] != nil: + "No Tide with ID \(id) found" + } + let tide <- self.withdrawTide(id: id) + let res <- tide.withdraw(amount: tide.getTideBalance()) + Burner.burn(<-tide) + return <-res + } + } + + init() { + let pathIdentifier = "TidalTideManager_\(self.account.address)" + self.TideManagerStoragePath = StoragePath(identifier: pathIdentifier)! + self.TideManagerPublicPath = PublicPath(identifier: pathIdentifier)! + + self.currentIdentifier = 0 + } +} diff --git a/cadence/scripts/tidal/get_tide_ids.cdc b/cadence/scripts/tidal/get_tide_ids.cdc new file mode 100644 index 00000000..48d41849 --- /dev/null +++ b/cadence/scripts/tidal/get_tide_ids.cdc @@ -0,0 +1,12 @@ +import "Tidal" + +/// Retrieves the IDs of Tides configured at the provided address or `nil` if a TideManager is not stored +/// +/// @param address: The address of the Flow account in question +/// +/// @return A UInt64 array of all Tide IDs stored in the account's TideManager +access(all) +fun main(address: Address): [UInt64]? { + return getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) + ?.getIDs() +} diff --git a/cadence/scripts/tokens/get_balance.cdc b/cadence/scripts/tokens/get_balance.cdc new file mode 100644 index 00000000..331f2614 --- /dev/null +++ b/cadence/scripts/tokens/get_balance.cdc @@ -0,0 +1,15 @@ +import "FungibleToken" + +/// Returns the balance of the stored Vault at the given address if exists, otherwise nil +/// +/// @param address: The address of the account that owns the vault +/// @param vaultPathIdentifier: The identifier of the vault's storage path +/// +/// @returns The balance of the stored Vault at the given address +/// +access(all) fun main(address: Address, vaultPathIdentifier: String): UFix64? { + let path = StoragePath(identifier: vaultPathIdentifier) ?? panic("Malformed StoragePath identifier") + return getAuthAccount(address).storage.borrow<&{FungibleToken.Vault}>( + from: path + )?.balance ?? nil +} diff --git a/cadence/transactions/flow-token/transfer_flow.cdc b/cadence/transactions/flow-token/transfer_flow.cdc new file mode 100644 index 00000000..7a9e5f3c --- /dev/null +++ b/cadence/transactions/flow-token/transfer_flow.cdc @@ -0,0 +1,24 @@ +import "FungibleToken" +import "FlowToken" + +transaction(recipient: Address, amount: UFix64) { + + let providerVault: auth(FungibleToken.Withdraw) &FlowToken.Vault + let receiver: &{FungibleToken.Receiver} + + prepare(signer: auth(BorrowValue) &Account) { + self.providerVault = signer.storage.borrow( + from: /storage/flowTokenVault + )! + self.receiver = getAccount(recipient).capabilities.borrow<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + ?? panic("Could not borrow receiver reference") + } + + execute { + self.receiver.deposit( + from: <-self.providerVault.withdraw( + amount: amount + ) + ) + } +} diff --git a/cadence/transactions/tidal/close_tide.cdc b/cadence/transactions/tidal/close_tide.cdc new file mode 100644 index 00000000..e7e596e4 --- /dev/null +++ b/cadence/transactions/tidal/close_tide.cdc @@ -0,0 +1,51 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +import "Tidal" + +/// Withdraws the full balance from an existing Tide stored in the signer's TideManager and closes the Tide. If the +/// signer does not yet have a Vault of the withdrawn Type, one is configured. +/// +/// @param id: The Tide.id() of the Tide from which the full balance will be withdrawn +/// +transaction(id: UInt64) { + let manager: auth(Tidal.Owner) &Tidal.TideManager + let receiver: &{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { + // reference the signer's TideManager & underlying Tide + self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") + + // get the data for where the vault type is canoncially stored + let vaultType = tide.getSupportedVaultTypes().keys[0] + let tokenContract = getAccount(vaultType.address!).contracts.borrow<&{FungibleToken}>(name: vaultType.contractName!) + ?? panic("Vault type \(vaultType.identifier) is not defined by a FungibleToken contract") + let vaultData = tokenContract.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for vault type \(vaultType.identifier)") + + // configure a receiving Vault if none exists + if signer.storage.type(at: vaultData.storagePath) == nil { + signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) + let vaultCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + signer.capabilities.publish(vaultCap, at: vaultData.metadataPath) + signer.capabilities.publish(vaultCap, at: vaultData.receiverPath) + } + + // reference the signer's receiver + self.receiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) + ?? panic("Signer does not have a vault of type \(vaultType.identifier) at path \(vaultData.storagePath) from which to source funds") + } + + execute { + self.receiver.deposit( + from: <-self.manager.closeTide(id) + ) + } +} diff --git a/cadence/transactions/tidal/deposit_to_tide.cdc b/cadence/transactions/tidal/deposit_to_tide.cdc new file mode 100644 index 00000000..eacaa9d6 --- /dev/null +++ b/cadence/transactions/tidal/deposit_to_tide.cdc @@ -0,0 +1,41 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +import "Tidal" + +/// Deposits to an existing Tide stored in the signer's TideManager +/// +/// @param id: The Tide.id() of the Tide to which the amount will be deposited +/// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type +/// +transaction(id: UInt64, amount: UFix64) { + let manager: &Tidal.TideManager + let depositVault: @{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue) &Account) { + // reference the signer's TideManager & underlying Tide + self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") + + // get the data for where the vault type is canoncially stored + let vaultType = tide.getSupportedVaultTypes().keys[0] + let tokenContract = getAccount(vaultType.address!).contracts.borrow<&{FungibleToken}>(name: vaultType.contractName!) + ?? panic("Vault type \(vaultType.identifier) is not defined by a FungibleToken contract") + let vaultData = tokenContract.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for vault type \(vaultType.identifier)") + + // withdraw the amount to deposit into the new Tide + let sourceVault = signer.storage.borrow(from: vaultData.storagePath) + ?? panic("Signer does not have a vault of type \(vaultType.identifier) at path \(vaultData.storagePath) from which to source funds") + self.depositVault <- sourceVault.withdraw(amount: amount) + } + + execute { + self.manager.depositToTide(id, from: <-self.depositVault) + } +} diff --git a/cadence/transactions/tidal/open_tide.cdc b/cadence/transactions/tidal/open_tide.cdc new file mode 100644 index 00000000..c2c5fad2 --- /dev/null +++ b/cadence/transactions/tidal/open_tide.cdc @@ -0,0 +1,51 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +import "Tidal" + +/// Opens a new Tide in the Tidal platform, funding the Tide with the specified Vault and amount +/// +/// @param vaultIdentifier: The Vault's Type identifier +/// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' +/// @param amount: The amount to deposit into the new Tide +/// +transaction(vaultIdentifier: String, amount: UFix64) { + let manager: &Tidal.TideManager + let depositVault: @{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { + // get the data for where the vault type is canoncially stored + let vaultType = CompositeType(vaultIdentifier) + ?? panic("Vault identifier \(vaultIdentifier) is not associated with a valid Type") + let tokenContract = getAccount(vaultType.address!).contracts.borrow<&{FungibleToken}>(name: vaultType.contractName!) + ?? panic("Vault type \(vaultIdentifier) is not defined by a FungibleToken contract") + let vaultData = tokenContract.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for vault type \(vaultIdentifier)") + + // withdraw the amount to deposit into the new Tide + let sourceVault = signer.storage.borrow(from: vaultData.storagePath) + ?? panic("Signer does not have a vault of type \(vaultIdentifier) at path \(vaultData.storagePath) from which to source funds") + self.depositVault <- sourceVault.withdraw(amount: amount) + + // configure the TideManager if needed + if signer.storage.type(at: Tidal.TideManagerStoragePath) == nil { + signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) + signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) + // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) + signer.capabilities.storage.issue( + Tidal.TideManagerStoragePath + ) + } + self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + } + + execute { + self.manager.createTide(withVault: <-self.depositVault) + } +} \ No newline at end of file diff --git a/cadence/transactions/tidal/setup.cdc b/cadence/transactions/tidal/setup.cdc new file mode 100644 index 00000000..37369730 --- /dev/null +++ b/cadence/transactions/tidal/setup.cdc @@ -0,0 +1,28 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +import "Tidal" + +/// Configures a Tidal.TideManager at the canonical path. If one is already configured, the transaction no-ops +/// +transaction { + prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { + if signer.storage.type(at: Tidal.TideManagerStoragePath) == Type<@Tidal.TideManager>() { + return // early return if TideManager is found + } + + // configure the TideManager + signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) + signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) + // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) + signer.capabilities.storage.issue(Tidal.TideManagerStoragePath) + + // confirm setup of TideManager at canonical path + let storedType = signer.storage.type(at: Tidal.TideManagerStoragePath) ?? Type() + if storedType != Type<@Tidal.TideManager>() { + panic("Setup was unsuccessful - Expected TideManager at \(Tidal.TideManagerStoragePath) but found \(storedType.identifier)") + } + } +} diff --git a/cadence/transactions/tidal/withdraw_from_tide.cdc b/cadence/transactions/tidal/withdraw_from_tide.cdc new file mode 100644 index 00000000..c2db4bd1 --- /dev/null +++ b/cadence/transactions/tidal/withdraw_from_tide.cdc @@ -0,0 +1,52 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +import "Tidal" + +/// Withdraws from an existing Tide stored in the signer's TideManager. If the signer does not yet have a Vault of the +/// withdrawn Type, one is configured. +/// +/// @param id: The Tide.id() of the Tide from which the amount will be withdrawn +/// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type +/// +transaction(id: UInt64, amount: UFix64) { + let manager: auth(Tidal.Owner) &Tidal.TideManager + let receiver: &{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { + // reference the signer's TideManager & underlying Tide + self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") + + // get the data for where the vault type is canoncially stored + let vaultType = tide.getSupportedVaultTypes().keys[0] + let tokenContract = getAccount(vaultType.address!).contracts.borrow<&{FungibleToken}>(name: vaultType.contractName!) + ?? panic("Vault type \(vaultType.identifier) is not defined by a FungibleToken contract") + let vaultData = tokenContract.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for vault type \(vaultType.identifier)") + + // configure a receiving Vault if none exists + if signer.storage.type(at: vaultData.storagePath) == nil { + signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) + let vaultCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + signer.capabilities.publish(vaultCap, at: vaultData.metadataPath) + signer.capabilities.publish(vaultCap, at: vaultData.receiverPath) + } + + // reference the signer's receiver + self.receiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) + ?? panic("Signer does not have a vault of type \(vaultType.identifier) at path \(vaultData.storagePath) from which to source funds") + } + + execute { + self.receiver.deposit( + from: <-self.manager.withdrawFromTide(id, amount: amount) + ) + } +} From 7c754ee180c83f9f333803e1c5cf65d8d5825dea Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 14 May 2025 16:55:36 -0600 Subject: [PATCH 02/80] update DFB to include PriceOracle interface --- .../contracts/internal-dependencies/interfaces/DFB.cdc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc b/cadence/contracts/internal-dependencies/interfaces/DFB.cdc index 0e2a55d5..16826843 100644 --- a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc +++ b/cadence/contracts/internal-dependencies/interfaces/DFB.cdc @@ -1,3 +1,4 @@ +import "EVM" import "FungibleToken" /// DeFiBlocks Interfaces @@ -193,4 +194,13 @@ access(all) contract DFB { } } } + + /// An interface for a price oracle adapter. Implementations should adapt this interface to various price feed + /// oracles deployed on Flow + access(all) struct interface PriceOracle { + /// Returns the asset type serving as the price basis - e.g. USD in FLOW/USD + access(all) view fun unitOfAccount(): Type + /// Returns the latest price data for a given asset denominated in unitOfAccount() + access(all) fun price(ofToken: Type): UFix64 + } } From 32d1f26ffac8655973cc3e1134e5b445ba28ab14 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 15 May 2025 13:40:19 -0600 Subject: [PATCH 03/80] add initial USDA contract for use with MockOracle --- .../internal-dependencies/tokens/USDA.cdc | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 cadence/contracts/internal-dependencies/tokens/USDA.cdc diff --git a/cadence/contracts/internal-dependencies/tokens/USDA.cdc b/cadence/contracts/internal-dependencies/tokens/USDA.cdc new file mode 100644 index 00000000..f0a595ba --- /dev/null +++ b/cadence/contracts/internal-dependencies/tokens/USDA.cdc @@ -0,0 +1,217 @@ +import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" + +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +access(all) contract USDA : FungibleToken { + + /// Total supply of USDA in existence + access(all) var totalSupply: UFix64 + + /// Storage and Public Paths + access(all) let VaultStoragePath: StoragePath + access(all) let VaultPublicPath: PublicPath + access(all) let ReceiverPublicPath: PublicPath + access(all) let AdminStoragePath: StoragePath + + /// The event that is emitted when new tokens are minted + access(all) event Minted(type: String, amount: UFix64, toUUID: UInt64, minterUUID: UInt64) + /// Emitted whenever a new Minter is created + access(all) event MinterCreated(uuid: UInt64) + + /// createEmptyVault + /// + /// Function that creates a new Vault with a balance of zero + /// and returns it to the calling context. A user must call this function + /// and store the returned Vault in their storage in order to allow their + /// account to be able to receive deposits of this token type. + /// + access(all) fun createEmptyVault(vaultType: Type): @USDA.Vault { + return <- create Vault(balance: 0.0) + } + + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + return FungibleTokenMetadataViews.FTDisplay( + name: "AlpenFlow USD", + symbol: "USDA", + description: "A mocked version of AlpenFlow USD", + externalURL: MetadataViews.ExternalURL("https://flow.com"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + return FungibleTokenMetadataViews.FTVaultData( + storagePath: self.VaultStoragePath, + receiverPath: self.ReceiverPublicPath, + metadataPath: self.VaultPublicPath, + receiverLinkedType: Type<&USDA.Vault>(), + metadataLinkedType: Type<&USDA.Vault>(), + createEmptyVaultFunction: (fun(): @{FungibleToken.Vault} { + return <-USDA.createEmptyVault(vaultType: Type<@USDA.Vault>()) + }) + ) + case Type(): + return FungibleTokenMetadataViews.TotalSupply( + totalSupply: USDA.totalSupply + ) + } + return nil + } + + /* --- CONSTRUCTS --- */ + + /// Vault + /// + /// Each user stores an instance of only the Vault in their storage + /// The functions in the Vault and governed by the pre and post conditions + /// in FungibleToken when they are called. + /// The checks happen at runtime whenever a function is called. + /// + /// Resources can only be created in the context of the contract that they + /// are defined in, so there is no way for a malicious user to create Vaults + /// out of thin air. A special Minter resource needs to be defined to mint + /// new tokens. + /// + access(all) resource Vault: FungibleToken.Vault { + + /// The total balance of this vault + access(all) var balance: UFix64 + + /// Identifies the destruction of a Vault even when destroyed outside of Buner.burn() scope + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid, balance: UFix64 = self.balance) + + init(balance: UFix64) { + self.balance = balance + } + + /// Called when a fungible token is burned via the `Burner.burn()` method + access(contract) fun burnCallback() { + if self.balance > 0.0 { + USDA.totalSupply = USDA.totalSupply - self.balance + } + self.balance = 0.0 + } + + access(all) view fun getViews(): [Type] { + return USDA.getContractViews(resourceType: nil) + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + return USDA.resolveContractView(resourceType: nil, viewType: view) + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[self.getType()] = true + return supportedTypes + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return self.getSupportedVaultTypes()[type] ?? false + } + + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return amount <= self.balance + } + + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @USDA.Vault { + self.balance = self.balance - amount + return <-create Vault(balance: amount) + } + + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let vault <- from as! @USDA.Vault + self.balance = self.balance + vault.balance + destroy vault + } + + access(all) fun createEmptyVault(): @USDA.Vault { + return <-create Vault(balance: 0.0) + } + } + + /// Minter + /// + /// Resource object that token admin accounts can hold to mint new tokens. + /// + access(all) resource Minter { + /// Identifies when a Minter is destroyed, coupling with MinterCreated event to trace Minter UUIDs + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid) + + init() { + emit MinterCreated(uuid: self.uuid) + } + + /// mintTokens + /// + /// Function that mints new tokens, adds them to the total supply, + /// and returns them to the calling context. + /// + access(all) fun mintTokens(amount: UFix64): @USDA.Vault { + USDA.totalSupply = USDA.totalSupply + amount + let vault <-create Vault(balance: amount) + emit Minted(type: vault.getType().identifier, amount: amount, toUUID: vault.uuid, minterUUID: self.uuid) + return <-vault + } + } + + init() { + let initialMint = 1000.0 + + self.totalSupply = 0.0 + + let address = self.account.address + self.VaultStoragePath = StoragePath(identifier: "usdaTokenVault_\(address)")! + self.VaultPublicPath = PublicPath(identifier: "usdaTokenVault_\(address)")! + self.ReceiverPublicPath = PublicPath(identifier: "usdaTokenReceiver_\(address)")! + self.AdminStoragePath = StoragePath(identifier: "usdaTokenAdmin_\(address)")! + + + // Create a public capability to the stored Vault that exposes + // the `deposit` method and getAcceptedTypes method through the `Receiver` interface + // and the `balance` method through the `Balance` interface + // + self.account.storage.save(<-create Vault(balance: self.totalSupply), to: self.VaultStoragePath) + let vaultCap = self.account.capabilities.storage.issue<&USDA.Vault>(self.VaultStoragePath) + self.account.capabilities.publish(vaultCap, at: self.VaultPublicPath) + let receiverCap = self.account.capabilities.storage.issue<&USDA.Vault>(self.VaultStoragePath) + self.account.capabilities.publish(receiverCap, at: self.ReceiverPublicPath) + + // Create a Minter & mint the initial supply of tokens to the contract account's Vault + let admin <- create Minter() + + self.account.capabilities.borrow<&Vault>(self.ReceiverPublicPath)!.deposit( + from: <- admin.mintTokens(amount: initialMint) + ) + + self.account.storage.save(<-admin, to: self.AdminStoragePath) + } +} From be1f7c44f982cd63d4b6a4527bf0f46a3ba32ddf Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 15 May 2025 13:55:12 -0600 Subject: [PATCH 04/80] add MockOracle contract & supporting txns & scripts --- cadence/contracts/mocks/MockOracle.cdc | 63 +++++++++++++++++++ cadence/scripts/mocks/oracle/get_price.cdc | 12 ++++ .../transactions/mocks/oracle/bump_price.cdc | 18 ++++++ .../transactions/mocks/oracle/set_price.cdc | 18 ++++++ flow.json | 25 ++++++++ 5 files changed, 136 insertions(+) create mode 100644 cadence/contracts/mocks/MockOracle.cdc create mode 100644 cadence/scripts/mocks/oracle/get_price.cdc create mode 100644 cadence/transactions/mocks/oracle/bump_price.cdc create mode 100644 cadence/transactions/mocks/oracle/set_price.cdc diff --git a/cadence/contracts/mocks/MockOracle.cdc b/cadence/contracts/mocks/MockOracle.cdc new file mode 100644 index 00000000..79f3afac --- /dev/null +++ b/cadence/contracts/mocks/MockOracle.cdc @@ -0,0 +1,63 @@ +import "FungibleToken" + +import "DFB" + +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +access(all) contract MockOracle { + + /// token price denominated in USD + access(self) let mockedPrices: {Type: UFix64} + /// the token type in which prices are denominated + access(self) let unitOfAccount: Type + /// bps up or down by which current price moves when bumpPrice is called + access(self) let bumpVariance: UInt16 + + access(all) struct PriceOracle : DFB.PriceOracle { + + /// Returns the asset type serving as the price basis - e.g. USD in FLOW/USD + access(all) view fun unitOfAccount(): Type { + return MockOracle.unitOfAccount + } + + /// Returns the latest price data for a given asset denominated in unitOfAccount() + access(all) fun price(ofToken: Type): UFix64 { + return MockOracle.mockedPrices[ofToken] ?? panic("Unsupported token type \(ofToken.identifier)") + } + } + + // resets the price of the token within 0-bumpVariance (bps) of the current price + // allows for mocked data to have variability + access(all) fun bumpPrice(forToken: Type) { + let current = self.mockedPrices[forToken] + ?? panic("MockOracle does not have a price set for token \(forToken.identifier)") + let sign = revertibleRandom(modulo: 2) // 0 - down | 1 - up + let variance = self.convertToBPS(revertibleRandom(modulo: self.bumpVariance)) // bps up or down + if sign == 0 { + self.mockedPrices[forToken] = current - (current * variance) + } else { + self.mockedPrices[forToken] = current + (current * variance) + } + } + + access(all) fun setPrice(forToken: Type, price: UFix64) { + self.mockedPrices[forToken] = price + } + + access(self) view fun convertToBPS(_ variance: UInt16): UFix64 { + var res = UFix64(variance) + for i in InclusiveRange(0, 3) { + res = res / 10.0 + } + return res + } + + init(unitOfAccountIdentifier: String) { + self.mockedPrices = {} + // e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' + self.unitOfAccount = CompositeType(unitOfAccountIdentifier) ?? panic("Invalid unitOfAccountIdentifier \(unitOfAccountIdentifier)") + self.bumpVariance = 100 // 0.1% variance + } +} diff --git a/cadence/scripts/mocks/oracle/get_price.cdc b/cadence/scripts/mocks/oracle/get_price.cdc new file mode 100644 index 00000000..c7efe2cd --- /dev/null +++ b/cadence/scripts/mocks/oracle/get_price.cdc @@ -0,0 +1,12 @@ +import "MockOracle" + +/// Gets the mocked price data from the MockOracle contract denominated in the current unitOfAccount token type +/// +/// @param forTokenIdentifier: The Vault Type identifier for the token whose price will be retrieved +access(all) +fun main(forTokenIdentifier: String): UFix64 { + // Type identifier - e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' + return MockOracle.PriceOracle().price( + ofToken: CompositeType(forTokenIdentifier) ?? panic("Invalid forTokenIdentifier \(forTokenIdentifier)") + ) +} diff --git a/cadence/transactions/mocks/oracle/bump_price.cdc b/cadence/transactions/mocks/oracle/bump_price.cdc new file mode 100644 index 00000000..2fbffe5c --- /dev/null +++ b/cadence/transactions/mocks/oracle/bump_price.cdc @@ -0,0 +1,18 @@ +import "MockOracle" + +/// Bumps the current token price in the MockOracle contract by some percentage (up or down) between +/// 0-bumpVariance (default 1%) randomly chosen using revertibleRandom +/// +/// @param vaultIdentifier: The Vault's Type identifier +/// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' +transaction(forTokenIdentifier: String) { + let tokenType: Type + + prepare(signer: &Account) { + self.tokenType = CompositeType(forTokenIdentifier) ?? panic("Invalid Type \(forTokenIdentifier)") + } + + execute { + MockOracle.bumpPrice(forToken: self.tokenType) + } +} diff --git a/cadence/transactions/mocks/oracle/set_price.cdc b/cadence/transactions/mocks/oracle/set_price.cdc new file mode 100644 index 00000000..285e9939 --- /dev/null +++ b/cadence/transactions/mocks/oracle/set_price.cdc @@ -0,0 +1,18 @@ +import "MockOracle" + +/// Upserts the provided Token & price pairing in the MockOracle contract +/// +/// @param vaultIdentifier: The Vault's Type identifier +/// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' +/// @param price: The price to set the token to in the MockOracle denominated in USD +transaction(forTokenIdentifier: String, price: UFix64) { + let tokenType: Type + + prepare(signer: &Account) { + self.tokenType = CompositeType(forTokenIdentifier) ?? panic("Invalid Type \(forTokenIdentifier)") + } + + execute { + MockOracle.setPrice(forToken: self.tokenType, price: price) + } +} diff --git a/flow.json b/flow.json index d3ef9d5c..c50c1bc1 100644 --- a/flow.json +++ b/flow.json @@ -12,6 +12,12 @@ "testing": "0000000000000007" } }, + "MockOracle": { + "source": "cadence/contracts/mocks/MockOracle.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "SwapStack": { "source": "cadence/contracts/internal-dependencies/connectors/SwapStack.cdc", "aliases": { @@ -23,6 +29,12 @@ "aliases": { "testing": "0000000000000008" } + }, + "USDA": { + "source": "cadence/contracts/internal-dependencies/tokens/USDA.cdc", + "aliases": { + "testing": "0000000000000007" + } } }, "dependencies": { @@ -117,6 +129,19 @@ "deployments": { "emulator": { "emulator-account": [ + "DFB", + "FungibleTokenStack", + "SwapStack", + "USDA", + { + "name": "MockOracle", + "args": [ + { + "value": "A.f8d6e0586b0a20c7.USDA.Vault", + "type": "String" + } + ] + }, "Tidal" ] } From 8dfb7de453eedd4cdf47f34847bdf0c230984f8e Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 15 May 2025 14:46:47 -0600 Subject: [PATCH 05/80] remove unused import --- cadence/contracts/internal-dependencies/interfaces/DFB.cdc | 1 - 1 file changed, 1 deletion(-) diff --git a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc b/cadence/contracts/internal-dependencies/interfaces/DFB.cdc index 16826843..9c9e582c 100644 --- a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc +++ b/cadence/contracts/internal-dependencies/interfaces/DFB.cdc @@ -1,4 +1,3 @@ -import "EVM" import "FungibleToken" /// DeFiBlocks Interfaces From f538ddd02b73f3529f52755359c57b689c2fb00a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 15 May 2025 16:03:46 -0600 Subject: [PATCH 06/80] add MockSwapper contract --- cadence/contracts/mocks/MockSwapper.cdc | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 cadence/contracts/mocks/MockSwapper.cdc diff --git a/cadence/contracts/mocks/MockSwapper.cdc b/cadence/contracts/mocks/MockSwapper.cdc new file mode 100644 index 00000000..83ef52ee --- /dev/null +++ b/cadence/contracts/mocks/MockSwapper.cdc @@ -0,0 +1,94 @@ +import "FungibleToken" +import "Burner" + +import "DFB" +import "SwapStack" + +access(all) contract MockSwapper { + + access(all) struct Swapper : DFB.Swapper { + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(self) let inVault: {DFB.Sink, DFB.Source} + access(self) let outVault: {DFB.Sink, DFB.Source} + access(self) let oracle: {DFB.PriceOracle} + + init(inVault: {DFB.Sink, DFB.Source}, outVault: {DFB.Sink, DFB.Source}, oracle: {DFB.PriceOracle}, uniqueID: {DFB.UniqueIdentifier}?) { + self.inVault = inVault + self.outVault = outVault + self.oracle = oracle + self.uniqueID = uniqueID + } + + /// The type of Vault this Swapper accepts when performing a swap + access(all) view fun inVaultType(): Type { + return self.inVault.getSinkType() + } + + /// The type of Vault this Swapper provides when performing a swap + access(all) view fun outVaultType(): Type { + return self.outVault.getSourceType() + } + + /// The estimated amount required to provide a Vault with the desired output balance + access(all) fun amountIn(forDesired: UFix64, reverse: Bool): {DFB.Quote} { + return self._estimate(amount: forDesired, out: false, reverse: reverse) + } + + /// The estimated amount delivered out for a provided input balance + access(all) fun amountOut(forProvided: UFix64, reverse: Bool): {DFB.Quote} { + return self._estimate(amount: forProvided, out: true, reverse: reverse) + } + + /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose + /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted + /// to use multiple Flow swap protocols. + access(all) fun swap(quote: {DFB.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} { + return <- self._swap(<-inVault, reverse: false) + } + + /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose + /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted + /// to use multiple Flow swap protocols. + access(all) fun swapBack(quote: {DFB.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} { + return <- self._swap(<-residual, reverse: true) + } + + /// Internal estimator returning a quote for the amount in/out and in the desired direction + access(self) fun _estimate(amount: UFix64, out: Bool, reverse: Bool): {DFB.Quote} { + let price = reverse + ? self.oracle.price(ofToken: self.outVaultType()) / self.oracle.price(ofToken: self.inVaultType()) + : self.oracle.price(ofToken: self.inVaultType()) / self.oracle.price(ofToken: self.outVaultType()) + return SwapStack.BasicQuote( + inVault: reverse ? self.outVaultType() : self.inVaultType(), + outVault: reverse ? self.inVaultType() : self.outVaultType(), + inAmount: out ? amount : amount / price, + outAmount: out ? amount * price : amount + ) + } + + access(self) fun _swap(_ from: @{FungibleToken.Vault}, reverse: Bool): @{FungibleToken.Vault} { + let inAmount = from.balance + if reverse { + self.outVault.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + } else { + self.inVault.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + } + Burner.burn(<-from) + + let outAmount = self.amountOut(forProvided: inAmount, reverse: reverse).outAmount + var outVault: @{FungibleToken.Vault}? <- nil + if reverse { + outVault <-! self.inVault.withdrawAvailable(maxAmount: outAmount) + } else { + outVault <-! self.outVault.withdrawAvailable(maxAmount: outAmount) + } + let withdrawn = (outVault?.balance)! + assert(withdrawn == outAmount, + message: "MockSwapper outVault returned invalid balance - expected \(outAmount), received \(withdrawn)") + return <- outVault! + } + } + +} \ No newline at end of file From 5270eb4bfe1a163956e12333416811f30ac9c871 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 15 May 2025 16:51:25 -0600 Subject: [PATCH 07/80] update MockSwapper to better represent sourcing liquidity from pools outside of the struct --- cadence/contracts/mocks/MockSwapper.cdc | 81 +++++++++++++++++-------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/cadence/contracts/mocks/MockSwapper.cdc b/cadence/contracts/mocks/MockSwapper.cdc index 83ef52ee..b7e1db25 100644 --- a/cadence/contracts/mocks/MockSwapper.cdc +++ b/cadence/contracts/mocks/MockSwapper.cdc @@ -1,42 +1,69 @@ import "FungibleToken" import "Burner" +import "MockOracle" + import "DFB" import "SwapStack" +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// access(all) contract MockSwapper { + /// Mocked liquidity sources + access(self) let liquidityConnectors: {Type: {DFB.Sink, DFB.Source}} + + /// Mock setter enabling the configuration of liquidity sources used by mock swappers + access(all) fun setLiquidityConnector(_ connector: {DFB.Sink, DFB.Source}) { + pre { + connector.getSinkType() == connector.getSourceType(): + "Connector sink Type \(connector.getSinkType().identifier) != connector source Type \(connector.getSourceType().identifier)" + } + self.liquidityConnectors[connector.getSinkType()] = connector + } + + // Swapper + // + /// Mocked DeFiBlocks Swapper implementation. Be sure to set connectors for Vaults you wish to handle via this mock + /// in MockSwapper.liquidityConnectors via .setLiquidityConnector before instantiating mocks access(all) struct Swapper : DFB.Swapper { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction access(contract) let uniqueID: {DFB.UniqueIdentifier}? - access(self) let inVault: {DFB.Sink, DFB.Source} - access(self) let outVault: {DFB.Sink, DFB.Source} + access(self) let inVault: Type + access(self) let outVault: Type access(self) let oracle: {DFB.PriceOracle} - init(inVault: {DFB.Sink, DFB.Source}, outVault: {DFB.Sink, DFB.Source}, oracle: {DFB.PriceOracle}, uniqueID: {DFB.UniqueIdentifier}?) { + init(inVault: Type, outVault: Type, uniqueID: {DFB.UniqueIdentifier}?) { + pre { + MockSwapper.liquidityConnectors[inVault] != nil: + "Invalid inVault - \(inVault.identifier) does not have a MockSwapper connector to handle funds" + MockSwapper.liquidityConnectors[outVault] != nil: + "Invalid outVault - \(outVault.identifier) does not have a MockSwapper connector to handle funds" + } self.inVault = inVault self.outVault = outVault - self.oracle = oracle + self.oracle = MockOracle.PriceOracle() self.uniqueID = uniqueID } /// The type of Vault this Swapper accepts when performing a swap access(all) view fun inVaultType(): Type { - return self.inVault.getSinkType() + return self.inVault } /// The type of Vault this Swapper provides when performing a swap access(all) view fun outVaultType(): Type { - return self.outVault.getSourceType() + return self.outVault } - /// The estimated amount required to provide a Vault with the desired output balance + /// The estimated amount required to provide a Vault with the desired output balance, sourcing pricing from the + /// mocked oracle access(all) fun amountIn(forDesired: UFix64, reverse: Bool): {DFB.Quote} { return self._estimate(amount: forDesired, out: false, reverse: reverse) } - /// The estimated amount delivered out for a provided input balance + /// The estimated amount delivered out for a provided input balance, sourcing pricing from the mocked oracle access(all) fun amountOut(forProvided: UFix64, reverse: Bool): {DFB.Quote} { return self._estimate(amount: forProvided, out: true, reverse: reverse) } @@ -44,6 +71,8 @@ access(all) contract MockSwapper { /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted /// to use multiple Flow swap protocols. + /// NOTE: This mock sources pricing data from the mocked oracle, allowing for pricing to be manually manipulated + /// for testing and demonstration purposes access(all) fun swap(quote: {DFB.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} { return <- self._swap(<-inVault, reverse: false) } @@ -51,6 +80,8 @@ access(all) contract MockSwapper { /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted /// to use multiple Flow swap protocols. + /// NOTE: This mock sources pricing data from the mocked oracle, allowing for pricing to be manually manipulated + /// for testing and demonstration purposes access(all) fun swapBack(quote: {DFB.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} { return <- self._swap(<-residual, reverse: true) } @@ -70,25 +101,23 @@ access(all) contract MockSwapper { access(self) fun _swap(_ from: @{FungibleToken.Vault}, reverse: Bool): @{FungibleToken.Vault} { let inAmount = from.balance - if reverse { - self.outVault.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - } else { - self.inVault.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - } + var swapInVault = reverse ? MockSwapper.liquidityConnectors[from.getType()]! : MockSwapper.liquidityConnectors[self.inVaultType()]! + var swapOutVault = reverse ? MockSwapper.liquidityConnectors[self.inVaultType()]! : MockSwapper.liquidityConnectors[self.outVaultType()]! + + swapInVault.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) Burner.burn(<-from) let outAmount = self.amountOut(forProvided: inAmount, reverse: reverse).outAmount - var outVault: @{FungibleToken.Vault}? <- nil - if reverse { - outVault <-! self.inVault.withdrawAvailable(maxAmount: outAmount) - } else { - outVault <-! self.outVault.withdrawAvailable(maxAmount: outAmount) - } - let withdrawn = (outVault?.balance)! - assert(withdrawn == outAmount, - message: "MockSwapper outVault returned invalid balance - expected \(outAmount), received \(withdrawn)") - return <- outVault! + var outVault <- swapOutVault.withdrawAvailable(maxAmount: outAmount) + + assert(outVault.balance == outAmount, + message: "MockSwapper outVault returned invalid balance - expected \(outAmount), received \(outVault.balance)") + + return <- outVault } } - + + init() { + self.liquidityConnectors = {} + } } \ No newline at end of file From 58bd4540c855eac9f8e1ac51dc4119079c43490d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 15 May 2025 16:58:50 -0600 Subject: [PATCH 08/80] update flow.json --- flow.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flow.json b/flow.json index c50c1bc1..e605775b 100644 --- a/flow.json +++ b/flow.json @@ -18,6 +18,12 @@ "testing": "0000000000000008" } }, + "MockSwapper": { + "source": "cadence/contracts/mocks/MockSwapper.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "SwapStack": { "source": "cadence/contracts/internal-dependencies/connectors/SwapStack.cdc", "aliases": { From 3e7dddb35b8f6f696034c62e74138c8e4a727f6d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 15 May 2025 16:59:18 -0600 Subject: [PATCH 09/80] add MockSwapper.setLiquidityConnector transaction --- .../mocks/swapper/set_liquidity_connector.cdc | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 cadence/transactions/mocks/swapper/set_liquidity_connector.cdc diff --git a/cadence/transactions/mocks/swapper/set_liquidity_connector.cdc b/cadence/transactions/mocks/swapper/set_liquidity_connector.cdc new file mode 100644 index 00000000..8be0f90a --- /dev/null +++ b/cadence/transactions/mocks/swapper/set_liquidity_connector.cdc @@ -0,0 +1,25 @@ +import "FungibleToken" + +import "MockSwapper" + +import "FungibleTokenStack" +import "DFB" + +transaction(vaultStoragePath: StoragePath) { + + let vaultCap: Capability + + prepare(signer: auth(IssueStorageCapabilityController) &Account) { + self.vaultCap = signer.capabilities.storage.issue(vaultStoragePath) + } + + execute { + let vaultConnector = FungibleTokenStack.VaultSinkAndSource( + min: nil, + max: nil, + vault: self.vaultCap, + uniqueID: nil + ) + MockSwapper.setLiquidityConnector(vaultConnector) + } +} From 07cc65e8b8d69670f16ba3a1c31b4ca54e6a68ce Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 16 May 2025 11:47:17 -0600 Subject: [PATCH 10/80] update transaction comment --- .../transactions/mocks/swapper/set_liquidity_connector.cdc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cadence/transactions/mocks/swapper/set_liquidity_connector.cdc b/cadence/transactions/mocks/swapper/set_liquidity_connector.cdc index 8be0f90a..5fea57d1 100644 --- a/cadence/transactions/mocks/swapper/set_liquidity_connector.cdc +++ b/cadence/transactions/mocks/swapper/set_liquidity_connector.cdc @@ -5,6 +5,12 @@ import "MockSwapper" import "FungibleTokenStack" import "DFB" +/// Configures the MockSwapper contract with a liquidity source connected to the signer's Vault at the provided +/// storage path +/// +/// @param vaultStoragePath: The StoragePath where the underlying Vault is stored and from which a Capability will be +/// issued +/// transaction(vaultStoragePath: StoragePath) { let vaultCap: Capability From 675e146450de168f3bbd0ff8826aeb31af0a2218 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 19 May 2025 15:48:31 -0600 Subject: [PATCH 11/80] add MockSwapper to emulator deployment --- flow.json | 1 + 1 file changed, 1 insertion(+) diff --git a/flow.json b/flow.json index e605775b..6f75c49f 100644 --- a/flow.json +++ b/flow.json @@ -148,6 +148,7 @@ } ] }, + "MockSwapper", "Tidal" ] } From 1b5055bea68af29d953af5116f8b09df1068502f Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 19 May 2025 16:06:00 -0600 Subject: [PATCH 12/80] add supporting USDA transactions --- cadence/transactions/usda/mint.cdc | 30 +++++++++++++++++++++++++++++ cadence/transactions/usda/setup.cdc | 26 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 cadence/transactions/usda/mint.cdc create mode 100644 cadence/transactions/usda/setup.cdc diff --git a/cadence/transactions/usda/mint.cdc b/cadence/transactions/usda/mint.cdc new file mode 100644 index 00000000..2fb29eb8 --- /dev/null +++ b/cadence/transactions/usda/mint.cdc @@ -0,0 +1,30 @@ +import "FungibleToken" + +import "USDA" + +/// MOCK TRANSACTION - DO NOT USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +/// Mints the given amount of USDA tokens to the named recipient, reverting if they do not have a Vault configured +/// +/// @param recipient: The Flow address to receive the minted tokens +/// @param amount: The amount of USDA tokens to mint +/// +transaction(recipient: Address, amount: UFix64) { + + let minter: &USDA.Minter + let receiver: &{FungibleToken.Receiver} + + prepare(signer: auth(BorrowValue) &Account) { + self.minter = signer.storage.borrow<&USDA.Minter>(from: USDA.AdminStoragePath) + ?? panic("Could not find USDA Minter at \(USDA.AdminStoragePath)") + self.receiver = getAccount(recipient).capabilities.borrow<&{FungibleToken.Receiver}>(USDA.ReceiverPublicPath) + ?? panic("Could not find FungibleToken Receiver in \(recipient) at path \(USDA.ReceiverPublicPath)") + } + + execute { + self.receiver.deposit( + from: <-self.minter.mintTokens(amount: amount) + ) + } +} diff --git a/cadence/transactions/usda/setup.cdc b/cadence/transactions/usda/setup.cdc new file mode 100644 index 00000000..7dbe9c1b --- /dev/null +++ b/cadence/transactions/usda/setup.cdc @@ -0,0 +1,26 @@ +import "FungibleToken" +import "ViewResolver" + +import "USDA" + +/// Configures a USDA Vault if one is not found, ensuring proper configuration in storage +/// +transaction { + prepare(signer: auth(SaveValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // configure if nothing is found at canonical path + if signer.storage.type(at: USDA.VaultStoragePath) == nil { + // save the new vault + signer.storage.save(<-USDA.createEmptyVault(vaultType: Type<@USDA.Vault>()), to: USDA.VaultStoragePath) + // publish a public capability on the Vault + let cap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(USDA.VaultStoragePath) + signer.capabilities.unpublish(USDA.ReceiverPublicPath) + signer.capabilities.publish(cap, at: USDA.ReceiverPublicPath) + // issue an authorized capability to initialize a CapabilityController on the account, but do not publish + signer.capabilities.storage.issue(USDA.VaultStoragePath) + } + // ensure proper configuration + if signer.storage.type(at: USDA.VaultStoragePath) != Type<@USDA.Vault>(){ + panic("Could not configure USDA Vault at \(USDA.VaultStoragePath) - check for collision and try again") + } + } +} From dee1f146010887bb52055ed205e89971cd1abf43 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 19 May 2025 16:33:52 -0600 Subject: [PATCH 13/80] update MockOracle to return 1.0 for unitOfAccount price --- cadence/contracts/mocks/MockOracle.cdc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cadence/contracts/mocks/MockOracle.cdc b/cadence/contracts/mocks/MockOracle.cdc index 79f3afac..8441ab6c 100644 --- a/cadence/contracts/mocks/MockOracle.cdc +++ b/cadence/contracts/mocks/MockOracle.cdc @@ -24,6 +24,9 @@ access(all) contract MockOracle { /// Returns the latest price data for a given asset denominated in unitOfAccount() access(all) fun price(ofToken: Type): UFix64 { + if ofToken == self.unitOfAccount() { + return 1.0 + } return MockOracle.mockedPrices[ofToken] ?? panic("Unsupported token type \(ofToken.identifier)") } } @@ -31,6 +34,9 @@ access(all) contract MockOracle { // resets the price of the token within 0-bumpVariance (bps) of the current price // allows for mocked data to have variability access(all) fun bumpPrice(forToken: Type) { + if forToken == self.unitOfAccount { + return + } let current = self.mockedPrices[forToken] ?? panic("MockOracle does not have a price set for token \(forToken.identifier)") let sign = revertibleRandom(modulo: 2) // 0 - down | 1 - up From bc249081e9ba95816dd0879503c314d64c7fbbde Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 19 May 2025 16:42:35 -0600 Subject: [PATCH 14/80] fix internal dependency pre-condition --- .../internal-dependencies/connectors/FungibleTokenStack.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc b/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc index 0b7b7ac7..5d4ddf6e 100644 --- a/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc +++ b/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc @@ -137,7 +137,7 @@ access(all) contract FungibleTokenStack { ) { pre { vault.check(): "Invalid Vault Capability provided" - FungibleTokenStack.definingContractIsFungibleToken(vault.getType()): + FungibleTokenStack.definingContractIsFungibleToken(vault.borrow()!.getType()): "The contract defining Vault \(vault.borrow()!.getType().identifier) does not conform to FungibleToken contract interface" min ?? 0.0 < max ?? UFix64.max: "Minimum balance must be less than maximum balance if either is declared" From d4efa70a9694762fa17da720622ff794be54235b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 19 May 2025 16:51:07 -0600 Subject: [PATCH 15/80] add emulator setup script --- local/setup_emulator.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 local/setup_emulator.sh diff --git a/local/setup_emulator.sh b/local/setup_emulator.sh new file mode 100644 index 00000000..65d0d219 --- /dev/null +++ b/local/setup_emulator.sh @@ -0,0 +1,4 @@ +flow deploy +flow transactions send ./cadence/transactions/mocks/oracle/set_price.cdc 'A.0ae53cb6e3f42a79.FlowToken.Vault' 0.5 +flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc /storage/flowTokenVault +flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc /storage/usdaTokenVault_0xf8d6e0586b0a20c7 \ No newline at end of file From c5fd5ef66e3598e058518e0e8407161857063e85 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 22 May 2025 12:52:28 -0600 Subject: [PATCH 16/80] update USDA initialization args --- .../contracts/internal-dependencies/tokens/USDA.cdc | 3 +-- flow.json | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cadence/contracts/internal-dependencies/tokens/USDA.cdc b/cadence/contracts/internal-dependencies/tokens/USDA.cdc index f0a595ba..c8f9081a 100644 --- a/cadence/contracts/internal-dependencies/tokens/USDA.cdc +++ b/cadence/contracts/internal-dependencies/tokens/USDA.cdc @@ -183,8 +183,7 @@ access(all) contract USDA : FungibleToken { } } - init() { - let initialMint = 1000.0 + init(initialMint: UFix64) { self.totalSupply = 0.0 diff --git a/flow.json b/flow.json index 6f75c49f..b03a748e 100644 --- a/flow.json +++ b/flow.json @@ -138,7 +138,15 @@ "DFB", "FungibleTokenStack", "SwapStack", - "USDA", + { + "name": "USDA", + "args": [ + { + "value": "1000000.0", + "type": "UFix64" + } + ] + }, { "name": "MockOracle", "args": [ From 815f75fe070456a50b9713b9493cb67fe33e4782 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 28 May 2025 15:01:55 -0700 Subject: [PATCH 17/80] update internal dependencies --- .../connectors/FungibleTokenStack.cdc | 38 +- .../connectors/SwapStack.cdc | 100 +-- .../internal-dependencies/interfaces/DFB.cdc | 666 ++++++++++++++++-- .../internal-dependencies/utils/DFBUtils.cdc | 45 ++ flow.json | 7 + 5 files changed, 729 insertions(+), 127 deletions(-) create mode 100644 cadence/contracts/internal-dependencies/utils/DFBUtils.cdc diff --git a/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc b/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc index 5d4ddf6e..96253435 100644 --- a/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc +++ b/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc @@ -1,6 +1,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" +import "DFBUtils" import "DFB" /// FungibleTokenStack @@ -18,18 +19,18 @@ access(all) contract FungibleTokenStack { access(all) let maximumBalance: UFix64 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(contract) let uniqueID: DFB.UniqueIdentifier? /// An unentitled Capability on the Vault to which deposits are distributed access(self) let depositVault: Capability<&{FungibleToken.Vault}> init( max: UFix64?, depositVault: Capability<&{FungibleToken.Vault}>, - uniqueID: {DFB.UniqueIdentifier}? + uniqueID: DFB.UniqueIdentifier? ) { pre { depositVault.check(): "Provided invalid Capability" - FungibleTokenStack.definingContractIsFungibleToken(depositVault.borrow()!.getType()): + DFBUtils.definingContractIsFungibleToken(depositVault.borrow()!.getType()): "The contract defining Vault \(depositVault.borrow()!.getType().identifier) does not conform to FungibleToken contract interface" } self.maximumBalance = max ?? UFix64.max // assume no maximum if none provided @@ -70,18 +71,18 @@ access(all) contract FungibleTokenStack { access(all) let minimumBalance: UFix64 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(contract) let uniqueID: DFB.UniqueIdentifier? /// An entitled Capability on the Vault from which withdrawals are sourced access(self) let withdrawVault: Capability init( min: UFix64?, withdrawVault: Capability, - uniqueID: {DFB.UniqueIdentifier}? + uniqueID: DFB.UniqueIdentifier? ) { pre { withdrawVault.check(): "Provided invalid Capability" - FungibleTokenStack.definingContractIsFungibleToken(withdrawVault.borrow()!.getType()): + DFBUtils.definingContractIsFungibleToken(withdrawVault.borrow()!.getType()): "The contract defining Vault \(withdrawVault.borrow()!.getType().identifier) does not conform to FungibleToken contract interface" } self.minimumBalance = min ?? 0.0 // assume no minimum if none provided @@ -108,7 +109,7 @@ access(all) contract FungibleTokenStack { access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { let available = self.minimumAvailable() if !self.withdrawVault.check() || available == 0.0 || maxAmount == 0.0 { - return <- FungibleTokenStack.getEmptyVault(self.withdrawVaultType) + return <- DFBUtils.getEmptyVault(self.withdrawVaultType) } // take the lesser between the available and maximum requested amount let withdrawalAmount = available <= maxAmount ? available : maxAmount @@ -125,7 +126,7 @@ access(all) contract FungibleTokenStack { access(all) let vaultType: Type /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(contract) let uniqueID: DFB.UniqueIdentifier? /// An entitled Capability on the Vault from which withdrawals are sourced & deposit are routed access(self) let vault: Capability @@ -133,11 +134,11 @@ access(all) contract FungibleTokenStack { min: UFix64?, max: UFix64?, vault: Capability, - uniqueID: {DFB.UniqueIdentifier}? + uniqueID: DFB.UniqueIdentifier? ) { pre { vault.check(): "Invalid Vault Capability provided" - FungibleTokenStack.definingContractIsFungibleToken(vault.borrow()!.getType()): + DFBUtils.definingContractIsFungibleToken(vault.borrow()!.getType()): "The contract defining Vault \(vault.borrow()!.getType().identifier) does not conform to FungibleToken contract interface" min ?? 0.0 < max ?? UFix64.max: "Minimum balance must be less than maximum balance if either is declared" @@ -189,22 +190,7 @@ access(all) contract FungibleTokenStack { let finalAmount = vault.balance < maxAmount ? vault.balance : maxAmount return <-vault.withdraw(amount: finalAmount) } - return <- FungibleTokenStack.getEmptyVault(self.vaultType) + return <- DFBUtils.getEmptyVault(self.vaultType) } } - - /// Helper returning an empty Vault of the given Type assuming it is a FungibleToken Vault and it is defined by a - /// FungibleToken conforming contract - access(all) fun getEmptyVault(_ vaultType: Type): @{FungibleToken.Vault} { - return <- getAccount(vaultType.address!) - .contracts - .borrow<&{FungibleToken}>(name: vaultType.contractName!)! - .createEmptyVault(vaultType: vaultType) - } - - /// Checks that the contract defining vaultType conforms to the FungibleToken contract interface. This is required - /// to source empty Vaults in the event inner Capabilities become invalid - access(self) view fun definingContractIsFungibleToken(_ vaultType: Type): Bool { - return getAccount(vaultType.address!).contracts.borrow<&{FungibleToken}>(name: vaultType.contractName!) != nil - } } diff --git a/cadence/contracts/internal-dependencies/connectors/SwapStack.cdc b/cadence/contracts/internal-dependencies/connectors/SwapStack.cdc index c63e1f6c..d634c620 100644 --- a/cadence/contracts/internal-dependencies/connectors/SwapStack.cdc +++ b/cadence/contracts/internal-dependencies/connectors/SwapStack.cdc @@ -5,28 +5,28 @@ import "DFB" /// SwapStack /// -/// This contract defines StackFi Sink & Source connector implementations for use with DeFi protocols. These -/// connectors can be used alone or in conjunction with other StackFi connectors to create complex DeFi workflows. +/// This contract defines DeFiBlocks Sink & Source connector implementations for use with DeFi protocols. These +/// connectors can be used alone or in conjunction with other DeFiBlocks connectors to create complex DeFi workflows. /// access(all) contract SwapStack { - /// A simple implementation of DFB.Quote allowing callers of Swapper.amountIn() and .amountOut() to cache quoted + /// A simple implementation of DFB.Quote allowing callers of Swapper.quoteIn() and .quoteOut() to cache quoted /// amount in and/or out. /// access(all) struct BasicQuote : DFB.Quote { - access(all) let inVault: Type - access(all) let outVault: Type + access(all) let inType: Type + access(all) let outType: Type access(all) let inAmount: UFix64 access(all) let outAmount: UFix64 init( - inVault: Type, - outVault: Type, + inType: Type, + outType: Type, inAmount: UFix64, outAmount: UFix64 ) { - self.inVault = inVault - self.outVault = outVault + self.inType = inType + self.outType = outType self.inAmount = inAmount self.outAmount = outAmount } @@ -36,15 +36,15 @@ access(all) contract SwapStack { /// that should fulfill the Swap /// access(all) struct MultiSwapperQuote : DFB.Quote { - access(all) let inVault: Type - access(all) let outVault: Type + access(all) let inType: Type + access(all) let outType: Type access(all) let inAmount: UFix64 access(all) let outAmount: UFix64 access(all) let swapperIndex: Int init( - inVault: Type, - outVault: Type, + inType: Type, + outType: Type, inAmount: UFix64, outAmount: UFix64, swapperIndex: Int @@ -52,8 +52,8 @@ access(all) contract SwapStack { pre { swapperIndex >= 0: "Invalid swapperIndex - provided \(swapperIndex) is less than 0" } - self.inVault = inVault - self.outVault = outVault + self.inType = inType + self.outType = outType self.inAmount = inAmount self.outAmount = outAmount self.swapperIndex = swapperIndex @@ -66,7 +66,7 @@ access(all) contract SwapStack { /// access(all) struct MultiSwapper : DFB.Swapper { access(all) let swappers: [{DFB.Swapper}] - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(contract) let uniqueID: DFB.UniqueIdentifier? access(self) let inVault: Type access(self) let outVault: Type @@ -74,7 +74,7 @@ access(all) contract SwapStack { inVault: Type, outVault: Type, swappers: [{DFB.Swapper}], - uniqueID: {DFB.UniqueIdentifier}? + uniqueID: DFB.UniqueIdentifier? ) { pre { inVault.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()): @@ -83,10 +83,10 @@ access(all) contract SwapStack { "Invalid outVault type - \(outVault.identifier) is not a FungibleToken Vault implementation" } for swapper in swappers { - assert(swapper.inVaultType() == inVault, - message: "Mismatched inVault \(inVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.inVaultType().identifier)") - assert(swapper.outVaultType() == outVault, - message: "Mismatched outVault \(outVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.outVaultType().identifier)") + assert(swapper.inType() == inVault, + message: "Mismatched inVault \(inVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.inType().identifier)") + assert(swapper.outType() == outVault, + message: "Mismatched outVault \(outVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.outType().identifier)") } self.inVault = inVault self.outVault = outVault @@ -95,21 +95,21 @@ access(all) contract SwapStack { } /// The type of Vault this Swapper accepts when performing a swap - access(all) view fun inVaultType(): Type { + access(all) view fun inType(): Type { return self.inVault } /// The type of Vault this Swapper provides when performing a swap - access(all) view fun outVaultType(): Type { + access(all) view fun outType(): Type { return self.outVault } /// The estimated amount required to provide a Vault with the desired output balance - access(all) fun amountIn(forDesired: UFix64, reverse: Bool): {DFB.Quote} { + access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DFB.Quote} { let estimate = self._estimate(amount: forDesired, out: true, reverse: reverse) return MultiSwapperQuote( - inVault: reverse ? self.outVaultType() : self.inVaultType(), - outVault: reverse ? self.inVaultType() : self.outVaultType(), + inType: reverse ? self.outType() : self.inType(), + outType: reverse ? self.inType() : self.outType(), inAmount: estimate[1], outAmount: forDesired, swapperIndex: Int(estimate[0]) @@ -117,11 +117,11 @@ access(all) contract SwapStack { } /// The estimated amount delivered out for a provided input balance - access(all) fun amountOut(forProvided: UFix64, reverse: Bool): {DFB.Quote} { + access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DFB.Quote} { let estimate = self._estimate(amount: forProvided, out: true, reverse: reverse) return MultiSwapperQuote( - inVault: reverse ? self.outVaultType() : self.inVaultType(), - outVault: reverse ? self.inVaultType() : self.outVaultType(), + inType: reverse ? self.outType() : self.inType(), + outType: reverse ? self.inType() : self.outType(), inAmount: forProvided, outAmount: estimate[1], swapperIndex: Int(estimate[0]) @@ -151,8 +151,8 @@ access(all) contract SwapStack { for i, swapper in self.swappers { // call the appropriate estimator let estimate = out - ? swapper.amountOut(forProvided: amount, reverse: true).outAmount - : swapper.amountIn(forDesired: amount, reverse: true).inAmount + ? swapper.quoteOut(forProvided: amount, reverse: true).outAmount + : swapper.quoteIn(forDesired: amount, reverse: true).inAmount if (out ? res[1] < estimate : estimate < res[1]) { // take minimum for in, maximum for out res = [UFix64(i), estimate] @@ -166,7 +166,7 @@ access(all) contract SwapStack { access(self) fun _swap(quote: {DFB.Quote}?, from: @{FungibleToken.Vault}, reverse: Bool): @{FungibleToken.Vault} { var multiQuote = quote as? MultiSwapperQuote if multiQuote != nil || multiQuote!.swapperIndex > self.swappers.length { - multiQuote = self.amountOut(forProvided: from.balance, reverse: reverse) as! MultiSwapperQuote + multiQuote = self.quoteOut(forProvided: from.balance, reverse: reverse) as! MultiSwapperQuote } let optimalSwapper = &self.swappers[multiQuote!.swapperIndex] as &{DFB.Swapper} if reverse { @@ -177,18 +177,18 @@ access(all) contract SwapStack { } } - /// SwapSink StackFi connector that deposits the resulting post-conversion currency of a token swap to an inner - /// StackFi Sink, sourcing funds from a deposited Vault of a pre-set Type. + /// SwapSink DeFiBlocks connector that deposits the resulting post-conversion currency of a token swap to an inner + /// DeFiBlocks Sink, sourcing funds from a deposited Vault of a pre-set Type. /// access(all) struct SwapSink : DFB.Sink { access(self) let swapper: {DFB.Swapper} access(self) let sink: {DFB.Sink} - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(contract) let uniqueID: DFB.UniqueIdentifier? - init(swapper: {DFB.Swapper}, sink: {DFB.Sink}, uniqueID: {DFB.UniqueIdentifier}?) { + init(swapper: {DFB.Swapper}, sink: {DFB.Sink}, uniqueID: DFB.UniqueIdentifier?) { pre { - swapper.outVaultType() == sink.getSinkType(): - "Swapper outputs \(swapper.outVaultType().identifier) but Sink takes \(sink.getSinkType().identifier) - " + swapper.outType() == sink.getSinkType(): + "Swapper outputs \(swapper.outType().identifier) but Sink takes \(sink.getSinkType().identifier) - " .concat("Ensure the provided Swapper outputs a Vault Type compatible with the provided Sink") } self.swapper = swapper @@ -197,11 +197,11 @@ access(all) contract SwapStack { } access(all) view fun getSinkType(): Type { - return self.swapper.inVaultType() + return self.swapper.inType() } access(all) fun minimumCapacity(): UFix64 { - return self.swapper.amountIn(forDesired: self.sink.minimumCapacity(), reverse: false).inAmount + return self.swapper.quoteIn(forDesired: self.sink.minimumCapacity(), reverse: false).inAmount } access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { @@ -210,7 +210,7 @@ access(all) contract SwapStack { return // nothing to swap from, no capacity to ingest, invalid Vault type - do nothing } - let quote = self.swapper.amountIn(forDesired: from.balance, reverse: false) + let quote = self.swapper.quoteIn(forDesired: from.balance, reverse: false) let sinkLimit = quote.inAmount let swapVault <- from.createEmptyVault() @@ -234,18 +234,18 @@ access(all) contract SwapStack { } } - /// SwapSource StackFi connector that deposits the resulting post-conversion currency of a token swap to an inner - /// StackFi Sink, sourcing funds from a deposited Vault of a pre-set Type. + /// SwapSource DeFiBlocks connector that returns post-conversion currency, sourcing pre-converted funds from an inner + /// DeFiBlocks Source /// access(all) struct SwapSource : DFB.Source { access(self) let swapper: {DFB.Swapper} access(self) let source: {DFB.Source} - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(contract) let uniqueID: DFB.UniqueIdentifier? - init(swapper: {DFB.Swapper}, source: {DFB.Source}, uniqueID: {DFB.UniqueIdentifier}) { + init(swapper: {DFB.Swapper}, source: {DFB.Source}, uniqueID: DFB.UniqueIdentifier) { pre { - source.getSourceType() == swapper.inVaultType(): - "Source outputs \(source.getSourceType().identifier) but Swapper takes \(swapper.inVaultType().identifier) - " + source.getSourceType() == swapper.inType(): + "Source outputs \(source.getSourceType().identifier) but Swapper takes \(swapper.inType().identifier) - " .concat("Ensure the provided Source outputs a Vault Type compatible with the provided Swapper") } self.swapper = swapper @@ -254,14 +254,14 @@ access(all) contract SwapStack { } access(all) view fun getSourceType(): Type { - return self.swapper.outVaultType() + return self.swapper.outType() } access(all) fun minimumAvailable(): UFix64 { // estimate post-conversion currency based on the source's pre-conversion balance available let availableIn = self.source.minimumAvailable() return availableIn > 0.0 - ? self.swapper.amountOut(forProvided: availableIn, reverse: false).outAmount + ? self.swapper.quoteOut(forProvided: availableIn, reverse: false).outAmount : 0.0 } @@ -276,7 +276,7 @@ access(all) contract SwapStack { // find out how much liquidity to gather from the inner Source let availableIn = self.source.minimumAvailable() - let quote = self.swapper.amountIn(forDesired: amountOut, reverse: false) + let quote = self.swapper.quoteIn(forDesired: amountOut, reverse: false) let quoteIn = availableIn < quote.inAmount ? availableIn : quote.inAmount let sourceLiquidity <- self.source.withdrawAvailable(maxAmount: quoteIn) diff --git a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc b/cadence/contracts/internal-dependencies/interfaces/DFB.cdc index 9c9e582c..ec7ab5cb 100644 --- a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc +++ b/cadence/contracts/internal-dependencies/interfaces/DFB.cdc @@ -1,12 +1,16 @@ +import "Burner" +import "ViewResolver" import "FungibleToken" +import "DFBUtils" + /// DeFiBlocks Interfaces /// /// DeFiBlocks is a library of small DeFi components that act as glue to connect typical DeFi primitives (dexes, lending /// pools, farms) into individual aggregations. /// /// The core component of DeFiBlocks is the “Connector”; a conduit between the more complex pieces of the DeFi puzzle. -/// Connectors isn’t to do anything especially complex, but make it simple and straightforward to connect the +/// Connectors aren't to do anything especially complex, but make it simple and straightforward to connect the /// traditional DeFi pieces together into new, custom aggregations. /// /// Connectors should be thought of analogously with the small text processing tools of Unix that are mostly meant to be @@ -15,14 +19,18 @@ import "FungibleToken" /// access(all) contract DFB { + /* --- FIELDS --- */ + + /// The current ID assigned to UniqueIdentifiers as they are initialized + access(all) var currentID: UInt64 + /* --- INTERFACE-LEVEL EVENTS --- */ /// Emitted when value is deposited to a Sink access(all) event Deposited( type: String, amount: UFix64, - inUUID: UInt64, - uniqueIDType: String?, + fromUUID: UInt64, uniqueID: UInt64?, sinkType: String ) @@ -30,8 +38,7 @@ access(all) contract DFB { access(all) event Withdrawn( type: String, amount: UFix64, - outUUID: UInt64, - uniqueIDType: String?, + withdrawnUUID: UInt64, uniqueID: UInt64?, sourceType: String ) @@ -43,16 +50,98 @@ access(all) contract DFB { outAmount: UFix64, inUUID: UInt64, outUUID: UInt64, - uniqueIDType: String?, uniqueID: UInt64?, swapperType: String ) + /// Emitted when an AutoBalancer is created + access(all) event CreatedAutoBalancer( + lowerThreshold: UFix64, + upperThreshold: UFix64, + balancerUUID: UInt64, + vaultType: String, + vaultUUID: UInt64, + uniqueID: UInt64? + ) + /// Emitted when AutoBalancer.rebalance() is called + access(all) event Rebalanced( + amount: UFix64, + value: UFix64, + unitOfAccount: String, + isSurplus: Bool, + vaultUUID: UInt64, + balancerUUID: UInt64, + address: Address?, + uniqueID: UInt64? + ) + + /* --- PUBLIC METHODS --- */ + + /// Returns an AutoBalancer wrapping the provided Vault. + /// + /// @param oracle: The oracle used to query deposited & withdrawn value and to determine if a rebalance should execute + /// @param vault: The Vault wrapped by the AutoBalancer + /// @param rebalanceRange: The percentage range from the AutoBalancer's base value at which a rebalance is executed + /// @param outSink: An optional DeFiBlocks Sink to which excess value is directed when rebalancing + /// @param inSource: An optional DeFiBlocks Source from which value is withdrawn to the inner vault when rebalancing + /// @param uniqueID: An optional DeFiBlocks UniqueIdentifier used for identifying rebalance events + /// + access(all) fun createAutoBalancer( + oracle: {PriceOracle}, + vault: @{FungibleToken.Vault}, + lowerThreshold: UFix64, + upperThreshold: UFix64, + rebalanceSink: {Sink}?, + rebalanceSource: {Source}?, + uniqueID: UniqueIdentifier? + ): @AutoBalancer { + let vaultUUID = vault.uuid + let ab <- create AutoBalancer( + lower: lowerThreshold, + upper: upperThreshold, + oracle: oracle, + vault: <-vault, + outSink: rebalanceSink, + inSource: rebalanceSource, + uniqueID: uniqueID + ) + emit CreatedAutoBalancer( + lowerThreshold: lowerThreshold, + upperThreshold: upperThreshold, + balancerUUID: ab.uuid, + vaultType: ab.vaultType().identifier, + vaultUUID: vaultUUID, + uniqueID: ab.id() + ) + return <- ab + } + + /* --- CONSTRUCTS --- */ - /// This interface enables protocols to trace stack operations via the interface-level events, identifying their - /// UniqueIdentifier types and IDs. Implementations should ensure ID values are unique on initialization. + /// This construct enables protocols to trace stack operations via DFB interface-level events, identifying them by + /// UniqueIdentifier IDs. Implementations should ensure that access to them is encapsulated by the structures they + /// are used to identify. /// - access(all) struct interface UniqueIdentifier { + access(all) struct UniqueIdentifier { access(all) let id: UInt64 + + init() { + self.id = DFB.currentID + DFB.currentID = DFB.currentID + 1 + } + } + + /// A struct interface containing a UniqueIdentifier and convenience getter on the underlying ID value + /// + access(all) struct interface IdentifiableStruct { + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: UniqueIdentifier? + /// Convenience method returning the inner UniqueIdentifier's id or `nil` if none is set. + /// NOTE: This interface method may be spoofed if the function is overridden, so callers should not rely on it + /// for critical identification + access(all) view fun id(): UInt64? { + return self.uniqueID?.id + } } /// A Sink Connector (or just “Sink”) is analogous to the Fungible Token Receiver interface that accepts deposits of @@ -61,10 +150,7 @@ access(all) contract DFB { /// accept for deposit. Implementations should therefore avoid the possibility of reversion with graceful fallback /// on unexpected conditions, executing no-ops instead of reverting. /// - access(all) struct interface Sink { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: {UniqueIdentifier}? + access(all) struct interface Sink : IdentifiableStruct { /// Returns the Vault type accepted by this Sink access(all) view fun getSinkType(): Type /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity @@ -72,14 +158,14 @@ access(all) contract DFB { /// Deposits up to the Sink's capacity from the provided Vault access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { post { - emit Deposited( + DFB.emitDeposited( type: from.getType().identifier, - amount: before(from.balance) >= from.balance ? before(from.balance) - from.balance : 0.0, - inUUID: from.uuid, - uniqueIDType: self.uniqueID?.getType()?.identifier ?? nil, - uniqueID: self.uniqueID?.id ?? nil, + beforeBalance: before(from.balance), + afterBalance: from.balance, + fromUUID: from.uuid, + uniqueID: self.uniqueID?.id, sinkType: self.getType().identifier - ) + ): "Unknown error emitting DFB.Withdrawn from Sink \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED") } } } @@ -90,10 +176,7 @@ access(all) contract DFB { /// of funds available to be withdrawn. Implementations should therefore avoid the possibility of reversion with /// graceful fallback on unexpected conditions, executing no-ops or returning an empty Vault instead of reverting. /// - access(all) struct interface Source { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: {UniqueIdentifier}? + access(all) struct interface Source : IdentifiableStruct { /// Returns the Vault type provided by this Source access(all) view fun getSourceType(): Type /// Returns an estimate of how much of the associated Vault Type can be provided by this Source @@ -102,14 +185,13 @@ access(all) contract DFB { /// returned access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { post { - emit Withdrawn( + DFB.emitWithdrawn( type: result.getType().identifier, amount: result.balance, - outUUID: result.uuid, - uniqueIDType: self.uniqueID?.getType()?.identifier ?? nil, + withdrawnUUID: result.uuid, // toUUID uniqueID: self.uniqueID?.id ?? nil, - sourceType: self.getType().identifier - ) + sourceType: self.getType().identifier // maybe include + ): "Unknown error emitting DFB.Withdrawn from Source \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED") } } } @@ -120,9 +202,9 @@ access(all) contract DFB { /// access(all) struct interface Quote { /// The quoted pre-swap Vault type - access(all) let inVault: Type + access(all) let inType: Type /// The quoted post-swap Vault type - access(all) let outVault: Type + access(all) let outType: Type /// The quoted amount of pre-swap currency access(all) let inAmount: UFix64 /// The quoted amount of post-swap currency for the defined inAmount @@ -131,28 +213,24 @@ access(all) contract DFB { /// A basic interface for a struct that swaps between tokens. Implementations may choose to adapt this interface /// to fit any given swap protocol or set of protocols. - /// - access(all) struct interface Swapper { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: {UniqueIdentifier}? + access(all) struct interface Swapper : IdentifiableStruct { /// The type of Vault this Swapper accepts when performing a swap - access(all) view fun inVaultType(): Type + access(all) view fun inType(): Type /// The type of Vault this Swapper provides when performing a swap - access(all) view fun outVaultType(): Type + access(all) view fun outType(): Type /// The estimated amount required to provide a Vault with the desired output balance - access(all) fun amountIn(forDesired: UFix64, reverse: Bool): {Quote} + access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {Quote} // fun quoteIn/Out /// The estimated amount delivered out for a provided input balance - access(all) fun amountOut(forProvided: UFix64, reverse: Bool): {Quote} + access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {Quote} /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted /// to use multiple Flow swap protocols. access(all) fun swap(quote: {Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} { pre { - inVault.getType() == self.inVaultType(): - "Invalid vault provided for swap - \(inVault.getType().identifier) is not \(self.inVaultType().identifier)" - (quote?.inVault ?? inVault.getType()) == inVault.getType(): - "Quote.inVault type \(quote!.inVault.identifier) does not match the provided inVault \(inVault.getType().identifier)" + inVault.getType() == self.inType(): + "Invalid vault provided for swap - \(inVault.getType().identifier) is not \(self.inType().identifier)" + (quote?.inType ?? inVault.getType()) == inVault.getType(): + "Quote.inType type \(quote!.inType.identifier) does not match the provided inVault \(inVault.getType().identifier)" } post { emit Swapped( @@ -162,7 +240,6 @@ access(all) contract DFB { outAmount: result.balance, inUUID: before(inVault.uuid), outUUID: result.uuid, - uniqueIDType: self.uniqueID?.getType()?.identifier ?? nil, uniqueID: self.uniqueID?.id ?? nil, swapperType: self.getType().identifier ) @@ -171,12 +248,11 @@ access(all) contract DFB { /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted /// to use multiple Flow swap protocols. + // TODO: Impl detail - accept quote that was just used by swap() but reverse the direction assuming swap() was just called access(all) fun swapBack(quote: {Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} { pre { - residual.getType() == self.outVaultType(): - "Invalid vault provided for swapBack - \(residual.getType().identifier) is not \(self.outVaultType().identifier)" - (quote?.inVault ?? residual.getType()) == residual.getType(): - "Quote.inVault type \(quote!.inVault.identifier) does not match the provided inVault \(residual.getType().identifier)" + residual.getType() == self.outType(): + "Invalid vault provided for swapBack - \(residual.getType().identifier) is not \(self.outType().identifier)" } post { emit Swapped( @@ -186,7 +262,6 @@ access(all) contract DFB { outAmount: result.balance, inUUID: before(residual.uuid), outUUID: result.uuid, - uniqueIDType: self.uniqueID?.getType()?.identifier ?? nil, uniqueID: self.uniqueID?.id ?? nil, swapperType: self.getType().identifier ) @@ -199,7 +274,496 @@ access(all) contract DFB { access(all) struct interface PriceOracle { /// Returns the asset type serving as the price basis - e.g. USD in FLOW/USD access(all) view fun unitOfAccount(): Type - /// Returns the latest price data for a given asset denominated in unitOfAccount() - access(all) fun price(ofToken: Type): UFix64 + /// Returns the latest price data for a given asset denominated in unitOfAccount() if available, otherwise `nil` + /// should be returned. Callers should note that although an optional is supported, implementations may choose + /// to revert. + access(all) fun price(ofToken: Type): UFix64? + } + + /// A resource interface containing a UniqueIdentifier an convenience getters about it + /// + access(all) resource interface IdentifiableResource { + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: UniqueIdentifier? + /// Convenience method returning the inner UniqueIdentifier's id or `nil` if none is set. + /// NOTE: This interface method may be spoofed if the function is overridden, so callers should not rely on it + /// for critical identification + access(all) view fun id(): UInt64? { + return self.uniqueID?.id + } + } + + /// AutoBalancerSink + /// + /// A DeFiBlocks Sink enabling the deposit of funds to an underlying AutoBalancer resource. As written, this Source + /// may be used with externally defined AutoBalancer implementations + /// + access(all) struct AutoBalancerSink : Sink { + /// The Type this Sink accepts + access(self) let type: Type + /// An authorized Capability on the underlying AutoBalancer where funds are deposited + access(self) let autoBalancer: Capability<&AutoBalancer> + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: UniqueIdentifier? + + init(autoBalancer: Capability<&AutoBalancer>, uniqueID: UniqueIdentifier?) { + pre { + autoBalancer.check(): + "Invalid AutoBalancer Capability Provided" + } + self.type = autoBalancer.borrow()!.vaultType() + self.autoBalancer = autoBalancer + self.uniqueID = uniqueID + } + + /// Returns the Vault type accepted by this Sink + access(all) view fun getSinkType(): Type { + return self.type + } + /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity + access(all) fun minimumCapacity(): UFix64 { + if let ab = self.autoBalancer.borrow() { + return UFix64.max - ab.vaultBalance() + } + return 0.0 + } + /// Deposits up to the Sink's capacity from the provided Vault + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + if let ab = self.autoBalancer.borrow() { + ab.deposit(from: <- from.withdraw(amount: from.balance)) + } + return + } + } + + /// AutoBalancerSource + /// + /// A DeFiBlocks Source targeting an underlying AutoBalancer resource. As written, this Source may be used with + /// externally defined AutoBalancer implementations + /// + access(all) struct AutoBalancerSource : Source { + /// The Type this Source provides + access(self) let type: Type + /// An authorized Capability on the underlying AutoBalancer where funds are sourced + access(self) let autoBalancer: Capability + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: UniqueIdentifier? + + init(autoBalancer: Capability, uniqueID: UniqueIdentifier?) { + pre { + autoBalancer.check(): + "Invalid AutoBalancer Capability Provided" + } + self.type = autoBalancer.borrow()!.vaultType() + self.autoBalancer = autoBalancer + self.uniqueID = uniqueID + } + + /// Returns the Vault type provided by this Source + access(all) view fun getSourceType(): Type { + return self.type + } + /// Returns an estimate of how much of the associated Vault Type can be provided by this Source + access(all) fun minimumAvailable(): UFix64 { + if let ab = self.autoBalancer.borrow() { + return ab.vaultBalance() + } + return 0.0 + } + /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be + /// returned + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + if let ab = self.autoBalancer.borrow() { + return <-ab.withdraw( + amount: maxAmount <= ab.vaultBalance() ? maxAmount : ab.vaultBalance() + ) + } + return <- DFBUtils.getEmptyVault(self.type) + } + } + + /// Entitlement used by the AutoBalancer to set inner Sink and Source + access(all) entitlement Auto + access(all) entitlement Set + access(all) entitlement Get + + /// A resource interface designed to enable permissionless rebalancing of value around a wrapped Vault. An + /// AutoBalancer can be a critical component of DeFiBlocks stacks by allowing for strategies to compound, repay + /// loans or direct accumulated value to other sub-systems and/or user Vaults. + /// + access(all) resource AutoBalancer : IdentifiableResource, FungibleToken.Receiver, FungibleToken.Provider, ViewResolver.Resolver, Burner.Burnable { + /// The value in deposits & withdrawals over time denominated in oracle.unitOfAccount() + access(self) var _valueOfDeposits: UFix64 + /// The percentage low and high thresholds defining when a rebalance executes + access(self) var _rebalanceRange: [UFix64; 2] + /// Oracle used to track the baseValue for deposits & withdrawals over time + access(self) let _oracle: {PriceOracle} + /// The inner Vault's Type captured for the ResourceDestroyed event + access(self) let _vaultType: Type + /// Vault used to deposit & withdraw from made optional only so the Vault can be burned via Burner.burn() if the + /// AutoBalancer is burned and the Vault's burnCallback() can be called in the process + access(self) var _vault: @{FungibleToken.Vault}? + /// An optional Sink used to deposit excess funds from the inner Vault once the converted value exceeds the + /// rebalance range. This Sink may be used to compound yield into a position or direct excess value to an + /// external Vault + access(self) var _rebalanceSink: {Sink}? + /// An optional Source used to deposit excess funds to the inner Vault once the converted value is below the + /// rebalance range + access(self) var _rebalanceSource: {Source}? + /// Capability on this AutoBalancer instance + access(self) var _selfCap: Capability? + /// An optional UniqueIdentifier tying this AutoBalancer to a given stack + access(contract) let uniqueID: UniqueIdentifier? + + /// Emitted when the AutoBalancer is destroyed + access(all) event ResourceDestroyed( + uuid: UInt64 = self.uuid, + vaultType: String = self._vaultType.identifier, + balance: UFix64? = self._vault?.balance, + uniqueID: UInt64? = self.uniqueID?.id + ) + + init( + lower: UFix64, + upper: UFix64, + oracle: {PriceOracle}, + vault: @{FungibleToken.Vault}, + outSink: {Sink}?, + inSource: {Source}?, + uniqueID: UniqueIdentifier? + ) { + pre { + lower < upper && 0.01 <= lower && lower < 1.0 && 1.0 < upper && upper < 2.0: + "Invalid rebalanceRange [lower, upper]: [\(lower), \(upper)] - thresholds must be set such that 0.01 <= lower < 1.0 and 1.0 < upper < 2.0 relative to value of deposits" + vault.balance == 0.0: + "Vault \(vault.getType().identifier) has a non-zero balance - AutoBalancer must be initialized with an empty Vault" + DFBUtils.definingContractIsFungibleToken(vault.getType()): + "The contract defining Vault \(vault.getType().identifier) does not conform to FungibleToken contract interface" + } + assert(oracle.price(ofToken: vault.getType()) != nil, + message: "Provided Oracle \(oracle.getType().identifier) could not provide a price for vault \(vault.getType().identifier)") + self._valueOfDeposits = 0.0 + self._rebalanceRange = [lower, upper] + self._oracle = oracle + self._vault <- vault + self._vaultType = self._vault.getType() + self._rebalanceSink = outSink + self._rebalanceSource = inSource + self._selfCap = nil + self.uniqueID = uniqueID + } + + /* Core AutoBalancer Functionality */ + + /// Returns the balance of the inner Vault + /// + /// @return the current balance of the inner Vault + /// + access(all) view fun vaultBalance(): UFix64 { + return self._borrowVault().balance + } + + /// Returns the Type of the inner Vault + /// + /// @return the Type of the inner Vault + /// + access(all) view fun vaultType(): Type { + return self._borrowVault().getType() + } + + /// Returns the low and high rebalance thresholds as a fixed length UFix64 containing [low, high] + /// + /// @return a sorted fixed-length array containing the relative lower and upper thresholds conditioning + /// rebalance execution + /// + access(all) view fun rebalanceThresholds(): [UFix64; 2] { + return self._rebalanceRange + } + + /// Returns the value of all accounted deposits/withdraws as they have occurred denominated in unitOfAccount. + /// The returned value is the value as tracked historically, not necessarily the current value of the inner + /// Vault's balance. + /// + /// @return the historical value of deposits + /// + access(all) view fun valueOfDeposits(): UFix64 { + return self._valueOfDeposits + } + + /// Returns the token Type serving as the price basis of this AutoBalancer + /// + /// @return the price denomination of value of the underlying vault as returned from the inner PriceOracle + /// + access(all) view fun unitOfAccount(): Type { + return self._oracle.unitOfAccount() + } + + /// Returns the current value of the inner Vault's balance. If a price is not available from the AutoBalancer's + /// PriceOracle, `nil` is returned + /// + /// @return the current value of the inner's Vault's balance denominated in unitOfAccount() if a price is + /// available, `nil` otherwise + /// + access(all) fun currentValue(): UFix64? { + if let price = self._oracle.price(ofToken: self.vaultType()) { + return price * self._borrowVault().balance + } + return nil + } + + /// Convenience method issuing a Sink allowing for deposits to this AutoBalancer. If the AutoBalancer's + /// Capability on itself is not set or is invalid, `nil` is returned. + /// + /// @return a Sink routing deposits to this AutoBalancer + /// + access(all) fun createBalancerSink(): {Sink}? { + if self._selfCap == nil || !self._selfCap!.check() { + return nil + } + return AutoBalancerSink(autoBalancer: self._selfCap!, uniqueID: self.uniqueID) + } + + /// Convenience method issuing a Source enabling withdrawals from this AutoBalancer. If the AutoBalancer's + /// Capability on itself is not set or is invalid, `nil` is returned. + /// + /// @return a Source routing withdrawals from this AutoBalancer + /// + access(Get) fun createBalancerSource(): {Source}? { + if self._selfCap == nil || !self._selfCap!.check() { + return nil + } + return AutoBalancerSource(autoBalancer: self._selfCap!, uniqueID: self.uniqueID) + } + + /// A setter enabling an AutoBalancer to set a Sink to which overflow value should be deposited + /// + /// @param sink: The optional Sink DeFiBlocks connector from which funds are sourced when this AutoBalancer + /// current value rises above the upper threshold relative to its valueOfDeposits(). If `nil`, overflown + /// value will not rebalance + /// + access(Set) fun setSink(_ sink: {Sink}?) { + self._rebalanceSink = sink + } + + /// A setter enabling an AutoBalancer to set a Source from which underflow value should be withdrawn + /// + /// @param source: The optional Source DeFiBlocks connector from which funds are sourced when this AutoBalancer + /// current value falls below the lower threshold relative to its valueOfDeposits(). If `nil`, underflown + /// value will not rebalance + /// + access(Set) fun setSource(_ source: {Source}?) { + self._rebalanceSource = source + } + + /// Enables the setting of a Capability on the AutoBalancer for the distribution of Sinks & Sources targeting + /// the AutoBalancer instance. Due to the mechanisms of Capabilities, this must be done after the AutoBalancer + /// has been saved to account storage and an authorized Capability has been issued. + access(Set) fun setSelfCapability(_ cap: Capability) { + pre { + self._selfCap == nil || self._selfCap!.check() != true: + "Internal AutoBalancer Capability has been set and is still valid - cannot be re-assigned" + cap.check(): "Invalid AutoBalancer Capability provided" + self.getType() == cap.borrow()!.getType() && self.uuid == cap.borrow()!.uuid: + "Provided Capability does not target this AutoBalancer of type \(self.getType().identifier) with UUID \(self.uuid) - " + .concat("provided Capability for AutoBalancer of type \(cap.borrow()!.getType().identifier) with UUID \(cap.borrow()!.uuid)") + } + self._selfCap = cap + } + + /// Sets the rebalance range of this AutoBalancer + /// + /// @param range: a sorted array containing lower and upper thresholds that condition rebalance execution. The + /// thresholds must be values such that 0.01 <= range[0] < 1.0 && 1.0 < range[1] < 2.0 + /// + access(Set) fun setRebalanceRange(_ range: [UFix64; 2]) { + pre { + range[0] < range[1] && 0.01 <= range[0] && range[0] < 1.0 && 1.0 < range[1] && range[1] < 2.0: + "Invalid rebalanceRange [lower, upper]: [\(range[0]), \(range[1])] - thresholds must be set such that 0.01 <= range[0] < 1.0 and 1.0 < range[1] < 2.0 relative to value of deposits" + } + self._rebalanceRange = range + } + + /// Allows for external parties to call on the AutoBalancer and execute a rebalance according to it's rebalance + /// parameters. This method must be called by external party regularly in order for rebalancing to occur. + /// + /// @param force: if false, rebalance will occur only when beyond upper or lower thresholds; if true, rebalance + /// will execute as long as a price is available via the oracle and the current value is non-zero + /// + access(Auto) fun rebalance(force: Bool) { + let currentPrice = self._oracle.price(ofToken: self._vaultType) + if currentPrice == nil { + return // no price available -> do nothing + } + let currentValue = self.currentValue()! + // calculate the difference between the current value and the historical value of deposits + var valueDiff: UFix64 = currentValue < self._valueOfDeposits ? self._valueOfDeposits - currentValue : currentValue - self._valueOfDeposits + // if deficit detected, choose lower threshold, otherwise choose upper threshold + let isDeficit = self._valueOfDeposits < currentValue + let threshold = isDeficit ? self._rebalanceRange[0] : self._rebalanceRange[1] + if currentPrice == 0.0 || valueDiff == 0.0 || ((valueDiff / self._valueOfDeposits) < threshold && !force) { + // division by zero, no difference, or difference does not exceed rebalance ratio & not forced -> no-op + return + } + + let vault = self._borrowVault() + var amount = valueDiff / currentPrice! + var executed = false + if isDeficit && self._rebalanceSource != nil { + // rebalance back up to baseline sourcing funds from _rebalanceSource + vault.deposit(from: <- self._rebalanceSource!.withdrawAvailable(maxAmount: amount)) + executed = true + } else if !isDeficit && self._rebalanceSink != nil { + // rebalance back down to baseline depositing excess to _rebalanceSink + if amount > vault.balance { + amount = vault.balance // protect underflow + } + let surplus <- vault.withdraw(amount: amount) + self._rebalanceSink!.depositCapacity(from: &surplus as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + executed = true + if surplus.balance == 0.0 { + Burner.burn(<-surplus) // could destroy + } else { + amount = amount - surplus.balance // update the rebalanced amount + valueDiff = valueDiff - (surplus.balance * currentPrice!) // update the value difference + vault.deposit(from: <-surplus) // deposit any excess not taken by the Sink + } + } + // emit event only if rebalance was executed + if executed { + emit Rebalanced( + amount: amount, + value: valueDiff, + unitOfAccount: self.unitOfAccount().identifier, + isSurplus: !isDeficit, + vaultUUID: self._borrowVault().uuid, + balancerUUID: self.uuid, + address: self.owner?.address, + uniqueID: self.id() + ) + } + } + + /* ViewResolver.Resolver conformance */ + + /// Passthrough to inner Vault's view Types + access(all) view fun getViews(): [Type] { + return self._borrowVault().getViews() + } + + /// Passthrough to inner Vault's view resolution + access(all) fun resolveView(_ view: Type): AnyStruct? { + return self._borrowVault().resolveView(view) + } + + /* FungibleToken.Receiver & .Provider conformance */ + + /// Only the nested Vault type is supported by this AutoBalancer for deposits & withdrawal for the sake of + /// single asset accounting + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + return { self.vaultType(): true } + } + + /// True if the provided Type is the nested Vault Type, false otherwise + access(all) view fun isSupportedVaultType(type: Type): Bool { + return self.getSupportedVaultTypes()[type] == true + } + + /// Passthrough to the inner Vault's isAvailableToWithdraw() method + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return self._borrowVault().isAvailableToWithdraw(amount: amount) + } + + /// Deposits the provided Vault to the nested Vault if it is of the same Type, reverting otherwise. In the + /// process, the current value of the deposited amount (denominated in unitOfAccount) increments the + /// AutoBalancer's baseValue. If a price is not available via the internal PriceOracle, an average price is + /// calculated base on the inner vault balance & valueOfDeposits and valueOfDeposits is incremented by the + /// value of the deposited vault on the basis of that average + access(all) fun deposit(from: @{FungibleToken.Vault}) { + pre { + from.getType() == self.vaultType(): + "Invalid Vault type \(from.getType().identifier) deposited - this AutoBalancer only accepts \(self.vaultType().identifier)" + } + // if no price available use an average price based on historical value of deposits and inner vault balance + let price = self._oracle.price(ofToken: from.getType()) ?? (self.valueOfDeposits() / self.vaultBalance()) + self._valueOfDeposits = self._valueOfDeposits + (from.balance * price) + self._borrowVault().deposit(from: <-from) + } + + /// Returns the requested amount of the nested Vault type, reducing the baseValue by the current value + /// (denominated in unitOfAccount) of the token amount. The AutoBalancer's valueOfDeposits is decremented + /// in proportion to the amount withdrawn relative to the inner Vault's balance + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { + // adjust historical value of deposits proportionate to the amount withdrawn & return withdrawn vault + let adjustment = (1.0 - amount / self.vaultBalance()) * self._valueOfDeposits + self._valueOfDeposits = adjustment <= self._valueOfDeposits ? self._valueOfDeposits - adjustment : 0.0 + return <- self._borrowVault().withdraw(amount: amount) + } + + /* Burnable.Burner conformance */ + + /// Executed in Burner.burn(). Passes along the inner vault to be burned, executing the inner Vault's + /// burnCallback() logic + access(contract) fun burnCallback() { + let vault <- self._vault <- nil + Burner.burn(<-vault) // executes the inner Vault's burnCallback() + } + + /* Internal */ + + access(self) view fun _borrowVault(): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} { + return (&self._vault)! + } + } + + /* --- INTERNAL CONDITIONAL EVENT EMITTERS --- */ + + /// Emits Deposited event if a change in balance is detected + access(self) view fun emitDeposited( + type: String, + beforeBalance: UFix64, + afterBalance: UFix64, + fromUUID: UInt64, + uniqueID: UInt64?, + sinkType: String + ): Bool { + if beforeBalance == afterBalance { + return true + } + emit Deposited( + type: type, + amount: beforeBalance > afterBalance ? beforeBalance - afterBalance : afterBalance - beforeBalance, + fromUUID: fromUUID, + uniqueID: uniqueID, + sinkType: sinkType + ) + return true + } + + /// Emits Withdrawn event if a change in balance is detected + access(self) view fun emitWithdrawn( + type: String, + amount: UFix64, + withdrawnUUID: UInt64, + uniqueID: UInt64?, + sourceType: String + ): Bool { + if amount == 0.0 { + return true + } + emit Withdrawn( + type: type, + amount: amount, + withdrawnUUID: withdrawnUUID, + uniqueID: uniqueID, + sourceType: sourceType + ) + return true + } + + init() { + self.currentID = 0 } } diff --git a/cadence/contracts/internal-dependencies/utils/DFBUtils.cdc b/cadence/contracts/internal-dependencies/utils/DFBUtils.cdc new file mode 100644 index 00000000..57460ed6 --- /dev/null +++ b/cadence/contracts/internal-dependencies/utils/DFBUtils.cdc @@ -0,0 +1,45 @@ +import "FungibleToken" + +/// DFBUtils +/// +/// Utility methods commonly used across DeFiBlocks (DFB) related contracts +/// +access(all) contract DFBUtils { + + /// Checks that the contract defining vaultType conforms to the FungibleToken contract interface. This is required + /// to source empty Vaults in the event inner Capabilities become invalid + /// + /// @param vaultType: The Type of the Vault in question + /// + /// @return true if the Type a Vault and is defined by a FungibleToken contract, false otherwise + /// + access(all) view fun definingContractIsFungibleToken(_ vaultType: Type): Bool { + if !vaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + return false + } + return getAccount(vaultType.address!).contracts.borrow<&{FungibleToken}>(name: vaultType.contractName!) != nil + } + + /// Returns an empty Vault of the given Type. Reverts if the provided Type is not defined by a FungibleToken + /// or if the returned Vault is not of the requested Type. Callers can use .definingContractIsFungibleToken() + /// to check the type before calling if they would like to prevent reverting. + /// + /// @param vaultType: The Type of the Vault to return as an empty Vault + /// + /// @return an empty Vault of the requested Type + /// + access(all) fun getEmptyVault(_ vaultType: Type): @{FungibleToken.Vault} { + pre { + self.definingContractIsFungibleToken(vaultType): + "Invalid vault Type \(vaultType.identifier) requested - cannot fulfill an empty Vault of an invalid type" + } + post { + result.getType() == vaultType: + "Invalid Vault returned - expected \(vaultType.identifier) but returned \(result.getType().identifier)" + } + return <- getAccount(vaultType.address!) + .contracts + .borrow<&{FungibleToken}>(name: vaultType.contractName!)! + .createEmptyVault(vaultType: vaultType) + } +} diff --git a/flow.json b/flow.json index b03a748e..e7583d78 100644 --- a/flow.json +++ b/flow.json @@ -6,6 +6,12 @@ "testing": "0000000000000007" } }, + "DFBUtils": { + "source": "cadence/contracts/internal-dependencies/utils/DFBUtils.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "FungibleTokenStack": { "source": "cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc", "aliases": { @@ -135,6 +141,7 @@ "deployments": { "emulator": { "emulator-account": [ + "DFBUtils", "DFB", "FungibleTokenStack", "SwapStack", From 987fd7d9250252a39b8a55547c35189db5ee4ddd Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 28 May 2025 15:04:04 -0700 Subject: [PATCH 18/80] remove Tidal.UniqueID definition in favor of DFB.UniqueIdentifier --- cadence/contracts/Tidal.cdc | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/cadence/contracts/Tidal.cdc b/cadence/contracts/Tidal.cdc index 68315ecb..1ddf659f 100644 --- a/cadence/contracts/Tidal.cdc +++ b/cadence/contracts/Tidal.cdc @@ -19,29 +19,19 @@ access(all) contract Tidal { access(all) event AddedToManager(id: UInt64, idType: String, owner: Address?, managerUUID: UInt64) access(all) event BurnedTide(id: UInt64, idType: String, remainingBalance: UFix64) - access(self) var currentIdentifier: UInt64 - access(all) fun createTideManager(): @TideManager { return <-create TideManager() } /* --- CONSTRUCTS --- */ - access(all) struct UniqueID : DFB.UniqueIdentifier { - access(all) let id: UInt64 - init() { - self.id = Tidal.currentIdentifier - Tidal.currentIdentifier = Tidal.currentIdentifier + 1 - } - } - access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, FungibleToken.Provider, ViewResolver.Resolver { - access(contract) let uniqueID: {DFB.UniqueIdentifier} + access(contract) let uniqueID: DFB.UniqueIdentifier access(self) let vault: @{FungibleToken.Vault} init(_ vault: @{FungibleToken.Vault}) { self.vault <- vault - self.uniqueID = UniqueID() + self.uniqueID = DFB.UniqueIdentifier() } access(all) view fun id(): UInt64 { @@ -182,7 +172,5 @@ access(all) contract Tidal { let pathIdentifier = "TidalTideManager_\(self.account.address)" self.TideManagerStoragePath = StoragePath(identifier: pathIdentifier)! self.TideManagerPublicPath = PublicPath(identifier: pathIdentifier)! - - self.currentIdentifier = 0 } } From c62b6d052ac9f3c523cda66c69ae42eac5e6a530 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 28 May 2025 15:05:48 -0700 Subject: [PATCH 19/80] update mocked PriceOracle to return optional on price() --- cadence/contracts/mocks/MockOracle.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/contracts/mocks/MockOracle.cdc b/cadence/contracts/mocks/MockOracle.cdc index 8441ab6c..ac771760 100644 --- a/cadence/contracts/mocks/MockOracle.cdc +++ b/cadence/contracts/mocks/MockOracle.cdc @@ -23,11 +23,11 @@ access(all) contract MockOracle { } /// Returns the latest price data for a given asset denominated in unitOfAccount() - access(all) fun price(ofToken: Type): UFix64 { + access(all) fun price(ofToken: Type): UFix64? { if ofToken == self.unitOfAccount() { return 1.0 } - return MockOracle.mockedPrices[ofToken] ?? panic("Unsupported token type \(ofToken.identifier)") + return MockOracle.mockedPrices[ofToken] } } From 1d14d0578363ed83cc3a4be0d6196853fae048d1 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 28 May 2025 15:22:50 -0700 Subject: [PATCH 20/80] update MockSwapper based on updated DFB interfaces --- cadence/contracts/mocks/MockSwapper.cdc | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cadence/contracts/mocks/MockSwapper.cdc b/cadence/contracts/mocks/MockSwapper.cdc index b7e1db25..ccf3179e 100644 --- a/cadence/contracts/mocks/MockSwapper.cdc +++ b/cadence/contracts/mocks/MockSwapper.cdc @@ -29,12 +29,12 @@ access(all) contract MockSwapper { /// Mocked DeFiBlocks Swapper implementation. Be sure to set connectors for Vaults you wish to handle via this mock /// in MockSwapper.liquidityConnectors via .setLiquidityConnector before instantiating mocks access(all) struct Swapper : DFB.Swapper { - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + access(contract) let uniqueID: DFB.UniqueIdentifier? access(self) let inVault: Type access(self) let outVault: Type access(self) let oracle: {DFB.PriceOracle} - init(inVault: Type, outVault: Type, uniqueID: {DFB.UniqueIdentifier}?) { + init(inVault: Type, outVault: Type, uniqueID: DFB.UniqueIdentifier?) { pre { MockSwapper.liquidityConnectors[inVault] != nil: "Invalid inVault - \(inVault.identifier) does not have a MockSwapper connector to handle funds" @@ -48,23 +48,23 @@ access(all) contract MockSwapper { } /// The type of Vault this Swapper accepts when performing a swap - access(all) view fun inVaultType(): Type { + access(all) view fun inType(): Type { return self.inVault } /// The type of Vault this Swapper provides when performing a swap - access(all) view fun outVaultType(): Type { + access(all) view fun outType(): Type { return self.outVault } /// The estimated amount required to provide a Vault with the desired output balance, sourcing pricing from the /// mocked oracle - access(all) fun amountIn(forDesired: UFix64, reverse: Bool): {DFB.Quote} { + access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DFB.Quote} { return self._estimate(amount: forDesired, out: false, reverse: reverse) } /// The estimated amount delivered out for a provided input balance, sourcing pricing from the mocked oracle - access(all) fun amountOut(forProvided: UFix64, reverse: Bool): {DFB.Quote} { + access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DFB.Quote} { return self._estimate(amount: forProvided, out: true, reverse: reverse) } @@ -88,12 +88,14 @@ access(all) contract MockSwapper { /// Internal estimator returning a quote for the amount in/out and in the desired direction access(self) fun _estimate(amount: UFix64, out: Bool, reverse: Bool): {DFB.Quote} { - let price = reverse - ? self.oracle.price(ofToken: self.outVaultType()) / self.oracle.price(ofToken: self.inVaultType()) - : self.oracle.price(ofToken: self.inVaultType()) / self.oracle.price(ofToken: self.outVaultType()) + let outTokenPrice = self.oracle.price(ofToken: self.outType()) + ?? panic("Price for token \(self.outType().identifier) is currently unavailable") + let inTokenPrice = self.oracle.price(ofToken: self.inType()) + ?? panic("Price for token \(self.inType().identifier) is currently unavailable") + let price = reverse ? outTokenPrice / inTokenPrice : inTokenPrice / outTokenPrice return SwapStack.BasicQuote( - inVault: reverse ? self.outVaultType() : self.inVaultType(), - outVault: reverse ? self.inVaultType() : self.outVaultType(), + inType: reverse ? self.outType() : self.inType(), + outType: reverse ? self.inType() : self.outType(), inAmount: out ? amount : amount / price, outAmount: out ? amount * price : amount ) @@ -101,13 +103,13 @@ access(all) contract MockSwapper { access(self) fun _swap(_ from: @{FungibleToken.Vault}, reverse: Bool): @{FungibleToken.Vault} { let inAmount = from.balance - var swapInVault = reverse ? MockSwapper.liquidityConnectors[from.getType()]! : MockSwapper.liquidityConnectors[self.inVaultType()]! - var swapOutVault = reverse ? MockSwapper.liquidityConnectors[self.inVaultType()]! : MockSwapper.liquidityConnectors[self.outVaultType()]! + var swapInVault = reverse ? MockSwapper.liquidityConnectors[from.getType()]! : MockSwapper.liquidityConnectors[self.inType()]! + var swapOutVault = reverse ? MockSwapper.liquidityConnectors[self.inType()]! : MockSwapper.liquidityConnectors[self.outType()]! swapInVault.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) Burner.burn(<-from) - let outAmount = self.amountOut(forProvided: inAmount, reverse: reverse).outAmount + let outAmount = self.quoteOut(forProvided: inAmount, reverse: reverse).outAmount var outVault <- swapOutVault.withdrawAvailable(maxAmount: outAmount) assert(outVault.balance == outAmount, From 2e83803ab267d813c7c141ef334ef01843ad1aeb Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 28 May 2025 17:50:00 -0700 Subject: [PATCH 21/80] add initial strategy framing --- cadence/contracts/Tidal.cdc | 46 +++++++++------- cadence/contracts/TidalStrategies.cdc | 78 +++++++++++++++++++++++++++ flow.json | 7 +++ 3 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 cadence/contracts/TidalStrategies.cdc diff --git a/cadence/contracts/Tidal.cdc b/cadence/contracts/Tidal.cdc index 1ddf659f..133ed8ba 100644 --- a/cadence/contracts/Tidal.cdc +++ b/cadence/contracts/Tidal.cdc @@ -3,6 +3,7 @@ import "Burner" import "ViewResolver" import "DFB" +import "TidalStrategies" /// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION @@ -25,25 +26,32 @@ access(all) contract Tidal { /* --- CONSTRUCTS --- */ - access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, FungibleToken.Provider, ViewResolver.Resolver { + access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, ViewResolver.Resolver { access(contract) let uniqueID: DFB.UniqueIdentifier - access(self) let vault: @{FungibleToken.Vault} + access(self) let strategy: {TidalStrategies.Strategy} - init(_ vault: @{FungibleToken.Vault}) { - self.vault <- vault + init(strategyNumber: UInt64, withVault: @{FungibleToken.Vault}) { + pre { + TidalStrategies.isSupportedCollateralType(withVault.getType(), forStrategy: strategyNumber) == true: + "Provided vault of type \(withVault.getType().identifier) is unsupported collateral Type for strategy \(strategyNumber)" + } self.uniqueID = DFB.UniqueIdentifier() + let vaultType = withVault.getType() + self.strategy = TidalStrategies.createStrategy(number: strategyNumber, vault: <-withVault) + assert(self.strategy.getSinkType() == vaultType, message: "TODO") + assert(self.strategy.getSourceType() == vaultType, message: "TODO") } access(all) view fun id(): UInt64 { return self.uniqueID.id } - access(all) view fun getTideBalance(): UFix64 { - return self.vault.balance + access(all) fun getTideBalance(): UFix64 { + return self.strategy.minimumAvailable() } access(contract) fun burnCallback() { - emit BurnedTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier,remainingBalance: self.vault.balance) + emit BurnedTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, remainingBalance: self.getTideBalance()) } access(all) view fun getViews(): [Type] { @@ -60,28 +68,29 @@ access(all) contract Tidal { "Deposited vault of type \(from.getType().identifier) is not supported by this Tide" } emit DepositedToTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: from.balance, owner: self.owner?.address, fromUUID: from.uuid) - self.vault.deposit(from: <-from) + self.strategy.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + assert(from.balance == 0.0, message: "TODO") + Burner.burn(<-from) } access(all) view fun getSupportedVaultTypes(): {Type: Bool} { - return { self.vault.getType() : true } + return { self.strategy.getSinkType(): true } } - access(all) view fun isSupportedVaultType(type: Type): Bool { return self.getSupportedVaultTypes()[type] ?? false } - access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { - return amount <= self.vault.balance - } - access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { - pre { - self.isAvailableToWithdraw(amount: amount): - "Requested amount \(amount) is greater than withdrawable balance of \(self.vault.balance)" + post { + result.balance == amount: "TODO" } - let res <- self.vault.withdraw(amount: amount) + let available = self.strategy.minimumAvailable() + assert( + amount <= available, + message: "Requested amount \(amount) is greater than withdrawable balance of \(available)" + ) + let res <- self.strategy.withdrawAvailable(maxAmount: amount) emit WithdrawnFromTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: amount, owner: self.owner?.address, toUUID: res.uuid) return <- res } @@ -172,5 +181,6 @@ access(all) contract Tidal { let pathIdentifier = "TidalTideManager_\(self.account.address)" self.TideManagerStoragePath = StoragePath(identifier: pathIdentifier)! self.TideManagerPublicPath = PublicPath(identifier: pathIdentifier)! + } } diff --git a/cadence/contracts/TidalStrategies.cdc b/cadence/contracts/TidalStrategies.cdc new file mode 100644 index 00000000..fd1fa849 --- /dev/null +++ b/cadence/contracts/TidalStrategies.cdc @@ -0,0 +1,78 @@ +import "FungibleToken" + +import "DFB" + +access(all) contract TidalStrategies { + + access(self) let strategies: {UInt64: {Strategy}} + + access(all) view fun getCollateralTypes(forStrategy: UInt64): [Type]? { + return self.strategies[forStrategy]?.getSupportedCollateralTypes() ?? nil + } + + access(all) view fun isSupportedCollateralType(_ type: Type, forStrategy: UInt64): Bool? { + return self.strategies[forStrategy]?.isSupportedCollateralType(type) ?? nil + } + + access(all) fun createStrategy(number: UInt64, vault: @{FungibleToken.Vault}): {Strategy} { + destroy vault // TODO: Update vault handling + return DummyStrategy(id: DFB.UniqueIdentifier()) + } + + access(all) struct interface Strategy : DFB.Sink, DFB.Source { + access(all) view fun getSupportedCollateralTypes(): [Type] + access(all) view fun isSupportedCollateralType(_ type: Type): Bool + } + + access(all) struct DummyStrategy : Strategy { + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(self) var sink: {DFB.Sink}? // TODO: update from optional + access(self) var source: {DFB.Source}? // TODO: update from optional + + init(id: DFB.UniqueIdentifier?) { + self.uniqueID = id + self.sink = nil + self.source = nil + } + + access(all) view fun getSupportedCollateralTypes(): [Type] { + return [] // TODO: tmp + } + + access(all) view fun isSupportedCollateralType(_ type: Type): Bool { + return false // TODO: tmp + } + + /// Returns the Vault type accepted by this Sink + access(all) view fun getSinkType(): Type { + return self.sink!.getSinkType() + } + /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity + access(all) fun minimumCapacity(): UFix64 { + return self.sink!.minimumCapacity() + } + /// Deposits up to the Sink's capacity from the provided Vault + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + self.sink!.depositCapacity(from: from) + } + /// Returns the Vault type provided by this Source + access(all) view fun getSourceType(): Type { + return self.source!.getSourceType() + } + /// Returns an estimate of how much of the associated Vault Type can be provided by this Source + access(all) fun minimumAvailable(): UFix64 { + return self.source!.minimumAvailable() + } + /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be + /// returned + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + return <- self.source!.withdrawAvailable(maxAmount: maxAmount) + } + } + + init() { + self.strategies = {} + } +} \ No newline at end of file diff --git a/flow.json b/flow.json index e7583d78..c7e18441 100644 --- a/flow.json +++ b/flow.json @@ -42,6 +42,12 @@ "testing": "0000000000000008" } }, + "TidalStrategies": { + "source": "cadence/contracts/TidalStrategies.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "USDA": { "source": "cadence/contracts/internal-dependencies/tokens/USDA.cdc", "aliases": { @@ -164,6 +170,7 @@ ] }, "MockSwapper", + "TidalStrategies", "Tidal" ] } From 648141f8ef760453ad5b054c04334300a777cb50 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:19:31 -0600 Subject: [PATCH 22/80] add initial strategy constructor contract --- cadence/contracts/TidalStrategies.cdc | 12 +- .../internal-dependencies/AlpenFlow.cdc | 695 ++++++++++++++++++ flow.json | 7 + 3 files changed, 711 insertions(+), 3 deletions(-) create mode 100644 cadence/contracts/internal-dependencies/AlpenFlow.cdc diff --git a/cadence/contracts/TidalStrategies.cdc b/cadence/contracts/TidalStrategies.cdc index fd1fa849..01d45272 100644 --- a/cadence/contracts/TidalStrategies.cdc +++ b/cadence/contracts/TidalStrategies.cdc @@ -18,12 +18,18 @@ access(all) contract TidalStrategies { destroy vault // TODO: Update vault handling return DummyStrategy(id: DFB.UniqueIdentifier()) } + + access(all) struct interface StrategyBuilder { + access(all) fun createStrategy(funds: @{FungibleToken.Vault}): {Strategy} + } - access(all) struct interface Strategy : DFB.Sink, DFB.Source { + access(all) struct interface StrategyInfo { access(all) view fun getSupportedCollateralTypes(): [Type] access(all) view fun isSupportedCollateralType(_ type: Type): Bool } + access(all) struct Strategy : StrategyInfo, DFB.Sink, DFB.Source {} + access(all) struct DummyStrategy : Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction @@ -38,11 +44,11 @@ access(all) contract TidalStrategies { } access(all) view fun getSupportedCollateralTypes(): [Type] { - return [] // TODO: tmp + return [self.sink!.getSinkType()] } access(all) view fun isSupportedCollateralType(_ type: Type): Bool { - return false // TODO: tmp + return self.sink!.getSinkType() == type } /// Returns the Vault type accepted by this Sink diff --git a/cadence/contracts/internal-dependencies/AlpenFlow.cdc b/cadence/contracts/internal-dependencies/AlpenFlow.cdc new file mode 100644 index 00000000..a57bbafa --- /dev/null +++ b/cadence/contracts/internal-dependencies/AlpenFlow.cdc @@ -0,0 +1,695 @@ +import "FungibleToken" +import "ViewResolver" +import "Burner" +import "MetadataViews" +import "FungibleTokenMetadataViews" +import "DFB" +// CHANGE: Import FlowToken to use the real FLOW token implementation +// This replaces our test FlowVault with the actual Flow token +import "FlowToken" + +access(all) contract AlpenFlow: FungibleToken { + + access(all) entitlement Withdraw + + // REMOVED: FlowVault resource implementation (previously lines 12-56) + // The FlowVault resource has been removed to prevent type conflicts + // with the real FlowToken.Vault when integrating with Tidal contracts. + // All references to FlowVault will now use FlowToken.Vault instead. + + access(all) entitlement EPosition + access(all) entitlement EGovernance + access(all) entitlement EImplementation + + // A structure used internally to track a position's balance for a particular token. + access(all) struct InternalBalance { + access(all) var direction: BalanceDirection + + // Interally, position balances are tracked using a "scaled balance". The "scaled balance" is the + // actual balance divided by the current interest index for the associated token. This means we don't + // need to update the balance of a position as time passes, even as interest rates change. We only need + // to update the scaled balance when the user deposits or withdraws funds. The interest index + // is a number relatively close to 1.0, so the scaled balance will be roughly of the same order + // of magnitude as the actual balance (thus we can use UFix64 for the scaled balance). + access(all) var scaledBalance: UFix64 + + init() { + self.direction = BalanceDirection.Credit + self.scaledBalance = 0.0 + } + + access(all) fun recordDeposit(amount: UFix64, tokenState: auth(EImplementation) &TokenState) { + if self.direction == BalanceDirection.Credit { + // Depositing into a credit position just increases the balance. + + // To maximize precision, we could convert the scaled balance to a true balance, add the + // deposit amount, and then convert the result back to a scaled balance. However, this will + // only cause problems for very small deposits (fractions of a cent), so we save computational + // cycles by just scaling the deposit amount and adding it directly to the scaled balance. + let scaledDeposit = AlpenFlow.trueBalanceToScaledBalance(trueBalance: amount, + interestIndex: tokenState.creditInterestIndex) + + self.scaledBalance = self.scaledBalance + scaledDeposit + + // Increase the total credit balance for the token + tokenState.updateCreditBalance(amount: Fix64(amount)) + } else { + // When depositing into a debit position, we first need to compute the true balance to see + // if this deposit will flip the position from debit to credit. + let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + if trueBalance > amount { + // The deposit isn't big enough to clear the debt, so we just decrement the debt. + let updatedBalance = trueBalance - amount + + self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.debitInterestIndex) + + // Decrease the total debit balance for the token + tokenState.updateDebitBalance(amount: -1.0 * Fix64(amount)) + } else { + // The deposit is enough to clear the debt, so we switch to a credit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Credit + self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.creditInterestIndex) + + // Increase the credit balance AND decrease the debit balance + tokenState.updateCreditBalance(amount: Fix64(updatedBalance)) + tokenState.updateDebitBalance(amount: -1.0 * Fix64(trueBalance)) + } + } + } + + access(all) fun recordWithdrawal(amount: UFix64, tokenState: &TokenState) { + if self.direction == BalanceDirection.Debit { + // Withdrawing from a debit position just increases the debt amount. + + // To maximize precision, we could convert the scaled balance to a true balance, subtract the + // withdrawal amount, and then convert the result back to a scaled balance. However, this will + // only cause problems for very small withdrawals (fractions of a cent), so we save computational + // cycles by just scaling the withdrawal amount and subtracting it directly from the scaled balance. + let scaledWithdrawal = AlpenFlow.trueBalanceToScaledBalance(trueBalance: amount, + interestIndex: tokenState.debitInterestIndex) + + self.scaledBalance = self.scaledBalance + scaledWithdrawal + + // Increase the total debit balance for the token + tokenState.updateDebitBalance(amount: Fix64(amount)) + } else { + // When withdrawing from a credit position, we first need to compute the true balance to see + // if this withdrawal will flip the position from credit to debit. + let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + if trueBalance >= amount { + // The withdrawal isn't big enough to push the position into debt, so we just decrement the + // credit balance. + let updatedBalance = trueBalance - amount + + self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.creditInterestIndex) + + // Decrease the total credit balance for the token + tokenState.updateCreditBalance(amount: -1.0 * Fix64(amount)) + } else { + // The withdrawal is enough to push the position into debt, so we switch to a debit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Debit + self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.debitInterestIndex) + + // Decrease the credit balance AND increase the debit balance + tokenState.updateCreditBalance(amount: -1.0 * Fix64(trueBalance)) + tokenState.updateDebitBalance(amount: Fix64(updatedBalance)) + } + } + } + } + + access(all) entitlement mapping ImplementationUpdates { + EImplementation -> Mutate + } + + access(all) struct InternalPosition { + access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} + + init() { + self.balances = {} + } + } + + access(all) struct interface InterestCurve { + access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 + { + post { + result <= 1.0: "Interest rate can't exceed 100%" + } + } + } + + access(all) struct SimpleInterestCurve: InterestCurve { + access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { + return 0.0 + } + } + + // A multiplication function for interest calcuations. It assumes that both values are very close to 1 + // and represent fixed point numbers with 16 decimal places of precision. + access(all) fun interestMul(_ a: UInt64, _ b: UInt64): UInt64 { + let aScaled = a / 100000000 + let bScaled = b / 100000000 + + return aScaled * bScaled + } + + // Converts a yearly interest rate (as a UFix64) to a per-second multiplication factor + // (stored in a UInt64 as a fixed point number with 16 decimal places). The input to this function will be + // just the relative interest rate (e.g. 0.05 for 5% interest), but the result will be + // the per-second multiplier (e.g. 1.000000000001). + access(all) fun perSecondInterestRate(yearlyRate: UFix64): UInt64 { + // Covert the yearly rate to an integer maintaning the 10^8 multiplier of UFix64. + // We would need to multiply by an additional 10^8 to match the promised multiplier of + // 10^16. HOWEVER, since we are about to divide by 31536000, we can save multiply a factor + // 1000 smaller, and then divide by 31536. + let yearlyScaledValue = UInt64.fromBigEndianBytes(yearlyRate.toBigEndianBytes())! * 100000 + let perSecondScaledValue = (yearlyScaledValue / 31536) + 10000000000000000 + + return perSecondScaledValue + } + + // Updates an interest index to reflect the passage of time. The result is: + // newIndex = oldIndex * perSecondRate^seconds + access(all) fun compoundInterestIndex(oldIndex: UInt64, perSecondRate: UInt64, elapsedSeconds: UFix64): UInt64 { + var result = oldIndex + var current = perSecondRate + var secondsCounter = UInt64(elapsedSeconds) + + while secondsCounter > 0 { + if secondsCounter & 1 == 1 { + result = AlpenFlow.interestMul(result, current) + } + current = AlpenFlow.interestMul(current, current) + secondsCounter = secondsCounter >> 1 + } + + return result + } + + access(all) fun scaledBalanceToTrueBalance(scaledBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return scaledBalance * indexMultiplier + } + + access(all) fun trueBalanceToScaledBalance(trueBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return trueBalance / indexMultiplier + } + + access(all) struct TokenState { + access(all) var lastUpdate: UFix64 + access(all) var totalCreditBalance: UFix64 + access(all) var totalDebitBalance: UFix64 + access(all) var creditInterestIndex: UInt64 + access(all) var debitInterestIndex: UInt64 + access(all) var currentCreditRate: UInt64 + access(all) var currentDebitRate: UInt64 + access(all) var interestCurve: {InterestCurve} + + access(all) fun updateCreditBalance(amount: Fix64) { + // temporary cast the credit balance to a signed value so we can add/subtract + let adjustedBalance = Fix64(self.totalCreditBalance) + amount + self.totalCreditBalance = UFix64(adjustedBalance) + } + + access(all) fun updateDebitBalance(amount: Fix64) { + // temporary cast the debit balance to a signed value so we can add/subtract + let adjustedBalance = Fix64(self.totalDebitBalance) + amount + self.totalDebitBalance = UFix64(adjustedBalance) + } + + access(all) fun updateInterestIndices() { + let currentTime = getCurrentBlock().timestamp + let timeDelta = currentTime - self.lastUpdate + self.creditInterestIndex = AlpenFlow.compoundInterestIndex(oldIndex: self.creditInterestIndex, perSecondRate: self.currentCreditRate, elapsedSeconds: timeDelta) + self.debitInterestIndex = AlpenFlow.compoundInterestIndex(oldIndex: self.debitInterestIndex, perSecondRate: self.currentDebitRate, elapsedSeconds: timeDelta) + self.lastUpdate = currentTime + } + + access(all) fun updateInterestRates() { + // If there's no credit balance, we can't calculate a meaningful credit rate + // so we'll just set both rates to zero and return early + if self.totalCreditBalance <= 0.0 { + self.currentCreditRate = 10000000000000000 // 1.0 in fixed point (no interest) + self.currentDebitRate = 10000000000000000 // 1.0 in fixed point (no interest) + return + } + + let debitRate = self.interestCurve.interestRate(creditBalance: self.totalCreditBalance, debitBalance: self.totalDebitBalance) + let debitIncome = self.totalDebitBalance * (1.0 + debitRate) + + // Calculate insurance amount (0.1% of credit balance) + let insuranceAmount = self.totalCreditBalance * 0.001 + + // Calculate credit rate, ensuring we don't have underflows + var creditRate: UFix64 = 0.0 + if debitIncome >= insuranceAmount { + creditRate = ((debitIncome - insuranceAmount) / self.totalCreditBalance) - 1.0 + } else { + // If debit income doesn't cover insurance, we have a negative credit rate + // but since we can't represent negative rates in our model, we'll use 0.0 + creditRate = 0.0 + } + + self.currentCreditRate = AlpenFlow.perSecondInterestRate(yearlyRate: creditRate) + self.currentDebitRate = AlpenFlow.perSecondInterestRate(yearlyRate: debitRate) + } + + init(interestCurve: {InterestCurve}) { + self.lastUpdate = 0.0 + self.totalCreditBalance = 0.0 + self.totalDebitBalance = 0.0 + self.creditInterestIndex = 10000000000000000 + self.debitInterestIndex = 10000000000000000 + self.currentCreditRate = 10000000000000000 + self.currentDebitRate = 10000000000000000 + self.interestCurve = interestCurve + } + } + + access(all) resource Pool { + // A simple version number that is incremented whenever one or more interest indices + // are updated. This is used to detect when the interest indices need to be updated in + // InternalPositions. + access(EImplementation) var version: UInt64 + + // Global state for tracking each token + access(self) var globalLedger: {Type: TokenState} + + // Individual user positions + access(self) var positions: {UInt64: InternalPosition} + + // The actual reserves of each token + access(self) var reserves: @{Type: {FungibleToken.Vault}} + + // Auto-incrementing position identifier counter + access(self) var nextPositionID: UInt64 + + // The default token type used as the "unit of account" for the pool. + access(self) let defaultToken: Type + + // The exchange rate between the default token and each other token supported by the pool. + // Multiplying a quantity of the specified token by the amount stored in this dictionary + // will provide the value of that quantity of tokens in terms of the default token. + access(self) var exchangeRates: {Type: UFix64} + + // The liquidation threshold for each token. + access(self) var liquidationThresholds: {Type: UFix64} + + init(defaultToken: Type, defaultTokenThreshold: UFix64) { + self.version = 0 + self.globalLedger = {defaultToken: TokenState(interestCurve: SimpleInterestCurve())} + self.positions = {} + self.reserves <- {} + self.defaultToken = defaultToken + self.exchangeRates = {defaultToken: 1.0} + self.liquidationThresholds = {defaultToken: defaultTokenThreshold} + self.nextPositionID = 0 + + // CHANGE: Don't create vault here - let the caller provide initial reserves + // The pool starts with empty reserves map + // Vaults will be added when tokens are first deposited + } + + access(EPosition) fun deposit(pid: UInt64, funds: @{FungibleToken.Vault}) { + pre { + self.positions[pid] != nil: "Invalid position ID" + self.globalLedger[funds.getType()] != nil: "Invalid token type" + funds.balance > 0.0: "Deposit amount must be positive" + } + + // Get a reference to the user's position and global token state for the affected token. + let type = funds.getType() + let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition + let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState + + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() + } + + // Update the global interest indices on the affected token to reflect the passage of time. + tokenState.updateInterestIndices() + + // CHANGE: Create vault if it doesn't exist yet + if self.reserves[type] == nil { + self.reserves[type] <-! funds.createEmptyVault() + } + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Reflect the deposit in the position's balance + position.balances[type]!.recordDeposit(amount: funds.balance, tokenState: tokenState) + + // Update the internal interest rate to reflect the new credit balance + tokenState.updateInterestRates() + + // Add the money to the reserves + reserveVault.deposit(from: <-funds) + } + + access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { + pre { + self.positions[pid] != nil: "Invalid position ID" + self.globalLedger[type] != nil: "Invalid token type" + amount > 0.0: "Withdrawal amount must be positive" + } + + // Get a reference to the user's position and global token state for the affected token. + let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition + let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState + + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() + } + + // Update the global interest indices on the affected token to reflect the passage of time. + tokenState.updateInterestIndices() + + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Reflect the withdrawal in the position's balance + position.balances[type]!.recordWithdrawal(amount: amount, tokenState: tokenState) + + // Ensure that this withdrawal doesn't cause the position to be overdrawn. + assert(self.positionHealth(pid: pid) >= 1.0, message: "Position is overdrawn") + + // Update the internal interest rate to reflect the new credit balance + tokenState.updateInterestRates() + + return <- reserveVault.withdraw(amount: amount) + } + + // Returns the health of the given position, which is the ratio of the position's effective collateral + // to its debt (as denominated in the default token). ("Effective collateral" means the + // value of each credit balance times the liquidation threshold for that token. i.e. the maximum borrowable amount) + access(all) fun positionHealth(pid: UInt64): UFix64 { + let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition + + // Get the position's collateral and debt values in terms of the default token. + var effectiveCollateral = 0.0 + var totalDebt = 0.0 + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState + if balance.direction == BalanceDirection.Credit { + let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + effectiveCollateral = effectiveCollateral + trueBalance * self.liquidationThresholds[type]! + } else { + let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + totalDebt = totalDebt + trueBalance + } + } + + // Calculate the health as the ratio of collateral to debt. + if totalDebt == 0.0 { + return 1.0 + } + return effectiveCollateral / totalDebt + } + + access(all) fun createPosition(): UInt64 { + let id = self.nextPositionID + self.nextPositionID = self.nextPositionID + 1 + self.positions[id] = InternalPosition() + return id + } + + // Helper function for testing – returns the current reserve balance for the specified token type. + access(all) fun reserveBalance(type: Type): UFix64 { + // CHANGE: Handle case where no vault exists yet for this token type + let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) + if vaultRef == nil { + return 0.0 + } + return vaultRef!.balance + } + + // Add getPositionDetails function that's used by DFB implementations + access(all) fun getPositionDetails(pid: UInt64): PositionDetails { + let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition + let balances: [PositionBalance] = [] + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState + + let trueBalance = balance.direction == BalanceDirection.Credit + ? AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.creditInterestIndex) + : AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.debitInterestIndex) + + balances.append(PositionBalance( + type: type, + direction: balance.direction, + balance: trueBalance + )) + } + + let health = self.positionHealth(pid: pid) + + return PositionDetails( + balances: balances, + poolDefaultToken: self.defaultToken, + defaultTokenAvailableBalance: 0.0, // TODO: Calculate this properly + health: health + ) + } + } + + access(all) struct Position { + access(self) let id: UInt64 + access(self) let pool: Capability + + // Returns the balances (both positive and negative) for all tokens in this position. + access(all) fun getBalances(): [PositionBalance] { + return [] + } + + // Returns the maximum amount of the given token type that could be withdrawn from this position. + access(all) fun getAvailableBalance(type: Type): UFix64 { + return 0.0 + } + + // Returns the maximum amount of the given token type that could be deposited into this position. + access(all) fun getDepositCapacity(type: Type): UFix64 { + return 0.0 + } + // Deposits tokens into the position, paying down debt (if one exists) and/or + // increasing collateral. The provided Vault must be a supported token type. + access(all) fun deposit(from: @{FungibleToken.Vault}) + { + destroy from + } + + // Withdraws tokens from the position by withdrawing collateral and/or + // creating/increasing a loan. The requested Vault type must be a supported token. + access(all) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} + { + // CHANGE: This is a stub implementation - real implementation would call pool.withdraw + panic("Position.withdraw is not implemented - use Pool.withdraw directly") + } + + // Returns a NEW sink for the given token type that will accept deposits of that token and + // update the position's collateral and/or debt accordingly. Note that calling this method multiple + // times will create multiple sinks, each of which will continue to work regardless of how many + // other sinks have been created. + access(all) fun createSink(type: Type): {DFB.Sink} { + let pool = self.pool.borrow()! + return AlpenFlowSink(pool: pool, positionID: self.id) + } + + // Returns a NEW source for the given token type that will service withdrawals of that token and + // update the position's collateral and/or debt accordingly. Note that calling this method multiple + // times will create multiple sources, each of which will continue to work regardless of how many + // other sources have been created. + access(all) fun createSource(type: Type): {DFB.Source} { + let pool = self.pool.borrow()! + return AlpenFlowSource(pool: pool, positionID: self.id, tokenType: type) + } + + // Provides a sink to the Position that will have tokens proactively pushed into it when the + // position has excess collateral. (Remember that sinks do NOT have to accept all tokens provided + // to them; the sink can choose to accept only some (or none) of the tokens provided, leaving the position + // overcollateralized.) + // + // Each position can have only one sink, and the sink must accept the default token type + // configured for the pool. Providing a new sink will replace the existing sink. Pass nil + // to configure the position to not push tokens. + access(all) fun provideSink(sink: {DFB.Sink}?) { + } + + // Provides a source to the Position that will have tokens proactively pulled from it when the + // position has insufficient collateral. If the source can cover the position's debt, the position + // will not be liquidated. + // + // Each position can have only one source, and the source must accept the default token type + // configured for the pool. Providing a new source will replace the existing source. Pass nil + // to configure the position to not pull tokens. + access(all) fun provideSource(source: {DFB.Source}?) { + } + + init(id: UInt64, pool: Capability) { + self.id = id + self.pool = pool + } + } + + // CHANGE: Removed FlowToken-specific implementation + // Helper for unit-tests – creates a new Pool with a generic default token + // Tests should specify the actual token type they want to use + access(all) fun createTestPool(defaultTokenThreshold: UFix64): @Pool { + // For backward compatibility, we'll panic here + // Tests should use createPool with explicit token type + panic("Use createPool with explicit token type instead") + } + + // CHANGE: Removed - tests should use proper token minting + // This function is kept for backward compatibility but will panic + access(all) fun createTestVault(balance: UFix64): @{FungibleToken.Vault} { + panic("Use proper token minting instead of createTestVault") + } + + // CHANGE: Add a proper pool creation function for tests + access(all) fun createPool(defaultToken: Type, defaultTokenThreshold: UFix64): @Pool { + return <- create Pool(defaultToken: defaultToken, defaultTokenThreshold: defaultTokenThreshold) + } + + // Helper for unit-tests - initializes a pool with a vault containing the specified balance + access(all) fun createTestPoolWithBalance(defaultTokenThreshold: UFix64, initialBalance: UFix64): @Pool { + // CHANGE: This function is deprecated - tests should create pools with explicit token types + panic("Use createPool with explicit token type and deposit tokens separately") + } + + // DFB.Sink implementation for AlpenFlow + access(all) struct AlpenFlowSink: DFB.Sink { + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(contract) let pool: auth(EPosition) &Pool + access(contract) let positionID: UInt64 + + access(all) view fun getSinkType(): Type { + // CHANGE: For now, return a generic FungibleToken.Vault type + // The actual type depends on what tokens the pool accepts + return Type<@{FungibleToken.Vault}>() + } + + access(all) fun minimumCapacity(): UFix64 { + // For now, return 0 as there's no minimum + return 0.0 + } + + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + let amount = from.balance + if amount > 0.0 { + let vault <- from.withdraw(amount: amount) + self.pool.deposit(pid: self.positionID, funds: <-vault) + } + } + + init(pool: auth(EPosition) &Pool, positionID: UInt64) { + self.uniqueID = nil + self.pool = pool + self.positionID = positionID + } + } + + // DFB.Source implementation for AlpenFlow + access(all) struct AlpenFlowSource: DFB.Source { + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(contract) let pool: auth(EPosition) &Pool + access(contract) let positionID: UInt64 + access(contract) let tokenType: Type + + access(all) view fun getSourceType(): Type { + return self.tokenType + } + + access(all) fun minimumAvailable(): UFix64 { + // Return the available balance for withdrawal + let position = self.pool.getPositionDetails(pid: self.positionID) + for balance in position.balances { + if balance.type == self.tokenType && balance.direction == BalanceDirection.Credit { + return balance.balance + } + } + return 0.0 + } + + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + let available = self.minimumAvailable() + let withdrawAmount = available < maxAmount ? available : maxAmount + if withdrawAmount > 0.0 { + return <- self.pool.withdraw(pid: self.positionID, amount: withdrawAmount, type: self.tokenType) + } else { + return <- AlpenFlow.createEmptyVault(vaultType: self.tokenType) + } + } + + init(pool: auth(EPosition) &Pool, positionID: UInt64, tokenType: Type) { + self.uniqueID = nil + self.pool = pool + self.positionID = positionID + self.tokenType = tokenType + } + } + + // AlpenFlow starts here! + + access(all) enum BalanceDirection: UInt8 { + access(all) case Credit + access(all) case Debit + } + + // A structure returned externally to report a position's balance for a particular token. + // This structure is NOT used internally. + access(all) struct PositionBalance { + access(all) let type: Type + access(all) let direction: BalanceDirection + access(all) let balance: UFix64 + + init(type: Type, direction: BalanceDirection, balance: UFix64) { + self.type = type + self.direction = direction + self.balance = balance + } + } + + // A structure returned externally to report all of the details associated with a position. + // This structure is NOT used internally. + access(all) struct PositionDetails { + access(all) let balances: [PositionBalance] + access(all) let poolDefaultToken: Type + access(all) let defaultTokenAvailableBalance: UFix64 + access(all) let health: UFix64 + + init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix64) { + self.balances = balances + self.poolDefaultToken = poolDefaultToken + self.defaultTokenAvailableBalance = defaultTokenAvailableBalance + self.health = health + } + } +} \ No newline at end of file diff --git a/flow.json b/flow.json index c7e18441..124d69d0 100644 --- a/flow.json +++ b/flow.json @@ -1,5 +1,11 @@ { "contracts": { + "AlpenFlow": { + "source": "cadence/contracts/internal-dependencies/AlpenFlow.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "DFB": { "source": "cadence/contracts/internal-dependencies/interfaces/DFB.cdc", "aliases": { @@ -151,6 +157,7 @@ "DFB", "FungibleTokenStack", "SwapStack", + "AlpenFlow", { "name": "USDA", "args": [ From 17e821958bd023d96363f928b163b27b933000ee Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:44:00 -0600 Subject: [PATCH 23/80] update internal dependencies --- .../internal-dependencies/AlpenFlow.cdc | 695 ------- .../internal-dependencies/TidalProtocol.cdc | 1703 +++++++++++++++++ .../connectors/FungibleTokenStack.cdc | 196 -- .../connectors/SwapStack.cdc | 304 --- .../internal-dependencies/interfaces/DFB.cdc | 769 -------- .../tokens/{USDA.cdc => MOET.cdc} | 48 +- flow.json | 8 +- 7 files changed, 1731 insertions(+), 1992 deletions(-) delete mode 100644 cadence/contracts/internal-dependencies/AlpenFlow.cdc create mode 100644 cadence/contracts/internal-dependencies/TidalProtocol.cdc delete mode 100644 cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc delete mode 100644 cadence/contracts/internal-dependencies/connectors/SwapStack.cdc delete mode 100644 cadence/contracts/internal-dependencies/interfaces/DFB.cdc rename cadence/contracts/internal-dependencies/tokens/{USDA.cdc => MOET.cdc} (85%) diff --git a/cadence/contracts/internal-dependencies/AlpenFlow.cdc b/cadence/contracts/internal-dependencies/AlpenFlow.cdc deleted file mode 100644 index a57bbafa..00000000 --- a/cadence/contracts/internal-dependencies/AlpenFlow.cdc +++ /dev/null @@ -1,695 +0,0 @@ -import "FungibleToken" -import "ViewResolver" -import "Burner" -import "MetadataViews" -import "FungibleTokenMetadataViews" -import "DFB" -// CHANGE: Import FlowToken to use the real FLOW token implementation -// This replaces our test FlowVault with the actual Flow token -import "FlowToken" - -access(all) contract AlpenFlow: FungibleToken { - - access(all) entitlement Withdraw - - // REMOVED: FlowVault resource implementation (previously lines 12-56) - // The FlowVault resource has been removed to prevent type conflicts - // with the real FlowToken.Vault when integrating with Tidal contracts. - // All references to FlowVault will now use FlowToken.Vault instead. - - access(all) entitlement EPosition - access(all) entitlement EGovernance - access(all) entitlement EImplementation - - // A structure used internally to track a position's balance for a particular token. - access(all) struct InternalBalance { - access(all) var direction: BalanceDirection - - // Interally, position balances are tracked using a "scaled balance". The "scaled balance" is the - // actual balance divided by the current interest index for the associated token. This means we don't - // need to update the balance of a position as time passes, even as interest rates change. We only need - // to update the scaled balance when the user deposits or withdraws funds. The interest index - // is a number relatively close to 1.0, so the scaled balance will be roughly of the same order - // of magnitude as the actual balance (thus we can use UFix64 for the scaled balance). - access(all) var scaledBalance: UFix64 - - init() { - self.direction = BalanceDirection.Credit - self.scaledBalance = 0.0 - } - - access(all) fun recordDeposit(amount: UFix64, tokenState: auth(EImplementation) &TokenState) { - if self.direction == BalanceDirection.Credit { - // Depositing into a credit position just increases the balance. - - // To maximize precision, we could convert the scaled balance to a true balance, add the - // deposit amount, and then convert the result back to a scaled balance. However, this will - // only cause problems for very small deposits (fractions of a cent), so we save computational - // cycles by just scaling the deposit amount and adding it directly to the scaled balance. - let scaledDeposit = AlpenFlow.trueBalanceToScaledBalance(trueBalance: amount, - interestIndex: tokenState.creditInterestIndex) - - self.scaledBalance = self.scaledBalance + scaledDeposit - - // Increase the total credit balance for the token - tokenState.updateCreditBalance(amount: Fix64(amount)) - } else { - // When depositing into a debit position, we first need to compute the true balance to see - // if this deposit will flip the position from debit to credit. - let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, - interestIndex: tokenState.debitInterestIndex) - - if trueBalance > amount { - // The deposit isn't big enough to clear the debt, so we just decrement the debt. - let updatedBalance = trueBalance - amount - - self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.debitInterestIndex) - - // Decrease the total debit balance for the token - tokenState.updateDebitBalance(amount: -1.0 * Fix64(amount)) - } else { - // The deposit is enough to clear the debt, so we switch to a credit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Credit - self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.creditInterestIndex) - - // Increase the credit balance AND decrease the debit balance - tokenState.updateCreditBalance(amount: Fix64(updatedBalance)) - tokenState.updateDebitBalance(amount: -1.0 * Fix64(trueBalance)) - } - } - } - - access(all) fun recordWithdrawal(amount: UFix64, tokenState: &TokenState) { - if self.direction == BalanceDirection.Debit { - // Withdrawing from a debit position just increases the debt amount. - - // To maximize precision, we could convert the scaled balance to a true balance, subtract the - // withdrawal amount, and then convert the result back to a scaled balance. However, this will - // only cause problems for very small withdrawals (fractions of a cent), so we save computational - // cycles by just scaling the withdrawal amount and subtracting it directly from the scaled balance. - let scaledWithdrawal = AlpenFlow.trueBalanceToScaledBalance(trueBalance: amount, - interestIndex: tokenState.debitInterestIndex) - - self.scaledBalance = self.scaledBalance + scaledWithdrawal - - // Increase the total debit balance for the token - tokenState.updateDebitBalance(amount: Fix64(amount)) - } else { - // When withdrawing from a credit position, we first need to compute the true balance to see - // if this withdrawal will flip the position from credit to debit. - let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, - interestIndex: tokenState.creditInterestIndex) - - if trueBalance >= amount { - // The withdrawal isn't big enough to push the position into debt, so we just decrement the - // credit balance. - let updatedBalance = trueBalance - amount - - self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.creditInterestIndex) - - // Decrease the total credit balance for the token - tokenState.updateCreditBalance(amount: -1.0 * Fix64(amount)) - } else { - // The withdrawal is enough to push the position into debt, so we switch to a debit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Debit - self.scaledBalance = AlpenFlow.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.debitInterestIndex) - - // Decrease the credit balance AND increase the debit balance - tokenState.updateCreditBalance(amount: -1.0 * Fix64(trueBalance)) - tokenState.updateDebitBalance(amount: Fix64(updatedBalance)) - } - } - } - } - - access(all) entitlement mapping ImplementationUpdates { - EImplementation -> Mutate - } - - access(all) struct InternalPosition { - access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} - - init() { - self.balances = {} - } - } - - access(all) struct interface InterestCurve { - access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 - { - post { - result <= 1.0: "Interest rate can't exceed 100%" - } - } - } - - access(all) struct SimpleInterestCurve: InterestCurve { - access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { - return 0.0 - } - } - - // A multiplication function for interest calcuations. It assumes that both values are very close to 1 - // and represent fixed point numbers with 16 decimal places of precision. - access(all) fun interestMul(_ a: UInt64, _ b: UInt64): UInt64 { - let aScaled = a / 100000000 - let bScaled = b / 100000000 - - return aScaled * bScaled - } - - // Converts a yearly interest rate (as a UFix64) to a per-second multiplication factor - // (stored in a UInt64 as a fixed point number with 16 decimal places). The input to this function will be - // just the relative interest rate (e.g. 0.05 for 5% interest), but the result will be - // the per-second multiplier (e.g. 1.000000000001). - access(all) fun perSecondInterestRate(yearlyRate: UFix64): UInt64 { - // Covert the yearly rate to an integer maintaning the 10^8 multiplier of UFix64. - // We would need to multiply by an additional 10^8 to match the promised multiplier of - // 10^16. HOWEVER, since we are about to divide by 31536000, we can save multiply a factor - // 1000 smaller, and then divide by 31536. - let yearlyScaledValue = UInt64.fromBigEndianBytes(yearlyRate.toBigEndianBytes())! * 100000 - let perSecondScaledValue = (yearlyScaledValue / 31536) + 10000000000000000 - - return perSecondScaledValue - } - - // Updates an interest index to reflect the passage of time. The result is: - // newIndex = oldIndex * perSecondRate^seconds - access(all) fun compoundInterestIndex(oldIndex: UInt64, perSecondRate: UInt64, elapsedSeconds: UFix64): UInt64 { - var result = oldIndex - var current = perSecondRate - var secondsCounter = UInt64(elapsedSeconds) - - while secondsCounter > 0 { - if secondsCounter & 1 == 1 { - result = AlpenFlow.interestMul(result, current) - } - current = AlpenFlow.interestMul(current, current) - secondsCounter = secondsCounter >> 1 - } - - return result - } - - access(all) fun scaledBalanceToTrueBalance(scaledBalance: UFix64, interestIndex: UInt64): UFix64 { - // The interest index is essentially a fixed point number with 16 decimal places, we convert - // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and - // additional 10^8 as required for the UFix64 representation). - let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 - return scaledBalance * indexMultiplier - } - - access(all) fun trueBalanceToScaledBalance(trueBalance: UFix64, interestIndex: UInt64): UFix64 { - // The interest index is essentially a fixed point number with 16 decimal places, we convert - // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and - // additional 10^8 as required for the UFix64 representation). - let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 - return trueBalance / indexMultiplier - } - - access(all) struct TokenState { - access(all) var lastUpdate: UFix64 - access(all) var totalCreditBalance: UFix64 - access(all) var totalDebitBalance: UFix64 - access(all) var creditInterestIndex: UInt64 - access(all) var debitInterestIndex: UInt64 - access(all) var currentCreditRate: UInt64 - access(all) var currentDebitRate: UInt64 - access(all) var interestCurve: {InterestCurve} - - access(all) fun updateCreditBalance(amount: Fix64) { - // temporary cast the credit balance to a signed value so we can add/subtract - let adjustedBalance = Fix64(self.totalCreditBalance) + amount - self.totalCreditBalance = UFix64(adjustedBalance) - } - - access(all) fun updateDebitBalance(amount: Fix64) { - // temporary cast the debit balance to a signed value so we can add/subtract - let adjustedBalance = Fix64(self.totalDebitBalance) + amount - self.totalDebitBalance = UFix64(adjustedBalance) - } - - access(all) fun updateInterestIndices() { - let currentTime = getCurrentBlock().timestamp - let timeDelta = currentTime - self.lastUpdate - self.creditInterestIndex = AlpenFlow.compoundInterestIndex(oldIndex: self.creditInterestIndex, perSecondRate: self.currentCreditRate, elapsedSeconds: timeDelta) - self.debitInterestIndex = AlpenFlow.compoundInterestIndex(oldIndex: self.debitInterestIndex, perSecondRate: self.currentDebitRate, elapsedSeconds: timeDelta) - self.lastUpdate = currentTime - } - - access(all) fun updateInterestRates() { - // If there's no credit balance, we can't calculate a meaningful credit rate - // so we'll just set both rates to zero and return early - if self.totalCreditBalance <= 0.0 { - self.currentCreditRate = 10000000000000000 // 1.0 in fixed point (no interest) - self.currentDebitRate = 10000000000000000 // 1.0 in fixed point (no interest) - return - } - - let debitRate = self.interestCurve.interestRate(creditBalance: self.totalCreditBalance, debitBalance: self.totalDebitBalance) - let debitIncome = self.totalDebitBalance * (1.0 + debitRate) - - // Calculate insurance amount (0.1% of credit balance) - let insuranceAmount = self.totalCreditBalance * 0.001 - - // Calculate credit rate, ensuring we don't have underflows - var creditRate: UFix64 = 0.0 - if debitIncome >= insuranceAmount { - creditRate = ((debitIncome - insuranceAmount) / self.totalCreditBalance) - 1.0 - } else { - // If debit income doesn't cover insurance, we have a negative credit rate - // but since we can't represent negative rates in our model, we'll use 0.0 - creditRate = 0.0 - } - - self.currentCreditRate = AlpenFlow.perSecondInterestRate(yearlyRate: creditRate) - self.currentDebitRate = AlpenFlow.perSecondInterestRate(yearlyRate: debitRate) - } - - init(interestCurve: {InterestCurve}) { - self.lastUpdate = 0.0 - self.totalCreditBalance = 0.0 - self.totalDebitBalance = 0.0 - self.creditInterestIndex = 10000000000000000 - self.debitInterestIndex = 10000000000000000 - self.currentCreditRate = 10000000000000000 - self.currentDebitRate = 10000000000000000 - self.interestCurve = interestCurve - } - } - - access(all) resource Pool { - // A simple version number that is incremented whenever one or more interest indices - // are updated. This is used to detect when the interest indices need to be updated in - // InternalPositions. - access(EImplementation) var version: UInt64 - - // Global state for tracking each token - access(self) var globalLedger: {Type: TokenState} - - // Individual user positions - access(self) var positions: {UInt64: InternalPosition} - - // The actual reserves of each token - access(self) var reserves: @{Type: {FungibleToken.Vault}} - - // Auto-incrementing position identifier counter - access(self) var nextPositionID: UInt64 - - // The default token type used as the "unit of account" for the pool. - access(self) let defaultToken: Type - - // The exchange rate between the default token and each other token supported by the pool. - // Multiplying a quantity of the specified token by the amount stored in this dictionary - // will provide the value of that quantity of tokens in terms of the default token. - access(self) var exchangeRates: {Type: UFix64} - - // The liquidation threshold for each token. - access(self) var liquidationThresholds: {Type: UFix64} - - init(defaultToken: Type, defaultTokenThreshold: UFix64) { - self.version = 0 - self.globalLedger = {defaultToken: TokenState(interestCurve: SimpleInterestCurve())} - self.positions = {} - self.reserves <- {} - self.defaultToken = defaultToken - self.exchangeRates = {defaultToken: 1.0} - self.liquidationThresholds = {defaultToken: defaultTokenThreshold} - self.nextPositionID = 0 - - // CHANGE: Don't create vault here - let the caller provide initial reserves - // The pool starts with empty reserves map - // Vaults will be added when tokens are first deposited - } - - access(EPosition) fun deposit(pid: UInt64, funds: @{FungibleToken.Vault}) { - pre { - self.positions[pid] != nil: "Invalid position ID" - self.globalLedger[funds.getType()] != nil: "Invalid token type" - funds.balance > 0.0: "Deposit amount must be positive" - } - - // Get a reference to the user's position and global token state for the affected token. - let type = funds.getType() - let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition - let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState - - // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance() - } - - // Update the global interest indices on the affected token to reflect the passage of time. - tokenState.updateInterestIndices() - - // CHANGE: Create vault if it doesn't exist yet - if self.reserves[type] == nil { - self.reserves[type] <-! funds.createEmptyVault() - } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Reflect the deposit in the position's balance - position.balances[type]!.recordDeposit(amount: funds.balance, tokenState: tokenState) - - // Update the internal interest rate to reflect the new credit balance - tokenState.updateInterestRates() - - // Add the money to the reserves - reserveVault.deposit(from: <-funds) - } - - access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { - pre { - self.positions[pid] != nil: "Invalid position ID" - self.globalLedger[type] != nil: "Invalid token type" - amount > 0.0: "Withdrawal amount must be positive" - } - - // Get a reference to the user's position and global token state for the affected token. - let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition - let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState - - // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance() - } - - // Update the global interest indices on the affected token to reflect the passage of time. - tokenState.updateInterestIndices() - - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Reflect the withdrawal in the position's balance - position.balances[type]!.recordWithdrawal(amount: amount, tokenState: tokenState) - - // Ensure that this withdrawal doesn't cause the position to be overdrawn. - assert(self.positionHealth(pid: pid) >= 1.0, message: "Position is overdrawn") - - // Update the internal interest rate to reflect the new credit balance - tokenState.updateInterestRates() - - return <- reserveVault.withdraw(amount: amount) - } - - // Returns the health of the given position, which is the ratio of the position's effective collateral - // to its debt (as denominated in the default token). ("Effective collateral" means the - // value of each credit balance times the liquidation threshold for that token. i.e. the maximum borrowable amount) - access(all) fun positionHealth(pid: UInt64): UFix64 { - let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition - - // Get the position's collateral and debt values in terms of the default token. - var effectiveCollateral = 0.0 - var totalDebt = 0.0 - - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState - if balance.direction == BalanceDirection.Credit { - let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex) - - effectiveCollateral = effectiveCollateral + trueBalance * self.liquidationThresholds[type]! - } else { - let trueBalance = AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex) - - totalDebt = totalDebt + trueBalance - } - } - - // Calculate the health as the ratio of collateral to debt. - if totalDebt == 0.0 { - return 1.0 - } - return effectiveCollateral / totalDebt - } - - access(all) fun createPosition(): UInt64 { - let id = self.nextPositionID - self.nextPositionID = self.nextPositionID + 1 - self.positions[id] = InternalPosition() - return id - } - - // Helper function for testing – returns the current reserve balance for the specified token type. - access(all) fun reserveBalance(type: Type): UFix64 { - // CHANGE: Handle case where no vault exists yet for this token type - let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) - if vaultRef == nil { - return 0.0 - } - return vaultRef!.balance - } - - // Add getPositionDetails function that's used by DFB implementations - access(all) fun getPositionDetails(pid: UInt64): PositionDetails { - let position = &self.positions[pid]! as auth(EImplementation) &InternalPosition - let balances: [PositionBalance] = [] - - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = &self.globalLedger[type]! as auth(EImplementation) &TokenState - - let trueBalance = balance.direction == BalanceDirection.Credit - ? AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.creditInterestIndex) - : AlpenFlow.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.debitInterestIndex) - - balances.append(PositionBalance( - type: type, - direction: balance.direction, - balance: trueBalance - )) - } - - let health = self.positionHealth(pid: pid) - - return PositionDetails( - balances: balances, - poolDefaultToken: self.defaultToken, - defaultTokenAvailableBalance: 0.0, // TODO: Calculate this properly - health: health - ) - } - } - - access(all) struct Position { - access(self) let id: UInt64 - access(self) let pool: Capability - - // Returns the balances (both positive and negative) for all tokens in this position. - access(all) fun getBalances(): [PositionBalance] { - return [] - } - - // Returns the maximum amount of the given token type that could be withdrawn from this position. - access(all) fun getAvailableBalance(type: Type): UFix64 { - return 0.0 - } - - // Returns the maximum amount of the given token type that could be deposited into this position. - access(all) fun getDepositCapacity(type: Type): UFix64 { - return 0.0 - } - // Deposits tokens into the position, paying down debt (if one exists) and/or - // increasing collateral. The provided Vault must be a supported token type. - access(all) fun deposit(from: @{FungibleToken.Vault}) - { - destroy from - } - - // Withdraws tokens from the position by withdrawing collateral and/or - // creating/increasing a loan. The requested Vault type must be a supported token. - access(all) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} - { - // CHANGE: This is a stub implementation - real implementation would call pool.withdraw - panic("Position.withdraw is not implemented - use Pool.withdraw directly") - } - - // Returns a NEW sink for the given token type that will accept deposits of that token and - // update the position's collateral and/or debt accordingly. Note that calling this method multiple - // times will create multiple sinks, each of which will continue to work regardless of how many - // other sinks have been created. - access(all) fun createSink(type: Type): {DFB.Sink} { - let pool = self.pool.borrow()! - return AlpenFlowSink(pool: pool, positionID: self.id) - } - - // Returns a NEW source for the given token type that will service withdrawals of that token and - // update the position's collateral and/or debt accordingly. Note that calling this method multiple - // times will create multiple sources, each of which will continue to work regardless of how many - // other sources have been created. - access(all) fun createSource(type: Type): {DFB.Source} { - let pool = self.pool.borrow()! - return AlpenFlowSource(pool: pool, positionID: self.id, tokenType: type) - } - - // Provides a sink to the Position that will have tokens proactively pushed into it when the - // position has excess collateral. (Remember that sinks do NOT have to accept all tokens provided - // to them; the sink can choose to accept only some (or none) of the tokens provided, leaving the position - // overcollateralized.) - // - // Each position can have only one sink, and the sink must accept the default token type - // configured for the pool. Providing a new sink will replace the existing sink. Pass nil - // to configure the position to not push tokens. - access(all) fun provideSink(sink: {DFB.Sink}?) { - } - - // Provides a source to the Position that will have tokens proactively pulled from it when the - // position has insufficient collateral. If the source can cover the position's debt, the position - // will not be liquidated. - // - // Each position can have only one source, and the source must accept the default token type - // configured for the pool. Providing a new source will replace the existing source. Pass nil - // to configure the position to not pull tokens. - access(all) fun provideSource(source: {DFB.Source}?) { - } - - init(id: UInt64, pool: Capability) { - self.id = id - self.pool = pool - } - } - - // CHANGE: Removed FlowToken-specific implementation - // Helper for unit-tests – creates a new Pool with a generic default token - // Tests should specify the actual token type they want to use - access(all) fun createTestPool(defaultTokenThreshold: UFix64): @Pool { - // For backward compatibility, we'll panic here - // Tests should use createPool with explicit token type - panic("Use createPool with explicit token type instead") - } - - // CHANGE: Removed - tests should use proper token minting - // This function is kept for backward compatibility but will panic - access(all) fun createTestVault(balance: UFix64): @{FungibleToken.Vault} { - panic("Use proper token minting instead of createTestVault") - } - - // CHANGE: Add a proper pool creation function for tests - access(all) fun createPool(defaultToken: Type, defaultTokenThreshold: UFix64): @Pool { - return <- create Pool(defaultToken: defaultToken, defaultTokenThreshold: defaultTokenThreshold) - } - - // Helper for unit-tests - initializes a pool with a vault containing the specified balance - access(all) fun createTestPoolWithBalance(defaultTokenThreshold: UFix64, initialBalance: UFix64): @Pool { - // CHANGE: This function is deprecated - tests should create pools with explicit token types - panic("Use createPool with explicit token type and deposit tokens separately") - } - - // DFB.Sink implementation for AlpenFlow - access(all) struct AlpenFlowSink: DFB.Sink { - access(contract) let uniqueID: DFB.UniqueIdentifier? - access(contract) let pool: auth(EPosition) &Pool - access(contract) let positionID: UInt64 - - access(all) view fun getSinkType(): Type { - // CHANGE: For now, return a generic FungibleToken.Vault type - // The actual type depends on what tokens the pool accepts - return Type<@{FungibleToken.Vault}>() - } - - access(all) fun minimumCapacity(): UFix64 { - // For now, return 0 as there's no minimum - return 0.0 - } - - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - let amount = from.balance - if amount > 0.0 { - let vault <- from.withdraw(amount: amount) - self.pool.deposit(pid: self.positionID, funds: <-vault) - } - } - - init(pool: auth(EPosition) &Pool, positionID: UInt64) { - self.uniqueID = nil - self.pool = pool - self.positionID = positionID - } - } - - // DFB.Source implementation for AlpenFlow - access(all) struct AlpenFlowSource: DFB.Source { - access(contract) let uniqueID: DFB.UniqueIdentifier? - access(contract) let pool: auth(EPosition) &Pool - access(contract) let positionID: UInt64 - access(contract) let tokenType: Type - - access(all) view fun getSourceType(): Type { - return self.tokenType - } - - access(all) fun minimumAvailable(): UFix64 { - // Return the available balance for withdrawal - let position = self.pool.getPositionDetails(pid: self.positionID) - for balance in position.balances { - if balance.type == self.tokenType && balance.direction == BalanceDirection.Credit { - return balance.balance - } - } - return 0.0 - } - - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - let available = self.minimumAvailable() - let withdrawAmount = available < maxAmount ? available : maxAmount - if withdrawAmount > 0.0 { - return <- self.pool.withdraw(pid: self.positionID, amount: withdrawAmount, type: self.tokenType) - } else { - return <- AlpenFlow.createEmptyVault(vaultType: self.tokenType) - } - } - - init(pool: auth(EPosition) &Pool, positionID: UInt64, tokenType: Type) { - self.uniqueID = nil - self.pool = pool - self.positionID = positionID - self.tokenType = tokenType - } - } - - // AlpenFlow starts here! - - access(all) enum BalanceDirection: UInt8 { - access(all) case Credit - access(all) case Debit - } - - // A structure returned externally to report a position's balance for a particular token. - // This structure is NOT used internally. - access(all) struct PositionBalance { - access(all) let type: Type - access(all) let direction: BalanceDirection - access(all) let balance: UFix64 - - init(type: Type, direction: BalanceDirection, balance: UFix64) { - self.type = type - self.direction = direction - self.balance = balance - } - } - - // A structure returned externally to report all of the details associated with a position. - // This structure is NOT used internally. - access(all) struct PositionDetails { - access(all) let balances: [PositionBalance] - access(all) let poolDefaultToken: Type - access(all) let defaultTokenAvailableBalance: UFix64 - access(all) let health: UFix64 - - init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix64) { - self.balances = balances - self.poolDefaultToken = poolDefaultToken - self.defaultTokenAvailableBalance = defaultTokenAvailableBalance - self.health = health - } - } -} \ No newline at end of file diff --git a/cadence/contracts/internal-dependencies/TidalProtocol.cdc b/cadence/contracts/internal-dependencies/TidalProtocol.cdc new file mode 100644 index 00000000..5fc75be8 --- /dev/null +++ b/cadence/contracts/internal-dependencies/TidalProtocol.cdc @@ -0,0 +1,1703 @@ +import "Burner" +import "FungibleToken" +import "ViewResolver" +import "MetadataViews" +import "FungibleTokenMetadataViews" + +import "DFBUtils" +import "DFB" +import "MOET" + +access(all) contract TidalProtocol { + + /// The canonical StoragePath where the primary TidalProtocol Pool is stored + access(all) let PoolStoragePath: StoragePath + /// The canonical StoragePath where the PoolFactory resource is stored + access(all) let PoolFactoryPath: StoragePath + /// The canonical PublicPath where the primary TidalProtocol Pool can be accessed publicly + access(all) let PoolPublicPath: PublicPath + + /* --- PUBLIC METHODS ---- */ + + /// Takes out a TidalProtocol loan with the provided collateral, returning a Position that can be used to manage + /// collateral and borrowed fund flows + /// + /// @param collateral: The collateral used as the basis for a loan. Only certain collateral types are supported, so + /// callers should be sure to check the provided Vault is supported to prevent reversion. + /// @param issuanceSink: The DeFiBlocks Sink connector where the protocol will deposit borrowed funds. If the + /// position becomes overcollateralized, additional funds will be borrowed (to maintain target LTV) and + /// deposited to the provided Sink. + /// @param repaymentSource: An optional DeFiBlocks Source connector from which the protocol will attempt to source + /// borrowed funds in the event of undercollateralization prior to liquidating. If none is provided, the + /// position health will not be actively managed on the down side, meaning liquidation is possible as soon as + /// the loan becomes undercollateralized. + /// + /// @return the Position via which the caller can manage their position + /// + access(all) fun openPosition( + collateral: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): Position { + let pid = self.borrowPool().createPosition( + funds: <-collateral, + issuanceSink: issuanceSink, + repaymentSource: repaymentSource, + pushToDrawDownSink: pushToDrawDownSink + ) + let cap = self.account.capabilities.storage.issue(self.PoolStoragePath) + return Position(id: pid, pool: cap) + } + + /* --- CONSTRUCTS & INTERNAL METHODS ---- */ + + access(all) entitlement EPosition + access(all) entitlement EGovernance + access(all) entitlement EImplementation + + // RESTORED: BalanceSheet and health computation from Dieter's implementation + // A convenience function for computing a health value from effective collateral and debt values. + access(all) fun healthComputation(effectiveCollateral: UFix64, effectiveDebt: UFix64): UFix64 { + var health = 0.0 + + if effectiveCollateral == 0.0 { + health = 0.0 + } else if effectiveDebt == 0.0 { + health = UFix64.max + } else if (effectiveDebt / effectiveCollateral) == 0.0 { + // If debt is so small relative to collateral that division rounds to zero, + // the health is essentially infinite + health = UFix64.max + } else { + health = effectiveCollateral / effectiveDebt + } + + return health + } + + access(all) struct BalanceSheet { + access(all) let effectiveCollateral: UFix64 + access(all) let effectiveDebt: UFix64 + access(all) let health: UFix64 + + init(effectiveCollateral: UFix64, effectiveDebt: UFix64) { + self.effectiveCollateral = effectiveCollateral + self.effectiveDebt = effectiveDebt + self.health = TidalProtocol.healthComputation(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) + } + } + + // A structure used internally to track a position's balance for a particular token. + access(all) struct InternalBalance { + access(all) var direction: BalanceDirection + + // Internally, position balances are tracked using a "scaled balance". The "scaled balance" is the + // actual balance divided by the current interest index for the associated token. This means we don't + // need to update the balance of a position as time passes, even as interest rates change. We only need + // to update the scaled balance when the user deposits or withdraws funds. The interest index + // is a number relatively close to 1.0, so the scaled balance will be roughly of the same order + // of magnitude as the actual balance (thus we can use UFix64 for the scaled balance). + access(all) var scaledBalance: UFix64 + + init() { + self.direction = BalanceDirection.Credit + self.scaledBalance = 0.0 + } + + access(all) fun recordDeposit(amount: UFix64, tokenState: auth(EImplementation) &TokenState) { + if self.direction == BalanceDirection.Credit { + // Depositing into a credit position just increases the balance. + + // To maximize precision, we could convert the scaled balance to a true balance, add the + // deposit amount, and then convert the result back to a scaled balance. However, this will + // only cause problems for very small deposits (fractions of a cent), so we save computational + // cycles by just scaling the deposit amount and adding it directly to the scaled balance. + let scaledDeposit = TidalProtocol.trueBalanceToScaledBalance(trueBalance: amount, + interestIndex: tokenState.creditInterestIndex) + + self.scaledBalance = self.scaledBalance + scaledDeposit + + // Increase the total credit balance for the token + tokenState.updateCreditBalance(amount: Fix64(amount)) + } else { + // When depositing into a debit position, we first need to compute the true balance to see + // if this deposit will flip the position from debit to credit. + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + if trueBalance > amount { + // The deposit isn't big enough to clear the debt, so we just decrement the debt. + let updatedBalance = trueBalance - amount + + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.debitInterestIndex) + + // Decrease the total debit balance for the token + tokenState.updateDebitBalance(amount: -1.0 * Fix64(amount)) + } else { + // The deposit is enough to clear the debt, so we switch to a credit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Credit + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.creditInterestIndex) + + // Increase the credit balance AND decrease the debit balance + tokenState.updateCreditBalance(amount: Fix64(updatedBalance)) + tokenState.updateDebitBalance(amount: -1.0 * Fix64(trueBalance)) + } + } + } + + access(all) fun recordWithdrawal(amount: UFix64, tokenState: &TokenState) { + if self.direction == BalanceDirection.Debit { + // Withdrawing from a debit position just increases the debt amount. + + // To maximize precision, we could convert the scaled balance to a true balance, subtract the + // withdrawal amount, and then convert the result back to a scaled balance. However, this will + // only cause problems for very small withdrawals (fractions of a cent), so we save computational + // cycles by just scaling the withdrawal amount and subtracting it directly from the scaled balance. + let scaledWithdrawal = TidalProtocol.trueBalanceToScaledBalance(trueBalance: amount, + interestIndex: tokenState.debitInterestIndex) + + self.scaledBalance = self.scaledBalance + scaledWithdrawal + + // Increase the total debit balance for the token + tokenState.updateDebitBalance(amount: Fix64(amount)) + } else { + // When withdrawing from a credit position, we first need to compute the true balance to see + // if this withdrawal will flip the position from credit to debit. + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + if trueBalance >= amount { + // The withdrawal isn't big enough to push the position into debt, so we just decrement the + // credit balance. + let updatedBalance = trueBalance - amount + + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.creditInterestIndex) + + // Decrease the total credit balance for the token + tokenState.updateCreditBalance(amount: -1.0 * Fix64(amount)) + } else { + // The withdrawal is enough to push the position into debt, so we switch to a debit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Debit + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.debitInterestIndex) + + // Decrease the credit balance AND increase the debit balance + tokenState.updateCreditBalance(amount: -1.0 * Fix64(trueBalance)) + tokenState.updateDebitBalance(amount: Fix64(updatedBalance)) + } + } + } + } + + access(all) entitlement mapping ImplementationUpdates { + EImplementation -> Mutate + EImplementation -> FungibleToken.Withdraw + } + + // RESTORED: InternalPosition as resource per Dieter's design + // This MUST be a resource to properly manage queued deposits + access(all) resource InternalPosition { + access(EImplementation) var targetHealth: UFix64 + access(EImplementation) var minHealth: UFix64 + access(EImplementation) var maxHealth: UFix64 + access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} + access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} + access(mapping ImplementationUpdates) var drawDownSink: {DFB.Sink}? + access(mapping ImplementationUpdates) var topUpSource: {DFB.Source}? + + init() { + self.balances = {} + self.queuedDeposits <- {} + self.targetHealth = 1.3 + self.minHealth = 1.1 + self.maxHealth = 1.5 + self.drawDownSink = nil + self.topUpSource = nil + } + + access(EImplementation) fun setDrawDownSink(_ sink: {DFB.Sink}?) { + pre { + sink?.getSinkType() ?? Type<@MOET.Vault>() == Type<@MOET.Vault>(): + "Invalid Sink provided - Sink \(sink.getType().identifier) must accept MOET" + } + self.drawDownSink = sink + } + + access(EImplementation) fun setTopUpSource(_ source: {DFB.Source}?) { + self.topUpSource = source + } + } + + access(all) struct interface InterestCurve { + access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { + post { + result <= 1.0: "Interest rate can't exceed 100%" + } + } + } + + access(all) struct SimpleInterestCurve: InterestCurve { + access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { + return 0.0 + } + } + + // A multiplication function for interest calcuations. It assumes that both values are very close to 1 + // and represent fixed point numbers with 16 decimal places of precision. + access(all) fun interestMul(_ a: UInt64, _ b: UInt64): UInt64 { + let aScaled = a / 100000000 + let bScaled = b / 100000000 + + return aScaled * bScaled + } + + // Converts a yearly interest rate (as a UFix64) to a per-second multiplication factor + // (stored in a UInt64 as a fixed point number with 16 decimal places). The input to this function will be + // just the relative interest rate (e.g. 0.05 for 5% interest), but the result will be + // the per-second multiplier (e.g. 1.000000000001). + access(all) fun perSecondInterestRate(yearlyRate: UFix64): UInt64 { + // Covert the yearly rate to an integer maintaning the 10^8 multiplier of UFix64. + // We would need to multiply by an additional 10^8 to match the promised multiplier of + // 10^16. HOWEVER, since we are about to divide by 31536000, we can save multiply a factor + // 1000 smaller, and then divide by 31536. + let yearlyScaledValue = UInt64.fromBigEndianBytes(yearlyRate.toBigEndianBytes())! * 100000 + let perSecondScaledValue = (yearlyScaledValue / 31536) + 10000000000000000 + + return perSecondScaledValue + } + + // Updates an interest index to reflect the passage of time. The result is: + // newIndex = oldIndex * perSecondRate^seconds + access(all) fun compoundInterestIndex(oldIndex: UInt64, perSecondRate: UInt64, elapsedSeconds: UFix64): UInt64 { + var result = oldIndex + var current = perSecondRate + var secondsCounter = UInt64(elapsedSeconds) + + while secondsCounter > 0 { + if secondsCounter & 1 == 1 { + result = TidalProtocol.interestMul(result, current) + } + current = TidalProtocol.interestMul(current, current) + secondsCounter = secondsCounter >> 1 + } + + return result + } + + access(all) fun scaledBalanceToTrueBalance(scaledBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return scaledBalance * indexMultiplier + } + + access(all) fun trueBalanceToScaledBalance(trueBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return trueBalance / indexMultiplier + } + + access(all) struct TokenState { + access(all) var lastUpdate: UFix64 + access(all) var totalCreditBalance: UFix64 + access(all) var totalDebitBalance: UFix64 + access(all) var creditInterestIndex: UInt64 + access(all) var debitInterestIndex: UInt64 + access(all) var currentCreditRate: UInt64 + access(all) var currentDebitRate: UInt64 + access(all) var interestCurve: {InterestCurve} + + // RESTORED: Deposit rate limiting from Dieter's implementation + access(all) var depositRate: UFix64 + access(all) var depositCapacity: UFix64 + access(all) var depositCapacityCap: UFix64 + + access(all) fun updateCreditBalance(amount: Fix64) { + // temporary cast the credit balance to a signed value so we can add/subtract + let adjustedBalance = Fix64(self.totalCreditBalance) + amount + self.totalCreditBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 + } + + access(all) fun updateDebitBalance(amount: Fix64) { + // temporary cast the debit balance to a signed value so we can add/subtract + let adjustedBalance = Fix64(self.totalDebitBalance) + amount + self.totalDebitBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 + } + + // RESTORED: Enhanced updateInterestIndices with deposit capacity update + access(all) fun updateInterestIndices() { + let currentTime = getCurrentBlock().timestamp + let timeDelta = currentTime - self.lastUpdate + self.creditInterestIndex = TidalProtocol.compoundInterestIndex(oldIndex: self.creditInterestIndex, perSecondRate: self.currentCreditRate, elapsedSeconds: timeDelta) + self.debitInterestIndex = TidalProtocol.compoundInterestIndex(oldIndex: self.debitInterestIndex, perSecondRate: self.currentDebitRate, elapsedSeconds: timeDelta) + self.lastUpdate = currentTime + + // RESTORED: Update deposit capacity based on time + let newDepositCapacity = self.depositCapacity + (self.depositRate * timeDelta) + if newDepositCapacity >= self.depositCapacityCap { + self.depositCapacity = self.depositCapacityCap + } else { + self.depositCapacity = newDepositCapacity + } + } + + // RESTORED: Deposit limit function from Dieter's implementation + access(all) fun depositLimit(): UFix64 { + // Each deposit is limited to 5% of the total deposit capacity + return self.depositCapacity * 0.05 + } + + // RESTORED: Rename to updateForTimeChange to match Dieter's implementation + access(all) fun updateForTimeChange() { + self.updateInterestIndices() + } + + access(all) fun updateInterestRates() { + // If there's no credit balance, we can't calculate a meaningful credit rate + // so we'll just set both rates to zero and return early + if self.totalCreditBalance <= 0.0 { + self.currentCreditRate = 10000000000000000 // 1.0 in fixed point (no interest) + self.currentDebitRate = 10000000000000000 // 1.0 in fixed point (no interest) + return + } + + let debitRate = self.interestCurve.interestRate(creditBalance: self.totalCreditBalance, debitBalance: self.totalDebitBalance) + let debitIncome = self.totalDebitBalance * (1.0 + debitRate) + + // Calculate insurance amount (0.1% of credit balance) + let insuranceAmount = self.totalCreditBalance * 0.001 + + // Calculate credit rate, ensuring we don't have underflows + var creditRate: UFix64 = 0.0 + if debitIncome >= insuranceAmount { + creditRate = ((debitIncome - insuranceAmount) / self.totalCreditBalance) - 1.0 + } else { + // If debit income doesn't cover insurance, we have a negative credit rate + // but since we can't represent negative rates in our model, we'll use 0.0 + creditRate = 0.0 + } + + self.currentCreditRate = TidalProtocol.perSecondInterestRate(yearlyRate: creditRate) + self.currentDebitRate = TidalProtocol.perSecondInterestRate(yearlyRate: debitRate) + } + + // RESTORED: Parameterized init from Dieter's implementation + init(interestCurve: {InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64) { + self.lastUpdate = getCurrentBlock().timestamp + self.totalCreditBalance = 0.0 + self.totalDebitBalance = 0.0 + self.creditInterestIndex = 10000000000000000 + self.debitInterestIndex = 10000000000000000 + self.currentCreditRate = 10000000000000000 + self.currentDebitRate = 10000000000000000 + self.interestCurve = interestCurve + self.depositRate = depositRate + self.depositCapacity = depositCapacityCap + self.depositCapacityCap = depositCapacityCap + } + } + + access(all) resource Pool { + // A simple version number that is incremented whenever one or more interest indices + // are updated. This is used to detect when the interest indices need to be updated in + // InternalPositions. + access(EImplementation) var version: UInt64 + + // Global state for tracking each token + access(self) var globalLedger: {Type: TokenState} + + // Individual user positions - RESTORED as resources per Dieter's design + access(self) var positions: @{UInt64: InternalPosition} + + // The actual reserves of each token + access(self) var reserves: @{Type: {FungibleToken.Vault}} + + // Auto-incrementing position identifier counter + access(self) var nextPositionID: UInt64 + + // The default token type used as the "unit of account" for the pool. + access(self) let defaultToken: Type + + // RESTORED: Price oracle from Dieter's implementation + // A price oracle that will return the price of each token in terms of the default token. + access(self) var priceOracle: {DFB.PriceOracle} + + // RESTORED: Position update queue from Dieter's implementation + access(EImplementation) var positionsNeedingUpdates: [UInt64] + access(self) var positionsProcessedPerCallback: UInt64 + + // RESTORED: Collateral and borrow factors from Dieter's implementation + // These dictionaries determine borrowing limits. Each token has a collateral factor and a + // borrow factor. + // + // When determining the total collateral amount that can be borrowed against, the value of the + // token (as given by the oracle) is multiplied by the collateral factor. So, a token with a + // collateral factor of 0.8 would only allow you to borrow 80% as much as if you had a the same + // value of a token with a collateral factor of 1.0. The total "effective collateral" for a + // position is the value of each token multiplied by its collateral factor. + // + // At the same time, the "borrow factor" determines if the user can borrow against all of that + // effective collateral, or if they can only borrow a portion of it to manage risk. + access(self) var collateralFactor: {Type: UFix64} + access(self) var borrowFactor: {Type: UFix64} + + // REMOVED: Static exchange rates and liquidation thresholds + // These have been replaced by dynamic oracle pricing and risk factors + + // RESTORED: tokenState() helper function from Dieter's implementation + // A convenience function that returns a reference to a particular token state, making sure + // it's up-to-date for the passage of time. This should always be used when accessing a token + // state to avoid missing interest updates (duplicate calls to updateForTimeChange() are a nop + // within a single block). + access(self) fun tokenState(type: Type): auth(EImplementation) &TokenState { + let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState + state.updateForTimeChange() + return state + } + + init(defaultToken: Type, priceOracle: {DFB.PriceOracle}) { + pre { + priceOracle.unitOfAccount() == defaultToken: "Price oracle must return prices in terms of the default token" + } + + self.version = 0 + self.globalLedger = {defaultToken: TokenState( + interestCurve: SimpleInterestCurve(), + depositRate: 1000000.0, // Default: no rate limiting for default token + depositCapacityCap: 1000000.0 // Default: high capacity cap + )} + self.positions <- {} + self.reserves <- {} + self.defaultToken = defaultToken + self.priceOracle = priceOracle + self.collateralFactor = {defaultToken: 1.0} + self.borrowFactor = {defaultToken: 1.0} + self.nextPositionID = 0 + self.positionsNeedingUpdates = [] + self.positionsProcessedPerCallback = 100 + + // CHANGE: Don't create vault here - let the caller provide initial reserves + // The pool starts with empty reserves map + // Vaults will be added when tokens are first deposited + } + + // Add a new token type to the pool + // This function should only be called by governance in the future + access(EGovernance) fun addSupportedToken( + tokenType: Type, + collateralFactor: UFix64, + borrowFactor: UFix64, + interestCurve: {InterestCurve}, + depositRate: UFix64, + depositCapacityCap: UFix64 + ) { + pre { + self.globalLedger[tokenType] == nil: "Token type already supported" + tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" + collateralFactor > 0.0 && collateralFactor <= 1.0: "Collateral factor must be between 0 and 1" + borrowFactor > 0.0 && borrowFactor <= 1.0: "Borrow factor must be between 0 and 1" + depositRate > 0.0: "Deposit rate must be positive" + depositCapacityCap > 0.0: "Deposit capacity cap must be positive" + DFBUtils.definingContractIsFungibleToken(tokenType): + "Invalid token contract definition for tokenType \(tokenType.identifier) - defining contract is not FungibleToken conformant" + } + + // Add token to global ledger with its interest curve and deposit parameters + self.globalLedger[tokenType] = TokenState( + interestCurve: interestCurve, + depositRate: depositRate, + depositCapacityCap: depositCapacityCap + ) + + // Set collateral factor (what percentage of value can be used as collateral) + self.collateralFactor[tokenType] = collateralFactor + + // Set borrow factor (risk adjustment for borrowed amounts) + self.borrowFactor[tokenType] = borrowFactor + } + + // Get supported token types + access(all) fun getSupportedTokens(): [Type] { + return self.globalLedger.keys + } + + // Check if a token type is supported + access(all) fun isTokenSupported(tokenType: Type): Bool { + return self.globalLedger[tokenType] != nil + } + + // RESTORED: Public deposit function from Dieter's implementation + // Allows anyone to deposit funds into any position + access(all) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { + self.depositAndPush(pid: pid, from: <-from, pushToDrawDownSink: false) + } + + // RESTORED: Enhanced deposit with queue processing and rebalancing from Dieter's implementation + access(EPosition) fun depositAndPush(pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { + pre { + self.positions[pid] != nil: "Invalid position ID" + self.globalLedger[from.getType()] != nil: "Invalid token type" + } + + if from.balance == 0.0 { + Burner.burn(<-from) + return + } + + // Get a reference to the user's position and global token state for the affected token. + let type = from.getType() + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + // Update time-based state + // REMOVED: This is now handled by tokenState() helper function + // tokenState.updateForTimeChange() + + // RESTORED: Deposit rate limiting from Dieter's implementation + let depositAmount = from.balance + let depositLimit = tokenState.depositLimit() + + if depositAmount > depositLimit { + // The deposit is too big, so we need to queue the excess + let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) + + if position.queuedDeposits[type] == nil { + position.queuedDeposits[type] <-! queuedDeposit + } else { + position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) + } + } + + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() + } + + // CHANGE: Create vault if it doesn't exist yet + if self.reserves[type] == nil { + self.reserves[type] <-! from.createEmptyVault() + } + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Reflect the deposit in the position's balance + position.balances[type]!.recordDeposit(amount: from.balance, tokenState: tokenState) + + // Add the money to the reserves + reserveVault.deposit(from: <-from) + + // RESTORED: Rebalancing and queue management + if pushToDrawDownSink { + self.rebalancePosition(pid: pid, force: true) + } + + self.queuePositionForUpdateIfNecessary(pid: pid) + } + + access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { + // RESTORED: Call the enhanced function with pullFromTopUpSource = false for backward compatibility + return <- self.withdrawAndPull(pid: pid, type: type, amount: amount, pullFromTopUpSource: false) + } + + // RESTORED: Enhanced withdraw with top-up source integration from Dieter's implementation + access(EPosition) fun withdrawAndPull( + pid: UInt64, + type: Type, + amount: UFix64, + pullFromTopUpSource: Bool + ): @{FungibleToken.Vault} { + pre { + self.positions[pid] != nil: "Invalid position ID" + self.globalLedger[type] != nil: "Invalid token type" + } + if amount == 0.0 { + return <- DFBUtils.getEmptyVault(type) + } + + // Get a reference to the user's position and global token state for the affected token. + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + // Update the global interest indices on the affected token to reflect the passage of time. + // REMOVED: This is now handled by tokenState() helper function + // tokenState.updateForTimeChange() + + // RESTORED: Top-up source integration from Dieter's implementation + // Preflight to see if the funds are available + let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DFB.Source}? + let topUpType = topUpSource?.getSourceType() ?? self.defaultToken + + let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: topUpType, + targetHealth: position.minHealth, + withdrawType: type, + withdrawAmount: amount + ) + + var canWithdraw = false + + if requiredDeposit == 0.0 { + // We can service this withdrawal without any top up + canWithdraw = true + } else { + // We need more funds to service this withdrawal, see if they are available from the top up source + if pullFromTopUpSource && topUpSource != nil { + // If we have to rebalance, let's try to rebalance to the target health, not just the minimum + let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: topUpType, + targetHealth: position.targetHealth, + withdrawType: type, + withdrawAmount: amount + ) + + let pulledVault <- topUpSource!.withdrawAvailable(maxAmount: idealDeposit) + + // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here. + // The top up source may not have enough funds get us to the target health, but could have + // enough to keep us over the minimum. + if pulledVault.balance >= requiredDeposit { + // We can service this withdrawal if we deposit funds from our top up source + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + canWithdraw = true + } else { + // We can't get the funds required to service this withdrawal, so we need to redeposit what we got + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + } + } + } + + if !canWithdraw { + // We can't service this withdrawal, so we just abort + panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal") + } + + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() + } + + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Reflect the withdrawal in the position's balance + position.balances[type]!.recordWithdrawal(amount: amount, tokenState: tokenState) + + // Ensure that this withdrawal doesn't cause the position to be overdrawn. + assert(self.positionHealth(pid: pid) >= 1.0, message: "Position is overdrawn") + + // Queue for update if necessary + self.queuePositionForUpdateIfNecessary(pid: pid) + + return <- reserveVault.withdraw(amount: amount) + } + + // RESTORED: Position queue management from Dieter's implementation + access(self) fun queuePositionForUpdateIfNecessary(pid: UInt64) { + if self.positionsNeedingUpdates.contains(pid) { + // If this position is already queued for an update, no need to check anything else + return + } else { + // If this position is not already queued for an update, we need to check if it needs one + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + if position.queuedDeposits.length > 0 { + // This position has deposits that need to be processed, so we need to queue it for an update + self.positionsNeedingUpdates.append(pid) + return + } + + let positionHealth = self.positionHealth(pid: pid) + + if positionHealth < position.minHealth || positionHealth > position.maxHealth { + // This position is outside the configured health bounds, we queue it for an update + self.positionsNeedingUpdates.append(pid) + return + } + } + } + + // RESTORED: Position rebalancing from Dieter's implementation + // Rebalances the position to the target health value. If force is true, the position will be + // rebalanced even if it is currently healthy, otherwise, this function will do nothing if the + // position is within the min/max health bounds. + access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let balanceSheet = self.positionBalanceSheet(pid: pid) + + if !force && (balanceSheet.health >= position.minHealth && balanceSheet.health <= position.maxHealth) { + // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! + return + } + + if balanceSheet.health < position.targetHealth { + // The position is undercollateralized, see if the source can get more collateral to bring it up to the target health. + if position.topUpSource != nil { + let topUpSource = position.topUpSource! as auth(FungibleToken.Withdraw) &{DFB.Source} + let idealDeposit = self.fundsRequiredForTargetHealth( + pid: pid, + type: topUpSource.getSourceType(), + targetHealth: position.targetHealth + ) + + let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + } + } else if balanceSheet.health > position.targetHealth { + // The position is overcollateralized, we'll withdraw funds to match the target health and offer it to the sink. + if position.drawDownSink != nil { + let drawDownSink = position.drawDownSink! + let sinkType = drawDownSink.getSinkType() + let idealWithdrawal = self.fundsAvailableAboveTargetHealth( + pid: pid, + type: sinkType, + targetHealth: position.targetHealth + ) + + // Compute how many tokens of the sink's type are available to hit our target health. + let sinkCapacity = drawDownSink.minimumCapacity() + let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal + + if sinkAmount > 0.0 && sinkType == self.defaultToken { // second conditional included for sake of tracer bullet + // BUG: Calling through to withdrawAndPull results in an insufficient funds from the position's + // topUpSource. These funds should come from the protocol or reserves, not from the user's + // funds. To unblock here, we just mint MOET when a position is overcollateralized + // let sinkVault <- self.withdrawAndPull( + // pid: pid, + // type: sinkType, + // amount: sinkAmount, + // pullFromTopUpSource: false + // ) + + let tokenState = self.tokenState(type: self.defaultToken) + if position.balances[self.defaultToken] == nil { + position.balances[self.defaultToken] = InternalBalance() + } + position.balances[self.defaultToken]!.recordWithdrawal(amount: sinkAmount, tokenState: tokenState) + let sinkVault <- TidalProtocol.borrowMOETMinter().mintTokens(amount: sinkAmount) + // Push what we can into the sink, and redeposit the rest + drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + + if sinkVault.balance > 0.0 { + self.depositAndPush(pid: pid, from: <-sinkVault, pushToDrawDownSink: false) + } else { + Burner.burn(<-sinkVault) + } + } + } + } + } + + // RESTORED: Provider functions for sink/source from Dieter's implementation + access(EPosition) fun provideDrawDownSink(pid: UInt64, sink: {DFB.Sink}?) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + position.setDrawDownSink(sink) + } + + access(EPosition) fun provideTopUpSource(pid: UInt64, source: {DFB.Source}?) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + position.setTopUpSource(source) + } + + // RESTORED: Available balance with source integration from Dieter's implementation + access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + if pullFromTopUpSource && position.topUpSource != nil { + let topUpSource = position.topUpSource! + let sourceType = topUpSource.getSourceType() + let sourceAmount = topUpSource.minimumAvailable() + + return self.fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: type, + targetHealth: position.minHealth, + depositType: sourceType, + depositAmount: sourceAmount + ) + } else { + return self.fundsAvailableAboveTargetHealth( + pid: pid, + type: type, + targetHealth: position.minHealth + ) + } + } + + // Returns the health of the given position, which is the ratio of the position's effective collateral + // to its debt (as denominated in the default token). ("Effective collateral" means the + // value of each credit balance times the liquidation threshold for that token. i.e. the maximum borrowable amount) + access(all) fun positionHealth(pid: UInt64): UFix64 { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + // Get the position's collateral and debt values in terms of the default token. + var effectiveCollateral = 0.0 + var effectiveDebt = 0.0 + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self.tokenState(type: type) + if balance.direction == BalanceDirection.Credit { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + // RESTORED: Oracle-based pricing from Dieter's implementation + let tokenPrice = self.priceOracle.price(ofToken: type)! + let value = tokenPrice * trueBalance + effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) + } else { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + // RESTORED: Oracle-based pricing for debt calculation + let tokenPrice = self.priceOracle.price(ofToken: type)! + let value = tokenPrice * trueBalance + effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) + } + } + + // Calculate the health as the ratio of collateral to debt. + if effectiveDebt == 0.0 { + return 1.0 + } + return effectiveCollateral / effectiveDebt + } + + // RESTORED: Position balance sheet calculation from Dieter's implementation + access(self) fun positionBalanceSheet(pid: UInt64): BalanceSheet { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let priceOracle = &self.priceOracle as &{DFB.PriceOracle} + + // Get the position's collateral and debt values in terms of the default token. + var effectiveCollateral = 0.0 + var effectiveDebt = 0.0 + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self.tokenState(type: type) + if balance.direction == BalanceDirection.Credit { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + let value = priceOracle.price(ofToken: type)! * trueBalance + + effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) + } else { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + let value = priceOracle.price(ofToken: type)! * trueBalance + + effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) + } + } + + return BalanceSheet(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) + } + + /// Creates a lending position against the provided collateral funds, depositing the loaned amount to the + /// given Sink. If a Source is provided, the position will be configured to pull loan repayment when the loan + /// becomes undercollateralized, preferring repayment to outright liquidation. + access(all) fun createPosition( + funds: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): UInt64 { + pre { + self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier)" + } + // construct a new InternalPosition, assigning it the current position ID + let id = self.nextPositionID + self.nextPositionID = self.nextPositionID + 1 + self.positions[id] <-! create InternalPosition() + + + // assign issuance & repayment connectors within the InternalPosition + let iPos = (&self.positions[id] as auth(EImplementation) &InternalPosition?)! + let fundsType = funds.getType() + iPos.setDrawDownSink(issuanceSink) + if repaymentSource != nil { + iPos.setTopUpSource(repaymentSource) + } + + // deposit the initial funds & return the position ID + self.depositAndPush( + pid: id, + from: <-funds, + pushToDrawDownSink: pushToDrawDownSink + ) + return id + } + + // Helper function for testing – returns the current reserve balance for the specified token type. + access(all) fun reserveBalance(type: Type): UFix64 { + // CHANGE: Handle case where no vault exists yet for this token type + let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) + if vaultRef == nil { + return 0.0 + } + return vaultRef!.balance + } + + // Add getPositionDetails function that's used by DFB implementations + access(all) fun getPositionDetails(pid: UInt64): PositionDetails { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let balances: [PositionBalance] = [] + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self.tokenState(type: type) + + let trueBalance = balance.direction == BalanceDirection.Credit + ? TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.creditInterestIndex) + : TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.debitInterestIndex) + + balances.append(PositionBalance( + type: type, + direction: balance.direction, + balance: trueBalance + )) + } + + let health = self.positionHealth(pid: pid) + let defaultTokenAvailable = self.availableBalance(pid: pid, type: self.defaultToken, pullFromTopUpSource: false) + + return PositionDetails( + balances: balances, + poolDefaultToken: self.defaultToken, + defaultTokenAvailableBalance: defaultTokenAvailable, + health: health + ) + } + + // RESTORED: Advanced position health management functions from Dieter's implementation + + // The quantity of funds of a specified token which would need to be deposited to bring the + // position to the target health. This function will return 0.0 if the position is already at or over + // that health value. + access(all) fun fundsRequiredForTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { + return self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: type, + targetHealth: targetHealth, + withdrawType: self.defaultToken, + withdrawAmount: 0.0 + ) + } + + // The quantity of funds of a specified token which would need to be deposited to bring the + // position to the target health assuming we also withdraw a specified amount of another + // token. This function will return 0.0 if the position would already be at or over the target + // health value after the proposed withdrawal. + access(all) fun fundsRequiredForTargetHealthAfterWithdrawing( + pid: UInt64, + depositType: Type, + targetHealth: UFix64, + withdrawType: Type, + withdrawAmount: UFix64 + ): UFix64 { + if depositType == withdrawType && withdrawAmount > 0.0 { + // If the deposit and withdrawal types are the same, we compute the required deposit assuming + // no withdrawal (which is less work) and increase that by the withdraw amount at the end + return self.fundsRequiredForTargetHealth(pid: pid, type: depositType, targetHealth: targetHealth) + withdrawAmount + } + + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral + var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + + if withdrawAmount != 0.0 { + if position.balances[withdrawType] == nil || position.balances[withdrawType]!.direction == BalanceDirection.Debit { + // If the position doesn't have any collateral for the withdrawn token, we can just compute how much + // additional effective debt the withdrawal will create. + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) + } else { + let withdrawTokenState = self.tokenState(type: withdrawType) + // REMOVED: This is now handled by tokenState() helper function + // withdrawTokenState.updateForTimeChange() + + // The user has a collateral position in the given token, we need to figure out if this withdrawal + // will flip over into debt, or just draw down the collateral. + let collateralBalance = position.balances[withdrawType]!.scaledBalance + let trueCollateral = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: collateralBalance, + interestIndex: withdrawTokenState.creditInterestIndex + ) + + if trueCollateral >= withdrawAmount { + // This withdrawal will draw down collateral, but won't create debt, we just need to account + // for the collateral decrease. + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) + } else { + // The withdrawal will wipe out all of the collateral, and create some debt. + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + ((withdrawAmount - trueCollateral) * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) + + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (trueCollateral * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) + } + } + } + + // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) + // Now we can figure out how many of the given token would need to be deposited to bring the position + // to the target health value. + var healthAfterWithdrawal = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) + + if healthAfterWithdrawal >= targetHealth { + // The position is already at or above the target health, so we don't need to deposit anything. + return 0.0 + } + + // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep + // track of the number of tokens that went towards paying off debt. + var debtTokenCount = 0.0 + + if position.balances[depositType] != nil && position.balances[depositType]!.direction == BalanceDirection.Debit { + // The user has a debt position in the given token, we start by looking at the health impact of paying off + // the entire debt. + let depositTokenState = self.tokenState(type: depositType) + // REMOVED: This is now handled by tokenState() helper function + // depositTokenState.updateForTimeChange() + + let debtBalance = position.balances[depositType]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: depositTokenState.debitInterestIndex + ) + let debtEffectiveValue = self.priceOracle.price(ofToken: depositType)! * trueDebt / self.borrowFactor[depositType]! + + // Check what the new health would be if we paid off all of this debt + let potentialHealth = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal - debtEffectiveValue + ) + + // Does paying off all of the debt reach the target health? Then we're done. + if potentialHealth >= targetHealth { + // We can reach the target health by paying off some or all of the debt. We can easily + // compute how many units of the token would be needed to reach the target health. + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveDebt = healthChange * effectiveCollateralAfterWithdrawal / (targetHealth * targetHealth) + + // The amount of the token to pay back, in units of the token. + let paybackAmount = requiredEffectiveDebt * self.borrowFactor[depositType]! / self.priceOracle.price(ofToken: depositType)! + + return paybackAmount + } else { + // We can pay off the entire debt, but we still need to deposit more to reach the target health. + // We have logic below that can determine the collateral deposition required to reach the target health + // from this new health position. Rather than copy that logic here, we fall through into it. But first + // we have to record the amount of tokens that went towards debt payback and adjust the effective + // debt to reflect that it has been paid off. + debtTokenCount = trueDebt + effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue + healthAfterWithdrawal = potentialHealth + } + } + + // At this point, we're either dealing with a position that didn't have a debt position in the deposit + // token, or we've accounted for the debt payoff and adjusted the effective debt above. + + // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the + // target health. We can rearrange the health equation to solve for the required collateral: + // targetHealth = effectiveCollateral / effectiveDebt + // targetHealth * effectiveDebt = effectiveCollateral + // requiredCollateral = targetHealth * effectiveDebtAfterWithdrawal + + // We need to increase the effective collateral from its current value to the required value, so we + // multiply the required health change by the effective debt, and turn that into a token amount. + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveCollateral = healthChange * effectiveDebtAfterWithdrawal + + // The amount of the token to deposit, in units of the token. + let collateralTokenCount = requiredEffectiveCollateral / self.priceOracle.price(ofToken: depositType)! / self.collateralFactor[depositType]! + + // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. + return collateralTokenCount + debtTokenCount + } + + // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health + // at or above the provided target. + access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { + return self.fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: type, + targetHealth: targetHealth, + depositType: self.defaultToken, + depositAmount: 0.0 + ) + } + + // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health + // at or above the provided target, assuming we also deposit a specified amount of another token. + access(all) fun fundsAvailableAboveTargetHealthAfterDepositing( + pid: UInt64, + withdrawType: Type, + targetHealth: UFix64, + depositType: Type, + depositAmount: UFix64 + ): UFix64 { + if depositType == withdrawType && depositAmount > 0.0 { + // If the deposit and withdrawal types are the same, we compute the available funds assuming + // no deposit (which is less work) and increase that by the deposit amount at the end + return self.fundsAvailableAboveTargetHealth(pid: pid, type: withdrawType, targetHealth: targetHealth) + depositAmount + } + + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt + + if depositAmount != 0.0 { + if position.balances[depositType] == nil || position.balances[depositType]!.direction == BalanceDirection.Credit { + // If there's no debt for the deposit token, we can just compute how much additional effective collateral the deposit will create. + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + (depositAmount * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) + } else { + let depositTokenState = self.tokenState(type: depositType) + + // The user has a debt position in the given token, we need to figure out if this deposit + // will result in net collateral, or just bring down the debt. + let debtBalance = position.balances[depositType]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: depositTokenState.debitInterestIndex + ) + + if trueDebt >= depositAmount { + // This deposit will pay down some debt, but won't result in net collateral, we + // just need to account for the debt decrease. + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (depositAmount * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) + } else { + // The deposit will wipe out all of the debt, and create some collateral. + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (trueDebt * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) + + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + ((depositAmount - trueDebt) * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) + } + } + } + + // We now have new effective collateral and debt values that reflect the proposed deposit (if any!) + // Now we can figure out how many of the withdrawal token are available while keeping the position + // at or above the target health value. + var healthAfterDeposit = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + + if healthAfterDeposit <= targetHealth { + // The position is already at or below the target health, so we can't withdraw anything. + return 0.0 + } + + // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep + // track of the number of tokens that are available from collateral + var collateralTokenCount = 0.0 + + if position.balances[withdrawType] != nil && position.balances[withdrawType]!.direction == BalanceDirection.Credit { + // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all + // of that collateral + let withdrawTokenState = self.tokenState(type: withdrawType) + // REMOVED: This is now handled by tokenState() helper function + // withdrawTokenState.updateForTimeChange() + + let creditBalance = position.balances[withdrawType]!.scaledBalance + let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: creditBalance, + interestIndex: withdrawTokenState.creditInterestIndex + ) + let collateralEffectiveValue = self.priceOracle.price(ofToken: withdrawType)! * trueCredit * self.collateralFactor[withdrawType]! + + // Check what the new health would be if we took out all of this collateral + let potentialHealth = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, + effectiveDebt: effectiveDebtAfterDeposit + ) + + // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. + if potentialHealth <= targetHealth { + // We will hit the health target before using up all of the withdraw token credit. We can easily + // compute how many units of the token would bring the position down to the target health. + let availableHealth = healthAfterDeposit - targetHealth + let availableEffectiveValue = availableHealth * effectiveDebtAfterDeposit + + // The amount of the token we can take using that amount of health + let availableTokenCount = availableEffectiveValue / self.collateralFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! + + return availableTokenCount + } else { + // We can flip this credit position into a debit position, before hitting the target health. + // We have logic below that can determine health changes for debit positions. Rather than copy that here, + // fall through into it. But first we have to record the amount of tokens that are available as collateral + // and then adjust the effective collateral to reflect that it has come out + collateralTokenCount = trueCredit + effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue + // NOTE: The above invalidates the healthAfterDeposit value, but it's not used below... + } + } + + // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw + // token, or we've accounted for the credit balance and adjusted the effective collateral above. + + // We can calculate the available debt increase that would bring us to the target health + var availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit + + let availableTokens = availableDebtIncrease * self.borrowFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! + + return availableTokens + collateralTokenCount + } + + // Returns the health the position would have if the given amount of the specified token were deposited. + access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix64 { + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + var effectiveCollateralIncrease = 0.0 + var effectiveDebtDecrease = 0.0 + + if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Credit { + // Since the user has no debt in the given token, we can just compute how much + // additional collateral this deposit will create. + effectiveCollateralIncrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } else { + // The user has a debit position in the given token, we need to figure out if this deposit + // will only pay off some of the debt, or if it will also create new collateral. + let debtBalance = position.balances[type]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: tokenState.debitInterestIndex + ) + + if trueDebt >= amount { + // This deposit will wipe out some or all of the debt, but won't create new collateral, we + // just need to account for the debt decrease. + effectiveDebtDecrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + } else { + // This deposit will wipe out all of the debt, and create new collateral. + effectiveDebtDecrease = trueDebt * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + effectiveCollateralIncrease = (amount - trueDebt) * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } + } + + return TidalProtocol.healthComputation( + effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, + effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease + ) + } + + // Returns health value of this position if the given amount of the specified token were withdrawn without + // using the top up source. + // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates + // that the proposed withdrawal would fail (unless a top up source is available and used). + access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix64 { + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + var effectiveCollateralDecrease = 0.0 + var effectiveDebtIncrease = 0.0 + + if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Debit { + // The user has no credit position in the given token, we can just compute how much + // additional effective debt this withdrawal will create. + effectiveDebtIncrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + } else { + // The user has a credit position in the given token, we need to figure out if this withdrawal + // will only draw down some of the collateral, or if it will also create new debt. + let creditBalance = position.balances[type]!.scaledBalance + let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: creditBalance, + interestIndex: tokenState.creditInterestIndex + ) + + if trueCredit >= amount { + // This withdrawal will draw down some collateral, but won't create new debt, we + // just need to account for the collateral decrease. + effectiveCollateralDecrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } else { + // The withdrawal will wipe out all of the collateral, and create new debt. + effectiveDebtIncrease = (amount - trueCredit) * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + effectiveCollateralDecrease = trueCredit * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } + } + + return TidalProtocol.healthComputation( + effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, + effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease + ) + } + + // RESTORED: Async update infrastructure from Dieter's implementation + access(EImplementation) fun asyncUpdate() { + // TODO: In the production version, this function should only process some positions (limited by positionsProcessedPerCallback) AND + // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or + // sink aborts) won't prevent other positions from being updated. + var processed: UInt64 = 0 + while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { + let pid = self.positionsNeedingUpdates.removeFirst() + self.asyncUpdatePosition(pid: pid) + self.queuePositionForUpdateIfNecessary(pid: pid) + processed = processed + 1 + } + } + + // RESTORED: Async position update from Dieter's implementation + access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + // First check queued deposits, their addition could affect the rebalance we attempt later + for depositType in position.queuedDeposits.keys { + let queuedVault <- position.queuedDeposits.remove(key: depositType)! + let queuedAmount = queuedVault.balance + let depositTokenState = self.tokenState(type: depositType) + + let maxDeposit = depositTokenState.depositLimit() + + if maxDeposit >= queuedAmount { + // We can deposit all of the queued deposit, so just do it and remove it from the queue + self.depositAndPush(pid: pid, from: <-queuedVault, pushToDrawDownSink: false) + } else { + // We can only deposit part of the queued deposit, so do that and leave the rest in the queue + // for the next time we run. + let depositVault <- queuedVault.withdraw(amount: maxDeposit) + self.depositAndPush(pid: pid, from: <-depositVault, pushToDrawDownSink: false) + + // We need to update the queued vault to reflect the amount we used up + position.queuedDeposits[depositType] <-! queuedVault + } + } + + // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance + // the position if necessary. + self.rebalancePosition(pid: pid, force: false) + } + } + + /// Resource enabling the contract account to create a Pool. This pattern is used in place of contract methods to + /// ensure limited access to pool creation. While this could be done in contract's init, doing so here will allow + /// for the setting of the Pool's PriceOracle without the introduction of a concrete PriceOracle defining contract + /// which would include an external contract dependency. + /// + access(all) resource PoolFactory { + /// Creates a Pool and saves it to the canonical path, reverting if one is already stored + access(all) fun createPool(defaultToken: Type, priceOracle: {DFB.PriceOracle}) { + pre { + TidalProtocol.account.storage.type(at: TidalProtocol.PoolStoragePath) == nil: + "Storage collision - Pool has already been created & saved to \(TidalProtocol.PoolStoragePath)" + } + let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle) + TidalProtocol.account.storage.save(<-pool, to: TidalProtocol.PoolStoragePath) + let cap = TidalProtocol.account.capabilities.storage.issue<&Pool>(TidalProtocol.PoolStoragePath) + TidalProtocol.account.capabilities.unpublish(TidalProtocol.PoolPublicPath) + TidalProtocol.account.capabilities.publish(cap, at: TidalProtocol.PoolPublicPath) + } + } + + // TODO: Consider making this a resource given how critical it is to accessing a loan + access(all) struct Position { + access(self) let id: UInt64 + access(self) let pool: Capability + + init(id: UInt64, pool: Capability) { + pre { + pool.check(): "Invalid Pool Capability provided - cannot construct Position" + } + self.id = id + self.pool = pool + } + + // Returns the balances (both positive and negative) for all tokens in this position. + access(all) fun getBalances(): [PositionBalance] { + let pool = self.pool.borrow()! + return pool.getPositionDetails(pid: self.id).balances + } + + // RESTORED: Enhanced available balance from Dieter's implementation + access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 { + let pool = self.pool.borrow()! + return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource) + } + + // RESTORED: Health functions from Dieter's implementation + access(all) fun getHealth(): UFix64 { + let pool = self.pool.borrow()! + return pool.positionHealth(pid: self.id) + } + + access(all) fun getTargetHealth(): UFix64 { + // DIETER'S DESIGN: Position is just a relay struct, return 0.0 + return 0.0 + } + + access(all) fun setTargetHealth(targetHealth: UFix64) { + // DIETER'S DESIGN: Position is just a relay struct, do nothing + } + + access(all) fun getMinHealth(): UFix64 { + // DIETER'S DESIGN: Position is just a relay struct, return 0.0 + return 0.0 + } + + access(all) fun setMinHealth(minHealth: UFix64) { + // DIETER'S DESIGN: Position is just a relay struct, do nothing + } + + access(all) fun getMaxHealth(): UFix64 { + // DIETER'S DESIGN: Position is just a relay struct, return 0.0 + return 0.0 + } + + access(all) fun setMaxHealth(maxHealth: UFix64) { + // DIETER'S DESIGN: Position is just a relay struct, do nothing + } + + // Returns the maximum amount of the given token type that could be deposited into this position. + access(all) fun getDepositCapacity(type: Type): UFix64 { + // There's no limit on deposits from the position's perspective + return UFix64.max + } + + // RESTORED: Simple deposit that calls depositAndPush with pushToDrawDownSink = false + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let pool = self.pool.borrow()! + pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: false) + } + + // RESTORED: Enhanced deposit from Dieter's implementation + access(all) fun depositAndPush(from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { + let pool = self.pool.borrow()! + pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: pushToDrawDownSink) + } + + // RESTORED: Simple withdraw that calls withdrawAndPull with pullFromTopUpSource = false + access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} { + return <- self.withdrawAndPull(type: type, amount: amount, pullFromTopUpSource: false) + } + + // RESTORED: Enhanced withdraw from Dieter's implementation + access(FungibleToken.Withdraw) fun withdrawAndPull(type: Type, amount: UFix64, pullFromTopUpSource: Bool): @{FungibleToken.Vault} { + let pool = self.pool.borrow()! + return <- pool.withdrawAndPull(pid: self.id, type: type, amount: amount, pullFromTopUpSource: pullFromTopUpSource) + } + + // Returns a NEW sink for the given token type that will accept deposits of that token and + // update the position's collateral and/or debt accordingly. Note that calling this method multiple + // times will create multiple sinks, each of which will continue to work regardless of how many + // other sinks have been created. + access(all) fun createSink(type: Type): {DFB.Sink} { + // RESTORED: Create enhanced sink with pushToDrawDownSink option + return self.createSinkWithOptions(type: type, pushToDrawDownSink: false) + } + + // RESTORED: Enhanced sink creation from Dieter's implementation + access(all) fun createSinkWithOptions(type: Type, pushToDrawDownSink: Bool): {DFB.Sink} { + let pool = self.pool.borrow()! + return PositionSink(id: self.id, pool: self.pool, type: type, pushToDrawDownSink: pushToDrawDownSink) + } + + // Returns a NEW source for the given token type that will service withdrawals of that token and + // update the position's collateral and/or debt accordingly. Note that calling this method multiple + // times will create multiple sources, each of which will continue to work regardless of how many + // other sources have been created. + access(FungibleToken.Withdraw) fun createSource(type: Type): {DFB.Source} { + // RESTORED: Create enhanced source with pullFromTopUpSource option + return self.createSourceWithOptions(type: type, pullFromTopUpSource: false) + } + + // RESTORED: Enhanced source creation from Dieter's implementation + access(FungibleToken.Withdraw) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DFB.Source} { + let pool = self.pool.borrow()! + return PositionSource(id: self.id, pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource) + } + + // RESTORED: Provider functions implementation from Dieter's design + // Provides a sink to the Position that will have tokens proactively pushed into it when the + // position has excess collateral. (Remember that sinks do NOT have to accept all tokens provided + // to them; the sink can choose to accept only some (or none) of the tokens provided, leaving the position + // overcollateralized.) + // + // Each position can have only one sink, and the sink must accept the default token type + // configured for the pool. Providing a new sink will replace the existing sink. Pass nil + // to configure the position to not push tokens. + access(FungibleToken.Withdraw) fun provideSink(sink: {DFB.Sink}?) { + let pool = self.pool.borrow()! + pool.provideDrawDownSink(pid: self.id, sink: sink) + } + + // Provides a source to the Position that will have tokens proactively pulled from it when the + // position has insufficient collateral. If the source can cover the position's debt, the position + // will not be liquidated. + // + // Each position can have only one source, and the source must accept the default token type + // configured for the pool. Providing a new source will replace the existing source. Pass nil + // to configure the position to not pull tokens. + access(all) fun provideSource(source: {DFB.Source}?) { + let pool = self.pool.borrow()! + pool.provideTopUpSource(pid: self.id, source: source) + } + } + + // RESTORED: Enhanced position sink from Dieter's implementation + access(all) struct PositionSink: DFB.Sink { + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(self) let pool: Capability + access(self) let positionID: UInt64 + access(self) let type: Type + access(self) let pushToDrawDownSink: Bool + + init(id: UInt64, pool: Capability, type: Type, pushToDrawDownSink: Bool) { + self.uniqueID = nil + self.positionID = id + self.pool = pool + self.type = type + self.pushToDrawDownSink = pushToDrawDownSink + } + + access(all) view fun getSinkType(): Type { + return self.type + } + + access(all) fun minimumCapacity(): UFix64 { + // A position object has no limit to deposits unless the Capability has been revoked + return self.pool.check() ? UFix64.max : 0.0 + } + + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + if let pool = self.pool.borrow() { + pool.depositAndPush( + pid: self.positionID, + from: <-from.withdraw(amount: from.balance), + pushToDrawDownSink: self.pushToDrawDownSink + ) + } + } + } + + // RESTORED: Enhanced position source from Dieter's implementation + access(all) struct PositionSource: DFB.Source { + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(self) let pool: Capability + access(self) let positionID: UInt64 + access(self) let type: Type + access(self) let pullFromTopUpSource: Bool + + init(id: UInt64, pool: Capability, type: Type, pullFromTopUpSource: Bool) { + self.uniqueID = nil + self.positionID = id + self.pool = pool + self.type = type + self.pullFromTopUpSource = pullFromTopUpSource + } + + access(all) view fun getSourceType(): Type { + return self.type + } + + access(all) fun minimumAvailable(): UFix64 { + if !self.pool.check() { + return 0.0 + } + let pool = self.pool.borrow()! + return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) + } + + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + if !self.pool.check() { + return <- DFBUtils.getEmptyVault(self.type) + } + let pool = self.pool.borrow()! + let available = pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) + let withdrawAmount = (available > maxAmount) ? maxAmount : available + if withdrawAmount > 0.0 { + return <- pool.withdrawAndPull(pid: self.positionID, type: self.type, amount: withdrawAmount, pullFromTopUpSource: self.pullFromTopUpSource) + } else { + // Create an empty vault - this is a limitation we need to handle properly + return <- DFBUtils.getEmptyVault(self.type) + } + } + } + + access(all) enum BalanceDirection: UInt8 { + access(all) case Credit + access(all) case Debit + } + + // A structure returned externally to report a position's balance for a particular token. + // This structure is NOT used internally. + access(all) struct PositionBalance { + access(all) let type: Type + access(all) let direction: BalanceDirection + access(all) let balance: UFix64 + + init(type: Type, direction: BalanceDirection, balance: UFix64) { + self.type = type + self.direction = direction + self.balance = balance + } + } + + // A structure returned externally to report all of the details associated with a position. + // This structure is NOT used internally. + access(all) struct PositionDetails { + access(all) let balances: [PositionBalance] + access(all) let poolDefaultToken: Type + access(all) let defaultTokenAvailableBalance: UFix64 + access(all) let health: UFix64 + + init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix64) { + self.balances = balances + self.poolDefaultToken = poolDefaultToken + self.defaultTokenAvailableBalance = defaultTokenAvailableBalance + self.health = health + } + } + + access(self) view fun borrowPool(): auth(EPosition) &Pool { + return self.account.storage.borrow(from: self.PoolStoragePath) + ?? panic("Could not borrow reference to internal TidalProtocol Pool resource") + } + + access(self) view fun borrowMOETMinter(): &MOET.Minter { + return self.account.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath) + ?? panic("Could not borrow reference to internal MOET Minter resource") + } + + init() { + self.PoolStoragePath = StoragePath(identifier: "tidalProtocolPool_\(self.account.address)")! + self.PoolFactoryPath = StoragePath(identifier: "tidalProtocolPoolFactory_\(self.account.address)")! + self.PoolPublicPath = PublicPath(identifier: "tidalProtocolPool_\(self.account.address)")! + + // save Pool in storage & configure public Capability + self.account.storage.save( + <-create PoolFactory(), + to: self.PoolFactoryPath + ) + let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)! + } +} diff --git a/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc b/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc deleted file mode 100644 index 96253435..00000000 --- a/cadence/contracts/internal-dependencies/connectors/FungibleTokenStack.cdc +++ /dev/null @@ -1,196 +0,0 @@ -import "FungibleToken" -import "FungibleTokenMetadataViews" - -import "DFBUtils" -import "DFB" - -/// FungibleTokenStack -/// -/// This contract defines generic StackFi Sink & Source connector implementations for use with underlying Vault -/// Capabilities. These connectors can be used alone or in conjunction with other StackFi connectors to create complex -/// DeFi workflows. -/// -access(all) contract FungibleTokenStack { - - access(all) struct VaultSink : DFB.Sink { - /// The Vault Type accepted by the Sink - access(all) let depositVaultType: Type - /// The maximum balance of the linked Vault, checked before executing a deposit - access(all) let maximumBalance: UFix64 - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: DFB.UniqueIdentifier? - /// An unentitled Capability on the Vault to which deposits are distributed - access(self) let depositVault: Capability<&{FungibleToken.Vault}> - - init( - max: UFix64?, - depositVault: Capability<&{FungibleToken.Vault}>, - uniqueID: DFB.UniqueIdentifier? - ) { - pre { - depositVault.check(): "Provided invalid Capability" - DFBUtils.definingContractIsFungibleToken(depositVault.borrow()!.getType()): - "The contract defining Vault \(depositVault.borrow()!.getType().identifier) does not conform to FungibleToken contract interface" - } - self.maximumBalance = max ?? UFix64.max // assume no maximum if none provided - self.uniqueID = uniqueID - self.depositVaultType = depositVault.borrow()!.getType() - self.depositVault = depositVault - } - - /// Returns the Vault type accepted by this Sink - access(all) view fun getSinkType(): Type { - return self.depositVaultType - } - - /// Returns an estimate of how much of the associated Vault can be accepted by this Sink - access(all) fun minimumCapacity(): UFix64 { - if let vault = self.depositVault.borrow() { - return vault.balance < self.maximumBalance ? self.maximumBalance - vault.balance : 0.0 - } - return 0.0 - } - - /// Deposits up to the Sink's capacity from the provided Vault - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - let minimumCapacity = self.minimumCapacity() - if !self.depositVault.check() || minimumCapacity == 0.0 { - return - } - // deposit the lesser of the originating vault balance and minimum capacity - let capacity = minimumCapacity <= from.balance ? minimumCapacity : from.balance - self.depositVault.borrow()!.deposit(from: <-from.withdraw(amount: capacity)) - } - } - - access(all) struct VaultSource : DFB.Source { - /// Returns the Vault type provided by this Source - access(all) let withdrawVaultType: Type - /// The minimum balance of the linked Vault - access(all) let minimumBalance: UFix64 - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: DFB.UniqueIdentifier? - /// An entitled Capability on the Vault from which withdrawals are sourced - access(self) let withdrawVault: Capability - - init( - min: UFix64?, - withdrawVault: Capability, - uniqueID: DFB.UniqueIdentifier? - ) { - pre { - withdrawVault.check(): "Provided invalid Capability" - DFBUtils.definingContractIsFungibleToken(withdrawVault.borrow()!.getType()): - "The contract defining Vault \(withdrawVault.borrow()!.getType().identifier) does not conform to FungibleToken contract interface" - } - self.minimumBalance = min ?? 0.0 // assume no minimum if none provided - self.withdrawVault = withdrawVault - self.uniqueID = uniqueID - self.withdrawVaultType = withdrawVault.borrow()!.getType() - } - - /// Returns the Vault type provided by this Source - access(all) view fun getSourceType(): Type { - return self.withdrawVaultType - } - - /// Returns an estimate of how much of the associated Vault can be provided by this Source - access(all) fun minimumAvailable(): UFix64 { - if let vault = self.withdrawVault.borrow() { - return vault.balance < self.minimumBalance ? vault.balance - self.minimumBalance : 0.0 - } - return 0.0 - } - - /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be - /// returned - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - let available = self.minimumAvailable() - if !self.withdrawVault.check() || available == 0.0 || maxAmount == 0.0 { - return <- DFBUtils.getEmptyVault(self.withdrawVaultType) - } - // take the lesser between the available and maximum requested amount - let withdrawalAmount = available <= maxAmount ? available : maxAmount - return <- self.withdrawVault.borrow()!.withdraw(amount: withdrawalAmount) - } - } - - access(all) struct VaultSinkAndSource : DFB.Sink, DFB.Source { - /// The minimum balance of the linked Vault - access(all) let minimumBalance: UFix64 - /// The maximum balance of the linked Vault - access(all) let maximumBalance: UFix64 - /// The type of Vault this connector accepts (as a Sink) and provides (as a Source) - access(all) let vaultType: Type - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: DFB.UniqueIdentifier? - /// An entitled Capability on the Vault from which withdrawals are sourced & deposit are routed - access(self) let vault: Capability - - init( - min: UFix64?, - max: UFix64?, - vault: Capability, - uniqueID: DFB.UniqueIdentifier? - ) { - pre { - vault.check(): "Invalid Vault Capability provided" - DFBUtils.definingContractIsFungibleToken(vault.borrow()!.getType()): - "The contract defining Vault \(vault.borrow()!.getType().identifier) does not conform to FungibleToken contract interface" - min ?? 0.0 < max ?? UFix64.max: - "Minimum balance must be less than maximum balance if either is declared" - } - self.minimumBalance = min ?? 0.0 - self.maximumBalance = max ?? UFix64.max - self.vaultType = vault.borrow()!.getType() - self.uniqueID = uniqueID - self.vault = vault - } - - /// Returns the Vault type accepted by this Sink - access(all) view fun getSinkType(): Type { - return self.vaultType - } - - /// Returns the Vault type provided by this Source - access(all) view fun getSourceType(): Type { - return self.vaultType - } - - /// Returns an estimate of how much of the associated Vault can be accepted by this Sink - access(all) fun minimumCapacity(): UFix64 { - if let vault = self.vault.borrow() { - return vault.balance < self.maximumBalance ? self.maximumBalance - vault.balance : 0.0 - } - return 0.0 - } - - /// Returns an estimate of how much of the associated Vault can be provided by this Source - access(all) fun minimumAvailable(): UFix64 { - if let vault = self.vault.borrow() { - return vault.balance < self.minimumBalance ? vault.balance - self.minimumBalance : 0.0 - } - return 0.0 - } - - /// Deposits up to the Sink's capacity from the provided Vault - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - if let vault = self.vault.borrow() { - vault.deposit(from: <-from.withdraw(amount: from.balance)) - } - } - - /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be - /// returned - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - if let vault = self.vault.borrow() { - let finalAmount = vault.balance < maxAmount ? vault.balance : maxAmount - return <-vault.withdraw(amount: finalAmount) - } - return <- DFBUtils.getEmptyVault(self.vaultType) - } - } -} diff --git a/cadence/contracts/internal-dependencies/connectors/SwapStack.cdc b/cadence/contracts/internal-dependencies/connectors/SwapStack.cdc deleted file mode 100644 index d634c620..00000000 --- a/cadence/contracts/internal-dependencies/connectors/SwapStack.cdc +++ /dev/null @@ -1,304 +0,0 @@ -import "Burner" -import "FungibleToken" - -import "DFB" - -/// SwapStack -/// -/// This contract defines DeFiBlocks Sink & Source connector implementations for use with DeFi protocols. These -/// connectors can be used alone or in conjunction with other DeFiBlocks connectors to create complex DeFi workflows. -/// -access(all) contract SwapStack { - - /// A simple implementation of DFB.Quote allowing callers of Swapper.quoteIn() and .quoteOut() to cache quoted - /// amount in and/or out. - /// - access(all) struct BasicQuote : DFB.Quote { - access(all) let inType: Type - access(all) let outType: Type - access(all) let inAmount: UFix64 - access(all) let outAmount: UFix64 - - init( - inType: Type, - outType: Type, - inAmount: UFix64, - outAmount: UFix64 - ) { - self.inType = inType - self.outType = outType - self.inAmount = inAmount - self.outAmount = outAmount - } - } - - /// A MultiSwapper specific DFB.Quote implementation allowing for callers to set the Swapper used in MultiSwapper - /// that should fulfill the Swap - /// - access(all) struct MultiSwapperQuote : DFB.Quote { - access(all) let inType: Type - access(all) let outType: Type - access(all) let inAmount: UFix64 - access(all) let outAmount: UFix64 - access(all) let swapperIndex: Int - - init( - inType: Type, - outType: Type, - inAmount: UFix64, - outAmount: UFix64, - swapperIndex: Int - ) { - pre { - swapperIndex >= 0: "Invalid swapperIndex - provided \(swapperIndex) is less than 0" - } - self.inType = inType - self.outType = outType - self.inAmount = inAmount - self.outAmount = outAmount - self.swapperIndex = swapperIndex - } - } - - /// A Swapper implementation routing swap requests to the optimal contained Swapper. Once constructed, this can - /// effectively be used as an aggregator across all contained Swapper implementations, though it is limited to the - /// routes and pools exposed by its inner Swappers as well as runtime computation limits. - /// - access(all) struct MultiSwapper : DFB.Swapper { - access(all) let swappers: [{DFB.Swapper}] - access(contract) let uniqueID: DFB.UniqueIdentifier? - access(self) let inVault: Type - access(self) let outVault: Type - - init( - inVault: Type, - outVault: Type, - swappers: [{DFB.Swapper}], - uniqueID: DFB.UniqueIdentifier? - ) { - pre { - inVault.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()): - "Invalid inVault type - \(inVault.identifier) is not a FungibleToken Vault implementation" - outVault.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()): - "Invalid outVault type - \(outVault.identifier) is not a FungibleToken Vault implementation" - } - for swapper in swappers { - assert(swapper.inType() == inVault, - message: "Mismatched inVault \(inVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.inType().identifier)") - assert(swapper.outType() == outVault, - message: "Mismatched outVault \(outVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.outType().identifier)") - } - self.inVault = inVault - self.outVault = outVault - self.uniqueID = uniqueID - self.swappers = swappers - } - - /// The type of Vault this Swapper accepts when performing a swap - access(all) view fun inType(): Type { - return self.inVault - } - - /// The type of Vault this Swapper provides when performing a swap - access(all) view fun outType(): Type { - return self.outVault - } - - /// The estimated amount required to provide a Vault with the desired output balance - access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DFB.Quote} { - let estimate = self._estimate(amount: forDesired, out: true, reverse: reverse) - return MultiSwapperQuote( - inType: reverse ? self.outType() : self.inType(), - outType: reverse ? self.inType() : self.outType(), - inAmount: estimate[1], - outAmount: forDesired, - swapperIndex: Int(estimate[0]) - ) - } - - /// The estimated amount delivered out for a provided input balance - access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DFB.Quote} { - let estimate = self._estimate(amount: forProvided, out: true, reverse: reverse) - return MultiSwapperQuote( - inType: reverse ? self.outType() : self.inType(), - outType: reverse ? self.inType() : self.outType(), - inAmount: forProvided, - outAmount: estimate[1], - swapperIndex: Int(estimate[0]) - ) - } - /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose - /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted - /// to use multiple Flow swap protocols. If the provided quote is not a MultiSwapperQuote, a new quote is - /// requested and the optimal Swapper used to fulfill the swap. - /// NOTE: providing a Quote does not guarantee the fulfilled swap will enforce the quote's defined outAmount - access(all) fun swap(quote: {DFB.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} { - return <-self._swap(quote: quote, from: <-inVault, reverse: false) - } - - /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose - /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted - /// to use multiple Flow swap protocols. - /// NOTE: providing a Quote does not guarantee the fulfilled swap will enforce the quote's defined outAmount - access(all) fun swapBack(quote: {DFB.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} { - return <-self._swap(quote: quote, from: <-residual, reverse: true) - } - - /// Returns the the index of the optimal Swapper (result[0]) and the associated amountOut or amountIn (result[0]) - /// as a UFix64 array - access(self) fun _estimate(amount: UFix64, out: Bool, reverse: Bool): [UFix64; 2] { - var res: [UFix64; 2] = [0.0, 0.0] - for i, swapper in self.swappers { - // call the appropriate estimator - let estimate = out - ? swapper.quoteOut(forProvided: amount, reverse: true).outAmount - : swapper.quoteIn(forDesired: amount, reverse: true).inAmount - if (out ? res[1] < estimate : estimate < res[1]) { - // take minimum for in, maximum for out - res = [UFix64(i), estimate] - } - } - return res - } - - /// Swaps the provided Vault in the defined direction. If the quote is not a MultiSwapperQuote, a new quote is - /// requested and the current optimal Swapper used to fulfill the swap. - access(self) fun _swap(quote: {DFB.Quote}?, from: @{FungibleToken.Vault}, reverse: Bool): @{FungibleToken.Vault} { - var multiQuote = quote as? MultiSwapperQuote - if multiQuote != nil || multiQuote!.swapperIndex > self.swappers.length { - multiQuote = self.quoteOut(forProvided: from.balance, reverse: reverse) as! MultiSwapperQuote - } - let optimalSwapper = &self.swappers[multiQuote!.swapperIndex] as &{DFB.Swapper} - if reverse { - return <- optimalSwapper.swapBack(quote: multiQuote, residual: <-from) - } else { - return <- optimalSwapper.swap(quote: multiQuote, inVault: <-from) - } - } - } - - /// SwapSink DeFiBlocks connector that deposits the resulting post-conversion currency of a token swap to an inner - /// DeFiBlocks Sink, sourcing funds from a deposited Vault of a pre-set Type. - /// - access(all) struct SwapSink : DFB.Sink { - access(self) let swapper: {DFB.Swapper} - access(self) let sink: {DFB.Sink} - access(contract) let uniqueID: DFB.UniqueIdentifier? - - init(swapper: {DFB.Swapper}, sink: {DFB.Sink}, uniqueID: DFB.UniqueIdentifier?) { - pre { - swapper.outType() == sink.getSinkType(): - "Swapper outputs \(swapper.outType().identifier) but Sink takes \(sink.getSinkType().identifier) - " - .concat("Ensure the provided Swapper outputs a Vault Type compatible with the provided Sink") - } - self.swapper = swapper - self.sink = sink - self.uniqueID = uniqueID - } - - access(all) view fun getSinkType(): Type { - return self.swapper.inType() - } - - access(all) fun minimumCapacity(): UFix64 { - return self.swapper.quoteIn(forDesired: self.sink.minimumCapacity(), reverse: false).inAmount - } - - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - let limit = self.sink.minimumCapacity() - if from.balance == 0.0 || limit == 0.0 || !from.getType().isInstance(self.getSinkType()) { - return // nothing to swap from, no capacity to ingest, invalid Vault type - do nothing - } - - let quote = self.swapper.quoteIn(forDesired: from.balance, reverse: false) - let sinkLimit = quote.inAmount - let swapVault <- from.createEmptyVault() - - if sinkLimit < swapVault.balance { - // The sink is limited to fewer tokens than we have available. Only swap - // the amount we need to meet the sink limit. - swapVault.deposit(from: <-from.withdraw(amount: sinkLimit)) - } else { - // The sink can accept all of the available tokens, so we swap everything - swapVault.deposit(from: <-from.withdraw(amount: from.balance)) - } - - let swappedTokens <- self.swapper.swap(quote: quote, inVault: <-swapVault) - self.sink.depositCapacity(from: &swappedTokens as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - - if swappedTokens.balance > 0.0 { - from.deposit(from: <-self.swapper.swapBack(quote: nil, residual: <-swappedTokens)) - } else { - Burner.burn(<-swappedTokens) - } - } - } - - /// SwapSource DeFiBlocks connector that returns post-conversion currency, sourcing pre-converted funds from an inner - /// DeFiBlocks Source - /// - access(all) struct SwapSource : DFB.Source { - access(self) let swapper: {DFB.Swapper} - access(self) let source: {DFB.Source} - access(contract) let uniqueID: DFB.UniqueIdentifier? - - init(swapper: {DFB.Swapper}, source: {DFB.Source}, uniqueID: DFB.UniqueIdentifier) { - pre { - source.getSourceType() == swapper.inType(): - "Source outputs \(source.getSourceType().identifier) but Swapper takes \(swapper.inType().identifier) - " - .concat("Ensure the provided Source outputs a Vault Type compatible with the provided Swapper") - } - self.swapper = swapper - self.source = source - self.uniqueID = uniqueID - } - - access(all) view fun getSourceType(): Type { - return self.swapper.outType() - } - - access(all) fun minimumAvailable(): UFix64 { - // estimate post-conversion currency based on the source's pre-conversion balance available - let availableIn = self.source.minimumAvailable() - return availableIn > 0.0 - ? self.swapper.quoteOut(forProvided: availableIn, reverse: false).outAmount - : 0.0 - } - - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - let minimumAvail = self.minimumAvailable() - if minimumAvail == 0.0 || maxAmount == 0.0 { - return <- SwapStack.getEmptyVault(self.getSourceType()) - } - - // expect output amount as the lesser between the amount available and the maximum amount - var amountOut = minimumAvail < maxAmount ? minimumAvail : maxAmount - - // find out how much liquidity to gather from the inner Source - let availableIn = self.source.minimumAvailable() - let quote = self.swapper.quoteIn(forDesired: amountOut, reverse: false) - let quoteIn = availableIn < quote.inAmount ? availableIn : quote.inAmount - - let sourceLiquidity <- self.source.withdrawAvailable(maxAmount: quoteIn) - if sourceLiquidity.balance == 0.0 { - Burner.burn(<-sourceLiquidity) - return <- SwapStack.getEmptyVault(self.getSourceType()) - } - let outVault <- self.swapper.swap(quote: quote, inVault: <-sourceLiquidity) - if outVault.balance > amountOut { - // TODO - what to do if excess is found? - // - can swapBack() but can't deposit to the inner source and can't return an unsupported Vault type - // -> could make inner {Source} an intersection {Source, Sink} - } - return <- outVault - } - } - - /// Returns an empty Vault of the given Type, sourcing the new Vault from the defining FT contract - access(self) fun getEmptyVault(_ vaultType: Type): @{FungibleToken.Vault} { - return <- getAccount(vaultType.address!) - .contracts - .borrow<&{FungibleToken}>(name: vaultType.contractName!)! - .createEmptyVault(vaultType: vaultType) - } -} diff --git a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc b/cadence/contracts/internal-dependencies/interfaces/DFB.cdc deleted file mode 100644 index ec7ab5cb..00000000 --- a/cadence/contracts/internal-dependencies/interfaces/DFB.cdc +++ /dev/null @@ -1,769 +0,0 @@ -import "Burner" -import "ViewResolver" -import "FungibleToken" - -import "DFBUtils" - -/// DeFiBlocks Interfaces -/// -/// DeFiBlocks is a library of small DeFi components that act as glue to connect typical DeFi primitives (dexes, lending -/// pools, farms) into individual aggregations. -/// -/// The core component of DeFiBlocks is the “Connector”; a conduit between the more complex pieces of the DeFi puzzle. -/// Connectors aren't to do anything especially complex, but make it simple and straightforward to connect the -/// traditional DeFi pieces together into new, custom aggregations. -/// -/// Connectors should be thought of analogously with the small text processing tools of Unix that are mostly meant to be -/// connected with pipe operations instead of being operated individually. All Connectors are either a “Source” or -/// “Sink”. -/// -access(all) contract DFB { - - /* --- FIELDS --- */ - - /// The current ID assigned to UniqueIdentifiers as they are initialized - access(all) var currentID: UInt64 - - /* --- INTERFACE-LEVEL EVENTS --- */ - - /// Emitted when value is deposited to a Sink - access(all) event Deposited( - type: String, - amount: UFix64, - fromUUID: UInt64, - uniqueID: UInt64?, - sinkType: String - ) - /// Emitted when value is withdrawn from a Source - access(all) event Withdrawn( - type: String, - amount: UFix64, - withdrawnUUID: UInt64, - uniqueID: UInt64?, - sourceType: String - ) - /// Emitted when a Swapper executes a Swap - access(all) event Swapped( - inVault: String, - outVault: String, - inAmount: UFix64, - outAmount: UFix64, - inUUID: UInt64, - outUUID: UInt64, - uniqueID: UInt64?, - swapperType: String - ) - /// Emitted when an AutoBalancer is created - access(all) event CreatedAutoBalancer( - lowerThreshold: UFix64, - upperThreshold: UFix64, - balancerUUID: UInt64, - vaultType: String, - vaultUUID: UInt64, - uniqueID: UInt64? - ) - /// Emitted when AutoBalancer.rebalance() is called - access(all) event Rebalanced( - amount: UFix64, - value: UFix64, - unitOfAccount: String, - isSurplus: Bool, - vaultUUID: UInt64, - balancerUUID: UInt64, - address: Address?, - uniqueID: UInt64? - ) - - /* --- PUBLIC METHODS --- */ - - /// Returns an AutoBalancer wrapping the provided Vault. - /// - /// @param oracle: The oracle used to query deposited & withdrawn value and to determine if a rebalance should execute - /// @param vault: The Vault wrapped by the AutoBalancer - /// @param rebalanceRange: The percentage range from the AutoBalancer's base value at which a rebalance is executed - /// @param outSink: An optional DeFiBlocks Sink to which excess value is directed when rebalancing - /// @param inSource: An optional DeFiBlocks Source from which value is withdrawn to the inner vault when rebalancing - /// @param uniqueID: An optional DeFiBlocks UniqueIdentifier used for identifying rebalance events - /// - access(all) fun createAutoBalancer( - oracle: {PriceOracle}, - vault: @{FungibleToken.Vault}, - lowerThreshold: UFix64, - upperThreshold: UFix64, - rebalanceSink: {Sink}?, - rebalanceSource: {Source}?, - uniqueID: UniqueIdentifier? - ): @AutoBalancer { - let vaultUUID = vault.uuid - let ab <- create AutoBalancer( - lower: lowerThreshold, - upper: upperThreshold, - oracle: oracle, - vault: <-vault, - outSink: rebalanceSink, - inSource: rebalanceSource, - uniqueID: uniqueID - ) - emit CreatedAutoBalancer( - lowerThreshold: lowerThreshold, - upperThreshold: upperThreshold, - balancerUUID: ab.uuid, - vaultType: ab.vaultType().identifier, - vaultUUID: vaultUUID, - uniqueID: ab.id() - ) - return <- ab - } - - /* --- CONSTRUCTS --- */ - - /// This construct enables protocols to trace stack operations via DFB interface-level events, identifying them by - /// UniqueIdentifier IDs. Implementations should ensure that access to them is encapsulated by the structures they - /// are used to identify. - /// - access(all) struct UniqueIdentifier { - access(all) let id: UInt64 - - init() { - self.id = DFB.currentID - DFB.currentID = DFB.currentID + 1 - } - } - - /// A struct interface containing a UniqueIdentifier and convenience getter on the underlying ID value - /// - access(all) struct interface IdentifiableStruct { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: UniqueIdentifier? - /// Convenience method returning the inner UniqueIdentifier's id or `nil` if none is set. - /// NOTE: This interface method may be spoofed if the function is overridden, so callers should not rely on it - /// for critical identification - access(all) view fun id(): UInt64? { - return self.uniqueID?.id - } - } - - /// A Sink Connector (or just “Sink”) is analogous to the Fungible Token Receiver interface that accepts deposits of - /// funds. It differs from the standard Receiver interface in that it is a struct interface (instead of resource - /// interface) and allows for the graceful handling of Sinks that have a limited capacity on the amount they can - /// accept for deposit. Implementations should therefore avoid the possibility of reversion with graceful fallback - /// on unexpected conditions, executing no-ops instead of reverting. - /// - access(all) struct interface Sink : IdentifiableStruct { - /// Returns the Vault type accepted by this Sink - access(all) view fun getSinkType(): Type - /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity - access(all) fun minimumCapacity(): UFix64 - /// Deposits up to the Sink's capacity from the provided Vault - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - post { - DFB.emitDeposited( - type: from.getType().identifier, - beforeBalance: before(from.balance), - afterBalance: from.balance, - fromUUID: from.uuid, - uniqueID: self.uniqueID?.id, - sinkType: self.getType().identifier - ): "Unknown error emitting DFB.Withdrawn from Sink \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED") - } - } - } - - /// A Source Connector (or just “Source”) is analogous to the Fungible Token Provider interface that provides funds - /// on demand. It differs from the standard Provider interface in that it is a struct interface (instead of resource - /// interface) and allows for graceful handling of the case that the Source might not know exactly the total amount - /// of funds available to be withdrawn. Implementations should therefore avoid the possibility of reversion with - /// graceful fallback on unexpected conditions, executing no-ops or returning an empty Vault instead of reverting. - /// - access(all) struct interface Source : IdentifiableStruct { - /// Returns the Vault type provided by this Source - access(all) view fun getSourceType(): Type - /// Returns an estimate of how much of the associated Vault Type can be provided by this Source - access(all) fun minimumAvailable(): UFix64 - /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be - /// returned - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - post { - DFB.emitWithdrawn( - type: result.getType().identifier, - amount: result.balance, - withdrawnUUID: result.uuid, // toUUID - uniqueID: self.uniqueID?.id ?? nil, - sourceType: self.getType().identifier // maybe include - ): "Unknown error emitting DFB.Withdrawn from Source \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED") - } - } - } - - /// An interface for an estimate to be returned by a Swapper when asking for a swap estimate. This may be helpful - /// for passing additional parameters to a Swapper relevant to the use case. Implementations may choose to add - /// fields relevant to their Swapper implementation and downcast in swap() and/or swapBack() scope. - /// - access(all) struct interface Quote { - /// The quoted pre-swap Vault type - access(all) let inType: Type - /// The quoted post-swap Vault type - access(all) let outType: Type - /// The quoted amount of pre-swap currency - access(all) let inAmount: UFix64 - /// The quoted amount of post-swap currency for the defined inAmount - access(all) let outAmount: UFix64 - } - - /// A basic interface for a struct that swaps between tokens. Implementations may choose to adapt this interface - /// to fit any given swap protocol or set of protocols. - access(all) struct interface Swapper : IdentifiableStruct { - /// The type of Vault this Swapper accepts when performing a swap - access(all) view fun inType(): Type - /// The type of Vault this Swapper provides when performing a swap - access(all) view fun outType(): Type - /// The estimated amount required to provide a Vault with the desired output balance - access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {Quote} // fun quoteIn/Out - /// The estimated amount delivered out for a provided input balance - access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {Quote} - /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose - /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted - /// to use multiple Flow swap protocols. - access(all) fun swap(quote: {Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} { - pre { - inVault.getType() == self.inType(): - "Invalid vault provided for swap - \(inVault.getType().identifier) is not \(self.inType().identifier)" - (quote?.inType ?? inVault.getType()) == inVault.getType(): - "Quote.inType type \(quote!.inType.identifier) does not match the provided inVault \(inVault.getType().identifier)" - } - post { - emit Swapped( - inVault: before(inVault.getType().identifier), - outVault: result.getType().identifier, - inAmount: before(inVault.balance), - outAmount: result.balance, - inUUID: before(inVault.uuid), - outUUID: result.uuid, - uniqueID: self.uniqueID?.id ?? nil, - swapperType: self.getType().identifier - ) - } - } - /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose - /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted - /// to use multiple Flow swap protocols. - // TODO: Impl detail - accept quote that was just used by swap() but reverse the direction assuming swap() was just called - access(all) fun swapBack(quote: {Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} { - pre { - residual.getType() == self.outType(): - "Invalid vault provided for swapBack - \(residual.getType().identifier) is not \(self.outType().identifier)" - } - post { - emit Swapped( - inVault: before(residual.getType().identifier), - outVault: result.getType().identifier, - inAmount: before(residual.balance), - outAmount: result.balance, - inUUID: before(residual.uuid), - outUUID: result.uuid, - uniqueID: self.uniqueID?.id ?? nil, - swapperType: self.getType().identifier - ) - } - } - } - - /// An interface for a price oracle adapter. Implementations should adapt this interface to various price feed - /// oracles deployed on Flow - access(all) struct interface PriceOracle { - /// Returns the asset type serving as the price basis - e.g. USD in FLOW/USD - access(all) view fun unitOfAccount(): Type - /// Returns the latest price data for a given asset denominated in unitOfAccount() if available, otherwise `nil` - /// should be returned. Callers should note that although an optional is supported, implementations may choose - /// to revert. - access(all) fun price(ofToken: Type): UFix64? - } - - /// A resource interface containing a UniqueIdentifier an convenience getters about it - /// - access(all) resource interface IdentifiableResource { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: UniqueIdentifier? - /// Convenience method returning the inner UniqueIdentifier's id or `nil` if none is set. - /// NOTE: This interface method may be spoofed if the function is overridden, so callers should not rely on it - /// for critical identification - access(all) view fun id(): UInt64? { - return self.uniqueID?.id - } - } - - /// AutoBalancerSink - /// - /// A DeFiBlocks Sink enabling the deposit of funds to an underlying AutoBalancer resource. As written, this Source - /// may be used with externally defined AutoBalancer implementations - /// - access(all) struct AutoBalancerSink : Sink { - /// The Type this Sink accepts - access(self) let type: Type - /// An authorized Capability on the underlying AutoBalancer where funds are deposited - access(self) let autoBalancer: Capability<&AutoBalancer> - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: UniqueIdentifier? - - init(autoBalancer: Capability<&AutoBalancer>, uniqueID: UniqueIdentifier?) { - pre { - autoBalancer.check(): - "Invalid AutoBalancer Capability Provided" - } - self.type = autoBalancer.borrow()!.vaultType() - self.autoBalancer = autoBalancer - self.uniqueID = uniqueID - } - - /// Returns the Vault type accepted by this Sink - access(all) view fun getSinkType(): Type { - return self.type - } - /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity - access(all) fun minimumCapacity(): UFix64 { - if let ab = self.autoBalancer.borrow() { - return UFix64.max - ab.vaultBalance() - } - return 0.0 - } - /// Deposits up to the Sink's capacity from the provided Vault - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - if let ab = self.autoBalancer.borrow() { - ab.deposit(from: <- from.withdraw(amount: from.balance)) - } - return - } - } - - /// AutoBalancerSource - /// - /// A DeFiBlocks Source targeting an underlying AutoBalancer resource. As written, this Source may be used with - /// externally defined AutoBalancer implementations - /// - access(all) struct AutoBalancerSource : Source { - /// The Type this Source provides - access(self) let type: Type - /// An authorized Capability on the underlying AutoBalancer where funds are sourced - access(self) let autoBalancer: Capability - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: UniqueIdentifier? - - init(autoBalancer: Capability, uniqueID: UniqueIdentifier?) { - pre { - autoBalancer.check(): - "Invalid AutoBalancer Capability Provided" - } - self.type = autoBalancer.borrow()!.vaultType() - self.autoBalancer = autoBalancer - self.uniqueID = uniqueID - } - - /// Returns the Vault type provided by this Source - access(all) view fun getSourceType(): Type { - return self.type - } - /// Returns an estimate of how much of the associated Vault Type can be provided by this Source - access(all) fun minimumAvailable(): UFix64 { - if let ab = self.autoBalancer.borrow() { - return ab.vaultBalance() - } - return 0.0 - } - /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be - /// returned - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - if let ab = self.autoBalancer.borrow() { - return <-ab.withdraw( - amount: maxAmount <= ab.vaultBalance() ? maxAmount : ab.vaultBalance() - ) - } - return <- DFBUtils.getEmptyVault(self.type) - } - } - - /// Entitlement used by the AutoBalancer to set inner Sink and Source - access(all) entitlement Auto - access(all) entitlement Set - access(all) entitlement Get - - /// A resource interface designed to enable permissionless rebalancing of value around a wrapped Vault. An - /// AutoBalancer can be a critical component of DeFiBlocks stacks by allowing for strategies to compound, repay - /// loans or direct accumulated value to other sub-systems and/or user Vaults. - /// - access(all) resource AutoBalancer : IdentifiableResource, FungibleToken.Receiver, FungibleToken.Provider, ViewResolver.Resolver, Burner.Burnable { - /// The value in deposits & withdrawals over time denominated in oracle.unitOfAccount() - access(self) var _valueOfDeposits: UFix64 - /// The percentage low and high thresholds defining when a rebalance executes - access(self) var _rebalanceRange: [UFix64; 2] - /// Oracle used to track the baseValue for deposits & withdrawals over time - access(self) let _oracle: {PriceOracle} - /// The inner Vault's Type captured for the ResourceDestroyed event - access(self) let _vaultType: Type - /// Vault used to deposit & withdraw from made optional only so the Vault can be burned via Burner.burn() if the - /// AutoBalancer is burned and the Vault's burnCallback() can be called in the process - access(self) var _vault: @{FungibleToken.Vault}? - /// An optional Sink used to deposit excess funds from the inner Vault once the converted value exceeds the - /// rebalance range. This Sink may be used to compound yield into a position or direct excess value to an - /// external Vault - access(self) var _rebalanceSink: {Sink}? - /// An optional Source used to deposit excess funds to the inner Vault once the converted value is below the - /// rebalance range - access(self) var _rebalanceSource: {Source}? - /// Capability on this AutoBalancer instance - access(self) var _selfCap: Capability? - /// An optional UniqueIdentifier tying this AutoBalancer to a given stack - access(contract) let uniqueID: UniqueIdentifier? - - /// Emitted when the AutoBalancer is destroyed - access(all) event ResourceDestroyed( - uuid: UInt64 = self.uuid, - vaultType: String = self._vaultType.identifier, - balance: UFix64? = self._vault?.balance, - uniqueID: UInt64? = self.uniqueID?.id - ) - - init( - lower: UFix64, - upper: UFix64, - oracle: {PriceOracle}, - vault: @{FungibleToken.Vault}, - outSink: {Sink}?, - inSource: {Source}?, - uniqueID: UniqueIdentifier? - ) { - pre { - lower < upper && 0.01 <= lower && lower < 1.0 && 1.0 < upper && upper < 2.0: - "Invalid rebalanceRange [lower, upper]: [\(lower), \(upper)] - thresholds must be set such that 0.01 <= lower < 1.0 and 1.0 < upper < 2.0 relative to value of deposits" - vault.balance == 0.0: - "Vault \(vault.getType().identifier) has a non-zero balance - AutoBalancer must be initialized with an empty Vault" - DFBUtils.definingContractIsFungibleToken(vault.getType()): - "The contract defining Vault \(vault.getType().identifier) does not conform to FungibleToken contract interface" - } - assert(oracle.price(ofToken: vault.getType()) != nil, - message: "Provided Oracle \(oracle.getType().identifier) could not provide a price for vault \(vault.getType().identifier)") - self._valueOfDeposits = 0.0 - self._rebalanceRange = [lower, upper] - self._oracle = oracle - self._vault <- vault - self._vaultType = self._vault.getType() - self._rebalanceSink = outSink - self._rebalanceSource = inSource - self._selfCap = nil - self.uniqueID = uniqueID - } - - /* Core AutoBalancer Functionality */ - - /// Returns the balance of the inner Vault - /// - /// @return the current balance of the inner Vault - /// - access(all) view fun vaultBalance(): UFix64 { - return self._borrowVault().balance - } - - /// Returns the Type of the inner Vault - /// - /// @return the Type of the inner Vault - /// - access(all) view fun vaultType(): Type { - return self._borrowVault().getType() - } - - /// Returns the low and high rebalance thresholds as a fixed length UFix64 containing [low, high] - /// - /// @return a sorted fixed-length array containing the relative lower and upper thresholds conditioning - /// rebalance execution - /// - access(all) view fun rebalanceThresholds(): [UFix64; 2] { - return self._rebalanceRange - } - - /// Returns the value of all accounted deposits/withdraws as they have occurred denominated in unitOfAccount. - /// The returned value is the value as tracked historically, not necessarily the current value of the inner - /// Vault's balance. - /// - /// @return the historical value of deposits - /// - access(all) view fun valueOfDeposits(): UFix64 { - return self._valueOfDeposits - } - - /// Returns the token Type serving as the price basis of this AutoBalancer - /// - /// @return the price denomination of value of the underlying vault as returned from the inner PriceOracle - /// - access(all) view fun unitOfAccount(): Type { - return self._oracle.unitOfAccount() - } - - /// Returns the current value of the inner Vault's balance. If a price is not available from the AutoBalancer's - /// PriceOracle, `nil` is returned - /// - /// @return the current value of the inner's Vault's balance denominated in unitOfAccount() if a price is - /// available, `nil` otherwise - /// - access(all) fun currentValue(): UFix64? { - if let price = self._oracle.price(ofToken: self.vaultType()) { - return price * self._borrowVault().balance - } - return nil - } - - /// Convenience method issuing a Sink allowing for deposits to this AutoBalancer. If the AutoBalancer's - /// Capability on itself is not set or is invalid, `nil` is returned. - /// - /// @return a Sink routing deposits to this AutoBalancer - /// - access(all) fun createBalancerSink(): {Sink}? { - if self._selfCap == nil || !self._selfCap!.check() { - return nil - } - return AutoBalancerSink(autoBalancer: self._selfCap!, uniqueID: self.uniqueID) - } - - /// Convenience method issuing a Source enabling withdrawals from this AutoBalancer. If the AutoBalancer's - /// Capability on itself is not set or is invalid, `nil` is returned. - /// - /// @return a Source routing withdrawals from this AutoBalancer - /// - access(Get) fun createBalancerSource(): {Source}? { - if self._selfCap == nil || !self._selfCap!.check() { - return nil - } - return AutoBalancerSource(autoBalancer: self._selfCap!, uniqueID: self.uniqueID) - } - - /// A setter enabling an AutoBalancer to set a Sink to which overflow value should be deposited - /// - /// @param sink: The optional Sink DeFiBlocks connector from which funds are sourced when this AutoBalancer - /// current value rises above the upper threshold relative to its valueOfDeposits(). If `nil`, overflown - /// value will not rebalance - /// - access(Set) fun setSink(_ sink: {Sink}?) { - self._rebalanceSink = sink - } - - /// A setter enabling an AutoBalancer to set a Source from which underflow value should be withdrawn - /// - /// @param source: The optional Source DeFiBlocks connector from which funds are sourced when this AutoBalancer - /// current value falls below the lower threshold relative to its valueOfDeposits(). If `nil`, underflown - /// value will not rebalance - /// - access(Set) fun setSource(_ source: {Source}?) { - self._rebalanceSource = source - } - - /// Enables the setting of a Capability on the AutoBalancer for the distribution of Sinks & Sources targeting - /// the AutoBalancer instance. Due to the mechanisms of Capabilities, this must be done after the AutoBalancer - /// has been saved to account storage and an authorized Capability has been issued. - access(Set) fun setSelfCapability(_ cap: Capability) { - pre { - self._selfCap == nil || self._selfCap!.check() != true: - "Internal AutoBalancer Capability has been set and is still valid - cannot be re-assigned" - cap.check(): "Invalid AutoBalancer Capability provided" - self.getType() == cap.borrow()!.getType() && self.uuid == cap.borrow()!.uuid: - "Provided Capability does not target this AutoBalancer of type \(self.getType().identifier) with UUID \(self.uuid) - " - .concat("provided Capability for AutoBalancer of type \(cap.borrow()!.getType().identifier) with UUID \(cap.borrow()!.uuid)") - } - self._selfCap = cap - } - - /// Sets the rebalance range of this AutoBalancer - /// - /// @param range: a sorted array containing lower and upper thresholds that condition rebalance execution. The - /// thresholds must be values such that 0.01 <= range[0] < 1.0 && 1.0 < range[1] < 2.0 - /// - access(Set) fun setRebalanceRange(_ range: [UFix64; 2]) { - pre { - range[0] < range[1] && 0.01 <= range[0] && range[0] < 1.0 && 1.0 < range[1] && range[1] < 2.0: - "Invalid rebalanceRange [lower, upper]: [\(range[0]), \(range[1])] - thresholds must be set such that 0.01 <= range[0] < 1.0 and 1.0 < range[1] < 2.0 relative to value of deposits" - } - self._rebalanceRange = range - } - - /// Allows for external parties to call on the AutoBalancer and execute a rebalance according to it's rebalance - /// parameters. This method must be called by external party regularly in order for rebalancing to occur. - /// - /// @param force: if false, rebalance will occur only when beyond upper or lower thresholds; if true, rebalance - /// will execute as long as a price is available via the oracle and the current value is non-zero - /// - access(Auto) fun rebalance(force: Bool) { - let currentPrice = self._oracle.price(ofToken: self._vaultType) - if currentPrice == nil { - return // no price available -> do nothing - } - let currentValue = self.currentValue()! - // calculate the difference between the current value and the historical value of deposits - var valueDiff: UFix64 = currentValue < self._valueOfDeposits ? self._valueOfDeposits - currentValue : currentValue - self._valueOfDeposits - // if deficit detected, choose lower threshold, otherwise choose upper threshold - let isDeficit = self._valueOfDeposits < currentValue - let threshold = isDeficit ? self._rebalanceRange[0] : self._rebalanceRange[1] - if currentPrice == 0.0 || valueDiff == 0.0 || ((valueDiff / self._valueOfDeposits) < threshold && !force) { - // division by zero, no difference, or difference does not exceed rebalance ratio & not forced -> no-op - return - } - - let vault = self._borrowVault() - var amount = valueDiff / currentPrice! - var executed = false - if isDeficit && self._rebalanceSource != nil { - // rebalance back up to baseline sourcing funds from _rebalanceSource - vault.deposit(from: <- self._rebalanceSource!.withdrawAvailable(maxAmount: amount)) - executed = true - } else if !isDeficit && self._rebalanceSink != nil { - // rebalance back down to baseline depositing excess to _rebalanceSink - if amount > vault.balance { - amount = vault.balance // protect underflow - } - let surplus <- vault.withdraw(amount: amount) - self._rebalanceSink!.depositCapacity(from: &surplus as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - executed = true - if surplus.balance == 0.0 { - Burner.burn(<-surplus) // could destroy - } else { - amount = amount - surplus.balance // update the rebalanced amount - valueDiff = valueDiff - (surplus.balance * currentPrice!) // update the value difference - vault.deposit(from: <-surplus) // deposit any excess not taken by the Sink - } - } - // emit event only if rebalance was executed - if executed { - emit Rebalanced( - amount: amount, - value: valueDiff, - unitOfAccount: self.unitOfAccount().identifier, - isSurplus: !isDeficit, - vaultUUID: self._borrowVault().uuid, - balancerUUID: self.uuid, - address: self.owner?.address, - uniqueID: self.id() - ) - } - } - - /* ViewResolver.Resolver conformance */ - - /// Passthrough to inner Vault's view Types - access(all) view fun getViews(): [Type] { - return self._borrowVault().getViews() - } - - /// Passthrough to inner Vault's view resolution - access(all) fun resolveView(_ view: Type): AnyStruct? { - return self._borrowVault().resolveView(view) - } - - /* FungibleToken.Receiver & .Provider conformance */ - - /// Only the nested Vault type is supported by this AutoBalancer for deposits & withdrawal for the sake of - /// single asset accounting - access(all) view fun getSupportedVaultTypes(): {Type: Bool} { - return { self.vaultType(): true } - } - - /// True if the provided Type is the nested Vault Type, false otherwise - access(all) view fun isSupportedVaultType(type: Type): Bool { - return self.getSupportedVaultTypes()[type] == true - } - - /// Passthrough to the inner Vault's isAvailableToWithdraw() method - access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { - return self._borrowVault().isAvailableToWithdraw(amount: amount) - } - - /// Deposits the provided Vault to the nested Vault if it is of the same Type, reverting otherwise. In the - /// process, the current value of the deposited amount (denominated in unitOfAccount) increments the - /// AutoBalancer's baseValue. If a price is not available via the internal PriceOracle, an average price is - /// calculated base on the inner vault balance & valueOfDeposits and valueOfDeposits is incremented by the - /// value of the deposited vault on the basis of that average - access(all) fun deposit(from: @{FungibleToken.Vault}) { - pre { - from.getType() == self.vaultType(): - "Invalid Vault type \(from.getType().identifier) deposited - this AutoBalancer only accepts \(self.vaultType().identifier)" - } - // if no price available use an average price based on historical value of deposits and inner vault balance - let price = self._oracle.price(ofToken: from.getType()) ?? (self.valueOfDeposits() / self.vaultBalance()) - self._valueOfDeposits = self._valueOfDeposits + (from.balance * price) - self._borrowVault().deposit(from: <-from) - } - - /// Returns the requested amount of the nested Vault type, reducing the baseValue by the current value - /// (denominated in unitOfAccount) of the token amount. The AutoBalancer's valueOfDeposits is decremented - /// in proportion to the amount withdrawn relative to the inner Vault's balance - access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { - // adjust historical value of deposits proportionate to the amount withdrawn & return withdrawn vault - let adjustment = (1.0 - amount / self.vaultBalance()) * self._valueOfDeposits - self._valueOfDeposits = adjustment <= self._valueOfDeposits ? self._valueOfDeposits - adjustment : 0.0 - return <- self._borrowVault().withdraw(amount: amount) - } - - /* Burnable.Burner conformance */ - - /// Executed in Burner.burn(). Passes along the inner vault to be burned, executing the inner Vault's - /// burnCallback() logic - access(contract) fun burnCallback() { - let vault <- self._vault <- nil - Burner.burn(<-vault) // executes the inner Vault's burnCallback() - } - - /* Internal */ - - access(self) view fun _borrowVault(): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} { - return (&self._vault)! - } - } - - /* --- INTERNAL CONDITIONAL EVENT EMITTERS --- */ - - /// Emits Deposited event if a change in balance is detected - access(self) view fun emitDeposited( - type: String, - beforeBalance: UFix64, - afterBalance: UFix64, - fromUUID: UInt64, - uniqueID: UInt64?, - sinkType: String - ): Bool { - if beforeBalance == afterBalance { - return true - } - emit Deposited( - type: type, - amount: beforeBalance > afterBalance ? beforeBalance - afterBalance : afterBalance - beforeBalance, - fromUUID: fromUUID, - uniqueID: uniqueID, - sinkType: sinkType - ) - return true - } - - /// Emits Withdrawn event if a change in balance is detected - access(self) view fun emitWithdrawn( - type: String, - amount: UFix64, - withdrawnUUID: UInt64, - uniqueID: UInt64?, - sourceType: String - ): Bool { - if amount == 0.0 { - return true - } - emit Withdrawn( - type: type, - amount: amount, - withdrawnUUID: withdrawnUUID, - uniqueID: uniqueID, - sourceType: sourceType - ) - return true - } - - init() { - self.currentID = 0 - } -} diff --git a/cadence/contracts/internal-dependencies/tokens/USDA.cdc b/cadence/contracts/internal-dependencies/tokens/MOET.cdc similarity index 85% rename from cadence/contracts/internal-dependencies/tokens/USDA.cdc rename to cadence/contracts/internal-dependencies/tokens/MOET.cdc index c8f9081a..4ab12018 100644 --- a/cadence/contracts/internal-dependencies/tokens/USDA.cdc +++ b/cadence/contracts/internal-dependencies/tokens/MOET.cdc @@ -6,9 +6,9 @@ import "FungibleTokenMetadataViews" /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// -access(all) contract USDA : FungibleToken { +access(all) contract MOET : FungibleToken { - /// Total supply of USDA in existence + /// Total supply of MOET in existence access(all) var totalSupply: UFix64 /// Storage and Public Paths @@ -29,7 +29,7 @@ access(all) contract USDA : FungibleToken { /// and store the returned Vault in their storage in order to allow their /// account to be able to receive deposits of this token type. /// - access(all) fun createEmptyVault(vaultType: Type): @USDA.Vault { + access(all) fun createEmptyVault(vaultType: Type): @MOET.Vault { return <- create Vault(balance: 0.0) } @@ -58,9 +58,9 @@ access(all) contract USDA : FungibleToken { ) let medias = MetadataViews.Medias([media]) return FungibleTokenMetadataViews.FTDisplay( - name: "AlpenFlow USD", - symbol: "USDA", - description: "A mocked version of AlpenFlow USD", + name: "TidalProtocol USD", + symbol: "MOET", + description: "A mocked version of TidalProtocol stablecoin", externalURL: MetadataViews.ExternalURL("https://flow.com"), logos: medias, socials: { @@ -72,15 +72,15 @@ access(all) contract USDA : FungibleToken { storagePath: self.VaultStoragePath, receiverPath: self.ReceiverPublicPath, metadataPath: self.VaultPublicPath, - receiverLinkedType: Type<&USDA.Vault>(), - metadataLinkedType: Type<&USDA.Vault>(), + receiverLinkedType: Type<&MOET.Vault>(), + metadataLinkedType: Type<&MOET.Vault>(), createEmptyVaultFunction: (fun(): @{FungibleToken.Vault} { - return <-USDA.createEmptyVault(vaultType: Type<@USDA.Vault>()) + return <-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()) }) ) case Type(): return FungibleTokenMetadataViews.TotalSupply( - totalSupply: USDA.totalSupply + totalSupply: MOET.totalSupply ) } return nil @@ -115,17 +115,17 @@ access(all) contract USDA : FungibleToken { /// Called when a fungible token is burned via the `Burner.burn()` method access(contract) fun burnCallback() { if self.balance > 0.0 { - USDA.totalSupply = USDA.totalSupply - self.balance + MOET.totalSupply = MOET.totalSupply - self.balance } self.balance = 0.0 } access(all) view fun getViews(): [Type] { - return USDA.getContractViews(resourceType: nil) + return MOET.getContractViews(resourceType: nil) } access(all) fun resolveView(_ view: Type): AnyStruct? { - return USDA.resolveContractView(resourceType: nil, viewType: view) + return MOET.resolveContractView(resourceType: nil, viewType: view) } access(all) view fun getSupportedVaultTypes(): {Type: Bool} { @@ -142,18 +142,18 @@ access(all) contract USDA : FungibleToken { return amount <= self.balance } - access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @USDA.Vault { + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @MOET.Vault { self.balance = self.balance - amount return <-create Vault(balance: amount) } access(all) fun deposit(from: @{FungibleToken.Vault}) { - let vault <- from as! @USDA.Vault + let vault <- from as! @MOET.Vault self.balance = self.balance + vault.balance destroy vault } - access(all) fun createEmptyVault(): @USDA.Vault { + access(all) fun createEmptyVault(): @MOET.Vault { return <-create Vault(balance: 0.0) } } @@ -175,8 +175,8 @@ access(all) contract USDA : FungibleToken { /// Function that mints new tokens, adds them to the total supply, /// and returns them to the calling context. /// - access(all) fun mintTokens(amount: UFix64): @USDA.Vault { - USDA.totalSupply = USDA.totalSupply + amount + access(all) fun mintTokens(amount: UFix64): @MOET.Vault { + MOET.totalSupply = MOET.totalSupply + amount let vault <-create Vault(balance: amount) emit Minted(type: vault.getType().identifier, amount: amount, toUUID: vault.uuid, minterUUID: self.uuid) return <-vault @@ -188,10 +188,10 @@ access(all) contract USDA : FungibleToken { self.totalSupply = 0.0 let address = self.account.address - self.VaultStoragePath = StoragePath(identifier: "usdaTokenVault_\(address)")! - self.VaultPublicPath = PublicPath(identifier: "usdaTokenVault_\(address)")! - self.ReceiverPublicPath = PublicPath(identifier: "usdaTokenReceiver_\(address)")! - self.AdminStoragePath = StoragePath(identifier: "usdaTokenAdmin_\(address)")! + self.VaultStoragePath = StoragePath(identifier: "moetTokenVault_\(address)")! + self.VaultPublicPath = PublicPath(identifier: "moetTokenVault_\(address)")! + self.ReceiverPublicPath = PublicPath(identifier: "moetTokenReceiver_\(address)")! + self.AdminStoragePath = StoragePath(identifier: "moetTokenAdmin_\(address)")! // Create a public capability to the stored Vault that exposes @@ -199,9 +199,9 @@ access(all) contract USDA : FungibleToken { // and the `balance` method through the `Balance` interface // self.account.storage.save(<-create Vault(balance: self.totalSupply), to: self.VaultStoragePath) - let vaultCap = self.account.capabilities.storage.issue<&USDA.Vault>(self.VaultStoragePath) + let vaultCap = self.account.capabilities.storage.issue<&MOET.Vault>(self.VaultStoragePath) self.account.capabilities.publish(vaultCap, at: self.VaultPublicPath) - let receiverCap = self.account.capabilities.storage.issue<&USDA.Vault>(self.VaultStoragePath) + let receiverCap = self.account.capabilities.storage.issue<&MOET.Vault>(self.VaultStoragePath) self.account.capabilities.publish(receiverCap, at: self.ReceiverPublicPath) // Create a Minter & mint the initial supply of tokens to the contract account's Vault diff --git a/flow.json b/flow.json index e89eaac1..7a962e03 100644 --- a/flow.json +++ b/flow.json @@ -1,7 +1,7 @@ { "contracts": { - "AlpenFlow": { - "source": "cadence/contracts/internal-dependencies/AlpenFlow.cdc", + "TidalProtocol": { + "source": "cadence/contracts/internal-dependencies/TidalProtocol.cdc", "aliases": { "testing": "0000000000000007" } @@ -54,7 +54,7 @@ "testing": "0000000000000008" } }, - "USDA": { + "MOET": { "source": "cadence/contracts/internal-dependencies/tokens/USDA.cdc", "aliases": { "testing": "0000000000000007" @@ -164,7 +164,7 @@ "FungibleTokenStack", "SwapStack", { - "name": "USDA", + "name": "MOET", "args": [ { "value": "1000000.0", From a2d335ac984af5caac2ee9f42d59e2010434fb23 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:48:34 -0600 Subject: [PATCH 24/80] rename Tidal contract to TidalYield --- .../contracts/{Tidal.cdc => TidalYield.cdc} | 4 ++-- cadence/scripts/tidal/get_tide_ids.cdc | 4 ++-- cadence/transactions/tidal/close_tide.cdc | 8 ++++---- .../transactions/tidal/deposit_to_tide.cdc | 8 ++++---- cadence/transactions/tidal/open_tide.cdc | 20 +++++++++---------- cadence/transactions/tidal/setup.cdc | 20 +++++++++---------- .../transactions/tidal/withdraw_from_tide.cdc | 8 ++++---- flow.json | 6 +++--- 8 files changed, 39 insertions(+), 39 deletions(-) rename cadence/contracts/{Tidal.cdc => TidalYield.cdc} (98%) diff --git a/cadence/contracts/Tidal.cdc b/cadence/contracts/TidalYield.cdc similarity index 98% rename from cadence/contracts/Tidal.cdc rename to cadence/contracts/TidalYield.cdc index dd92c99a..6db718bf 100644 --- a/cadence/contracts/Tidal.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -9,7 +9,7 @@ import "TidalStrategies" /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// -access(all) contract Tidal { +access(all) contract TidalYield { access(all) let TideManagerStoragePath: StoragePath access(all) let TideManagerPublicPath: PublicPath @@ -178,7 +178,7 @@ access(all) contract Tidal { } init() { - let pathIdentifier = "TidalTideManager_\(self.account.address)" + let pathIdentifier = "TidalYieldTideManager_\(self.account.address)" self.TideManagerStoragePath = StoragePath(identifier: pathIdentifier)! self.TideManagerPublicPath = PublicPath(identifier: pathIdentifier)! } diff --git a/cadence/scripts/tidal/get_tide_ids.cdc b/cadence/scripts/tidal/get_tide_ids.cdc index 48d41849..a214161a 100644 --- a/cadence/scripts/tidal/get_tide_ids.cdc +++ b/cadence/scripts/tidal/get_tide_ids.cdc @@ -1,4 +1,4 @@ -import "Tidal" +import "TidalYield" /// Retrieves the IDs of Tides configured at the provided address or `nil` if a TideManager is not stored /// @@ -7,6 +7,6 @@ import "Tidal" /// @return A UInt64 array of all Tide IDs stored in the account's TideManager access(all) fun main(address: Address): [UInt64]? { - return getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) + return getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) ?.getIDs() } diff --git a/cadence/transactions/tidal/close_tide.cdc b/cadence/transactions/tidal/close_tide.cdc index e7e596e4..d51035f6 100644 --- a/cadence/transactions/tidal/close_tide.cdc +++ b/cadence/transactions/tidal/close_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" /// Withdraws the full balance from an existing Tide stored in the signer's TideManager and closes the Tide. If the /// signer does not yet have a Vault of the withdrawn Type, one is configured. @@ -10,13 +10,13 @@ import "Tidal" /// @param id: The Tide.id() of the Tide from which the full balance will be withdrawn /// transaction(id: UInt64) { - let manager: auth(Tidal.Owner) &Tidal.TideManager + let manager: auth(TidalYield.Owner) &TidalYield.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal/deposit_to_tide.cdc b/cadence/transactions/tidal/deposit_to_tide.cdc index eacaa9d6..c48431c1 100644 --- a/cadence/transactions/tidal/deposit_to_tide.cdc +++ b/cadence/transactions/tidal/deposit_to_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" /// Deposits to an existing Tide stored in the signer's TideManager /// @@ -10,13 +10,13 @@ import "Tidal" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: &Tidal.TideManager + let manager: &TidalYield.TideManager let depositVault: @{FungibleToken.Vault} prepare(signer: auth(BorrowValue) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal/open_tide.cdc b/cadence/transactions/tidal/open_tide.cdc index c2c5fad2..18bc9d17 100644 --- a/cadence/transactions/tidal/open_tide.cdc +++ b/cadence/transactions/tidal/open_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" /// Opens a new Tide in the Tidal platform, funding the Tide with the specified Vault and amount /// @@ -11,7 +11,7 @@ import "Tidal" /// @param amount: The amount to deposit into the new Tide /// transaction(vaultIdentifier: String, amount: UFix64) { - let manager: &Tidal.TideManager + let manager: &TidalYield.TideManager let depositVault: @{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { @@ -32,17 +32,17 @@ transaction(vaultIdentifier: String, amount: UFix64) { self.depositVault <- sourceVault.withdraw(amount: amount) // configure the TideManager if needed - if signer.storage.type(at: Tidal.TideManagerStoragePath) == nil { - signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) - signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) + if signer.storage.type(at: TidalYield.TideManagerStoragePath) == nil { + signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) + signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue( - Tidal.TideManagerStoragePath + signer.capabilities.storage.issue( + TidalYield.TideManagerStoragePath ) } - self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") } execute { diff --git a/cadence/transactions/tidal/setup.cdc b/cadence/transactions/tidal/setup.cdc index 37369730..1238c587 100644 --- a/cadence/transactions/tidal/setup.cdc +++ b/cadence/transactions/tidal/setup.cdc @@ -2,27 +2,27 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" -/// Configures a Tidal.TideManager at the canonical path. If one is already configured, the transaction no-ops +/// Configures a TidalYield.TideManager at the canonical path. If one is already configured, the transaction no-ops /// transaction { prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { - if signer.storage.type(at: Tidal.TideManagerStoragePath) == Type<@Tidal.TideManager>() { + if signer.storage.type(at: TidalYield.TideManagerStoragePath) == Type<@TidalYield.TideManager>() { return // early return if TideManager is found } // configure the TideManager - signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) - signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) + signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) + signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue(Tidal.TideManagerStoragePath) + signer.capabilities.storage.issue(TidalYield.TideManagerStoragePath) // confirm setup of TideManager at canonical path - let storedType = signer.storage.type(at: Tidal.TideManagerStoragePath) ?? Type() - if storedType != Type<@Tidal.TideManager>() { - panic("Setup was unsuccessful - Expected TideManager at \(Tidal.TideManagerStoragePath) but found \(storedType.identifier)") + let storedType = signer.storage.type(at: TidalYield.TideManagerStoragePath) ?? Type() + if storedType != Type<@TidalYield.TideManager>() { + panic("Setup was unsuccessful - Expected TideManager at \(TidalYield.TideManagerStoragePath) but found \(storedType.identifier)") } } } diff --git a/cadence/transactions/tidal/withdraw_from_tide.cdc b/cadence/transactions/tidal/withdraw_from_tide.cdc index c2db4bd1..3413173d 100644 --- a/cadence/transactions/tidal/withdraw_from_tide.cdc +++ b/cadence/transactions/tidal/withdraw_from_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" /// Withdraws from an existing Tide stored in the signer's TideManager. If the signer does not yet have a Vault of the /// withdrawn Type, one is configured. @@ -11,13 +11,13 @@ import "Tidal" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: auth(Tidal.Owner) &Tidal.TideManager + let manager: auth(TidalYield.Owner) &TidalYield.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/flow.json b/flow.json index 7a962e03..a866fbda 100644 --- a/flow.json +++ b/flow.json @@ -42,8 +42,8 @@ "testing": "0000000000000007" } }, - "Tidal": { - "source": "cadence/contracts/Tidal.cdc", + "TidalYield": { + "source": "cadence/contracts/TidalYield.cdc", "aliases": { "testing": "0000000000000008" } @@ -191,7 +191,7 @@ ] }, "MockSwapper", - "Tidal" + "TidalYield" ] } } From 8e87097bb5ea67be6844502924cc042de2fc43c9 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:23:10 -0600 Subject: [PATCH 25/80] update TidalStrategies StrategyFactory & StrategyBuilder constructs --- cadence/contracts/TidalStrategies.cdc | 187 +++++++++++++++++++++----- 1 file changed, 152 insertions(+), 35 deletions(-) diff --git a/cadence/contracts/TidalStrategies.cdc b/cadence/contracts/TidalStrategies.cdc index 01d45272..e1faecbb 100644 --- a/cadence/contracts/TidalStrategies.cdc +++ b/cadence/contracts/TidalStrategies.cdc @@ -1,10 +1,16 @@ import "FungibleToken" +import "FlowToken" +import "DFBUtils" import "DFB" +// TODO: rename to TidalYieldStrategies access(all) contract TidalStrategies { - - access(self) let strategies: {UInt64: {Strategy}} + + access(all) let FactoryStoragePath: StoragePath + access(all) let FactoryPublicPath: PublicPath + + /* --- PUBLIC METHODS --- */ access(all) view fun getCollateralTypes(forStrategy: UInt64): [Type]? { return self.strategies[forStrategy]?.getSupportedCollateralTypes() ?? nil @@ -14,71 +20,182 @@ access(all) contract TidalStrategies { return self.strategies[forStrategy]?.isSupportedCollateralType(type) ?? nil } - access(all) fun createStrategy(number: UInt64, vault: @{FungibleToken.Vault}): {Strategy} { + access(all) fun createStrategy(type: Type, vault: @{FungibleToken.Vault}): {Strategy} { destroy vault // TODO: Update vault handling return DummyStrategy(id: DFB.UniqueIdentifier()) } + access(all) fun createStrategyFactory(): @StrategyFactory { + return <- create StrategyFactory() + } + + /* --- CONSTRUCTS --- */ + + /// A StrategyBuilder is responsible for stacking DeFiBlocks connectors in a manner that composes a final Strategy. + /// Since DeFiBlock Sink/Source only support single assets and some Strategies may be multi-asset, we deal with + /// building a Strategy distinctly from encapsulating the top-level DFB connectors acting as entrypoints in to the + /// composed DeFiBlocks infrastructure. + /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away + /// with the added layer of abstraction introduced by a StrategyBuilder. access(all) struct interface StrategyBuilder { - access(all) fun createStrategy(funds: @{FungibleToken.Vault}): {Strategy} + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} } - - access(all) struct interface StrategyInfo { + + /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of composed + /// DeFiBlocks components. These compositions are intended to capitalize on some yield-bearing opportunity so that + /// a Strategy bears yield on that which is deposited into it, albeit not without some risk. A Strategy then can be + /// thought of as the top-level of a nesting of DeFiBlocks connectors & adapters where one can deposit & withdraw + /// funds into the composed DeFi workflows + /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away + /// with the added layer of abstraction introduced by a StrategyBuilder. + access(all) struct interface Strategy : DFB.IdentifiableStruct { access(all) view fun getSupportedCollateralTypes(): [Type] access(all) view fun isSupportedCollateralType(_ type: Type): Bool + access(all) fun availableBalance(ofToken: Type): UFix64 + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} } - access(all) struct Strategy : StrategyInfo, DFB.Sink, DFB.Source {} + access(all) struct DummySink : DFB.Sink { + access(contract) let uniqueID: DFB.UniqueIdentifier? + init(_ id: DFB.UniqueIdentifier?) { + self.uniqueID = id + } + access(all) view fun getSinkType(): Type { + return Type<@FlowToken.Vault>() + } + access(all) fun minimumCapacity(): UFix64 { + return 0.0 + } + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + return + } + } + access(all) struct DummySource : DFB.Source { + access(contract) let uniqueID: DFB.UniqueIdentifier? + init(_ id: DFB.UniqueIdentifier?) { + self.uniqueID = id + } + access(all) view fun getSourceType(): Type { + return Type<@FlowToken.Vault>() + } + access(all) fun minimumAvailable(): UFix64 { + return 0.0 + } + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + return <- DFBUtils.getEmptyVault(self.getSourceType()) + } + } access(all) struct DummyStrategy : Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? - access(self) var sink: {DFB.Sink}? // TODO: update from optional - access(self) var source: {DFB.Source}? // TODO: update from optional + access(self) var sink: {DFB.Sink} + access(self) var source: {DFB.Source} - init(id: DFB.UniqueIdentifier?) { + init(id: DFB.UniqueIdentifier?, sink: {DFB.Sink}, source: {DFB.Source}) { self.uniqueID = id - self.sink = nil - self.source = nil + self.sink = sink + self.source = source } access(all) view fun getSupportedCollateralTypes(): [Type] { - return [self.sink!.getSinkType()] + return [self.sink.getSinkType()] } access(all) view fun isSupportedCollateralType(_ type: Type): Bool { - return self.sink!.getSinkType() == type + return self.sink.getSinkType() == type } - /// Returns the Vault type accepted by this Sink - access(all) view fun getSinkType(): Type { - return self.sink!.getSinkType() + /// Returns the amount available for withdrawal via the inner Source + access(all) fun availableBalance(ofToken: Type): UFix64 { + return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 } - /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity - access(all) fun minimumCapacity(): UFix64 { - return self.sink!.minimumCapacity() + + /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + self.sink.depositCapacity(from: from) } - /// Deposits up to the Sink's capacity from the provided Vault - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - self.sink!.depositCapacity(from: from) + + /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, + /// an empty Vault is returned. + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { + if ofToken != self.source.getSourceType() { + return <- DFBUtils.getEmptyVault(ofToken) + } + return <- self.source.withdrawAvailable(maxAmount: maxAmount) } - /// Returns the Vault type provided by this Source - access(all) view fun getSourceType(): Type { - return self.source!.getSourceType() + } + + access(all) struct DummyStrategyBuilder : StrategyBuilder { + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { + return [] } - /// Returns an estimate of how much of the associated Vault Type can be provided by this Source - access(all) fun minimumAvailable(): UFix64 { - return self.source!.minimumAvailable() + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { + return [] } - /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be - /// returned - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - return <- self.source!.withdrawAvailable(maxAmount: maxAmount) + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { + let id = DFB.UniqueIdentifier() + let strat = DummyStrategy( + id: id, + sink: DummySink(id), + source: DummySource(id) + ) + strat.deposit(from: &withFunds as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + destroy withFunds + return strat } } + access(all) resource StrategyFactory { + /// The strategies this factory can build + access(self) let builders: {Type: {StrategyBuilder}} + + init() { + self.builders = {} + } + + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { + return self.builders[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? [] + } + + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { + return self.builders[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? [] + } + + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { + pre { + self.builders[type] != nil: "Strategy \(type.identifier) is unsupported" + } + return self.builders[type]!.createStrategy(type, withFunds: <-withFunds) + } + + access(Mutate) fun setStrategyBuilder(_ strategy: Type, builder: {StrategyBuilder}) { + self.builders[strategy] = builder + } + + access(Mutate) fun removeStrategy(_ strategy: Type): Bool { + return self.builders.remove(key: strategy) != nil + } + } + + access(self) view fun _borrowFactory(): &StrategyFactory { + return self.account.storage.borrow<&StrategyFactory>(from: self.FactoryStoragePath) + ?? panic("Could not borrow reference to StrategyFactory from \(self.FactoryStoragePath)") + } + + init() { - self.strategies = {} + let pathIdentifier = "TidalYieldStrategyFactory_\(self.account.address)" + self.FactoryStoragePath = StoragePath(identifier: pathIdentifier)! + self.FactoryPublicPath = PublicPath(identifier: pathIdentifier)! + + // configure a StrategyFactory in storage and publish a public Capability + self.account.storage.save(<-create StrategyFactory(), to: self.FactoryStoragePath) + let cap = self.account.capabilities.storage.issue<&StrategyFactory>(self.FactoryStoragePath) + self.account.capabilities.publish(cap, at: self.FactoryPublicPath) } -} \ No newline at end of file +} From c6f0bb5b9a90ee005fcab2cfbaf00b9e2e847b8d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:24:31 -0600 Subject: [PATCH 26/80] rename TidalStrategies to TidalYieldFactory --- .../contracts/{TidalStrategies.cdc => TidalYieldFactory.cdc} | 5 +++-- flow.json | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) rename cadence/contracts/{TidalStrategies.cdc => TidalYieldFactory.cdc} (98%) diff --git a/cadence/contracts/TidalStrategies.cdc b/cadence/contracts/TidalYieldFactory.cdc similarity index 98% rename from cadence/contracts/TidalStrategies.cdc rename to cadence/contracts/TidalYieldFactory.cdc index e1faecbb..1dc8231a 100644 --- a/cadence/contracts/TidalStrategies.cdc +++ b/cadence/contracts/TidalYieldFactory.cdc @@ -4,8 +4,9 @@ import "FlowToken" import "DFBUtils" import "DFB" -// TODO: rename to TidalYieldStrategies -access(all) contract TidalStrategies { +/// This contract is used by TidalYield to manage supported Strategies and create Factories for new Tides. +/// +access(all) contract TidalYieldFactory { access(all) let FactoryStoragePath: StoragePath access(all) let FactoryPublicPath: PublicPath diff --git a/flow.json b/flow.json index a866fbda..981b17e3 100644 --- a/flow.json +++ b/flow.json @@ -48,8 +48,8 @@ "testing": "0000000000000008" } }, - "TidalStrategies": { - "source": "cadence/contracts/TidalStrategies.cdc", + "TidalYieldFactory": { + "source": "cadence/contracts/TidalYieldFactory.cdc", "aliases": { "testing": "0000000000000008" } From f91bb25c86a7a580e2bf66c0c31fd5895dc8ce13 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:33:57 -0600 Subject: [PATCH 27/80] update contract comments --- cadence/contracts/TidalYieldFactory.cdc | 43 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/cadence/contracts/TidalYieldFactory.cdc b/cadence/contracts/TidalYieldFactory.cdc index 1dc8231a..0ebc0ecf 100644 --- a/cadence/contracts/TidalYieldFactory.cdc +++ b/cadence/contracts/TidalYieldFactory.cdc @@ -13,19 +13,18 @@ access(all) contract TidalYieldFactory { /* --- PUBLIC METHODS --- */ - access(all) view fun getCollateralTypes(forStrategy: UInt64): [Type]? { - return self.strategies[forStrategy]?.getSupportedCollateralTypes() ?? nil + access(all) view fun getSupportedStrategies(): [Type] { + return self._borrowFactory().getSupportedStrategies() } - - access(all) view fun isSupportedCollateralType(_ type: Type, forStrategy: UInt64): Bool? { - return self.strategies[forStrategy]?.isSupportedCollateralType(type) ?? nil + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { + return self._borrowFactory().getSupportedInitializationVaults(forStrategy: forStrategy) } - - access(all) fun createStrategy(type: Type, vault: @{FungibleToken.Vault}): {Strategy} { - destroy vault // TODO: Update vault handling - return DummyStrategy(id: DFB.UniqueIdentifier()) + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { + return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) + } + access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { + return self._borrowFactory().createStrategy(type, withFunds: <-withFunds) } - access(all) fun createStrategyFactory(): @StrategyFactory { return <- create StrategyFactory() } @@ -39,8 +38,12 @@ access(all) contract TidalYieldFactory { /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyBuilder. access(all) struct interface StrategyBuilder { + /// Returns the Vault types which can be used to initialize a given Strategy access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] + /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the + /// provided Vault type access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] + /// Composes a Strategy of the given type with the provided funds access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} } @@ -52,11 +55,21 @@ access(all) contract TidalYieldFactory { /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyBuilder. access(all) struct interface Strategy : DFB.IdentifiableStruct { + /// Returns the type of Vaults that this Strategy instance can handle access(all) view fun getSupportedCollateralTypes(): [Type] + /// Returns whether the provided Vault type is supported by this Strategy instance access(all) view fun isSupportedCollateralType(_ type: Type): Bool + /// Returns the balance of the given token available for withdrawal. Note that this may be an estimate due to + /// the lack of guarantees inherent to DeFiBlocks Sources access(all) fun availableBalance(ofToken: Type): UFix64 + /// Deposits up to the balance of the referenced Vault into this Strategy access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} + /// Withdraws from this Strategy and returns the resulting Vault of the requested token Type + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { + post { + result.getType() == ofToken: "Invalid Vault returns - requests \(ofToken.identifier) but returned \(result.getType().identifier)" + } + } } access(all) struct DummySink : DFB.Sink { @@ -159,6 +172,10 @@ access(all) contract TidalYieldFactory { self.builders = {} } + access(all) view fun getSupportedStrategies(): [Type] { + return self.builders.keys + } + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { return self.builders[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? [] } @@ -183,7 +200,9 @@ access(all) contract TidalYieldFactory { } } - access(self) view fun _borrowFactory(): &StrategyFactory { + /* --- INTERNAL METHODS --- */ + + access(account) view fun _borrowFactory(): &StrategyFactory { return self.account.storage.borrow<&StrategyFactory>(from: self.FactoryStoragePath) ?? panic("Could not borrow reference to StrategyFactory from \(self.FactoryStoragePath)") } From b02a551d2dec7f7cbe422aa0d5d416eb893a2bc2 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:37:47 -0600 Subject: [PATCH 28/80] move mocke strategy components from TidalYieldFactory to MockStrategy --- cadence/contracts/TidalYieldFactory.cdc | 92 ------------------ cadence/contracts/TidalYieldStrategies.cdc | 2 + cadence/contracts/mocks/MockStrategy.cdc | 106 +++++++++++++++++++++ flow.json | 6 ++ 4 files changed, 114 insertions(+), 92 deletions(-) create mode 100644 cadence/contracts/TidalYieldStrategies.cdc create mode 100644 cadence/contracts/mocks/MockStrategy.cdc diff --git a/cadence/contracts/TidalYieldFactory.cdc b/cadence/contracts/TidalYieldFactory.cdc index 0ebc0ecf..c8761aa5 100644 --- a/cadence/contracts/TidalYieldFactory.cdc +++ b/cadence/contracts/TidalYieldFactory.cdc @@ -72,98 +72,6 @@ access(all) contract TidalYieldFactory { } } - access(all) struct DummySink : DFB.Sink { - access(contract) let uniqueID: DFB.UniqueIdentifier? - init(_ id: DFB.UniqueIdentifier?) { - self.uniqueID = id - } - access(all) view fun getSinkType(): Type { - return Type<@FlowToken.Vault>() - } - access(all) fun minimumCapacity(): UFix64 { - return 0.0 - } - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - return - } - } - access(all) struct DummySource : DFB.Source { - access(contract) let uniqueID: DFB.UniqueIdentifier? - init(_ id: DFB.UniqueIdentifier?) { - self.uniqueID = id - } - access(all) view fun getSourceType(): Type { - return Type<@FlowToken.Vault>() - } - access(all) fun minimumAvailable(): UFix64 { - return 0.0 - } - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - return <- DFBUtils.getEmptyVault(self.getSourceType()) - } - } - - access(all) struct DummyStrategy : Strategy { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) let uniqueID: DFB.UniqueIdentifier? - access(self) var sink: {DFB.Sink} - access(self) var source: {DFB.Source} - - init(id: DFB.UniqueIdentifier?, sink: {DFB.Sink}, source: {DFB.Source}) { - self.uniqueID = id - self.sink = sink - self.source = source - } - - access(all) view fun getSupportedCollateralTypes(): [Type] { - return [self.sink.getSinkType()] - } - - access(all) view fun isSupportedCollateralType(_ type: Type): Bool { - return self.sink.getSinkType() == type - } - - /// Returns the amount available for withdrawal via the inner Source - access(all) fun availableBalance(ofToken: Type): UFix64 { - return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 - } - - /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference - access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - self.sink.depositCapacity(from: from) - } - - /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, - /// an empty Vault is returned. - access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { - if ofToken != self.source.getSourceType() { - return <- DFBUtils.getEmptyVault(ofToken) - } - return <- self.source.withdrawAvailable(maxAmount: maxAmount) - } - } - - access(all) struct DummyStrategyBuilder : StrategyBuilder { - access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { - return [] - } - access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { - return [] - } - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { - let id = DFB.UniqueIdentifier() - let strat = DummyStrategy( - id: id, - sink: DummySink(id), - source: DummySource(id) - ) - strat.deposit(from: &withFunds as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - destroy withFunds - return strat - } - } - access(all) resource StrategyFactory { /// The strategies this factory can build access(self) let builders: {Type: {StrategyBuilder}} diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc new file mode 100644 index 00000000..04828f75 --- /dev/null +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -0,0 +1,2 @@ +import "FungibleToken" + diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc new file mode 100644 index 00000000..c2ff1cef --- /dev/null +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -0,0 +1,106 @@ +import "FungibleToken" +import "FlowToken" + +import "DFBUtils" +import "DFB" + +import "TidalYieldFactory" + +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +access(all) contract MockStrategy { + + access(all) struct DummySink : DFB.Sink { + access(contract) let uniqueID: DFB.UniqueIdentifier? + init(_ id: DFB.UniqueIdentifier?) { + self.uniqueID = id + } + access(all) view fun getSinkType(): Type { + return Type<@FlowToken.Vault>() + } + access(all) fun minimumCapacity(): UFix64 { + return 0.0 + } + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + return + } + } + access(all) struct DummySource : DFB.Source { + access(contract) let uniqueID: DFB.UniqueIdentifier? + init(_ id: DFB.UniqueIdentifier?) { + self.uniqueID = id + } + access(all) view fun getSourceType(): Type { + return Type<@FlowToken.Vault>() + } + access(all) fun minimumAvailable(): UFix64 { + return 0.0 + } + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + return <- DFBUtils.getEmptyVault(self.getSourceType()) + } + } + + access(all) struct DummyStrategy : TidalYieldFactory.Strategy { + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(self) var sink: {DFB.Sink} + access(self) var source: {DFB.Source} + + init(id: DFB.UniqueIdentifier?, sink: {DFB.Sink}, source: {DFB.Source}) { + self.uniqueID = id + self.sink = sink + self.source = source + } + + access(all) view fun getSupportedCollateralTypes(): [Type] { + return [self.sink.getSinkType()] + } + + access(all) view fun isSupportedCollateralType(_ type: Type): Bool { + return self.sink.getSinkType() == type + } + + /// Returns the amount available for withdrawal via the inner Source + access(all) fun availableBalance(ofToken: Type): UFix64 { + return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 + } + + /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + self.sink.depositCapacity(from: from) + } + + /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, + /// an empty Vault is returned. + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { + if ofToken != self.source.getSourceType() { + return <- DFBUtils.getEmptyVault(ofToken) + } + return <- self.source.withdrawAvailable(maxAmount: maxAmount) + } + } + + access(all) struct DummyStrategyBuilder : TidalYieldFactory.StrategyBuilder { + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { + return [] + } + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { + return [] + } + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {TidalYieldFactory.Strategy} { + let id = DFB.UniqueIdentifier() + let strat = DummyStrategy( + id: id, + sink: DummySink(id), + source: DummySource(id) + ) + strat.deposit(from: &withFunds as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + destroy withFunds + return strat + } + } +} \ No newline at end of file diff --git a/flow.json b/flow.json index 981b17e3..5e75ec6f 100644 --- a/flow.json +++ b/flow.json @@ -30,6 +30,12 @@ "testing": "0000000000000008" } }, + "MockStrategy": { + "source": "cadence/contracts/mocks/MockStrategy.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "MockSwapper": { "source": "cadence/contracts/mocks/MockSwapper.cdc", "aliases": { From 9057569c517b6830ec62fbf601355b0edd489848 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:51:47 -0600 Subject: [PATCH 29/80] consolidate former Factory contract to TidalYield primary contract --- cadence/contracts/TidalYield.cdc | 153 ++++++++++++++++++++---- cadence/contracts/TidalYieldFactory.cdc | 129 -------------------- flow.json | 6 - 3 files changed, 129 insertions(+), 159 deletions(-) delete mode 100644 cadence/contracts/TidalYieldFactory.cdc diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 6db718bf..2878e199 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -3,7 +3,6 @@ import "Burner" import "ViewResolver" import "DFB" -import "TidalStrategies" /// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION @@ -13,6 +12,8 @@ access(all) contract TidalYield { access(all) let TideManagerStoragePath: StoragePath access(all) let TideManagerPublicPath: PublicPath + access(all) let FactoryStoragePath: StoragePath + access(all) let FactoryPublicPath: PublicPath access(all) event CreatedTide(id: UInt64, idType: String, uuid: UInt64, initialAmount: UFix64, creator: Address?) access(all) event DepositedToTide(id: UInt64, idType: String, amount: UFix64, owner: Address?, fromUUID: UInt64) @@ -20,26 +21,114 @@ access(all) contract TidalYield { access(all) event AddedToManager(id: UInt64, idType: String, owner: Address?, managerUUID: UInt64) access(all) event BurnedTide(id: UInt64, idType: String, remainingBalance: UFix64) + /* --- PUBLIC METHODS --- */ + + access(all) view fun getSupportedStrategies(): [Type] { + return self._borrowFactory().getSupportedStrategies() + } + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + return self._borrowFactory().getSupportedInitializationVaults(forStrategy: forStrategy) + } + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) + } + access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { + return self._borrowFactory().createStrategy(type, withFunds: <-withFunds) + } + access(all) fun createStrategyFactory(): @StrategyFactory { + return <- create StrategyFactory() + } access(all) fun createTideManager(): @TideManager { return <-create TideManager() } /* --- CONSTRUCTS --- */ - access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, ViewResolver.Resolver { - access(contract) let uniqueID: DFB.UniqueIdentifier - access(self) let strategy: {TidalStrategies.Strategy} + /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of composed + /// DeFiBlocks components. These compositions are intended to capitalize on some yield-bearing opportunity so that + /// a Strategy bears yield on that which is deposited into it, albeit not without some risk. A Strategy then can be + /// thought of as the top-level of a nesting of DeFiBlocks connectors & adapters where one can deposit & withdraw + /// funds into the composed DeFi workflows + /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away + /// with the added layer of abstraction introduced by a StrategyBuilder. + access(all) struct interface Strategy : DFB.IdentifiableStruct { + /// Returns the type of Vaults that this Strategy instance can handle + access(all) view fun getSupportedCollateralTypes(): {Type: Bool} + /// Returns whether the provided Vault type is supported by this Strategy instance + access(all) view fun isSupportedCollateralType(_ type: Type): Bool + /// Returns the balance of the given token available for withdrawal. Note that this may be an estimate due to + /// the lack of guarantees inherent to DeFiBlocks Sources + access(all) fun availableBalance(ofToken: Type): UFix64 + /// Deposits up to the balance of the referenced Vault into this Strategy + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + /// Withdraws from this Strategy and returns the resulting Vault of the requested token Type + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { + post { + result.getType() == ofToken: "Invalid Vault returns - requests \(ofToken.identifier) but returned \(result.getType().identifier)" + } + } + } - init(strategyNumber: UInt64, withVault: @{FungibleToken.Vault}) { + /// A StrategyBuilder is responsible for stacking DeFiBlocks connectors in a manner that composes a final Strategy. + /// Since DeFiBlock Sink/Source only support single assets and some Strategies may be multi-asset, we deal with + /// building a Strategy distinctly from encapsulating the top-level DFB connectors acting as entrypoints in to the + /// composed DeFiBlocks infrastructure. + /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away + /// with the added layer of abstraction introduced by a StrategyBuilder. + access(all) struct interface StrategyBuilder { + /// Returns the Vault types which can be used to initialize a given Strategy + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} + /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the + /// provided Vault type + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} + /// Composes a Strategy of the given type with the provided funds + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} + } + + access(all) resource StrategyFactory { + /// The strategies this factory can build + access(self) let builders: {Type: {StrategyBuilder}} + + init() { + self.builders = {} + } + access(all) view fun getSupportedStrategies(): [Type] { + return self.builders.keys + } + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + return self.builders[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? {} + } + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return self.builders[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? {} + } + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { pre { - TidalStrategies.isSupportedCollateralType(withVault.getType(), forStrategy: strategyNumber) == true: - "Provided vault of type \(withVault.getType().identifier) is unsupported collateral Type for strategy \(strategyNumber)" + self.builders[type] != nil: "Strategy \(type.identifier) is unsupported" } + return self.builders[type]!.createStrategy(type, withFunds: <-withFunds) + } + access(Mutate) fun setStrategyBuilder(_ strategy: Type, builder: {StrategyBuilder}) { + self.builders[strategy] = builder + } + access(Mutate) fun removeStrategy(_ strategy: Type): Bool { + return self.builders.remove(key: strategy) != nil + } + } + + access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, ViewResolver.Resolver { + access(contract) let uniqueID: DFB.UniqueIdentifier + access(self) let vaultType: Type + access(self) let strategy: {Strategy} + + init(strategyType: Type, withVault: @{FungibleToken.Vault}) { + // pre { + // TidalStrategies.isSupportedCollateralType(withVault.getType(), forStrategy: strategyNumber) == true: + // "Provided vault of type \(withVault.getType().identifier) is unsupported collateral Type for strategy \(strategyNumber)" + // } self.uniqueID = DFB.UniqueIdentifier() - let vaultType = withVault.getType() - self.strategy = TidalStrategies.createStrategy(number: strategyNumber, vault: <-withVault) - assert(self.strategy.getSinkType() == vaultType, message: "TODO") - assert(self.strategy.getSourceType() == vaultType, message: "TODO") + self.vaultType = withVault.getType() + self.strategy = TidalYield.createStrategy(type: strategyType, withFunds: <-withVault) + assert(self.strategy.isSupportedCollateralType(self.vaultType), message: "TODO") } access(all) view fun id(): UInt64 { @@ -47,7 +136,7 @@ access(all) contract TidalYield { } access(all) fun getTideBalance(): UFix64 { - return self.strategy.minimumAvailable() + return self.strategy.availableBalance(ofToken: self.vaultType) } access(contract) fun burnCallback() { @@ -68,13 +157,13 @@ access(all) contract TidalYield { "Deposited vault of type \(from.getType().identifier) is not supported by this Tide" } emit DepositedToTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: from.balance, owner: self.owner?.address, fromUUID: from.uuid) - self.strategy.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + self.strategy.deposit(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) assert(from.balance == 0.0, message: "TODO") Burner.burn(<-from) } access(all) view fun getSupportedVaultTypes(): {Type: Bool} { - return { self.strategy.getSinkType(): true } + return self.strategy.getSupportedCollateralTypes() } access(all) view fun isSupportedVaultType(type: Type): Bool { @@ -85,12 +174,12 @@ access(all) contract TidalYield { post { result.balance == amount: "TODO" } - let available = self.strategy.minimumAvailable() + let available = self.strategy.availableBalance(ofToken: self.vaultType) assert( amount <= available, message: "Requested amount \(amount) is greater than withdrawable balance of \(available)" ) - let res <- self.strategy.withdrawAvailable(maxAmount: amount) + let res <- self.strategy.withdraw(maxAmount: amount, ofToken: self.vaultType) emit WithdrawnFromTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: amount, owner: self.owner?.address, toUUID: res.uuid) return <- res } @@ -121,22 +210,22 @@ access(all) contract TidalYield { return self.tides.length } - access(all) fun createTide(withVault: @{FungibleToken.Vault}) { + access(all) fun createTide(strategyType: Type, withVault: @{FungibleToken.Vault}) { let balance = withVault.balance - let tide <-create Tide(<-withVault) + let tide <-create Tide(strategyType: strategyType, withVault: <-withVault) // TODO: fix init - emit CreatedTide(id: tide.uniqueID!.id, idType: tide.uniqueID!.getType().identifier, uuid: tide.uuid, initialAmount: balance, creator: self.owner?.address) + emit CreatedTide(id: tide.uniqueID.id, idType: tide.uniqueID.getType().identifier, uuid: tide.uuid, initialAmount: balance, creator: self.owner?.address) self.addTide(<-tide) } access(all) fun addTide(_ tide: @Tide) { pre { - self.tides[tide.uniqueID!.id] == nil: - "Collision with Tide ID \(tide.uniqueID!.id) - a Tide with this ID already exists" + self.tides[tide.uniqueID.id] == nil: + "Collision with Tide ID \(tide.uniqueID.id) - a Tide with this ID already exists" } - emit AddedToManager(id: tide.uniqueID!.id, idType: tide.uniqueID!.getType().identifier, owner: self.owner?.address, managerUUID: self.uuid) - self.tides[tide.uniqueID!.id] <-! tide + emit AddedToManager(id: tide.uniqueID.id, idType: tide.uniqueID.getType().identifier, owner: self.owner?.address, managerUUID: self.uuid) + self.tides[tide.uniqueID.id] <-! tide } access(all) fun depositToTide(_ id: UInt64, from: @{FungibleToken.Vault}) { @@ -177,9 +266,25 @@ access(all) contract TidalYield { } } + /* --- INTERNAL METHODS --- */ + + access(self) view fun _borrowFactory(): &StrategyFactory { + return self.account.storage.borrow<&StrategyFactory>(from: self.FactoryStoragePath) + ?? panic("Could not borrow reference to StrategyFactory from \(self.FactoryStoragePath)") + } + init() { - let pathIdentifier = "TidalYieldTideManager_\(self.account.address)" + var pathIdentifier = "TidalYieldTideManager_\(self.account.address)" self.TideManagerStoragePath = StoragePath(identifier: pathIdentifier)! self.TideManagerPublicPath = PublicPath(identifier: pathIdentifier)! + + pathIdentifier = "TidalYieldStrategyFactory_\(self.account.address)" + self.FactoryStoragePath = StoragePath(identifier: pathIdentifier)! + self.FactoryPublicPath = PublicPath(identifier: pathIdentifier)! + + // configure a StrategyFactory in storage and publish a public Capability + self.account.storage.save(<-create StrategyFactory(), to: self.FactoryStoragePath) + let cap = self.account.capabilities.storage.issue<&StrategyFactory>(self.FactoryStoragePath) + self.account.capabilities.publish(cap, at: self.FactoryPublicPath) } } diff --git a/cadence/contracts/TidalYieldFactory.cdc b/cadence/contracts/TidalYieldFactory.cdc deleted file mode 100644 index c8761aa5..00000000 --- a/cadence/contracts/TidalYieldFactory.cdc +++ /dev/null @@ -1,129 +0,0 @@ -import "FungibleToken" -import "FlowToken" - -import "DFBUtils" -import "DFB" - -/// This contract is used by TidalYield to manage supported Strategies and create Factories for new Tides. -/// -access(all) contract TidalYieldFactory { - - access(all) let FactoryStoragePath: StoragePath - access(all) let FactoryPublicPath: PublicPath - - /* --- PUBLIC METHODS --- */ - - access(all) view fun getSupportedStrategies(): [Type] { - return self._borrowFactory().getSupportedStrategies() - } - access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { - return self._borrowFactory().getSupportedInitializationVaults(forStrategy: forStrategy) - } - access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { - return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) - } - access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { - return self._borrowFactory().createStrategy(type, withFunds: <-withFunds) - } - access(all) fun createStrategyFactory(): @StrategyFactory { - return <- create StrategyFactory() - } - - /* --- CONSTRUCTS --- */ - - /// A StrategyBuilder is responsible for stacking DeFiBlocks connectors in a manner that composes a final Strategy. - /// Since DeFiBlock Sink/Source only support single assets and some Strategies may be multi-asset, we deal with - /// building a Strategy distinctly from encapsulating the top-level DFB connectors acting as entrypoints in to the - /// composed DeFiBlocks infrastructure. - /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away - /// with the added layer of abstraction introduced by a StrategyBuilder. - access(all) struct interface StrategyBuilder { - /// Returns the Vault types which can be used to initialize a given Strategy - access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] - /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the - /// provided Vault type - access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] - /// Composes a Strategy of the given type with the provided funds - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} - } - - /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of composed - /// DeFiBlocks components. These compositions are intended to capitalize on some yield-bearing opportunity so that - /// a Strategy bears yield on that which is deposited into it, albeit not without some risk. A Strategy then can be - /// thought of as the top-level of a nesting of DeFiBlocks connectors & adapters where one can deposit & withdraw - /// funds into the composed DeFi workflows - /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away - /// with the added layer of abstraction introduced by a StrategyBuilder. - access(all) struct interface Strategy : DFB.IdentifiableStruct { - /// Returns the type of Vaults that this Strategy instance can handle - access(all) view fun getSupportedCollateralTypes(): [Type] - /// Returns whether the provided Vault type is supported by this Strategy instance - access(all) view fun isSupportedCollateralType(_ type: Type): Bool - /// Returns the balance of the given token available for withdrawal. Note that this may be an estimate due to - /// the lack of guarantees inherent to DeFiBlocks Sources - access(all) fun availableBalance(ofToken: Type): UFix64 - /// Deposits up to the balance of the referenced Vault into this Strategy - access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - /// Withdraws from this Strategy and returns the resulting Vault of the requested token Type - access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { - post { - result.getType() == ofToken: "Invalid Vault returns - requests \(ofToken.identifier) but returned \(result.getType().identifier)" - } - } - } - - access(all) resource StrategyFactory { - /// The strategies this factory can build - access(self) let builders: {Type: {StrategyBuilder}} - - init() { - self.builders = {} - } - - access(all) view fun getSupportedStrategies(): [Type] { - return self.builders.keys - } - - access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { - return self.builders[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? [] - } - - access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { - return self.builders[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? [] - } - - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { - pre { - self.builders[type] != nil: "Strategy \(type.identifier) is unsupported" - } - return self.builders[type]!.createStrategy(type, withFunds: <-withFunds) - } - - access(Mutate) fun setStrategyBuilder(_ strategy: Type, builder: {StrategyBuilder}) { - self.builders[strategy] = builder - } - - access(Mutate) fun removeStrategy(_ strategy: Type): Bool { - return self.builders.remove(key: strategy) != nil - } - } - - /* --- INTERNAL METHODS --- */ - - access(account) view fun _borrowFactory(): &StrategyFactory { - return self.account.storage.borrow<&StrategyFactory>(from: self.FactoryStoragePath) - ?? panic("Could not borrow reference to StrategyFactory from \(self.FactoryStoragePath)") - } - - - init() { - let pathIdentifier = "TidalYieldStrategyFactory_\(self.account.address)" - self.FactoryStoragePath = StoragePath(identifier: pathIdentifier)! - self.FactoryPublicPath = PublicPath(identifier: pathIdentifier)! - - // configure a StrategyFactory in storage and publish a public Capability - self.account.storage.save(<-create StrategyFactory(), to: self.FactoryStoragePath) - let cap = self.account.capabilities.storage.issue<&StrategyFactory>(self.FactoryStoragePath) - self.account.capabilities.publish(cap, at: self.FactoryPublicPath) - } -} diff --git a/flow.json b/flow.json index 5e75ec6f..390cd9a8 100644 --- a/flow.json +++ b/flow.json @@ -54,12 +54,6 @@ "testing": "0000000000000008" } }, - "TidalYieldFactory": { - "source": "cadence/contracts/TidalYieldFactory.cdc", - "aliases": { - "testing": "0000000000000008" - } - }, "MOET": { "source": "cadence/contracts/internal-dependencies/tokens/USDA.cdc", "aliases": { From 7ef3e8023573b9995ba095d10d5e8f8d7ccb955a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:56:03 -0600 Subject: [PATCH 30/80] fix mock strategies --- cadence/contracts/mocks/MockStrategy.cdc | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index c2ff1cef..ffb60ef6 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -4,7 +4,7 @@ import "FlowToken" import "DFBUtils" import "DFB" -import "TidalYieldFactory" +import "TidalYield" /// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION @@ -43,7 +43,7 @@ access(all) contract MockStrategy { } } - access(all) struct DummyStrategy : TidalYieldFactory.Strategy { + access(all) struct DummyStrategy : TidalYield.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -56,8 +56,8 @@ access(all) contract MockStrategy { self.source = source } - access(all) view fun getSupportedCollateralTypes(): [Type] { - return [self.sink.getSinkType()] + access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { + return {self.sink.getSinkType(): true } } access(all) view fun isSupportedCollateralType(_ type: Type): Bool { @@ -84,14 +84,14 @@ access(all) contract MockStrategy { } } - access(all) struct DummyStrategyBuilder : TidalYieldFactory.StrategyBuilder { - access(all) view fun getSupportedInitializationVaults(forStrategy: Type): [Type] { - return [] + access(all) struct DummyStrategyBuilder : TidalYield.StrategyBuilder { + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + return {} } - access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): [Type] { - return [] + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return {} } - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {TidalYieldFactory.Strategy} { + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {TidalYield.Strategy} { let id = DFB.UniqueIdentifier() let strat = DummyStrategy( id: id, From c9dbc0aa76e7ce90f1f65a9947c72447a6e3dc33 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:43:50 -0600 Subject: [PATCH 31/80] fix flow.json config --- flow.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flow.json b/flow.json index 390cd9a8..2decac32 100644 --- a/flow.json +++ b/flow.json @@ -55,7 +55,7 @@ } }, "MOET": { - "source": "cadence/contracts/internal-dependencies/tokens/USDA.cdc", + "source": "cadence/contracts/internal-dependencies/tokens/MOET.cdc", "aliases": { "testing": "0000000000000007" } @@ -185,7 +185,7 @@ "name": "MockOracle", "args": [ { - "value": "A.f8d6e0586b0a20c7.USDA.Vault", + "value": "A.f8d6e0586b0a20c7.MOET.Vault", "type": "String" } ] From 71ab6f4989744db3a44a2c1304b430db206040e2 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:21:20 -0600 Subject: [PATCH 32/80] add TidalYieldAutoBalancers contract & update flow.json config --- cadence/contracts/TidalYieldAutoBalancers.cdc | 92 +++++++++++++++++++ flow.json | 14 +++ 2 files changed, 106 insertions(+) create mode 100644 cadence/contracts/TidalYieldAutoBalancers.cdc diff --git a/cadence/contracts/TidalYieldAutoBalancers.cdc b/cadence/contracts/TidalYieldAutoBalancers.cdc new file mode 100644 index 00000000..a0e3fa4b --- /dev/null +++ b/cadence/contracts/TidalYieldAutoBalancers.cdc @@ -0,0 +1,92 @@ +import "FungibleToken" + +import "DFB" + +access(all) contract TidalYieldAutoBalancers { + + /// The path prefix used for StoragePath & PublicPath derivations + access(all) let pathPrefix: String + + /* --- PUBLIC METHODS --- */ + + /// Returns the path (StoragePath or PublicPath) at which an AutoBalancer is stored with the associated + /// UniqueIdentifier.id. + access(all) view fun deriveAutoBalancerPath(id: UInt64, storage: Bool): Path { + return storage ? StoragePath(identifier: "\(self.pathPrefix)\(id)")! : PublicPath(identifier: "\(self.pathPrefix)\(id)")! + } + + /// Returns an unauthorized reference to an AutoBalancer with the given UniqueIdentifier.id value. If none is + /// configured, `nil` will be returned. + access(all) fun borrowAutoBalancer(id: UInt64): &DFB.AutoBalancer? { + let publicPath = self.deriveAutoBalancerPath(id: id, storage: false) as! PublicPath + return self.account.capabilities.borrow<&DFB.AutoBalancer>(publicPath) + } + + /* --- INTERNAL METHODS --- */ + + /// Configures a new AutoBalancer in storage, configures its public Capability, and sets its inner authorized + /// Capability. If an AutoBalancer is stored with an associated UniqueID value, the operation reverts. + access(account) fun _initNewAutoBalancer( + oracle: {DFB.PriceOracle}, + vaultType: Type, + lowerThreshold: UFix64, + upperThreshold: UFix64, + rebalanceSink: {DFB.Sink}?, + rebalanceSource: {DFB.Source}?, + uniqueID: DFB.UniqueIdentifier + ) { + + // derive paths & prevent collision + let storagePath = self.deriveAutoBalancerPath(id: uniqueID.id, storage: true) as! StoragePath + let publicPath = self.deriveAutoBalancerPath(id: uniqueID.id, storage: false) as! PublicPath + var storedType = self.account.storage.type(at: storagePath) + var publishedCap = self.account.capabilities.exists(publicPath) + assert(storedType == nil, + message: "Storage collision when creating AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(storagePath)") + assert(!publishedCap, + message: "Published Capability collision found when publishing AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(publicPath)") + + // create & save AutoBalancer + let autoBalancer <- DFB.createAutoBalancer( + oracle: oracle, + vaultType: vaultType, + lowerThreshold: lowerThreshold, + upperThreshold: upperThreshold, + rebalanceSink: rebalanceSink, + rebalanceSource: rebalanceSource, + uniqueID: uniqueID + ) + self.account.storage.save(<-autoBalancer, to: storagePath) + let autoBalancerRef = self._borrowAutoBalancer(uniqueID.id) + + // issue & publish public capability + let publicCap = self.account.capabilities.storage.issue<&DFB.AutoBalancer>(storagePath) + self.account.capabilities.publish(publicCap, at: publicPath) + + // issue private capability & set within AutoBalancer + let authorizedCap = self.account.capabilities.storage.issue(storagePath) + autoBalancerRef.setSelfCapability(authorizedCap) + + // ensure proper configuration before closing + storedType = self.account.storage.type(at: storagePath) + publishedCap = self.account.capabilities.exists(publicPath) + assert(storedType == Type<@DFB.AutoBalancer>(), + message: "Error when configuring AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(storagePath)") + assert(!publishedCap, + message: "Error when publishing AutoBalancer Capability for UniqueIdentifier.id \(uniqueID.id) at path \(publicPath)") + } + + /// Returns an authorized reference on the AutoBalancer with the associated UniqueIdentifier.id. If none is found, + /// the operation reverts. + access(account) + fun _borrowAutoBalancer(_ id: UInt64): auth(DFB.Auto, DFB.Set, DFB.Get, FungibleToken.Withdraw) &DFB.AutoBalancer { + let storagePath = self.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath + return self.account.storage.borrow( + from: storagePath + ) ?? panic("Could not borrow reference to AutoBalancer with UniqueIdentifier.id \(id) from StoragePath \(storagePath)") + } + + init() { + self.pathPrefix = "TidalYieldAutoBalancer_" + } +} diff --git a/flow.json b/flow.json index 2decac32..5e1e8c1f 100644 --- a/flow.json +++ b/flow.json @@ -54,6 +54,18 @@ "testing": "0000000000000008" } }, + "TidalYieldAutoBalancers": { + "source": "cadence/contracts/TidalYieldAutoBalancers.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, + "TidalYieldStrategies": { + "source": "cadence/contracts/TidalYieldStrategies.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "MOET": { "source": "cadence/contracts/internal-dependencies/tokens/MOET.cdc", "aliases": { @@ -191,6 +203,8 @@ ] }, "MockSwapper", + "TidalYieldAutoBalancers", + "TidalYieldStrategies", "TidalYield" ] } From 657e9b1a8f2356879a8a36fcfccf7d010ee77770 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:07:19 -0600 Subject: [PATCH 33/80] rename StrategyBuilder to StrategyComposer --- cadence/contracts/TidalYield.cdc | 43 +++++++++++++++--------- cadence/contracts/mocks/MockStrategy.cdc | 7 ++-- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 2878e199..68a498e9 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -10,9 +10,13 @@ import "DFB" /// access(all) contract TidalYield { + /// Canonical StoragePath for where TideManager should be stored access(all) let TideManagerStoragePath: StoragePath + /// Canonical PublicPath for where TideManager Capability should be published access(all) let TideManagerPublicPath: PublicPath + /// Canonical StoragePath for where StrategyFactory should be stored access(all) let FactoryStoragePath: StoragePath + /// Canonical PublicPath for where StrategyFactory Capability should be published access(all) let FactoryPublicPath: PublicPath access(all) event CreatedTide(id: UInt64, idType: String, uuid: UInt64, initialAmount: UFix64, creator: Address?) @@ -50,7 +54,7 @@ access(all) contract TidalYield { /// thought of as the top-level of a nesting of DeFiBlocks connectors & adapters where one can deposit & withdraw /// funds into the composed DeFi workflows /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away - /// with the added layer of abstraction introduced by a StrategyBuilder. + /// with the added layer of abstraction introduced by a StrategyComposer. access(all) struct interface Strategy : DFB.IdentifiableStruct { /// Returns the type of Vaults that this Strategy instance can handle access(all) view fun getSupportedCollateralTypes(): {Type: Bool} @@ -69,49 +73,56 @@ access(all) contract TidalYield { } } - /// A StrategyBuilder is responsible for stacking DeFiBlocks connectors in a manner that composes a final Strategy. + /// A StrategyComposer is responsible for stacking DeFiBlocks connectors in a manner that composes a final Strategy. /// Since DeFiBlock Sink/Source only support single assets and some Strategies may be multi-asset, we deal with /// building a Strategy distinctly from encapsulating the top-level DFB connectors acting as entrypoints in to the /// composed DeFiBlocks infrastructure. /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away - /// with the added layer of abstraction introduced by a StrategyBuilder. - access(all) struct interface StrategyBuilder { + /// with the added layer of abstraction introduced by a StrategyComposer. + access(all) struct interface StrategyComposer { + /// Returns the Types of Strategies composed by this StrategyComposer + access(all) view fun getComposedStrategyTypes(): {Type: Bool} /// Returns the Vault types which can be used to initialize a given Strategy access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the /// provided Vault type access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} /// Composes a Strategy of the given type with the provided funds - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): {Strategy} { + pre { + self.getComposedStrategyTypes()[type] == true: + "Strategy \(type.identifier) is unsupported by StrategyComposer \(self.getType().identifier)" + } + } } access(all) resource StrategyFactory { /// The strategies this factory can build - access(self) let builders: {Type: {StrategyBuilder}} + access(self) let composers: {Type: {StrategyComposer}} init() { - self.builders = {} + self.composers = {} } access(all) view fun getSupportedStrategies(): [Type] { - return self.builders.keys + return self.composers.keys } access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { - return self.builders[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? {} + return self.composers[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? {} } access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { - return self.builders[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? {} + return self.composers[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? {} } access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { pre { - self.builders[type] != nil: "Strategy \(type.identifier) is unsupported" + self.composers[type] != nil: "Strategy \(type.identifier) is unsupported" } - return self.builders[type]!.createStrategy(type, withFunds: <-withFunds) + return self.composers[type]!.createStrategy(type, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not } - access(Mutate) fun setStrategyBuilder(_ strategy: Type, builder: {StrategyBuilder}) { - self.builders[strategy] = builder + access(Mutate) fun setStrategyComposer(_ strategy: Type, builder: {StrategyComposer}) { + self.composers[strategy] = builder } access(Mutate) fun removeStrategy(_ strategy: Type): Bool { - return self.builders.remove(key: strategy) != nil + return self.composers.remove(key: strategy) != nil } } @@ -132,7 +143,7 @@ access(all) contract TidalYield { } access(all) view fun id(): UInt64 { - return self.uniqueID!.id + return self.uniqueID.id } access(all) fun getTideBalance(): UFix64 { diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index ffb60ef6..35feecb6 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -84,14 +84,17 @@ access(all) contract MockStrategy { } } - access(all) struct DummyStrategyBuilder : TidalYield.StrategyBuilder { + access(all) struct DummyStrategyComposer : TidalYield.StrategyComposer { + access(all) view fun getComposedStrategyTypes(): {Type: Bool} { + return { Type(): true } + } access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { return {} } access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { return {} } - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {TidalYield.Strategy} { + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): {TidalYield.Strategy} { let id = DFB.UniqueIdentifier() let strat = DummyStrategy( id: id, From 7f6cac0558c595583691ca4e31a6324fb1728ac3 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:19:42 -0600 Subject: [PATCH 34/80] update initial TraceStrategy and TracerStrategyComposer --- cadence/contracts/TidalYieldAutoBalancers.cdc | 3 +- cadence/contracts/TidalYieldStrategies.cdc | 176 ++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/TidalYieldAutoBalancers.cdc b/cadence/contracts/TidalYieldAutoBalancers.cdc index a0e3fa4b..c1e99b34 100644 --- a/cadence/contracts/TidalYieldAutoBalancers.cdc +++ b/cadence/contracts/TidalYieldAutoBalancers.cdc @@ -34,7 +34,7 @@ access(all) contract TidalYieldAutoBalancers { rebalanceSink: {DFB.Sink}?, rebalanceSource: {DFB.Source}?, uniqueID: DFB.UniqueIdentifier - ) { + ): auth(DFB.Auto, DFB.Set, DFB.Get, FungibleToken.Withdraw) &DFB.AutoBalancer { // derive paths & prevent collision let storagePath = self.deriveAutoBalancerPath(id: uniqueID.id, storage: true) as! StoragePath @@ -74,6 +74,7 @@ access(all) contract TidalYieldAutoBalancers { message: "Error when configuring AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(storagePath)") assert(!publishedCap, message: "Error when publishing AutoBalancer Capability for UniqueIdentifier.id \(uniqueID.id) at path \(publicPath)") + return autoBalancerRef } /// Returns an authorized reference on the AutoBalancer with the associated UniqueIdentifier.id. If none is found, diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 04828f75..11d198c6 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -1,2 +1,178 @@ +// standards import "FungibleToken" +import "FlowToken" +// DeFiBlocks +import "DFBUtils" +import "DFB" +import "SwapStack" +// Lending protocol +import "TidalProtocol" +// TidalYield platform +import "TidalYield" +import "TidalYieldAutoBalancers" +// tokens +import "YieldToken" +import "MOET" +// mocks +import "MockOracle" +import "MockSwapper" +import "MockStrategy" +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +/// TidalYieldStrategies +/// +/// This contract defines Strategies used in the TidalYield platform. +/// +/// A Strategy instance can be thought of as an an objects wrapping a stack of DeFiBlocks connectors wired together to +/// (optimally) generate some yield on initial deposits. Strategies can be simple such as swapping into a yield-bearing +/// asset (such as stFLOW) or more complex compositions of DeFiBlocks connectors. +/// +/// A StrategyComposer is tasked with the creation of a supported Strategy. It's within the composition of DeFiBlocks +/// connectors that the true power of the components lies. +/// +access(all) contract TidalYieldStrategies { + + /// This is the first Strategy implementation, wrapping a TidalProtocol Position along with its related Sink & + /// Source. While this object is a simple wrapper for the top-level collateralized position, the true magic of the + /// DeFiBlocks is in the stacking of the related connectors. This composition logic can be found in the + /// TracerStrategyComposer construct. + access(all) struct TracerStrategy : TidalYield.Strategy { + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(self) let position: TidalProtocol.Position + access(self) var sink: {DFB.Sink} + access(self) var source: {DFB.Source} + + init(id: DFB.UniqueIdentifier?, collateralType: Type, position: TidalProtocol.Position) { + self.uniqueID = id + self.position = position + self.sink = position.createSink(type: collateralType) + self.source = position.createSource(type: collateralType) + } + access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { + return {self.sink.getSinkType(): true } + } + access(all) view fun isSupportedCollateralType(_ type: Type): Bool { + return self.sink.getSinkType() == type + } + /// Returns the amount available for withdrawal via the inner Source + access(all) fun availableBalance(ofToken: Type): UFix64 { + return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 + } + /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + self.sink.depositCapacity(from: from) + } + /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, + /// an empty Vault is returned. + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { + if ofToken != self.source.getSourceType() { + return <- DFBUtils.getEmptyVault(ofToken) + } + return <- self.source.withdrawAvailable(maxAmount: maxAmount) + } + } + + /// This StrategyComposer builds a TracerStrategy + access(all) struct TracerStrategyComposer : TidalYield.StrategyComposer { + /// Returns the Types of Strategies composed by this StrategyComposer + access(all) view fun getComposedStrategyTypes(): {Type: Bool} { + return { Type(): true } + } + + /// Returns the Vault types which can be used to initialize a given Strategy + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + return { Type<@FlowToken.Vault>(): true } + } + + /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the + /// provided Vault type + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return { Type<@FlowToken.Vault>(): true } + } + + /// Composes a Strategy of the given type with the provided funds + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): {TidalYield.Strategy} { + // init the UniqueIdentifier that identifies all associated DFB components + let id = DFB.UniqueIdentifier() + // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy + let oracle = MockOracle.PriceOracle() + + // token types + let collateralType = withFunds.getType() + let yieldTokenType = Type<@YieldToken.Vault>() + let moetTokenType = Type<@MOET.Vault>() + let flowTokenType = Type<@FlowToken.Vault>() + + // configure and AutoBalancer for this stack + let autoBalancer = TidalYieldAutoBalancers._initNewAutoBalancer( + oracle: oracle, + vaultType: yieldTokenType, + lowerThreshold: params["lowerThreshold"] as! UFix64? ?? panic("Malformed params missing \"lowerThreshold\""), + upperThreshold: params["upperThreshold"] as! UFix64? ?? panic("Malformed params missing \"upperThreshold\""), + rebalanceSink: nil, + rebalanceSource: nil, + uniqueID: id + ) + // enables deposits of YieldToken to the AutoBalancer + let abaSink = autoBalancer.createBalancerSink() ?? panic("Could not retrieve Sink from AutoBalancer with id \(id.id)") + // enables withdrawals of YieldToken from the AutoBalancer + let abaSource = autoBalancer.createBalancerSource() ?? panic("Could not retrieve Sink from AutoBalancer with id \(id.id)") + + // init MOET <> YIELD swappers + // + // MOET -> YieldToken + let moetToYieldSwapper = MockSwapper.Swapper( + inVault: moetTokenType, + outVault: yieldTokenType, + uniqueID: id + ) + // YieldToken -> MOET + let yieldToMoetSwapper = MockSwapper.Swapper( + inVault: yieldTokenType, + outVault: moetTokenType, + uniqueID: id + ) + + // init SwapSink directing swapped funds to AutoBalancer + // + // Swaps provided MOET to YieldToken & deposits to the AutoBalancer + let abaSwapSink = SwapStack.SwapSink(swapper: moetToYieldSwapper, sink: abaSink, uniqueID: id) + // Swaps YieldToken & provides swapped MOET, sourcing YieldToken from the AutoBalancer + let abaSwapSource = SwapStack.SwapSource(swapper: moetToYieldSwapper, source: abaSource, uniqueID: id) + + // open a TidalProtocol position + let position = TidalProtocol.openPosition( + collateral: <-withFunds, + issuanceSink: abaSwapSink, + repaymentSource: abaSwapSource, + pushToDrawDownSink: true + ) + // get Sink & Source connectors relating to the new Position + let positionSink = position.createSinkWithOptions(type: collateralType, pushToDrawDownSink: true) + let positionSource = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) // TODO: may need to be false + + // init YieldToken -> FLOW Swapper + let yieldToFlowSwapper = MockSwapper.Swapper( + inVault: yieldTokenType, + outVault: flowTokenType, + uniqueID: id + ) + // allows for YieldToken to be deposited to the Position + let positionSwapSink = SwapStack.SwapSink(swapper: yieldToFlowSwapper, sink: positionSink, uniqueID: id) + + // set the AutoBalancer's rebalance Sink which it will use to deposit overflown value, + // recollateralizing the position + autoBalancer.setSink(positionSwapSink) + + return TracerStrategy( + id: DFB.UniqueIdentifier(), + collateralType: collateralType, + position: position + ) + } + } +} From c03b0c306d833a1d620251f56a6a6d32d09358cc Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:42:01 -0600 Subject: [PATCH 35/80] refactor Strategy from struct to resource --- cadence/contracts/TidalYield.cdc | 24 ++++++++++++++-------- cadence/contracts/TidalYieldStrategies.cdc | 9 ++++---- cadence/contracts/mocks/MockStrategy.cdc | 10 ++++----- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 68a498e9..07ff8dd2 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -36,8 +36,8 @@ access(all) contract TidalYield { access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) } - access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { - return self._borrowFactory().createStrategy(type, withFunds: <-withFunds) + access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): @{Strategy} { + return <- self._borrowFactory().createStrategy(type, withFunds: <-withFunds) } access(all) fun createStrategyFactory(): @StrategyFactory { return <- create StrategyFactory() @@ -52,10 +52,16 @@ access(all) contract TidalYield { /// DeFiBlocks components. These compositions are intended to capitalize on some yield-bearing opportunity so that /// a Strategy bears yield on that which is deposited into it, albeit not without some risk. A Strategy then can be /// thought of as the top-level of a nesting of DeFiBlocks connectors & adapters where one can deposit & withdraw - /// funds into the composed DeFi workflows + /// funds into the composed DeFi workflows. + /// + /// While two types of strategies may not highly differ with respect to their fields, the compositions of DeFiBlocks + /// components & connections they provide access to likely do. This difference in wiring is why the Strategy is a + /// resource - because the Type and uniqueness of composition of a given Strategy must be preserved as that is its + /// distinguishing factor. These qualities are preserved by restricting the party who can construct it, which for + /// resources is within the contract that defines it. /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyComposer. - access(all) struct interface Strategy : DFB.IdentifiableStruct { + access(all) resource interface Strategy : DFB.IdentifiableResource { /// Returns the type of Vaults that this Strategy instance can handle access(all) view fun getSupportedCollateralTypes(): {Type: Bool} /// Returns whether the provided Vault type is supported by this Strategy instance @@ -88,7 +94,7 @@ access(all) contract TidalYield { /// provided Vault type access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} /// Composes a Strategy of the given type with the provided funds - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): {Strategy} { + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): @{Strategy} { pre { self.getComposedStrategyTypes()[type] == true: "Strategy \(type.identifier) is unsupported by StrategyComposer \(self.getType().identifier)" @@ -112,11 +118,11 @@ access(all) contract TidalYield { access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { return self.composers[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? {} } - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): {Strategy} { + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): @{Strategy} { pre { self.composers[type] != nil: "Strategy \(type.identifier) is unsupported" } - return self.composers[type]!.createStrategy(type, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not + return <- self.composers[type]!.createStrategy(type, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not } access(Mutate) fun setStrategyComposer(_ strategy: Type, builder: {StrategyComposer}) { self.composers[strategy] = builder @@ -129,7 +135,7 @@ access(all) contract TidalYield { access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, ViewResolver.Resolver { access(contract) let uniqueID: DFB.UniqueIdentifier access(self) let vaultType: Type - access(self) let strategy: {Strategy} + access(self) let strategy: @{Strategy} init(strategyType: Type, withVault: @{FungibleToken.Vault}) { // pre { @@ -138,7 +144,7 @@ access(all) contract TidalYield { // } self.uniqueID = DFB.UniqueIdentifier() self.vaultType = withVault.getType() - self.strategy = TidalYield.createStrategy(type: strategyType, withFunds: <-withVault) + self.strategy <- TidalYield.createStrategy(type: strategyType, withFunds: <-withVault) assert(self.strategy.isSupportedCollateralType(self.vaultType), message: "TODO") } diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 11d198c6..0c1a8c8f 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -16,7 +16,6 @@ import "MOET" // mocks import "MockOracle" import "MockSwapper" -import "MockStrategy" /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -38,7 +37,7 @@ access(all) contract TidalYieldStrategies { /// Source. While this object is a simple wrapper for the top-level collateralized position, the true magic of the /// DeFiBlocks is in the stacking of the related connectors. This composition logic can be found in the /// TracerStrategyComposer construct. - access(all) struct TracerStrategy : TidalYield.Strategy { + access(all) resource TracerStrategy : TidalYield.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -80,7 +79,7 @@ access(all) contract TidalYieldStrategies { access(all) struct TracerStrategyComposer : TidalYield.StrategyComposer { /// Returns the Types of Strategies composed by this StrategyComposer access(all) view fun getComposedStrategyTypes(): {Type: Bool} { - return { Type(): true } + return { Type<@TracerStrategy>(): true } } /// Returns the Vault types which can be used to initialize a given Strategy @@ -95,7 +94,7 @@ access(all) contract TidalYieldStrategies { } /// Composes a Strategy of the given type with the provided funds - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): {TidalYield.Strategy} { + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): @{TidalYield.Strategy} { // init the UniqueIdentifier that identifies all associated DFB components let id = DFB.UniqueIdentifier() // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy @@ -168,7 +167,7 @@ access(all) contract TidalYieldStrategies { // recollateralizing the position autoBalancer.setSink(positionSwapSink) - return TracerStrategy( + return <-create TracerStrategy( id: DFB.UniqueIdentifier(), collateralType: collateralType, position: position diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 35feecb6..0aef5566 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -43,7 +43,7 @@ access(all) contract MockStrategy { } } - access(all) struct DummyStrategy : TidalYield.Strategy { + access(all) resource DummyStrategy : TidalYield.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -86,7 +86,7 @@ access(all) contract MockStrategy { access(all) struct DummyStrategyComposer : TidalYield.StrategyComposer { access(all) view fun getComposedStrategyTypes(): {Type: Bool} { - return { Type(): true } + return { Type<@DummyStrategy>(): true } } access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { return {} @@ -94,16 +94,16 @@ access(all) contract MockStrategy { access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { return {} } - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): {TidalYield.Strategy} { + access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): @{TidalYield.Strategy} { let id = DFB.UniqueIdentifier() - let strat = DummyStrategy( + let strat <- create DummyStrategy( id: id, sink: DummySink(id), source: DummySource(id) ) strat.deposit(from: &withFunds as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) destroy withFunds - return strat + return <- strat } } } \ No newline at end of file From 63733a49dfc3ff491fa10be4d3acbf0aa019ff42 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:45:50 -0600 Subject: [PATCH 36/80] update contract comments --- cadence/contracts/TidalYield.cdc | 55 +++++++++++++++++++------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 07ff8dd2..7785e929 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -10,6 +10,8 @@ import "DFB" /// access(all) contract TidalYield { + /* --- FIELDS --- */ + /// Canonical StoragePath for where TideManager should be stored access(all) let TideManagerStoragePath: StoragePath /// Canonical PublicPath for where TideManager Capability should be published @@ -19,33 +21,14 @@ access(all) contract TidalYield { /// Canonical PublicPath for where StrategyFactory Capability should be published access(all) let FactoryPublicPath: PublicPath + /* --- EVENTS --- */ + access(all) event CreatedTide(id: UInt64, idType: String, uuid: UInt64, initialAmount: UFix64, creator: Address?) access(all) event DepositedToTide(id: UInt64, idType: String, amount: UFix64, owner: Address?, fromUUID: UInt64) access(all) event WithdrawnFromTide(id: UInt64, idType: String, amount: UFix64, owner: Address?, toUUID: UInt64) access(all) event AddedToManager(id: UInt64, idType: String, owner: Address?, managerUUID: UInt64) access(all) event BurnedTide(id: UInt64, idType: String, remainingBalance: UFix64) - /* --- PUBLIC METHODS --- */ - - access(all) view fun getSupportedStrategies(): [Type] { - return self._borrowFactory().getSupportedStrategies() - } - access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { - return self._borrowFactory().getSupportedInitializationVaults(forStrategy: forStrategy) - } - access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { - return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) - } - access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): @{Strategy} { - return <- self._borrowFactory().createStrategy(type, withFunds: <-withFunds) - } - access(all) fun createStrategyFactory(): @StrategyFactory { - return <- create StrategyFactory() - } - access(all) fun createTideManager(): @TideManager { - return <-create TideManager() - } - /* --- CONSTRUCTS --- */ /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of composed @@ -90,7 +73,7 @@ access(all) contract TidalYield { access(all) view fun getComposedStrategyTypes(): {Type: Bool} /// Returns the Vault types which can be used to initialize a given Strategy access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} - /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the + /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the /// provided Vault type access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} /// Composes a Strategy of the given type with the provided funds @@ -283,6 +266,34 @@ access(all) contract TidalYield { } } + /* --- PUBLIC METHODS --- */ + + /// Returns the Types of Strategies that can be used in Tides + access(all) view fun getSupportedStrategies(): [Type] { + return self._borrowFactory().getSupportedStrategies() + } + /// Returns the Vault types which can be used to initialize a given Strategy + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + return self._borrowFactory().getSupportedInitializationVaults(forStrategy: forStrategy) + } + /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the + /// provided Vault type + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) + } + /// Creates a Strategy of the requested Type using the provided Vault as an initial deposit + access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): @{Strategy} { + return <- self._borrowFactory().createStrategy(type, withFunds: <-withFunds) + } + /// Creates a TideManager used to create and manage Tides + access(all) fun createTideManager(): @TideManager { + return <-create TideManager() + } + /// Creates a StrategyFactory resource + access(all) fun createStrategyFactory(): @StrategyFactory { + return <- create StrategyFactory() + } + /* --- INTERNAL METHODS --- */ access(self) view fun _borrowFactory(): &StrategyFactory { From 5774e7e6af25d7866e091b30f4d1b029f0c40ccf Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:55:39 -0600 Subject: [PATCH 37/80] update TidalYield interface conditions & defaults, update contract formatting --- cadence/contracts/TidalYield.cdc | 44 +++++++++++++++------- cadence/contracts/TidalYieldStrategies.cdc | 36 ++++++++++-------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 7785e929..08a7c78f 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -48,16 +48,24 @@ access(all) contract TidalYield { /// Returns the type of Vaults that this Strategy instance can handle access(all) view fun getSupportedCollateralTypes(): {Type: Bool} /// Returns whether the provided Vault type is supported by this Strategy instance - access(all) view fun isSupportedCollateralType(_ type: Type): Bool + access(all) view fun isSupportedCollateralType(_ type: Type): Bool { + return self.getSupportedCollateralTypes()[type] ?? false + } /// Returns the balance of the given token available for withdrawal. Note that this may be an estimate due to /// the lack of guarantees inherent to DeFiBlocks Sources access(all) fun availableBalance(ofToken: Type): UFix64 /// Deposits up to the balance of the referenced Vault into this Strategy - access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + pre { + self.isSupportedCollateralType(from.getType()): + "Cannot deposit Vault \(from.getType().identifier) to Strategy \(self.getType().identifier) - unsupported deposit type" + } + } /// Withdraws from this Strategy and returns the resulting Vault of the requested token Type access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { post { - result.getType() == ofToken: "Invalid Vault returns - requests \(ofToken.identifier) but returned \(result.getType().identifier)" + result.getType() == ofToken: + "Invalid Vault returns - requests \(ofToken.identifier) but returned \(result.getType().identifier)" } } } @@ -77,8 +85,15 @@ access(all) contract TidalYield { /// provided Vault type access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} /// Composes a Strategy of the given type with the provided funds - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): @{Strategy} { + access(all) fun createStrategy( + _ type: Type, + uniqueID: DFB.UniqueIdentifier, + withFunds: @{FungibleToken.Vault}, + params: {String: AnyStruct} + ): @{Strategy} { pre { + self.getSupportedInitializationVaults(forStrategy: type)[withFunds.getType()] == true: + "Cannot initialize Strategy \(type.identifier) with Vault \(withFunds.getType().identifier) - unsupported initialization Vault" self.getComposedStrategyTypes()[type] == true: "Strategy \(type.identifier) is unsupported by StrategyComposer \(self.getType().identifier)" } @@ -101,11 +116,11 @@ access(all) contract TidalYield { access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { return self.composers[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? {} } - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}): @{Strategy} { + access(all) fun createStrategy(_ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault}): @{Strategy} { pre { self.composers[type] != nil: "Strategy \(type.identifier) is unsupported" } - return <- self.composers[type]!.createStrategy(type, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not + return <- self.composers[type]!.createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not } access(Mutate) fun setStrategyComposer(_ strategy: Type, builder: {StrategyComposer}) { self.composers[strategy] = builder @@ -121,14 +136,15 @@ access(all) contract TidalYield { access(self) let strategy: @{Strategy} init(strategyType: Type, withVault: @{FungibleToken.Vault}) { - // pre { - // TidalStrategies.isSupportedCollateralType(withVault.getType(), forStrategy: strategyNumber) == true: - // "Provided vault of type \(withVault.getType().identifier) is unsupported collateral Type for strategy \(strategyNumber)" - // } self.uniqueID = DFB.UniqueIdentifier() self.vaultType = withVault.getType() - self.strategy <- TidalYield.createStrategy(type: strategyType, withFunds: <-withVault) - assert(self.strategy.isSupportedCollateralType(self.vaultType), message: "TODO") + self.strategy <- TidalYield.createStrategy( + type: strategyType, + uniqueID: self.uniqueID, + withFunds: <-withVault + ) + assert(self.strategy.isSupportedCollateralType(self.vaultType), + message: "TODO") } access(all) view fun id(): UInt64 { @@ -282,8 +298,8 @@ access(all) contract TidalYield { return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) } /// Creates a Strategy of the requested Type using the provided Vault as an initial deposit - access(all) fun createStrategy(type: Type, withFunds: @{FungibleToken.Vault}): @{Strategy} { - return <- self._borrowFactory().createStrategy(type, withFunds: <-withFunds) + access(all) fun createStrategy(type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault}): @{Strategy} { + return <- self._borrowFactory().createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds) } /// Creates a TideManager used to create and manage Tides access(all) fun createTideManager(): @TideManager { diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 0c1a8c8f..72793779 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -51,11 +51,12 @@ access(all) contract TidalYieldStrategies { self.sink = position.createSink(type: collateralType) self.source = position.createSource(type: collateralType) } + + // Inherited from TidalYield.Strategy default implementation + // access(all) view fun isSupportedCollateralType(_ type: Type): Bool + access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { - return {self.sink.getSinkType(): true } - } - access(all) view fun isSupportedCollateralType(_ type: Type): Bool { - return self.sink.getSinkType() == type + return { self.sink.getSinkType(): true } } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { @@ -94,9 +95,12 @@ access(all) contract TidalYieldStrategies { } /// Composes a Strategy of the given type with the provided funds - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): @{TidalYield.Strategy} { - // init the UniqueIdentifier that identifies all associated DFB components - let id = DFB.UniqueIdentifier() + access(all) fun createStrategy( + _ type: Type, + uniqueID: DFB.UniqueIdentifier, + withFunds: @{FungibleToken.Vault}, + params: {String: AnyStruct} + ): @{TidalYield.Strategy} { // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy let oracle = MockOracle.PriceOracle() @@ -114,12 +118,12 @@ access(all) contract TidalYieldStrategies { upperThreshold: params["upperThreshold"] as! UFix64? ?? panic("Malformed params missing \"upperThreshold\""), rebalanceSink: nil, rebalanceSource: nil, - uniqueID: id + uniqueID: uniqueID ) // enables deposits of YieldToken to the AutoBalancer - let abaSink = autoBalancer.createBalancerSink() ?? panic("Could not retrieve Sink from AutoBalancer with id \(id.id)") + let abaSink = autoBalancer.createBalancerSink() ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") // enables withdrawals of YieldToken from the AutoBalancer - let abaSource = autoBalancer.createBalancerSource() ?? panic("Could not retrieve Sink from AutoBalancer with id \(id.id)") + let abaSource = autoBalancer.createBalancerSource() ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") // init MOET <> YIELD swappers // @@ -127,21 +131,21 @@ access(all) contract TidalYieldStrategies { let moetToYieldSwapper = MockSwapper.Swapper( inVault: moetTokenType, outVault: yieldTokenType, - uniqueID: id + uniqueID: uniqueID ) // YieldToken -> MOET let yieldToMoetSwapper = MockSwapper.Swapper( inVault: yieldTokenType, outVault: moetTokenType, - uniqueID: id + uniqueID: uniqueID ) // init SwapSink directing swapped funds to AutoBalancer // // Swaps provided MOET to YieldToken & deposits to the AutoBalancer - let abaSwapSink = SwapStack.SwapSink(swapper: moetToYieldSwapper, sink: abaSink, uniqueID: id) + let abaSwapSink = SwapStack.SwapSink(swapper: moetToYieldSwapper, sink: abaSink, uniqueID: uniqueID) // Swaps YieldToken & provides swapped MOET, sourcing YieldToken from the AutoBalancer - let abaSwapSource = SwapStack.SwapSource(swapper: moetToYieldSwapper, source: abaSource, uniqueID: id) + let abaSwapSource = SwapStack.SwapSource(swapper: moetToYieldSwapper, source: abaSource, uniqueID: uniqueID) // open a TidalProtocol position let position = TidalProtocol.openPosition( @@ -158,10 +162,10 @@ access(all) contract TidalYieldStrategies { let yieldToFlowSwapper = MockSwapper.Swapper( inVault: yieldTokenType, outVault: flowTokenType, - uniqueID: id + uniqueID: uniqueID ) // allows for YieldToken to be deposited to the Position - let positionSwapSink = SwapStack.SwapSink(swapper: yieldToFlowSwapper, sink: positionSink, uniqueID: id) + let positionSwapSink = SwapStack.SwapSink(swapper: yieldToFlowSwapper, sink: positionSink, uniqueID: uniqueID) // set the AutoBalancer's rebalance Sink which it will use to deposit overflown value, // recollateralizing the position From 015f2eb78cc3117494fa06cd8be3ba76883108ae Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:03:36 -0600 Subject: [PATCH 38/80] update contract comments --- cadence/contracts/TidalYield.cdc | 23 +++++++++++++++++++++- cadence/contracts/TidalYieldStrategies.cdc | 6 +++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 08a7c78f..d2ab1ce8 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -31,6 +31,8 @@ access(all) contract TidalYield { /* --- CONSTRUCTS --- */ + /// Strategy + /// /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of composed /// DeFiBlocks components. These compositions are intended to capitalize on some yield-bearing opportunity so that /// a Strategy bears yield on that which is deposited into it, albeit not without some risk. A Strategy then can be @@ -42,6 +44,7 @@ access(all) contract TidalYield { /// resource - because the Type and uniqueness of composition of a given Strategy must be preserved as that is its /// distinguishing factor. These qualities are preserved by restricting the party who can construct it, which for /// resources is within the contract that defines it. + /// /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyComposer. access(all) resource interface Strategy : DFB.IdentifiableResource { @@ -70,10 +73,13 @@ access(all) contract TidalYield { } } + /// StrategyComposer + /// /// A StrategyComposer is responsible for stacking DeFiBlocks connectors in a manner that composes a final Strategy. /// Since DeFiBlock Sink/Source only support single assets and some Strategies may be multi-asset, we deal with /// building a Strategy distinctly from encapsulating the top-level DFB connectors acting as entrypoints in to the /// composed DeFiBlocks infrastructure. + /// /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyComposer. access(all) struct interface StrategyComposer { @@ -100,13 +106,18 @@ access(all) contract TidalYield { } } + /// StrategyFactory + /// + /// This resource enables the management of StrategyComposers and the construction of the Strategies they compose. + /// access(all) resource StrategyFactory { - /// The strategies this factory can build + /// A mapping of StrategyComposers indexed on the related Strategies they can compose access(self) let composers: {Type: {StrategyComposer}} init() { self.composers = {} } + access(all) view fun getSupportedStrategies(): [Type] { return self.composers.keys } @@ -130,6 +141,10 @@ access(all) contract TidalYield { } } + /// Tide + /// + /// A Tide is a resource enabling the management of a composed Strategy + /// access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, ViewResolver.Resolver { access(contract) let uniqueID: DFB.UniqueIdentifier access(self) let vaultType: Type @@ -201,8 +216,14 @@ access(all) contract TidalYield { } } + /// Entitlement enabling access on owner-related privileged operations on the TideManager resource access(all) entitlement Owner + /// TideManager + /// + /// A TideManager encapsulates nested Tide resources. Through a TideManager, one can create, manage, and close + /// out inner Tide resources. + /// access(all) resource TideManager : ViewResolver.ResolverCollection { access(self) let tides: @{UInt64: Tide} diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 72793779..5f6b5b19 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -24,7 +24,7 @@ import "MockSwapper" /// /// This contract defines Strategies used in the TidalYield platform. /// -/// A Strategy instance can be thought of as an an objects wrapping a stack of DeFiBlocks connectors wired together to +/// A Strategy instance can be thought of as an an objects wrapping a stack of DeFiBlocks connectors wired together to /// (optimally) generate some yield on initial deposits. Strategies can be simple such as swapping into a yield-bearing /// asset (such as stFLOW) or more complex compositions of DeFiBlocks connectors. /// @@ -88,7 +88,7 @@ access(all) contract TidalYieldStrategies { return { Type<@FlowToken.Vault>(): true } } - /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the + /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the /// provided Vault type access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { return { Type<@FlowToken.Vault>(): true } @@ -170,7 +170,7 @@ access(all) contract TidalYieldStrategies { // set the AutoBalancer's rebalance Sink which it will use to deposit overflown value, // recollateralizing the position autoBalancer.setSink(positionSwapSink) - + return <-create TracerStrategy( id: DFB.UniqueIdentifier(), collateralType: collateralType, From e005eb2e147d527a270ef7f97db23864cad10675 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:11:54 -0600 Subject: [PATCH 39/80] remove redundant dependency DFBUtils --- .../internal-dependencies/utils/DFBUtils.cdc | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 cadence/contracts/internal-dependencies/utils/DFBUtils.cdc diff --git a/cadence/contracts/internal-dependencies/utils/DFBUtils.cdc b/cadence/contracts/internal-dependencies/utils/DFBUtils.cdc deleted file mode 100644 index 57460ed6..00000000 --- a/cadence/contracts/internal-dependencies/utils/DFBUtils.cdc +++ /dev/null @@ -1,45 +0,0 @@ -import "FungibleToken" - -/// DFBUtils -/// -/// Utility methods commonly used across DeFiBlocks (DFB) related contracts -/// -access(all) contract DFBUtils { - - /// Checks that the contract defining vaultType conforms to the FungibleToken contract interface. This is required - /// to source empty Vaults in the event inner Capabilities become invalid - /// - /// @param vaultType: The Type of the Vault in question - /// - /// @return true if the Type a Vault and is defined by a FungibleToken contract, false otherwise - /// - access(all) view fun definingContractIsFungibleToken(_ vaultType: Type): Bool { - if !vaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { - return false - } - return getAccount(vaultType.address!).contracts.borrow<&{FungibleToken}>(name: vaultType.contractName!) != nil - } - - /// Returns an empty Vault of the given Type. Reverts if the provided Type is not defined by a FungibleToken - /// or if the returned Vault is not of the requested Type. Callers can use .definingContractIsFungibleToken() - /// to check the type before calling if they would like to prevent reverting. - /// - /// @param vaultType: The Type of the Vault to return as an empty Vault - /// - /// @return an empty Vault of the requested Type - /// - access(all) fun getEmptyVault(_ vaultType: Type): @{FungibleToken.Vault} { - pre { - self.definingContractIsFungibleToken(vaultType): - "Invalid vault Type \(vaultType.identifier) requested - cannot fulfill an empty Vault of an invalid type" - } - post { - result.getType() == vaultType: - "Invalid Vault returned - expected \(vaultType.identifier) but returned \(result.getType().identifier)" - } - return <- getAccount(vaultType.address!) - .contracts - .borrow<&{FungibleToken}>(name: vaultType.contractName!)! - .createEmptyVault(vaultType: vaultType) - } -} From 4716e799cf14340ad401b8b2c21714c1dd5d3ca8 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:06:02 -0600 Subject: [PATCH 40/80] add contract comments to TidalYield --- cadence/contracts/TidalYield.cdc | 95 +++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index d2ab1ce8..cf14efdb 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -118,24 +118,42 @@ access(all) contract TidalYield { self.composers = {} } + /// Returns the Strategy types that can be produced by this StrategyFactory access(all) view fun getSupportedStrategies(): [Type] { return self.composers.keys } + /// Returns the Vaults that can be used to initialize a Strategy of the given Type access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { return self.composers[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? {} } + /// Returns the Vaults that can be deposited to a Strategy initialized with the provided Type access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { - return self.composers[forStrategy]?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) ?? {} - } - access(all) fun createStrategy(_ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault}): @{Strategy} { + return self.composers[forStrategy] + ?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith) + ?? {} + } + /// Initializes a new Strategy of the given type with the provided Vault, identifying all associated DeFiBlocks + /// components by the provided UniqueIdentifier + access(all) + fun createStrategy(_ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault}): @{Strategy} { pre { self.composers[type] != nil: "Strategy \(type.identifier) is unsupported" } + post { + result.getType() == type: + "Invalid Strategy returned - expected \(type.identifier) but returned \(result.getType().identifier)" + } return <- self.composers[type]!.createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not } - access(Mutate) fun setStrategyComposer(_ strategy: Type, builder: {StrategyComposer}) { - self.composers[strategy] = builder + /// Sets the provided Strategy and Composer association in the StrategyFactory + access(Mutate) fun setStrategyComposer(_ strategy: Type, composer: {StrategyComposer}) { + pre { + composer.getComposedStrategyTypes()[strategy] == true: + "Strategy \(strategy.identifier) cannot be composed by StrategyComposer \(composer.getType().identifier)" + } + self.composers[strategy] = composer } + /// Removes the Strategy from this StrategyFactory and returns whether the value existed or not access(Mutate) fun removeStrategy(_ strategy: Type): Bool { return self.composers.remove(key: strategy) != nil } @@ -146,8 +164,11 @@ access(all) contract TidalYield { /// A Tide is a resource enabling the management of a composed Strategy /// access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, ViewResolver.Resolver { + /// The UniqueIdentifier that identifies all related DeFiBlocks connectors used in the encapsulated Strategy access(contract) let uniqueID: DFB.UniqueIdentifier + /// The type of Vault this Tide can receive as a deposit and provides as a withdrawal access(self) let vaultType: Type + /// The Strategy granting top-level access to the yield-bearing DeFiBlocks composition access(self) let strategy: @{Strategy} init(strategyType: Type, withVault: @{FungibleToken.Vault}) { @@ -162,26 +183,27 @@ access(all) contract TidalYield { message: "TODO") } + /// Returns the Tide's ID as defined by it's DFB.UniqueIdentifier.id access(all) view fun id(): UInt64 { return self.uniqueID.id } - + /// Returns the balance of the Tide's vaultType available via the encapsulated Strategy access(all) fun getTideBalance(): UFix64 { return self.strategy.availableBalance(ofToken: self.vaultType) } - + /// Burner.Burnable conformance - emits the BurnedTide event when burned access(contract) fun burnCallback() { emit BurnedTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, remainingBalance: self.getTideBalance()) } - + /// TODO: TidalYield specific views access(all) view fun getViews(): [Type] { return [] } - + /// TODO: TidalYield specific view resolution access(all) fun resolveView(_ view: Type): AnyStruct? { return nil } - + /// Deposits the provided Vault to the Strategy access(all) fun deposit(from: @{FungibleToken.Vault}) { pre { self.isSupportedVaultType(type: from.getType()): @@ -192,26 +214,30 @@ access(all) contract TidalYield { assert(from.balance == 0.0, message: "TODO") Burner.burn(<-from) } - + /// Returns the Vaults types supported by this Tide as a mapping associated with their current support status access(all) view fun getSupportedVaultTypes(): {Type: Bool} { return self.strategy.getSupportedCollateralTypes() } - + /// Returns whether the given Vault type is supported by this Tide access(all) view fun isSupportedVaultType(type: Type): Bool { return self.getSupportedVaultTypes()[type] ?? false } - + /// Withdraws the requested amount from the Strategy access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { post { - result.balance == amount: "TODO" + result.balance == amount: + "Invalid Vault balance returned - requested \(amount) but returned \(result.balance)" + self.vaultType == result.getType(): + "Invalid Vault returned - expected \(self.vaultType.identifier) but returned \(result.getType().identifier)" } let available = self.strategy.availableBalance(ofToken: self.vaultType) - assert( - amount <= available, - message: "Requested amount \(amount) is greater than withdrawable balance of \(available)" - ) + assert(amount <= available, + message: "Requested amount \(amount) is greater than withdrawable balance of \(available)") + let res <- self.strategy.withdraw(maxAmount: amount, ofToken: self.vaultType) + emit WithdrawnFromTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: amount, owner: self.owner?.address, toUUID: res.uuid) + return <- res } } @@ -225,28 +251,30 @@ access(all) contract TidalYield { /// out inner Tide resources. /// access(all) resource TideManager : ViewResolver.ResolverCollection { + /// The open Tides managed by this TideManager access(self) let tides: @{UInt64: Tide} init() { self.tides <- {} } + /// Borrows the unauthorized Tide with the given id, returning `nil` if none exists access(all) view fun borrowTide(id: UInt64): &Tide? { return &self.tides[id] } - + /// Borrows the Tide with the given ID as a ViewResolver.Resolver, returning `nil` if none exists access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? { return &self.tides[id] } - + /// Returns the Tide IDs managed by this TideManager access(all) view fun getIDs(): [UInt64] { return self.tides.keys } - + /// Returns the number of open Tides currently managed by this TideManager access(all) view fun getNumberOfTides(): Int { return self.tides.length } - + /// Creates a new Tide executing the specified Strategy with the provided funds access(all) fun createTide(strategyType: Type, withVault: @{FungibleToken.Vault}) { let balance = withVault.balance let tide <-create Tide(strategyType: strategyType, withVault: <-withVault) // TODO: fix init @@ -255,7 +283,8 @@ access(all) contract TidalYield { self.addTide(<-tide) } - + /// Adds an open Tide to this TideManager resource. This effectively transfers ownership of the newly added + /// Tide to the owner of this TideManager access(all) fun addTide(_ tide: @Tide) { pre { self.tides[tide.uniqueID.id] == nil: @@ -264,7 +293,7 @@ access(all) contract TidalYield { emit AddedToManager(id: tide.uniqueID.id, idType: tide.uniqueID.getType().identifier, owner: self.owner?.address, managerUUID: self.uuid) self.tides[tide.uniqueID.id] <-! tide } - + /// Deposits additional funds to the specified Tide, reverting if none exists with the provided ID access(all) fun depositToTide(_ id: UInt64, from: @{FungibleToken.Vault}) { pre { self.tides[id] != nil: @@ -273,16 +302,18 @@ access(all) contract TidalYield { let tide = (&self.tides[id] as &Tide?)! tide.deposit(from: <-from) } - - access(Owner) fun withdrawTide(id: UInt64): @Tide { + /// Withdraws the specified Tide, reverting if none exists with the provided ID + access(FungibleToken.Withdraw) fun withdrawTide(id: UInt64): @Tide { pre { self.tides[id] != nil: "No Tide with ID \(id) found" } return <- self.tides.remove(key: id)! } - - access(Owner) fun withdrawFromTide(_ id: UInt64, amount: UFix64): @{FungibleToken.Vault} { + /// Withdraws funds from the specified Tide in the given amount. The resulting Vault Type will be whatever + /// denomination is supported by the Tide, so callers should examine the Tide to know the resulting Vault to + /// expect + access(FungibleToken.Withdraw) fun withdrawFromTide(_ id: UInt64, amount: UFix64): @{FungibleToken.Vault} { pre { self.tides[id] != nil: "No Tide with ID \(id) found" @@ -290,15 +321,16 @@ access(all) contract TidalYield { let tide = (&self.tides[id] as auth(FungibleToken.Withdraw) &Tide?)! return <- tide.withdraw(amount: amount) } - - access(Owner) fun closeTide(_ id: UInt64): @{FungibleToken.Vault} { + /// Withdraws and returns all available funds from the specified Tide, destroying the Tide and access to any + /// Strategy-related wiring with it + access(FungibleToken.Withdraw) fun closeTide(_ id: UInt64): @{FungibleToken.Vault} { pre { self.tides[id] != nil: "No Tide with ID \(id) found" } let tide <- self.withdrawTide(id: id) let res <- tide.withdraw(amount: tide.getTideBalance()) - Burner.burn(<-tide) + Burner.burn(<-tide) // TODO: need to garbage collect anything related to the strategy (e.g. stored AutoBalancer) - add burnCallback on strategy for cleanup return <-res } } @@ -333,6 +365,7 @@ access(all) contract TidalYield { /* --- INTERNAL METHODS --- */ + /// Returns a reference to the StrategyFactory stored in this contract's account storage access(self) view fun _borrowFactory(): &StrategyFactory { return self.account.storage.borrow<&StrategyFactory>(from: self.FactoryStoragePath) ?? panic("Could not borrow reference to StrategyFactory from \(self.FactoryStoragePath)") From 11cf0de83b87c9d9922bd74a70c660a4ca7930b8 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:27:18 -0600 Subject: [PATCH 41/80] add AutoBalancer cleanup on Strategy burn --- cadence/contracts/TidalYield.cdc | 26 ++++++++++++------- cadence/contracts/TidalYieldAutoBalancers.cdc | 18 +++++++++++++ cadence/contracts/TidalYieldStrategies.cdc | 6 ++++- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index cf14efdb..9de4d134 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -47,7 +47,7 @@ access(all) contract TidalYield { /// /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyComposer. - access(all) resource interface Strategy : DFB.IdentifiableResource { + access(all) resource interface Strategy : DFB.IdentifiableResource, Burner.Burnable { /// Returns the type of Vaults that this Strategy instance can handle access(all) view fun getSupportedCollateralTypes(): {Type: Bool} /// Returns whether the provided Vault type is supported by this Strategy instance @@ -169,18 +169,19 @@ access(all) contract TidalYield { /// The type of Vault this Tide can receive as a deposit and provides as a withdrawal access(self) let vaultType: Type /// The Strategy granting top-level access to the yield-bearing DeFiBlocks composition - access(self) let strategy: @{Strategy} + access(self) var strategy: @{Strategy}? init(strategyType: Type, withVault: @{FungibleToken.Vault}) { self.uniqueID = DFB.UniqueIdentifier() self.vaultType = withVault.getType() - self.strategy <- TidalYield.createStrategy( + let _strategy <- TidalYield.createStrategy( type: strategyType, uniqueID: self.uniqueID, withFunds: <-withVault ) - assert(self.strategy.isSupportedCollateralType(self.vaultType), + assert(_strategy.isSupportedCollateralType(self.vaultType), message: "TODO") + self.strategy <-_strategy } /// Returns the Tide's ID as defined by it's DFB.UniqueIdentifier.id @@ -189,11 +190,13 @@ access(all) contract TidalYield { } /// Returns the balance of the Tide's vaultType available via the encapsulated Strategy access(all) fun getTideBalance(): UFix64 { - return self.strategy.availableBalance(ofToken: self.vaultType) + return self._borrowStrategy().availableBalance(ofToken: self.vaultType) } /// Burner.Burnable conformance - emits the BurnedTide event when burned access(contract) fun burnCallback() { emit BurnedTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, remainingBalance: self.getTideBalance()) + let _strategy <- self.strategy <- nil + Burner.burn(<-_strategy) } /// TODO: TidalYield specific views access(all) view fun getViews(): [Type] { @@ -210,13 +213,13 @@ access(all) contract TidalYield { "Deposited vault of type \(from.getType().identifier) is not supported by this Tide" } emit DepositedToTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: from.balance, owner: self.owner?.address, fromUUID: from.uuid) - self.strategy.deposit(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + self._borrowStrategy().deposit(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) assert(from.balance == 0.0, message: "TODO") Burner.burn(<-from) } /// Returns the Vaults types supported by this Tide as a mapping associated with their current support status access(all) view fun getSupportedVaultTypes(): {Type: Bool} { - return self.strategy.getSupportedCollateralTypes() + return self._borrowStrategy().getSupportedCollateralTypes() } /// Returns whether the given Vault type is supported by this Tide access(all) view fun isSupportedVaultType(type: Type): Bool { @@ -230,16 +233,21 @@ access(all) contract TidalYield { self.vaultType == result.getType(): "Invalid Vault returned - expected \(self.vaultType.identifier) but returned \(result.getType().identifier)" } - let available = self.strategy.availableBalance(ofToken: self.vaultType) + let available = self._borrowStrategy().availableBalance(ofToken: self.vaultType) assert(amount <= available, message: "Requested amount \(amount) is greater than withdrawable balance of \(available)") - let res <- self.strategy.withdraw(maxAmount: amount, ofToken: self.vaultType) + let res <- self._borrowStrategy().withdraw(maxAmount: amount, ofToken: self.vaultType) emit WithdrawnFromTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: amount, owner: self.owner?.address, toUUID: res.uuid) return <- res } + /// Returns + access(self) view fun _borrowStrategy(): auth(FungibleToken.Withdraw) &{Strategy} { + return &self.strategy as auth(FungibleToken.Withdraw) &{Strategy}? + ?? panic("Unknown error - could not borrow Strategy for Tide #\(self.id())") + } } /// Entitlement enabling access on owner-related privileged operations on the TideManager resource diff --git a/cadence/contracts/TidalYieldAutoBalancers.cdc b/cadence/contracts/TidalYieldAutoBalancers.cdc index c1e99b34..ff30bb0d 100644 --- a/cadence/contracts/TidalYieldAutoBalancers.cdc +++ b/cadence/contracts/TidalYieldAutoBalancers.cdc @@ -1,3 +1,4 @@ +import "Burner" import "FungibleToken" import "DFB" @@ -87,6 +88,23 @@ access(all) contract TidalYieldAutoBalancers { ) ?? panic("Could not borrow reference to AutoBalancer with UniqueIdentifier.id \(id) from StoragePath \(storagePath)") } + /// Called by strategies defined in the TidalYield account which leverage account-hosted AutoBalancers when a + /// Strategy is burned + access(account) fun _cleanupAutoBalancer(id: UInt64) { + let storagePath = self.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath + let publicPath = self.deriveAutoBalancerPath(id: id, storage: false) as! PublicPath + // unpublish the public AutoBalancer Capability + self.account.capabilities.unpublish(publicPath) + // delete any CapabilityControllers targetting the AutoBalancer + self.account.capabilities.storage.forEachController(forPath: storagePath, fun(_ controller: &StorageCapabilityController): Bool { + controller.delete() + return true + }) + // load & burn the AutoBalancer + let autoBalancer <-self.account.storage.load<@DFB.AutoBalancer>(from: storagePath) + Burner.burn(<-autoBalancer) + } + init() { self.pathPrefix = "TidalYieldAutoBalancer_" } diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 5f6b5b19..6b7c4142 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -45,7 +45,7 @@ access(all) contract TidalYieldStrategies { access(self) var sink: {DFB.Sink} access(self) var source: {DFB.Source} - init(id: DFB.UniqueIdentifier?, collateralType: Type, position: TidalProtocol.Position) { + init(id: DFB.UniqueIdentifier, collateralType: Type, position: TidalProtocol.Position) { self.uniqueID = id self.position = position self.sink = position.createSink(type: collateralType) @@ -74,6 +74,10 @@ access(all) contract TidalYieldStrategies { } return <- self.source.withdrawAvailable(maxAmount: maxAmount) } + /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer + access(contract) fun burnCallback() { + TidalYieldAutoBalancers._cleanupAutoBalancer(id: self.id()!) + } } /// This StrategyComposer builds a TracerStrategy From d9f35f412cd6fab031c1c017620ceada1b0810b3 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:09:45 -0600 Subject: [PATCH 42/80] refactor StrategyComposer from struct interface to resource interface --- cadence/contracts/TidalYield.cdc | 25 ++++++++++++++++------ cadence/contracts/TidalYieldStrategies.cdc | 2 +- cadence/contracts/mocks/MockStrategy.cdc | 13 ++++++++--- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 9de4d134..818fd5a5 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -82,7 +82,7 @@ access(all) contract TidalYield { /// /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyComposer. - access(all) struct interface StrategyComposer { + access(all) resource interface StrategyComposer { /// Returns the Types of Strategies composed by this StrategyComposer access(all) view fun getComposedStrategyTypes(): {Type: Bool} /// Returns the Vault types which can be used to initialize a given Strategy @@ -112,10 +112,10 @@ access(all) contract TidalYield { /// access(all) resource StrategyFactory { /// A mapping of StrategyComposers indexed on the related Strategies they can compose - access(self) let composers: {Type: {StrategyComposer}} + access(self) let composers: @{Type: {StrategyComposer}} init() { - self.composers = {} + self.composers <- {} } /// Returns the Strategy types that can be produced by this StrategyFactory @@ -143,19 +143,30 @@ access(all) contract TidalYield { result.getType() == type: "Invalid Strategy returned - expected \(type.identifier) but returned \(result.getType().identifier)" } - return <- self.composers[type]!.createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not + return <- self._borrowComposer(forStrategy: type) + .createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not } /// Sets the provided Strategy and Composer association in the StrategyFactory - access(Mutate) fun setStrategyComposer(_ strategy: Type, composer: {StrategyComposer}) { + access(Mutate) fun addStrategyComposer(_ strategy: Type, composer: @{StrategyComposer}) { pre { composer.getComposedStrategyTypes()[strategy] == true: "Strategy \(strategy.identifier) cannot be composed by StrategyComposer \(composer.getType().identifier)" } - self.composers[strategy] = composer + let old <- self.composers[strategy] <- composer + Burner.burn(<-old) } /// Removes the Strategy from this StrategyFactory and returns whether the value existed or not access(Mutate) fun removeStrategy(_ strategy: Type): Bool { - return self.composers.remove(key: strategy) != nil + if let removed <- self.composers.remove(key: strategy) { + Burner.burn(<-removed) + return true + } + return false + } + /// Returns a reference to the StrategyComposer for the requested Strategy type, reverting if none exists + access(self) view fun _borrowComposer(forStrategy: Type): &{StrategyComposer} { + return &self.composers[forStrategy] as &{StrategyComposer}? + ?? panic("Could not borrow StrategyComposer for Strategy \(forStrategy.identifier)") } } diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 6b7c4142..7df26567 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -81,7 +81,7 @@ access(all) contract TidalYieldStrategies { } /// This StrategyComposer builds a TracerStrategy - access(all) struct TracerStrategyComposer : TidalYield.StrategyComposer { + access(all) resource TracerStrategyComposer : TidalYield.StrategyComposer { /// Returns the Types of Strategies composed by this StrategyComposer access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@TracerStrategy>(): true } diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 0aef5566..bdcfc32f 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -82,9 +82,11 @@ access(all) contract MockStrategy { } return <- self.source.withdrawAvailable(maxAmount: maxAmount) } + + access(contract) fun burnCallback() {} // no-op } - access(all) struct DummyStrategyComposer : TidalYield.StrategyComposer { + access(all) resource DummyStrategyComposer : TidalYield.StrategyComposer { access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@DummyStrategy>(): true } } @@ -94,7 +96,12 @@ access(all) contract MockStrategy { access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { return {} } - access(all) fun createStrategy(_ type: Type, withFunds: @{FungibleToken.Vault}, params: {String: AnyStruct}): @{TidalYield.Strategy} { + access(all) fun createStrategy( + _ type: Type, + uniqueID: DFB.UniqueIdentifier, + withFunds: @{FungibleToken.Vault}, + params: {String: AnyStruct} + ): @{TidalYield.Strategy} { let id = DFB.UniqueIdentifier() let strat <- create DummyStrategy( id: id, @@ -106,4 +113,4 @@ access(all) contract MockStrategy { return <- strat } } -} \ No newline at end of file +} From 1fa0b7ae2e35b5d5ff5eaec69326af9d4be503eb Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:19:46 -0600 Subject: [PATCH 43/80] update comments & error messages --- cadence/contracts/TidalYield.cdc | 33 +++++++++++++++++----- cadence/contracts/TidalYieldStrategies.cdc | 25 +++++++++++++--- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 818fd5a5..3ac4ad56 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -33,13 +33,13 @@ access(all) contract TidalYield { /// Strategy /// - /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of composed + /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of stacked /// DeFiBlocks components. These compositions are intended to capitalize on some yield-bearing opportunity so that /// a Strategy bears yield on that which is deposited into it, albeit not without some risk. A Strategy then can be /// thought of as the top-level of a nesting of DeFiBlocks connectors & adapters where one can deposit & withdraw /// funds into the composed DeFi workflows. /// - /// While two types of strategies may not highly differ with respect to their fields, the compositions of DeFiBlocks + /// While two types of strategies may not highly differ with respect to their fields, the stacking of DeFiBlocks /// components & connections they provide access to likely do. This difference in wiring is why the Strategy is a /// resource - because the Type and uniqueness of composition of a given Strategy must be preserved as that is its /// distinguishing factor. These qualities are preserved by restricting the party who can construct it, which for @@ -78,7 +78,7 @@ access(all) contract TidalYield { /// A StrategyComposer is responsible for stacking DeFiBlocks connectors in a manner that composes a final Strategy. /// Since DeFiBlock Sink/Source only support single assets and some Strategies may be multi-asset, we deal with /// building a Strategy distinctly from encapsulating the top-level DFB connectors acting as entrypoints in to the - /// composed DeFiBlocks infrastructure. + /// DeFiBlocks stack. /// /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away /// with the added layer of abstraction introduced by a StrategyComposer. @@ -170,6 +170,21 @@ access(all) contract TidalYield { } } + /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which + /// may utilize resource consumption (i.e. account storage). Contracts defining Strategies that do not require + /// such protections may wish to expose Strategy creation publicly via public Capabilities. + access(all) resource interface StrategyComposerIssuer { + /// Returns the StrategyComposer types supported by this issuer + access(all) view fun getSupportedComposers(): {Type: Bool} + /// Returns the requested StrategyComposer. If the requested type is unsupported, a revert should be expected + access(all) fun issueComposer(_ type: Type): @{StrategyComposer} { + post { + result.getType() == type: + "Invalid StrategyComposer returned - requested \(type.identifier) but returned \(result.getType().identifier)" + } + } + } + /// Tide /// /// A Tide is a resource enabling the management of a composed Strategy @@ -179,7 +194,7 @@ access(all) contract TidalYield { access(contract) let uniqueID: DFB.UniqueIdentifier /// The type of Vault this Tide can receive as a deposit and provides as a withdrawal access(self) let vaultType: Type - /// The Strategy granting top-level access to the yield-bearing DeFiBlocks composition + /// The Strategy granting top-level access to the yield-bearing DeFiBlocks stack access(self) var strategy: @{Strategy}? init(strategyType: Type, withVault: @{FungibleToken.Vault}) { @@ -191,7 +206,7 @@ access(all) contract TidalYield { withFunds: <-withVault ) assert(_strategy.isSupportedCollateralType(self.vaultType), - message: "TODO") + message: "Vault type \(self.vaultType.identifier) is not supported by Strategy \(strategyType.identifier)") self.strategy <-_strategy } @@ -223,9 +238,13 @@ access(all) contract TidalYield { self.isSupportedVaultType(type: from.getType()): "Deposited vault of type \(from.getType().identifier) is not supported by this Tide" } + let amount = from.balance emit DepositedToTide(id: self.uniqueID.id, idType: self.uniqueID.getType().identifier, amount: from.balance, owner: self.owner?.address, fromUUID: from.uuid) self._borrowStrategy().deposit(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - assert(from.balance == 0.0, message: "TODO") + assert( + from.balance == 0.0, + message: "Deposit amount \(amount) of \(self.vaultType.identifier) could not be deposited to Tide \(self.id())" + ) Burner.burn(<-from) } /// Returns the Vaults types supported by this Tide as a mapping associated with their current support status @@ -349,7 +368,7 @@ access(all) contract TidalYield { } let tide <- self.withdrawTide(id: id) let res <- tide.withdraw(amount: tide.getTideBalance()) - Burner.burn(<-tide) // TODO: need to garbage collect anything related to the strategy (e.g. stored AutoBalancer) - add burnCallback on strategy for cleanup + Burner.burn(<-tide) return <-res } } diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 7df26567..d88003d7 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -24,18 +24,18 @@ import "MockSwapper" /// /// This contract defines Strategies used in the TidalYield platform. /// -/// A Strategy instance can be thought of as an an objects wrapping a stack of DeFiBlocks connectors wired together to +/// A Strategy instance can be thought of as objects wrapping a stack of DeFiBlocks connectors wired together to /// (optimally) generate some yield on initial deposits. Strategies can be simple such as swapping into a yield-bearing -/// asset (such as stFLOW) or more complex compositions of DeFiBlocks connectors. +/// asset (such as stFLOW) or more complex DeFiBlocks stacks. /// -/// A StrategyComposer is tasked with the creation of a supported Strategy. It's within the composition of DeFiBlocks +/// A StrategyComposer is tasked with the creation of a supported Strategy. It's within the stacking of DeFiBlocks /// connectors that the true power of the components lies. /// access(all) contract TidalYieldStrategies { /// This is the first Strategy implementation, wrapping a TidalProtocol Position along with its related Sink & /// Source. While this object is a simple wrapper for the top-level collateralized position, the true magic of the - /// DeFiBlocks is in the stacking of the related connectors. This composition logic can be found in the + /// DeFiBlocks is in the stacking of the related connectors. This stacking logic can be found in the /// TracerStrategyComposer construct. access(all) resource TracerStrategy : TidalYield.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- @@ -182,4 +182,21 @@ access(all) contract TidalYieldStrategies { ) } } + + /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which + /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage + /// via configured AutoBalancers + access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { + access(all) view fun getSupportedComposers(): {Type: Bool} { + return { Type<@TracerStrategyComposer>(): true } + } + access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { + switch type { + case Type<@TracerStrategyComposer>(): + return <- create TracerStrategyComposer() + default: + panic("Unsupported StrategyComposer requested: \(type.identifier)") + } + } + } } From 8218909b25d98d1ff888f86ff5ef6aa113dc19aa Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:21:37 -0600 Subject: [PATCH 44/80] add StrategyComposerIssuer to contract init block --- cadence/contracts/TidalYieldStrategies.cdc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index d88003d7..cc9d66ce 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -33,6 +33,8 @@ import "MockSwapper" /// access(all) contract TidalYieldStrategies { + access(all) let IssuerStoragePath: StoragePath + /// This is the first Strategy implementation, wrapping a TidalProtocol Position along with its related Sink & /// Source. While this object is a simple wrapper for the top-level collateralized position, the true magic of the /// DeFiBlocks is in the stacking of the related connectors. This stacking logic can be found in the @@ -199,4 +201,10 @@ access(all) contract TidalYieldStrategies { } } } + + init() { + self.IssuerStoragePath = StoragePath(identifier: "TidalYieldStrategyComposerIssuer_\(self.account.address)")! + + self.account.storage.save(<-create StrategyComposerIssuer(), to: self.IssuerStoragePath) + } } From e3e3bf8280d9c5e67b09cf16799208152ac330e3 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:33:26 -0600 Subject: [PATCH 45/80] rename cadence/transactions/tidal to cadence/transactions/tidal-yield --- .../transactions/{tidal => tidal-yield}/close_tide.cdc | 0 .../{tidal => tidal-yield}/deposit_to_tide.cdc | 0 .../transactions/{tidal => tidal-yield}/open_tide.cdc | 9 +++++++-- cadence/transactions/{tidal => tidal-yield}/setup.cdc | 0 .../{tidal => tidal-yield}/withdraw_from_tide.cdc | 0 5 files changed, 7 insertions(+), 2 deletions(-) rename cadence/transactions/{tidal => tidal-yield}/close_tide.cdc (100%) rename cadence/transactions/{tidal => tidal-yield}/deposit_to_tide.cdc (100%) rename cadence/transactions/{tidal => tidal-yield}/open_tide.cdc (85%) rename cadence/transactions/{tidal => tidal-yield}/setup.cdc (100%) rename cadence/transactions/{tidal => tidal-yield}/withdraw_from_tide.cdc (100%) diff --git a/cadence/transactions/tidal/close_tide.cdc b/cadence/transactions/tidal-yield/close_tide.cdc similarity index 100% rename from cadence/transactions/tidal/close_tide.cdc rename to cadence/transactions/tidal-yield/close_tide.cdc diff --git a/cadence/transactions/tidal/deposit_to_tide.cdc b/cadence/transactions/tidal-yield/deposit_to_tide.cdc similarity index 100% rename from cadence/transactions/tidal/deposit_to_tide.cdc rename to cadence/transactions/tidal-yield/deposit_to_tide.cdc diff --git a/cadence/transactions/tidal/open_tide.cdc b/cadence/transactions/tidal-yield/open_tide.cdc similarity index 85% rename from cadence/transactions/tidal/open_tide.cdc rename to cadence/transactions/tidal-yield/open_tide.cdc index 18bc9d17..13f32959 100644 --- a/cadence/transactions/tidal/open_tide.cdc +++ b/cadence/transactions/tidal-yield/open_tide.cdc @@ -10,11 +10,16 @@ import "TidalYield" /// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' /// @param amount: The amount to deposit into the new Tide /// -transaction(vaultIdentifier: String, amount: UFix64) { +transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) { let manager: &TidalYield.TideManager + let strategy: Type let depositVault: @{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { + // create the Strategy Type to compose which the Tide should manage + self.strategy = CompositeType(strategyIdentifier) + ?? panic("Invalid strategyIdentifier \(strategyIdentifier) - ensure the provided strategyIdentifier corresponds to a valid Strategy Type") + // get the data for where the vault type is canoncially stored let vaultType = CompositeType(vaultIdentifier) ?? panic("Vault identifier \(vaultIdentifier) is not associated with a valid Type") @@ -46,6 +51,6 @@ transaction(vaultIdentifier: String, amount: UFix64) { } execute { - self.manager.createTide(withVault: <-self.depositVault) + self.manager.createTide(strategyType: self.strategy, withVault: <-self.depositVault) } } \ No newline at end of file diff --git a/cadence/transactions/tidal/setup.cdc b/cadence/transactions/tidal-yield/setup.cdc similarity index 100% rename from cadence/transactions/tidal/setup.cdc rename to cadence/transactions/tidal-yield/setup.cdc diff --git a/cadence/transactions/tidal/withdraw_from_tide.cdc b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc similarity index 100% rename from cadence/transactions/tidal/withdraw_from_tide.cdc rename to cadence/transactions/tidal-yield/withdraw_from_tide.cdc From 79d0ad609f16e4028e5df76cd2b6c93fae046658 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:36:47 -0600 Subject: [PATCH 46/80] rename scripts sub-directory --- cadence/scripts/{tidal => tidal-yield}/get_tide_ids.cdc | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cadence/scripts/{tidal => tidal-yield}/get_tide_ids.cdc (100%) diff --git a/cadence/scripts/tidal/get_tide_ids.cdc b/cadence/scripts/tidal-yield/get_tide_ids.cdc similarity index 100% rename from cadence/scripts/tidal/get_tide_ids.cdc rename to cadence/scripts/tidal-yield/get_tide_ids.cdc From 5aa2138fda104c31a28e314ca2735bb3a2aa1265 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:48:41 -0600 Subject: [PATCH 47/80] update MockStrategy contract construct names --- cadence/contracts/mocks/MockStrategy.cdc | 41 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index bdcfc32f..25d5125b 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -11,8 +11,10 @@ import "TidalYield" /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// access(all) contract MockStrategy { + + access(all) let IssuerStoragePath : StoragePath - access(all) struct DummySink : DFB.Sink { + access(all) struct Sink : DFB.Sink { access(contract) let uniqueID: DFB.UniqueIdentifier? init(_ id: DFB.UniqueIdentifier?) { self.uniqueID = id @@ -27,7 +29,7 @@ access(all) contract MockStrategy { return } } - access(all) struct DummySource : DFB.Source { + access(all) struct Source : DFB.Source { access(contract) let uniqueID: DFB.UniqueIdentifier? init(_ id: DFB.UniqueIdentifier?) { self.uniqueID = id @@ -43,7 +45,7 @@ access(all) contract MockStrategy { } } - access(all) resource DummyStrategy : TidalYield.Strategy { + access(all) resource Strategy : TidalYield.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -86,9 +88,9 @@ access(all) contract MockStrategy { access(contract) fun burnCallback() {} // no-op } - access(all) resource DummyStrategyComposer : TidalYield.StrategyComposer { + access(all) resource StrategyComposer : TidalYield.StrategyComposer { access(all) view fun getComposedStrategyTypes(): {Type: Bool} { - return { Type<@DummyStrategy>(): true } + return { Type<@Strategy>(): true } } access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { return {} @@ -103,14 +105,37 @@ access(all) contract MockStrategy { params: {String: AnyStruct} ): @{TidalYield.Strategy} { let id = DFB.UniqueIdentifier() - let strat <- create DummyStrategy( + let strat <- create Strategy( id: id, - sink: DummySink(id), - source: DummySource(id) + sink: Sink(id), + source: Source(id) ) strat.deposit(from: &withFunds as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) destroy withFunds return <- strat } } + + /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which + /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage + /// via configured AutoBalancers + access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { + access(all) view fun getSupportedComposers(): {Type: Bool} { + return { Type<@StrategyComposer>(): true } + } + access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { + switch type { + case Type<@StrategyComposer>(): + return <- create StrategyComposer() + default: + panic("Unsupported StrategyComposer requested: \(type.identifier)") + } + } + } + + init() { + self.IssuerStoragePath = StoragePath(identifier: "TidalYieldStrategyComposerIssuer_\(self.account.address)")! + + self.account.storage.save(<-create StrategyComposerIssuer(), to: self.IssuerStoragePath) + } } From ac345cb7d2681199eb02429d91d3ee1c47322af4 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:55:00 -0600 Subject: [PATCH 48/80] add TidalProtocol supporting txns & scripts --- .../tidal-protocol/get_available_balance.cdc | 13 ++++++++ .../get_reserve_balance_for_type.cdc | 17 ++++++++++ .../scripts/tidal-protocol/pool_exists.cdc | 9 ++++++ .../tidal-protocol/position_health.cdc | 13 ++++++++ .../pool-factory/create_and_store_pool.cdc | 30 +++++++++++++++++ ..._supported_token_simple_interest_curve.cdc | 32 +++++++++++++++++++ .../pool-management/rebalance_position.cdc | 14 ++++++++ 7 files changed, 128 insertions(+) create mode 100644 cadence/scripts/tidal-protocol/get_available_balance.cdc create mode 100644 cadence/scripts/tidal-protocol/get_reserve_balance_for_type.cdc create mode 100644 cadence/scripts/tidal-protocol/pool_exists.cdc create mode 100644 cadence/scripts/tidal-protocol/position_health.cdc create mode 100644 cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc create mode 100644 cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc create mode 100644 cadence/transactions/tidal-protocol/pool-management/rebalance_position.cdc diff --git a/cadence/scripts/tidal-protocol/get_available_balance.cdc b/cadence/scripts/tidal-protocol/get_available_balance.cdc new file mode 100644 index 00000000..ad7caf93 --- /dev/null +++ b/cadence/scripts/tidal-protocol/get_available_balance.cdc @@ -0,0 +1,13 @@ +import "TidalProtocol" + +access(all) +fun main(pid: UInt64, vaultIdentifier: String, pullFromTopUpSource: Bool): UFix64 { + let vaultType = CompositeType(vaultIdentifier) ?? panic("Invalid vaultIdentifier \(vaultIdentifier)") + + let protocolAddress= Type<@TidalProtocol.Pool>().address! + + let pool = getAccount(protocolAddress).capabilities.borrow<&TidalProtocol.Pool>(TidalProtocol.PoolPublicPath) + ?? panic("Could not find a configured TidalProtocol Pool in account \(protocolAddress) at path \(TidalProtocol.PoolPublicPath)") + + return pool.availableBalance(pid: pid, type: vaultType, pullFromTopUpSource: pullFromTopUpSource) +} diff --git a/cadence/scripts/tidal-protocol/get_reserve_balance_for_type.cdc b/cadence/scripts/tidal-protocol/get_reserve_balance_for_type.cdc new file mode 100644 index 00000000..9b57511d --- /dev/null +++ b/cadence/scripts/tidal-protocol/get_reserve_balance_for_type.cdc @@ -0,0 +1,17 @@ +import "TidalProtocol" + +/// Returns the Pool's reserve balance for a given Vault type +/// +/// @param vaultIdentifier: The Type identifier (e.g. vault.getType().identifier) of the related token vault +/// +access(all) +fun main(vaultIdentifier: String): UFix64 { + let vaultType = CompositeType(vaultIdentifier) ?? panic("Invalid vaultIdentifier \(vaultIdentifier)") + + let protocolAddress= Type<@TidalProtocol.Pool>().address! + + let pool = getAccount(protocolAddress).capabilities.borrow<&TidalProtocol.Pool>(TidalProtocol.PoolPublicPath) + ?? panic("Could not find a configured TidalProtocol Pool in account \(protocolAddress) at path \(TidalProtocol.PoolPublicPath)") + + return pool.reserveBalance(type: vaultType) +} diff --git a/cadence/scripts/tidal-protocol/pool_exists.cdc b/cadence/scripts/tidal-protocol/pool_exists.cdc new file mode 100644 index 00000000..fc10792e --- /dev/null +++ b/cadence/scripts/tidal-protocol/pool_exists.cdc @@ -0,0 +1,9 @@ +import "TidalProtocol" + +/// Returns whether there is a Pool stored in the provided account's address. This address would normally be the +/// TidalProtocol contract address +/// +access(all) +fun main(address: Address): Bool { + return getAccount(address).storage.type(at: TidalProtocol.PoolStoragePath) == Type<@TidalProtocol.Pool>() +} diff --git a/cadence/scripts/tidal-protocol/position_health.cdc b/cadence/scripts/tidal-protocol/position_health.cdc new file mode 100644 index 00000000..9d3697db --- /dev/null +++ b/cadence/scripts/tidal-protocol/position_health.cdc @@ -0,0 +1,13 @@ +import "TidalProtocol" + +/// Returns the position health for a given position id, reverting if the position does not exist +/// +/// @param pid: The Position ID +/// +access(all) +fun main(pid: UInt64): UFix64 { + let protocolAddress= Type<@TidalProtocol.Pool>().address! + return getAccount(protocolAddress).capabilities.borrow<&TidalProtocol.Pool>(TidalProtocol.PoolPublicPath) + ?.positionHealth(pid: pid) + ?? panic("Could not find a configured TidalProtocol Pool in account \(protocolAddress) at path \(TidalProtocol.PoolPublicPath)") +} diff --git a/cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc b/cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc new file mode 100644 index 00000000..164ea283 --- /dev/null +++ b/cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc @@ -0,0 +1,30 @@ +import "FungibleToken" + +import "DFB" +import "TidalProtocol" +import "MockOracle" + +/// THIS TRANSACTION IS NOT INTENDED FOR PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +/// Creates the protocol pool in the TidalProtocol account via the stored PoolFactory resource +/// +/// @param defaultTokenIdentifier: The Type identifier (e.g. resource.getType().identifier) of the Pool's default token +/// +transaction(defaultTokenIdentifier: String) { + + let factory: &TidalProtocol.PoolFactory + let defaultToken: Type + let oracle: {DFB.PriceOracle} + + prepare(signer: auth(BorrowValue) &Account) { + self.factory = signer.storage.borrow<&TidalProtocol.PoolFactory>(from: TidalProtocol.PoolFactoryPath) + ?? panic("Could not find PoolFactory in signer's account") + self.defaultToken = CompositeType(defaultTokenIdentifier) ?? panic("Invalid defaultTokenIdentifier \(defaultTokenIdentifier)") + self.oracle = MockOracle.PriceOracle() + } + + execute { + self.factory.createPool(defaultToken: self.defaultToken, priceOracle: self.oracle) + } +} diff --git a/cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc b/cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc new file mode 100644 index 00000000..e2468a77 --- /dev/null +++ b/cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc @@ -0,0 +1,32 @@ +import "TidalProtocol" + +/// Adds a token type as supported to the stored pool, reverting if a Pool is not found +/// +transaction( + tokenTypeIdentifier: String, + collateralFactor: UFix64, + borrowFactor: UFix64, + depositRate: UFix64, + depositCapacityCap: UFix64 +) { + let tokenType: Type + let pool: auth(TidalProtocol.EGovernance) &TidalProtocol.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + self.pool = signer.storage.borrow(from: TidalProtocol.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(TidalProtocol.PoolStoragePath) - ensure a Pool has been configured") + } + + execute { + self.pool.addSupportedToken( + tokenType: self.tokenType, + collateralFactor: collateralFactor, + borrowFactor: borrowFactor, + interestCurve: TidalProtocol.SimpleInterestCurve(), + depositRate: depositRate, + depositCapacityCap: depositCapacityCap + ) + } +} diff --git a/cadence/transactions/tidal-protocol/pool-management/rebalance_position.cdc b/cadence/transactions/tidal-protocol/pool-management/rebalance_position.cdc new file mode 100644 index 00000000..62aa11b6 --- /dev/null +++ b/cadence/transactions/tidal-protocol/pool-management/rebalance_position.cdc @@ -0,0 +1,14 @@ +import "TidalProtocol" + +transaction(pid: UInt64, force: Bool) { + let pool: auth(TidalProtocol.EPosition) &TidalProtocol.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow(from: TidalProtocol.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(TidalProtocol.PoolStoragePath) - ensure a Pool has been configured") + } + + execute { + self.pool.rebalancePosition(pid: pid, force: force) + } +} \ No newline at end of file From c3abff2677a9c0a226433cd77c1f6669ce1d1627 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:06:38 -0600 Subject: [PATCH 49/80] add initial test and test helpers & update flow.json testing aliases --- cadence/contracts/mocks/MockStrategy.cdc | 2 +- cadence/tests/deployment_test.cdc | 14 ++ cadence/tests/test_helpers.cdc | 180 +++++++++++++++++++++++ flow.json | 23 +-- 4 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 cadence/tests/deployment_test.cdc create mode 100644 cadence/tests/test_helpers.cdc diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 25d5125b..05f14bc2 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -134,7 +134,7 @@ access(all) contract MockStrategy { } init() { - self.IssuerStoragePath = StoragePath(identifier: "TidalYieldStrategyComposerIssuer_\(self.account.address)")! + self.IssuerStoragePath = StoragePath(identifier: "MockStrategyComposerIssuer_\(self.account.address)")! self.account.storage.save(<-create StrategyComposerIssuer(), to: self.IssuerStoragePath) } diff --git a/cadence/tests/deployment_test.cdc b/cadence/tests/deployment_test.cdc new file mode 100644 index 00000000..1dfc0a7a --- /dev/null +++ b/cadence/tests/deployment_test.cdc @@ -0,0 +1,14 @@ +import Test +import BlockchainHelpers + +import "test_helpers.cdc" + +access(all) +fun setup() { + deployContracts() +} + +access(all) +fun test_DeploymentSuccess() { + log("Success: Contracts deployed") +} \ No newline at end of file diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc new file mode 100644 index 00000000..d2746bf3 --- /dev/null +++ b/cadence/tests/test_helpers.cdc @@ -0,0 +1,180 @@ +import Test + +import "MOET" + +/* --- Test execution helpers --- */ + +access(all) +fun _executeScript(_ path: String, _ args: [AnyStruct]): Test.ScriptResult { + return Test.executeScript(Test.readFile(path), args) +} + +access(all) +fun _executeTransaction(_ path: String, _ args: [AnyStruct], _ signer: Test.TestAccount): Test.TransactionResult { + let txn = Test.Transaction( + code: Test.readFile(path), + authorizers: [signer.address], + signers: [signer], + arguments: args + ) + return Test.executeTransaction(txn) +} + +/* --- Setup helpers --- */ + +// Common test setup function that deploys all required contracts +access(all) fun deployContracts() { + // DeFiBlocks contracts + var err = Test.deployContract( + name: "DFBUtils", + path: "../../DeFiBlocks/cadence/contracts/utils/DFBUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "DFB", + path: "../../DeFiBlocks/cadence/contracts/interfaces/DFB.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "SwapStack", + path: "../../DeFiBlocks/cadence/contracts/connectors/SwapStack.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // TidalProtocol contracts + let initialMoetSupply = 0.0 + err = Test.deployContract( + name: "MOET", + path: "../contracts/internal-dependencies/tokens/MOET.cdc", + arguments: [initialMoetSupply] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "TidalProtocol", + path: "../contracts/internal-dependencies/TidalProtocol.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Mocked contracts + let initialYieldSupply = 0.0 + err = Test.deployContract( + name: "YieldToken", + path: "../contracts/internal-dependencies/tokens/YieldToken.cdc", + arguments: [initialYieldSupply] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "MockOracle", + path: "../contracts/mocks/MockOracle.cdc", + arguments: [Type<@MOET.Vault>().identifier] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "MockSwapper", + path: "../contracts/mocks/MockSwapper.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // TidalYield contracts + err = Test.deployContract( + name: "TidalYieldAutoBalancers", + path: "../contracts/TidalYieldAutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "TidalYield", + path: "../contracts/TidalYield.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "TidalYieldStrategies", + path: "../contracts/TidalYieldStrategies.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Mocked Strategy + err = Test.deployContract( + name: "MockStrategy", + path: "../contracts/mocks/MockStrategy.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +/* --- Script helpers */ + +access(all) +fun getBalance(address: Address, vaultPublicPath: PublicPath): UFix64? { + let res = _executeScript("../scripts/tokens/get_balance.cdc", [address, vaultPublicPath]) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! UFix64? +} + +/* --- Transaction Helpers --- */ + +access(all) +fun createAndStorePool(signer: Test.TestAccount, defaultTokenIdentifier: String, beFailed: Bool) { + let createRes = _executeTransaction( + "../transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc", + [defaultTokenIdentifier], + signer + ) + Test.expect(createRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, price: UFix64) { + let setRes = _executeTransaction( + "./transactions/mock-oracle/set_price.cdc", + [forTokenIdentifier, price], + signer + ) + Test.expect(setRes, Test.beSucceeded()) +} + +access(all) +fun addSupportedTokenSimpleInterestCurve( + signer: Test.TestAccount, + tokenTypeIdentifier: String, + collateralFactor: UFix64, + borrowFactor: UFix64, + depositRate: UFix64, + depositCapacityCap: UFix64 +) { + let additionRes = _executeTransaction( + "../transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc", + [ tokenTypeIdentifier, collateralFactor, borrowFactor, depositRate, depositCapacityCap ], + signer + ) + Test.expect(additionRes, Test.beSucceeded()) +} + +access(all) +fun rebalancePosition(signer: Test.TestAccount, pid: UInt64, force: Bool, beFailed: Bool) { + let rebalanceRes = _executeTransaction( + "../transactions/tidal-protocol/pool-management/rebalance_position.cdc", + [ pid, force ], + signer + ) + Test.expect(rebalanceRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun setupMoetVault(_ signer: Test.TestAccount, beFailed: Bool) { + let setupRes = _executeTransaction("../transactions/moet/setup_vault.cdc", [], signer) + Test.expect(setupRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun mintMoet(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bool) { + let mintRes = _executeTransaction("../transactions/moet/mint_moet.cdc", [to, amount], signer) + Test.expect(mintRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} \ No newline at end of file diff --git a/flow.json b/flow.json index 5e1e8c1f..e23780b3 100644 --- a/flow.json +++ b/flow.json @@ -3,7 +3,7 @@ "TidalProtocol": { "source": "cadence/contracts/internal-dependencies/TidalProtocol.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000008" } }, "DFB": { @@ -27,19 +27,19 @@ "MockOracle": { "source": "cadence/contracts/mocks/MockOracle.cdc", "aliases": { - "testing": "0000000000000008" + "testing": "0000000000000009" } }, "MockStrategy": { "source": "cadence/contracts/mocks/MockStrategy.cdc", "aliases": { - "testing": "0000000000000008" + "testing": "0000000000000009" } }, "MockSwapper": { "source": "cadence/contracts/mocks/MockSwapper.cdc", "aliases": { - "testing": "0000000000000008" + "testing": "0000000000000009" } }, "SwapStack": { @@ -51,31 +51,31 @@ "TidalYield": { "source": "cadence/contracts/TidalYield.cdc", "aliases": { - "testing": "0000000000000008" + "testing": "0000000000000009" } }, "TidalYieldAutoBalancers": { "source": "cadence/contracts/TidalYieldAutoBalancers.cdc", "aliases": { - "testing": "0000000000000008" + "testing": "0000000000000009" } }, "TidalYieldStrategies": { "source": "cadence/contracts/TidalYieldStrategies.cdc", "aliases": { - "testing": "0000000000000008" + "testing": "0000000000000009" } }, "MOET": { "source": "cadence/contracts/internal-dependencies/tokens/MOET.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000008" } }, "YieldToken": { "source": "cadence/contracts/internal-dependencies/tokens/YieldToken.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "00000000000000010" } } }, @@ -184,6 +184,7 @@ } ] }, + "TidalProtocol", { "name": "YieldToken", "args": [ @@ -204,8 +205,8 @@ }, "MockSwapper", "TidalYieldAutoBalancers", - "TidalYieldStrategies", - "TidalYield" + "TidalYield", + "TidalYieldStrategies" ] } } From 7aea0ade9ef36172a476cb97ebae35c40dc667cb Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:07:53 -0600 Subject: [PATCH 50/80] add Cadence tests to github ci action --- .github/workflows/cadence_tests.yml | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/cadence_tests.yml diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml new file mode 100644 index 00000000..0f3cd8fa --- /dev/null +++ b/.github/workflows/cadence_tests.yml @@ -0,0 +1,33 @@ +name: CI + +on: pull_request + +jobs: + tests: + name: Flow CLI Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + submodules: "true" + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.23.x" + - uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Install Flow CLI + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + - name: Flow CLI Version + run: flow version + - name: Update PATH + run: echo "/root/.local/bin" >> $GITHUB_PATH + - name: Install dependencies + run: flow deps install --skip-alias --skip-deployments + - name: Run tests + run: flow test --cover --covercode="contracts" --coverprofile="coverage.lcov" ./cadence/tests/*_test.cdc From dd29732a572bc27b82170ba02239b502c76f9fa1 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:09:06 -0600 Subject: [PATCH 51/80] rename usda/ to moet/ --- cadence/transactions/{usda => moet}/mint.cdc | 0 cadence/transactions/{usda => moet}/setup.cdc | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename cadence/transactions/{usda => moet}/mint.cdc (100%) rename cadence/transactions/{usda => moet}/setup.cdc (100%) diff --git a/cadence/transactions/usda/mint.cdc b/cadence/transactions/moet/mint.cdc similarity index 100% rename from cadence/transactions/usda/mint.cdc rename to cadence/transactions/moet/mint.cdc diff --git a/cadence/transactions/usda/setup.cdc b/cadence/transactions/moet/setup.cdc similarity index 100% rename from cadence/transactions/usda/setup.cdc rename to cadence/transactions/moet/setup.cdc From 1e75a4d6d7c638d2849364a5b086973baff3aecf Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:30:17 -0600 Subject: [PATCH 52/80] update setup_emulator.sh script --- local/setup_emulator.sh | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/local/setup_emulator.sh b/local/setup_emulator.sh index 3efe60c4..c8f6bb3b 100644 --- a/local/setup_emulator.sh +++ b/local/setup_emulator.sh @@ -1,7 +1,32 @@ +# install DeFiBlocks submodule as dependency git submodule update --init --recursive +# execute emulator deployment flow deploy + +# set mocked prices in the MockOracle contract, initialized with MOET as unitOfAccount flow transactions send ./cadence/transactions/mocks/oracle/set_price.cdc 'A.0ae53cb6e3f42a79.FlowToken.Vault' 0.5 flow transactions send ./cadence/transactions/mocks/oracle/set_price.cdc 'A.f8d6e0586b0a20c7.YieldToken.Vault' 1.0 + +# configure TidalProtocol +# +# create Pool with MOET as default token +flow transactions send ./cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc 'A.f8d6e0586b0a20c7.MOET.Vault' +# add FLOW as supported token - params: collateralFactor, borrowFactor, depositRate, depositCapacityCap +flow transactions send ./cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc \ + 'A.0ae53cb6e3f42a79.FlowToken.Vault' \ + 0.8 \ + 1.0 \ + 1_000_000.0 \ + 1_000_000.0 + +# configure TidalYield +# +# wire up liquidity to MockSwapper, mocking AMM liquidity sources flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc /storage/flowTokenVault -flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc /storage/usdaTokenVault_0xf8d6e0586b0a20c7 +flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc /storage/moetTokenVault_0xf8d6e0586b0a20c7 flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc /storage/yieldTokenVault_0xf8d6e0586b0a20c7 +# add TracerStrategy as supported Strategy with the ability to initialize when new Tides are created +flow transactions send ./cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc \ + 'A.f8d6e0586b0a20c7.TidalYieldStrategies.TracerStrategy' \ + 'A.f8d6e0586b0a20c7.TidalYieldStrategies.TracerStrategyComposer' \ + /storage/TidalYieldStrategyComposerIssuer_0xf8d6e0586b0a20c7 From 27a4c0aefc42dafc9ef94fe4c241cca1945004c7 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:59:13 -0600 Subject: [PATCH 53/80] remove Owner entitlement from TidalYield contract --- cadence/contracts/TidalYield.cdc | 3 --- 1 file changed, 3 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 3ac4ad56..86307c46 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -280,9 +280,6 @@ access(all) contract TidalYield { } } - /// Entitlement enabling access on owner-related privileged operations on the TideManager resource - access(all) entitlement Owner - /// TideManager /// /// A TideManager encapsulates nested Tide resources. Through a TideManager, one can create, manage, and close From a585d98c709fce8b51073316b24e1ea9fec1dc97 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:59:37 -0600 Subject: [PATCH 54/80] fix MockStrategy getters --- cadence/contracts/mocks/MockStrategy.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 05f14bc2..9aae3861 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -93,10 +93,10 @@ access(all) contract MockStrategy { return { Type<@Strategy>(): true } } access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { - return {} + return { Type<@FlowToken.Vault>(): true } } access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { - return {} + return { Type<@FlowToken.Vault>(): true } } access(all) fun createStrategy( _ type: Type, From e733ffd1b24771caa890831c43b5a78c9ea18818 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:00:03 -0600 Subject: [PATCH 55/80] add initial Tide management tests --- .../tidal-yield/get_supported_strategies.cdc | 7 ++ cadence/tests/test_helpers.cdc | 37 +++++++++ cadence/tests/tide_management_test.cdc | 77 +++++++++++++++++++ .../admin/add_strategy_composer.cdc | 36 +++++++++ .../transactions/tidal-yield/close_tide.cdc | 4 +- 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 cadence/scripts/tidal-yield/get_supported_strategies.cdc create mode 100644 cadence/tests/tide_management_test.cdc create mode 100644 cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc diff --git a/cadence/scripts/tidal-yield/get_supported_strategies.cdc b/cadence/scripts/tidal-yield/get_supported_strategies.cdc new file mode 100644 index 00000000..fab9cfc0 --- /dev/null +++ b/cadence/scripts/tidal-yield/get_supported_strategies.cdc @@ -0,0 +1,7 @@ +import "TidalYield" + +/// Returns the Strategy Types currently supported by TidalYield +/// +access(all) fun main(): [Type] { + return TidalYield.getSupportedStrategies() +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index d2746bf3..c8720cd0 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -118,6 +118,13 @@ fun getBalance(address: Address, vaultPublicPath: PublicPath): UFix64? { return res.returnValue as! UFix64? } +access(all) +fun getTideIDs(address: Address): [UInt64]? { + let res = _executeScript("../scripts/tidal-yield/get_tide_ids.cdc", [address]) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! [UInt64]? +} + /* --- Transaction Helpers --- */ access(all) @@ -177,4 +184,34 @@ access(all) fun mintMoet(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bool) { let mintRes = _executeTransaction("../transactions/moet/mint_moet.cdc", [to, amount], signer) Test.expect(mintRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun addStrategyComposer(signer: Test.TestAccount, strategyIdentifier: String, composerIdentifier: String, issuerStoragePath: StoragePath, beFailed: Bool) { + let addRes = _executeTransaction("../transactions/tidal-yield/admin/add_strategy_composer.cdc", + [ strategyIdentifier, composerIdentifier, issuerStoragePath ], + signer + ) + Test.expect(addRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun openTide( + signer: Test.TestAccount, + strategyIdentifier: String, + vaultIdentifier: String, + amount: UFix64, + beFailed: Bool +) { + let res = _executeTransaction("../transactions/tidal-yield/open_tide.cdc", + [ strategyIdentifier, vaultIdentifier, amount ], + signer + ) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun closeTide(signer: Test.TestAccount, id: UInt64, beFailed: Bool) { + let res = _executeTransaction("../transactions/tidal-yield/close_tide.cdc", [id], signer) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) } \ No newline at end of file diff --git a/cadence/tests/tide_management_test.cdc b/cadence/tests/tide_management_test.cdc new file mode 100644 index 00000000..819b8fa4 --- /dev/null +++ b/cadence/tests/tide_management_test.cdc @@ -0,0 +1,77 @@ +import Test +import BlockchainHelpers + +import "test_helpers.cdc" + +import "FlowToken" +import "MockStrategy" + +access(all) let tidalYieldAccount = Test.getAccount(0x0000000000000009) + +access(all) var strategyIdentifier = Type<@MockStrategy.Strategy>().identifier +access(all) var flowTokenIdentifier = Type<@FlowToken.Vault>().identifier + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun setup() { + deployContracts() + + // enable mocked Strategy creation + addStrategyComposer(signer: tidalYieldAccount, + strategyIdentifier: strategyIdentifier, + composerIdentifier: Type<@MockStrategy.StrategyComposer>().identifier, + issuerStoragePath: MockStrategy.IssuerStoragePath, + beFailed: false + ) + + snapshot = getCurrentBlockHeight() +} + +access(all) +fun test_CreateTideSucceeds() { + let fundingAmount = 100.0 + + let user = Test.createAccount() + mintFlow(to: user, amount: fundingAmount) + + openTide( + signer: user, + strategyIdentifier: strategyIdentifier, + vaultIdentifier: flowTokenIdentifier, + amount: fundingAmount, + beFailed: false + ) + + let tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(1, tideIDs!.length) +} + +access(all) +fun test_CloseTideSucceeds() { + Test.reset(to: snapshot) + + let fundingAmount = 100.0 + + let user = Test.createAccount() + mintFlow(to: user, amount: fundingAmount) + + openTide( + signer: user, + strategyIdentifier: strategyIdentifier, + vaultIdentifier: flowTokenIdentifier, + amount: fundingAmount, + beFailed: false + ) + + var tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(1, tideIDs!.length) + + closeTide(signer: user, id: tideIDs![0], beFailed: false) + + tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(0, tideIDs!.length) +} \ No newline at end of file diff --git a/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc new file mode 100644 index 00000000..609c2936 --- /dev/null +++ b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc @@ -0,0 +1,36 @@ +import "TidalYield" + +/// Adds the provided Strategy type to the TidalYield StrategyFactory as built by the given StrategyComposer type +/// +/// @param strategyIdentifier: The Type identifier of the Strategy to add to the StrategyFactory +/// @param composerIdentifier: The Type identifier of the StrategyComposer that builds the Strategy Type +/// +transaction(strategyIdentifier: String, composerIdentifier: String, issuerStoragePath: StoragePath) { + + /// The Strategy Type to add to the StrategyFactory + let strategyType: Type + /// The StrategyComposer that builds the Strategy Type + let composer: @{TidalYield.StrategyComposer} + /// Authorized reference to the StrategyFactory to which the Strategy Type & StrategyComposer will be added + let factory: auth(Mutate) &TidalYield.StrategyFactory + + prepare(signer: auth(BorrowValue) &Account) { + // construct the types + self.strategyType = CompositeType(strategyIdentifier) ?? panic("Invalid Strategy type \(strategyIdentifier)") + let composerType = CompositeType(composerIdentifier) ?? panic("Invalid StrategyComposer type \(composerIdentifier)") + + // borrow reference to StrategyComposerIssuer & create the StategyComposer + let issuer = signer.storage.borrow<&{TidalYield.StrategyComposerIssuer}>(from: issuerStoragePath) + ?? panic("Could not borrow reference to StrategyComposerIssuer from \(issuerStoragePath)") + self.composer <- issuer.issueComposer(composerType) + + // assign StrategyFactory + self.factory = signer.storage.borrow(from: TidalYield.FactoryStoragePath) + ?? panic("Could not borrow reference to StrategyFactory from \(TidalYield.FactoryStoragePath)") + } + + execute { + // add the Strategy Type as built by the new StrategyComposer + self.factory.addStrategyComposer(self.strategyType, composer: <-self.composer) + } +} diff --git a/cadence/transactions/tidal-yield/close_tide.cdc b/cadence/transactions/tidal-yield/close_tide.cdc index d51035f6..13cf8d3c 100644 --- a/cadence/transactions/tidal-yield/close_tide.cdc +++ b/cadence/transactions/tidal-yield/close_tide.cdc @@ -10,12 +10,12 @@ import "TidalYield" /// @param id: The Tide.id() of the Tide from which the full balance will be withdrawn /// transaction(id: UInt64) { - let manager: auth(TidalYield.Owner) &TidalYield.TideManager + let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) + self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") From 374d8c79f51ba920dd58699ab4eb3b49ae45205a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:58:06 -0600 Subject: [PATCH 56/80] fix createTide route caused by TracerStrategy composition errors --- cadence/contracts/TidalYield.cdc | 5 ++-- cadence/contracts/TidalYieldAutoBalancers.cdc | 2 +- cadence/contracts/TidalYieldStrategies.cdc | 23 +++++++------- .../{open_tide.cdc => create_tide.cdc} | 2 +- flow.json | 30 +++++++++---------- 5 files changed, 30 insertions(+), 32 deletions(-) rename cadence/transactions/tidal-yield/{open_tide.cdc => create_tide.cdc} (97%) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 86307c46..659d7f43 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -94,8 +94,7 @@ access(all) contract TidalYield { access(all) fun createStrategy( _ type: Type, uniqueID: DFB.UniqueIdentifier, - withFunds: @{FungibleToken.Vault}, - params: {String: AnyStruct} + withFunds: @{FungibleToken.Vault} ): @{Strategy} { pre { self.getSupportedInitializationVaults(forStrategy: type)[withFunds.getType()] == true: @@ -144,7 +143,7 @@ access(all) contract TidalYield { "Invalid Strategy returned - expected \(type.identifier) but returned \(result.getType().identifier)" } return <- self._borrowComposer(forStrategy: type) - .createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds, params: {}) // TODO: decide on params inclusion or not + .createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds) } /// Sets the provided Strategy and Composer association in the StrategyFactory access(Mutate) fun addStrategyComposer(_ strategy: Type, composer: @{StrategyComposer}) { diff --git a/cadence/contracts/TidalYieldAutoBalancers.cdc b/cadence/contracts/TidalYieldAutoBalancers.cdc index ff30bb0d..1bf813b3 100644 --- a/cadence/contracts/TidalYieldAutoBalancers.cdc +++ b/cadence/contracts/TidalYieldAutoBalancers.cdc @@ -73,7 +73,7 @@ access(all) contract TidalYieldAutoBalancers { publishedCap = self.account.capabilities.exists(publicPath) assert(storedType == Type<@DFB.AutoBalancer>(), message: "Error when configuring AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(storagePath)") - assert(!publishedCap, + assert(publishedCap, message: "Error when publishing AutoBalancer Capability for UniqueIdentifier.id \(uniqueID.id) at path \(publicPath)") return autoBalancerRef } diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index cc9d66ce..c5480736 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -104,13 +104,12 @@ access(all) contract TidalYieldStrategies { access(all) fun createStrategy( _ type: Type, uniqueID: DFB.UniqueIdentifier, - withFunds: @{FungibleToken.Vault}, - params: {String: AnyStruct} + withFunds: @{FungibleToken.Vault} ): @{TidalYield.Strategy} { // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy let oracle = MockOracle.PriceOracle() - // token types + // assign token types let collateralType = withFunds.getType() let yieldTokenType = Type<@YieldToken.Vault>() let moetTokenType = Type<@MOET.Vault>() @@ -118,13 +117,13 @@ access(all) contract TidalYieldStrategies { // configure and AutoBalancer for this stack let autoBalancer = TidalYieldAutoBalancers._initNewAutoBalancer( - oracle: oracle, - vaultType: yieldTokenType, - lowerThreshold: params["lowerThreshold"] as! UFix64? ?? panic("Malformed params missing \"lowerThreshold\""), - upperThreshold: params["upperThreshold"] as! UFix64? ?? panic("Malformed params missing \"upperThreshold\""), - rebalanceSink: nil, - rebalanceSource: nil, - uniqueID: uniqueID + oracle: oracle, // used to determine value of deposits & when to rebalance + vaultType: yieldTokenType, // the type of Vault held by the AutoBalancer + lowerThreshold: 0.95, // set AutoBalancer to pull from rebalanceSource when balance is 5% below value of deposits + upperThreshold: 1.05, // set AutoBalancer to push to rebalanceSink when balance is 5% below value of deposits + rebalanceSink: nil, // nil on init - will be set once a PositionSink is available + rebalanceSource: nil, // nil on init - not set for TracerStrategy + uniqueID: uniqueID // identifies AutoBalancer as part of this Strategy ) // enables deposits of YieldToken to the AutoBalancer let abaSink = autoBalancer.createBalancerSink() ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") @@ -151,7 +150,7 @@ access(all) contract TidalYieldStrategies { // Swaps provided MOET to YieldToken & deposits to the AutoBalancer let abaSwapSink = SwapStack.SwapSink(swapper: moetToYieldSwapper, sink: abaSink, uniqueID: uniqueID) // Swaps YieldToken & provides swapped MOET, sourcing YieldToken from the AutoBalancer - let abaSwapSource = SwapStack.SwapSource(swapper: moetToYieldSwapper, source: abaSource, uniqueID: uniqueID) + let abaSwapSource = SwapStack.SwapSource(swapper: yieldToMoetSwapper, source: abaSource, uniqueID: uniqueID) // open a TidalProtocol position let position = TidalProtocol.openPosition( @@ -167,7 +166,7 @@ access(all) contract TidalYieldStrategies { // init YieldToken -> FLOW Swapper let yieldToFlowSwapper = MockSwapper.Swapper( inVault: yieldTokenType, - outVault: flowTokenType, + outVault: flowTokenType, // TODO: before uniqueID: uniqueID ) // allows for YieldToken to be deposited to the Position diff --git a/cadence/transactions/tidal-yield/open_tide.cdc b/cadence/transactions/tidal-yield/create_tide.cdc similarity index 97% rename from cadence/transactions/tidal-yield/open_tide.cdc rename to cadence/transactions/tidal-yield/create_tide.cdc index 13f32959..4b378b3a 100644 --- a/cadence/transactions/tidal-yield/open_tide.cdc +++ b/cadence/transactions/tidal-yield/create_tide.cdc @@ -42,7 +42,7 @@ transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue( + signer.capabilities.storage.issue<&TidalYield.TideManager>( TidalYield.TideManagerStoragePath ) } diff --git a/flow.json b/flow.json index e23780b3..5084153a 100644 --- a/flow.json +++ b/flow.json @@ -1,11 +1,5 @@ { "contracts": { - "TidalProtocol": { - "source": "cadence/contracts/internal-dependencies/TidalProtocol.cdc", - "aliases": { - "testing": "0000000000000008" - } - }, "DFB": { "source": "./DeFiBlocks/cadence/contracts/interfaces/DFB.cdc", "aliases": { @@ -24,6 +18,12 @@ "testing": "0000000000000007" } }, + "MOET": { + "source": "cadence/contracts/internal-dependencies/tokens/MOET.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "MockOracle": { "source": "cadence/contracts/mocks/MockOracle.cdc", "aliases": { @@ -48,6 +48,12 @@ "testing": "0000000000000007" } }, + "TidalProtocol": { + "source": "cadence/contracts/internal-dependencies/TidalProtocol.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "TidalYield": { "source": "cadence/contracts/TidalYield.cdc", "aliases": { @@ -66,16 +72,10 @@ "testing": "0000000000000009" } }, - "MOET": { - "source": "cadence/contracts/internal-dependencies/tokens/MOET.cdc", - "aliases": { - "testing": "0000000000000008" - } - }, "YieldToken": { "source": "cadence/contracts/internal-dependencies/tokens/YieldToken.cdc", "aliases": { - "testing": "00000000000000010" + "testing": "0000000000000010" } } }, @@ -179,7 +179,7 @@ "name": "MOET", "args": [ { - "value": "1000000.0", + "value": "1000000.00000000", "type": "UFix64" } ] @@ -189,7 +189,7 @@ "name": "YieldToken", "args": [ { - "value": "1000000.0", + "value": "1000000.00000000", "type": "UFix64" } ] From fb72fb5d2c04b5852e2f051e6538d785e3721774 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:02:47 -0600 Subject: [PATCH 57/80] remove deprecated TidalYield.Owner entitlement usage --- cadence/transactions/tidal-yield/create_tide.cdc | 2 ++ cadence/transactions/tidal-yield/setup.cdc | 2 +- cadence/transactions/tidal-yield/withdraw_from_tide.cdc | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cadence/transactions/tidal-yield/create_tide.cdc b/cadence/transactions/tidal-yield/create_tide.cdc index 4b378b3a..104d60bd 100644 --- a/cadence/transactions/tidal-yield/create_tide.cdc +++ b/cadence/transactions/tidal-yield/create_tide.cdc @@ -6,6 +6,8 @@ import "TidalYield" /// Opens a new Tide in the Tidal platform, funding the Tide with the specified Vault and amount /// +/// @param strategyIdentifier: The Strategy's Type identifier. Must be a Strategy Type that is currently supported by +/// TidalYield. See `TidalYield.getSupportedStrategies()` to get those currently supported. /// @param vaultIdentifier: The Vault's Type identifier /// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' /// @param amount: The amount to deposit into the new Tide diff --git a/cadence/transactions/tidal-yield/setup.cdc b/cadence/transactions/tidal-yield/setup.cdc index 1238c587..0cd13ee2 100644 --- a/cadence/transactions/tidal-yield/setup.cdc +++ b/cadence/transactions/tidal-yield/setup.cdc @@ -17,7 +17,7 @@ transaction { let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue(TidalYield.TideManagerStoragePath) + signer.capabilities.storage.issue(TidalYield.TideManagerStoragePath) // confirm setup of TideManager at canonical path let storedType = signer.storage.type(at: TidalYield.TideManagerStoragePath) ?? Type() diff --git a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc index 3413173d..08cbf13f 100644 --- a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc +++ b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc @@ -11,12 +11,12 @@ import "TidalYield" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: auth(TidalYield.Owner) &TidalYield.TideManager + let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) + self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") From e1471cf1b2a23d664d0c69e3346c0bf1a1afaf4a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:12:54 -0600 Subject: [PATCH 58/80] update contract comments --- cadence/contracts/TidalYield.cdc | 8 ++++--- cadence/contracts/TidalYieldAutoBalancers.cdc | 22 ++++++++++++++----- cadence/contracts/TidalYieldStrategies.cdc | 1 + 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/TidalYield.cdc index 659d7f43..4e04e900 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -1,10 +1,10 @@ +// standards import "FungibleToken" import "Burner" import "ViewResolver" - +// DeFiBlocks import "DFB" -/// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// @@ -169,6 +169,8 @@ access(all) contract TidalYield { } } + /// StrategyComposerIssuer + /// /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Contracts defining Strategies that do not require /// such protections may wish to expose Strategy creation publicly via public Capabilities. @@ -272,7 +274,7 @@ access(all) contract TidalYield { return <- res } - /// Returns + /// Returns an authorized reference to the encapsulated Strategy access(self) view fun _borrowStrategy(): auth(FungibleToken.Withdraw) &{Strategy} { return &self.strategy as auth(FungibleToken.Withdraw) &{Strategy}? ?? panic("Unknown error - could not borrow Strategy for Tide #\(self.id())") diff --git a/cadence/contracts/TidalYieldAutoBalancers.cdc b/cadence/contracts/TidalYieldAutoBalancers.cdc index 1bf813b3..e06dc22d 100644 --- a/cadence/contracts/TidalYieldAutoBalancers.cdc +++ b/cadence/contracts/TidalYieldAutoBalancers.cdc @@ -1,17 +1,29 @@ +// standards import "Burner" import "FungibleToken" - +// DeFiBlocks import "DFB" +/// TidalYieldAutoBalancers +/// +/// This contract deals with the storage, retrieval and cleanup of DeFiBlocks AutoBalancers as they are used in +/// TidalYield defined Strategies. +/// +/// AutoBalancers are stored in contract account storage at paths derived by their related DFB.UniqueIdentifier.id +/// which identifies all DeFiBlocks components in the stack related to their composite Strategy. +/// +/// When a Tide and necessarily the related Strategy is closed & burned, the related AutoBalancer and its Capabilities +/// are destroyed and deleted +/// access(all) contract TidalYieldAutoBalancers { /// The path prefix used for StoragePath & PublicPath derivations access(all) let pathPrefix: String - + /* --- PUBLIC METHODS --- */ - /// Returns the path (StoragePath or PublicPath) at which an AutoBalancer is stored with the associated - /// UniqueIdentifier.id. + /// Returns the path (StoragePath or PublicPath) at which an AutoBalancer is stored with the associated + /// UniqueIdentifier.id. access(all) view fun deriveAutoBalancerPath(id: UInt64, storage: Bool): Path { return storage ? StoragePath(identifier: "\(self.pathPrefix)\(id)")! : PublicPath(identifier: "\(self.pathPrefix)\(id)")! } @@ -36,7 +48,7 @@ access(all) contract TidalYieldAutoBalancers { rebalanceSource: {DFB.Source}?, uniqueID: DFB.UniqueIdentifier ): auth(DFB.Auto, DFB.Set, DFB.Get, FungibleToken.Withdraw) &DFB.AutoBalancer { - + // derive paths & prevent collision let storagePath = self.deriveAutoBalancerPath(id: uniqueID.id, storage: true) as! StoragePath let publicPath = self.deriveAutoBalancerPath(id: uniqueID.id, storage: false) as! PublicPath diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index c5480736..02145b2a 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -33,6 +33,7 @@ import "MockSwapper" /// access(all) contract TidalYieldStrategies { + /// Canonical StoragePath where the StrategyComposerIssuer should be stored access(all) let IssuerStoragePath: StoragePath /// This is the first Strategy implementation, wrapping a TidalProtocol Position along with its related Sink & From 235143b68d612ac032506e23fdc657395fc46330 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:03:42 -0600 Subject: [PATCH 59/80] update DeFiBlocks to latest --- DeFiBlocks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DeFiBlocks b/DeFiBlocks index 71465264..a2dcbd50 160000 --- a/DeFiBlocks +++ b/DeFiBlocks @@ -1 +1 @@ -Subproject commit 7146526409c18321dc1e0244862b2a47f9e53abc +Subproject commit a2dcbd506152a31fe713617f8ab684315d4cbf44 From 893293987075ff0a08efe478cdd5eaa83ad1903e Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:04:54 -0600 Subject: [PATCH 60/80] update get_balance script --- cadence/scripts/tokens/get_balance.cdc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cadence/scripts/tokens/get_balance.cdc b/cadence/scripts/tokens/get_balance.cdc index 331f2614..9fecdaea 100644 --- a/cadence/scripts/tokens/get_balance.cdc +++ b/cadence/scripts/tokens/get_balance.cdc @@ -3,13 +3,12 @@ import "FungibleToken" /// Returns the balance of the stored Vault at the given address if exists, otherwise nil /// /// @param address: The address of the account that owns the vault -/// @param vaultPathIdentifier: The identifier of the vault's storage path +/// @param vaultStoragePath: The StoragePath where the Vault can be found /// -/// @returns The balance of the stored Vault at the given address +/// @returns The balance of the stored Vault at the given address or `nil` if the Vault is not found /// -access(all) fun main(address: Address, vaultPathIdentifier: String): UFix64? { - let path = StoragePath(identifier: vaultPathIdentifier) ?? panic("Malformed StoragePath identifier") +access(all) fun main(address: Address, vaultStoragePath: StoragePath): UFix64? { return getAuthAccount(address).storage.borrow<&{FungibleToken.Vault}>( - from: path + from: vaultStoragePath )?.balance ?? nil } From 0bea1a398668b34729681b8803ef7e76ff1bf1e5 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:06:24 -0600 Subject: [PATCH 61/80] fix MockStrategy interface conformance --- cadence/contracts/mocks/MockStrategy.cdc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 9aae3861..5a5cc008 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -101,8 +101,7 @@ access(all) contract MockStrategy { access(all) fun createStrategy( _ type: Type, uniqueID: DFB.UniqueIdentifier, - withFunds: @{FungibleToken.Vault}, - params: {String: AnyStruct} + withFunds: @{FungibleToken.Vault} ): @{TidalYield.Strategy} { let id = DFB.UniqueIdentifier() let strat <- create Strategy( From bf62a2a46dff9693689b613bdde250b25f43e638 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:58:49 -0600 Subject: [PATCH 62/80] rename token-related transactions --- cadence/transactions/moet/mint.cdc | 30 ------------------- cadence/transactions/moet/mint_moet.cdc | 29 ++++++++++++++++++ cadence/transactions/moet/setup.cdc | 26 ---------------- cadence/transactions/moet/setup_vault.cdc | 29 ++++++++++++++++++ .../yield-token/{mint.cdc => mint_yield.cdc} | 0 .../{setup.cdc => setup_vault.cdc} | 5 ++-- 6 files changed, 60 insertions(+), 59 deletions(-) delete mode 100644 cadence/transactions/moet/mint.cdc create mode 100644 cadence/transactions/moet/mint_moet.cdc delete mode 100644 cadence/transactions/moet/setup.cdc create mode 100644 cadence/transactions/moet/setup_vault.cdc rename cadence/transactions/yield-token/{mint.cdc => mint_yield.cdc} (100%) rename cadence/transactions/yield-token/{setup.cdc => setup_vault.cdc} (84%) diff --git a/cadence/transactions/moet/mint.cdc b/cadence/transactions/moet/mint.cdc deleted file mode 100644 index 2fb29eb8..00000000 --- a/cadence/transactions/moet/mint.cdc +++ /dev/null @@ -1,30 +0,0 @@ -import "FungibleToken" - -import "USDA" - -/// MOCK TRANSACTION - DO NOT USE IN PRODUCTION -/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -/// -/// Mints the given amount of USDA tokens to the named recipient, reverting if they do not have a Vault configured -/// -/// @param recipient: The Flow address to receive the minted tokens -/// @param amount: The amount of USDA tokens to mint -/// -transaction(recipient: Address, amount: UFix64) { - - let minter: &USDA.Minter - let receiver: &{FungibleToken.Receiver} - - prepare(signer: auth(BorrowValue) &Account) { - self.minter = signer.storage.borrow<&USDA.Minter>(from: USDA.AdminStoragePath) - ?? panic("Could not find USDA Minter at \(USDA.AdminStoragePath)") - self.receiver = getAccount(recipient).capabilities.borrow<&{FungibleToken.Receiver}>(USDA.ReceiverPublicPath) - ?? panic("Could not find FungibleToken Receiver in \(recipient) at path \(USDA.ReceiverPublicPath)") - } - - execute { - self.receiver.deposit( - from: <-self.minter.mintTokens(amount: amount) - ) - } -} diff --git a/cadence/transactions/moet/mint_moet.cdc b/cadence/transactions/moet/mint_moet.cdc new file mode 100644 index 00000000..1a0ddd43 --- /dev/null +++ b/cadence/transactions/moet/mint_moet.cdc @@ -0,0 +1,29 @@ +import "FungibleToken" + +import "MOET" + +/// Mints MOET using the Minter stored in the signer's account and deposits to the recipients MOET Vault. If the +/// recipient's MOET Vault is not configured with a public Capability or the signer does not have a MOET Minter +/// stored, the transaction will revert. +/// +/// @param to: The recipient's Flow address +/// @param amount: How many MOET tokens to mint to the recipient's account +/// +transaction(to: Address, amount: UFix64) { + + let receiver: &{FungibleToken.Vault} + let minter: &MOET.Minter + + prepare(signer: auth(BorrowValue) &Account) { + self.minter = signer.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath) + ?? panic("Could not borrow reference to MOET Minter from signer's account at path \(MOET.AdminStoragePath)") + self.receiver = getAccount(to).capabilities.borrow<&{FungibleToken.Vault}>(MOET.VaultPublicPath) + ?? panic("Could not borrow reference to MOET Vault from recipient's account at path \(MOET.VaultPublicPath)") + } + + execute { + self.receiver.deposit( + from: <-self.minter.mintTokens(amount: amount) + ) + } +} diff --git a/cadence/transactions/moet/setup.cdc b/cadence/transactions/moet/setup.cdc deleted file mode 100644 index 7dbe9c1b..00000000 --- a/cadence/transactions/moet/setup.cdc +++ /dev/null @@ -1,26 +0,0 @@ -import "FungibleToken" -import "ViewResolver" - -import "USDA" - -/// Configures a USDA Vault if one is not found, ensuring proper configuration in storage -/// -transaction { - prepare(signer: auth(SaveValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { - // configure if nothing is found at canonical path - if signer.storage.type(at: USDA.VaultStoragePath) == nil { - // save the new vault - signer.storage.save(<-USDA.createEmptyVault(vaultType: Type<@USDA.Vault>()), to: USDA.VaultStoragePath) - // publish a public capability on the Vault - let cap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(USDA.VaultStoragePath) - signer.capabilities.unpublish(USDA.ReceiverPublicPath) - signer.capabilities.publish(cap, at: USDA.ReceiverPublicPath) - // issue an authorized capability to initialize a CapabilityController on the account, but do not publish - signer.capabilities.storage.issue(USDA.VaultStoragePath) - } - // ensure proper configuration - if signer.storage.type(at: USDA.VaultStoragePath) != Type<@USDA.Vault>(){ - panic("Could not configure USDA Vault at \(USDA.VaultStoragePath) - check for collision and try again") - } - } -} diff --git a/cadence/transactions/moet/setup_vault.cdc b/cadence/transactions/moet/setup_vault.cdc new file mode 100644 index 00000000..3dfa7810 --- /dev/null +++ b/cadence/transactions/moet/setup_vault.cdc @@ -0,0 +1,29 @@ +import "FungibleToken" + +import "MOET" + +/// Creates & stores a MOET Vault in the signer's account, also configuring its public Vault Capability +/// +transaction { + + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // configure if nothing is found at canonical path + if signer.storage.type(at: MOET.VaultStoragePath) == nil { + // save the new vault + signer.storage.save(<-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()), to: MOET.VaultStoragePath) + // publish a public capability on the Vault + let cap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(MOET.VaultStoragePath) + signer.capabilities.unpublish(MOET.VaultPublicPath) + signer.capabilities.unpublish(MOET.ReceiverPublicPath) + signer.capabilities.publish(cap, at: MOET.VaultPublicPath) + signer.capabilities.publish(cap, at: MOET.ReceiverPublicPath) + // issue an authorized capability to initialize a CapabilityController on the account, but do not publish + signer.capabilities.storage.issue(MOET.VaultStoragePath) + } + + // ensure proper configuration + if signer.storage.type(at: MOET.VaultStoragePath) != Type<@MOET.Vault>(){ + panic("Could not configure MOET Vault at \(MOET.VaultStoragePath) - check for collision and try again") + } + } +} diff --git a/cadence/transactions/yield-token/mint.cdc b/cadence/transactions/yield-token/mint_yield.cdc similarity index 100% rename from cadence/transactions/yield-token/mint.cdc rename to cadence/transactions/yield-token/mint_yield.cdc diff --git a/cadence/transactions/yield-token/setup.cdc b/cadence/transactions/yield-token/setup_vault.cdc similarity index 84% rename from cadence/transactions/yield-token/setup.cdc rename to cadence/transactions/yield-token/setup_vault.cdc index 5cf8a679..f2f6941e 100644 --- a/cadence/transactions/yield-token/setup.cdc +++ b/cadence/transactions/yield-token/setup_vault.cdc @@ -19,8 +19,7 @@ transaction { signer.capabilities.storage.issue(YieldToken.VaultStoragePath) } // ensure proper configuration - if signer.storage.type(at: YieldToken.VaultStoragePath) != Type<@YieldToken.Vault>(){ - panic("Could not configure YieldToken Vault at \(YieldToken.VaultStoragePath) - check for collision and try again") - } + assert(signer.storage.type(at: YieldToken.VaultStoragePath) == Type<@YieldToken.Vault>(), + message: "Could not configure YieldToken Vault at \(YieldToken.VaultStoragePath) - check for collision and try again") } } From 3f47e72604f2db8095dfc6616efa9052266007c3 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:59:20 -0600 Subject: [PATCH 63/80] add behavioral tests covering TracerStrategy --- .../get_auto_balancer_balance_by_id.cdc | 6 + .../scripts/tidal-yield/get_tide_balance.cdc | 18 +++ cadence/tests/test_helpers.cdc | 62 ++++++++-- cadence/tests/tide_management_test.cdc | 4 +- cadence/tests/tracer_strategy_test.cdc | 112 ++++++++++++++++++ 5 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc create mode 100644 cadence/scripts/tidal-yield/get_tide_balance.cdc create mode 100644 cadence/tests/tracer_strategy_test.cdc diff --git a/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc b/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc new file mode 100644 index 00000000..f9fe864f --- /dev/null +++ b/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc @@ -0,0 +1,6 @@ +import "TidalYieldAutoBalancers" + +access(all) +fun main(id: UInt64): UFix64? { + return TidalYieldAutoBalancers.borrowAutoBalancer(id: id)?.vaultBalance() +} diff --git a/cadence/scripts/tidal-yield/get_tide_balance.cdc b/cadence/scripts/tidal-yield/get_tide_balance.cdc new file mode 100644 index 00000000..70e535c9 --- /dev/null +++ b/cadence/scripts/tidal-yield/get_tide_balance.cdc @@ -0,0 +1,18 @@ +import "TidalYield" + +/// Returns the balance of the tide with the given ID at the provided address or nil if either the address does not +/// have a TideManager stored or the Tide is not available. Note this `nil` does not mean a Tide with the given ID +/// does not exist, solely that the Tide is not stored at the provided address. +/// +/// @param address: The address of the account to look for the Tide +/// @param id: The ID of the Tide to query the balance of +/// +/// @return the balance of the Tide or `nil` if the Tide was not found +/// +access(all) +fun main(address: Address, id: UInt64): UFix64? { + let tide = getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) + ?.borrowTide(id: id) + ?? nil + return tide?.getTideBalance() ?? nil +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index c8720cd0..ad85c34d 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -43,6 +43,12 @@ access(all) fun deployContracts() { arguments: [] ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FungibleTokenStack", + path: "../../DeFiBlocks/cadence/contracts/connectors/FungibleTokenStack.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // TidalProtocol contracts let initialMoetSupply = 0.0 @@ -109,6 +115,14 @@ access(all) fun deployContracts() { Test.expect(err, Test.beNil()) } +access(all) +fun setupTidalProtocol(signer: Test.TestAccount) { + let res = _executeTransaction("../transactions/tidal-protocol/create_and_store_pool.cdc", + [], + signer + ) +} + /* --- Script helpers */ access(all) @@ -137,16 +151,6 @@ fun createAndStorePool(signer: Test.TestAccount, defaultTokenIdentifier: String, Test.expect(createRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } -access(all) -fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, price: UFix64) { - let setRes = _executeTransaction( - "./transactions/mock-oracle/set_price.cdc", - [forTokenIdentifier, price], - signer - ) - Test.expect(setRes, Test.beSucceeded()) -} - access(all) fun addSupportedTokenSimpleInterestCurve( signer: Test.TestAccount, @@ -180,12 +184,24 @@ fun setupMoetVault(_ signer: Test.TestAccount, beFailed: Bool) { Test.expect(setupRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun setupYieldVault(_ signer: Test.TestAccount, beFailed: Bool) { + let setupRes = _executeTransaction("../transactions/yield-token/setup_vault.cdc", [], signer) + Test.expect(setupRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + access(all) fun mintMoet(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bool) { let mintRes = _executeTransaction("../transactions/moet/mint_moet.cdc", [to, amount], signer) Test.expect(mintRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun mintYield(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bool) { + let mintRes = _executeTransaction("../transactions/yield-token/mint_yield.cdc", [to, amount], signer) + Test.expect(mintRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + access(all) fun addStrategyComposer(signer: Test.TestAccount, strategyIdentifier: String, composerIdentifier: String, issuerStoragePath: StoragePath, beFailed: Bool) { let addRes = _executeTransaction("../transactions/tidal-yield/admin/add_strategy_composer.cdc", @@ -196,14 +212,14 @@ fun addStrategyComposer(signer: Test.TestAccount, strategyIdentifier: String, co } access(all) -fun openTide( +fun createTide( signer: Test.TestAccount, strategyIdentifier: String, vaultIdentifier: String, amount: UFix64, beFailed: Bool ) { - let res = _executeTransaction("../transactions/tidal-yield/open_tide.cdc", + let res = _executeTransaction("../transactions/tidal-yield/create_tide.cdc", [ strategyIdentifier, vaultIdentifier, amount ], signer ) @@ -214,4 +230,26 @@ access(all) fun closeTide(signer: Test.TestAccount, id: UInt64, beFailed: Bool) { let res = _executeTransaction("../transactions/tidal-yield/close_tide.cdc", [id], signer) Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +/* --- Mock helpers --- */ + +access(all) +fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, price: UFix64) { + let setRes = _executeTransaction( + "../transactions/mocks/oracle/set_price.cdc", + [ forTokenIdentifier, price ], + signer + ) + Test.expect(setRes, Test.beSucceeded()) +} + +access(all) +fun setMockSwapperLiquidityConnector(signer: Test.TestAccount, vaultStoragePath: StoragePath) { + let setRes = _executeTransaction( + "../transactions/mocks/swapper/set_liquidity_connector.cdc", + [ vaultStoragePath ], + signer + ) + Test.expect(setRes, Test.beSucceeded()) } \ No newline at end of file diff --git a/cadence/tests/tide_management_test.cdc b/cadence/tests/tide_management_test.cdc index 819b8fa4..df328edf 100644 --- a/cadence/tests/tide_management_test.cdc +++ b/cadence/tests/tide_management_test.cdc @@ -35,7 +35,7 @@ fun test_CreateTideSucceeds() { let user = Test.createAccount() mintFlow(to: user, amount: fundingAmount) - openTide( + createTide( signer: user, strategyIdentifier: strategyIdentifier, vaultIdentifier: flowTokenIdentifier, @@ -57,7 +57,7 @@ fun test_CloseTideSucceeds() { let user = Test.createAccount() mintFlow(to: user, amount: fundingAmount) - openTide( + createTide( signer: user, strategyIdentifier: strategyIdentifier, vaultIdentifier: flowTokenIdentifier, diff --git a/cadence/tests/tracer_strategy_test.cdc b/cadence/tests/tracer_strategy_test.cdc new file mode 100644 index 00000000..88dc1cec --- /dev/null +++ b/cadence/tests/tracer_strategy_test.cdc @@ -0,0 +1,112 @@ +import Test +import BlockchainHelpers + +import "test_helpers.cdc" + +import "FlowToken" +import "MOET" +import "YieldToken" +import "TidalYieldStrategies" + +access(all) let protocolAccount = Test.getAccount(0x0000000000000008) +access(all) let tidalYieldAccount = Test.getAccount(0x0000000000000009) +access(all) let yieldTokenAccount = Test.getAccount(0x0000000000000010) + +access(all) var strategyIdentifier = Type<@TidalYieldStrategies.TracerStrategy>().identifier +access(all) var flowTokenIdentifier = Type<@FlowToken.Vault>().identifier +access(all) var yieldTokenIdentifier = Type<@YieldToken.Vault>().identifier +access(all) var moetTokenIdentifier = Type<@MOET.Vault>().identifier + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun setup() { + deployContracts() + + // set mocked token prices + setMockOraclePrice(signer: tidalYieldAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.0) + setMockOraclePrice(signer: tidalYieldAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) + + // mint tokens & set liquidity in mock swapper contract + setupYieldVault(protocolAccount, beFailed: false) + mintFlow(to: protocolAccount, amount: 100_000_00.0) + mintMoet(signer: protocolAccount, to: protocolAccount.address, amount: 100_000_00.0, beFailed: false) + mintYield(signer: yieldTokenAccount, to: protocolAccount.address, amount: 100_000_00.0, beFailed: false) + setMockSwapperLiquidityConnector(signer: protocolAccount, vaultStoragePath: MOET.VaultStoragePath) + setMockSwapperLiquidityConnector(signer: protocolAccount, vaultStoragePath: YieldToken.VaultStoragePath) + setMockSwapperLiquidityConnector(signer: protocolAccount, vaultStoragePath: /storage/flowTokenVault) + + // setup TidalProtocol with a Pool & add FLOW as supported token + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: moetTokenIdentifier, beFailed: false) + addSupportedTokenSimpleInterestCurve( + signer: protocolAccount, + tokenTypeIdentifier: flowTokenIdentifier, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // enable mocked Strategy creation + addStrategyComposer(signer: tidalYieldAccount, + strategyIdentifier: strategyIdentifier, + composerIdentifier: Type<@TidalYieldStrategies.TracerStrategyComposer>().identifier, + issuerStoragePath: TidalYieldStrategies.IssuerStoragePath, + beFailed: false + ) + + snapshot = getCurrentBlockHeight() +} + +access(all) +fun test_SetupSucceeds() { + log("Success: TracerStrategy setup succeeded") +} + +access(all) +fun test_CreateTideSucceeds() { + let fundingAmount = 100.0 + + let user = Test.createAccount() + mintFlow(to: user, amount: fundingAmount) + + createTide( + signer: user, + strategyIdentifier: strategyIdentifier, + vaultIdentifier: flowTokenIdentifier, + amount: fundingAmount, + beFailed: false + ) + + let tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(1, tideIDs!.length) +} + +access(all) +fun test_CloseTideSucceeds() { + Test.reset(to: snapshot) + + let fundingAmount = 100.0 + + let user = Test.createAccount() + mintFlow(to: user, amount: fundingAmount) + + createTide( + signer: user, + strategyIdentifier: strategyIdentifier, + vaultIdentifier: flowTokenIdentifier, + amount: fundingAmount, + beFailed: false + ) + + var tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(1, tideIDs!.length) + + closeTide(signer: user, id: tideIDs![0], beFailed: false) + + tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(0, tideIDs!.length) +} \ No newline at end of file From 22d7d0d971b7b148f5c74019cffc5d56e88a7b0f Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 17:02:15 -0600 Subject: [PATCH 64/80] update scripts --- cadence/scripts/tidal-protocol/pool_exists.cdc | 9 --------- .../tidal-yield/get_auto_balancer_balance_by_id.cdc | 2 ++ cadence/scripts/tidal-yield/get_tide_ids.cdc | 1 + 3 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 cadence/scripts/tidal-protocol/pool_exists.cdc diff --git a/cadence/scripts/tidal-protocol/pool_exists.cdc b/cadence/scripts/tidal-protocol/pool_exists.cdc deleted file mode 100644 index fc10792e..00000000 --- a/cadence/scripts/tidal-protocol/pool_exists.cdc +++ /dev/null @@ -1,9 +0,0 @@ -import "TidalProtocol" - -/// Returns whether there is a Pool stored in the provided account's address. This address would normally be the -/// TidalProtocol contract address -/// -access(all) -fun main(address: Address): Bool { - return getAccount(address).storage.type(at: TidalProtocol.PoolStoragePath) == Type<@TidalProtocol.Pool>() -} diff --git a/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc b/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc index f9fe864f..8240d644 100644 --- a/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc +++ b/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc @@ -1,5 +1,7 @@ import "TidalYieldAutoBalancers" +/// Returns the balance of the AutoBalancer related to the provided Tide ID or `nil` if none exists +/// access(all) fun main(id: UInt64): UFix64? { return TidalYieldAutoBalancers.borrowAutoBalancer(id: id)?.vaultBalance() diff --git a/cadence/scripts/tidal-yield/get_tide_ids.cdc b/cadence/scripts/tidal-yield/get_tide_ids.cdc index a214161a..58a3a60f 100644 --- a/cadence/scripts/tidal-yield/get_tide_ids.cdc +++ b/cadence/scripts/tidal-yield/get_tide_ids.cdc @@ -5,6 +5,7 @@ import "TidalYield" /// @param address: The address of the Flow account in question /// /// @return A UInt64 array of all Tide IDs stored in the account's TideManager +/// access(all) fun main(address: Address): [UInt64]? { return getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) From 6268d2ef01167bb2837f578b8fc581068225fa39 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 12 Jun 2025 17:10:22 -0600 Subject: [PATCH 65/80] add AutoBalancer rebalance transaction --- .../admin/rebalance_auto_balancer_by_id.cdc | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 cadence/transactions/tidal-yield/admin/rebalance_auto_balancer_by_id.cdc diff --git a/cadence/transactions/tidal-yield/admin/rebalance_auto_balancer_by_id.cdc b/cadence/transactions/tidal-yield/admin/rebalance_auto_balancer_by_id.cdc new file mode 100644 index 00000000..8ecdd031 --- /dev/null +++ b/cadence/transactions/tidal-yield/admin/rebalance_auto_balancer_by_id.cdc @@ -0,0 +1,29 @@ +import "DFB" + +import "TidalYieldAutoBalancers" + +/// Calls on the AutoBalancer to rebalance which will result in a rebalancing around the value of deposits. If force is +/// `true`, rebalancing should occur regardless of the lower & upper thresholds configured on the AutoBalancer. +/// Otherwise, rebalancing will only occur if the value of deposits is above or below the relative thresholds and +/// a rebalance Sink or Source is set. +/// +/// For more information on DeFiBlocks AutoBalancers, see the DFB contract. +/// +/// @param id: The Tide ID for which the AutoBalancer is associated +/// @param force: Whether or not to force rebalancing, bypassing it's thresholds for automatic rebalancing +/// +transaction(id: UInt64, force: Bool) { + // the AutoBalancer that will be rebalanced + let autoBalancer: auth(DFB.Auto) &DFB.AutoBalancer + + prepare(signer: auth(BorrowValue) &Account) { + // derive the path and borrow an authorized reference to the AutoBalancer + let storagePath = TidalYieldAutoBalancers.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath + self.autoBalancer = signer.storage.borrow(from: storagePath) + ?? panic("Could not borrow reference to AutoBalancer id \(id) at path \(storagePath)") + } + + execute { + self.autoBalancer.rebalance(force: force) + } +} From 058bbe8eeff4c51b9d61dbd77789e1925ce957f2 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:49:04 -0600 Subject: [PATCH 66/80] undo Tidal contract renaming --- .../contracts/{TidalYield.cdc => Tidal.cdc} | 4 ++-- cadence/contracts/TidalYieldStrategies.cdc | 14 ++++++------ cadence/contracts/mocks/MockStrategy.cdc | 12 +++++----- .../tidal-yield/get_supported_strategies.cdc | 6 ++--- .../scripts/tidal-yield/get_tide_balance.cdc | 4 ++-- cadence/scripts/tidal-yield/get_tide_ids.cdc | 4 ++-- cadence/tests/test_helpers.cdc | 4 ++-- .../admin/add_strategy_composer.cdc | 14 ++++++------ .../transactions/tidal-yield/close_tide.cdc | 8 +++---- .../transactions/tidal-yield/create_tide.cdc | 22 +++++++++---------- .../tidal-yield/deposit_to_tide.cdc | 8 +++---- cadence/transactions/tidal-yield/setup.cdc | 20 ++++++++--------- .../tidal-yield/withdraw_from_tide.cdc | 8 +++---- flow.json | 6 ++--- 14 files changed, 67 insertions(+), 67 deletions(-) rename cadence/contracts/{TidalYield.cdc => Tidal.cdc} (99%) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/Tidal.cdc similarity index 99% rename from cadence/contracts/TidalYield.cdc rename to cadence/contracts/Tidal.cdc index 4e04e900..59d3d00a 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/Tidal.cdc @@ -8,7 +8,7 @@ import "DFB" /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// -access(all) contract TidalYield { +access(all) contract Tidal { /* --- FIELDS --- */ @@ -201,7 +201,7 @@ access(all) contract TidalYield { init(strategyType: Type, withVault: @{FungibleToken.Vault}) { self.uniqueID = DFB.UniqueIdentifier() self.vaultType = withVault.getType() - let _strategy <- TidalYield.createStrategy( + let _strategy <- Tidal.createStrategy( type: strategyType, uniqueID: self.uniqueID, withFunds: <-withVault diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 02145b2a..ea3dcb6e 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -8,7 +8,7 @@ import "SwapStack" // Lending protocol import "TidalProtocol" // TidalYield platform -import "TidalYield" +import "Tidal" import "TidalYieldAutoBalancers" // tokens import "YieldToken" @@ -40,7 +40,7 @@ access(all) contract TidalYieldStrategies { /// Source. While this object is a simple wrapper for the top-level collateralized position, the true magic of the /// DeFiBlocks is in the stacking of the related connectors. This stacking logic can be found in the /// TracerStrategyComposer construct. - access(all) resource TracerStrategy : TidalYield.Strategy { + access(all) resource TracerStrategy : Tidal.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -55,7 +55,7 @@ access(all) contract TidalYieldStrategies { self.source = position.createSource(type: collateralType) } - // Inherited from TidalYield.Strategy default implementation + // Inherited from Tidal.Strategy default implementation // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { @@ -84,7 +84,7 @@ access(all) contract TidalYieldStrategies { } /// This StrategyComposer builds a TracerStrategy - access(all) resource TracerStrategyComposer : TidalYield.StrategyComposer { + access(all) resource TracerStrategyComposer : Tidal.StrategyComposer { /// Returns the Types of Strategies composed by this StrategyComposer access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@TracerStrategy>(): true } @@ -106,7 +106,7 @@ access(all) contract TidalYieldStrategies { _ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault} - ): @{TidalYield.Strategy} { + ): @{Tidal.Strategy} { // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy let oracle = MockOracle.PriceOracle() @@ -188,11 +188,11 @@ access(all) contract TidalYieldStrategies { /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage /// via configured AutoBalancers - access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { + access(all) resource StrategyComposerIssuer : Tidal.StrategyComposerIssuer { access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@TracerStrategyComposer>(): true } } - access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { + access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { switch type { case Type<@TracerStrategyComposer>(): return <- create TracerStrategyComposer() diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 5a5cc008..555777e2 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -4,7 +4,7 @@ import "FlowToken" import "DFBUtils" import "DFB" -import "TidalYield" +import "Tidal" /// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION @@ -45,7 +45,7 @@ access(all) contract MockStrategy { } } - access(all) resource Strategy : TidalYield.Strategy { + access(all) resource Strategy : Tidal.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -88,7 +88,7 @@ access(all) contract MockStrategy { access(contract) fun burnCallback() {} // no-op } - access(all) resource StrategyComposer : TidalYield.StrategyComposer { + access(all) resource StrategyComposer : Tidal.StrategyComposer { access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@Strategy>(): true } } @@ -102,7 +102,7 @@ access(all) contract MockStrategy { _ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault} - ): @{TidalYield.Strategy} { + ): @{Tidal.Strategy} { let id = DFB.UniqueIdentifier() let strat <- create Strategy( id: id, @@ -118,11 +118,11 @@ access(all) contract MockStrategy { /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage /// via configured AutoBalancers - access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { + access(all) resource StrategyComposerIssuer : Tidal.StrategyComposerIssuer { access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@StrategyComposer>(): true } } - access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { + access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { switch type { case Type<@StrategyComposer>(): return <- create StrategyComposer() diff --git a/cadence/scripts/tidal-yield/get_supported_strategies.cdc b/cadence/scripts/tidal-yield/get_supported_strategies.cdc index fab9cfc0..0abb8b7c 100644 --- a/cadence/scripts/tidal-yield/get_supported_strategies.cdc +++ b/cadence/scripts/tidal-yield/get_supported_strategies.cdc @@ -1,7 +1,7 @@ -import "TidalYield" +import "Tidal" -/// Returns the Strategy Types currently supported by TidalYield +/// Returns the Strategy Types currently supported by Tidal /// access(all) fun main(): [Type] { - return TidalYield.getSupportedStrategies() + return Tidal.getSupportedStrategies() } diff --git a/cadence/scripts/tidal-yield/get_tide_balance.cdc b/cadence/scripts/tidal-yield/get_tide_balance.cdc index 70e535c9..c2289693 100644 --- a/cadence/scripts/tidal-yield/get_tide_balance.cdc +++ b/cadence/scripts/tidal-yield/get_tide_balance.cdc @@ -1,4 +1,4 @@ -import "TidalYield" +import "Tidal" /// Returns the balance of the tide with the given ID at the provided address or nil if either the address does not /// have a TideManager stored or the Tide is not available. Note this `nil` does not mean a Tide with the given ID @@ -11,7 +11,7 @@ import "TidalYield" /// access(all) fun main(address: Address, id: UInt64): UFix64? { - let tide = getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) + let tide = getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) ?.borrowTide(id: id) ?? nil return tide?.getTideBalance() ?? nil diff --git a/cadence/scripts/tidal-yield/get_tide_ids.cdc b/cadence/scripts/tidal-yield/get_tide_ids.cdc index 58a3a60f..e7eaed2d 100644 --- a/cadence/scripts/tidal-yield/get_tide_ids.cdc +++ b/cadence/scripts/tidal-yield/get_tide_ids.cdc @@ -1,4 +1,4 @@ -import "TidalYield" +import "Tidal" /// Retrieves the IDs of Tides configured at the provided address or `nil` if a TideManager is not stored /// @@ -8,6 +8,6 @@ import "TidalYield" /// access(all) fun main(address: Address): [UInt64]? { - return getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) + return getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) ?.getIDs() } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index ad85c34d..7a7c48ea 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -94,8 +94,8 @@ access(all) fun deployContracts() { ) Test.expect(err, Test.beNil()) err = Test.deployContract( - name: "TidalYield", - path: "../contracts/TidalYield.cdc", + name: "Tidal", + path: "../contracts/Tidal.cdc", arguments: [] ) Test.expect(err, Test.beNil()) diff --git a/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc index 609c2936..f540c3c9 100644 --- a/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc +++ b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc @@ -1,6 +1,6 @@ -import "TidalYield" +import "Tidal" -/// Adds the provided Strategy type to the TidalYield StrategyFactory as built by the given StrategyComposer type +/// Adds the provided Strategy type to the Tidal StrategyFactory as built by the given StrategyComposer type /// /// @param strategyIdentifier: The Type identifier of the Strategy to add to the StrategyFactory /// @param composerIdentifier: The Type identifier of the StrategyComposer that builds the Strategy Type @@ -10,9 +10,9 @@ transaction(strategyIdentifier: String, composerIdentifier: String, issuerStorag /// The Strategy Type to add to the StrategyFactory let strategyType: Type /// The StrategyComposer that builds the Strategy Type - let composer: @{TidalYield.StrategyComposer} + let composer: @{Tidal.StrategyComposer} /// Authorized reference to the StrategyFactory to which the Strategy Type & StrategyComposer will be added - let factory: auth(Mutate) &TidalYield.StrategyFactory + let factory: auth(Mutate) &Tidal.StrategyFactory prepare(signer: auth(BorrowValue) &Account) { // construct the types @@ -20,13 +20,13 @@ transaction(strategyIdentifier: String, composerIdentifier: String, issuerStorag let composerType = CompositeType(composerIdentifier) ?? panic("Invalid StrategyComposer type \(composerIdentifier)") // borrow reference to StrategyComposerIssuer & create the StategyComposer - let issuer = signer.storage.borrow<&{TidalYield.StrategyComposerIssuer}>(from: issuerStoragePath) + let issuer = signer.storage.borrow<&{Tidal.StrategyComposerIssuer}>(from: issuerStoragePath) ?? panic("Could not borrow reference to StrategyComposerIssuer from \(issuerStoragePath)") self.composer <- issuer.issueComposer(composerType) // assign StrategyFactory - self.factory = signer.storage.borrow(from: TidalYield.FactoryStoragePath) - ?? panic("Could not borrow reference to StrategyFactory from \(TidalYield.FactoryStoragePath)") + self.factory = signer.storage.borrow(from: Tidal.FactoryStoragePath) + ?? panic("Could not borrow reference to StrategyFactory from \(Tidal.FactoryStoragePath)") } execute { diff --git a/cadence/transactions/tidal-yield/close_tide.cdc b/cadence/transactions/tidal-yield/close_tide.cdc index 13cf8d3c..de833c39 100644 --- a/cadence/transactions/tidal-yield/close_tide.cdc +++ b/cadence/transactions/tidal-yield/close_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" /// Withdraws the full balance from an existing Tide stored in the signer's TideManager and closes the Tide. If the /// signer does not yet have a Vault of the withdrawn Type, one is configured. @@ -10,13 +10,13 @@ import "TidalYield" /// @param id: The Tide.id() of the Tide from which the full balance will be withdrawn /// transaction(id: UInt64) { - let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager + let manager: auth(FungibleToken.Withdraw) &Tidal.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal-yield/create_tide.cdc b/cadence/transactions/tidal-yield/create_tide.cdc index 104d60bd..70079edb 100644 --- a/cadence/transactions/tidal-yield/create_tide.cdc +++ b/cadence/transactions/tidal-yield/create_tide.cdc @@ -2,18 +2,18 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" /// Opens a new Tide in the Tidal platform, funding the Tide with the specified Vault and amount /// /// @param strategyIdentifier: The Strategy's Type identifier. Must be a Strategy Type that is currently supported by -/// TidalYield. See `TidalYield.getSupportedStrategies()` to get those currently supported. +/// Tidal. See `Tidal.getSupportedStrategies()` to get those currently supported. /// @param vaultIdentifier: The Vault's Type identifier /// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' /// @param amount: The amount to deposit into the new Tide /// transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) { - let manager: &TidalYield.TideManager + let manager: &Tidal.TideManager let strategy: Type let depositVault: @{FungibleToken.Vault} @@ -39,17 +39,17 @@ transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) self.depositVault <- sourceVault.withdraw(amount: amount) // configure the TideManager if needed - if signer.storage.type(at: TidalYield.TideManagerStoragePath) == nil { - signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) - signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) + if signer.storage.type(at: Tidal.TideManagerStoragePath) == nil { + signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) + signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue<&TidalYield.TideManager>( - TidalYield.TideManagerStoragePath + signer.capabilities.storage.issue<&Tidal.TideManager>( + Tidal.TideManagerStoragePath ) } - self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") } execute { diff --git a/cadence/transactions/tidal-yield/deposit_to_tide.cdc b/cadence/transactions/tidal-yield/deposit_to_tide.cdc index c48431c1..eacaa9d6 100644 --- a/cadence/transactions/tidal-yield/deposit_to_tide.cdc +++ b/cadence/transactions/tidal-yield/deposit_to_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" /// Deposits to an existing Tide stored in the signer's TideManager /// @@ -10,13 +10,13 @@ import "TidalYield" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: &TidalYield.TideManager + let manager: &Tidal.TideManager let depositVault: @{FungibleToken.Vault} prepare(signer: auth(BorrowValue) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal-yield/setup.cdc b/cadence/transactions/tidal-yield/setup.cdc index 0cd13ee2..b35bbe01 100644 --- a/cadence/transactions/tidal-yield/setup.cdc +++ b/cadence/transactions/tidal-yield/setup.cdc @@ -2,27 +2,27 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" -/// Configures a TidalYield.TideManager at the canonical path. If one is already configured, the transaction no-ops +/// Configures a Tidal.TideManager at the canonical path. If one is already configured, the transaction no-ops /// transaction { prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { - if signer.storage.type(at: TidalYield.TideManagerStoragePath) == Type<@TidalYield.TideManager>() { + if signer.storage.type(at: Tidal.TideManagerStoragePath) == Type<@Tidal.TideManager>() { return // early return if TideManager is found } // configure the TideManager - signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) - signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) + signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) + signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue(TidalYield.TideManagerStoragePath) + signer.capabilities.storage.issue(Tidal.TideManagerStoragePath) // confirm setup of TideManager at canonical path - let storedType = signer.storage.type(at: TidalYield.TideManagerStoragePath) ?? Type() - if storedType != Type<@TidalYield.TideManager>() { - panic("Setup was unsuccessful - Expected TideManager at \(TidalYield.TideManagerStoragePath) but found \(storedType.identifier)") + let storedType = signer.storage.type(at: Tidal.TideManagerStoragePath) ?? Type() + if storedType != Type<@Tidal.TideManager>() { + panic("Setup was unsuccessful - Expected TideManager at \(Tidal.TideManagerStoragePath) but found \(storedType.identifier)") } } } diff --git a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc index 08cbf13f..3f54e98f 100644 --- a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc +++ b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" /// Withdraws from an existing Tide stored in the signer's TideManager. If the signer does not yet have a Vault of the /// withdrawn Type, one is configured. @@ -11,13 +11,13 @@ import "TidalYield" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager + let manager: auth(FungibleToken.Withdraw) &Tidal.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/flow.json b/flow.json index 5084153a..1d0465b9 100644 --- a/flow.json +++ b/flow.json @@ -54,8 +54,8 @@ "testing": "0000000000000008" } }, - "TidalYield": { - "source": "cadence/contracts/TidalYield.cdc", + "Tidal": { + "source": "cadence/contracts/Tidal.cdc", "aliases": { "testing": "0000000000000009" } @@ -205,7 +205,7 @@ }, "MockSwapper", "TidalYieldAutoBalancers", - "TidalYield", + "Tidal", "TidalYieldStrategies" ] } From 02f252c1a67bf489212df8a80fa0845d9362b7dc Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:56:10 -0600 Subject: [PATCH 67/80] rename Tidal to TidalYield --- .../contracts/{Tidal.cdc => TidalYield.cdc} | 4 ++-- cadence/contracts/TidalYieldStrategies.cdc | 14 +++++------ cadence/contracts/mocks/MockStrategy.cdc | 12 +++++----- .../tidal-yield/get_supported_strategies.cdc | 6 ++--- .../scripts/tidal-yield/get_tide_balance.cdc | 4 ++-- cadence/scripts/tidal-yield/get_tide_ids.cdc | 4 ++-- cadence/tests/test_helpers.cdc | 4 ++-- .../admin/add_strategy_composer.cdc | 14 +++++------ .../transactions/tidal-yield/close_tide.cdc | 8 +++---- .../transactions/tidal-yield/create_tide.cdc | 24 +++++++++---------- .../tidal-yield/deposit_to_tide.cdc | 8 +++---- cadence/transactions/tidal-yield/setup.cdc | 20 ++++++++-------- .../tidal-yield/withdraw_from_tide.cdc | 8 +++---- flow.json | 6 ++--- 14 files changed, 68 insertions(+), 68 deletions(-) rename cadence/contracts/{Tidal.cdc => TidalYield.cdc} (99%) diff --git a/cadence/contracts/Tidal.cdc b/cadence/contracts/TidalYield.cdc similarity index 99% rename from cadence/contracts/Tidal.cdc rename to cadence/contracts/TidalYield.cdc index 59d3d00a..4e04e900 100644 --- a/cadence/contracts/Tidal.cdc +++ b/cadence/contracts/TidalYield.cdc @@ -8,7 +8,7 @@ import "DFB" /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// -access(all) contract Tidal { +access(all) contract TidalYield { /* --- FIELDS --- */ @@ -201,7 +201,7 @@ access(all) contract Tidal { init(strategyType: Type, withVault: @{FungibleToken.Vault}) { self.uniqueID = DFB.UniqueIdentifier() self.vaultType = withVault.getType() - let _strategy <- Tidal.createStrategy( + let _strategy <- TidalYield.createStrategy( type: strategyType, uniqueID: self.uniqueID, withFunds: <-withVault diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index ea3dcb6e..02145b2a 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -8,7 +8,7 @@ import "SwapStack" // Lending protocol import "TidalProtocol" // TidalYield platform -import "Tidal" +import "TidalYield" import "TidalYieldAutoBalancers" // tokens import "YieldToken" @@ -40,7 +40,7 @@ access(all) contract TidalYieldStrategies { /// Source. While this object is a simple wrapper for the top-level collateralized position, the true magic of the /// DeFiBlocks is in the stacking of the related connectors. This stacking logic can be found in the /// TracerStrategyComposer construct. - access(all) resource TracerStrategy : Tidal.Strategy { + access(all) resource TracerStrategy : TidalYield.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -55,7 +55,7 @@ access(all) contract TidalYieldStrategies { self.source = position.createSource(type: collateralType) } - // Inherited from Tidal.Strategy default implementation + // Inherited from TidalYield.Strategy default implementation // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { @@ -84,7 +84,7 @@ access(all) contract TidalYieldStrategies { } /// This StrategyComposer builds a TracerStrategy - access(all) resource TracerStrategyComposer : Tidal.StrategyComposer { + access(all) resource TracerStrategyComposer : TidalYield.StrategyComposer { /// Returns the Types of Strategies composed by this StrategyComposer access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@TracerStrategy>(): true } @@ -106,7 +106,7 @@ access(all) contract TidalYieldStrategies { _ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault} - ): @{Tidal.Strategy} { + ): @{TidalYield.Strategy} { // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy let oracle = MockOracle.PriceOracle() @@ -188,11 +188,11 @@ access(all) contract TidalYieldStrategies { /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage /// via configured AutoBalancers - access(all) resource StrategyComposerIssuer : Tidal.StrategyComposerIssuer { + access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@TracerStrategyComposer>(): true } } - access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { + access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { switch type { case Type<@TracerStrategyComposer>(): return <- create TracerStrategyComposer() diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 555777e2..5a5cc008 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -4,7 +4,7 @@ import "FlowToken" import "DFBUtils" import "DFB" -import "Tidal" +import "TidalYield" /// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION @@ -45,7 +45,7 @@ access(all) contract MockStrategy { } } - access(all) resource Strategy : Tidal.Strategy { + access(all) resource Strategy : TidalYield.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -88,7 +88,7 @@ access(all) contract MockStrategy { access(contract) fun burnCallback() {} // no-op } - access(all) resource StrategyComposer : Tidal.StrategyComposer { + access(all) resource StrategyComposer : TidalYield.StrategyComposer { access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@Strategy>(): true } } @@ -102,7 +102,7 @@ access(all) contract MockStrategy { _ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault} - ): @{Tidal.Strategy} { + ): @{TidalYield.Strategy} { let id = DFB.UniqueIdentifier() let strat <- create Strategy( id: id, @@ -118,11 +118,11 @@ access(all) contract MockStrategy { /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage /// via configured AutoBalancers - access(all) resource StrategyComposerIssuer : Tidal.StrategyComposerIssuer { + access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@StrategyComposer>(): true } } - access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { + access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { switch type { case Type<@StrategyComposer>(): return <- create StrategyComposer() diff --git a/cadence/scripts/tidal-yield/get_supported_strategies.cdc b/cadence/scripts/tidal-yield/get_supported_strategies.cdc index 0abb8b7c..fab9cfc0 100644 --- a/cadence/scripts/tidal-yield/get_supported_strategies.cdc +++ b/cadence/scripts/tidal-yield/get_supported_strategies.cdc @@ -1,7 +1,7 @@ -import "Tidal" +import "TidalYield" -/// Returns the Strategy Types currently supported by Tidal +/// Returns the Strategy Types currently supported by TidalYield /// access(all) fun main(): [Type] { - return Tidal.getSupportedStrategies() + return TidalYield.getSupportedStrategies() } diff --git a/cadence/scripts/tidal-yield/get_tide_balance.cdc b/cadence/scripts/tidal-yield/get_tide_balance.cdc index c2289693..70e535c9 100644 --- a/cadence/scripts/tidal-yield/get_tide_balance.cdc +++ b/cadence/scripts/tidal-yield/get_tide_balance.cdc @@ -1,4 +1,4 @@ -import "Tidal" +import "TidalYield" /// Returns the balance of the tide with the given ID at the provided address or nil if either the address does not /// have a TideManager stored or the Tide is not available. Note this `nil` does not mean a Tide with the given ID @@ -11,7 +11,7 @@ import "Tidal" /// access(all) fun main(address: Address, id: UInt64): UFix64? { - let tide = getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) + let tide = getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) ?.borrowTide(id: id) ?? nil return tide?.getTideBalance() ?? nil diff --git a/cadence/scripts/tidal-yield/get_tide_ids.cdc b/cadence/scripts/tidal-yield/get_tide_ids.cdc index e7eaed2d..58a3a60f 100644 --- a/cadence/scripts/tidal-yield/get_tide_ids.cdc +++ b/cadence/scripts/tidal-yield/get_tide_ids.cdc @@ -1,4 +1,4 @@ -import "Tidal" +import "TidalYield" /// Retrieves the IDs of Tides configured at the provided address or `nil` if a TideManager is not stored /// @@ -8,6 +8,6 @@ import "Tidal" /// access(all) fun main(address: Address): [UInt64]? { - return getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) + return getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) ?.getIDs() } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 7a7c48ea..ad85c34d 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -94,8 +94,8 @@ access(all) fun deployContracts() { ) Test.expect(err, Test.beNil()) err = Test.deployContract( - name: "Tidal", - path: "../contracts/Tidal.cdc", + name: "TidalYield", + path: "../contracts/TidalYield.cdc", arguments: [] ) Test.expect(err, Test.beNil()) diff --git a/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc index f540c3c9..609c2936 100644 --- a/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc +++ b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc @@ -1,6 +1,6 @@ -import "Tidal" +import "TidalYield" -/// Adds the provided Strategy type to the Tidal StrategyFactory as built by the given StrategyComposer type +/// Adds the provided Strategy type to the TidalYield StrategyFactory as built by the given StrategyComposer type /// /// @param strategyIdentifier: The Type identifier of the Strategy to add to the StrategyFactory /// @param composerIdentifier: The Type identifier of the StrategyComposer that builds the Strategy Type @@ -10,9 +10,9 @@ transaction(strategyIdentifier: String, composerIdentifier: String, issuerStorag /// The Strategy Type to add to the StrategyFactory let strategyType: Type /// The StrategyComposer that builds the Strategy Type - let composer: @{Tidal.StrategyComposer} + let composer: @{TidalYield.StrategyComposer} /// Authorized reference to the StrategyFactory to which the Strategy Type & StrategyComposer will be added - let factory: auth(Mutate) &Tidal.StrategyFactory + let factory: auth(Mutate) &TidalYield.StrategyFactory prepare(signer: auth(BorrowValue) &Account) { // construct the types @@ -20,13 +20,13 @@ transaction(strategyIdentifier: String, composerIdentifier: String, issuerStorag let composerType = CompositeType(composerIdentifier) ?? panic("Invalid StrategyComposer type \(composerIdentifier)") // borrow reference to StrategyComposerIssuer & create the StategyComposer - let issuer = signer.storage.borrow<&{Tidal.StrategyComposerIssuer}>(from: issuerStoragePath) + let issuer = signer.storage.borrow<&{TidalYield.StrategyComposerIssuer}>(from: issuerStoragePath) ?? panic("Could not borrow reference to StrategyComposerIssuer from \(issuerStoragePath)") self.composer <- issuer.issueComposer(composerType) // assign StrategyFactory - self.factory = signer.storage.borrow(from: Tidal.FactoryStoragePath) - ?? panic("Could not borrow reference to StrategyFactory from \(Tidal.FactoryStoragePath)") + self.factory = signer.storage.borrow(from: TidalYield.FactoryStoragePath) + ?? panic("Could not borrow reference to StrategyFactory from \(TidalYield.FactoryStoragePath)") } execute { diff --git a/cadence/transactions/tidal-yield/close_tide.cdc b/cadence/transactions/tidal-yield/close_tide.cdc index de833c39..13cf8d3c 100644 --- a/cadence/transactions/tidal-yield/close_tide.cdc +++ b/cadence/transactions/tidal-yield/close_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" /// Withdraws the full balance from an existing Tide stored in the signer's TideManager and closes the Tide. If the /// signer does not yet have a Vault of the withdrawn Type, one is configured. @@ -10,13 +10,13 @@ import "Tidal" /// @param id: The Tide.id() of the Tide from which the full balance will be withdrawn /// transaction(id: UInt64) { - let manager: auth(FungibleToken.Withdraw) &Tidal.TideManager + let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal-yield/create_tide.cdc b/cadence/transactions/tidal-yield/create_tide.cdc index 70079edb..3dc6a4b3 100644 --- a/cadence/transactions/tidal-yield/create_tide.cdc +++ b/cadence/transactions/tidal-yield/create_tide.cdc @@ -2,18 +2,18 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" -/// Opens a new Tide in the Tidal platform, funding the Tide with the specified Vault and amount +/// Opens a new Tide in the TidalYield platform, funding the Tide with the specified Vault and amount /// /// @param strategyIdentifier: The Strategy's Type identifier. Must be a Strategy Type that is currently supported by -/// Tidal. See `Tidal.getSupportedStrategies()` to get those currently supported. +/// TidalYield. See `TidalYield.getSupportedStrategies()` to get those currently supported. /// @param vaultIdentifier: The Vault's Type identifier /// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' /// @param amount: The amount to deposit into the new Tide /// transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) { - let manager: &Tidal.TideManager + let manager: &TidalYield.TideManager let strategy: Type let depositVault: @{FungibleToken.Vault} @@ -39,17 +39,17 @@ transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) self.depositVault <- sourceVault.withdraw(amount: amount) // configure the TideManager if needed - if signer.storage.type(at: Tidal.TideManagerStoragePath) == nil { - signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) - signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) + if signer.storage.type(at: TidalYield.TideManagerStoragePath) == nil { + signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) + signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue<&Tidal.TideManager>( - Tidal.TideManagerStoragePath + signer.capabilities.storage.issue<&TidalYield.TideManager>( + TidalYield.TideManagerStoragePath ) } - self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") } execute { diff --git a/cadence/transactions/tidal-yield/deposit_to_tide.cdc b/cadence/transactions/tidal-yield/deposit_to_tide.cdc index eacaa9d6..c48431c1 100644 --- a/cadence/transactions/tidal-yield/deposit_to_tide.cdc +++ b/cadence/transactions/tidal-yield/deposit_to_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" /// Deposits to an existing Tide stored in the signer's TideManager /// @@ -10,13 +10,13 @@ import "Tidal" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: &Tidal.TideManager + let manager: &TidalYield.TideManager let depositVault: @{FungibleToken.Vault} prepare(signer: auth(BorrowValue) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal-yield/setup.cdc b/cadence/transactions/tidal-yield/setup.cdc index b35bbe01..0cd13ee2 100644 --- a/cadence/transactions/tidal-yield/setup.cdc +++ b/cadence/transactions/tidal-yield/setup.cdc @@ -2,27 +2,27 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" -/// Configures a Tidal.TideManager at the canonical path. If one is already configured, the transaction no-ops +/// Configures a TidalYield.TideManager at the canonical path. If one is already configured, the transaction no-ops /// transaction { prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { - if signer.storage.type(at: Tidal.TideManagerStoragePath) == Type<@Tidal.TideManager>() { + if signer.storage.type(at: TidalYield.TideManagerStoragePath) == Type<@TidalYield.TideManager>() { return // early return if TideManager is found } // configure the TideManager - signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) - signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) + signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) + signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue(Tidal.TideManagerStoragePath) + signer.capabilities.storage.issue(TidalYield.TideManagerStoragePath) // confirm setup of TideManager at canonical path - let storedType = signer.storage.type(at: Tidal.TideManagerStoragePath) ?? Type() - if storedType != Type<@Tidal.TideManager>() { - panic("Setup was unsuccessful - Expected TideManager at \(Tidal.TideManagerStoragePath) but found \(storedType.identifier)") + let storedType = signer.storage.type(at: TidalYield.TideManagerStoragePath) ?? Type() + if storedType != Type<@TidalYield.TideManager>() { + panic("Setup was unsuccessful - Expected TideManager at \(TidalYield.TideManagerStoragePath) but found \(storedType.identifier)") } } } diff --git a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc index 3f54e98f..08cbf13f 100644 --- a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc +++ b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "Tidal" +import "TidalYield" /// Withdraws from an existing Tide stored in the signer's TideManager. If the signer does not yet have a Vault of the /// withdrawn Type, one is configured. @@ -11,13 +11,13 @@ import "Tidal" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: auth(FungibleToken.Withdraw) &Tidal.TideManager + let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/flow.json b/flow.json index 1d0465b9..5084153a 100644 --- a/flow.json +++ b/flow.json @@ -54,8 +54,8 @@ "testing": "0000000000000008" } }, - "Tidal": { - "source": "cadence/contracts/Tidal.cdc", + "TidalYield": { + "source": "cadence/contracts/TidalYield.cdc", "aliases": { "testing": "0000000000000009" } @@ -205,7 +205,7 @@ }, "MockSwapper", "TidalYieldAutoBalancers", - "Tidal", + "TidalYield", "TidalYieldStrategies" ] } From e085da258abe4fe08feda40bc642e4f1cbd6c106 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:53:16 -0600 Subject: [PATCH 68/80] fix MOET deposit logic --- cadence/contracts/internal-dependencies/tokens/MOET.cdc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/internal-dependencies/tokens/MOET.cdc b/cadence/contracts/internal-dependencies/tokens/MOET.cdc index 4ab12018..09aaa3db 100644 --- a/cadence/contracts/internal-dependencies/tokens/MOET.cdc +++ b/cadence/contracts/internal-dependencies/tokens/MOET.cdc @@ -149,8 +149,11 @@ access(all) contract MOET : FungibleToken { access(all) fun deposit(from: @{FungibleToken.Vault}) { let vault <- from as! @MOET.Vault - self.balance = self.balance + vault.balance + let amount = vault.balance + vault.balance = 0.0 destroy vault + + self.balance = self.balance + amount } access(all) fun createEmptyVault(): @MOET.Vault { From f960a016834029ed2519d047fbb42cfbeb62e221 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:54:32 -0600 Subject: [PATCH 69/80] fix YieldToken deposit logic --- .../contracts/internal-dependencies/tokens/YieldToken.cdc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/internal-dependencies/tokens/YieldToken.cdc b/cadence/contracts/internal-dependencies/tokens/YieldToken.cdc index dcd800df..e923079f 100644 --- a/cadence/contracts/internal-dependencies/tokens/YieldToken.cdc +++ b/cadence/contracts/internal-dependencies/tokens/YieldToken.cdc @@ -149,8 +149,11 @@ access(all) contract YieldToken : FungibleToken { access(all) fun deposit(from: @{FungibleToken.Vault}) { let vault <- from as! @YieldToken.Vault - self.balance = self.balance + vault.balance + let amount = vault.balance + vault.balance = 0.0 destroy vault + + self.balance = self.balance + amount } access(all) fun createEmptyVault(): @YieldToken.Vault { From fdbe1a253af7dee8ca4dd5d4d4a34c222d4771e3 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:43:50 -0600 Subject: [PATCH 70/80] update addStrategyComposer pre-condition --- cadence/contracts/Tidal.cdc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cadence/contracts/Tidal.cdc b/cadence/contracts/Tidal.cdc index 59d3d00a..eb692cd7 100644 --- a/cadence/contracts/Tidal.cdc +++ b/cadence/contracts/Tidal.cdc @@ -148,6 +148,8 @@ access(all) contract Tidal { /// Sets the provided Strategy and Composer association in the StrategyFactory access(Mutate) fun addStrategyComposer(_ strategy: Type, composer: @{StrategyComposer}) { pre { + strategy.isSubtype(of: Type<@{Strategy}>()): + "Invalid Strategy Type \(strategy.identifier) - provided Type does not implement the Strategy interface" composer.getComposedStrategyTypes()[strategy] == true: "Strategy \(strategy.identifier) cannot be composed by StrategyComposer \(composer.getType().identifier)" } From 051302c10b3219a5a849d0e531a76c09411dbf7d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 17 Jun 2025 12:04:33 -0600 Subject: [PATCH 71/80] fix mock price getter script --- cadence/scripts/mocks/oracle/get_price.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/scripts/mocks/oracle/get_price.cdc b/cadence/scripts/mocks/oracle/get_price.cdc index c7efe2cd..8c31d542 100644 --- a/cadence/scripts/mocks/oracle/get_price.cdc +++ b/cadence/scripts/mocks/oracle/get_price.cdc @@ -8,5 +8,5 @@ fun main(forTokenIdentifier: String): UFix64 { // Type identifier - e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' return MockOracle.PriceOracle().price( ofToken: CompositeType(forTokenIdentifier) ?? panic("Invalid forTokenIdentifier \(forTokenIdentifier)") - ) + ) ?? panic("MockOracle does not have a price for token \(forTokenIdentifier)") } From 671520487ae6ae419516e29da909d8721f9f9b7e Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 17 Jun 2025 12:07:47 -0600 Subject: [PATCH 72/80] update flow.json emulator aliases --- flow.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/flow.json b/flow.json index 1d0465b9..59503983 100644 --- a/flow.json +++ b/flow.json @@ -3,18 +3,21 @@ "DFB": { "source": "./DeFiBlocks/cadence/contracts/interfaces/DFB.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000007" } }, "DFBUtils": { "source": "./DeFiBlocks/cadence/contracts/utils/DFBUtils.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000007" } }, "FungibleTokenStack": { "source": "./DeFiBlocks/cadence/contracts/connectors/FungibleTokenStack.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000007" } }, @@ -27,54 +30,63 @@ "MockOracle": { "source": "cadence/contracts/mocks/MockOracle.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000009" } }, "MockStrategy": { "source": "cadence/contracts/mocks/MockStrategy.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000009" } }, "MockSwapper": { "source": "cadence/contracts/mocks/MockSwapper.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000009" } }, "SwapStack": { "source": "./DeFiBlocks/cadence/contracts/connectors/SwapStack.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000007" } }, "TidalProtocol": { "source": "cadence/contracts/internal-dependencies/TidalProtocol.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000008" } }, "Tidal": { "source": "cadence/contracts/Tidal.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000009" } }, "TidalYieldAutoBalancers": { "source": "cadence/contracts/TidalYieldAutoBalancers.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000009" } }, "TidalYieldStrategies": { "source": "cadence/contracts/TidalYieldStrategies.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000009" } }, "YieldToken": { "source": "cadence/contracts/internal-dependencies/tokens/YieldToken.cdc", "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000010" } } @@ -210,4 +222,4 @@ ] } } -} \ No newline at end of file +} From 662660afee42d0a8c7335524924061d128266304 Mon Sep 17 00:00:00 2001 From: Alex Ni <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:20:05 -0400 Subject: [PATCH 73/80] add test user --- DeFiBlocks | 2 +- flow.json | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/DeFiBlocks b/DeFiBlocks index a2dcbd50..71465264 160000 --- a/DeFiBlocks +++ b/DeFiBlocks @@ -1 +1 @@ -Subproject commit a2dcbd506152a31fe713617f8ab684315d4cbf44 +Subproject commit 7146526409c18321dc1e0244862b2a47f9e53abc diff --git a/flow.json b/flow.json index 59503983..00e0455a 100644 --- a/flow.json +++ b/flow.json @@ -178,7 +178,14 @@ "type": "file", "location": "emulator-account.pkey" } - } + }, + "test-user": { + "address": "179b6b1cb6755e31", + "key": { + "type": "file", + "location": "test-user.pkey" + } + } }, "deployments": { "emulator": { From 6bf8ccf552d6943c3d8e1ed9cf0feb8e767ca775 Mon Sep 17 00:00:00 2001 From: Alex Ni <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:46:09 -0400 Subject: [PATCH 74/80] add +x --- local/setup_emulator.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 local/setup_emulator.sh diff --git a/local/setup_emulator.sh b/local/setup_emulator.sh old mode 100644 new mode 100755 From 77057b6074a99273d09ec906a8747777153cfc0b Mon Sep 17 00:00:00 2001 From: Alex Ni <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Jun 2025 20:51:01 -0400 Subject: [PATCH 75/80] swap git module --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index d2babe94..82aadd90 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "DeFiBlocks"] path = DeFiBlocks - url = https://github.com/onflow/DeFiBlocks + url = git@github.com:onflow/DeFiBlocks.git From b7d182cedc092d7814b374d3ad5803cf217734e0 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:10:22 -0600 Subject: [PATCH 76/80] Revert "Rename Tidal to TidalYield" (#10) --- .../contracts/{TidalYield.cdc => Tidal.cdc} | 4 ++-- cadence/contracts/TidalYieldStrategies.cdc | 14 +++++------ cadence/contracts/mocks/MockStrategy.cdc | 12 +++++----- .../tidal-yield/get_supported_strategies.cdc | 6 ++--- .../scripts/tidal-yield/get_tide_balance.cdc | 4 ++-- cadence/scripts/tidal-yield/get_tide_ids.cdc | 4 ++-- cadence/tests/test_helpers.cdc | 4 ++-- .../admin/add_strategy_composer.cdc | 14 +++++------ .../transactions/tidal-yield/close_tide.cdc | 8 +++---- .../transactions/tidal-yield/create_tide.cdc | 24 +++++++++---------- .../tidal-yield/deposit_to_tide.cdc | 8 +++---- cadence/transactions/tidal-yield/setup.cdc | 20 ++++++++-------- .../tidal-yield/withdraw_from_tide.cdc | 8 +++---- flow.json | 6 ++--- 14 files changed, 68 insertions(+), 68 deletions(-) rename cadence/contracts/{TidalYield.cdc => Tidal.cdc} (99%) diff --git a/cadence/contracts/TidalYield.cdc b/cadence/contracts/Tidal.cdc similarity index 99% rename from cadence/contracts/TidalYield.cdc rename to cadence/contracts/Tidal.cdc index 86e0df36..eb692cd7 100644 --- a/cadence/contracts/TidalYield.cdc +++ b/cadence/contracts/Tidal.cdc @@ -8,7 +8,7 @@ import "DFB" /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// -access(all) contract TidalYield { +access(all) contract Tidal { /* --- FIELDS --- */ @@ -203,7 +203,7 @@ access(all) contract TidalYield { init(strategyType: Type, withVault: @{FungibleToken.Vault}) { self.uniqueID = DFB.UniqueIdentifier() self.vaultType = withVault.getType() - let _strategy <- TidalYield.createStrategy( + let _strategy <- Tidal.createStrategy( type: strategyType, uniqueID: self.uniqueID, withFunds: <-withVault diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index 02145b2a..ea3dcb6e 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -8,7 +8,7 @@ import "SwapStack" // Lending protocol import "TidalProtocol" // TidalYield platform -import "TidalYield" +import "Tidal" import "TidalYieldAutoBalancers" // tokens import "YieldToken" @@ -40,7 +40,7 @@ access(all) contract TidalYieldStrategies { /// Source. While this object is a simple wrapper for the top-level collateralized position, the true magic of the /// DeFiBlocks is in the stacking of the related connectors. This stacking logic can be found in the /// TracerStrategyComposer construct. - access(all) resource TracerStrategy : TidalYield.Strategy { + access(all) resource TracerStrategy : Tidal.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -55,7 +55,7 @@ access(all) contract TidalYieldStrategies { self.source = position.createSource(type: collateralType) } - // Inherited from TidalYield.Strategy default implementation + // Inherited from Tidal.Strategy default implementation // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { @@ -84,7 +84,7 @@ access(all) contract TidalYieldStrategies { } /// This StrategyComposer builds a TracerStrategy - access(all) resource TracerStrategyComposer : TidalYield.StrategyComposer { + access(all) resource TracerStrategyComposer : Tidal.StrategyComposer { /// Returns the Types of Strategies composed by this StrategyComposer access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@TracerStrategy>(): true } @@ -106,7 +106,7 @@ access(all) contract TidalYieldStrategies { _ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault} - ): @{TidalYield.Strategy} { + ): @{Tidal.Strategy} { // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy let oracle = MockOracle.PriceOracle() @@ -188,11 +188,11 @@ access(all) contract TidalYieldStrategies { /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage /// via configured AutoBalancers - access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { + access(all) resource StrategyComposerIssuer : Tidal.StrategyComposerIssuer { access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@TracerStrategyComposer>(): true } } - access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { + access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { switch type { case Type<@TracerStrategyComposer>(): return <- create TracerStrategyComposer() diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc index 5a5cc008..555777e2 100644 --- a/cadence/contracts/mocks/MockStrategy.cdc +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -4,7 +4,7 @@ import "FlowToken" import "DFBUtils" import "DFB" -import "TidalYield" +import "Tidal" /// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION @@ -45,7 +45,7 @@ access(all) contract MockStrategy { } } - access(all) resource Strategy : TidalYield.Strategy { + access(all) resource Strategy : Tidal.Strategy { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- /// specific Identifier to associated connectors on construction access(contract) let uniqueID: DFB.UniqueIdentifier? @@ -88,7 +88,7 @@ access(all) contract MockStrategy { access(contract) fun burnCallback() {} // no-op } - access(all) resource StrategyComposer : TidalYield.StrategyComposer { + access(all) resource StrategyComposer : Tidal.StrategyComposer { access(all) view fun getComposedStrategyTypes(): {Type: Bool} { return { Type<@Strategy>(): true } } @@ -102,7 +102,7 @@ access(all) contract MockStrategy { _ type: Type, uniqueID: DFB.UniqueIdentifier, withFunds: @{FungibleToken.Vault} - ): @{TidalYield.Strategy} { + ): @{Tidal.Strategy} { let id = DFB.UniqueIdentifier() let strat <- create Strategy( id: id, @@ -118,11 +118,11 @@ access(all) contract MockStrategy { /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since TracerStrategy creation consumes account storage /// via configured AutoBalancers - access(all) resource StrategyComposerIssuer : TidalYield.StrategyComposerIssuer { + access(all) resource StrategyComposerIssuer : Tidal.StrategyComposerIssuer { access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@StrategyComposer>(): true } } - access(all) fun issueComposer(_ type: Type): @{TidalYield.StrategyComposer} { + access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { switch type { case Type<@StrategyComposer>(): return <- create StrategyComposer() diff --git a/cadence/scripts/tidal-yield/get_supported_strategies.cdc b/cadence/scripts/tidal-yield/get_supported_strategies.cdc index fab9cfc0..0abb8b7c 100644 --- a/cadence/scripts/tidal-yield/get_supported_strategies.cdc +++ b/cadence/scripts/tidal-yield/get_supported_strategies.cdc @@ -1,7 +1,7 @@ -import "TidalYield" +import "Tidal" -/// Returns the Strategy Types currently supported by TidalYield +/// Returns the Strategy Types currently supported by Tidal /// access(all) fun main(): [Type] { - return TidalYield.getSupportedStrategies() + return Tidal.getSupportedStrategies() } diff --git a/cadence/scripts/tidal-yield/get_tide_balance.cdc b/cadence/scripts/tidal-yield/get_tide_balance.cdc index 70e535c9..c2289693 100644 --- a/cadence/scripts/tidal-yield/get_tide_balance.cdc +++ b/cadence/scripts/tidal-yield/get_tide_balance.cdc @@ -1,4 +1,4 @@ -import "TidalYield" +import "Tidal" /// Returns the balance of the tide with the given ID at the provided address or nil if either the address does not /// have a TideManager stored or the Tide is not available. Note this `nil` does not mean a Tide with the given ID @@ -11,7 +11,7 @@ import "TidalYield" /// access(all) fun main(address: Address, id: UInt64): UFix64? { - let tide = getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) + let tide = getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) ?.borrowTide(id: id) ?? nil return tide?.getTideBalance() ?? nil diff --git a/cadence/scripts/tidal-yield/get_tide_ids.cdc b/cadence/scripts/tidal-yield/get_tide_ids.cdc index 58a3a60f..e7eaed2d 100644 --- a/cadence/scripts/tidal-yield/get_tide_ids.cdc +++ b/cadence/scripts/tidal-yield/get_tide_ids.cdc @@ -1,4 +1,4 @@ -import "TidalYield" +import "Tidal" /// Retrieves the IDs of Tides configured at the provided address or `nil` if a TideManager is not stored /// @@ -8,6 +8,6 @@ import "TidalYield" /// access(all) fun main(address: Address): [UInt64]? { - return getAccount(address).capabilities.borrow<&TidalYield.TideManager>(TidalYield.TideManagerPublicPath) + return getAccount(address).capabilities.borrow<&Tidal.TideManager>(Tidal.TideManagerPublicPath) ?.getIDs() } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index ad85c34d..7a7c48ea 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -94,8 +94,8 @@ access(all) fun deployContracts() { ) Test.expect(err, Test.beNil()) err = Test.deployContract( - name: "TidalYield", - path: "../contracts/TidalYield.cdc", + name: "Tidal", + path: "../contracts/Tidal.cdc", arguments: [] ) Test.expect(err, Test.beNil()) diff --git a/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc index 609c2936..f540c3c9 100644 --- a/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc +++ b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc @@ -1,6 +1,6 @@ -import "TidalYield" +import "Tidal" -/// Adds the provided Strategy type to the TidalYield StrategyFactory as built by the given StrategyComposer type +/// Adds the provided Strategy type to the Tidal StrategyFactory as built by the given StrategyComposer type /// /// @param strategyIdentifier: The Type identifier of the Strategy to add to the StrategyFactory /// @param composerIdentifier: The Type identifier of the StrategyComposer that builds the Strategy Type @@ -10,9 +10,9 @@ transaction(strategyIdentifier: String, composerIdentifier: String, issuerStorag /// The Strategy Type to add to the StrategyFactory let strategyType: Type /// The StrategyComposer that builds the Strategy Type - let composer: @{TidalYield.StrategyComposer} + let composer: @{Tidal.StrategyComposer} /// Authorized reference to the StrategyFactory to which the Strategy Type & StrategyComposer will be added - let factory: auth(Mutate) &TidalYield.StrategyFactory + let factory: auth(Mutate) &Tidal.StrategyFactory prepare(signer: auth(BorrowValue) &Account) { // construct the types @@ -20,13 +20,13 @@ transaction(strategyIdentifier: String, composerIdentifier: String, issuerStorag let composerType = CompositeType(composerIdentifier) ?? panic("Invalid StrategyComposer type \(composerIdentifier)") // borrow reference to StrategyComposerIssuer & create the StategyComposer - let issuer = signer.storage.borrow<&{TidalYield.StrategyComposerIssuer}>(from: issuerStoragePath) + let issuer = signer.storage.borrow<&{Tidal.StrategyComposerIssuer}>(from: issuerStoragePath) ?? panic("Could not borrow reference to StrategyComposerIssuer from \(issuerStoragePath)") self.composer <- issuer.issueComposer(composerType) // assign StrategyFactory - self.factory = signer.storage.borrow(from: TidalYield.FactoryStoragePath) - ?? panic("Could not borrow reference to StrategyFactory from \(TidalYield.FactoryStoragePath)") + self.factory = signer.storage.borrow(from: Tidal.FactoryStoragePath) + ?? panic("Could not borrow reference to StrategyFactory from \(Tidal.FactoryStoragePath)") } execute { diff --git a/cadence/transactions/tidal-yield/close_tide.cdc b/cadence/transactions/tidal-yield/close_tide.cdc index 13cf8d3c..de833c39 100644 --- a/cadence/transactions/tidal-yield/close_tide.cdc +++ b/cadence/transactions/tidal-yield/close_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" /// Withdraws the full balance from an existing Tide stored in the signer's TideManager and closes the Tide. If the /// signer does not yet have a Vault of the withdrawn Type, one is configured. @@ -10,13 +10,13 @@ import "TidalYield" /// @param id: The Tide.id() of the Tide from which the full balance will be withdrawn /// transaction(id: UInt64) { - let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager + let manager: auth(FungibleToken.Withdraw) &Tidal.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal-yield/create_tide.cdc b/cadence/transactions/tidal-yield/create_tide.cdc index 3dc6a4b3..70079edb 100644 --- a/cadence/transactions/tidal-yield/create_tide.cdc +++ b/cadence/transactions/tidal-yield/create_tide.cdc @@ -2,18 +2,18 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" -/// Opens a new Tide in the TidalYield platform, funding the Tide with the specified Vault and amount +/// Opens a new Tide in the Tidal platform, funding the Tide with the specified Vault and amount /// /// @param strategyIdentifier: The Strategy's Type identifier. Must be a Strategy Type that is currently supported by -/// TidalYield. See `TidalYield.getSupportedStrategies()` to get those currently supported. +/// Tidal. See `Tidal.getSupportedStrategies()` to get those currently supported. /// @param vaultIdentifier: The Vault's Type identifier /// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' /// @param amount: The amount to deposit into the new Tide /// transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) { - let manager: &TidalYield.TideManager + let manager: &Tidal.TideManager let strategy: Type let depositVault: @{FungibleToken.Vault} @@ -39,17 +39,17 @@ transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) self.depositVault <- sourceVault.withdraw(amount: amount) // configure the TideManager if needed - if signer.storage.type(at: TidalYield.TideManagerStoragePath) == nil { - signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) - signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) + if signer.storage.type(at: Tidal.TideManagerStoragePath) == nil { + signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) + signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue<&TidalYield.TideManager>( - TidalYield.TideManagerStoragePath + signer.capabilities.storage.issue<&Tidal.TideManager>( + Tidal.TideManagerStoragePath ) } - self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") } execute { diff --git a/cadence/transactions/tidal-yield/deposit_to_tide.cdc b/cadence/transactions/tidal-yield/deposit_to_tide.cdc index c48431c1..eacaa9d6 100644 --- a/cadence/transactions/tidal-yield/deposit_to_tide.cdc +++ b/cadence/transactions/tidal-yield/deposit_to_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" /// Deposits to an existing Tide stored in the signer's TideManager /// @@ -10,13 +10,13 @@ import "TidalYield" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: &TidalYield.TideManager + let manager: &Tidal.TideManager let depositVault: @{FungibleToken.Vault} prepare(signer: auth(BorrowValue) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow<&TidalYield.TideManager>(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow<&Tidal.TideManager>(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/cadence/transactions/tidal-yield/setup.cdc b/cadence/transactions/tidal-yield/setup.cdc index 0cd13ee2..b35bbe01 100644 --- a/cadence/transactions/tidal-yield/setup.cdc +++ b/cadence/transactions/tidal-yield/setup.cdc @@ -2,27 +2,27 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" -/// Configures a TidalYield.TideManager at the canonical path. If one is already configured, the transaction no-ops +/// Configures a Tidal.TideManager at the canonical path. If one is already configured, the transaction no-ops /// transaction { prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { - if signer.storage.type(at: TidalYield.TideManagerStoragePath) == Type<@TidalYield.TideManager>() { + if signer.storage.type(at: Tidal.TideManagerStoragePath) == Type<@Tidal.TideManager>() { return // early return if TideManager is found } // configure the TideManager - signer.storage.save(<-TidalYield.createTideManager(), to: TidalYield.TideManagerStoragePath) - let cap = signer.capabilities.storage.issue<&TidalYield.TideManager>(TidalYield.TideManagerStoragePath) - signer.capabilities.publish(cap, at: TidalYield.TideManagerPublicPath) + signer.storage.save(<-Tidal.createTideManager(), to: Tidal.TideManagerStoragePath) + let cap = signer.capabilities.storage.issue<&Tidal.TideManager>(Tidal.TideManagerStoragePath) + signer.capabilities.publish(cap, at: Tidal.TideManagerPublicPath) // issue an authorized capability for later access via Capability controller if needed (e.g. via HybridCustody) - signer.capabilities.storage.issue(TidalYield.TideManagerStoragePath) + signer.capabilities.storage.issue(Tidal.TideManagerStoragePath) // confirm setup of TideManager at canonical path - let storedType = signer.storage.type(at: TidalYield.TideManagerStoragePath) ?? Type() - if storedType != Type<@TidalYield.TideManager>() { - panic("Setup was unsuccessful - Expected TideManager at \(TidalYield.TideManagerStoragePath) but found \(storedType.identifier)") + let storedType = signer.storage.type(at: Tidal.TideManagerStoragePath) ?? Type() + if storedType != Type<@Tidal.TideManager>() { + panic("Setup was unsuccessful - Expected TideManager at \(Tidal.TideManagerStoragePath) but found \(storedType.identifier)") } } } diff --git a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc index 08cbf13f..3f54e98f 100644 --- a/cadence/transactions/tidal-yield/withdraw_from_tide.cdc +++ b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "ViewResolver" -import "TidalYield" +import "Tidal" /// Withdraws from an existing Tide stored in the signer's TideManager. If the signer does not yet have a Vault of the /// withdrawn Type, one is configured. @@ -11,13 +11,13 @@ import "TidalYield" /// @param amount: The amount to deposit into the new Tide, denominated in the Tide's Vault type /// transaction(id: UInt64, amount: UFix64) { - let manager: auth(FungibleToken.Withdraw) &TidalYield.TideManager + let manager: auth(FungibleToken.Withdraw) &Tidal.TideManager let receiver: &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, StorageCapabilities, PublishCapability) &Account) { // reference the signer's TideManager & underlying Tide - self.manager = signer.storage.borrow(from: TidalYield.TideManagerStoragePath) - ?? panic("Signer does not have a TideManager stored at path \(TidalYield.TideManagerStoragePath) - configure and retry") + self.manager = signer.storage.borrow(from: Tidal.TideManagerStoragePath) + ?? panic("Signer does not have a TideManager stored at path \(Tidal.TideManagerStoragePath) - configure and retry") let tide = self.manager.borrowTide(id: id) ?? panic("Tide with ID \(id) was not found") // get the data for where the vault type is canoncially stored diff --git a/flow.json b/flow.json index ca3f5fdd..00e0455a 100644 --- a/flow.json +++ b/flow.json @@ -62,8 +62,8 @@ "testing": "0000000000000008" } }, - "TidalYield": { - "source": "cadence/contracts/TidalYield.cdc", + "Tidal": { + "source": "cadence/contracts/Tidal.cdc", "aliases": { "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000009" @@ -224,7 +224,7 @@ }, "MockSwapper", "TidalYieldAutoBalancers", - "TidalYield", + "Tidal", "TidalYieldStrategies" ] } From 4923b4cf953c4a86618e4d6a256e8eb2ad65b518 Mon Sep 17 00:00:00 2001 From: Alex Ni <12097569+nialexsan@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:26:10 -0400 Subject: [PATCH 77/80] update tidal protocol with events --- .../internal-dependencies/TidalProtocol.cdc | 3417 +++++++++-------- 1 file changed, 1727 insertions(+), 1690 deletions(-) diff --git a/cadence/contracts/internal-dependencies/TidalProtocol.cdc b/cadence/contracts/internal-dependencies/TidalProtocol.cdc index 5fc75be8..7d9c4873 100644 --- a/cadence/contracts/internal-dependencies/TidalProtocol.cdc +++ b/cadence/contracts/internal-dependencies/TidalProtocol.cdc @@ -10,1694 +10,1731 @@ import "MOET" access(all) contract TidalProtocol { - /// The canonical StoragePath where the primary TidalProtocol Pool is stored - access(all) let PoolStoragePath: StoragePath - /// The canonical StoragePath where the PoolFactory resource is stored - access(all) let PoolFactoryPath: StoragePath - /// The canonical PublicPath where the primary TidalProtocol Pool can be accessed publicly - access(all) let PoolPublicPath: PublicPath - - /* --- PUBLIC METHODS ---- */ - - /// Takes out a TidalProtocol loan with the provided collateral, returning a Position that can be used to manage - /// collateral and borrowed fund flows - /// - /// @param collateral: The collateral used as the basis for a loan. Only certain collateral types are supported, so - /// callers should be sure to check the provided Vault is supported to prevent reversion. - /// @param issuanceSink: The DeFiBlocks Sink connector where the protocol will deposit borrowed funds. If the - /// position becomes overcollateralized, additional funds will be borrowed (to maintain target LTV) and - /// deposited to the provided Sink. - /// @param repaymentSource: An optional DeFiBlocks Source connector from which the protocol will attempt to source - /// borrowed funds in the event of undercollateralization prior to liquidating. If none is provided, the - /// position health will not be actively managed on the down side, meaning liquidation is possible as soon as - /// the loan becomes undercollateralized. - /// - /// @return the Position via which the caller can manage their position - /// - access(all) fun openPosition( - collateral: @{FungibleToken.Vault}, - issuanceSink: {DFB.Sink}, - repaymentSource: {DFB.Source}?, - pushToDrawDownSink: Bool - ): Position { - let pid = self.borrowPool().createPosition( - funds: <-collateral, - issuanceSink: issuanceSink, - repaymentSource: repaymentSource, - pushToDrawDownSink: pushToDrawDownSink - ) - let cap = self.account.capabilities.storage.issue(self.PoolStoragePath) - return Position(id: pid, pool: cap) - } - - /* --- CONSTRUCTS & INTERNAL METHODS ---- */ - - access(all) entitlement EPosition - access(all) entitlement EGovernance - access(all) entitlement EImplementation - - // RESTORED: BalanceSheet and health computation from Dieter's implementation - // A convenience function for computing a health value from effective collateral and debt values. - access(all) fun healthComputation(effectiveCollateral: UFix64, effectiveDebt: UFix64): UFix64 { - var health = 0.0 - - if effectiveCollateral == 0.0 { - health = 0.0 - } else if effectiveDebt == 0.0 { - health = UFix64.max - } else if (effectiveDebt / effectiveCollateral) == 0.0 { - // If debt is so small relative to collateral that division rounds to zero, - // the health is essentially infinite - health = UFix64.max - } else { - health = effectiveCollateral / effectiveDebt - } - - return health - } - - access(all) struct BalanceSheet { - access(all) let effectiveCollateral: UFix64 - access(all) let effectiveDebt: UFix64 - access(all) let health: UFix64 - - init(effectiveCollateral: UFix64, effectiveDebt: UFix64) { - self.effectiveCollateral = effectiveCollateral - self.effectiveDebt = effectiveDebt - self.health = TidalProtocol.healthComputation(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) - } - } - - // A structure used internally to track a position's balance for a particular token. - access(all) struct InternalBalance { - access(all) var direction: BalanceDirection - - // Internally, position balances are tracked using a "scaled balance". The "scaled balance" is the - // actual balance divided by the current interest index for the associated token. This means we don't - // need to update the balance of a position as time passes, even as interest rates change. We only need - // to update the scaled balance when the user deposits or withdraws funds. The interest index - // is a number relatively close to 1.0, so the scaled balance will be roughly of the same order - // of magnitude as the actual balance (thus we can use UFix64 for the scaled balance). - access(all) var scaledBalance: UFix64 - - init() { - self.direction = BalanceDirection.Credit - self.scaledBalance = 0.0 - } - - access(all) fun recordDeposit(amount: UFix64, tokenState: auth(EImplementation) &TokenState) { - if self.direction == BalanceDirection.Credit { - // Depositing into a credit position just increases the balance. - - // To maximize precision, we could convert the scaled balance to a true balance, add the - // deposit amount, and then convert the result back to a scaled balance. However, this will - // only cause problems for very small deposits (fractions of a cent), so we save computational - // cycles by just scaling the deposit amount and adding it directly to the scaled balance. - let scaledDeposit = TidalProtocol.trueBalanceToScaledBalance(trueBalance: amount, - interestIndex: tokenState.creditInterestIndex) - - self.scaledBalance = self.scaledBalance + scaledDeposit - - // Increase the total credit balance for the token - tokenState.updateCreditBalance(amount: Fix64(amount)) - } else { - // When depositing into a debit position, we first need to compute the true balance to see - // if this deposit will flip the position from debit to credit. - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, - interestIndex: tokenState.debitInterestIndex) - - if trueBalance > amount { - // The deposit isn't big enough to clear the debt, so we just decrement the debt. - let updatedBalance = trueBalance - amount - - self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.debitInterestIndex) - - // Decrease the total debit balance for the token - tokenState.updateDebitBalance(amount: -1.0 * Fix64(amount)) - } else { - // The deposit is enough to clear the debt, so we switch to a credit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Credit - self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.creditInterestIndex) - - // Increase the credit balance AND decrease the debit balance - tokenState.updateCreditBalance(amount: Fix64(updatedBalance)) - tokenState.updateDebitBalance(amount: -1.0 * Fix64(trueBalance)) - } - } - } - - access(all) fun recordWithdrawal(amount: UFix64, tokenState: &TokenState) { - if self.direction == BalanceDirection.Debit { - // Withdrawing from a debit position just increases the debt amount. - - // To maximize precision, we could convert the scaled balance to a true balance, subtract the - // withdrawal amount, and then convert the result back to a scaled balance. However, this will - // only cause problems for very small withdrawals (fractions of a cent), so we save computational - // cycles by just scaling the withdrawal amount and subtracting it directly from the scaled balance. - let scaledWithdrawal = TidalProtocol.trueBalanceToScaledBalance(trueBalance: amount, - interestIndex: tokenState.debitInterestIndex) - - self.scaledBalance = self.scaledBalance + scaledWithdrawal - - // Increase the total debit balance for the token - tokenState.updateDebitBalance(amount: Fix64(amount)) - } else { - // When withdrawing from a credit position, we first need to compute the true balance to see - // if this withdrawal will flip the position from credit to debit. - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, - interestIndex: tokenState.creditInterestIndex) - - if trueBalance >= amount { - // The withdrawal isn't big enough to push the position into debt, so we just decrement the - // credit balance. - let updatedBalance = trueBalance - amount - - self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.creditInterestIndex) - - // Decrease the total credit balance for the token - tokenState.updateCreditBalance(amount: -1.0 * Fix64(amount)) - } else { - // The withdrawal is enough to push the position into debt, so we switch to a debit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Debit - self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, - interestIndex: tokenState.debitInterestIndex) - - // Decrease the credit balance AND increase the debit balance - tokenState.updateCreditBalance(amount: -1.0 * Fix64(trueBalance)) - tokenState.updateDebitBalance(amount: Fix64(updatedBalance)) - } - } - } - } - - access(all) entitlement mapping ImplementationUpdates { - EImplementation -> Mutate - EImplementation -> FungibleToken.Withdraw - } - - // RESTORED: InternalPosition as resource per Dieter's design - // This MUST be a resource to properly manage queued deposits - access(all) resource InternalPosition { - access(EImplementation) var targetHealth: UFix64 - access(EImplementation) var minHealth: UFix64 - access(EImplementation) var maxHealth: UFix64 - access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} - access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} - access(mapping ImplementationUpdates) var drawDownSink: {DFB.Sink}? - access(mapping ImplementationUpdates) var topUpSource: {DFB.Source}? - - init() { - self.balances = {} - self.queuedDeposits <- {} - self.targetHealth = 1.3 - self.minHealth = 1.1 - self.maxHealth = 1.5 - self.drawDownSink = nil - self.topUpSource = nil - } - - access(EImplementation) fun setDrawDownSink(_ sink: {DFB.Sink}?) { - pre { - sink?.getSinkType() ?? Type<@MOET.Vault>() == Type<@MOET.Vault>(): - "Invalid Sink provided - Sink \(sink.getType().identifier) must accept MOET" - } - self.drawDownSink = sink - } - - access(EImplementation) fun setTopUpSource(_ source: {DFB.Source}?) { - self.topUpSource = source - } - } - - access(all) struct interface InterestCurve { - access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { - post { - result <= 1.0: "Interest rate can't exceed 100%" - } - } - } - - access(all) struct SimpleInterestCurve: InterestCurve { - access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { - return 0.0 - } - } - - // A multiplication function for interest calcuations. It assumes that both values are very close to 1 - // and represent fixed point numbers with 16 decimal places of precision. - access(all) fun interestMul(_ a: UInt64, _ b: UInt64): UInt64 { - let aScaled = a / 100000000 - let bScaled = b / 100000000 - - return aScaled * bScaled - } - - // Converts a yearly interest rate (as a UFix64) to a per-second multiplication factor - // (stored in a UInt64 as a fixed point number with 16 decimal places). The input to this function will be - // just the relative interest rate (e.g. 0.05 for 5% interest), but the result will be - // the per-second multiplier (e.g. 1.000000000001). - access(all) fun perSecondInterestRate(yearlyRate: UFix64): UInt64 { - // Covert the yearly rate to an integer maintaning the 10^8 multiplier of UFix64. - // We would need to multiply by an additional 10^8 to match the promised multiplier of - // 10^16. HOWEVER, since we are about to divide by 31536000, we can save multiply a factor - // 1000 smaller, and then divide by 31536. - let yearlyScaledValue = UInt64.fromBigEndianBytes(yearlyRate.toBigEndianBytes())! * 100000 - let perSecondScaledValue = (yearlyScaledValue / 31536) + 10000000000000000 - - return perSecondScaledValue - } - - // Updates an interest index to reflect the passage of time. The result is: - // newIndex = oldIndex * perSecondRate^seconds - access(all) fun compoundInterestIndex(oldIndex: UInt64, perSecondRate: UInt64, elapsedSeconds: UFix64): UInt64 { - var result = oldIndex - var current = perSecondRate - var secondsCounter = UInt64(elapsedSeconds) - - while secondsCounter > 0 { - if secondsCounter & 1 == 1 { - result = TidalProtocol.interestMul(result, current) - } - current = TidalProtocol.interestMul(current, current) - secondsCounter = secondsCounter >> 1 - } - - return result - } - - access(all) fun scaledBalanceToTrueBalance(scaledBalance: UFix64, interestIndex: UInt64): UFix64 { - // The interest index is essentially a fixed point number with 16 decimal places, we convert - // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and - // additional 10^8 as required for the UFix64 representation). - let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 - return scaledBalance * indexMultiplier - } - - access(all) fun trueBalanceToScaledBalance(trueBalance: UFix64, interestIndex: UInt64): UFix64 { - // The interest index is essentially a fixed point number with 16 decimal places, we convert - // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and - // additional 10^8 as required for the UFix64 representation). - let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 - return trueBalance / indexMultiplier - } - - access(all) struct TokenState { - access(all) var lastUpdate: UFix64 - access(all) var totalCreditBalance: UFix64 - access(all) var totalDebitBalance: UFix64 - access(all) var creditInterestIndex: UInt64 - access(all) var debitInterestIndex: UInt64 - access(all) var currentCreditRate: UInt64 - access(all) var currentDebitRate: UInt64 - access(all) var interestCurve: {InterestCurve} - - // RESTORED: Deposit rate limiting from Dieter's implementation - access(all) var depositRate: UFix64 - access(all) var depositCapacity: UFix64 - access(all) var depositCapacityCap: UFix64 - - access(all) fun updateCreditBalance(amount: Fix64) { - // temporary cast the credit balance to a signed value so we can add/subtract - let adjustedBalance = Fix64(self.totalCreditBalance) + amount - self.totalCreditBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 - } - - access(all) fun updateDebitBalance(amount: Fix64) { - // temporary cast the debit balance to a signed value so we can add/subtract - let adjustedBalance = Fix64(self.totalDebitBalance) + amount - self.totalDebitBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 - } - - // RESTORED: Enhanced updateInterestIndices with deposit capacity update - access(all) fun updateInterestIndices() { - let currentTime = getCurrentBlock().timestamp - let timeDelta = currentTime - self.lastUpdate - self.creditInterestIndex = TidalProtocol.compoundInterestIndex(oldIndex: self.creditInterestIndex, perSecondRate: self.currentCreditRate, elapsedSeconds: timeDelta) - self.debitInterestIndex = TidalProtocol.compoundInterestIndex(oldIndex: self.debitInterestIndex, perSecondRate: self.currentDebitRate, elapsedSeconds: timeDelta) - self.lastUpdate = currentTime - - // RESTORED: Update deposit capacity based on time - let newDepositCapacity = self.depositCapacity + (self.depositRate * timeDelta) - if newDepositCapacity >= self.depositCapacityCap { - self.depositCapacity = self.depositCapacityCap - } else { - self.depositCapacity = newDepositCapacity - } - } - - // RESTORED: Deposit limit function from Dieter's implementation - access(all) fun depositLimit(): UFix64 { - // Each deposit is limited to 5% of the total deposit capacity - return self.depositCapacity * 0.05 - } - - // RESTORED: Rename to updateForTimeChange to match Dieter's implementation - access(all) fun updateForTimeChange() { - self.updateInterestIndices() - } - - access(all) fun updateInterestRates() { - // If there's no credit balance, we can't calculate a meaningful credit rate - // so we'll just set both rates to zero and return early - if self.totalCreditBalance <= 0.0 { - self.currentCreditRate = 10000000000000000 // 1.0 in fixed point (no interest) - self.currentDebitRate = 10000000000000000 // 1.0 in fixed point (no interest) - return - } - - let debitRate = self.interestCurve.interestRate(creditBalance: self.totalCreditBalance, debitBalance: self.totalDebitBalance) - let debitIncome = self.totalDebitBalance * (1.0 + debitRate) - - // Calculate insurance amount (0.1% of credit balance) - let insuranceAmount = self.totalCreditBalance * 0.001 - - // Calculate credit rate, ensuring we don't have underflows - var creditRate: UFix64 = 0.0 - if debitIncome >= insuranceAmount { - creditRate = ((debitIncome - insuranceAmount) / self.totalCreditBalance) - 1.0 - } else { - // If debit income doesn't cover insurance, we have a negative credit rate - // but since we can't represent negative rates in our model, we'll use 0.0 - creditRate = 0.0 - } - - self.currentCreditRate = TidalProtocol.perSecondInterestRate(yearlyRate: creditRate) - self.currentDebitRate = TidalProtocol.perSecondInterestRate(yearlyRate: debitRate) - } - - // RESTORED: Parameterized init from Dieter's implementation - init(interestCurve: {InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64) { - self.lastUpdate = getCurrentBlock().timestamp - self.totalCreditBalance = 0.0 - self.totalDebitBalance = 0.0 - self.creditInterestIndex = 10000000000000000 - self.debitInterestIndex = 10000000000000000 - self.currentCreditRate = 10000000000000000 - self.currentDebitRate = 10000000000000000 - self.interestCurve = interestCurve - self.depositRate = depositRate - self.depositCapacity = depositCapacityCap - self.depositCapacityCap = depositCapacityCap - } - } - - access(all) resource Pool { - // A simple version number that is incremented whenever one or more interest indices - // are updated. This is used to detect when the interest indices need to be updated in - // InternalPositions. - access(EImplementation) var version: UInt64 - - // Global state for tracking each token - access(self) var globalLedger: {Type: TokenState} - - // Individual user positions - RESTORED as resources per Dieter's design - access(self) var positions: @{UInt64: InternalPosition} - - // The actual reserves of each token - access(self) var reserves: @{Type: {FungibleToken.Vault}} - - // Auto-incrementing position identifier counter - access(self) var nextPositionID: UInt64 - - // The default token type used as the "unit of account" for the pool. - access(self) let defaultToken: Type - - // RESTORED: Price oracle from Dieter's implementation - // A price oracle that will return the price of each token in terms of the default token. - access(self) var priceOracle: {DFB.PriceOracle} - - // RESTORED: Position update queue from Dieter's implementation - access(EImplementation) var positionsNeedingUpdates: [UInt64] - access(self) var positionsProcessedPerCallback: UInt64 - - // RESTORED: Collateral and borrow factors from Dieter's implementation - // These dictionaries determine borrowing limits. Each token has a collateral factor and a - // borrow factor. - // - // When determining the total collateral amount that can be borrowed against, the value of the - // token (as given by the oracle) is multiplied by the collateral factor. So, a token with a - // collateral factor of 0.8 would only allow you to borrow 80% as much as if you had a the same - // value of a token with a collateral factor of 1.0. The total "effective collateral" for a - // position is the value of each token multiplied by its collateral factor. - // - // At the same time, the "borrow factor" determines if the user can borrow against all of that - // effective collateral, or if they can only borrow a portion of it to manage risk. - access(self) var collateralFactor: {Type: UFix64} - access(self) var borrowFactor: {Type: UFix64} - - // REMOVED: Static exchange rates and liquidation thresholds - // These have been replaced by dynamic oracle pricing and risk factors - - // RESTORED: tokenState() helper function from Dieter's implementation - // A convenience function that returns a reference to a particular token state, making sure - // it's up-to-date for the passage of time. This should always be used when accessing a token - // state to avoid missing interest updates (duplicate calls to updateForTimeChange() are a nop - // within a single block). - access(self) fun tokenState(type: Type): auth(EImplementation) &TokenState { - let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState - state.updateForTimeChange() - return state - } - - init(defaultToken: Type, priceOracle: {DFB.PriceOracle}) { - pre { - priceOracle.unitOfAccount() == defaultToken: "Price oracle must return prices in terms of the default token" - } - - self.version = 0 - self.globalLedger = {defaultToken: TokenState( - interestCurve: SimpleInterestCurve(), - depositRate: 1000000.0, // Default: no rate limiting for default token - depositCapacityCap: 1000000.0 // Default: high capacity cap - )} - self.positions <- {} - self.reserves <- {} - self.defaultToken = defaultToken - self.priceOracle = priceOracle - self.collateralFactor = {defaultToken: 1.0} - self.borrowFactor = {defaultToken: 1.0} - self.nextPositionID = 0 - self.positionsNeedingUpdates = [] - self.positionsProcessedPerCallback = 100 - - // CHANGE: Don't create vault here - let the caller provide initial reserves - // The pool starts with empty reserves map - // Vaults will be added when tokens are first deposited - } - - // Add a new token type to the pool - // This function should only be called by governance in the future - access(EGovernance) fun addSupportedToken( - tokenType: Type, - collateralFactor: UFix64, - borrowFactor: UFix64, - interestCurve: {InterestCurve}, - depositRate: UFix64, - depositCapacityCap: UFix64 - ) { - pre { - self.globalLedger[tokenType] == nil: "Token type already supported" - tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): - "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" - collateralFactor > 0.0 && collateralFactor <= 1.0: "Collateral factor must be between 0 and 1" - borrowFactor > 0.0 && borrowFactor <= 1.0: "Borrow factor must be between 0 and 1" - depositRate > 0.0: "Deposit rate must be positive" - depositCapacityCap > 0.0: "Deposit capacity cap must be positive" - DFBUtils.definingContractIsFungibleToken(tokenType): - "Invalid token contract definition for tokenType \(tokenType.identifier) - defining contract is not FungibleToken conformant" - } - - // Add token to global ledger with its interest curve and deposit parameters - self.globalLedger[tokenType] = TokenState( - interestCurve: interestCurve, - depositRate: depositRate, - depositCapacityCap: depositCapacityCap - ) - - // Set collateral factor (what percentage of value can be used as collateral) - self.collateralFactor[tokenType] = collateralFactor - - // Set borrow factor (risk adjustment for borrowed amounts) - self.borrowFactor[tokenType] = borrowFactor - } - - // Get supported token types - access(all) fun getSupportedTokens(): [Type] { - return self.globalLedger.keys - } - - // Check if a token type is supported - access(all) fun isTokenSupported(tokenType: Type): Bool { - return self.globalLedger[tokenType] != nil - } - - // RESTORED: Public deposit function from Dieter's implementation - // Allows anyone to deposit funds into any position - access(all) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { - self.depositAndPush(pid: pid, from: <-from, pushToDrawDownSink: false) - } - - // RESTORED: Enhanced deposit with queue processing and rebalancing from Dieter's implementation - access(EPosition) fun depositAndPush(pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { - pre { - self.positions[pid] != nil: "Invalid position ID" - self.globalLedger[from.getType()] != nil: "Invalid token type" - } - - if from.balance == 0.0 { - Burner.burn(<-from) - return - } - - // Get a reference to the user's position and global token state for the affected token. - let type = from.getType() - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) - - // Update time-based state - // REMOVED: This is now handled by tokenState() helper function - // tokenState.updateForTimeChange() - - // RESTORED: Deposit rate limiting from Dieter's implementation - let depositAmount = from.balance - let depositLimit = tokenState.depositLimit() - - if depositAmount > depositLimit { - // The deposit is too big, so we need to queue the excess - let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) - - if position.queuedDeposits[type] == nil { - position.queuedDeposits[type] <-! queuedDeposit - } else { - position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) - } - } - - // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance() - } - - // CHANGE: Create vault if it doesn't exist yet - if self.reserves[type] == nil { - self.reserves[type] <-! from.createEmptyVault() - } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Reflect the deposit in the position's balance - position.balances[type]!.recordDeposit(amount: from.balance, tokenState: tokenState) - - // Add the money to the reserves - reserveVault.deposit(from: <-from) - - // RESTORED: Rebalancing and queue management - if pushToDrawDownSink { - self.rebalancePosition(pid: pid, force: true) - } - - self.queuePositionForUpdateIfNecessary(pid: pid) - } - - access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { - // RESTORED: Call the enhanced function with pullFromTopUpSource = false for backward compatibility - return <- self.withdrawAndPull(pid: pid, type: type, amount: amount, pullFromTopUpSource: false) - } - - // RESTORED: Enhanced withdraw with top-up source integration from Dieter's implementation - access(EPosition) fun withdrawAndPull( - pid: UInt64, - type: Type, - amount: UFix64, - pullFromTopUpSource: Bool - ): @{FungibleToken.Vault} { - pre { - self.positions[pid] != nil: "Invalid position ID" - self.globalLedger[type] != nil: "Invalid token type" - } - if amount == 0.0 { - return <- DFBUtils.getEmptyVault(type) - } - - // Get a reference to the user's position and global token state for the affected token. - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) - - // Update the global interest indices on the affected token to reflect the passage of time. - // REMOVED: This is now handled by tokenState() helper function - // tokenState.updateForTimeChange() - - // RESTORED: Top-up source integration from Dieter's implementation - // Preflight to see if the funds are available - let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DFB.Source}? - let topUpType = topUpSource?.getSourceType() ?? self.defaultToken - - let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( - pid: pid, - depositType: topUpType, - targetHealth: position.minHealth, - withdrawType: type, - withdrawAmount: amount - ) - - var canWithdraw = false - - if requiredDeposit == 0.0 { - // We can service this withdrawal without any top up - canWithdraw = true - } else { - // We need more funds to service this withdrawal, see if they are available from the top up source - if pullFromTopUpSource && topUpSource != nil { - // If we have to rebalance, let's try to rebalance to the target health, not just the minimum - let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( - pid: pid, - depositType: topUpType, - targetHealth: position.targetHealth, - withdrawType: type, - withdrawAmount: amount - ) - - let pulledVault <- topUpSource!.withdrawAvailable(maxAmount: idealDeposit) - - // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here. - // The top up source may not have enough funds get us to the target health, but could have - // enough to keep us over the minimum. - if pulledVault.balance >= requiredDeposit { - // We can service this withdrawal if we deposit funds from our top up source - self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) - canWithdraw = true - } else { - // We can't get the funds required to service this withdrawal, so we need to redeposit what we got - self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) - } - } - } - - if !canWithdraw { - // We can't service this withdrawal, so we just abort - panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal") - } - - // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance() - } - - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Reflect the withdrawal in the position's balance - position.balances[type]!.recordWithdrawal(amount: amount, tokenState: tokenState) - - // Ensure that this withdrawal doesn't cause the position to be overdrawn. - assert(self.positionHealth(pid: pid) >= 1.0, message: "Position is overdrawn") - - // Queue for update if necessary - self.queuePositionForUpdateIfNecessary(pid: pid) - - return <- reserveVault.withdraw(amount: amount) - } - - // RESTORED: Position queue management from Dieter's implementation - access(self) fun queuePositionForUpdateIfNecessary(pid: UInt64) { - if self.positionsNeedingUpdates.contains(pid) { - // If this position is already queued for an update, no need to check anything else - return - } else { - // If this position is not already queued for an update, we need to check if it needs one - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - - if position.queuedDeposits.length > 0 { - // This position has deposits that need to be processed, so we need to queue it for an update - self.positionsNeedingUpdates.append(pid) - return - } - - let positionHealth = self.positionHealth(pid: pid) - - if positionHealth < position.minHealth || positionHealth > position.maxHealth { - // This position is outside the configured health bounds, we queue it for an update - self.positionsNeedingUpdates.append(pid) - return - } - } - } - - // RESTORED: Position rebalancing from Dieter's implementation - // Rebalances the position to the target health value. If force is true, the position will be - // rebalanced even if it is currently healthy, otherwise, this function will do nothing if the - // position is within the min/max health bounds. - access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let balanceSheet = self.positionBalanceSheet(pid: pid) - - if !force && (balanceSheet.health >= position.minHealth && balanceSheet.health <= position.maxHealth) { - // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! - return - } - - if balanceSheet.health < position.targetHealth { - // The position is undercollateralized, see if the source can get more collateral to bring it up to the target health. - if position.topUpSource != nil { - let topUpSource = position.topUpSource! as auth(FungibleToken.Withdraw) &{DFB.Source} - let idealDeposit = self.fundsRequiredForTargetHealth( - pid: pid, - type: topUpSource.getSourceType(), - targetHealth: position.targetHealth - ) - - let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) - self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) - } - } else if balanceSheet.health > position.targetHealth { - // The position is overcollateralized, we'll withdraw funds to match the target health and offer it to the sink. - if position.drawDownSink != nil { - let drawDownSink = position.drawDownSink! - let sinkType = drawDownSink.getSinkType() - let idealWithdrawal = self.fundsAvailableAboveTargetHealth( - pid: pid, - type: sinkType, - targetHealth: position.targetHealth - ) - - // Compute how many tokens of the sink's type are available to hit our target health. - let sinkCapacity = drawDownSink.minimumCapacity() - let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal - - if sinkAmount > 0.0 && sinkType == self.defaultToken { // second conditional included for sake of tracer bullet - // BUG: Calling through to withdrawAndPull results in an insufficient funds from the position's - // topUpSource. These funds should come from the protocol or reserves, not from the user's - // funds. To unblock here, we just mint MOET when a position is overcollateralized - // let sinkVault <- self.withdrawAndPull( - // pid: pid, - // type: sinkType, - // amount: sinkAmount, - // pullFromTopUpSource: false - // ) - - let tokenState = self.tokenState(type: self.defaultToken) - if position.balances[self.defaultToken] == nil { - position.balances[self.defaultToken] = InternalBalance() - } - position.balances[self.defaultToken]!.recordWithdrawal(amount: sinkAmount, tokenState: tokenState) - let sinkVault <- TidalProtocol.borrowMOETMinter().mintTokens(amount: sinkAmount) - // Push what we can into the sink, and redeposit the rest - drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - - if sinkVault.balance > 0.0 { - self.depositAndPush(pid: pid, from: <-sinkVault, pushToDrawDownSink: false) - } else { - Burner.burn(<-sinkVault) - } - } - } - } - } - - // RESTORED: Provider functions for sink/source from Dieter's implementation - access(EPosition) fun provideDrawDownSink(pid: UInt64, sink: {DFB.Sink}?) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - position.setDrawDownSink(sink) - } - - access(EPosition) fun provideTopUpSource(pid: UInt64, source: {DFB.Source}?) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - position.setTopUpSource(source) - } - - // RESTORED: Available balance with source integration from Dieter's implementation - access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - - if pullFromTopUpSource && position.topUpSource != nil { - let topUpSource = position.topUpSource! - let sourceType = topUpSource.getSourceType() - let sourceAmount = topUpSource.minimumAvailable() - - return self.fundsAvailableAboveTargetHealthAfterDepositing( - pid: pid, - withdrawType: type, - targetHealth: position.minHealth, - depositType: sourceType, - depositAmount: sourceAmount - ) - } else { - return self.fundsAvailableAboveTargetHealth( - pid: pid, - type: type, - targetHealth: position.minHealth - ) - } - } - - // Returns the health of the given position, which is the ratio of the position's effective collateral - // to its debt (as denominated in the default token). ("Effective collateral" means the - // value of each credit balance times the liquidation threshold for that token. i.e. the maximum borrowable amount) - access(all) fun positionHealth(pid: UInt64): UFix64 { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - - // Get the position's collateral and debt values in terms of the default token. - var effectiveCollateral = 0.0 - var effectiveDebt = 0.0 - - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = self.tokenState(type: type) - if balance.direction == BalanceDirection.Credit { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex) - - // RESTORED: Oracle-based pricing from Dieter's implementation - let tokenPrice = self.priceOracle.price(ofToken: type)! - let value = tokenPrice * trueBalance - effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) - } else { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex) - - // RESTORED: Oracle-based pricing for debt calculation - let tokenPrice = self.priceOracle.price(ofToken: type)! - let value = tokenPrice * trueBalance - effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) - } - } - - // Calculate the health as the ratio of collateral to debt. - if effectiveDebt == 0.0 { - return 1.0 - } - return effectiveCollateral / effectiveDebt - } - - // RESTORED: Position balance sheet calculation from Dieter's implementation - access(self) fun positionBalanceSheet(pid: UInt64): BalanceSheet { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let priceOracle = &self.priceOracle as &{DFB.PriceOracle} - - // Get the position's collateral and debt values in terms of the default token. - var effectiveCollateral = 0.0 - var effectiveDebt = 0.0 - - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = self.tokenState(type: type) - if balance.direction == BalanceDirection.Credit { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex) - - let value = priceOracle.price(ofToken: type)! * trueBalance - - effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) - } else { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex) - - let value = priceOracle.price(ofToken: type)! * trueBalance - - effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) - } - } - - return BalanceSheet(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) - } - - /// Creates a lending position against the provided collateral funds, depositing the loaned amount to the - /// given Sink. If a Source is provided, the position will be configured to pull loan repayment when the loan - /// becomes undercollateralized, preferring repayment to outright liquidation. - access(all) fun createPosition( - funds: @{FungibleToken.Vault}, - issuanceSink: {DFB.Sink}, - repaymentSource: {DFB.Source}?, - pushToDrawDownSink: Bool - ): UInt64 { - pre { - self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier)" - } - // construct a new InternalPosition, assigning it the current position ID - let id = self.nextPositionID - self.nextPositionID = self.nextPositionID + 1 - self.positions[id] <-! create InternalPosition() - - - // assign issuance & repayment connectors within the InternalPosition - let iPos = (&self.positions[id] as auth(EImplementation) &InternalPosition?)! - let fundsType = funds.getType() - iPos.setDrawDownSink(issuanceSink) - if repaymentSource != nil { - iPos.setTopUpSource(repaymentSource) - } - - // deposit the initial funds & return the position ID - self.depositAndPush( - pid: id, - from: <-funds, - pushToDrawDownSink: pushToDrawDownSink - ) - return id - } - - // Helper function for testing – returns the current reserve balance for the specified token type. - access(all) fun reserveBalance(type: Type): UFix64 { - // CHANGE: Handle case where no vault exists yet for this token type - let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) - if vaultRef == nil { - return 0.0 - } - return vaultRef!.balance - } - - // Add getPositionDetails function that's used by DFB implementations - access(all) fun getPositionDetails(pid: UInt64): PositionDetails { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let balances: [PositionBalance] = [] - - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = self.tokenState(type: type) - - let trueBalance = balance.direction == BalanceDirection.Credit - ? TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.creditInterestIndex) - : TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.debitInterestIndex) - - balances.append(PositionBalance( - type: type, - direction: balance.direction, - balance: trueBalance - )) - } - - let health = self.positionHealth(pid: pid) - let defaultTokenAvailable = self.availableBalance(pid: pid, type: self.defaultToken, pullFromTopUpSource: false) - - return PositionDetails( - balances: balances, - poolDefaultToken: self.defaultToken, - defaultTokenAvailableBalance: defaultTokenAvailable, - health: health - ) - } - - // RESTORED: Advanced position health management functions from Dieter's implementation - - // The quantity of funds of a specified token which would need to be deposited to bring the - // position to the target health. This function will return 0.0 if the position is already at or over - // that health value. - access(all) fun fundsRequiredForTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { - return self.fundsRequiredForTargetHealthAfterWithdrawing( - pid: pid, - depositType: type, - targetHealth: targetHealth, - withdrawType: self.defaultToken, - withdrawAmount: 0.0 - ) - } - - // The quantity of funds of a specified token which would need to be deposited to bring the - // position to the target health assuming we also withdraw a specified amount of another - // token. This function will return 0.0 if the position would already be at or over the target - // health value after the proposed withdrawal. - access(all) fun fundsRequiredForTargetHealthAfterWithdrawing( - pid: UInt64, - depositType: Type, - targetHealth: UFix64, - withdrawType: Type, - withdrawAmount: UFix64 - ): UFix64 { - if depositType == withdrawType && withdrawAmount > 0.0 { - // If the deposit and withdrawal types are the same, we compute the required deposit assuming - // no withdrawal (which is less work) and increase that by the withdraw amount at the end - return self.fundsRequiredForTargetHealth(pid: pid, type: depositType, targetHealth: targetHealth) + withdrawAmount - } - - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - - var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt - - - if withdrawAmount != 0.0 { - if position.balances[withdrawType] == nil || position.balances[withdrawType]!.direction == BalanceDirection.Debit { - // If the position doesn't have any collateral for the withdrawn token, we can just compute how much - // additional effective debt the withdrawal will create. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) - } else { - let withdrawTokenState = self.tokenState(type: withdrawType) - // REMOVED: This is now handled by tokenState() helper function - // withdrawTokenState.updateForTimeChange() - - // The user has a collateral position in the given token, we need to figure out if this withdrawal - // will flip over into debt, or just draw down the collateral. - let collateralBalance = position.balances[withdrawType]!.scaledBalance - let trueCollateral = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: collateralBalance, - interestIndex: withdrawTokenState.creditInterestIndex - ) - - if trueCollateral >= withdrawAmount { - // This withdrawal will draw down collateral, but won't create debt, we just need to account - // for the collateral decrease. - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) - } else { - // The withdrawal will wipe out all of the collateral, and create some debt. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - ((withdrawAmount - trueCollateral) * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) - - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (trueCollateral * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) - } - } - } - - // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) - // Now we can figure out how many of the given token would need to be deposited to bring the position - // to the target health value. - var healthAfterWithdrawal = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal - ) - - if healthAfterWithdrawal >= targetHealth { - // The position is already at or above the target health, so we don't need to deposit anything. - return 0.0 - } - - // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep - // track of the number of tokens that went towards paying off debt. - var debtTokenCount = 0.0 - - if position.balances[depositType] != nil && position.balances[depositType]!.direction == BalanceDirection.Debit { - // The user has a debt position in the given token, we start by looking at the health impact of paying off - // the entire debt. - let depositTokenState = self.tokenState(type: depositType) - // REMOVED: This is now handled by tokenState() helper function - // depositTokenState.updateForTimeChange() - - let debtBalance = position.balances[depositType]!.scaledBalance - let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: debtBalance, - interestIndex: depositTokenState.debitInterestIndex - ) - let debtEffectiveValue = self.priceOracle.price(ofToken: depositType)! * trueDebt / self.borrowFactor[depositType]! - - // Check what the new health would be if we paid off all of this debt - let potentialHealth = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal - debtEffectiveValue - ) - - // Does paying off all of the debt reach the target health? Then we're done. - if potentialHealth >= targetHealth { - // We can reach the target health by paying off some or all of the debt. We can easily - // compute how many units of the token would be needed to reach the target health. - let healthChange = targetHealth - healthAfterWithdrawal - let requiredEffectiveDebt = healthChange * effectiveCollateralAfterWithdrawal / (targetHealth * targetHealth) - - // The amount of the token to pay back, in units of the token. - let paybackAmount = requiredEffectiveDebt * self.borrowFactor[depositType]! / self.priceOracle.price(ofToken: depositType)! - - return paybackAmount - } else { - // We can pay off the entire debt, but we still need to deposit more to reach the target health. - // We have logic below that can determine the collateral deposition required to reach the target health - // from this new health position. Rather than copy that logic here, we fall through into it. But first - // we have to record the amount of tokens that went towards debt payback and adjust the effective - // debt to reflect that it has been paid off. - debtTokenCount = trueDebt - effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue - healthAfterWithdrawal = potentialHealth - } - } - - // At this point, we're either dealing with a position that didn't have a debt position in the deposit - // token, or we've accounted for the debt payoff and adjusted the effective debt above. - - // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the - // target health. We can rearrange the health equation to solve for the required collateral: - // targetHealth = effectiveCollateral / effectiveDebt - // targetHealth * effectiveDebt = effectiveCollateral - // requiredCollateral = targetHealth * effectiveDebtAfterWithdrawal - - // We need to increase the effective collateral from its current value to the required value, so we - // multiply the required health change by the effective debt, and turn that into a token amount. - let healthChange = targetHealth - healthAfterWithdrawal - let requiredEffectiveCollateral = healthChange * effectiveDebtAfterWithdrawal - - // The amount of the token to deposit, in units of the token. - let collateralTokenCount = requiredEffectiveCollateral / self.priceOracle.price(ofToken: depositType)! / self.collateralFactor[depositType]! - - // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. - return collateralTokenCount + debtTokenCount - } - - // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health - // at or above the provided target. - access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { - return self.fundsAvailableAboveTargetHealthAfterDepositing( - pid: pid, - withdrawType: type, - targetHealth: targetHealth, - depositType: self.defaultToken, - depositAmount: 0.0 - ) - } - - // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health - // at or above the provided target, assuming we also deposit a specified amount of another token. - access(all) fun fundsAvailableAboveTargetHealthAfterDepositing( - pid: UInt64, - withdrawType: Type, - targetHealth: UFix64, - depositType: Type, - depositAmount: UFix64 - ): UFix64 { - if depositType == withdrawType && depositAmount > 0.0 { - // If the deposit and withdrawal types are the same, we compute the available funds assuming - // no deposit (which is less work) and increase that by the deposit amount at the end - return self.fundsAvailableAboveTargetHealth(pid: pid, type: withdrawType, targetHealth: targetHealth) + depositAmount - } - - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - - var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral - var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - if depositAmount != 0.0 { - if position.balances[depositType] == nil || position.balances[depositType]!.direction == BalanceDirection.Credit { - // If there's no debt for the deposit token, we can just compute how much additional effective collateral the deposit will create. - effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - (depositAmount * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) - } else { - let depositTokenState = self.tokenState(type: depositType) - - // The user has a debt position in the given token, we need to figure out if this deposit - // will result in net collateral, or just bring down the debt. - let debtBalance = position.balances[depositType]!.scaledBalance - let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: debtBalance, - interestIndex: depositTokenState.debitInterestIndex - ) - - if trueDebt >= depositAmount { - // This deposit will pay down some debt, but won't result in net collateral, we - // just need to account for the debt decrease. - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (depositAmount * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) - } else { - // The deposit will wipe out all of the debt, and create some collateral. - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (trueDebt * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) - - effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - ((depositAmount - trueDebt) * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) - } - } - } - - // We now have new effective collateral and debt values that reflect the proposed deposit (if any!) - // Now we can figure out how many of the withdrawal token are available while keeping the position - // at or above the target health value. - var healthAfterDeposit = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit - ) - - if healthAfterDeposit <= targetHealth { - // The position is already at or below the target health, so we can't withdraw anything. - return 0.0 - } - - // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep - // track of the number of tokens that are available from collateral - var collateralTokenCount = 0.0 - - if position.balances[withdrawType] != nil && position.balances[withdrawType]!.direction == BalanceDirection.Credit { - // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all - // of that collateral - let withdrawTokenState = self.tokenState(type: withdrawType) - // REMOVED: This is now handled by tokenState() helper function - // withdrawTokenState.updateForTimeChange() - - let creditBalance = position.balances[withdrawType]!.scaledBalance - let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: creditBalance, - interestIndex: withdrawTokenState.creditInterestIndex - ) - let collateralEffectiveValue = self.priceOracle.price(ofToken: withdrawType)! * trueCredit * self.collateralFactor[withdrawType]! - - // Check what the new health would be if we took out all of this collateral - let potentialHealth = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, - effectiveDebt: effectiveDebtAfterDeposit - ) - - // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. - if potentialHealth <= targetHealth { - // We will hit the health target before using up all of the withdraw token credit. We can easily - // compute how many units of the token would bring the position down to the target health. - let availableHealth = healthAfterDeposit - targetHealth - let availableEffectiveValue = availableHealth * effectiveDebtAfterDeposit - - // The amount of the token we can take using that amount of health - let availableTokenCount = availableEffectiveValue / self.collateralFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! - - return availableTokenCount - } else { - // We can flip this credit position into a debit position, before hitting the target health. - // We have logic below that can determine health changes for debit positions. Rather than copy that here, - // fall through into it. But first we have to record the amount of tokens that are available as collateral - // and then adjust the effective collateral to reflect that it has come out - collateralTokenCount = trueCredit - effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue - // NOTE: The above invalidates the healthAfterDeposit value, but it's not used below... - } - } - - // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw - // token, or we've accounted for the credit balance and adjusted the effective collateral above. - - // We can calculate the available debt increase that would bring us to the target health - var availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit - - let availableTokens = availableDebtIncrease * self.borrowFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! - - return availableTokens + collateralTokenCount - } - - // Returns the health the position would have if the given amount of the specified token were deposited. - access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix64 { - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) - - var effectiveCollateralIncrease = 0.0 - var effectiveDebtDecrease = 0.0 - - if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Credit { - // Since the user has no debt in the given token, we can just compute how much - // additional collateral this deposit will create. - effectiveCollateralIncrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! - } else { - // The user has a debit position in the given token, we need to figure out if this deposit - // will only pay off some of the debt, or if it will also create new collateral. - let debtBalance = position.balances[type]!.scaledBalance - let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: debtBalance, - interestIndex: tokenState.debitInterestIndex - ) - - if trueDebt >= amount { - // This deposit will wipe out some or all of the debt, but won't create new collateral, we - // just need to account for the debt decrease. - effectiveDebtDecrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - } else { - // This deposit will wipe out all of the debt, and create new collateral. - effectiveDebtDecrease = trueDebt * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - effectiveCollateralIncrease = (amount - trueDebt) * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! - } - } - - return TidalProtocol.healthComputation( - effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, - effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease - ) - } - - // Returns health value of this position if the given amount of the specified token were withdrawn without - // using the top up source. - // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates - // that the proposed withdrawal would fail (unless a top up source is available and used). - access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix64 { - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) - - var effectiveCollateralDecrease = 0.0 - var effectiveDebtIncrease = 0.0 - - if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Debit { - // The user has no credit position in the given token, we can just compute how much - // additional effective debt this withdrawal will create. - effectiveDebtIncrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - } else { - // The user has a credit position in the given token, we need to figure out if this withdrawal - // will only draw down some of the collateral, or if it will also create new debt. - let creditBalance = position.balances[type]!.scaledBalance - let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: creditBalance, - interestIndex: tokenState.creditInterestIndex - ) - - if trueCredit >= amount { - // This withdrawal will draw down some collateral, but won't create new debt, we - // just need to account for the collateral decrease. - effectiveCollateralDecrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! - } else { - // The withdrawal will wipe out all of the collateral, and create new debt. - effectiveDebtIncrease = (amount - trueCredit) * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - effectiveCollateralDecrease = trueCredit * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! - } - } - - return TidalProtocol.healthComputation( - effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, - effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease - ) - } - - // RESTORED: Async update infrastructure from Dieter's implementation - access(EImplementation) fun asyncUpdate() { - // TODO: In the production version, this function should only process some positions (limited by positionsProcessedPerCallback) AND - // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or - // sink aborts) won't prevent other positions from being updated. - var processed: UInt64 = 0 - while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { - let pid = self.positionsNeedingUpdates.removeFirst() - self.asyncUpdatePosition(pid: pid) - self.queuePositionForUpdateIfNecessary(pid: pid) - processed = processed + 1 - } - } - - // RESTORED: Async position update from Dieter's implementation - access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - - // First check queued deposits, their addition could affect the rebalance we attempt later - for depositType in position.queuedDeposits.keys { - let queuedVault <- position.queuedDeposits.remove(key: depositType)! - let queuedAmount = queuedVault.balance - let depositTokenState = self.tokenState(type: depositType) - - let maxDeposit = depositTokenState.depositLimit() - - if maxDeposit >= queuedAmount { - // We can deposit all of the queued deposit, so just do it and remove it from the queue - self.depositAndPush(pid: pid, from: <-queuedVault, pushToDrawDownSink: false) - } else { - // We can only deposit part of the queued deposit, so do that and leave the rest in the queue - // for the next time we run. - let depositVault <- queuedVault.withdraw(amount: maxDeposit) - self.depositAndPush(pid: pid, from: <-depositVault, pushToDrawDownSink: false) - - // We need to update the queued vault to reflect the amount we used up - position.queuedDeposits[depositType] <-! queuedVault - } - } - - // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance - // the position if necessary. - self.rebalancePosition(pid: pid, force: false) - } - } - - /// Resource enabling the contract account to create a Pool. This pattern is used in place of contract methods to - /// ensure limited access to pool creation. While this could be done in contract's init, doing so here will allow - /// for the setting of the Pool's PriceOracle without the introduction of a concrete PriceOracle defining contract - /// which would include an external contract dependency. - /// - access(all) resource PoolFactory { - /// Creates a Pool and saves it to the canonical path, reverting if one is already stored - access(all) fun createPool(defaultToken: Type, priceOracle: {DFB.PriceOracle}) { - pre { - TidalProtocol.account.storage.type(at: TidalProtocol.PoolStoragePath) == nil: - "Storage collision - Pool has already been created & saved to \(TidalProtocol.PoolStoragePath)" - } - let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle) - TidalProtocol.account.storage.save(<-pool, to: TidalProtocol.PoolStoragePath) - let cap = TidalProtocol.account.capabilities.storage.issue<&Pool>(TidalProtocol.PoolStoragePath) - TidalProtocol.account.capabilities.unpublish(TidalProtocol.PoolPublicPath) - TidalProtocol.account.capabilities.publish(cap, at: TidalProtocol.PoolPublicPath) - } - } - - // TODO: Consider making this a resource given how critical it is to accessing a loan - access(all) struct Position { - access(self) let id: UInt64 - access(self) let pool: Capability - - init(id: UInt64, pool: Capability) { - pre { - pool.check(): "Invalid Pool Capability provided - cannot construct Position" - } - self.id = id - self.pool = pool - } - - // Returns the balances (both positive and negative) for all tokens in this position. - access(all) fun getBalances(): [PositionBalance] { - let pool = self.pool.borrow()! - return pool.getPositionDetails(pid: self.id).balances - } - - // RESTORED: Enhanced available balance from Dieter's implementation - access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 { - let pool = self.pool.borrow()! - return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource) - } - - // RESTORED: Health functions from Dieter's implementation - access(all) fun getHealth(): UFix64 { - let pool = self.pool.borrow()! - return pool.positionHealth(pid: self.id) - } - - access(all) fun getTargetHealth(): UFix64 { - // DIETER'S DESIGN: Position is just a relay struct, return 0.0 - return 0.0 - } - - access(all) fun setTargetHealth(targetHealth: UFix64) { - // DIETER'S DESIGN: Position is just a relay struct, do nothing - } - - access(all) fun getMinHealth(): UFix64 { - // DIETER'S DESIGN: Position is just a relay struct, return 0.0 - return 0.0 - } - - access(all) fun setMinHealth(minHealth: UFix64) { - // DIETER'S DESIGN: Position is just a relay struct, do nothing - } - - access(all) fun getMaxHealth(): UFix64 { - // DIETER'S DESIGN: Position is just a relay struct, return 0.0 - return 0.0 - } - - access(all) fun setMaxHealth(maxHealth: UFix64) { - // DIETER'S DESIGN: Position is just a relay struct, do nothing - } - - // Returns the maximum amount of the given token type that could be deposited into this position. - access(all) fun getDepositCapacity(type: Type): UFix64 { - // There's no limit on deposits from the position's perspective - return UFix64.max - } - - // RESTORED: Simple deposit that calls depositAndPush with pushToDrawDownSink = false - access(all) fun deposit(from: @{FungibleToken.Vault}) { - let pool = self.pool.borrow()! - pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: false) - } - - // RESTORED: Enhanced deposit from Dieter's implementation - access(all) fun depositAndPush(from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { - let pool = self.pool.borrow()! - pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: pushToDrawDownSink) - } - - // RESTORED: Simple withdraw that calls withdrawAndPull with pullFromTopUpSource = false - access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} { - return <- self.withdrawAndPull(type: type, amount: amount, pullFromTopUpSource: false) - } - - // RESTORED: Enhanced withdraw from Dieter's implementation - access(FungibleToken.Withdraw) fun withdrawAndPull(type: Type, amount: UFix64, pullFromTopUpSource: Bool): @{FungibleToken.Vault} { - let pool = self.pool.borrow()! - return <- pool.withdrawAndPull(pid: self.id, type: type, amount: amount, pullFromTopUpSource: pullFromTopUpSource) - } - - // Returns a NEW sink for the given token type that will accept deposits of that token and - // update the position's collateral and/or debt accordingly. Note that calling this method multiple - // times will create multiple sinks, each of which will continue to work regardless of how many - // other sinks have been created. - access(all) fun createSink(type: Type): {DFB.Sink} { - // RESTORED: Create enhanced sink with pushToDrawDownSink option - return self.createSinkWithOptions(type: type, pushToDrawDownSink: false) - } - - // RESTORED: Enhanced sink creation from Dieter's implementation - access(all) fun createSinkWithOptions(type: Type, pushToDrawDownSink: Bool): {DFB.Sink} { - let pool = self.pool.borrow()! - return PositionSink(id: self.id, pool: self.pool, type: type, pushToDrawDownSink: pushToDrawDownSink) - } - - // Returns a NEW source for the given token type that will service withdrawals of that token and - // update the position's collateral and/or debt accordingly. Note that calling this method multiple - // times will create multiple sources, each of which will continue to work regardless of how many - // other sources have been created. - access(FungibleToken.Withdraw) fun createSource(type: Type): {DFB.Source} { - // RESTORED: Create enhanced source with pullFromTopUpSource option - return self.createSourceWithOptions(type: type, pullFromTopUpSource: false) - } - - // RESTORED: Enhanced source creation from Dieter's implementation - access(FungibleToken.Withdraw) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DFB.Source} { - let pool = self.pool.borrow()! - return PositionSource(id: self.id, pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource) - } - - // RESTORED: Provider functions implementation from Dieter's design - // Provides a sink to the Position that will have tokens proactively pushed into it when the - // position has excess collateral. (Remember that sinks do NOT have to accept all tokens provided - // to them; the sink can choose to accept only some (or none) of the tokens provided, leaving the position - // overcollateralized.) - // - // Each position can have only one sink, and the sink must accept the default token type - // configured for the pool. Providing a new sink will replace the existing sink. Pass nil - // to configure the position to not push tokens. - access(FungibleToken.Withdraw) fun provideSink(sink: {DFB.Sink}?) { - let pool = self.pool.borrow()! - pool.provideDrawDownSink(pid: self.id, sink: sink) - } - - // Provides a source to the Position that will have tokens proactively pulled from it when the - // position has insufficient collateral. If the source can cover the position's debt, the position - // will not be liquidated. - // - // Each position can have only one source, and the source must accept the default token type - // configured for the pool. Providing a new source will replace the existing source. Pass nil - // to configure the position to not pull tokens. - access(all) fun provideSource(source: {DFB.Source}?) { - let pool = self.pool.borrow()! - pool.provideTopUpSource(pid: self.id, source: source) - } - } - - // RESTORED: Enhanced position sink from Dieter's implementation - access(all) struct PositionSink: DFB.Sink { - access(contract) let uniqueID: DFB.UniqueIdentifier? - access(self) let pool: Capability - access(self) let positionID: UInt64 - access(self) let type: Type - access(self) let pushToDrawDownSink: Bool - - init(id: UInt64, pool: Capability, type: Type, pushToDrawDownSink: Bool) { - self.uniqueID = nil - self.positionID = id - self.pool = pool - self.type = type - self.pushToDrawDownSink = pushToDrawDownSink - } - - access(all) view fun getSinkType(): Type { - return self.type - } - - access(all) fun minimumCapacity(): UFix64 { - // A position object has no limit to deposits unless the Capability has been revoked - return self.pool.check() ? UFix64.max : 0.0 - } - - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - if let pool = self.pool.borrow() { - pool.depositAndPush( - pid: self.positionID, - from: <-from.withdraw(amount: from.balance), - pushToDrawDownSink: self.pushToDrawDownSink - ) - } - } - } - - // RESTORED: Enhanced position source from Dieter's implementation - access(all) struct PositionSource: DFB.Source { - access(contract) let uniqueID: DFB.UniqueIdentifier? - access(self) let pool: Capability - access(self) let positionID: UInt64 - access(self) let type: Type - access(self) let pullFromTopUpSource: Bool - - init(id: UInt64, pool: Capability, type: Type, pullFromTopUpSource: Bool) { - self.uniqueID = nil - self.positionID = id - self.pool = pool - self.type = type - self.pullFromTopUpSource = pullFromTopUpSource - } - - access(all) view fun getSourceType(): Type { - return self.type - } - - access(all) fun minimumAvailable(): UFix64 { - if !self.pool.check() { - return 0.0 - } - let pool = self.pool.borrow()! - return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) - } - - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - if !self.pool.check() { - return <- DFBUtils.getEmptyVault(self.type) - } - let pool = self.pool.borrow()! - let available = pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) - let withdrawAmount = (available > maxAmount) ? maxAmount : available - if withdrawAmount > 0.0 { - return <- pool.withdrawAndPull(pid: self.positionID, type: self.type, amount: withdrawAmount, pullFromTopUpSource: self.pullFromTopUpSource) - } else { - // Create an empty vault - this is a limitation we need to handle properly - return <- DFBUtils.getEmptyVault(self.type) - } - } - } - - access(all) enum BalanceDirection: UInt8 { - access(all) case Credit - access(all) case Debit - } - - // A structure returned externally to report a position's balance for a particular token. - // This structure is NOT used internally. - access(all) struct PositionBalance { - access(all) let type: Type - access(all) let direction: BalanceDirection - access(all) let balance: UFix64 - - init(type: Type, direction: BalanceDirection, balance: UFix64) { - self.type = type - self.direction = direction - self.balance = balance - } - } - - // A structure returned externally to report all of the details associated with a position. - // This structure is NOT used internally. - access(all) struct PositionDetails { - access(all) let balances: [PositionBalance] - access(all) let poolDefaultToken: Type - access(all) let defaultTokenAvailableBalance: UFix64 - access(all) let health: UFix64 - - init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix64) { - self.balances = balances - self.poolDefaultToken = poolDefaultToken - self.defaultTokenAvailableBalance = defaultTokenAvailableBalance - self.health = health - } - } - - access(self) view fun borrowPool(): auth(EPosition) &Pool { - return self.account.storage.borrow(from: self.PoolStoragePath) - ?? panic("Could not borrow reference to internal TidalProtocol Pool resource") - } - - access(self) view fun borrowMOETMinter(): &MOET.Minter { - return self.account.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath) - ?? panic("Could not borrow reference to internal MOET Minter resource") - } - - init() { - self.PoolStoragePath = StoragePath(identifier: "tidalProtocolPool_\(self.account.address)")! - self.PoolFactoryPath = StoragePath(identifier: "tidalProtocolPoolFactory_\(self.account.address)")! - self.PoolPublicPath = PublicPath(identifier: "tidalProtocolPool_\(self.account.address)")! - - // save Pool in storage & configure public Capability - self.account.storage.save( - <-create PoolFactory(), - to: self.PoolFactoryPath - ) - let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)! - } + /// The canonical StoragePath where the primary TidalProtocol Pool is stored + access(all) let PoolStoragePath: StoragePath + /// The canonical StoragePath where the PoolFactory resource is stored + access(all) let PoolFactoryPath: StoragePath + /// The canonical PublicPath where the primary TidalProtocol Pool can be accessed publicly + access(all) let PoolPublicPath: PublicPath + + /* --- EVENTS ---- */ + + access(all) event Opened(pid: UInt64, poolUUID: UInt64) + access(all) event Deposited(pid: UInt64, poolUUID: UInt64, type: String, amount: UFix64, depositedUUID: UInt64) + access(all) event Withdrawn(pid: UInt64, poolUUID: UInt64, type: String, amount: UFix64, withdrawnUUID: UInt64) + access(all) event Rebalanced(pid: UInt64, poolUUID: UInt64, atHealth: UFix64, amount: UFix64, fromUnder: Bool) + + /* --- PUBLIC METHODS ---- */ + + /// Takes out a TidalProtocol loan with the provided collateral, returning a Position that can be used to manage + /// collateral and borrowed fund flows + /// + /// @param collateral: The collateral used as the basis for a loan. Only certain collateral types are supported, so + /// callers should be sure to check the provided Vault is supported to prevent reversion. + /// @param issuanceSink: The DeFiBlocks Sink connector where the protocol will deposit borrowed funds. If the + /// position becomes overcollateralized, additional funds will be borrowed (to maintain target LTV) and + /// deposited to the provided Sink. + /// @param repaymentSource: An optional DeFiBlocks Source connector from which the protocol will attempt to source + /// borrowed funds in the event of undercollateralization prior to liquidating. If none is provided, the + /// position health will not be actively managed on the down side, meaning liquidation is possible as soon as + /// the loan becomes undercollateralized. + /// + /// @return the Position via which the caller can manage their position + /// + access(all) fun openPosition( + collateral: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): Position { + let pid = self.borrowPool().createPosition( + funds: <-collateral, + issuanceSink: issuanceSink, + repaymentSource: repaymentSource, + pushToDrawDownSink: pushToDrawDownSink + ) + let cap = self.account.capabilities.storage.issue(self.PoolStoragePath) + return Position(id: pid, pool: cap) + } + + /* --- CONSTRUCTS & INTERNAL METHODS ---- */ + + access(all) entitlement EPosition + access(all) entitlement EGovernance + access(all) entitlement EImplementation + + // RESTORED: BalanceSheet and health computation from Dieter's implementation + // A convenience function for computing a health value from effective collateral and debt values. + access(all) fun healthComputation(effectiveCollateral: UFix64, effectiveDebt: UFix64): UFix64 { + var health = 0.0 + + if effectiveCollateral == 0.0 { + health = 0.0 + } else if effectiveDebt == 0.0 { + health = UFix64.max + } else if (effectiveDebt / effectiveCollateral) == 0.0 { + // If debt is so small relative to collateral that division rounds to zero, + // the health is essentially infinite + health = UFix64.max + } else { + health = effectiveCollateral / effectiveDebt + } + + return health + } + + access(all) struct BalanceSheet { + access(all) let effectiveCollateral: UFix64 + access(all) let effectiveDebt: UFix64 + access(all) let health: UFix64 + + init(effectiveCollateral: UFix64, effectiveDebt: UFix64) { + self.effectiveCollateral = effectiveCollateral + self.effectiveDebt = effectiveDebt + self.health = TidalProtocol.healthComputation(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) + } + } + + // A structure used internally to track a position's balance for a particular token. + access(all) struct InternalBalance { + access(all) var direction: BalanceDirection + + // Internally, position balances are tracked using a "scaled balance". The "scaled balance" is the + // actual balance divided by the current interest index for the associated token. This means we don't + // need to update the balance of a position as time passes, even as interest rates change. We only need + // to update the scaled balance when the user deposits or withdraws funds. The interest index + // is a number relatively close to 1.0, so the scaled balance will be roughly of the same order + // of magnitude as the actual balance (thus we can use UFix64 for the scaled balance). + access(all) var scaledBalance: UFix64 + + init() { + self.direction = BalanceDirection.Credit + self.scaledBalance = 0.0 + } + + access(all) fun recordDeposit(amount: UFix64, tokenState: auth(EImplementation) &TokenState) { + if self.direction == BalanceDirection.Credit { + // Depositing into a credit position just increases the balance. + + // To maximize precision, we could convert the scaled balance to a true balance, add the + // deposit amount, and then convert the result back to a scaled balance. However, this will + // only cause problems for very small deposits (fractions of a cent), so we save computational + // cycles by just scaling the deposit amount and adding it directly to the scaled balance. + let scaledDeposit = TidalProtocol.trueBalanceToScaledBalance(trueBalance: amount, + interestIndex: tokenState.creditInterestIndex) + + self.scaledBalance = self.scaledBalance + scaledDeposit + + // Increase the total credit balance for the token + tokenState.updateCreditBalance(amount: Fix64(amount)) + } else { + // When depositing into a debit position, we first need to compute the true balance to see + // if this deposit will flip the position from debit to credit. + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + if trueBalance > amount { + // The deposit isn't big enough to clear the debt, so we just decrement the debt. + let updatedBalance = trueBalance - amount + + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.debitInterestIndex) + + // Decrease the total debit balance for the token + tokenState.updateDebitBalance(amount: -1.0 * Fix64(amount)) + } else { + // The deposit is enough to clear the debt, so we switch to a credit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Credit + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.creditInterestIndex) + + // Increase the credit balance AND decrease the debit balance + tokenState.updateCreditBalance(amount: Fix64(updatedBalance)) + tokenState.updateDebitBalance(amount: -1.0 * Fix64(trueBalance)) + } + } + } + + access(all) fun recordWithdrawal(amount: UFix64, tokenState: &TokenState) { + if self.direction == BalanceDirection.Debit { + // Withdrawing from a debit position just increases the debt amount. + + // To maximize precision, we could convert the scaled balance to a true balance, subtract the + // withdrawal amount, and then convert the result back to a scaled balance. However, this will + // only cause problems for very small withdrawals (fractions of a cent), so we save computational + // cycles by just scaling the withdrawal amount and subtracting it directly from the scaled balance. + let scaledWithdrawal = TidalProtocol.trueBalanceToScaledBalance(trueBalance: amount, + interestIndex: tokenState.debitInterestIndex) + + self.scaledBalance = self.scaledBalance + scaledWithdrawal + + // Increase the total debit balance for the token + tokenState.updateDebitBalance(amount: Fix64(amount)) + } else { + // When withdrawing from a credit position, we first need to compute the true balance to see + // if this withdrawal will flip the position from credit to debit. + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: self.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + if trueBalance >= amount { + // The withdrawal isn't big enough to push the position into debt, so we just decrement the + // credit balance. + let updatedBalance = trueBalance - amount + + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.creditInterestIndex) + + // Decrease the total credit balance for the token + tokenState.updateCreditBalance(amount: -1.0 * Fix64(amount)) + } else { + // The withdrawal is enough to push the position into debt, so we switch to a debit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Debit + self.scaledBalance = TidalProtocol.trueBalanceToScaledBalance(trueBalance: updatedBalance, + interestIndex: tokenState.debitInterestIndex) + + // Decrease the credit balance AND increase the debit balance + tokenState.updateCreditBalance(amount: -1.0 * Fix64(trueBalance)) + tokenState.updateDebitBalance(amount: Fix64(updatedBalance)) + } + } + } + } + + access(all) entitlement mapping ImplementationUpdates { + EImplementation -> Mutate + EImplementation -> FungibleToken.Withdraw + } + + // RESTORED: InternalPosition as resource per Dieter's design + // This MUST be a resource to properly manage queued deposits + access(all) resource InternalPosition { + access(EImplementation) var targetHealth: UFix64 + access(EImplementation) var minHealth: UFix64 + access(EImplementation) var maxHealth: UFix64 + access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} + access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} + access(mapping ImplementationUpdates) var drawDownSink: {DFB.Sink}? + access(mapping ImplementationUpdates) var topUpSource: {DFB.Source}? + + init() { + self.balances = {} + self.queuedDeposits <- {} + self.targetHealth = 1.3 + self.minHealth = 1.1 + self.maxHealth = 1.5 + self.drawDownSink = nil + self.topUpSource = nil + } + + access(EImplementation) fun setDrawDownSink(_ sink: {DFB.Sink}?) { + pre { + sink?.getSinkType() ?? Type<@MOET.Vault>() == Type<@MOET.Vault>(): + "Invalid Sink provided - Sink \(sink.getType().identifier) must accept MOET" + } + self.drawDownSink = sink + } + + access(EImplementation) fun setTopUpSource(_ source: {DFB.Source}?) { + self.topUpSource = source + } + } + + access(all) struct interface InterestCurve { + access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { + post { + result <= 1.0: "Interest rate can't exceed 100%" + } + } + } + + access(all) struct SimpleInterestCurve: InterestCurve { + access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { + return 0.0 + } + } + + // A multiplication function for interest calcuations. It assumes that both values are very close to 1 + // and represent fixed point numbers with 16 decimal places of precision. + access(all) fun interestMul(_ a: UInt64, _ b: UInt64): UInt64 { + let aScaled = a / 100000000 + let bScaled = b / 100000000 + + return aScaled * bScaled + } + + // Converts a yearly interest rate (as a UFix64) to a per-second multiplication factor + // (stored in a UInt64 as a fixed point number with 16 decimal places). The input to this function will be + // just the relative interest rate (e.g. 0.05 for 5% interest), but the result will be + // the per-second multiplier (e.g. 1.000000000001). + access(all) fun perSecondInterestRate(yearlyRate: UFix64): UInt64 { + // Covert the yearly rate to an integer maintaning the 10^8 multiplier of UFix64. + // We would need to multiply by an additional 10^8 to match the promised multiplier of + // 10^16. HOWEVER, since we are about to divide by 31536000, we can save multiply a factor + // 1000 smaller, and then divide by 31536. + let yearlyScaledValue = UInt64.fromBigEndianBytes(yearlyRate.toBigEndianBytes())! * 100000 + let perSecondScaledValue = (yearlyScaledValue / 31536) + 10000000000000000 + + return perSecondScaledValue + } + + // Updates an interest index to reflect the passage of time. The result is: + // newIndex = oldIndex * perSecondRate^seconds + access(all) fun compoundInterestIndex(oldIndex: UInt64, perSecondRate: UInt64, elapsedSeconds: UFix64): UInt64 { + var result = oldIndex + var current = perSecondRate + var secondsCounter = UInt64(elapsedSeconds) + + while secondsCounter > 0 { + if secondsCounter & 1 == 1 { + result = TidalProtocol.interestMul(result, current) + } + current = TidalProtocol.interestMul(current, current) + secondsCounter = secondsCounter >> 1 + } + + return result + } + + access(all) fun scaledBalanceToTrueBalance(scaledBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return scaledBalance * indexMultiplier + } + + access(all) fun trueBalanceToScaledBalance(trueBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return trueBalance / indexMultiplier + } + + access(all) struct TokenState { + access(all) var lastUpdate: UFix64 + access(all) var totalCreditBalance: UFix64 + access(all) var totalDebitBalance: UFix64 + access(all) var creditInterestIndex: UInt64 + access(all) var debitInterestIndex: UInt64 + access(all) var currentCreditRate: UInt64 + access(all) var currentDebitRate: UInt64 + access(all) var interestCurve: {InterestCurve} + + // RESTORED: Deposit rate limiting from Dieter's implementation + access(all) var depositRate: UFix64 + access(all) var depositCapacity: UFix64 + access(all) var depositCapacityCap: UFix64 + + access(all) fun updateCreditBalance(amount: Fix64) { + // temporary cast the credit balance to a signed value so we can add/subtract + let adjustedBalance = Fix64(self.totalCreditBalance) + amount + self.totalCreditBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 + } + + access(all) fun updateDebitBalance(amount: Fix64) { + // temporary cast the debit balance to a signed value so we can add/subtract + let adjustedBalance = Fix64(self.totalDebitBalance) + amount + self.totalDebitBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 + } + + // RESTORED: Enhanced updateInterestIndices with deposit capacity update + access(all) fun updateInterestIndices() { + let currentTime = getCurrentBlock().timestamp + let timeDelta = currentTime - self.lastUpdate + self.creditInterestIndex = TidalProtocol.compoundInterestIndex(oldIndex: self.creditInterestIndex, perSecondRate: self.currentCreditRate, elapsedSeconds: timeDelta) + self.debitInterestIndex = TidalProtocol.compoundInterestIndex(oldIndex: self.debitInterestIndex, perSecondRate: self.currentDebitRate, elapsedSeconds: timeDelta) + self.lastUpdate = currentTime + + // RESTORED: Update deposit capacity based on time + let newDepositCapacity = self.depositCapacity + (self.depositRate * timeDelta) + if newDepositCapacity >= self.depositCapacityCap { + self.depositCapacity = self.depositCapacityCap + } else { + self.depositCapacity = newDepositCapacity + } + } + + // RESTORED: Deposit limit function from Dieter's implementation + access(all) fun depositLimit(): UFix64 { + // Each deposit is limited to 5% of the total deposit capacity + return self.depositCapacity * 0.05 + } + + // RESTORED: Rename to updateForTimeChange to match Dieter's implementation + access(all) fun updateForTimeChange() { + self.updateInterestIndices() + } + + access(all) fun updateInterestRates() { + // If there's no credit balance, we can't calculate a meaningful credit rate + // so we'll just set both rates to zero and return early + if self.totalCreditBalance <= 0.0 { + self.currentCreditRate = 10000000000000000 // 1.0 in fixed point (no interest) + self.currentDebitRate = 10000000000000000 // 1.0 in fixed point (no interest) + return + } + + let debitRate = self.interestCurve.interestRate(creditBalance: self.totalCreditBalance, debitBalance: self.totalDebitBalance) + let debitIncome = self.totalDebitBalance * (1.0 + debitRate) + + // Calculate insurance amount (0.1% of credit balance) + let insuranceAmount = self.totalCreditBalance * 0.001 + + // Calculate credit rate, ensuring we don't have underflows + var creditRate: UFix64 = 0.0 + if debitIncome >= insuranceAmount { + creditRate = ((debitIncome - insuranceAmount) / self.totalCreditBalance) - 1.0 + } else { + // If debit income doesn't cover insurance, we have a negative credit rate + // but since we can't represent negative rates in our model, we'll use 0.0 + creditRate = 0.0 + } + + self.currentCreditRate = TidalProtocol.perSecondInterestRate(yearlyRate: creditRate) + self.currentDebitRate = TidalProtocol.perSecondInterestRate(yearlyRate: debitRate) + } + + // RESTORED: Parameterized init from Dieter's implementation + init(interestCurve: {InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64) { + self.lastUpdate = getCurrentBlock().timestamp + self.totalCreditBalance = 0.0 + self.totalDebitBalance = 0.0 + self.creditInterestIndex = 10000000000000000 + self.debitInterestIndex = 10000000000000000 + self.currentCreditRate = 10000000000000000 + self.currentDebitRate = 10000000000000000 + self.interestCurve = interestCurve + self.depositRate = depositRate + self.depositCapacity = depositCapacityCap + self.depositCapacityCap = depositCapacityCap + } + } + + access(all) resource Pool { + // A simple version number that is incremented whenever one or more interest indices + // are updated. This is used to detect when the interest indices need to be updated in + // InternalPositions. + access(EImplementation) var version: UInt64 + + // Global state for tracking each token + access(self) var globalLedger: {Type: TokenState} + + // Individual user positions - RESTORED as resources per Dieter's design + access(self) var positions: @{UInt64: InternalPosition} + + // The actual reserves of each token + access(self) var reserves: @{Type: {FungibleToken.Vault}} + + // Auto-incrementing position identifier counter + access(self) var nextPositionID: UInt64 + + // The default token type used as the "unit of account" for the pool. + access(self) let defaultToken: Type + + // RESTORED: Price oracle from Dieter's implementation + // A price oracle that will return the price of each token in terms of the default token. + access(self) var priceOracle: {DFB.PriceOracle} + + // RESTORED: Position update queue from Dieter's implementation + access(EImplementation) var positionsNeedingUpdates: [UInt64] + access(self) var positionsProcessedPerCallback: UInt64 + + // RESTORED: Collateral and borrow factors from Dieter's implementation + // These dictionaries determine borrowing limits. Each token has a collateral factor and a + // borrow factor. + // + // When determining the total collateral amount that can be borrowed against, the value of the + // token (as given by the oracle) is multiplied by the collateral factor. So, a token with a + // collateral factor of 0.8 would only allow you to borrow 80% as much as if you had a the same + // value of a token with a collateral factor of 1.0. The total "effective collateral" for a + // position is the value of each token multiplied by its collateral factor. + // + // At the same time, the "borrow factor" determines if the user can borrow against all of that + // effective collateral, or if they can only borrow a portion of it to manage risk. + access(self) var collateralFactor: {Type: UFix64} + access(self) var borrowFactor: {Type: UFix64} + + // REMOVED: Static exchange rates and liquidation thresholds + // These have been replaced by dynamic oracle pricing and risk factors + + // RESTORED: tokenState() helper function from Dieter's implementation + // A convenience function that returns a reference to a particular token state, making sure + // it's up-to-date for the passage of time. This should always be used when accessing a token + // state to avoid missing interest updates (duplicate calls to updateForTimeChange() are a nop + // within a single block). + access(self) fun tokenState(type: Type): auth(EImplementation) &TokenState { + let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState + state.updateForTimeChange() + return state + } + + init(defaultToken: Type, priceOracle: {DFB.PriceOracle}) { + pre { + priceOracle.unitOfAccount() == defaultToken: "Price oracle must return prices in terms of the default token" + } + + self.version = 0 + self.globalLedger = {defaultToken: TokenState( + interestCurve: SimpleInterestCurve(), + depositRate: 1000000.0, // Default: no rate limiting for default token + depositCapacityCap: 1000000.0 // Default: high capacity cap + )} + self.positions <- {} + self.reserves <- {} + self.defaultToken = defaultToken + self.priceOracle = priceOracle + self.collateralFactor = {defaultToken: 1.0} + self.borrowFactor = {defaultToken: 1.0} + self.nextPositionID = 0 + self.positionsNeedingUpdates = [] + self.positionsProcessedPerCallback = 100 + + // CHANGE: Don't create vault here - let the caller provide initial reserves + // The pool starts with empty reserves map + // Vaults will be added when tokens are first deposited + } + + // Add a new token type to the pool + // This function should only be called by governance in the future + access(EGovernance) fun addSupportedToken( + tokenType: Type, + collateralFactor: UFix64, + borrowFactor: UFix64, + interestCurve: {InterestCurve}, + depositRate: UFix64, + depositCapacityCap: UFix64 + ) { + pre { + self.globalLedger[tokenType] == nil: "Token type already supported" + tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" + collateralFactor > 0.0 && collateralFactor <= 1.0: "Collateral factor must be between 0 and 1" + borrowFactor > 0.0 && borrowFactor <= 1.0: "Borrow factor must be between 0 and 1" + depositRate > 0.0: "Deposit rate must be positive" + depositCapacityCap > 0.0: "Deposit capacity cap must be positive" + DFBUtils.definingContractIsFungibleToken(tokenType): + "Invalid token contract definition for tokenType \(tokenType.identifier) - defining contract is not FungibleToken conformant" + } + + // Add token to global ledger with its interest curve and deposit parameters + self.globalLedger[tokenType] = TokenState( + interestCurve: interestCurve, + depositRate: depositRate, + depositCapacityCap: depositCapacityCap + ) + + // Set collateral factor (what percentage of value can be used as collateral) + self.collateralFactor[tokenType] = collateralFactor + + // Set borrow factor (risk adjustment for borrowed amounts) + self.borrowFactor[tokenType] = borrowFactor + } + + // Get supported token types + access(all) fun getSupportedTokens(): [Type] { + return self.globalLedger.keys + } + + // Check if a token type is supported + access(all) fun isTokenSupported(tokenType: Type): Bool { + return self.globalLedger[tokenType] != nil + } + + // RESTORED: Enhanced deposit with queue processing and rebalancing from Dieter's implementation + access(EPosition) fun depositAndPush(pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { + pre { + self.positions[pid] != nil: "Invalid position ID" + self.globalLedger[from.getType()] != nil: "Invalid token type" + } + + if from.balance == 0.0 { + Burner.burn(<-from) + return + } + + // Get a reference to the user's position and global token state for the affected token. + let type = from.getType() + let amount = from.balance + let depositedUUID = from.uuid + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + // Update time-based state + // REMOVED: This is now handled by tokenState() helper function + // tokenState.updateForTimeChange() + + // RESTORED: Deposit rate limiting from Dieter's implementation + let depositAmount = from.balance + let depositLimit = tokenState.depositLimit() + + if depositAmount > depositLimit { + // The deposit is too big, so we need to queue the excess + let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) + + if position.queuedDeposits[type] == nil { + position.queuedDeposits[type] <-! queuedDeposit + } else { + position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) + } + } + + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() + } + + // CHANGE: Create vault if it doesn't exist yet + if self.reserves[type] == nil { + self.reserves[type] <-! from.createEmptyVault() + } + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Reflect the deposit in the position's balance + position.balances[type]!.recordDeposit(amount: from.balance, tokenState: tokenState) + + // Add the money to the reserves + reserveVault.deposit(from: <-from) + + // RESTORED: Rebalancing and queue management + if pushToDrawDownSink { + self.rebalancePosition(pid: pid, force: true) + } + + emit Deposited(pid: pid, poolUUID: self.uuid, type: type.identifier, amount: amount, depositedUUID: depositedUUID) + + self.queuePositionForUpdateIfNecessary(pid: pid) + } + + // RESTORED: Public deposit function from Dieter's implementation + // Allows anyone to deposit funds into any position + access(all) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { + self.depositAndPush(pid: pid, from: <-from, pushToDrawDownSink: false) + } + + access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { + // RESTORED: Call the enhanced function with pullFromTopUpSource = false for backward compatibility + return <- self.withdrawAndPull(pid: pid, type: type, amount: amount, pullFromTopUpSource: false) + } + + // RESTORED: Enhanced withdraw with top-up source integration from Dieter's implementation + access(EPosition) fun withdrawAndPull( + pid: UInt64, + type: Type, + amount: UFix64, + pullFromTopUpSource: Bool + ): @{FungibleToken.Vault} { + pre { + self.positions[pid] != nil: "Invalid position ID" + self.globalLedger[type] != nil: "Invalid token type" + } + if amount == 0.0 { + return <- DFBUtils.getEmptyVault(type) + } + + // Get a reference to the user's position and global token state for the affected token. + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + // Update the global interest indices on the affected token to reflect the passage of time. + // REMOVED: This is now handled by tokenState() helper function + // tokenState.updateForTimeChange() + + // RESTORED: Top-up source integration from Dieter's implementation + // Preflight to see if the funds are available + let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DFB.Source}? + let topUpType = topUpSource?.getSourceType() ?? self.defaultToken + + let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: topUpType, + targetHealth: position.minHealth, + withdrawType: type, + withdrawAmount: amount + ) + + var canWithdraw = false + + if requiredDeposit == 0.0 { + // We can service this withdrawal without any top up + canWithdraw = true + } else { + // We need more funds to service this withdrawal, see if they are available from the top up source + if pullFromTopUpSource && topUpSource != nil { + // If we have to rebalance, let's try to rebalance to the target health, not just the minimum + let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: topUpType, + targetHealth: position.targetHealth, + withdrawType: type, + withdrawAmount: amount + ) + + let pulledVault <- topUpSource!.withdrawAvailable(maxAmount: idealDeposit) + + // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here. + // The top up source may not have enough funds get us to the target health, but could have + // enough to keep us over the minimum. + if pulledVault.balance >= requiredDeposit { + // We can service this withdrawal if we deposit funds from our top up source + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + canWithdraw = true + } else { + // We can't get the funds required to service this withdrawal, so we need to redeposit what we got + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + } + } + } + + if !canWithdraw { + // We can't service this withdrawal, so we just abort + panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal") + } + + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() + } + + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Reflect the withdrawal in the position's balance + position.balances[type]!.recordWithdrawal(amount: amount, tokenState: tokenState) + + // Ensure that this withdrawal doesn't cause the position to be overdrawn. + assert(self.positionHealth(pid: pid) >= 1.0, message: "Position is overdrawn") + + // Queue for update if necessary + self.queuePositionForUpdateIfNecessary(pid: pid) + + let withdrawn <- reserveVault.withdraw(amount: amount) + + emit Withdrawn(pid: pid, poolUUID: self.uuid, type: type.identifier, amount: withdrawn.balance, withdrawnUUID: withdrawn.uuid) + + return <- withdrawn + } + + // RESTORED: Position queue management from Dieter's implementation + access(self) fun queuePositionForUpdateIfNecessary(pid: UInt64) { + if self.positionsNeedingUpdates.contains(pid) { + // If this position is already queued for an update, no need to check anything else + return + } else { + // If this position is not already queued for an update, we need to check if it needs one + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + if position.queuedDeposits.length > 0 { + // This position has deposits that need to be processed, so we need to queue it for an update + self.positionsNeedingUpdates.append(pid) + return + } + + let positionHealth = self.positionHealth(pid: pid) + + if positionHealth < position.minHealth || positionHealth > position.maxHealth { + // This position is outside the configured health bounds, we queue it for an update + self.positionsNeedingUpdates.append(pid) + return + } + } + } + + // RESTORED: Position rebalancing from Dieter's implementation + // Rebalances the position to the target health value. If force is true, the position will be + // rebalanced even if it is currently healthy, otherwise, this function will do nothing if the + // position is within the min/max health bounds. + access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let balanceSheet = self.positionBalanceSheet(pid: pid) + + if !force && (balanceSheet.health >= position.minHealth && balanceSheet.health <= position.maxHealth) { + // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! + return + } + + if balanceSheet.health < position.targetHealth { + // The position is undercollateralized, see if the source can get more collateral to bring it up to the target health. + if position.topUpSource != nil { + let topUpSource = position.topUpSource! as auth(FungibleToken.Withdraw) &{DFB.Source} + let idealDeposit = self.fundsRequiredForTargetHealth( + pid: pid, + type: topUpSource.getSourceType(), + targetHealth: position.targetHealth + ) + + let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) + + emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: pulledVault.balance, fromUnder: true) + + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + } + } else if balanceSheet.health > position.targetHealth { + // The position is overcollateralized, we'll withdraw funds to match the target health and offer it to the sink. + if position.drawDownSink != nil { + let drawDownSink = position.drawDownSink! + let sinkType = drawDownSink.getSinkType() + let idealWithdrawal = self.fundsAvailableAboveTargetHealth( + pid: pid, + type: sinkType, + targetHealth: position.targetHealth + ) + + // Compute how many tokens of the sink's type are available to hit our target health. + let sinkCapacity = drawDownSink.minimumCapacity() + let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal + + if sinkAmount > 0.0 && sinkType == self.defaultToken { // second conditional included for sake of tracer bullet + // BUG: Calling through to withdrawAndPull results in an insufficient funds from the position's + // topUpSource. These funds should come from the protocol or reserves, not from the user's + // funds. To unblock here, we just mint MOET when a position is overcollateralized + // let sinkVault <- self.withdrawAndPull( + // pid: pid, + // type: sinkType, + // amount: sinkAmount, + // pullFromTopUpSource: false + // ) + + let tokenState = self.tokenState(type: self.defaultToken) + if position.balances[self.defaultToken] == nil { + position.balances[self.defaultToken] = InternalBalance() + } + position.balances[self.defaultToken]!.recordWithdrawal(amount: sinkAmount, tokenState: tokenState) + let sinkVault <- TidalProtocol.borrowMOETMinter().mintTokens(amount: sinkAmount) + + emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: sinkVault.balance, fromUnder: false) + + // Push what we can into the sink, and redeposit the rest + drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + if sinkVault.balance > 0.0 { + self.depositAndPush(pid: pid, from: <-sinkVault, pushToDrawDownSink: false) + } else { + Burner.burn(<-sinkVault) + } + } + } + } + } + + // RESTORED: Provider functions for sink/source from Dieter's implementation + access(EPosition) fun provideDrawDownSink(pid: UInt64, sink: {DFB.Sink}?) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + position.setDrawDownSink(sink) + } + access(EPosition) fun provideTopUpSource(pid: UInt64, source: {DFB.Source}?) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + position.setTopUpSource(source) + } + + // RESTORED: Available balance with source integration from Dieter's implementation + access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + if pullFromTopUpSource && position.topUpSource != nil { + let topUpSource = position.topUpSource! + let sourceType = topUpSource.getSourceType() + let sourceAmount = topUpSource.minimumAvailable() + + return self.fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: type, + targetHealth: position.minHealth, + depositType: sourceType, + depositAmount: sourceAmount + ) + } else { + return self.fundsAvailableAboveTargetHealth( + pid: pid, + type: type, + targetHealth: position.minHealth + ) + } + } + + // Returns the health of the given position, which is the ratio of the position's effective collateral + // to its debt (as denominated in the default token). ("Effective collateral" means the + // value of each credit balance times the liquidation threshold for that token. i.e. the maximum borrowable amount) + access(all) fun positionHealth(pid: UInt64): UFix64 { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + // Get the position's collateral and debt values in terms of the default token. + var effectiveCollateral = 0.0 + var effectiveDebt = 0.0 + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self.tokenState(type: type) + if balance.direction == BalanceDirection.Credit { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + // RESTORED: Oracle-based pricing from Dieter's implementation + let tokenPrice = self.priceOracle.price(ofToken: type)! + let value = tokenPrice * trueBalance + effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) + } else { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + // RESTORED: Oracle-based pricing for debt calculation + let tokenPrice = self.priceOracle.price(ofToken: type)! + let value = tokenPrice * trueBalance + effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) + } + } + + // Calculate the health as the ratio of collateral to debt. + if effectiveDebt == 0.0 { + return 1.0 + } + return effectiveCollateral / effectiveDebt + } + + // RESTORED: Position balance sheet calculation from Dieter's implementation + access(self) fun positionBalanceSheet(pid: UInt64): BalanceSheet { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let priceOracle = &self.priceOracle as &{DFB.PriceOracle} + + // Get the position's collateral and debt values in terms of the default token. + var effectiveCollateral = 0.0 + var effectiveDebt = 0.0 + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self.tokenState(type: type) + if balance.direction == BalanceDirection.Credit { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + let value = priceOracle.price(ofToken: type)! * trueBalance + + effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) + } else { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.debitInterestIndex) + + let value = priceOracle.price(ofToken: type)! * trueBalance + + effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) + } + } + + return BalanceSheet(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) + } + + /// Creates a lending position against the provided collateral funds, depositing the loaned amount to the + /// given Sink. If a Source is provided, the position will be configured to pull loan repayment when the loan + /// becomes undercollateralized, preferring repayment to outright liquidation. + access(all) fun createPosition( + funds: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): UInt64 { + pre { + self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier)" + } + // construct a new InternalPosition, assigning it the current position ID + let id = self.nextPositionID + self.nextPositionID = self.nextPositionID + 1 + self.positions[id] <-! create InternalPosition() + + emit Opened(pid: id, poolUUID: self.uuid) + + // assign issuance & repayment connectors within the InternalPosition + let iPos = (&self.positions[id] as auth(EImplementation) &InternalPosition?)! + let fundsType = funds.getType() + iPos.setDrawDownSink(issuanceSink) + if repaymentSource != nil { + iPos.setTopUpSource(repaymentSource) + } + + // deposit the initial funds & return the position ID + self.depositAndPush( + pid: id, + from: <-funds, + pushToDrawDownSink: pushToDrawDownSink + ) + return id + } + + // Helper function for testing – returns the current reserve balance for the specified token type. + access(all) fun reserveBalance(type: Type): UFix64 { + // CHANGE: Handle case where no vault exists yet for this token type + let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) + if vaultRef == nil { + return 0.0 + } + return vaultRef!.balance + } + + // Add getPositionDetails function that's used by DFB implementations + access(all) fun getPositionDetails(pid: UInt64): PositionDetails { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let balances: [PositionBalance] = [] + + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self.tokenState(type: type) + let trueBalance = balance.direction == BalanceDirection.Credit + ? TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.creditInterestIndex) + : TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.debitInterestIndex) + + balances.append(PositionBalance( + type: type, + direction: balance.direction, + balance: trueBalance + )) + } + + let health = self.positionHealth(pid: pid) + let defaultTokenAvailable = self.availableBalance(pid: pid, type: self.defaultToken, pullFromTopUpSource: false) + + return PositionDetails( + balances: balances, + poolDefaultToken: self.defaultToken, + defaultTokenAvailableBalance: defaultTokenAvailable, + health: health + ) + } + + // RESTORED: Advanced position health management functions from Dieter's implementation + // The quantity of funds of a specified token which would need to be deposited to bring the + // position to the target health. This function will return 0.0 if the position is already at or over + // that health value. + access(all) fun fundsRequiredForTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { + return self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: type, + targetHealth: targetHealth, + withdrawType: self.defaultToken, + withdrawAmount: 0.0 + ) + } + + // The quantity of funds of a specified token which would need to be deposited to bring the + // position to the target health assuming we also withdraw a specified amount of another + // token. This function will return 0.0 if the position would already be at or over the target + // health value after the proposed withdrawal. + access(all) fun fundsRequiredForTargetHealthAfterWithdrawing( + pid: UInt64, + depositType: Type, + targetHealth: UFix64, + withdrawType: Type, + withdrawAmount: UFix64 + ): UFix64 { + if depositType == withdrawType && withdrawAmount > 0.0 { + // If the deposit and withdrawal types are the same, we compute the required deposit assuming + // no withdrawal (which is less work) and increase that by the withdraw amount at the end + return self.fundsRequiredForTargetHealth(pid: pid, type: depositType, targetHealth: targetHealth) + withdrawAmount + } + + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral + var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + if withdrawAmount != 0.0 { + if position.balances[withdrawType] == nil || position.balances[withdrawType]!.direction == BalanceDirection.Debit { + // If the position doesn't have any collateral for the withdrawn token, we can just compute how much + // additional effective debt the withdrawal will create. + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) + } else { + let withdrawTokenState = self.tokenState(type: withdrawType) + // REMOVED: This is now handled by tokenState() helper function + // withdrawTokenState.updateForTimeChange() + + // The user has a collateral position in the given token, we need to figure out if this withdrawal + // will flip over into debt, or just draw down the collateral. + let collateralBalance = position.balances[withdrawType]!.scaledBalance + let trueCollateral = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: collateralBalance, + interestIndex: withdrawTokenState.creditInterestIndex + ) + + if trueCollateral >= withdrawAmount { + // This withdrawal will draw down collateral, but won't create debt, we just need to account + // for the collateral decrease. + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) + } else { + // The withdrawal will wipe out all of the collateral, and create some debt. + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + ((withdrawAmount - trueCollateral) * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) + + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (trueCollateral * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) + } + } + } + + // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) + // Now we can figure out how many of the given token would need to be deposited to bring the position + // to the target health value. + var healthAfterWithdrawal = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) + + if healthAfterWithdrawal >= targetHealth { + // The position is already at or above the target health, so we don't need to deposit anything. + return 0.0 + } + + // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep + // track of the number of tokens that went towards paying off debt. + var debtTokenCount = 0.0 + + if position.balances[depositType] != nil && position.balances[depositType]!.direction == BalanceDirection.Debit { + // The user has a debt position in the given token, we start by looking at the health impact of paying off + // the entire debt. + let depositTokenState = self.tokenState(type: depositType) + // REMOVED: This is now handled by tokenState() helper function + // depositTokenState.updateForTimeChange() + let debtBalance = position.balances[depositType]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: depositTokenState.debitInterestIndex + ) + let debtEffectiveValue = self.priceOracle.price(ofToken: depositType)! * trueDebt / self.borrowFactor[depositType]! + + // Check what the new health would be if we paid off all of this debt + let potentialHealth = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal - debtEffectiveValue + ) + + // Does paying off all of the debt reach the target health? Then we're done. + if potentialHealth >= targetHealth { + // We can reach the target health by paying off some or all of the debt. We can easily + // compute how many units of the token would be needed to reach the target health. + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveDebt = healthChange * effectiveCollateralAfterWithdrawal / (targetHealth * targetHealth) + + // The amount of the token to pay back, in units of the token. + let paybackAmount = requiredEffectiveDebt * self.borrowFactor[depositType]! / self.priceOracle.price(ofToken: depositType)! + + return paybackAmount + } else { + // We can pay off the entire debt, but we still need to deposit more to reach the target health. + // We have logic below that can determine the collateral deposition required to reach the target health + // from this new health position. Rather than copy that logic here, we fall through into it. But first + // we have to record the amount of tokens that went towards debt payback and adjust the effective + // debt to reflect that it has been paid off. + debtTokenCount = trueDebt + effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue + healthAfterWithdrawal = potentialHealth + } + } + + // At this point, we're either dealing with a position that didn't have a debt position in the deposit + // token, or we've accounted for the debt payoff and adjusted the effective debt above. + + // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the + // target health. We can rearrange the health equation to solve for the required collateral: + // targetHealth = effectiveCollateral / effectiveDebt + // targetHealth * effectiveDebt = effectiveCollateral + // requiredCollateral = targetHealth * effectiveDebtAfterWithdrawal + + // We need to increase the effective collateral from its current value to the required value, so we + // multiply the required health change by the effective debt, and turn that into a token amount. + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveCollateral = healthChange * effectiveDebtAfterWithdrawal + + // The amount of the token to deposit, in units of the token. + let collateralTokenCount = requiredEffectiveCollateral / self.priceOracle.price(ofToken: depositType)! / self.collateralFactor[depositType]! + + // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. + return collateralTokenCount + debtTokenCount + } + + // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health + // at or above the provided target. + access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { + return self.fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: type, + targetHealth: targetHealth, + depositType: self.defaultToken, + depositAmount: 0.0 + ) + } + + // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health + // at or above the provided target, assuming we also deposit a specified amount of another token. + access(all) fun fundsAvailableAboveTargetHealthAfterDepositing( + pid: UInt64, + withdrawType: Type, + targetHealth: UFix64, + depositType: Type, + depositAmount: UFix64 + ): UFix64 { + if depositType == withdrawType && depositAmount > 0.0 { + // If the deposit and withdrawal types are the same, we compute the available funds assuming + // no deposit (which is less work) and increase that by the deposit amount at the end + return self.fundsAvailableAboveTargetHealth(pid: pid, type: withdrawType, targetHealth: targetHealth) + depositAmount + } + + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt + + if depositAmount != 0.0 { + if position.balances[depositType] == nil || position.balances[depositType]!.direction == BalanceDirection.Credit { + // If there's no debt for the deposit token, we can just compute how much additional effective collateral the deposit will create. + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + (depositAmount * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) + } else { + let depositTokenState = self.tokenState(type: depositType) + + // The user has a debt position in the given token, we need to figure out if this deposit + // will result in net collateral, or just bring down the debt. + let debtBalance = position.balances[depositType]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: depositTokenState.debitInterestIndex + ) + + if trueDebt >= depositAmount { + // This deposit will pay down some debt, but won't result in net collateral, we + // just need to account for the debt decrease. + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (depositAmount * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) + } else { + // The deposit will wipe out all of the debt, and create some collateral. + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (trueDebt * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) + + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + ((depositAmount - trueDebt) * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) + } + } + } + + // We now have new effective collateral and debt values that reflect the proposed deposit (if any!) + // Now we can figure out how many of the withdrawal token are available while keeping the position + // at or above the target health value. + var healthAfterDeposit = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + + if healthAfterDeposit <= targetHealth { + // The position is already at or below the target health, so we can't withdraw anything. + return 0.0 + } + + // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep + // track of the number of tokens that are available from collateral + var collateralTokenCount = 0.0 + + if position.balances[withdrawType] != nil && position.balances[withdrawType]!.direction == BalanceDirection.Credit { + // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all + // of that collateral + let withdrawTokenState = self.tokenState(type: withdrawType) + // REMOVED: This is now handled by tokenState() helper function + // withdrawTokenState.updateForTimeChange() + let creditBalance = position.balances[withdrawType]!.scaledBalance + let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: creditBalance, + interestIndex: withdrawTokenState.creditInterestIndex + ) + let collateralEffectiveValue = self.priceOracle.price(ofToken: withdrawType)! * trueCredit * self.collateralFactor[withdrawType]! + + // Check what the new health would be if we took out all of this collateral + let potentialHealth = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, + effectiveDebt: effectiveDebtAfterDeposit + ) + + // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. + if potentialHealth <= targetHealth { + // We will hit the health target before using up all of the withdraw token credit. We can easily + // compute how many units of the token would bring the position down to the target health. + let availableHealth = healthAfterDeposit - targetHealth + let availableEffectiveValue = availableHealth * effectiveDebtAfterDeposit + + // The amount of the token we can take using that amount of health + let availableTokenCount = availableEffectiveValue / self.collateralFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! + + return availableTokenCount + } else { + // We can flip this credit position into a debit position, before hitting the target health. + // We have logic below that can determine health changes for debit positions. Rather than copy that here, + // fall through into it. But first we have to record the amount of tokens that are available as collateral + // and then adjust the effective collateral to reflect that it has come out + collateralTokenCount = trueCredit + effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue + // NOTE: The above invalidates the healthAfterDeposit value, but it's not used below... + } + } + + // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw + // token, or we've accounted for the credit balance and adjusted the effective collateral above. + + // We can calculate the available debt increase that would bring us to the target health + var availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit + + let availableTokens = availableDebtIncrease * self.borrowFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! + + return availableTokens + collateralTokenCount + } + + // Returns the health the position would have if the given amount of the specified token were deposited. + access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix64 { + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + var effectiveCollateralIncrease = 0.0 + var effectiveDebtDecrease = 0.0 + + if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Credit { + // Since the user has no debt in the given token, we can just compute how much + // additional collateral this deposit will create. + effectiveCollateralIncrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } else { + // The user has a debit position in the given token, we need to figure out if this deposit + // will only pay off some of the debt, or if it will also create new collateral. + let debtBalance = position.balances[type]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: tokenState.debitInterestIndex + ) + + if trueDebt >= amount { + // This deposit will wipe out some or all of the debt, but won't create new collateral, we + // just need to account for the debt decrease. + effectiveDebtDecrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + } else { + // This deposit will wipe out all of the debt, and create new collateral. + effectiveDebtDecrease = trueDebt * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + effectiveCollateralIncrease = (amount - trueDebt) * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } + } + + return TidalProtocol.healthComputation( + effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, + effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease + ) + } + + // Returns health value of this position if the given amount of the specified token were withdrawn without + // using the top up source. + // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates + // that the proposed withdrawal would fail (unless a top up source is available and used). + access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix64 { + let balanceSheet = self.positionBalanceSheet(pid: pid) + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + let tokenState = self.tokenState(type: type) + + var effectiveCollateralDecrease = 0.0 + var effectiveDebtIncrease = 0.0 + + if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Debit { + // The user has no credit position in the given token, we can just compute how much + // additional effective debt this withdrawal will create. + effectiveDebtIncrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + } else { + // The user has a credit position in the given token, we need to figure out if this withdrawal + // will only draw down some of the collateral, or if it will also create new debt. + let creditBalance = position.balances[type]!.scaledBalance + let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: creditBalance, + interestIndex: tokenState.creditInterestIndex + ) + + if trueCredit >= amount { + // This withdrawal will draw down some collateral, but won't create new debt, we + // just need to account for the collateral decrease. + effectiveCollateralDecrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } else { + // The withdrawal will wipe out all of the collateral, and create new debt. + effectiveDebtIncrease = (amount - trueCredit) * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + effectiveCollateralDecrease = trueCredit * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } + } + + return TidalProtocol.healthComputation( + effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, + effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease + ) + } + + // RESTORED: Async update infrastructure from Dieter's implementation + access(EImplementation) fun asyncUpdate() { + // TODO: In the production version, this function should only process some positions (limited by positionsProcessedPerCallback) AND + // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or + // sink aborts) won't prevent other positions from being updated. + var processed: UInt64 = 0 + while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { + let pid = self.positionsNeedingUpdates.removeFirst() + self.asyncUpdatePosition(pid: pid) + self.queuePositionForUpdateIfNecessary(pid: pid) + processed = processed + 1 + } + } + + // RESTORED: Async position update from Dieter's implementation + access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { + let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + + // First check queued deposits, their addition could affect the rebalance we attempt later + for depositType in position.queuedDeposits.keys { + let queuedVault <- position.queuedDeposits.remove(key: depositType)! + let queuedAmount = queuedVault.balance + let depositTokenState = self.tokenState(type: depositType) + let maxDeposit = depositTokenState.depositLimit() + + if maxDeposit >= queuedAmount { + // We can deposit all of the queued deposit, so just do it and remove it from the queue + self.depositAndPush(pid: pid, from: <-queuedVault, pushToDrawDownSink: false) + } else { + // We can only deposit part of the queued deposit, so do that and leave the rest in the queue + // for the next time we run. + let depositVault <- queuedVault.withdraw(amount: maxDeposit) + self.depositAndPush(pid: pid, from: <-depositVault, pushToDrawDownSink: false) + + // We need to update the queued vault to reflect the amount we used up + position.queuedDeposits[depositType] <-! queuedVault + } + } + + // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance + // the position if necessary. + self.rebalancePosition(pid: pid, force: false) + } + } + + /// Resource enabling the contract account to create a Pool. This pattern is used in place of contract methods to + /// ensure limited access to pool creation. While this could be done in contract's init, doing so here will allow + /// for the setting of the Pool's PriceOracle without the introduction of a concrete PriceOracle defining contract + /// which would include an external contract dependency. + /// + access(all) resource PoolFactory { + /// Creates a Pool and saves it to the canonical path, reverting if one is already stored + access(all) fun createPool(defaultToken: Type, priceOracle: {DFB.PriceOracle}) { + pre { + TidalProtocol.account.storage.type(at: TidalProtocol.PoolStoragePath) == nil: + "Storage collision - Pool has already been created & saved to \(TidalProtocol.PoolStoragePath)" + } + let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle) + TidalProtocol.account.storage.save(<-pool, to: TidalProtocol.PoolStoragePath) + let cap = TidalProtocol.account.capabilities.storage.issue<&Pool>(TidalProtocol.PoolStoragePath) + TidalProtocol.account.capabilities.unpublish(TidalProtocol.PoolPublicPath) + TidalProtocol.account.capabilities.publish(cap, at: TidalProtocol.PoolPublicPath) + } + } + + // TODO: Consider making this a resource given how critical it is to accessing a loan + access(all) struct Position { + access(self) let id: UInt64 + access(self) let pool: Capability + + init(id: UInt64, pool: Capability) { + pre { + pool.check(): "Invalid Pool Capability provided - cannot construct Position" + } + self.id = id + self.pool = pool + } + + // Returns the balances (both positive and negative) for all tokens in this position. + access(all) fun getBalances(): [PositionBalance] { + let pool = self.pool.borrow()! + return pool.getPositionDetails(pid: self.id).balances + } + + // RESTORED: Enhanced available balance from Dieter's implementation + access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 { + let pool = self.pool.borrow()! + return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource) + } + + // RESTORED: Health functions from Dieter's implementation + access(all) fun getHealth(): UFix64 { + let pool = self.pool.borrow()! + return pool.positionHealth(pid: self.id) + } + + access(all) fun getTargetHealth(): UFix64 { + // DIETER'S DESIGN: Position is just a relay struct, return 0.0 + return 0.0 + } + + access(all) fun setTargetHealth(targetHealth: UFix64) { + // DIETER'S DESIGN: Position is just a relay struct, do nothing + } + + access(all) fun getMinHealth(): UFix64 { + // DIETER'S DESIGN: Position is just a relay struct, return 0.0 + return 0.0 + } + + access(all) fun setMinHealth(minHealth: UFix64) { + // DIETER'S DESIGN: Position is just a relay struct, do nothing + } + + access(all) fun getMaxHealth(): UFix64 { + // DIETER'S DESIGN: Position is just a relay struct, return 0.0 + return 0.0 + } + + access(all) fun setMaxHealth(maxHealth: UFix64) { + // DIETER'S DESIGN: Position is just a relay struct, do nothing + } + + // Returns the maximum amount of the given token type that could be deposited into this position. + access(all) fun getDepositCapacity(type: Type): UFix64 { + // There's no limit on deposits from the position's perspective + return UFix64.max + } + + // RESTORED: Simple deposit that calls depositAndPush with pushToDrawDownSink = false + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let pool = self.pool.borrow()! + pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: false) + } + + // RESTORED: Enhanced deposit from Dieter's implementation + access(all) fun depositAndPush(from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { + let pool = self.pool.borrow()! + pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: pushToDrawDownSink) + } + + // RESTORED: Simple withdraw that calls withdrawAndPull with pullFromTopUpSource = false + access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} { + return <- self.withdrawAndPull(type: type, amount: amount, pullFromTopUpSource: false) + } + + // RESTORED: Enhanced withdraw from Dieter's implementation + access(FungibleToken.Withdraw) fun withdrawAndPull(type: Type, amount: UFix64, pullFromTopUpSource: Bool): @{FungibleToken.Vault} { + let pool = self.pool.borrow()! + return <- pool.withdrawAndPull(pid: self.id, type: type, amount: amount, pullFromTopUpSource: pullFromTopUpSource) + } + + // Returns a NEW sink for the given token type that will accept deposits of that token and + // update the position's collateral and/or debt accordingly. Note that calling this method multiple + // times will create multiple sinks, each of which will continue to work regardless of how many + // other sinks have been created. + access(all) fun createSink(type: Type): {DFB.Sink} { + // RESTORED: Create enhanced sink with pushToDrawDownSink option + return self.createSinkWithOptions(type: type, pushToDrawDownSink: false) + } + + // RESTORED: Enhanced sink creation from Dieter's implementation + access(all) fun createSinkWithOptions(type: Type, pushToDrawDownSink: Bool): {DFB.Sink} { + let pool = self.pool.borrow()! + return PositionSink(id: self.id, pool: self.pool, type: type, pushToDrawDownSink: pushToDrawDownSink) + } + + // Returns a NEW source for the given token type that will service withdrawals of that token and + // update the position's collateral and/or debt accordingly. Note that calling this method multiple + // times will create multiple sources, each of which will continue to work regardless of how many + // other sources have been created. + access(FungibleToken.Withdraw) fun createSource(type: Type): {DFB.Source} { + // RESTORED: Create enhanced source with pullFromTopUpSource option + return self.createSourceWithOptions(type: type, pullFromTopUpSource: false) + } + + // RESTORED: Enhanced source creation from Dieter's implementation + access(FungibleToken.Withdraw) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DFB.Source} { + let pool = self.pool.borrow()! + return PositionSource(id: self.id, pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource) + } + + // RESTORED: Provider functions implementation from Dieter's design + // Provides a sink to the Position that will have tokens proactively pushed into it when the + // position has excess collateral. (Remember that sinks do NOT have to accept all tokens provided + // to them; the sink can choose to accept only some (or none) of the tokens provided, leaving the position + // overcollateralized.) + // + // Each position can have only one sink, and the sink must accept the default token type + // configured for the pool. Providing a new sink will replace the existing sink. Pass nil + // to configure the position to not push tokens. + access(FungibleToken.Withdraw) fun provideSink(sink: {DFB.Sink}?) { + let pool = self.pool.borrow()! + pool.provideDrawDownSink(pid: self.id, sink: sink) + } + + // Provides a source to the Position that will have tokens proactively pulled from it when the + // position has insufficient collateral. If the source can cover the position's debt, the position + // will not be liquidated. + // + // Each position can have only one source, and the source must accept the default token type + // configured for the pool. Providing a new source will replace the existing source. Pass nil + // to configure the position to not pull tokens. + access(all) fun provideSource(source: {DFB.Source}?) { + let pool = self.pool.borrow()! + pool.provideTopUpSource(pid: self.id, source: source) + } + } + + // RESTORED: Enhanced position sink from Dieter's implementation + access(all) struct PositionSink: DFB.Sink { + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(self) let pool: Capability + access(self) let positionID: UInt64 + access(self) let type: Type + access(self) let pushToDrawDownSink: Bool + + init(id: UInt64, pool: Capability, type: Type, pushToDrawDownSink: Bool) { + self.uniqueID = nil + self.positionID = id + self.pool = pool + self.type = type + self.pushToDrawDownSink = pushToDrawDownSink + } + + access(all) view fun getSinkType(): Type { + return self.type + } + + access(all) fun minimumCapacity(): UFix64 { + // A position object has no limit to deposits unless the Capability has been revoked + return self.pool.check() ? UFix64.max : 0.0 + } + + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + if let pool = self.pool.borrow() { + pool.depositAndPush( + pid: self.positionID, + from: <-from.withdraw(amount: from.balance), + pushToDrawDownSink: self.pushToDrawDownSink + ) + } + } + } + + // RESTORED: Enhanced position source from Dieter's implementation + access(all) struct PositionSource: DFB.Source { + access(contract) let uniqueID: DFB.UniqueIdentifier? + access(self) let pool: Capability + access(self) let positionID: UInt64 + access(self) let type: Type + access(self) let pullFromTopUpSource: Bool + + init(id: UInt64, pool: Capability, type: Type, pullFromTopUpSource: Bool) { + self.uniqueID = nil + self.positionID = id + self.pool = pool + self.type = type + self.pullFromTopUpSource = pullFromTopUpSource + } + + access(all) view fun getSourceType(): Type { + return self.type + } + + access(all) fun minimumAvailable(): UFix64 { + if !self.pool.check() { + return 0.0 + } + let pool = self.pool.borrow()! + return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) + } + + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + if !self.pool.check() { + return <- DFBUtils.getEmptyVault(self.type) + } + let pool = self.pool.borrow()! + let available = pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) + let withdrawAmount = (available > maxAmount) ? maxAmount : available + if withdrawAmount > 0.0 { + return <- pool.withdrawAndPull(pid: self.positionID, type: self.type, amount: withdrawAmount, pullFromTopUpSource: self.pullFromTopUpSource) + } else { + // Create an empty vault - this is a limitation we need to handle properly + return <- DFBUtils.getEmptyVault(self.type) + } + } + } + + access(all) enum BalanceDirection: UInt8 { + access(all) case Credit + access(all) case Debit + } + + // RESTORED: DummyPriceOracle for testing from Dieter's design pattern + access(all) struct DummyPriceOracle: DFB.PriceOracle { + access(self) var prices: {Type: UFix64} + access(self) let defaultToken: Type + + access(all) view fun unitOfAccount(): Type { + return self.defaultToken + } + + access(all) fun price(ofToken: Type): UFix64 { + return self.prices[ofToken] ?? 1.0 + } + + access(all) fun setPrice(ofToken: Type, price: UFix64) { + self.prices[ofToken] = price + } + + init(defaultToken: Type) { + self.defaultToken = defaultToken + self.prices = {defaultToken: 1.0} + } + } + + // A structure returned externally to report a position's balance for a particular token. + // This structure is NOT used internally. + access(all) struct PositionBalance { + access(all) let type: Type + access(all) let direction: BalanceDirection + access(all) let balance: UFix64 + + init(type: Type, direction: BalanceDirection, balance: UFix64) { + self.type = type + self.direction = direction + self.balance = balance + } + } + + // A structure returned externally to report all of the details associated with a position. + // This structure is NOT used internally. + access(all) struct PositionDetails { + access(all) let balances: [PositionBalance] + access(all) let poolDefaultToken: Type + access(all) let defaultTokenAvailableBalance: UFix64 + access(all) let health: UFix64 + + init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix64) { + self.balances = balances + self.poolDefaultToken = poolDefaultToken + self.defaultTokenAvailableBalance = defaultTokenAvailableBalance + self.health = health + } + } + + access(self) view fun borrowPool(): auth(EPosition) &Pool { + return self.account.storage.borrow(from: self.PoolStoragePath) + ?? panic("Could not borrow reference to internal TidalProtocol Pool resource") + } + + access(self) view fun borrowMOETMinter(): &MOET.Minter { + return self.account.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath) + ?? panic("Could not borrow reference to internal MOET Minter resource") + } + + init() { + self.PoolStoragePath = StoragePath(identifier: "tidalProtocolPool_\(self.account.address)")! + self.PoolFactoryPath = StoragePath(identifier: "tidalProtocolPoolFactory_\(self.account.address)")! + self.PoolPublicPath = PublicPath(identifier: "tidalProtocolPool_\(self.account.address)")! + + // save Pool in storage & configure public Capability + self.account.storage.save( + <-create PoolFactory(), + to: self.PoolFactoryPath + ) + let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)! + } } From 34d2ecd36a66d8a60ef696026c4c9edd5426d3b4 Mon Sep 17 00:00:00 2001 From: Kan Zhang Date: Thu, 19 Jun 2025 18:48:00 -0700 Subject: [PATCH 78/80] Add rebalance test --- cadence/tests/test_helpers.cdc | 6 +++++ cadence/tests/tracer_strategy_test.cdc | 33 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 7a7c48ea..0038a241 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -232,6 +232,12 @@ fun closeTide(signer: Test.TestAccount, id: UInt64, beFailed: Bool) { Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun rebalanceTide(signer: Test.TestAccount, id: UInt64, force: Bool, beFailed: Bool) { + let res = _executeTransaction("../transactions/tidal-yield/admin/rebalance_auto_balancer_by_id.cdc", [id, force], signer) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + /* --- Mock helpers --- */ access(all) diff --git a/cadence/tests/tracer_strategy_test.cdc b/cadence/tests/tracer_strategy_test.cdc index 88dc1cec..40f08f9e 100644 --- a/cadence/tests/tracer_strategy_test.cdc +++ b/cadence/tests/tracer_strategy_test.cdc @@ -106,6 +106,39 @@ fun test_CloseTideSucceeds() { closeTide(signer: user, id: tideIDs![0], beFailed: false) + tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(0, tideIDs!.length) +} + +access(all) +fun test_RebalanceTideSucceeds() { + Test.reset(to: snapshot) + + let fundingAmount = 100.0 + + let user = Test.createAccount() + mintFlow(to: user, amount: fundingAmount) + + createTide( + signer: user, + strategyIdentifier: strategyIdentifier, + vaultIdentifier: flowTokenIdentifier, + amount: fundingAmount, + beFailed: false + ) + + var tideIDs = getTideIDs(address: user.address) + Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") + Test.assertEqual(1, tideIDs!.length) + + setMockOraclePrice(signer: tidalYieldAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.1) + + log("Rebalancing Tide...") + rebalanceTide(signer: tidalYieldAccount, id: tideIDs![0], force: true, beFailed: false) + + closeTide(signer: user, id: tideIDs![0], beFailed: false) + tideIDs = getTideIDs(address: user.address) Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") Test.assertEqual(0, tideIDs!.length) From 059cd1a3b66e42b7c829962ef45d06be41a5f715 Mon Sep 17 00:00:00 2001 From: Kan Zhang Date: Thu, 19 Jun 2025 18:53:56 -0700 Subject: [PATCH 79/80] Update to latest defi blocks --- DeFiBlocks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DeFiBlocks b/DeFiBlocks index 71465264..a2dcbd50 160000 --- a/DeFiBlocks +++ b/DeFiBlocks @@ -1 +1 @@ -Subproject commit 7146526409c18321dc1e0244862b2a47f9e53abc +Subproject commit a2dcbd506152a31fe713617f8ab684315d4cbf44 From 8661eac97305e40b950a35d31cdfc9d04bbb8e7f Mon Sep 17 00:00:00 2001 From: Kan Zhang Date: Fri, 20 Jun 2025 12:28:53 -0700 Subject: [PATCH 80/80] Make adjustment to allow closing the tide and retain the full collateral (#11) * Make adjustments to allow closing the tide and retain the full collateral * Add reserves to tests to allow closing tide after yield value increase --- cadence/contracts/TidalYieldStrategies.cdc | 2 +- .../internal-dependencies/TidalProtocol.cdc | 2 +- .../mocks/MockTidalProtocolConsumer.cdc | 60 +++++++++++++++ cadence/scripts/tokens/get_balance.cdc | 14 +--- cadence/tests/test_helpers.cdc | 7 ++ cadence/tests/tracer_strategy_test.cdc | 37 +++++++-- .../position/create_wrapped_position.cdc | 75 +++++++++++++++++++ flow.json | 7 ++ 8 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 cadence/contracts/mocks/MockTidalProtocolConsumer.cdc create mode 100644 cadence/transactions/mocks/position/create_wrapped_position.cdc diff --git a/cadence/contracts/TidalYieldStrategies.cdc b/cadence/contracts/TidalYieldStrategies.cdc index ea3dcb6e..a26024b3 100644 --- a/cadence/contracts/TidalYieldStrategies.cdc +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -52,7 +52,7 @@ access(all) contract TidalYieldStrategies { self.uniqueID = id self.position = position self.sink = position.createSink(type: collateralType) - self.source = position.createSource(type: collateralType) + self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) } // Inherited from Tidal.Strategy default implementation diff --git a/cadence/contracts/internal-dependencies/TidalProtocol.cdc b/cadence/contracts/internal-dependencies/TidalProtocol.cdc index 7d9c4873..2e73656a 100644 --- a/cadence/contracts/internal-dependencies/TidalProtocol.cdc +++ b/cadence/contracts/internal-dependencies/TidalProtocol.cdc @@ -1259,7 +1259,7 @@ access(all) contract TidalProtocol { // We will hit the health target before using up all of the withdraw token credit. We can easily // compute how many units of the token would bring the position down to the target health. let availableHealth = healthAfterDeposit - targetHealth - let availableEffectiveValue = availableHealth * effectiveDebtAfterDeposit + let availableEffectiveValue = effectiveDebtAfterDeposit == 0.0 ? effectiveCollateralAfterDeposit : availableHealth * effectiveDebtAfterDeposit // The amount of the token we can take using that amount of health let availableTokenCount = availableEffectiveValue / self.collateralFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! diff --git a/cadence/contracts/mocks/MockTidalProtocolConsumer.cdc b/cadence/contracts/mocks/MockTidalProtocolConsumer.cdc new file mode 100644 index 00000000..b3afb67f --- /dev/null +++ b/cadence/contracts/mocks/MockTidalProtocolConsumer.cdc @@ -0,0 +1,60 @@ +import "FungibleToken" + +import "DFB" +import "TidalProtocol" + +/// THIS CONTRACT IS NOT SAFE FOR PRODUCTION - FOR TEST USE ONLY +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +/// A simple contract enabling the persistent storage of a Position similar to a pattern expected for platforms +/// building on top of TidalProtocol's lending protocol +/// +access(all) contract MockTidalProtocolConsumer { + + /// Canonical path for where the wrapper is to be stored + access(all) let WrapperStoragePath: StoragePath + + /// Opens a TidalProtocol Position and returns a PositionWrapper containing that new position + /// + access(all) + fun createPositionWrapper( + collateral: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): @PositionWrapper { + return <- create PositionWrapper( + position: TidalProtocol.openPosition( + collateral: <-collateral, + issuanceSink: issuanceSink, + repaymentSource: repaymentSource, + pushToDrawDownSink: pushToDrawDownSink + ) + ) + } + + /// A simple resource encapsulating a TidalProtocol Position + access(all) resource PositionWrapper { + + access(self) let position: TidalProtocol.Position + + init(position: TidalProtocol.Position) { + self.position = position + } + + /// NOT SAFE FOR PRODUCTION + /// + /// Returns a reference to the wrapped Position + access(all) fun borrowPosition(): &TidalProtocol.Position { + return &self.position + } + + access(all) fun borrowPositionForWithdraw(): auth(FungibleToken.Withdraw) &TidalProtocol.Position { + return &self.position + } + } + + init() { + self.WrapperStoragePath = /storage/tidalProtocolPositionWrapper + } +} diff --git a/cadence/scripts/tokens/get_balance.cdc b/cadence/scripts/tokens/get_balance.cdc index 9fecdaea..1617e427 100644 --- a/cadence/scripts/tokens/get_balance.cdc +++ b/cadence/scripts/tokens/get_balance.cdc @@ -1,14 +1,8 @@ import "FungibleToken" -/// Returns the balance of the stored Vault at the given address if exists, otherwise nil +/// Returns a account's balance of a FungibleToken Vault with public Capability published at the provided path /// -/// @param address: The address of the account that owns the vault -/// @param vaultStoragePath: The StoragePath where the Vault can be found -/// -/// @returns The balance of the stored Vault at the given address or `nil` if the Vault is not found -/// -access(all) fun main(address: Address, vaultStoragePath: StoragePath): UFix64? { - return getAuthAccount(address).storage.borrow<&{FungibleToken.Vault}>( - from: vaultStoragePath - )?.balance ?? nil +access(all) +fun main(address: Address, vaultPublicPath: PublicPath): UFix64? { + return getAccount(address).capabilities.borrow<&{FungibleToken.Vault}>(vaultPublicPath)?.balance ?? nil } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 0038a241..86400045 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -85,6 +85,13 @@ access(all) fun deployContracts() { arguments: [] ) Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "MockTidalProtocolConsumer", + path: "../contracts/mocks/MockTidalProtocolConsumer.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // TidalYield contracts err = Test.deployContract( diff --git a/cadence/tests/tracer_strategy_test.cdc b/cadence/tests/tracer_strategy_test.cdc index 40f08f9e..e3106791 100644 --- a/cadence/tests/tracer_strategy_test.cdc +++ b/cadence/tests/tracer_strategy_test.cdc @@ -17,6 +17,9 @@ access(all) var flowTokenIdentifier = Type<@FlowToken.Vault>().identifier access(all) var yieldTokenIdentifier = Type<@YieldToken.Vault>().identifier access(all) var moetTokenIdentifier = Type<@MOET.Vault>().identifier +access(all) let collateralFactor = 0.8 +access(all) let targetHealthFactor = 1.3 + access(all) var snapshot: UInt64 = 0 access(all) @@ -28,10 +31,11 @@ fun setup() { setMockOraclePrice(signer: tidalYieldAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) // mint tokens & set liquidity in mock swapper contract + let reserveAmount = 100_000_00.0 setupYieldVault(protocolAccount, beFailed: false) - mintFlow(to: protocolAccount, amount: 100_000_00.0) - mintMoet(signer: protocolAccount, to: protocolAccount.address, amount: 100_000_00.0, beFailed: false) - mintYield(signer: yieldTokenAccount, to: protocolAccount.address, amount: 100_000_00.0, beFailed: false) + mintFlow(to: protocolAccount, amount: reserveAmount) + mintMoet(signer: protocolAccount, to: protocolAccount.address, amount: reserveAmount, beFailed: false) + mintYield(signer: yieldTokenAccount, to: protocolAccount.address, amount: reserveAmount, beFailed: false) setMockSwapperLiquidityConnector(signer: protocolAccount, vaultStoragePath: MOET.VaultStoragePath) setMockSwapperLiquidityConnector(signer: protocolAccount, vaultStoragePath: YieldToken.VaultStoragePath) setMockSwapperLiquidityConnector(signer: protocolAccount, vaultStoragePath: /storage/flowTokenVault) @@ -47,6 +51,15 @@ fun setup() { depositCapacityCap: 1_000_000.0 ) + // open wrapped position (pushToDrawDownSink) + // the equivalent of depositing reserves + let openRes = executeTransaction( + "../transactions/mocks/position/create_wrapped_position.cdc", + [reserveAmount/2.0, /storage/flowTokenVault, true], + protocolAccount + ) + Test.expect(openRes, Test.beSucceeded()) + // enable mocked Strategy creation addStrategyComposer(signer: tidalYieldAccount, strategyIdentifier: strategyIdentifier, @@ -55,6 +68,7 @@ fun setup() { beFailed: false ) + snapshot = getCurrentBlockHeight() } @@ -109,6 +123,10 @@ fun test_CloseTideSucceeds() { tideIDs = getTideIDs(address: user.address) Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") Test.assertEqual(0, tideIDs!.length) + + let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + Test.assertEqual(fundingAmount, flowBalanceAfter) } access(all) @@ -116,8 +134,12 @@ fun test_RebalanceTideSucceeds() { Test.reset(to: snapshot) let fundingAmount = 100.0 + let priceIncrease = 1.5 let user = Test.createAccount() + + // Likely 0.0 + let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! mintFlow(to: user, amount: fundingAmount) createTide( @@ -132,9 +154,8 @@ fun test_RebalanceTideSucceeds() { Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") Test.assertEqual(1, tideIDs!.length) - setMockOraclePrice(signer: tidalYieldAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.1) + setMockOraclePrice(signer: tidalYieldAccount, forTokenIdentifier: yieldTokenIdentifier, price: priceIncrease) - log("Rebalancing Tide...") rebalanceTide(signer: tidalYieldAccount, id: tideIDs![0], force: true, beFailed: false) closeTide(signer: user, id: tideIDs![0], beFailed: false) @@ -142,4 +163,10 @@ fun test_RebalanceTideSucceeds() { tideIDs = getTideIDs(address: user.address) Test.assert(tideIDs != nil, message: "Expected user's Tide IDs to be non-nil but encountered nil") Test.assertEqual(0, tideIDs!.length) + + let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + let expectedBalance = fundingAmount * (collateralFactor / targetHealthFactor) * priceIncrease + Test.assert((flowBalanceAfter-flowBalanceBefore) >= expectedBalance, + message: "Expected user's Flow balance after rebalance to be at least \(expectedBalance) but got \(flowBalanceAfter)" + ) } \ No newline at end of file diff --git a/cadence/transactions/mocks/position/create_wrapped_position.cdc b/cadence/transactions/mocks/position/create_wrapped_position.cdc new file mode 100644 index 00000000..a71e5544 --- /dev/null +++ b/cadence/transactions/mocks/position/create_wrapped_position.cdc @@ -0,0 +1,75 @@ +import "FungibleToken" + +import "DFB" +import "FungibleTokenStack" + +import "MOET" +import "MockTidalProtocolConsumer" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Opens a Position with the amount of funds source from the Vault at the provided StoragePath and wraps it in a +/// MockTidalProtocolConsumer PositionWrapper +/// +transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: Bool) { + + // the funds that will be used as collateral for a TidalProtocol loan + let collateral: @{FungibleToken.Vault} + // this DeFiBlocks Sink that will receive the loaned funds + let sink: {DFB.Sink} + // DEBUG: this DeFiBlocks Source that will allow for the repayment of a loan if the position becomes undercollateralized + let source: {DFB.Source} + // the signer's account in which to store a PositionWrapper + let account: auth(SaveValue) &Account + + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // configure a MOET Vault to receive the loaned amount + if signer.storage.type(at: MOET.VaultStoragePath) == nil { + // save a new MOET Vault + signer.storage.save(<-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()), to: MOET.VaultStoragePath) + // issue un-entitled Capability + let vaultCap = signer.capabilities.storage.issue<&MOET.Vault>(MOET.VaultStoragePath) + // publish receiver Capability, unpublishing any that may exist to prevent collision + signer.capabilities.unpublish(MOET.VaultPublicPath) + signer.capabilities.publish(vaultCap, at: MOET.VaultPublicPath) + } + // assign a Vault Capability to be used in the VaultSink + let depositVaultCap = signer.capabilities.get<&{FungibleToken.Vault}>(MOET.VaultPublicPath) + let withdrawVaultCap = signer.capabilities.storage.issue(MOET.VaultStoragePath) + assert(depositVaultCap.check(), + message: "Invalid MOET Vault public Capability issued - ensure the Vault is properly configured") + assert(withdrawVaultCap.check(), + message: "Invalid MOET Vault private Capability issued - ensure the Vault is properly configured") + + // withdraw the collateral from the signer's stored Vault + let collateralSource = signer.storage.borrow(from: vaultStoragePath) + ?? panic("Could not borrow reference to Vault from \(vaultStoragePath)") + self.collateral <- collateralSource.withdraw(amount: amount) + // construct the DeFiBlocks Sink that will receive the loaned amount + self.sink = FungibleTokenStack.VaultSink( + max: nil, + depositVault: depositVaultCap, + uniqueID: nil + ) + self.source = FungibleTokenStack.VaultSource( + min: nil, + withdrawVault: withdrawVaultCap, + uniqueID: nil + ) + + // assign the signer's account enabling the execute block to save the wrapper + self.account = signer + } + + execute { + // open a position & save in the Wrapper + let wrapper <- MockTidalProtocolConsumer.createPositionWrapper( + collateral: <-self.collateral, + issuanceSink: self.sink, + repaymentSource: self.source, + pushToDrawDownSink: pushToDrawDownSink + ) + // save the wrapper into the signer's account - reverts on storage collision + self.account.storage.save(<-wrapper, to: MockTidalProtocolConsumer.WrapperStoragePath) + } +} diff --git a/flow.json b/flow.json index 00e0455a..59c591ab 100644 --- a/flow.json +++ b/flow.json @@ -48,6 +48,13 @@ "testing": "0000000000000009" } }, + "MockTidalProtocolConsumer": { + "source": "./cadence/contracts/mocks/MockTidalProtocolConsumer.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000008" + } + }, "SwapStack": { "source": "./DeFiBlocks/cadence/contracts/connectors/SwapStack.cdc", "aliases": {