Smart Contract

DapperStorageRent

A.a08e88e23f332538.DapperStorageRent

Deployed

1w ago
Feb 15, 2026, 03:08:46 PM UTC

Dependents

2275 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import PrivateReceiverForwarder from 0x18eb4ee6b3c026d2
4
5///
6/// DapperStorageRent
7/// Provide a means for accounts storage TopUps. To be used during transaction execution.
8access(all) contract DapperStorageRent {
9
10  access(all) let DapperStorageRentAdminStoragePath: StoragePath
11
12  /// Threshold of storage required to trigger a refill
13  access(contract) var StorageRentRefillThreshold: UInt64
14  /// List of all refilledAccounts
15  access(contract) var RefilledAccounts: [Address]
16  /// Detailed account information of refilled accounts
17  access(contract) var RefilledAccountInfos: {Address: RefilledAccountInfo}
18  /// List of all blockedAccounts
19  access(contract) var BlockedAccounts: [Address]
20  /// Blocks required between refill attempts
21  access(contract) var RefillRequiredBlocks: UInt64
22
23  /// Event emitted when an Admin blocks an address
24  access(all) event BlockedAddress(_ address: [Address])
25  /// Event emitted when a Refill is successful
26  access(all) event Refuelled(_ address: Address)
27  /// Event emitted when a Refill is not successful
28  access(all) event RefilledFailed(address: Address, reason: String)
29
30  /// getStorageRentRefillThreshold
31  /// Get the current StorageRentRefillThreshold
32  ///
33  /// @return UInt64 value of the current StorageRentRefillThreshold value
34  access(all) view fun getStorageRentRefillThreshold(): UInt64 {
35    return self.StorageRentRefillThreshold
36  }
37
38  /// getRefilledAccounts
39  /// Get the current StorageRentRefillThreshold
40  ///
41  /// @return List of refilled Accounts
42  access(all) view fun getRefilledAccounts(): [Address] {
43    return self.RefilledAccounts
44  }
45
46  /// getBlockedAccounts
47  /// Get the current StorageRentRefillThreshold
48  ///
49  /// @return List of blocked accounts
50  access(all) view fun getBlockedAccounts() : [Address] {
51    return self.BlockedAccounts
52  }
53
54  /// getRefilledAccountInfos
55  /// Get the current StorageRentRefillThreshold
56  ///
57  /// @return Address: RefilledAccountInfo mapping
58  access(all) view fun getRefilledAccountInfos(): {Address: RefilledAccountInfo} {
59    return self.RefilledAccountInfos
60  }
61
62  /// getRefillRequiredBlocks
63  /// Get the current StorageRentRefillThreshold
64  ///
65  /// @return UInt64 value of the current RefillRequiredBlocks value
66  access(all) view fun getRefillRequiredBlocks(): UInt64 {
67    return self.RefillRequiredBlocks
68  }
69
70  access(all) fun fundedRefill(address: Address, tokens: @FlowToken.Vault) {
71    let privateForwardingSenderRef = self.account.storage.borrow<&PrivateReceiverForwarder.Sender>(from: PrivateReceiverForwarder.SenderStoragePath)
72      privateForwardingSenderRef!.sendPrivateTokens(address,tokens:<-tokens)
73  }
74
75  access(all) fun fundedRefillV2(address: Address, tokens: @FlowToken.Vault): @FlowToken.Vault {
76    let privateForwardingSenderRef = self.account.storage.borrow<&PrivateReceiverForwarder.Sender>(from: PrivateReceiverForwarder.SenderStoragePath)
77      privateForwardingSenderRef!.sendPrivateTokens(address,tokens:<-tokens.withdraw(amount: tokens.balance))
78      return <-tokens
79  }
80
81  /// tryRefill
82  /// Attempt to refill an accounts storage capacity if it has dipped below threshold and passes other checks.
83  ///
84  /// @param address: Address to attempt a storage refill on
85  access(all) fun tryRefill(_ address: Address) {
86    let REFUEL_AMOUNT = 0.06;
87
88    self.cleanExpiredRefilledAccounts(10)
89
90    // Get the Flow Token reciever of the address
91    let recipient = getAccount(address)
92    let receiverRef = recipient.capabilities.borrow<&PrivateReceiverForwarder.Forwarder>(PrivateReceiverForwarder.PrivateReceiverPublicPath)
93
94    // Silently fail if the `receiverRef` is `nill`
95    if receiverRef == nil || receiverRef!.owner == nil {
96      emit RefilledFailed(address: address, reason: "Couldn't borrow the Accounts flowTokenVault")
97      return
98    }
99
100    // Silently fail if the account has already be refueled within the block allowance
101    if self.RefilledAccountInfos[address] != nil && getCurrentBlock().height - self.RefilledAccountInfos[address]!.atBlock < self.RefillRequiredBlocks {
102      emit RefilledFailed(address: address, reason: "RefillRequiredBlocks")
103      return
104    }
105
106    if recipient.storage.used < recipient.storage.capacity && (recipient.storage.capacity - recipient.storage.used) > self.StorageRentRefillThreshold {
107      emit RefilledFailed(address: address, reason: "Address is not below StorageRentRefillThreshold")
108      return
109    }
110
111    let vaultRef = self.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
112    if vaultRef == nil {
113      emit RefilledFailed(address: address, reason: "Couldn't borrow the Accounts FlowToken.Vault")
114      return
115    }
116
117    let privateForwardingSenderRef = self.account.storage.borrow<&PrivateReceiverForwarder.Sender>(from: PrivateReceiverForwarder.SenderStoragePath)
118    if privateForwardingSenderRef == nil {
119      emit RefilledFailed(address: address, reason: "Couldn't borrow the Accounts PrivateReceiverForwarder")
120      return
121    }
122
123    // Check to make sure the payment vault has sufficient funds
124    if let vaultBalanceRef = self.account.capabilities.borrow<&{FungibleToken.Balance}>(/public/flowTokenBalance) {
125      if vaultBalanceRef.balance <= REFUEL_AMOUNT {
126        emit RefilledFailed(address: address, reason: "Insufficient balance to refuel")
127        return
128      }
129    } else {
130      emit RefilledFailed(address: address, reason: "Couldn't borrow flowToken balance")
131      return
132    }
133
134    // 0.06 = 6MB of storage, or ~20k NBA TS moments
135    self.addRefilledAccount(address)
136    privateForwardingSenderRef!.sendPrivateTokens(address,tokens:<-vaultRef!.withdraw(amount: REFUEL_AMOUNT))
137    emit Refuelled(address)
138  }
139
140  /// checkEligibility
141  ///
142  /// @param address: Address to check eligibility on
143  /// @return Boolean valued based on if the provided address is below the storage threshold
144  access(all) fun checkEligibility(_ address: Address): Bool {
145    if self.RefilledAccountInfos[address] != nil && getCurrentBlock().height - self.RefilledAccountInfos[address]!.atBlock < self.RefillRequiredBlocks {
146       return false
147    }
148    let acct = getAccount(address)
149
150    if acct.storage.used < acct.storage.capacity && (acct.storage.capacity - acct.storage.used) > self.StorageRentRefillThreshold {
151       return false
152    }
153
154    return true
155  }
156
157  /// addRefilledAccount
158  ///
159  /// @param address: Address to add to RefilledAccounts/RefilledAccountInfos
160  access(contract) fun addRefilledAccount(_ address: Address) {
161    if self.RefilledAccountInfos[address] != nil {
162      self.RefilledAccounts.remove(at: self.RefilledAccountInfos[address]!.index)
163    }
164
165    self.RefilledAccounts.append(address)
166    self.RefilledAccountInfos[address] = RefilledAccountInfo(self.RefilledAccounts.length-1, getCurrentBlock().height)
167  }
168
169  /// cleanExpiredRefilledAccounts
170  /// public method to clean up expired accounts based on current block height
171  ///
172  /// @param batchSize: Int to set the batch size of the cleanup
173  access(all) fun cleanExpiredRefilledAccounts(_ batchSize: Int) {
174    var index = 0
175    while index < batchSize && self.RefilledAccounts.length > index {
176      if self.RefilledAccountInfos[self.RefilledAccounts[index]] != nil &&
177        getCurrentBlock().height - self.RefilledAccountInfos[self.RefilledAccounts[index]]!.atBlock < self.RefillRequiredBlocks {
178        break
179      }
180
181      self.RefilledAccountInfos.remove(key: self.RefilledAccounts[index])
182      self.RefilledAccounts.remove(at: index)
183      index = index + 1
184    }
185  }
186
187  /// RefilledAccountInfo struct
188  /// Holds the block number it was refilled at
189  access(all) struct RefilledAccountInfo {
190    access(all) let atBlock: UInt64
191    access(all) let index: Int
192
193    init(_ index: Int, _ atBlock: UInt64) {
194      self.index = index
195      self.atBlock = atBlock
196    }
197  }
198
199  /// Admin resource
200  /// Used to set different configuration levers such as StorageRentRefillThreshold, RefillRequiredBlocks, and BlockedAccounts
201  access(all) resource Admin {
202    access(all) fun setStorageRentRefillThreshold(_ threshold: UInt64) {
203      DapperStorageRent.StorageRentRefillThreshold = threshold
204    }
205
206    access(all) fun setRefillRequiredBlocks(_ blocks: UInt64) {
207      DapperStorageRent.RefillRequiredBlocks = blocks
208    }
209
210    access(all) fun blockAddress(_ address: Address) {
211        if !DapperStorageRent.getBlockedAccounts().contains(address) {
212            DapperStorageRent.BlockedAccounts.append(address)
213            emit BlockedAddress(DapperStorageRent.getBlockedAccounts())
214        }
215    }
216
217    access(all) fun unblockAddress(_ address: Address) {
218        if DapperStorageRent.getBlockedAccounts().contains(address) {
219            let position = DapperStorageRent.BlockedAccounts.firstIndex(of: address) ?? panic("Trying to unblock an address that is not blocked.")
220            if position != nil {
221                DapperStorageRent.BlockedAccounts.remove(at: position)
222                emit BlockedAddress(DapperStorageRent.getBlockedAccounts())
223            }
224        }
225    }
226  }
227
228  // DapperStorageRent init
229  init() {
230    self.DapperStorageRentAdminStoragePath = /storage/DapperStorageRentAdmin
231    self.StorageRentRefillThreshold = 5000
232    self.RefilledAccounts = []
233    self.RefilledAccountInfos = {}
234    self.RefillRequiredBlocks = 86400
235    self.BlockedAccounts = []
236
237    let admin <- create Admin()
238    self.account.storage.save(<-admin, to: self.DapperStorageRentAdminStoragePath)
239  }
240}