Skip to content

Commit

Permalink
add scoped providers (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
austinkline authored Nov 6, 2023
1 parent d808d82 commit 5d838b2
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
120 changes: 120 additions & 0 deletions contracts/flow-utils/ScopedFTProviders.cdc
Original file line number Diff line number Diff line change
@@ -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)
}
}
162 changes: 162 additions & 0 deletions contracts/flow-utils/ScopedNFTProviders.cdc
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions example/contracts/Importer.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -315,7 +331,9 @@
"TopShotLocking",
"ArrayUtils",
"StringUtils",
"AddressUtils"
"AddressUtils",
"ScopedNFTProviders",
"ScopedFTProviders"
],
"emulator-ft": [
"FungibleToken",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down

0 comments on commit 5d838b2

Please sign in to comment.