diff --git a/cadence/contracts/mocks/MockYieldToken.cdc b/cadence/contracts/mocks/MockYieldToken.cdc new file mode 100644 index 00000000..e5381918 --- /dev/null +++ b/cadence/contracts/mocks/MockYieldToken.cdc @@ -0,0 +1,219 @@ +import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" + +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +access(all) contract MockYieldToken : FungibleToken { + + /// Total supply of MockYieldToken in existence + access(all) var totalSupply: UFix64 + + /// Storage and Public Paths + access(all) let VaultStoragePath: StoragePath + access(all) let VaultPublicPath: PublicPath + access(all) let ReceiverPublicPath: PublicPath + access(all) let AdminStoragePath: StoragePath + + /// The event that is emitted when new tokens are minted + access(all) event Minted(type: String, amount: UFix64, toUUID: UInt64, minterUUID: UInt64) + /// Emitted whenever a new Minter is created + access(all) event MinterCreated(uuid: UInt64) + + /// createEmptyVault + /// + /// Function that creates a new Vault with a balance of zero + /// and returns it to the calling context. A user must call this function + /// and store the returned Vault in their storage in order to allow their + /// account to be able to receive deposits of this token type. + /// + access(all) fun createEmptyVault(vaultType: Type): @MockYieldToken.Vault { + return <- create Vault(balance: 0.0) + } + + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + return FungibleTokenMetadataViews.FTDisplay( + name: "Mocked Yield Token", + symbol: "YIELD", + description: "A mocked token contract representing a receipt on some yield bearing vault", + externalURL: MetadataViews.ExternalURL("https://flow.com"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + return FungibleTokenMetadataViews.FTVaultData( + storagePath: self.VaultStoragePath, + receiverPath: self.ReceiverPublicPath, + metadataPath: self.VaultPublicPath, + receiverLinkedType: Type<&MockYieldToken.Vault>(), + metadataLinkedType: Type<&MockYieldToken.Vault>(), + createEmptyVaultFunction: (fun(): @{FungibleToken.Vault} { + return <-MockYieldToken.createEmptyVault(vaultType: Type<@MockYieldToken.Vault>()) + }) + ) + case Type(): + return FungibleTokenMetadataViews.TotalSupply( + totalSupply: MockYieldToken.totalSupply + ) + } + return nil + } + + /* --- CONSTRUCTS --- */ + + /// Vault + /// + /// Each user stores an instance of only the Vault in their storage + /// The functions in the Vault and governed by the pre and post conditions + /// in FungibleToken when they are called. + /// The checks happen at runtime whenever a function is called. + /// + /// Resources can only be created in the context of the contract that they + /// are defined in, so there is no way for a malicious user to create Vaults + /// out of thin air. A special Minter resource needs to be defined to mint + /// new tokens. + /// + access(all) resource Vault: FungibleToken.Vault { + + /// The total balance of this vault + access(all) var balance: UFix64 + + /// Identifies the destruction of a Vault even when destroyed outside of Buner.burn() scope + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid, balance: UFix64 = self.balance) + + init(balance: UFix64) { + self.balance = balance + } + + /// Called when a fungible token is burned via the `Burner.burn()` method + access(contract) fun burnCallback() { + if self.balance > 0.0 { + MockYieldToken.totalSupply = MockYieldToken.totalSupply - self.balance + } + self.balance = 0.0 + } + + access(all) view fun getViews(): [Type] { + return MockYieldToken.getContractViews(resourceType: nil) + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + return MockYieldToken.resolveContractView(resourceType: nil, viewType: view) + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[self.getType()] = true + return supportedTypes + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return self.getSupportedVaultTypes()[type] ?? false + } + + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return amount <= self.balance + } + + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @MockYieldToken.Vault { + self.balance = self.balance - amount + return <-create Vault(balance: amount) + } + + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let vault <- from as! @MockYieldToken.Vault + let amount = vault.balance + vault.balance = 0.0 + destroy vault + + self.balance = self.balance + amount + } + + access(all) fun createEmptyVault(): @MockYieldToken.Vault { + return <-create Vault(balance: 0.0) + } + } + + /// Minter + /// + /// Resource object that token admin accounts can hold to mint new tokens. + /// + access(all) resource Minter { + /// Identifies when a Minter is destroyed, coupling with MinterCreated event to trace Minter UUIDs + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid) + + init() { + emit MinterCreated(uuid: self.uuid) + } + + /// mintTokens + /// + /// Function that mints new tokens, adds them to the total supply, + /// and returns them to the calling context. + /// + access(all) fun mintTokens(amount: UFix64): @MockYieldToken.Vault { + MockYieldToken.totalSupply = MockYieldToken.totalSupply + amount + let vault <-create Vault(balance: amount) + emit Minted(type: vault.getType().identifier, amount: amount, toUUID: vault.uuid, minterUUID: self.uuid) + return <-vault + } + } + + init(initialMint: UFix64) { + + self.totalSupply = 0.0 + + let address = self.account.address + self.VaultStoragePath = StoragePath(identifier: "mockYieldTokenVault_\(address)")! + self.VaultPublicPath = PublicPath(identifier: "mockYieldTokenVault_\(address)")! + self.ReceiverPublicPath = PublicPath(identifier: "mockYieldTokenReceiver_\(address)")! + self.AdminStoragePath = StoragePath(identifier: "mockYieldTokenAdmin_\(address)")! + + + // Create a public capability to the stored Vault that exposes + // the `deposit` method and getAcceptedTypes method through the `Receiver` interface + // and the `balance` method through the `Balance` interface + // + self.account.storage.save(<-create Vault(balance: self.totalSupply), to: self.VaultStoragePath) + let vaultCap = self.account.capabilities.storage.issue<&MockYieldToken.Vault>(self.VaultStoragePath) + self.account.capabilities.publish(vaultCap, at: self.VaultPublicPath) + let receiverCap = self.account.capabilities.storage.issue<&MockYieldToken.Vault>(self.VaultStoragePath) + self.account.capabilities.publish(receiverCap, at: self.ReceiverPublicPath) + + // Create a Minter & mint the initial supply of tokens to the contract account's Vault + let admin <- create Minter() + + self.account.capabilities.borrow<&Vault>(self.ReceiverPublicPath)!.deposit( + from: <- admin.mintTokens(amount: initialMint) + ) + + self.account.storage.save(<-admin, to: self.AdminStoragePath) + } +} diff --git a/cadence/scripts/tidal-protocol/position_details.cdc b/cadence/scripts/tidal-protocol/position_details.cdc new file mode 100644 index 00000000..fa1aeb6d --- /dev/null +++ b/cadence/scripts/tidal-protocol/position_details.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): TidalProtocol.PositionDetails { + let protocolAddress= Type<@TidalProtocol.Pool>().address! + return getAccount(protocolAddress).capabilities.borrow<&TidalProtocol.Pool>(TidalProtocol.PoolPublicPath) + ?.getPositionDetails(pid: pid) + ?? panic("Could not find a configured TidalProtocol Pool in account \(protocolAddress) at path \(TidalProtocol.PoolPublicPath)") +} diff --git a/cadence/tests/platform_integration_test.cdc b/cadence/tests/platform_integration_test.cdc index c4abbda0..f69011ea 100644 --- a/cadence/tests/platform_integration_test.cdc +++ b/cadence/tests/platform_integration_test.cdc @@ -14,7 +14,6 @@ access(all) let protocolAccount = Test.getAccount(0x0000000000000007) access(all) var snapshot: UInt64 = 0 -access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" access(all) let flowTokenIdentifier = "A.0000000000000003.FlowToken.Vault" access(all) let flowVaultStoragePath = /storage/flowTokenVault @@ -22,14 +21,8 @@ access(all) let flowVaultStoragePath = /storage/flowTokenVault access(all) fun setup() { deployContracts() - - // Deploy MockOracle for this test suite - let err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [defaultTokenIdentifier] - ) - Test.expect(err, Test.beNil()) + + snapshot = getCurrentBlockHeight() } access(all) @@ -39,8 +32,6 @@ fun testDeploymentSucceeds() { access(all) fun testCreatePoolSucceeds() { - snapshot = getCurrentBlockHeight() - createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) let existsRes = _executeScript("../scripts/tidal-protocol/pool_exists.cdc", [protocolAccount.address]) diff --git a/cadence/tests/pool_creation_workflow_test.cdc b/cadence/tests/pool_creation_workflow_test.cdc index dc3fcb45..e58d0db8 100644 --- a/cadence/tests/pool_creation_workflow_test.cdc +++ b/cadence/tests/pool_creation_workflow_test.cdc @@ -1,4 +1,5 @@ import Test +import BlockchainHelpers import "MOET" import "test_helpers.cdc" @@ -12,8 +13,6 @@ import "test_helpers.cdc" access(all) let protocolAccount = Test.getAccount(0x0000000000000007) access(all) var snapshot: UInt64 = 0 -access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" - // ----------------------------------------------------------------------------- // SETUP // ----------------------------------------------------------------------------- @@ -21,14 +20,6 @@ access(all) fun setup() { deployContracts() - // deploy mocks required for pool creation (oracle etc.) – not strictly needed here - var err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [defaultTokenIdentifier] - ) - Test.expect(err, Test.beNil()) - snapshot = getCurrentBlockHeight() } diff --git a/cadence/tests/position_lifecycle_happy_test.cdc b/cadence/tests/position_lifecycle_happy_test.cdc index 36c44742..a3b9a6fe 100644 --- a/cadence/tests/position_lifecycle_happy_test.cdc +++ b/cadence/tests/position_lifecycle_happy_test.cdc @@ -1,4 +1,5 @@ import Test +import BlockchainHelpers import "MOET" import "test_helpers.cdc" @@ -10,35 +11,21 @@ import "test_helpers.cdc" access(all) let protocolAccount = Test.getAccount(0x0000000000000007) access(all) var snapshot: UInt64 = 0 -access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" access(all) let flowTokenIdentifier = "A.0000000000000003.FlowToken.Vault" +access(all) var yieldTokenIdentifier = "A.0000000000000007.YieldToken.Vault" access(all) let flowVaultStoragePath = /storage/flowTokenVault access(all) fun setup() { deployContracts() - var err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [defaultTokenIdentifier] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "MockTidalProtocolConsumer", - path: "../contracts/mocks/MockTidalProtocolConsumer.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - snapshot = getCurrentBlockHeight() } // ----------------------------------------------------------------------------- access(all) fun testPositionLifecycleHappyPath() { - Test.reset(to: snapshot) + // Test.reset(to: snapshot) // price setup setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) diff --git a/cadence/tests/rebalance_overcollateralised_test.cdc b/cadence/tests/rebalance_overcollateralised_test.cdc index 34dc7366..1db416d8 100644 --- a/cadence/tests/rebalance_overcollateralised_test.cdc +++ b/cadence/tests/rebalance_overcollateralised_test.cdc @@ -1,4 +1,5 @@ import Test +import BlockchainHelpers import "MOET" import "test_helpers.cdc" @@ -6,36 +7,26 @@ import "test_helpers.cdc" access(all) let protocolAccount = Test.getAccount(0x0000000000000007) access(all) var snapshot: UInt64 = 0 -access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" access(all) let flowTokenIdentifier = "A.0000000000000003.FlowToken.Vault" +access(all) let moetTokenIdentifier = "A.0000000000000007.MOET.Vault" access(all) let flowVaultStoragePath = /storage/flowTokenVault access(all) fun setup() { deployContracts() - var err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [defaultTokenIdentifier] - ) - Test.expect(err, Test.beNil()) - err = Test.deployContract( - name: "MockTidalProtocolConsumer", - path: "../contracts/mocks/MockTidalProtocolConsumer.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) + snapshot = getCurrentBlockHeight() } access(all) fun testRebalanceOvercollateralised() { - Test.reset(to: snapshot) + // Test.reset(to: snapshot) let initialPrice = 1.0 let priceIncreasePct: UFix64 = 1.2 setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: initialPrice) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: moetTokenIdentifier, price: initialPrice) - createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: moetTokenIdentifier, beFailed: false) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, @@ -58,6 +49,13 @@ fun testRebalanceOvercollateralised() { let healthBefore = getPositionHealth(pid: 0, beFailed: false) + let detailsBefore = getPositionDetails(pid: 0, beFailed: false) + + log(detailsBefore.balances[0].balance) + + // TODO: This current fails + // Test.assert(detailsBefore.balances[0].balance == 1000.0) // check initial position balance + // increase price setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: initialPrice * priceIncreasePct) diff --git a/cadence/tests/rebalance_undercollateralised_test.cdc b/cadence/tests/rebalance_undercollateralised_test.cdc index 47e60a5b..81767468 100644 --- a/cadence/tests/rebalance_undercollateralised_test.cdc +++ b/cadence/tests/rebalance_undercollateralised_test.cdc @@ -1,4 +1,5 @@ import Test +import BlockchainHelpers import "MOET" import "test_helpers.cdc" @@ -6,31 +7,19 @@ import "test_helpers.cdc" access(all) let protocolAccount = Test.getAccount(0x0000000000000007) access(all) var snapshot: UInt64 = 0 -access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" access(all) let flowTokenIdentifier = "A.0000000000000003.FlowToken.Vault" access(all) let flowVaultStoragePath = /storage/flowTokenVault access(all) fun setup() { deployContracts() - var err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [defaultTokenIdentifier] - ) - Test.expect(err, Test.beNil()) - err = Test.deployContract( - name: "MockTidalProtocolConsumer", - path: "../contracts/mocks/MockTidalProtocolConsumer.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) + snapshot = getCurrentBlockHeight() } access(all) fun testRebalanceUndercollateralised() { - Test.reset(to: snapshot) + // Test.reset(to: snapshot) let initialPrice = 1.0 let priceDropPct: UFix64 = 0.2 setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: initialPrice) diff --git a/cadence/tests/reserve_withdrawal_test.cdc b/cadence/tests/reserve_withdrawal_test.cdc index 6f82621a..59a7e93d 100644 --- a/cadence/tests/reserve_withdrawal_test.cdc +++ b/cadence/tests/reserve_withdrawal_test.cdc @@ -1,4 +1,5 @@ import Test +import BlockchainHelpers import "MOET" import "test_helpers.cdc" @@ -8,20 +9,10 @@ access(all) let treasury = Test.createAccount() access(all) var snapshot: UInt64 = 0 -access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" - access(all) fun setup() { deployContracts() - // deploy mocks required for pool creation (oracle etc.) - var err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [defaultTokenIdentifier] - ) - Test.expect(err, Test.beNil()) - // Take snapshot after setup snapshot = getCurrentBlockHeight() } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 21d3ece2..74e1f426 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -1,5 +1,8 @@ import Test -import TidalProtocol from "TidalProtocol" + +import "TidalProtocol" + +access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" /* --- Test execution helpers --- */ @@ -19,15 +22,11 @@ fun _executeTransaction(_ path: String, _ args: [AnyStruct], _ signer: Test.Test return Test.executeTransaction(txn) } -access(all) -fun executeTransaction(_ path: String, _ args: [AnyStruct], _ signer: Test.TestAccount): Test.TransactionResult { - return _executeTransaction(path, args, signer) -} - /* --- Setup helpers --- */ // Common test setup function that deploys all required contracts -access(all) fun deployContracts() { +access(all) +fun deployContracts() { var err = Test.deployContract( name: "DFBUtils", path: "./mocks/DFBUtils.cdc", @@ -63,6 +62,21 @@ access(all) fun deployContracts() { arguments: [] ) Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "MockOracle", + path: "../contracts/mocks/MockOracle.cdc", + arguments: [defaultTokenIdentifier] + ) + Test.expect(err, Test.beNil()) + + let initialYieldTokenSupply = 0.0 + err = Test.deployContract( + name: "MockYieldToken", + path: "../contracts/mocks/MockYieldToken.cdc", + arguments: [initialYieldTokenSupply] + ) + Test.expect(err, Test.beNil()) // Deploy FungibleTokenStack err = Test.deployContract( @@ -107,6 +121,15 @@ fun getPositionHealth(pid: UInt64, beFailed: Bool): UFix64 { return res.status == Test.ResultStatus.failed ? 0.0 : res.returnValue as! UFix64 } +access(all) +fun getPositionDetails(pid: UInt64, beFailed: Bool): TidalProtocol.PositionDetails { + let res = _executeScript("../scripts/tidal-protocol/position_details.cdc", + [pid] + ) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) + return res.returnValue as! TidalProtocol.PositionDetails +} + access(all) fun poolExists(address: Address): Bool { let res = _executeScript("../scripts/tidal-protocol/pool_exists.cdc", [address]) @@ -153,6 +176,22 @@ fun addSupportedTokenSimpleInterestCurve( Test.expect(additionRes, Test.beSucceeded()) } +access(all) +fun addSupportedTokenSimpleInterestCurveWithResult( + signer: Test.TestAccount, + tokenTypeIdentifier: String, + collateralFactor: UFix64, + borrowFactor: UFix64, + depositRate: UFix64, + depositCapacityCap: UFix64 +): Test.TransactionResult { + return _executeTransaction( + "../transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc", + [ tokenTypeIdentifier, collateralFactor, borrowFactor, depositRate, depositCapacityCap ], + signer + ) +} + access(all) fun rebalancePosition(signer: Test.TestAccount, pid: UInt64, force: Bool, beFailed: Bool) { let rebalanceRes = _executeTransaction( diff --git a/cadence/tests/token_governance_addition_test.cdc b/cadence/tests/token_governance_addition_test.cdc index 1cbf3d39..0cf69c47 100644 --- a/cadence/tests/token_governance_addition_test.cdc +++ b/cadence/tests/token_governance_addition_test.cdc @@ -1,4 +1,5 @@ import Test +import BlockchainHelpers import "MOET" import "test_helpers.cdc" @@ -10,28 +11,18 @@ import "test_helpers.cdc" access(all) let protocolAccount = Test.getAccount(0x0000000000000007) access(all) var snapshot: UInt64 = 0 -access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" access(all) let flowTokenIdentifier = "A.0000000000000003.FlowToken.Vault" access(all) fun setup() { deployContracts() - var err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [defaultTokenIdentifier] - ) - Test.expect(err, Test.beNil()) - snapshot = getCurrentBlockHeight() } // ----------------------------------------------------------------------------- access(all) fun testAddSupportedTokenSucceedsAndDuplicateFails() { - // ensure fresh state - Test.reset(to: snapshot) // create pool first createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) @@ -47,7 +38,7 @@ fun testAddSupportedTokenSucceedsAndDuplicateFails() { ) // attempt duplicate addition – should fail - addSupportedTokenSimpleInterestCurve( + let res = addSupportedTokenSimpleInterestCurveWithResult( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, collateralFactor: 0.8, @@ -55,4 +46,5 @@ fun testAddSupportedTokenSucceedsAndDuplicateFails() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) + Test.expect(res, Test.beFailed()) } \ No newline at end of file diff --git a/flow.json b/flow.json index 7e5918d2..4ad13825 100644 --- a/flow.json +++ b/flow.json @@ -32,6 +32,13 @@ "testing": "0000000000000007" } }, + "MockYieldToken": { + "source": "./cadence/contracts/mocks/MockTidalProtocolConsumer.cdc", + "aliases": { + "emulator": "0x0000000000000007", + "testing": "0000000000000007" + } + }, "TidalProtocol": { "source": "./cadence/contracts/TidalProtocol.cdc", "aliases": {