Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure hybrid custody for creators #45

Merged
merged 5 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
)
}

self.configureHybridCustody(acct: acct)
emit ManagerSaved(uuid: self.uuid, contractAddress: self.acct.address, ownerAddress: self.owner!.address)
}

Expand All @@ -72,6 +80,9 @@

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 @@
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 {

Check warning on line 144 in contracts/ContractManager.cdc

View check run for this annotation

Codecov / codecov/patch

contracts/ContractManager.cdc#L144

Added line #L144 was not covered by tests
case "TESTNET":
factoryAddress = Address(0x1b7fa5972fcb8af5)
filterAddress = Address(0xe2664be06bb0fe62)
break

Check warning on line 148 in contracts/ContractManager.cdc

View check run for this annotation

Codecov / codecov/patch

contracts/ContractManager.cdc#L146-L148

Added lines #L146 - L148 were not covered by tests
case "MAINNET":
factoryAddress = Address(0x071d382668250606)
filterAddress = Address(0x78e93a79b05d0d7d)
break

Check warning on line 152 in contracts/ContractManager.cdc

View check run for this annotation

Codecov / codecov/patch

contracts/ContractManager.cdc#L150-L152

Added lines #L150 - L152 were not covered by tests
}
}

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

Check warning on line 202 in contracts/ContractManager.cdc

View check run for this annotation

Codecov / codecov/patch

contracts/ContractManager.cdc#L201-L202

Added lines #L201 - L202 were not covered by tests
}
}

if foundProvider {
return

Check warning on line 207 in contracts/ContractManager.cdc

View check run for this annotation

Codecov / codecov/patch

contracts/ContractManager.cdc#L207

Added line #L207 was not covered by tests
}

// 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
Loading