diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ec7c72..64d5d76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,6 @@ jobs: with: node-version: '14' - name: Install Flow CLI - run: bash -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.3.3 + run: bash -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.5.0 - name: Run tests run: sh ./test.sh diff --git a/README.md b/README.md index ead58f9..6015705 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,11 @@ Currently, the list includes: | TokenForwarding | 0x51ea0e37c27a1f1a | 0xe544175ee0461c4b | | DapperUtilityCoin | 0x82ec283f88a62e65 | 0xead892083b3e2c6c | | FlowUtilityToken | 0x82ec283f88a62e65 | 0xead892083b3e2c6c | +| ArrayUtils| 0x31ad40c07a2a9788 | 0xa340dc0a4ec828ab | +| StringUtils| 0x31ad40c07a2a9788 | 0xa340dc0a4ec828ab | +| AddressUtils | 0x31ad40c07a2a9788 | 0xa340dc0a4ec828ab | +| ScopedNFTProviders | 0x31ad40c07a2a9788 | 0xa340dc0a4ec828ab | +| ScopedFTProviders | 0x31ad40c07a2a9788 | 0xa340dc0a4ec828ab | ## Using a contract diff --git a/contracts/flow-utils/ScopedFTProviders.cdc b/contracts/flow-utils/ScopedFTProviders.cdc new file mode 100644 index 0000000..e2ef3dc --- /dev/null +++ b/contracts/flow-utils/ScopedFTProviders.cdc @@ -0,0 +1,120 @@ +import "FungibleToken" +import "StringUtils" + +// ScopedFTProviders +// +// TO AVOID RISK, PLEASE DEPLOY YOUR OWN VERSION OF THIS CONTRACT SO THAT +// MALICIOUS UPDATES ARE NOT POSSIBLE +// +// ScopedProviders are meant to solve the issue of unbounded access FungibleToken vaults +// when a provider is called for. +pub contract ScopedFTProviders { + pub struct interface FTFilter { + pub fun canWithdrawAmount(_ amount: UFix64): Bool + pub fun markAmountWithdrawn(_ amount: UFix64) + pub fun getDetails(): {String: AnyStruct} + } + + pub struct AllowanceFilter: FTFilter { + access(self) let allowance: UFix64 + access(self) var allowanceUsed: UFix64 + + init(_ allowance: UFix64) { + self.allowance = allowance + self.allowanceUsed = 0.0 + } + + pub fun canWithdrawAmount(_ amount: UFix64): Bool { + return amount + self.allowanceUsed <= self.allowance + } + + pub fun markAmountWithdrawn(_ amount: UFix64) { + self.allowanceUsed = self.allowanceUsed + amount + } + + pub fun getDetails(): {String: AnyStruct} { + return { + "allowance": self.allowance, + "allowanceUsed": self.allowanceUsed + } + } + } + + // ScopedFTProvider + // + // A ScopedFTProvider is a wrapped FungibleTokenProvider with + // filters that can be defined by anyone using the ScopedFTProvider. + pub resource ScopedFTProvider: FungibleToken.Provider { + access(self) let provider: Capability<&{FungibleToken.Provider}> + access(self) var filters: [{FTFilter}] + + // block timestamp that this provider can no longer be used after + access(self) let expiration: UFix64? + + pub init(provider: Capability<&{FungibleToken.Provider}>, filters: [{FTFilter}], expiration: UFix64?) { + self.provider = provider + self.filters = filters + self.expiration = expiration + } + + pub fun check(): Bool { + return self.provider.check() + } + + pub fun isExpired(): Bool { + if let expiration = self.expiration { + return getCurrentBlock().timestamp >= expiration + } + return false + } + + pub fun canWithdraw(_ amount: UFix64): Bool { + if self.isExpired() { + return false + } + + for filter in self.filters { + if !filter.canWithdrawAmount(amount) { + return false + } + } + + return true + } + + pub fun withdraw(amount: UFix64): @FungibleToken.Vault { + pre { + !self.isExpired(): "provider has expired" + } + + var i = 0 + while i < self.filters.length { + if !self.filters[i].canWithdrawAmount(amount) { + panic(StringUtils.join(["cannot withdraw tokens. filter of type", self.filters[i].getType().identifier, "failed."], " ")) + } + + self.filters[i].markAmountWithdrawn(amount) + i = i + 1 + } + + return <-self.provider.borrow()!.withdraw(amount: amount) + } + + pub fun getDetails(): [{String: AnyStruct}] { + let details: [{String: AnyStruct}] = [] + for filter in self.filters { + details.append(filter.getDetails()) + } + + return details + } + } + + pub fun createScopedFTProvider( + provider: Capability<&{FungibleToken.Provider}>, + filters: [{FTFilter}], + expiration: UFix64? + ): @ScopedFTProvider { + return <- create ScopedFTProvider(provider: provider, filters: filters, expiration: expiration) + } +} diff --git a/contracts/flow-utils/ScopedNFTProviders.cdc b/contracts/flow-utils/ScopedNFTProviders.cdc new file mode 100644 index 0000000..83bb6db --- /dev/null +++ b/contracts/flow-utils/ScopedNFTProviders.cdc @@ -0,0 +1,162 @@ +import "NonFungibleToken" +import "StringUtils" + +// ScopedNFTProviders +// +// TO AVOID RISK, PLEASE DEPLOY YOUR OWN VERSION OF THIS CONTRACT SO THAT +// MALICIOUS UPDATES ARE NOT POSSIBLE +// +// ScopedNFTProviders are meant to solve the issue of unbounded access to NFT Collections. +// A provider can be given extensible filters which allow limited access to resources based on any trait on the NFT itself. +// +// By using a scoped provider, only a subset of assets can be taken if the provider leaks +// instead of the entire nft collection. +pub contract ScopedNFTProviders { + pub struct interface NFTFilter { + pub fun canWithdraw(_ nft: &NonFungibleToken.NFT): Bool + pub fun markWithdrawn(_ nft: &NonFungibleToken.NFT) + pub fun getDetails(): {String: AnyStruct} + } + + pub struct NFTIDFilter: NFTFilter { + // the ids that are allowed to be withdrawn. + // If ids[num] is false, the id cannot be withdrawn anymore + access(self) let ids: {UInt64: Bool} + + init(_ ids: [UInt64]) { + let d: {UInt64: Bool} = {} + for i in ids { + d[i] = true + } + self.ids = d + } + + pub fun canWithdraw(_ nft: &NonFungibleToken.NFT): Bool { + return self.ids[nft.id] != nil && self.ids[nft.id] == true + } + + pub fun markWithdrawn(_ nft: &NonFungibleToken.NFT) { + self.ids[nft.id] = false + } + + pub fun getDetails(): {String: AnyStruct} { + return { + "ids": self.ids + } + } + } + + pub struct UUIDFilter: NFTFilter { + // the ids that are allowed to be withdrawn. + // If ids[num] is false, the id cannot be withdrawn anymore + access(self) let uuids: {UInt64: Bool} + + init(_ uuids: [UInt64]) { + let d: {UInt64: Bool} = {} + for i in uuids { + d[i] = true + } + self.uuids = d + } + + pub fun canWithdraw(_ nft: &NonFungibleToken.NFT): Bool { + return self.uuids[nft.uuid] != nil && self.uuids[nft.uuid]! == true + } + + pub fun markWithdrawn(_ nft: &NonFungibleToken.NFT) { + self.uuids[nft.uuid] = false + } + + pub fun getDetails(): {String: AnyStruct} { + return { + "uuids": self.uuids + } + } + } + + // ScopedNFTProvider + // + // Wrapper around an NFT Provider that is restricted to specific ids. + pub resource ScopedNFTProvider: NonFungibleToken.Provider { + access(self) let provider: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + access(self) let filters: [{NFTFilter}] + + // block timestamp that this provider can no longer be used after + access(self) let expiration: UFix64? + + pub fun isExpired(): Bool { + if let expiration = self.expiration { + return getCurrentBlock().timestamp >= expiration + } + return false + } + + pub init(provider: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, filters: [{NFTFilter}], expiration: UFix64?) { + self.provider = provider + self.expiration = expiration + self.filters = filters + } + + pub fun canWithdraw(_ id: UInt64): Bool { + if self.isExpired() { + return false + } + + let nft = self.provider.borrow()!.borrowNFT(id: id) + if nft == nil { + return false + } + + var i = 0 + while i < self.filters.length { + if !self.filters[i].canWithdraw(nft) { + return false + } + i = i + 1 + } + return true + } + + pub fun check(): Bool { + return self.provider.check() + } + + pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + pre { + !self.isExpired(): "provider has expired" + } + + let nft <- self.provider.borrow()!.withdraw(withdrawID: withdrawID) + let ref = &nft as &NonFungibleToken.NFT + + var i = 0 + while i < self.filters.length { + if !self.filters[i].canWithdraw(ref) { + panic(StringUtils.join(["cannot withdraw nft. filter of type", self.filters[i].getType().identifier, "failed."], " ")) + } + + self.filters[i].markWithdrawn(ref) + i = i + 1 + } + + return <-nft + } + + pub fun getDetails(): [{String: AnyStruct}] { + let details: [{String: AnyStruct}] = [] + for f in self.filters { + details.append(f.getDetails()) + } + + return details + } + } + + pub fun createScopedNFTProvider( + provider: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, + filters: [{NFTFilter}], + expiration: UFix64? + ): @ScopedNFTProvider { + return <- create ScopedNFTProvider(provider: provider, filters: filters, expiration: expiration) + } +} diff --git a/example/contracts/Importer.cdc b/example/contracts/Importer.cdc index a789f3c..87a3e3b 100644 --- a/example/contracts/Importer.cdc +++ b/example/contracts/Importer.cdc @@ -10,6 +10,8 @@ import "LostAndFoundHelper" import "DapperOffersV2" import "TopShot" import "AddressUtils" +import "ScopedNFTProviders" +import "ScopedFTProviders" // This contract doesn't do anything, it's just to show that deployments work // with this import system diff --git a/flow.json b/flow.json index 3a2c383..8c6b1fb 100644 --- a/flow.json +++ b/flow.json @@ -291,6 +291,22 @@ "testnet": "0x31ad40c07a2a9788", "mainnet": "0xa340dc0a4ec828ab" } + }, + "ScopedFTProviders": { + "source": "./contracts/flow-utils/ScopedFTProviders.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x31ad40c07a2a9788", + "mainnet": "0xa340dc0a4ec828ab" + } + }, + "ScopedNFTProviders": { + "source": "./contracts/flow-utils/ScopedNFTProviders.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x31ad40c07a2a9788", + "mainnet": "0xa340dc0a4ec828ab" + } } }, "deployments": { @@ -315,7 +331,9 @@ "TopShotLocking", "ArrayUtils", "StringUtils", - "AddressUtils" + "AddressUtils", + "ScopedNFTProviders", + "ScopedFTProviders" ], "emulator-ft": [ "FungibleToken", diff --git a/package.json b/package.json index 633f4ab..7a685ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flowtyio/flow-contracts", - "version": "0.0.16", + "version": "0.0.17", "main": "index.json", "description": "An NPM package for common flow contracts", "author": "flowtyio",