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 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 diff --git a/DeFiBlocks b/DeFiBlocks index 71465264..a2dcbd50 160000 --- a/DeFiBlocks +++ b/DeFiBlocks @@ -1 +1 @@ -Subproject commit 7146526409c18321dc1e0244862b2a47f9e53abc +Subproject commit a2dcbd506152a31fe713617f8ab684315d4cbf44 diff --git a/cadence/contracts/Tidal.cdc b/cadence/contracts/Tidal.cdc index 6de5a2af..eb692cd7 100644 --- a/cadence/contracts/Tidal.cdc +++ b/cadence/contracts/Tidal.cdc @@ -1,17 +1,27 @@ +// standards import "FungibleToken" import "Burner" import "ViewResolver" - +// DeFiBlocks import "DFB" -/// /// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /// access(all) contract Tidal { + /* --- FIELDS --- */ + + /// 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 + + /* --- 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) @@ -19,117 +29,309 @@ 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(all) fun createTideManager(): @TideManager { - return <-create TideManager() + /* --- CONSTRUCTS --- */ + + /// Strategy + /// + /// 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 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 + /// 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, 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 + 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}) { + 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)" + } + } } - /* --- CONSTRUCTS --- */ + /// 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 + /// 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. + 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 + 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, + uniqueID: DFB.UniqueIdentifier, + withFunds: @{FungibleToken.Vault} + ): @{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)" + } + } + } - access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, FungibleToken.Provider, ViewResolver.Resolver, DFB.IdentifiableResource { - access(contract) let uniqueID: DFB.UniqueIdentifier? - access(self) let vault: @{FungibleToken.Vault} + /// StrategyFactory + /// + /// This resource enables the management of StrategyComposers and the construction of the Strategies they compose. + /// + access(all) resource StrategyFactory { + /// A mapping of StrategyComposers indexed on the related Strategies they can compose + access(self) let composers: @{Type: {StrategyComposer}} - init(_ vault: @{FungibleToken.Vault}) { - self.vault <- vault - self.uniqueID = DFB.UniqueIdentifier() + init() { + self.composers <- {} } - access(all) view fun id(): UInt64 { - return self.uniqueID!.id + /// 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) + ?? {} + } + /// 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._borrowComposer(forStrategy: type) + .createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds) + } + /// 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)" + } + 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 { + 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)") + } + } - access(all) view fun getTideBalance(): UFix64 { - return self.vault.balance + /// 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. + 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)" + } } + } - access(contract) fun burnCallback() { - emit BurnedTide(id: self.uniqueID!.id, idType: self.uniqueID!.getType().identifier,remainingBalance: self.vault.balance) + /// Tide + /// + /// 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 stack + access(self) var strategy: @{Strategy}? + + init(strategyType: Type, withVault: @{FungibleToken.Vault}) { + self.uniqueID = DFB.UniqueIdentifier() + self.vaultType = withVault.getType() + let _strategy <- Tidal.createStrategy( + type: strategyType, + uniqueID: self.uniqueID, + withFunds: <-withVault + ) + assert(_strategy.isSupportedCollateralType(self.vaultType), + message: "Vault type \(self.vaultType.identifier) is not supported by Strategy \(strategyType.identifier)") + self.strategy <-_strategy } + /// 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._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] { 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()): "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) + 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: "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 access(all) view fun getSupportedVaultTypes(): {Type: Bool} { - return { self.vault.getType() : true } + return self._borrowStrategy().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 } - - access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { - return amount <= self.vault.balance - } - + /// Withdraws the requested amount from the Strategy 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: + "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 res <- self.vault.withdraw(amount: amount) - emit WithdrawnFromTide(id: self.uniqueID!.id, idType: self.uniqueID!.getType().identifier, amount: amount, owner: self.owner?.address, toUUID: res.uuid) + 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._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 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())") + } } - 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 { + /// 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 } - - access(all) fun createTide(withVault: @{FungibleToken.Vault}) { + /// 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(<-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) } - + /// 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: - "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 } - + /// 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: @@ -138,16 +340,18 @@ access(all) contract Tidal { 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" @@ -155,8 +359,9 @@ access(all) contract Tidal { 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" @@ -168,9 +373,54 @@ access(all) contract Tidal { } } + /* --- 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, 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 { + return <-create TideManager() + } + /// Creates a StrategyFactory resource + access(all) fun createStrategyFactory(): @StrategyFactory { + return <- create StrategyFactory() + } + + /* --- 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)") + } + init() { - let pathIdentifier = "TidalTideManager_\(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/TidalYieldAutoBalancers.cdc b/cadence/contracts/TidalYieldAutoBalancers.cdc new file mode 100644 index 00000000..e06dc22d --- /dev/null +++ b/cadence/contracts/TidalYieldAutoBalancers.cdc @@ -0,0 +1,123 @@ +// 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. + 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 + ): 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 + 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)") + return autoBalancerRef + } + + /// 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)") + } + + /// 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 new file mode 100644 index 00000000..a26024b3 --- /dev/null +++ b/cadence/contracts/TidalYieldStrategies.cdc @@ -0,0 +1,210 @@ +// standards +import "FungibleToken" +import "FlowToken" +// DeFiBlocks +import "DFBUtils" +import "DFB" +import "SwapStack" +// Lending protocol +import "TidalProtocol" +// TidalYield platform +import "Tidal" +import "TidalYieldAutoBalancers" +// tokens +import "YieldToken" +import "MOET" +// mocks +import "MockOracle" +import "MockSwapper" + +/// 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 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 DeFiBlocks stacks. +/// +/// 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 { + + /// 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 & + /// 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 { + /// 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.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) + } + + // Inherited from Tidal.Strategy default implementation + // access(all) view fun isSupportedCollateralType(_ type: Type): Bool + + access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { + return { self.sink.getSinkType(): true } + } + /// 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) + } + /// 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 + 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 } + } + + /// 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, + uniqueID: DFB.UniqueIdentifier, + withFunds: @{FungibleToken.Vault} + ): @{Tidal.Strategy} { + // this PriceOracle is mocked and will be shared by all components used in the TracerStrategy + let oracle = MockOracle.PriceOracle() + + // assign 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, // 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)") + // enables withdrawals of YieldToken from the AutoBalancer + let abaSource = autoBalancer.createBalancerSource() ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") + + // init MOET <> YIELD swappers + // + // MOET -> YieldToken + let moetToYieldSwapper = MockSwapper.Swapper( + inVault: moetTokenType, + outVault: yieldTokenType, + uniqueID: uniqueID + ) + // YieldToken -> MOET + let yieldToMoetSwapper = MockSwapper.Swapper( + inVault: yieldTokenType, + outVault: moetTokenType, + 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: uniqueID) + // Swaps YieldToken & provides swapped MOET, sourcing YieldToken from the AutoBalancer + let abaSwapSource = SwapStack.SwapSource(swapper: yieldToMoetSwapper, source: abaSource, uniqueID: uniqueID) + + // 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, // TODO: before + uniqueID: uniqueID + ) + // allows for YieldToken to be deposited to the Position + 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 + autoBalancer.setSink(positionSwapSink) + + return <-create TracerStrategy( + id: DFB.UniqueIdentifier(), + collateralType: collateralType, + position: position + ) + } + } + + /// 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) view fun getSupportedComposers(): {Type: Bool} { + return { Type<@TracerStrategyComposer>(): true } + } + access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { + switch type { + case Type<@TracerStrategyComposer>(): + return <- create TracerStrategyComposer() + 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) + } +} diff --git a/cadence/contracts/internal-dependencies/TidalProtocol.cdc b/cadence/contracts/internal-dependencies/TidalProtocol.cdc new file mode 100644 index 00000000..2e73656a --- /dev/null +++ b/cadence/contracts/internal-dependencies/TidalProtocol.cdc @@ -0,0 +1,1740 @@ +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 + + /* --- 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 = 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)! + + 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)! + } +} diff --git a/cadence/contracts/internal-dependencies/tokens/USDA.cdc b/cadence/contracts/internal-dependencies/tokens/MOET.cdc similarity index 84% rename from cadence/contracts/internal-dependencies/tokens/USDA.cdc rename to cadence/contracts/internal-dependencies/tokens/MOET.cdc index c8f9081a..09aaa3db 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,21 @@ 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 - self.balance = self.balance + vault.balance + let vault <- from as! @MOET.Vault + let amount = vault.balance + vault.balance = 0.0 destroy vault + + self.balance = self.balance + amount } - access(all) fun createEmptyVault(): @USDA.Vault { + access(all) fun createEmptyVault(): @MOET.Vault { return <-create Vault(balance: 0.0) } } @@ -175,8 +178,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 +191,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 +202,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/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 { diff --git a/cadence/contracts/mocks/MockStrategy.cdc b/cadence/contracts/mocks/MockStrategy.cdc new file mode 100644 index 00000000..555777e2 --- /dev/null +++ b/cadence/contracts/mocks/MockStrategy.cdc @@ -0,0 +1,140 @@ +import "FungibleToken" +import "FlowToken" + +import "DFBUtils" +import "DFB" + +import "Tidal" + +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +access(all) contract MockStrategy { + + access(all) let IssuerStoragePath : StoragePath + + access(all) struct Sink : 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 Source : 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) 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? + 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: 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) + } + + access(contract) fun burnCallback() {} // no-op + } + + access(all) resource StrategyComposer : Tidal.StrategyComposer { + access(all) view fun getComposedStrategyTypes(): {Type: Bool} { + return { Type<@Strategy>(): true } + } + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + return { Type<@FlowToken.Vault>(): true } + } + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return { Type<@FlowToken.Vault>(): true } + } + access(all) fun createStrategy( + _ type: Type, + uniqueID: DFB.UniqueIdentifier, + withFunds: @{FungibleToken.Vault} + ): @{Tidal.Strategy} { + let id = DFB.UniqueIdentifier() + let strat <- create Strategy( + id: 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 : Tidal.StrategyComposerIssuer { + access(all) view fun getSupportedComposers(): {Type: Bool} { + return { Type<@StrategyComposer>(): true } + } + access(all) fun issueComposer(_ type: Type): @{Tidal.StrategyComposer} { + switch type { + case Type<@StrategyComposer>(): + return <- create StrategyComposer() + default: + panic("Unsupported StrategyComposer requested: \(type.identifier)") + } + } + } + + init() { + self.IssuerStoragePath = StoragePath(identifier: "MockStrategyComposerIssuer_\(self.account.address)")! + + self.account.storage.save(<-create StrategyComposerIssuer(), to: self.IssuerStoragePath) + } +} 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/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)") } 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/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/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..8240d644 --- /dev/null +++ b/cadence/scripts/tidal-yield/get_auto_balancer_balance_by_id.cdc @@ -0,0 +1,8 @@ +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_supported_strategies.cdc b/cadence/scripts/tidal-yield/get_supported_strategies.cdc new file mode 100644 index 00000000..0abb8b7c --- /dev/null +++ b/cadence/scripts/tidal-yield/get_supported_strategies.cdc @@ -0,0 +1,7 @@ +import "Tidal" + +/// Returns the Strategy Types currently supported by Tidal +/// +access(all) fun main(): [Type] { + return Tidal.getSupportedStrategies() +} 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..c2289693 --- /dev/null +++ b/cadence/scripts/tidal-yield/get_tide_balance.cdc @@ -0,0 +1,18 @@ +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 +/// 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<&Tidal.TideManager>(Tidal.TideManagerPublicPath) + ?.borrowTide(id: id) + ?? nil + return tide?.getTideBalance() ?? nil +} diff --git a/cadence/scripts/tidal/get_tide_ids.cdc b/cadence/scripts/tidal-yield/get_tide_ids.cdc similarity index 99% rename from cadence/scripts/tidal/get_tide_ids.cdc rename to cadence/scripts/tidal-yield/get_tide_ids.cdc index 48d41849..e7eaed2d 100644 --- a/cadence/scripts/tidal/get_tide_ids.cdc +++ b/cadence/scripts/tidal-yield/get_tide_ids.cdc @@ -5,6 +5,7 @@ import "Tidal" /// @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) diff --git a/cadence/scripts/tokens/get_balance.cdc b/cadence/scripts/tokens/get_balance.cdc index 331f2614..1617e427 100644 --- a/cadence/scripts/tokens/get_balance.cdc +++ b/cadence/scripts/tokens/get_balance.cdc @@ -1,15 +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 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 +access(all) +fun main(address: Address, vaultPublicPath: PublicPath): UFix64? { + return getAccount(address).capabilities.borrow<&{FungibleToken.Vault}>(vaultPublicPath)?.balance ?? nil } 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..86400045 --- /dev/null +++ b/cadence/tests/test_helpers.cdc @@ -0,0 +1,268 @@ +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()) + err = Test.deployContract( + name: "FungibleTokenStack", + path: "../../DeFiBlocks/cadence/contracts/connectors/FungibleTokenStack.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()) + + err = Test.deployContract( + name: "MockTidalProtocolConsumer", + path: "../contracts/mocks/MockTidalProtocolConsumer.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: "Tidal", + path: "../contracts/Tidal.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()) +} + +access(all) +fun setupTidalProtocol(signer: Test.TestAccount) { + let res = _executeTransaction("../transactions/tidal-protocol/create_and_store_pool.cdc", + [], + signer + ) +} + +/* --- 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? +} + +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) +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 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 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", + [ strategyIdentifier, composerIdentifier, issuerStoragePath ], + signer + ) + Test.expect(addRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun createTide( + signer: Test.TestAccount, + strategyIdentifier: String, + vaultIdentifier: String, + amount: UFix64, + beFailed: Bool +) { + let res = _executeTransaction("../transactions/tidal-yield/create_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()) +} + +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) +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 new file mode 100644 index 00000000..df328edf --- /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) + + 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 diff --git a/cadence/tests/tracer_strategy_test.cdc b/cadence/tests/tracer_strategy_test.cdc new file mode 100644 index 00000000..e3106791 --- /dev/null +++ b/cadence/tests/tracer_strategy_test.cdc @@ -0,0 +1,172 @@ +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) let collateralFactor = 0.8 +access(all) let targetHealthFactor = 1.3 + +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 + let reserveAmount = 100_000_00.0 + setupYieldVault(protocolAccount, 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) + + // 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 + ) + + // 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, + 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) + + let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + Test.assertEqual(fundingAmount, flowBalanceAfter) +} + +access(all) +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( + 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: priceIncrease) + + 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) + + 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/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_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/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 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..f540c3c9 --- /dev/null +++ b/cadence/transactions/tidal-yield/admin/add_strategy_composer.cdc @@ -0,0 +1,36 @@ +import "Tidal" + +/// 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 +/// +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: @{Tidal.StrategyComposer} + /// Authorized reference to the StrategyFactory to which the Strategy Type & StrategyComposer will be added + let factory: auth(Mutate) &Tidal.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<&{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: Tidal.FactoryStoragePath) + ?? panic("Could not borrow reference to StrategyFactory from \(Tidal.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/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) + } +} diff --git a/cadence/transactions/tidal/close_tide.cdc b/cadence/transactions/tidal-yield/close_tide.cdc similarity index 92% rename from cadence/transactions/tidal/close_tide.cdc rename to cadence/transactions/tidal-yield/close_tide.cdc index e7e596e4..de833c39 100644 --- a/cadence/transactions/tidal/close_tide.cdc +++ b/cadence/transactions/tidal-yield/close_tide.cdc @@ -10,12 +10,12 @@ 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(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: Tidal.TideManagerStoragePath) + 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") diff --git a/cadence/transactions/tidal/open_tide.cdc b/cadence/transactions/tidal-yield/create_tide.cdc similarity index 77% rename from cadence/transactions/tidal/open_tide.cdc rename to cadence/transactions/tidal-yield/create_tide.cdc index c2c5fad2..70079edb 100644 --- a/cadence/transactions/tidal/open_tide.cdc +++ b/cadence/transactions/tidal-yield/create_tide.cdc @@ -6,15 +6,22 @@ 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 +/// 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(vaultIdentifier: String, amount: UFix64) { +transaction(strategyIdentifier: String, vaultIdentifier: String, amount: UFix64) { let manager: &Tidal.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") @@ -37,7 +44,7 @@ transaction(vaultIdentifier: String, amount: UFix64) { 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( + signer.capabilities.storage.issue<&Tidal.TideManager>( Tidal.TideManagerStoragePath ) } @@ -46,6 +53,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/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/setup.cdc b/cadence/transactions/tidal-yield/setup.cdc similarity index 91% rename from cadence/transactions/tidal/setup.cdc rename to cadence/transactions/tidal-yield/setup.cdc index 37369730..b35bbe01 100644 --- a/cadence/transactions/tidal/setup.cdc +++ b/cadence/transactions/tidal-yield/setup.cdc @@ -17,7 +17,7 @@ transaction { 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) + signer.capabilities.storage.issue(Tidal.TideManagerStoragePath) // confirm setup of TideManager at canonical path let storedType = signer.storage.type(at: Tidal.TideManagerStoragePath) ?? Type() diff --git a/cadence/transactions/tidal/withdraw_from_tide.cdc b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc similarity index 93% rename from cadence/transactions/tidal/withdraw_from_tide.cdc rename to cadence/transactions/tidal-yield/withdraw_from_tide.cdc index c2db4bd1..3f54e98f 100644 --- a/cadence/transactions/tidal/withdraw_from_tide.cdc +++ b/cadence/transactions/tidal-yield/withdraw_from_tide.cdc @@ -11,12 +11,12 @@ 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(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: Tidal.TideManagerStoragePath) + 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") diff --git a/cadence/transactions/usda/mint.cdc b/cadence/transactions/usda/mint.cdc deleted file mode 100644 index 2fb29eb8..00000000 --- a/cadence/transactions/usda/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/usda/setup.cdc b/cadence/transactions/usda/setup.cdc deleted file mode 100644 index 7dbe9c1b..00000000 --- a/cadence/transactions/usda/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/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") } } diff --git a/flow.json b/flow.json index beb10896..59c591ab 100644 --- a/flow.json +++ b/flow.json @@ -3,55 +3,98 @@ "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" } }, + "MOET": { + "source": "cadence/contracts/internal-dependencies/tokens/MOET.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "MockOracle": { "source": "cadence/contracts/mocks/MockOracle.cdc", "aliases": { - "testing": "0000000000000008" + "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" + } + }, + "MockTidalProtocolConsumer": { + "source": "./cadence/contracts/mocks/MockTidalProtocolConsumer.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000008" } }, "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": { - "testing": "0000000000000008" + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000009" } }, - "USDA": { - "source": "cadence/contracts/internal-dependencies/tokens/USDA.cdc", + "TidalYieldAutoBalancers": { + "source": "cadence/contracts/TidalYieldAutoBalancers.cdc", "aliases": { - "testing": "0000000000000007" + "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": { - "testing": "0000000000000007" + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000010" } } }, @@ -142,7 +185,14 @@ "type": "file", "location": "emulator-account.pkey" } - } + }, + "test-user": { + "address": "179b6b1cb6755e31", + "key": { + "type": "file", + "location": "test-user.pkey" + } + } }, "deployments": { "emulator": { @@ -152,19 +202,20 @@ "FungibleTokenStack", "SwapStack", { - "name": "USDA", + "name": "MOET", "args": [ { - "value": "1000000.0", + "value": "1000000.00000000", "type": "UFix64" } ] }, + "TidalProtocol", { "name": "YieldToken", "args": [ { - "value": "1000000.0", + "value": "1000000.00000000", "type": "UFix64" } ] @@ -173,14 +224,16 @@ "name": "MockOracle", "args": [ { - "value": "A.f8d6e0586b0a20c7.USDA.Vault", + "value": "A.f8d6e0586b0a20c7.MOET.Vault", "type": "String" } ] }, "MockSwapper", - "Tidal" + "TidalYieldAutoBalancers", + "Tidal", + "TidalYieldStrategies" ] } } -} \ No newline at end of file +} diff --git a/local/setup_emulator.sh b/local/setup_emulator.sh old mode 100644 new mode 100755 index 3efe60c4..c8f6bb3b --- 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