From 4831246d71a977d14aa8794acfcffa8759bbb8a4 Mon Sep 17 00:00:00 2001 From: Austin Kline Date: Mon, 23 Sep 2024 21:56:48 -0700 Subject: [PATCH 1/5] when saving a new ContractManager.Manager resource, configure hybrid custody for the owner of the account --- contracts/ContractManager.cdc | 126 ++++++++++++++++++++++++++++++++++ flow.json | 38 +++++++++- package-lock.json | 8 +-- package.json | 2 +- 4 files changed, 168 insertions(+), 6 deletions(-) diff --git a/contracts/ContractManager.cdc b/contracts/ContractManager.cdc index 165805a..1e45406 100644 --- a/contracts/ContractManager.cdc +++ b/contracts/ContractManager.cdc @@ -1,6 +1,13 @@ import "FlowToken" import "FungibleToken" import "FungibleTokenRouter" +import "HybridCustody" +import "MetadataViews" +import "ViewResolver" +import "AddressUtils" +import "CapabilityFactory" +import "CapabilityFilter" +import "FungibleTokenMetadataViews" access(all) contract ContractManager { access(all) let StoragePath: StoragePath @@ -58,6 +65,7 @@ access(all) contract ContractManager { ) } + self.configureHybridCustody(acct: acct) emit ManagerSaved(uuid: self.uuid, contractAddress: self.acct.address, ownerAddress: self.owner!.address) } @@ -84,6 +92,124 @@ access(all) contract ContractManager { self.data = {} self.resources <- {} } + + access(self) fun configureHybridCustody(acct: auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account) { + if acct.storage.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil { + let ownedAccount <- HybridCustody.createOwnedAccount(acct: self.acct) + acct.storage.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath) + } + + let owned = acct.storage.borrow(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + + let thumbnail = MetadataViews.HTTPFile(url: "https://avatars.flowty.io/6.x/thumbs/png?seed=".concat(self.acct.address.toString())) + let display = MetadataViews.Display(name: "Creator Hub", description: "Created by the Flowty Creator Hub", thumbnail: thumbnail) + owned.setDisplay(display) + + if !acct.capabilities.get<&{HybridCustody.OwnedAccountPublic, ViewResolver.Resolver}>(HybridCustody.OwnedAccountPublicPath).check() { + acct.capabilities.unpublish(HybridCustody.OwnedAccountPublicPath) + acct.capabilities.storage.issue<&{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, ViewResolver.Resolver}>(HybridCustody.OwnedAccountStoragePath) + acct.capabilities.publish( + acct.capabilities.storage.issue<&{HybridCustody.OwnedAccountPublic, ViewResolver.Resolver}>(HybridCustody.OwnedAccountStoragePath), + at: HybridCustody.OwnedAccountPublicPath + ) + } + + // make sure that only the owner of this resource is a valid parent + let parents = owned.getParentAddresses() + let owner = self.owner!.address + var foundOwner = false + for parent in parents { + if parent == owner { + foundOwner = true + continue + } + + // found a parent that should not be present + owned.removeParent(parent: parent) + } + + if foundOwner { + return + } + + // Flow maintains a set of pre-configured filter and factory resources that we will use: + // https://github.com/onflow/hybrid-custody?tab=readme-ov-file#hosted-capabilityfactory--capabilityfilter-implementations + var factoryAddress: Address? = nil + var filterAddress: Address? = nil + let network = AddressUtils.getNetworkFromAddress(ContractManager.account.address)! + switch network { + case "EMULATOR": + factoryAddress = ContractManager.account.address + filterAddress = ContractManager.account.address + break + case "TESTNET": + factoryAddress = Address(0x1b7fa5972fcb8af5) + filterAddress = Address(0xe2664be06bb0fe62) + break + case "MAINNET": + factoryAddress = Address(0x071d382668250606) + filterAddress = Address(0x78e93a79b05d0d7d) + break + } + + owned.publishToParent( + parentAddress: owner, + factory: getAccount(factoryAddress!).capabilities.get<&CapabilityFactory.Manager>(CapabilityFactory.PublicPath), + filter: getAccount(filterAddress!).capabilities.get<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) + ) + } + + // Configure a given fungible token vault so that it can be received by this contract account + access(Manage) fun configureVault(vaultType: Type) { + pre { + vaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "vault must be a fungible token" + } + + let address = AddressUtils.parseAddress(vaultType)! + let name = vaultType.identifier.split(separator: ".")[2] + + let ftContract = getAccount(address).contracts.borrow<&{FungibleToken}>(name: name) + ?? panic("vault contract does not implement FungibleToken") + let data = ftContract.resolveContractView(resourceType: vaultType, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData + + let acct = self.acct.borrow()! + if acct.storage.type(at: data.storagePath) == nil { + acct.storage.save(<- ftContract.createEmptyVault(vaultType: vaultType), to: data.storagePath) + } + + if !acct.capabilities.get<&{FungibleToken.Receiver}>(data.receiverPath).check() { + acct.capabilities.unpublish(data.receiverPath) + acct.capabilities.publish( + acct.capabilities.storage.issue<&{FungibleToken.Receiver}>(data.storagePath), + at: data.receiverPath + ) + } + + if !acct.capabilities.get<&{FungibleToken.Receiver}>(data.metadataPath).check() { + acct.capabilities.unpublish(data.metadataPath) + acct.capabilities.publish( + acct.capabilities.storage.issue<&{FungibleToken.Vault}>(data.storagePath), + at: data.metadataPath + ) + } + + // is there a valid provider capability for this vault type? + var foundProvider = false + for controller in acct.capabilities.storage.getControllers(forPath: data.storagePath) { + if controller.borrowType.isSubtype(of: Type()) { + foundProvider = true + break + } + } + + if foundProvider { + return + } + + // we did not find a provider, issue one so that its parent account is able to access it. + acct.capabilities.storage.issue(data.storagePath) + } } access(all) fun createManager(tokens: @FlowToken.Vault, defaultRouterAddress: Address): @Manager { diff --git a/flow.json b/flow.json index 6ee3c00..a1eff22 100644 --- a/flow.json +++ b/flow.json @@ -253,6 +253,38 @@ "testnet": "0x83231f90a288bc35", "mainnet": "0x707c0b39a8d689cb" } + }, + "CapabilityFactory": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityFactory.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x294e44e1ec6993c6", + "mainnet": "0xd8a7e05a7ac670c0" + } + }, + "CapabilityDelegator": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityDelegator.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x294e44e1ec6993c6", + "mainnet": "0xd8a7e05a7ac670c0" + } + }, + "CapabilityFilter": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityFilter.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x294e44e1ec6993c6", + "mainnet": "0xd8a7e05a7ac670c0" + } + }, + "HybridCustody": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/HybridCustody.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x294e44e1ec6993c6", + "mainnet": "0xd8a7e05a7ac670c0" + } } }, "deployments": { @@ -282,7 +314,11 @@ "ArrayUtils", "StringUtils", "AddressUtils", - "FungibleTokenRouter" + "FungibleTokenRouter", + "CapabilityFactory", + "CapabilityDelegator", + "CapabilityFilter", + "HybridCustody" ], "emulator-ft": [ "FungibleToken", diff --git a/package-lock.json b/package-lock.json index becc283..74bdd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@flowtyio/flow-contracts": "0.1.0-beta.31" + "@flowtyio/flow-contracts": "0.1.5" } }, "node_modules/@flowtyio/flow-contracts": { - "version": "0.1.0-beta.31", - "resolved": "https://registry.npmjs.org/@flowtyio/flow-contracts/-/flow-contracts-0.1.0-beta.31.tgz", - "integrity": "sha512-aqh2DqzagZSHbfsVdTT2gpsFMm6/RAWEiaKNk8eOBJI3ItXZzIQ/9o1sL3qZ532duYafT/lJSBO40xhwslAuhA==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@flowtyio/flow-contracts/-/flow-contracts-0.1.5.tgz", + "integrity": "sha512-bRM+DmKaOf69d8RrtsTyyfxyiyrLO9etZZEcW7+dz/f5L1fxMSvAXmcVCYmfqQ3tz2Wc6uxScI+d6ZFt2vTybQ==", "dependencies": { "commander": "^11.0.0" }, diff --git a/package.json b/package.json index ad008fc..078e36e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,6 @@ "author": "", "license": "ISC", "dependencies": { - "@flowtyio/flow-contracts": "0.1.0-beta.31" + "@flowtyio/flow-contracts": "0.1.5" } } From 9d8a2d240ce12340e426af18b5c63d93d10a81da Mon Sep 17 00:00:00 2001 From: Austin Kline Date: Tue, 24 Sep 2024 08:55:04 -0700 Subject: [PATCH 2/5] add hc dependencies and get tests working again --- contracts/ContractManager.cdc | 29 +++++++-------- flow.json | 24 ++++++++++--- tests/test_helpers.cdc | 8 +++++ ...gure_hybrid_custody_filter_and_factory.cdc | 36 +++++++++++++++++++ 4 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 transactions/setup/configure_hybrid_custody_filter_and_factory.cdc diff --git a/contracts/ContractManager.cdc b/contracts/ContractManager.cdc index 1e45406..4cc3c09 100644 --- a/contracts/ContractManager.cdc +++ b/contracts/ContractManager.cdc @@ -135,22 +135,19 @@ access(all) contract ContractManager { // Flow maintains a set of pre-configured filter and factory resources that we will use: // https://github.com/onflow/hybrid-custody?tab=readme-ov-file#hosted-capabilityfactory--capabilityfilter-implementations - var factoryAddress: Address? = nil - var filterAddress: Address? = nil - let network = AddressUtils.getNetworkFromAddress(ContractManager.account.address)! - switch network { - case "EMULATOR": - factoryAddress = ContractManager.account.address - filterAddress = ContractManager.account.address - break - case "TESTNET": - factoryAddress = Address(0x1b7fa5972fcb8af5) - filterAddress = Address(0xe2664be06bb0fe62) - break - case "MAINNET": - factoryAddress = Address(0x071d382668250606) - filterAddress = Address(0x78e93a79b05d0d7d) - break + var factoryAddress = ContractManager.account.address + var filterAddress = ContractManager.account.address + if let network = AddressUtils.getNetworkFromAddress(ContractManager.account.address) { + switch network { + case "TESTNET": + factoryAddress = Address(0x1b7fa5972fcb8af5) + filterAddress = Address(0xe2664be06bb0fe62) + break + case "MAINNET": + factoryAddress = Address(0x071d382668250606) + filterAddress = Address(0x78e93a79b05d0d7d) + break + } } owned.publishToParent( diff --git a/flow.json b/flow.json index a1eff22..5b2a618 100644 --- a/flow.json +++ b/flow.json @@ -259,7 +259,8 @@ "aliases": { "emulator": "0xf8d6e0586b0a20c7", "testnet": "0x294e44e1ec6993c6", - "mainnet": "0xd8a7e05a7ac670c0" + "mainnet": "0xd8a7e05a7ac670c0", + "testing": "0x0000000000000008" } }, "CapabilityDelegator": { @@ -267,7 +268,8 @@ "aliases": { "emulator": "0xf8d6e0586b0a20c7", "testnet": "0x294e44e1ec6993c6", - "mainnet": "0xd8a7e05a7ac670c0" + "mainnet": "0xd8a7e05a7ac670c0", + "testing": "0x0000000000000008" } }, "CapabilityFilter": { @@ -275,7 +277,8 @@ "aliases": { "emulator": "0xf8d6e0586b0a20c7", "testnet": "0x294e44e1ec6993c6", - "mainnet": "0xd8a7e05a7ac670c0" + "mainnet": "0xd8a7e05a7ac670c0", + "testing": "0x0000000000000008" } }, "HybridCustody": { @@ -283,7 +286,17 @@ "aliases": { "emulator": "0xf8d6e0586b0a20c7", "testnet": "0x294e44e1ec6993c6", - "mainnet": "0xd8a7e05a7ac670c0" + "mainnet": "0xd8a7e05a7ac670c0", + "testing": "0x0000000000000008" + } + }, + "FTAllFactory": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/factories/FTAllFactory.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x294e44e1ec6993c6", + "mainnet": "0xd8a7e05a7ac670c0", + "testing": "0x0000000000000008" } } }, @@ -318,7 +331,8 @@ "CapabilityFactory", "CapabilityDelegator", "CapabilityFilter", - "HybridCustody" + "HybridCustody", + "FTAllFactory" ], "emulator-ft": [ "FungibleToken", diff --git a/tests/test_helpers.cdc b/tests/test_helpers.cdc index 549d130..a42fe75 100644 --- a/tests/test_helpers.cdc +++ b/tests/test_helpers.cdc @@ -95,6 +95,12 @@ access(all) fun deployAll() { deploy("AddressUtils", "../node_modules/@flowtyio/flow-contracts/contracts/flow-utils/AddressUtils.cdc", []) deploy("FungibleTokenRouter", "../node_modules/@flowtyio/flow-contracts/contracts/fungible-token-router/FungibleTokenRouter.cdc", []) + deploy("CapabilityFilter", "../node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityFilter.cdc", []) + deploy("CapabilityFactory", "../node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityFactory.cdc", []) + deploy("CapabilityDelegator", "../node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityDelegator.cdc", []) + deploy("FTAllFactory", "../node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/factories/FTAllFactory.cdc", []) + deploy("HybridCustody", "../node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/HybridCustody.cdc", []) + deploy("FlowtyDrops", "../contracts/FlowtyDrops.cdc", []) deploy("NFTMetadata", "../contracts/nft/NFTMetadata.cdc", []) deploy("BaseCollection", "../contracts/nft/BaseCollection.cdc", []) @@ -165,6 +171,8 @@ access(all) fun deployAll() { "data": data } deploy("OpenEditionNFT", "../contracts/nft/OpenEditionNFT.cdc", [params, Type().identifier]) + + txExecutor("setup/configure_hybrid_custody_filter_and_factory.cdc", [flowtyDropsAccount], []) } access(all) fun deploy(_ name: String, _ path: String, _ arguments: [AnyStruct]) { diff --git a/transactions/setup/configure_hybrid_custody_filter_and_factory.cdc b/transactions/setup/configure_hybrid_custody_filter_and_factory.cdc new file mode 100644 index 0000000..3ceb844 --- /dev/null +++ b/transactions/setup/configure_hybrid_custody_filter_and_factory.cdc @@ -0,0 +1,36 @@ +import "FTAllFactory" +import "CapabilityFilter" +import "CapabilityFactory" +import "FungibleToken" + +transaction { + prepare(acct: auth(Storage, Capabilities) &Account) { + if acct.storage.type(at: CapabilityFilter.StoragePath) == nil { + acct.storage.save(<- CapabilityFilter.createFilter(Type<@CapabilityFilter.AllowAllFilter>()), to: CapabilityFilter.StoragePath) + + acct.capabilities.unpublish(CapabilityFilter.PublicPath) + acct.capabilities.publish( + acct.capabilities.storage.issue<&{CapabilityFilter.Filter}>(CapabilityFilter.StoragePath), + at: CapabilityFilter.PublicPath + ) + } + + if acct.storage.type(at: CapabilityFactory.StoragePath) == nil { + acct.storage.save(<- CapabilityFactory.createFactoryManager(), to: CapabilityFactory.StoragePath) + + acct.capabilities.unpublish(CapabilityFactory.PublicPath) + acct.capabilities.publish( + acct.capabilities.storage.issue<&CapabilityFactory.Manager>(CapabilityFactory.StoragePath), + at: CapabilityFactory.PublicPath + ) + + let manager = acct.storage.borrow(from: CapabilityFactory.StoragePath) + ?? panic("manager not found") + manager.addFactory(Type(), FTAllFactory.Factory()) + manager.addFactory(Type(), FTAllFactory.Factory()) + manager.addFactory(Type<&{FungibleToken.Balance}>(), FTAllFactory.Factory()) + manager.addFactory(Type<&{FungibleToken.Receiver}>(), FTAllFactory.Factory()) + manager.addFactory(Type<&{FungibleToken.Receiver, FungibleToken.Balance}>(), FTAllFactory.Factory()) + } + } +} \ No newline at end of file From d5e6fe54e51621369169dc93e914731d323d3599 Mon Sep 17 00:00:00 2001 From: Austin Kline Date: Tue, 24 Sep 2024 09:25:56 -0700 Subject: [PATCH 3/5] add test to validate access to child account tokens --- contracts/ContractManager.cdc | 3 ++ scripts/util/get_withdraw_controller_id.cdc | 14 ++++++++ tests/ContractManager_tests.cdc | 30 ++++++++++++++++ transactions/contract-manager/setup.cdc | 34 +++++++++++++++++-- transactions/flow-token/withdraw_tokens.cdc | 31 +++++++++++++++++ .../withdraw_fungible_tokens.cdc | 0 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 scripts/util/get_withdraw_controller_id.cdc create mode 100644 transactions/flow-token/withdraw_tokens.cdc create mode 100644 transactions/hybrid_custody/withdraw_fungible_tokens.cdc diff --git a/contracts/ContractManager.cdc b/contracts/ContractManager.cdc index 4cc3c09..a48f2b2 100644 --- a/contracts/ContractManager.cdc +++ b/contracts/ContractManager.cdc @@ -80,6 +80,9 @@ access(all) contract ContractManager { acct.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault)!.deposit(from: <-tokens) + // setup a provider capability so that tokens are accessible via hybrid custody + acct.capabilities.storage.issue(/storage/flowTokenVault) + let router <- FungibleTokenRouter.createRouter(defaultAddress: defaultRouterAddress) acct.storage.save(<-router, to: FungibleTokenRouter.StoragePath) diff --git a/scripts/util/get_withdraw_controller_id.cdc b/scripts/util/get_withdraw_controller_id.cdc new file mode 100644 index 0000000..217ca56 --- /dev/null +++ b/scripts/util/get_withdraw_controller_id.cdc @@ -0,0 +1,14 @@ +import "FungibleToken" + +access(all) fun main(addr: Address, path: StoragePath): UInt64 { + let acct = getAuthAccount(addr) + + let type = Type() + for controller in acct.capabilities.storage.getControllers(forPath: path) { + if controller.borrowType.isSubtype(of: type) { + return controller.capabilityID + } + } + + panic("no withdraw capability ID found") +} \ No newline at end of file diff --git a/tests/ContractManager_tests.cdc b/tests/ContractManager_tests.cdc index e4b0816..da2deca 100644 --- a/tests/ContractManager_tests.cdc +++ b/tests/ContractManager_tests.cdc @@ -1,6 +1,8 @@ import Test import "./test_helpers.cdc" import "ContractManager" +import "HybridCustody" +import "FungibleToken" access(all) fun setup() { deployAll() @@ -14,4 +16,32 @@ access(all) fun test_SetupContractManager() { let savedEvent = Test.eventsOfType(Type()).removeLast() as! ContractManager.ManagerSaved Test.assertEqual(acct.address, savedEvent.ownerAddress) +} + +access(all) fun test_SetupContractManager_CanWithdrawTokens() { + let acct = Test.createAccount() + mintFlowTokens(acct, 10.0) + + let amount = 5.0 + txExecutor("contract-manager/setup.cdc", [acct], [amount]) + let savedEvent = Test.eventsOfType(Type()).removeLast() as! ContractManager.ManagerSaved + let contractAddress = savedEvent.contractAddress + + // make sure there is a HybridCustody.AccountUpdated event + let updatedEvent = Test.eventsOfType(Type()).removeLast() as! HybridCustody.AccountUpdated + Test.assertEqual(acct.address, updatedEvent.parent!) + Test.assertEqual(contractAddress, updatedEvent.child) + Test.assertEqual(true, updatedEvent.active) + + // withdraw and destroy 1 token to prove we are able to access an account's tokens + let controllerId = scriptExecutor("util/get_withdraw_controller_id.cdc", [contractAddress, /storage/flowTokenVault])! as! UInt64 + txExecutor("flow-token/withdraw_tokens.cdc", [acct], [amount, controllerId]) + + let withdrawEvent = Test.eventsOfType(Type()).removeLast() as! FungibleToken.Withdrawn + Test.assertEqual(amount, withdrawEvent.amount) + Test.assertEqual(contractAddress, withdrawEvent.from!) + + let depositEvent = Test.eventsOfType(Type()).removeLast() as! FungibleToken.Deposited + Test.assertEqual(amount, depositEvent.amount) + Test.assertEqual(acct.address, depositEvent.to!) } \ No newline at end of file diff --git a/transactions/contract-manager/setup.cdc b/transactions/contract-manager/setup.cdc index bb8cfb8..5c7ec67 100644 --- a/transactions/contract-manager/setup.cdc +++ b/transactions/contract-manager/setup.cdc @@ -1,13 +1,43 @@ import "ContractManager" import "FlowToken" import "FungibleToken" +import "HybridCustody" +import "ViewResolver" transaction(flowTokenAmount: UFix64) { - prepare(acct: auth(Storage, Capabilities) &Account) { + prepare(acct: auth(Storage, Capabilities, Inbox) &Account) { let v = acct.storage.borrow(from: /storage/flowTokenVault)! let tokens <- v.withdraw(amount: flowTokenAmount) as! @FlowToken.Vault acct.storage.save(<- ContractManager.createManager(tokens: <-tokens, defaultRouterAddress: acct.address), to: ContractManager.StoragePath) - acct.storage.borrow(from: ContractManager.StoragePath)!.onSave() + let contractManager = acct.storage.borrow(from: ContractManager.StoragePath)! + contractManager.onSave() + + // there is a published hybrid custody capability to redeem + if acct.storage.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil { + let m <- HybridCustody.createManager(filter: nil) + acct.storage.save(<- m, to: HybridCustody.ManagerStoragePath) + + for c in acct.capabilities.storage.getControllers(forPath: HybridCustody.ManagerStoragePath) { + c.delete() + } + + acct.capabilities.unpublish(HybridCustody.ManagerPublicPath) + + acct.capabilities.publish( + acct.capabilities.storage.issue<&{HybridCustody.ManagerPublic}>(HybridCustody.ManagerStoragePath), + at: HybridCustody.ManagerPublicPath + ) + + acct.capabilities.storage.issue(HybridCustody.ManagerStoragePath) + } + + let inboxName = HybridCustody.getChildAccountIdentifier(acct.address) + let cap = acct.inbox.claim(inboxName, provider: contractManager.getAccount().address) + ?? panic("child account cap not found") + + let manager = acct.storage.borrow(from: HybridCustody.ManagerStoragePath) + ?? panic("manager no found") + manager.addAccount(cap: cap) } } \ No newline at end of file diff --git a/transactions/flow-token/withdraw_tokens.cdc b/transactions/flow-token/withdraw_tokens.cdc new file mode 100644 index 0000000..29cd3ec --- /dev/null +++ b/transactions/flow-token/withdraw_tokens.cdc @@ -0,0 +1,31 @@ +import "FlowToken" +import "FungibleToken" +import "HybridCustody" +import "ContractManager" +import "FungibleTokenMetadataViews" + +transaction(amount: UFix64, controllerID: UInt64) { + prepare(acct: auth(Storage, Capabilities) &Account) { + let contractManager = acct.storage.borrow(from: ContractManager.StoragePath) + ?? panic("contract manager not found") + + let manager = acct.storage.borrow(from: HybridCustody.ManagerStoragePath) + ?? panic("hybrid custody manager not found") + + let child = manager.borrowAccount(addr: contractManager.getAccount().address) + ?? panic("child account not found") + + let cap = child.getCapability(controllerID: controllerID, type: Type()) ?? panic("capability count not be borrowed") + let providerCap = cap as! Capability + let vault = providerCap.borrow() ?? panic("vault count not be borrowed") + let tokens <- vault.withdraw(amount: amount) + + let ftVaultData = tokens.resolveView(Type())! as! FungibleTokenMetadataViews.FTVaultData + + if acct.storage.type(at: ftVaultData.storagePath) == nil { + acct.storage.save(<- ftVaultData.createEmptyVault(), to: ftVaultData.storagePath) + } + + acct.storage.borrow<&{FungibleToken.Vault}>(from: ftVaultData.storagePath)!.deposit(from: <-tokens) + } +} \ No newline at end of file diff --git a/transactions/hybrid_custody/withdraw_fungible_tokens.cdc b/transactions/hybrid_custody/withdraw_fungible_tokens.cdc new file mode 100644 index 0000000..e69de29 From b1c8081e403fe83bc5a3aefe887b3ece5fa4199e Mon Sep 17 00:00:00 2001 From: Austin Kline Date: Tue, 24 Sep 2024 09:58:11 -0700 Subject: [PATCH 4/5] add test for vault setup --- contracts/ContractManager.cdc | 2 +- flow.json | 10 +++- tests/ContractManager_tests.cdc | 50 ++++++++++++++++++- tests/test_helpers.cdc | 2 + transactions/contract-manager/setup_vault.cdc | 12 +++++ transactions/example-token/mint.cdc | 11 ++++ .../example-token/withdraw_tokens.cdc | 27 ++++++++++ transactions/flow-token/withdraw_tokens.cdc | 7 +-- 8 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 transactions/contract-manager/setup_vault.cdc create mode 100644 transactions/example-token/mint.cdc create mode 100644 transactions/example-token/withdraw_tokens.cdc diff --git a/contracts/ContractManager.cdc b/contracts/ContractManager.cdc index a48f2b2..bd70ca2 100644 --- a/contracts/ContractManager.cdc +++ b/contracts/ContractManager.cdc @@ -171,7 +171,7 @@ access(all) contract ContractManager { let ftContract = getAccount(address).contracts.borrow<&{FungibleToken}>(name: name) ?? panic("vault contract does not implement FungibleToken") - let data = ftContract.resolveContractView(resourceType: vaultType, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData + let data = ftContract.resolveContractView(resourceType: vaultType, viewType: Type())! as! FungibleTokenMetadataViews.FTVaultData let acct = self.acct.borrow()! if acct.storage.type(at: data.storagePath) == nil { diff --git a/flow.json b/flow.json index 5b2a618..9c0b489 100644 --- a/flow.json +++ b/flow.json @@ -298,6 +298,13 @@ "mainnet": "0xd8a7e05a7ac670c0", "testing": "0x0000000000000008" } + }, + "ExampleToken": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/example/ExampleToken.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testing": "0x0000000000000008" + } } }, "deployments": { @@ -332,7 +339,8 @@ "CapabilityDelegator", "CapabilityFilter", "HybridCustody", - "FTAllFactory" + "FTAllFactory", + "ExampleToken" ], "emulator-ft": [ "FungibleToken", diff --git a/tests/ContractManager_tests.cdc b/tests/ContractManager_tests.cdc index da2deca..087feb5 100644 --- a/tests/ContractManager_tests.cdc +++ b/tests/ContractManager_tests.cdc @@ -3,6 +3,7 @@ import "./test_helpers.cdc" import "ContractManager" import "HybridCustody" import "FungibleToken" +import "ExampleToken" access(all) fun setup() { deployAll() @@ -35,7 +36,54 @@ access(all) fun test_SetupContractManager_CanWithdrawTokens() { // withdraw and destroy 1 token to prove we are able to access an account's tokens let controllerId = scriptExecutor("util/get_withdraw_controller_id.cdc", [contractAddress, /storage/flowTokenVault])! as! UInt64 - txExecutor("flow-token/withdraw_tokens.cdc", [acct], [amount, controllerId]) + txExecutor("flow-token/withdraw_tokens.cdc", [acct], [amount, contractAddress, controllerId]) + + let withdrawEvent = Test.eventsOfType(Type()).removeLast() as! FungibleToken.Withdrawn + Test.assertEqual(amount, withdrawEvent.amount) + Test.assertEqual(contractAddress, withdrawEvent.from!) + + let depositEvent = Test.eventsOfType(Type()).removeLast() as! FungibleToken.Deposited + Test.assertEqual(amount, depositEvent.amount) + Test.assertEqual(acct.address, depositEvent.to!) +} + +access(all) fun test_ContractManager_ChangedOwned_RevokesChildAccount() { + let acct = Test.createAccount() + mintFlowTokens(acct, 10.0) + + let amount = 5.0 + txExecutor("contract-manager/setup.cdc", [acct], [amount]) + let savedEvent = Test.eventsOfType(Type()).removeLast() as! ContractManager.ManagerSaved + let contractAddress = savedEvent.contractAddress + + let newOwner = Test.createAccount() + mintFlowTokens(acct, 10.0) + txExecutor("contract-manager/transfer_ownership.cdc", [acct, newOwner], []) + + // ensure that we do not have access to the withdraw capability from the original owner + let controllerId = scriptExecutor("util/get_withdraw_controller_id.cdc", [contractAddress, /storage/flowTokenVault])! as! UInt64 + Test.expectFailure(fun() { + txExecutor("flow-token/withdraw_tokens.cdc", [acct], [amount, contractAddress, controllerId]) + }, errorMessageSubstring: "child account not found") +} + +access(all) fun test_ContractManager_SetupExampleToken() { + let acct = Test.createAccount() + mintFlowTokens(acct, 10.0) + txExecutor("contract-manager/setup.cdc", [acct], [1.0]) + let savedEvent = Test.eventsOfType(Type()).removeLast() as! ContractManager.ManagerSaved + let contractAddress = savedEvent.contractAddress + + // setup ExampleToken + txExecutor("contract-manager/setup_vault.cdc", [acct], [Type<@ExampleToken.Vault>().identifier]) + + // send tokens to newly setup vault + let amount = 1.11 + txExecutor("example-token/mint.cdc", [flowtyDropsAccount], [contractAddress, amount]) + + // ensure that the parent account has access to the deposited tokens + let controllerId = scriptExecutor("util/get_withdraw_controller_id.cdc", [contractAddress, /storage/exampleTokenVault])! as! UInt64 + txExecutor("example-token/withdraw_tokens.cdc", [acct], [amount, contractAddress, controllerId]) let withdrawEvent = Test.eventsOfType(Type()).removeLast() as! FungibleToken.Withdrawn Test.assertEqual(amount, withdrawEvent.amount) diff --git a/tests/test_helpers.cdc b/tests/test_helpers.cdc index a42fe75..7d9b969 100644 --- a/tests/test_helpers.cdc +++ b/tests/test_helpers.cdc @@ -90,6 +90,8 @@ access(all) let openEditionAccount = Test.getAccount(Account0x7) access(all) let exampleTokenAccount = Test.getAccount(Account0x8) access(all) fun deployAll() { + deploy("ExampleToken", "../node_modules/@flowtyio/flow-contracts/contracts/example/ExampleToken.cdc", []) + deploy("ArrayUtils", "../node_modules/@flowtyio/flow-contracts/contracts/flow-utils/ArrayUtils.cdc", []) deploy("StringUtils", "../node_modules/@flowtyio/flow-contracts/contracts/flow-utils/StringUtils.cdc", []) deploy("AddressUtils", "../node_modules/@flowtyio/flow-contracts/contracts/flow-utils/AddressUtils.cdc", []) diff --git a/transactions/contract-manager/setup_vault.cdc b/transactions/contract-manager/setup_vault.cdc new file mode 100644 index 0000000..bf17ea9 --- /dev/null +++ b/transactions/contract-manager/setup_vault.cdc @@ -0,0 +1,12 @@ +import "ContractManager" +import "FungibleToken" + +transaction(identifier: String) { + prepare(acct: auth(BorrowValue) &Account) { + let manager = acct.storage.borrow(from: ContractManager.StoragePath) + ?? panic("manager not found") + + let type = CompositeType(identifier) ?? panic("invalid composite type identifier") + manager.configureVault(vaultType: type) + } +} \ No newline at end of file diff --git a/transactions/example-token/mint.cdc b/transactions/example-token/mint.cdc new file mode 100644 index 0000000..6981535 --- /dev/null +++ b/transactions/example-token/mint.cdc @@ -0,0 +1,11 @@ +import "ExampleToken" +import "FungibleToken" +import "FungibleTokenMetadataViews" + +transaction(to: Address, amount: UFix64) { + prepare(acct: auth(Storage) &Account) { + let tokens <- ExampleToken.mintTokens(amount: amount) + let ftVaultData = tokens.resolveView(Type())! as! FungibleTokenMetadataViews.FTVaultData + getAccount(to).capabilities.get<&{FungibleToken.Receiver}>(ftVaultData.receiverPath).borrow()!.deposit(from: <-tokens) + } +} \ No newline at end of file diff --git a/transactions/example-token/withdraw_tokens.cdc b/transactions/example-token/withdraw_tokens.cdc new file mode 100644 index 0000000..181eeb4 --- /dev/null +++ b/transactions/example-token/withdraw_tokens.cdc @@ -0,0 +1,27 @@ +import "FungibleToken" +import "HybridCustody" +import "ContractManager" +import "FungibleTokenMetadataViews" + +transaction(amount: UFix64, childAddr: Address, controllerID: UInt64) { + prepare(acct: auth(Storage, Capabilities) &Account) { + let manager = acct.storage.borrow(from: HybridCustody.ManagerStoragePath) + ?? panic("hybrid custody manager not found") + + let child = manager.borrowAccount(addr: childAddr) + ?? panic("child account not found") + + let cap = child.getCapability(controllerID: controllerID, type: Type()) ?? panic("capability count not be borrowed") + let providerCap = cap as! Capability + let vault = providerCap.borrow() ?? panic("vault count not be borrowed") + let tokens <- vault.withdraw(amount: amount) + + let ftVaultData = tokens.resolveView(Type())! as! FungibleTokenMetadataViews.FTVaultData + + if acct.storage.type(at: ftVaultData.storagePath) == nil { + acct.storage.save(<- ftVaultData.createEmptyVault(), to: ftVaultData.storagePath) + } + + acct.storage.borrow<&{FungibleToken.Vault}>(from: ftVaultData.storagePath)!.deposit(from: <-tokens) + } +} \ No newline at end of file diff --git a/transactions/flow-token/withdraw_tokens.cdc b/transactions/flow-token/withdraw_tokens.cdc index 29cd3ec..a5365d2 100644 --- a/transactions/flow-token/withdraw_tokens.cdc +++ b/transactions/flow-token/withdraw_tokens.cdc @@ -4,15 +4,12 @@ import "HybridCustody" import "ContractManager" import "FungibleTokenMetadataViews" -transaction(amount: UFix64, controllerID: UInt64) { +transaction(amount: UFix64, childAddr: Address, controllerID: UInt64) { prepare(acct: auth(Storage, Capabilities) &Account) { - let contractManager = acct.storage.borrow(from: ContractManager.StoragePath) - ?? panic("contract manager not found") - let manager = acct.storage.borrow(from: HybridCustody.ManagerStoragePath) ?? panic("hybrid custody manager not found") - let child = manager.borrowAccount(addr: contractManager.getAccount().address) + let child = manager.borrowAccount(addr: childAddr) ?? panic("child account not found") let cap = child.getCapability(controllerID: controllerID, type: Type()) ?? panic("capability count not be borrowed") From a0e38d289cf27522ec11c1338a25d1e227ea610f Mon Sep 17 00:00:00 2001 From: Austin Kline Date: Tue, 24 Sep 2024 10:36:26 -0700 Subject: [PATCH 5/5] update flow-contracts version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74bdd7e..139dc8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@flowtyio/flow-contracts": "0.1.5" + "@flowtyio/flow-contracts": "0.1.6" } }, "node_modules/@flowtyio/flow-contracts": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@flowtyio/flow-contracts/-/flow-contracts-0.1.5.tgz", - "integrity": "sha512-bRM+DmKaOf69d8RrtsTyyfxyiyrLO9etZZEcW7+dz/f5L1fxMSvAXmcVCYmfqQ3tz2Wc6uxScI+d6ZFt2vTybQ==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@flowtyio/flow-contracts/-/flow-contracts-0.1.6.tgz", + "integrity": "sha512-B8G/e+KllIPb6c4Vhvry0JFpw8k5N1PRZwLR3bvcylW0cf45CEQ0GKVJXOSs+z8P/NNNVcJkyGswjyCyysjiMg==", "dependencies": { "commander": "^11.0.0" }, diff --git a/package.json b/package.json index 078e36e..62de9a2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,6 @@ "author": "", "license": "ISC", "dependencies": { - "@flowtyio/flow-contracts": "0.1.5" + "@flowtyio/flow-contracts": "0.1.6" } }