Skip to content

Commit

Permalink
Configure hybrid custody for creators (#45)
Browse files Browse the repository at this point in the history
* when saving a new ContractManager.Manager resource, configure hybrid custody for the owner of the account
* add helper method to configure vaults on the contract manager account
  • Loading branch information
austinkline authored Sep 24, 2024
1 parent b6d1393 commit 66c279e
Show file tree
Hide file tree
Showing 14 changed files with 438 additions and 8 deletions.
126 changes: 126 additions & 0 deletions contracts/ContractManager.cdc
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}

Expand All @@ -72,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<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(/storage/flowTokenVault)

let router <- FungibleTokenRouter.createRouter(defaultAddress: defaultRouterAddress)
acct.storage.save(<-router, to: FungibleTokenRouter.StoragePath)

Expand All @@ -84,6 +95,121 @@ 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<auth(HybridCustody.Owner) &HybridCustody.OwnedAccount>(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 = 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(
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<FungibleTokenMetadataViews.FTVaultData>())! 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<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>()) {
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<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(data.storagePath)
}
}

access(all) fun createManager(tokens: @FlowToken.Vault, defaultRouterAddress: Address): @Manager {
Expand Down
60 changes: 59 additions & 1 deletion flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,58 @@
"testnet": "0x83231f90a288bc35",
"mainnet": "0x707c0b39a8d689cb"
}
},
"CapabilityFactory": {
"source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityFactory.cdc",
"aliases": {
"emulator": "0xf8d6e0586b0a20c7",
"testnet": "0x294e44e1ec6993c6",
"mainnet": "0xd8a7e05a7ac670c0",
"testing": "0x0000000000000008"
}
},
"CapabilityDelegator": {
"source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityDelegator.cdc",
"aliases": {
"emulator": "0xf8d6e0586b0a20c7",
"testnet": "0x294e44e1ec6993c6",
"mainnet": "0xd8a7e05a7ac670c0",
"testing": "0x0000000000000008"
}
},
"CapabilityFilter": {
"source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/CapabilityFilter.cdc",
"aliases": {
"emulator": "0xf8d6e0586b0a20c7",
"testnet": "0x294e44e1ec6993c6",
"mainnet": "0xd8a7e05a7ac670c0",
"testing": "0x0000000000000008"
}
},
"HybridCustody": {
"source": "./node_modules/@flowtyio/flow-contracts/contracts/hybrid-custody/HybridCustody.cdc",
"aliases": {
"emulator": "0xf8d6e0586b0a20c7",
"testnet": "0x294e44e1ec6993c6",
"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"
}
},
"ExampleToken": {
"source": "./node_modules/@flowtyio/flow-contracts/contracts/example/ExampleToken.cdc",
"aliases": {
"emulator": "0xf8d6e0586b0a20c7",
"testing": "0x0000000000000008"
}
}
},
"deployments": {
Expand Down Expand Up @@ -282,7 +334,13 @@
"ArrayUtils",
"StringUtils",
"AddressUtils",
"FungibleTokenRouter"
"FungibleTokenRouter",
"CapabilityFactory",
"CapabilityDelegator",
"CapabilityFilter",
"HybridCustody",
"FTAllFactory",
"ExampleToken"
],
"emulator-ft": [
"FungibleToken",
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"author": "",
"license": "ISC",
"dependencies": {
"@flowtyio/flow-contracts": "0.1.0-beta.31"
"@flowtyio/flow-contracts": "0.1.6"
}
}
14 changes: 14 additions & 0 deletions scripts/util/get_withdraw_controller_id.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "FungibleToken"

access(all) fun main(addr: Address, path: StoragePath): UInt64 {
let acct = getAuthAccount<auth(Capabilities) &Account>(addr)

let type = Type<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>()
for controller in acct.capabilities.storage.getControllers(forPath: path) {
if controller.borrowType.isSubtype(of: type) {
return controller.capabilityID
}
}

panic("no withdraw capability ID found")
}
78 changes: 78 additions & 0 deletions tests/ContractManager_tests.cdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Test
import "./test_helpers.cdc"
import "ContractManager"
import "HybridCustody"
import "FungibleToken"
import "ExampleToken"

access(all) fun setup() {
deployAll()
Expand All @@ -14,4 +17,79 @@ access(all) fun test_SetupContractManager() {

let savedEvent = Test.eventsOfType(Type<ContractManager.ManagerSaved>()).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<ContractManager.ManagerSaved>()).removeLast() as! ContractManager.ManagerSaved
let contractAddress = savedEvent.contractAddress

// make sure there is a HybridCustody.AccountUpdated event
let updatedEvent = Test.eventsOfType(Type<HybridCustody.AccountUpdated>()).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, contractAddress, controllerId])

let withdrawEvent = Test.eventsOfType(Type<FungibleToken.Withdrawn>()).removeLast() as! FungibleToken.Withdrawn
Test.assertEqual(amount, withdrawEvent.amount)
Test.assertEqual(contractAddress, withdrawEvent.from!)

let depositEvent = Test.eventsOfType(Type<FungibleToken.Deposited>()).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<ContractManager.ManagerSaved>()).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<ContractManager.ManagerSaved>()).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<FungibleToken.Withdrawn>()).removeLast() as! FungibleToken.Withdrawn
Test.assertEqual(amount, withdrawEvent.amount)
Test.assertEqual(contractAddress, withdrawEvent.from!)

let depositEvent = Test.eventsOfType(Type<FungibleToken.Deposited>()).removeLast() as! FungibleToken.Deposited
Test.assertEqual(amount, depositEvent.amount)
Test.assertEqual(acct.address, depositEvent.to!)
}
10 changes: 10 additions & 0 deletions tests/test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,19 @@ 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", [])
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", [])
Expand Down Expand Up @@ -165,6 +173,8 @@ access(all) fun deployAll() {
"data": data
}
deploy("OpenEditionNFT", "../contracts/nft/OpenEditionNFT.cdc", [params, Type<OpenEditionInitializer>().identifier])

txExecutor("setup/configure_hybrid_custody_filter_and_factory.cdc", [flowtyDropsAccount], [])
}

access(all) fun deploy(_ name: String, _ path: String, _ arguments: [AnyStruct]) {
Expand Down
Loading

0 comments on commit 66c279e

Please sign in to comment.