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

add DapperStorageRent to a few templates, update tests #98

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion cadence/__test__/src/examplenft.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export const deployExampleNFT = async () => {

await deployContractByName({ to: ExampleNFTAdmin, name: 'NonFungibleToken' })
await deployContractByName({ to: ExampleNFTAdmin, name: 'MetadataViews' })
return deployContractByName({ to: ExampleNFTAdmin, name: 'ExampleNFT' })
await deployContractByName({ to: ExampleNFTAdmin, name: 'PrivateReceiverForwarder' })
return deployContractByName({ to: ExampleNFTAdmin, name: 'DapperStorageRent' })
}

export const setupExampleNFTCollection = async (account) => {
Expand Down
247 changes: 247 additions & 0 deletions cadence/contracts/DapperStorageRent.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import FungibleToken from "./FungibleToken.cdc"
import FlowToken from "./FlowToken.cdc"
import PrivateReceiverForwarder from "./PrivateReceiverForwarder.cdc"

/// DapperStorageRent
/// Provide a means for accounts storage TopUps. To be used during transaction execution.
pub contract DapperStorageRent {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it essential to have the prefix of the name Dapper?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not essential, but was thinking it gave some good context to its relationship to BloctoStorageRent

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. I think it is better to remove and not use the company name as the prefix. Instead, we can go with a generic contract name to avoid some "XYZ" reasons. We can rename it to Refueler or something like that.


pub let DapperStorageRentAdminStoragePath: StoragePath

/// Threshold of storage required to trigger a refill
access(contract) var StorageRentRefillThreshold: UInt64
andrewdamelio marked this conversation as resolved.
Show resolved Hide resolved
/// List of all refilledAccounts
access(contract) var RefilledAccounts: [Address]
/// Detailed account information of refilled accounts
access(contract) var RefilledAccountInfos: {Address: RefilledAccountInfo}
/// List of all blockedAccounts
access(contract) var BlockedAccounts: [Address]
/// Blocks required between refill attempts
access(contract) var RefillRequiredBlocks: UInt64

/// Event emitted when an Admin blocks an address
pub event BlockedAddress(_ address: [Address])
andrewdamelio marked this conversation as resolved.
Show resolved Hide resolved
/// Event emitted when a Refill is successful
pub event Refuelled(_ address: Address)
/// Event emitted when a Refill is not successful
pub event RefilledFailed(address: Address, reason: String)

/// getStorageRentRefillThreshold
/// Get the current StorageRentRefillThreshold
///
/// @return UInt64 value of the current StorageRentRefillThreshold value
pub fun getStorageRentRefillThreshold(): UInt64 {
return self.StorageRentRefillThreshold
}

/// getRefilledAccounts
/// Get the current StorageRentRefillThreshold
///
/// @return List of refilled Accounts
pub fun getRefilledAccounts(): [Address] {
return self.RefilledAccounts
}

/// getBlockedAccounts
/// Get the current StorageRentRefillThreshold
///
/// @return List of blocked accounts
pub fun getBlockedAccounts() : [Address] {
return self.BlockedAccounts
}

/// getRefilledAccountInfos
/// Get the current StorageRentRefillThreshold
///
/// @return Address: RefilledAccountInfo mapping
pub fun getRefilledAccountInfos(): {Address: RefilledAccountInfo} {
return self.RefilledAccountInfos
}

/// getRefillRequiredBlocks
/// Get the current StorageRentRefillThreshold
///
/// @return UInt64 value of the current RefillRequiredBlocks value
pub fun getRefillRequiredBlocks(): UInt64 {
return self.RefillRequiredBlocks
}

/// tryRefill
/// Attempt to refill an accounts storage capacity if it has dipped below threshold and passes other checks.
///
/// @param address: Address to attempt a storage refill on
pub fun tryRefill(_ address: Address) {
let REFUEL_AMOUNT = 0.06;
andrewdamelio marked this conversation as resolved.
Show resolved Hide resolved

self.cleanExpiredRefilledAccounts(10)

// Get the Flow Token reciever of the address
let recipient = getAccount(address)
let receiverRef = recipient.getCapability<&PrivateReceiverForwarder.Forwarder>(PrivateReceiverForwarder.PrivateReceiverPublicPath).borrow()

// Sliently fail if the receiverRef is nill
andrewdamelio marked this conversation as resolved.
Show resolved Hide resolved
if receiverRef == nil || receiverRef!.owner == nil {
emit RefilledFailed(address: address, reason: "Couldn't borrow the Accounts flowTokenVault")
return
}

// Sliently fail if the account has already be refueled within the block allowance
andrewdamelio marked this conversation as resolved.
Show resolved Hide resolved
if self.RefilledAccountInfos[address] != nil && getCurrentBlock().height - self.RefilledAccountInfos[address]!.atBlock < self.RefillRequiredBlocks {
emit RefilledFailed(address: address, reason: "RefillRequiredBlocks")
return
}

// Get the users storage capacity and usage values
var low: UInt64 = recipient.storageUsed
var high: UInt64 = recipient.storageCapacity

if high < low {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it ever possible? that storageUsed > storageCapacity ? If it is, then it is big bug

high <-> low
}

// Sliently fail if the account has been blocked from receiving refills
andrewdamelio marked this conversation as resolved.
Show resolved Hide resolved
if DapperStorageRent.getBlockedAccounts().contains(address) {
emit RefilledFailed(address: address, reason: "Address is Blocked")
return
}

// If the user is below the threshold PrivateReceiverForwarder will send 0.06 Flow tokens for about 6MB of storage
if high - low < self.StorageRentRefillThreshold {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember that the value of self.StorageRentRefillThreshold should be greater than the txn cost of the tryRefil transaction.

let vaultRef = self.account.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
if vaultRef == nil {
emit RefilledFailed(address: address, reason: "Couldn't borrow the Accounts FlowToken.Vault")
return
}

let privateForwardingSenderRef = self.account.borrow<&PrivateReceiverForwarder.Sender>(from: PrivateReceiverForwarder.SenderStoragePath)
if privateForwardingSenderRef == nil {
emit RefilledFailed(address: address, reason: "Couldn't borrow the Accounts PrivateReceiverForwarder")
return
}

// Check to make sure the payment vault has sufficient funds
if let vaultBalanceRef = self.account.getCapability(/public/flowTokenBalance).borrow<&FlowToken.Vault{FungibleToken.Balance}>() {
if vaultBalanceRef.balance <= REFUEL_AMOUNT {
emit RefilledFailed(address: address, reason: "Insufficient balance to refuel")
return
}
} else {
emit RefilledFailed(address: address, reason: "Couldn't borrow flowToken balance")
return
}

// 0.06 = 6MB of storage, or ~20k NBA TS moments
privateForwardingSenderRef!.sendPrivateTokens(address,tokens:<-vaultRef!.withdraw(amount: REFUEL_AMOUNT))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make sure that the user would receive the 0.06 FLOW in the given account as the user can have a different receiver capability of different accounts in the private forwarder resource.

self.addRefilledAccount(address)
emit Refuelled(address)
} else {
emit RefilledFailed(address: address, reason: "Address is not below StorageRentRefillThreshold")
}
}

/// checkEligibility
///
/// @param address: Address to check eligibility on
/// @return Boolean valued based on if the provided address is below the storage threshold
pub fun checkEligibility(_ address: Address): Bool {
if self.RefilledAccountInfos[address] != nil && getCurrentBlock().height - self.RefilledAccountInfos[address]!.atBlock < self.RefillRequiredBlocks {
return false
}
let acct = getAccount(address)
var high: UInt64 = acct.storageCapacity
var low: UInt64 = acct.storageUsed
if high < low {
high <-> low
}

if high - low >= self.StorageRentRefillThreshold {
return false
}

return true
}

/// addRefilledAccount
///
/// @param address: Address to add to RefilledAccounts/RefilledAccountInfos
access(contract) fun addRefilledAccount(_ address: Address) {
if self.RefilledAccountInfos[address] != nil {
self.RefilledAccounts.remove(at: self.RefilledAccountInfos[address]!.index)
}

self.RefilledAccounts.append(address)
self.RefilledAccountInfos[address] = RefilledAccountInfo(self.RefilledAccounts.length-1, getCurrentBlock().height)
}

/// cleanExpiredRefilledAccounts
/// public method to clean up expired accounts based on current block height
///
/// @param batchSize: Int to set the batch size of the cleanup
pub fun cleanExpiredRefilledAccounts(_ batchSize: Int) {
var index = 0
while index < batchSize && self.RefilledAccounts.length > index {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect as it would only allow running the loop half times than the given batchSize.

If the given batchSize is 10, this loop will run 5 times because of self.RefilledAccounts.length also decreases when you remove the index.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch!! working on a fix

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@satyamakgec updated method to the below, thoughts?

  pub fun cleanExpiredRefilledAccounts(_ batchSize: Int) {
    var index = 0
    var refilledAccountsToCleanup: [Address] = [];
    var refilledAccountsLength = self.refilledAccounts.length
    while index < batchSize && index < refilledAccountsLength {
      if self.refilledAccountInfos[self.refilledAccounts[index]] != nil &&
        getCurrentBlock().height - self.refilledAccountInfos[self.refilledAccounts[index]]!.atBlock > self.refillRequiredBlocks {
        refilledAccountsToCleanup.append(self.refilledAccounts[index])
      }
      index = index + 1
    }

    for account in refilledAccountsToCleanup {
        if let idx = self.refilledAccounts.firstIndex(of: account) {
          self.refilledAccounts.remove(at: idx)
          self.refilledAccountInfos.remove(key: account)
        }
    }
  }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can avoid the below for loop as we can move the removed statements of the array in the while loop. We need only these 2 statements.

var refilledAccountsLength = self.refilledAccounts.length
while index < batchSize && index < refilledAccountsLength {

if self.RefilledAccountInfos[self.RefilledAccounts[index]] != nil &&
getCurrentBlock().height - self.RefilledAccountInfos[self.RefilledAccounts[index]]!.atBlock < self.RefillRequiredBlocks {
break
}

self.RefilledAccountInfos.remove(key: self.RefilledAccounts[index])
self.RefilledAccounts.remove(at: index)
index = index + 1
}
}

/// RefilledAccountInfo struct
/// Holds the block number it was refilled at
pub struct RefilledAccountInfo {
pub let atBlock: UInt64
pub let index: Int

init(_ index: Int, _ atBlock: UInt64) {
self.index = index
self.atBlock = atBlock
}
}

/// Admin resource
/// Used to set different configuration levers such as StorageRentRefillThreshold, RefillRequiredBlocks, and BlockedAccounts
pub resource Admin {
pub fun setStorageRentRefillThreshold(_ threshold: UInt64) {
DapperStorageRent.StorageRentRefillThreshold = threshold
}

pub fun setRefillRequiredBlocks(_ blocks: UInt64) {
DapperStorageRent.RefillRequiredBlocks = blocks
}

pub fun blockAddress(_ address: Address) {
if !DapperStorageRent.getBlockedAccounts().contains(address) {
DapperStorageRent.BlockedAccounts.append(address)
emit BlockedAddress(DapperStorageRent.getBlockedAccounts())
}
}

pub fun unblockAddress(_ address: Address) {
if DapperStorageRent.getBlockedAccounts().contains(address) {
let position = DapperStorageRent.BlockedAccounts.firstIndex(of: address) ?? panic("Trying to unblock an address that is not blocked.")
if position != nil {
DapperStorageRent.BlockedAccounts.remove(at: position)
emit BlockedAddress(DapperStorageRent.getBlockedAccounts())
}
}
}
}

// DapperStorageRent init
init() {
self.DapperStorageRentAdminStoragePath = /storage/DapperStorageRentAdmin
self.StorageRentRefillThreshold = 5000
self.RefilledAccounts = []
self.RefilledAccountInfos = {}
self.RefillRequiredBlocks = 86400
self.BlockedAccounts = []

let admin <- create Admin()
self.account.save(<-admin, to: self.DapperStorageRentAdminStoragePath)
}
}
77 changes: 77 additions & 0 deletions cadence/contracts/PrivateReceiverForwarder.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import FungibleToken from "./FungibleToken.cdc"

pub contract PrivateReceiverForwarder {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we need this contract? can't we directly transfer the funds to the address capability? because it also holds the capability for the same.

Maybe I am missing something here ?


// Event that is emitted when tokens are deposited to the target receiver
pub event PrivateDeposit(amount: UFix64, to: Address?)

pub let SenderStoragePath: StoragePath

pub let PrivateReceiverStoragePath: StoragePath
pub let PrivateReceiverPublicPath: PublicPath

pub resource Forwarder {

// This is where the deposited tokens will be sent.
// The type indicates that it is a reference to a receiver
//
access(self) var recipient: Capability<&{FungibleToken.Receiver}>

// deposit
//
// Function that takes a Vault object as an argument and forwards
// it to the recipient's Vault using the stored reference
//
access(contract) fun deposit(from: @FungibleToken.Vault) {
let receiverRef = self.recipient.borrow()!

let balance = from.balance

receiverRef.deposit(from: <-from)

emit PrivateDeposit(amount: balance, to: self.owner?.address)
}

init(recipient: Capability<&{FungibleToken.Receiver}>) {
pre {
recipient.borrow() != nil: "Could not borrow Receiver reference from the Capability"
}
self.recipient = recipient
}
}

// createNewForwarder creates a new Forwarder reference with the provided recipient
//
pub fun createNewForwarder(recipient: Capability<&{FungibleToken.Receiver}>): @Forwarder {
return <-create Forwarder(recipient: recipient)
}


pub resource Sender {
pub fun sendPrivateTokens(_ address: Address, tokens: @FungibleToken.Vault) {

let account = getAccount(address)

let privateReceiver = account.getCapability<&PrivateReceiverForwarder.Forwarder>(PrivateReceiverForwarder.PrivateReceiverPublicPath)
.borrow() ?? panic("Could not borrow reference to private forwarder")

privateReceiver.deposit(from: <-tokens)

}

pub fun replicate(): @Sender {
return <-create Sender()
}
}

init() {

self.SenderStoragePath = /storage/PrivateSender

self.PrivateReceiverStoragePath = /storage/PrivateReceiver
self.PrivateReceiverPublicPath = /public/PrivateReceiver

self.account.save(<-create Sender(), to: self.SenderStoragePath)

}
}
4 changes: 4 additions & 0 deletions cadence/transactions/mint_example_nft.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import NonFungibleToken from "../contracts/NonFungibleToken.cdc"
import ExampleNFT from "../contracts/ExampleNFT.cdc"
import MetadataViews from "../contracts/MetadataViews.cdc"
import FungibleToken from "../contracts/FungibleToken.cdc"
import DapperStorageRent from "../contracts/DapperStorageRent.cdc"

// This script uses the NFTMinter resource to mint a new NFT
// It must be run with the account that has the minter resource
Expand Down Expand Up @@ -61,6 +62,9 @@ transaction(
.borrow<&{NonFungibleToken.CollectionPublic}>()
?? panic("Could not get receiver reference to the NFT Collection")

// Attempt to refuel recipient before depositing NFT
DapperStorageRent.tryRefill(recipient)

// Mint the NFT and deposit it to the recipient's collection
self.minter.mintNFT(
recipient: receiver,
Expand Down
4 changes: 4 additions & 0 deletions cadence/transactions/mint_nonstandard_nft.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import NonFungibleToken from "../contracts/NonFungibleToken.cdc"
import NonStandardNFT from "../contracts/NonStandardNFT.cdc"
import MetadataViews from "../contracts/MetadataViews.cdc"
import FungibleToken from "../contracts/FungibleToken.cdc"
import DapperStorageRent from "../contracts/DapperStorageRent.cdc"

// This script uses the NFTMinter resource to mint a new NFT
// It must be run with the account that has the minter resource
Expand Down Expand Up @@ -61,6 +62,9 @@ transaction(
.borrow<&{NonFungibleToken.CollectionPublic}>()
?? panic("Could not get receiver reference to the NFT Collection")

// Attempt to refuel recipient before depositing NFT
DapperStorageRent.tryRefill(recipient)

// Mint the NFT and deposit it to the recipient's collection
self.minter.mintNFT(
recipient: receiver,
Expand Down
Loading