Smart Contract
FungibleToken
A.f233dcee88fe0abe.FungibleToken
1/**
2
3# The Flow Fungible Token standard
4
5## `FungibleToken` contract
6
7If a users wants to deploy a new token contract, their contract
8needs to implement the FungibleToken interface and their tokens
9need to implement the interfaces defined in this contract.
10
11/// Contributors (please add to this list if you contribute!):
12/// - Joshua Hannan - https://github.com/joshuahannan
13/// - Bastian Müller - https://twitter.com/turbolent
14/// - Dete Shirley - https://twitter.com/dete73
15/// - Bjarte Karlsen - https://twitter.com/0xBjartek
16/// - Austin Kline - https://twitter.com/austin_flowty
17/// - Giovanni Sanchez - https://twitter.com/gio_incognito
18/// - Deniz Edincik - https://twitter.com/bluesign
19/// - Jonny - https://github.com/dryruner
20///
21/// Repo reference: https://github.com/onflow/flow-ft
22
23## `Vault` resource interface
24
25Each fungible token resource type needs to implement the `Vault` resource interface.
26
27## `Provider`, `Receiver`, and `Balance` resource interfaces
28
29These interfaces declare pre-conditions and post-conditions that restrict
30the execution of the functions in the Vault.
31
32It gives users the ability to make custom resources that implement
33these interfaces to do various things with the tokens.
34For example, a faucet can be implemented by conforming
35to the Provider interface.
36
37*/
38
39import ViewResolver from 0x1d7e57aa55817448
40import Burner from 0xf233dcee88fe0abe
41
42/// FungibleToken
43///
44/// Fungible Token implementations should implement the fungible token
45/// interface.
46access(all) contract interface FungibleToken: ViewResolver {
47
48 // An entitlement for allowing the withdrawal of tokens from a Vault
49 access(all) entitlement Withdraw
50
51 /// The event that is emitted when tokens are withdrawn
52 /// from any Vault that implements the `Vault` interface
53 access(all) event Withdrawn(type: String,
54 amount: UFix64,
55 from: Address?,
56 fromUUID: UInt64,
57 withdrawnUUID: UInt64,
58 balanceAfter: UFix64)
59
60 /// The event that is emitted when tokens are deposited to
61 /// any Vault that implements the `Vault` interface
62 access(all) event Deposited(type: String,
63 amount: UFix64,
64 to: Address?,
65 toUUID: UInt64,
66 depositedUUID: UInt64,
67 balanceAfter: UFix64)
68
69 /// Event that is emitted when the global `Burner.burn()` method
70 /// is called with a non-zero balance
71 access(all) event Burned(type: String, amount: UFix64, fromUUID: UInt64)
72
73 /// Balance
74 ///
75 /// The interface that provides a standard field
76 /// for representing balance
77 ///
78 access(all) resource interface Balance: Burner.Burnable {
79 access(all) var balance: UFix64
80
81 // This default implementation needs to be in a separate interface
82 // from the one in `Vault` so that the conditions get enforced
83 // in the correct one
84 access(contract) fun burnCallback() {
85 self.balance = 0.0
86 }
87 }
88
89 /// Provider
90 ///
91 /// The interface that enforces the requirements for withdrawing
92 /// tokens from the implementing type.
93 ///
94 /// It does not enforce requirements on `balance` here,
95 /// because it leaves open the possibility of creating custom providers
96 /// that do not necessarily need their own balance.
97 ///
98 access(all) resource interface Provider {
99
100 /// Function to ask a provider if a specific amount of tokens
101 /// is available to be withdrawn
102 /// This could be useful to avoid panicing when calling withdraw
103 /// when the balance is unknown
104 /// Additionally, if the provider is pulling from multiple vaults
105 /// it only needs to check some of the vaults until the desired amount
106 /// is reached, potentially helping with performance.
107 ///
108 /// @param amount the amount of tokens requested to potentially withdraw
109 /// @return Bool Whether or not this amount is available to withdraw
110 ///
111 access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool
112
113 /// withdraw subtracts tokens from the implementing resource
114 /// and returns a Vault with the removed tokens.
115 ///
116 /// The function's access level is `access(Withdraw)`
117 /// So in order to access it, one would either need the object itself
118 /// or an entitled reference with `Withdraw`.
119 ///
120 /// @param amount the amount of tokens to withdraw from the resource
121 /// @return The Vault with the withdrawn tokens
122 ///
123 access(Withdraw) fun withdraw(amount: UFix64): @{Vault} {
124 post {
125 // `result` refers to the return value
126 result.balance == amount:
127 "FungibleToken.Provider.withdraw: Cannot withdraw tokens!"
128 .concat("The balance of the withdrawn tokens (").concat(result.balance.toString())
129 .concat(") is not equal to the amount requested to be withdrawn (")
130 .concat(amount.toString()).concat(")")
131 }
132 }
133 }
134
135 /// Receiver
136 ///
137 /// The interface that enforces the requirements for depositing
138 /// tokens into the implementing type.
139 ///
140 /// We do not include a condition that checks the balance because
141 /// we want to give users the ability to make custom receivers that
142 /// can do custom things with the tokens, like split them up and
143 /// send them to different places.
144 ///
145 access(all) resource interface Receiver {
146
147 /// deposit takes a Vault and deposits it into the implementing resource type
148 ///
149 /// @param from the Vault that contains the tokens to deposit
150 ///
151 access(all) fun deposit(from: @{Vault})
152
153 /// getSupportedVaultTypes returns a dictionary of Vault types
154 /// and whether the type is currently supported by this Receiver
155 ///
156 /// @return {Type: Bool} A dictionary that indicates the supported types
157 /// If a type is not supported, it should be `nil`, not false
158 ///
159 access(all) view fun getSupportedVaultTypes(): {Type: Bool}
160
161 /// Returns whether or not the given type is accepted by the Receiver
162 /// A vault that can accept any type should just return true by default
163 ///
164 /// @param type The type to query about
165 /// @return Bool Whether or not the vault type is supported
166 ///
167 access(all) view fun isSupportedVaultType(type: Type): Bool
168 }
169
170 /// Vault
171 /// Conforms to all other interfaces so that implementations
172 /// only have to conform to `Vault`
173 ///
174 access(all) resource interface Vault: Receiver, Provider, Balance, ViewResolver.Resolver, Burner.Burnable {
175
176 /// Field that tracks the balance of a vault
177 access(all) var balance: UFix64
178
179 /// Called when a fungible token is burned via the `Burner.burn()` method
180 /// Implementations can do any bookkeeping or emit any events
181 /// that should be emitted when a vault is destroyed.
182 /// Many implementations will want to update the token's total supply
183 /// to reflect that the tokens have been burned and removed from the supply.
184 /// Implementations also need to set the balance to zero before the end of the function
185 /// This is to prevent vault owners from spamming fake Burned events.
186 access(contract) fun burnCallback() {
187 pre {
188 emit Burned(type: self.getType().identifier, amount: self.balance, fromUUID: self.uuid)
189 }
190 post {
191 self.balance == 0.0:
192 "FungibleToken.Vault.burnCallback: Cannot burn this Vault with Burner.burn(). "
193 .concat("The balance must be set to zero during the burnCallback method so that it cannot be spammed.")
194 }
195 }
196
197 /// getSupportedVaultTypes
198 /// The default implementation is included here because vaults are expected
199 /// to only accepted their own type, so they have no need to provide an implementation
200 /// for this function
201 ///
202 access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
203 // Below check is implemented to make sure that run-time type would
204 // only get returned when the parent resource conforms with `FungibleToken.Vault`.
205 if self.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()) {
206 return {self.getType(): true}
207 } else {
208 // Return an empty dictionary as the default value for resource who don't
209 // implement `FungibleToken.Vault`, such as `FungibleTokenSwitchboard`, `TokenForwarder` etc.
210 return {}
211 }
212 }
213
214 /// Checks if the given type is supported by this Vault
215 access(all) view fun isSupportedVaultType(type: Type): Bool {
216 return self.getSupportedVaultTypes()[type] ?? false
217 }
218
219 /// withdraw subtracts `amount` from the Vault's balance
220 /// and returns a new Vault with the subtracted balance
221 ///
222 access(Withdraw) fun withdraw(amount: UFix64): @{Vault} {
223 pre {
224 self.balance >= amount:
225 "FungibleToken.Vault.withdraw: Cannot withdraw tokens! "
226 .concat("The amount requested to be withdrawn (").concat(amount.toString())
227 .concat(") is greater than the balance of the Vault (")
228 .concat(self.balance.toString()).concat(").")
229 }
230 post {
231 result.getType() == self.getType():
232 "FungibleToken.Vault.withdraw: Cannot withdraw tokens! "
233 .concat("The withdraw method tried to return an incompatible Vault type <")
234 .concat(result.getType().identifier).concat(">. ")
235 .concat("It must return a Vault with the same type as self <")
236 .concat(self.getType().identifier).concat(">.")
237
238 // use the special function `before` to get the value of the `balance` field
239 // at the beginning of the function execution
240 //
241 self.balance == before(self.balance) - amount:
242 "FungibleToken.Vault.withdraw: Cannot withdraw tokens! "
243 .concat("The sender's balance after the withdrawal (")
244 .concat(self.balance.toString())
245 .concat(") must be the difference of the previous balance (").concat(before(self.balance.toString()))
246 .concat(") and the amount withdrawn (").concat(amount.toString()).concat(")")
247
248 emit Withdrawn(
249 type: result.getType().identifier,
250 amount: amount,
251 from: self.owner?.address,
252 fromUUID: self.uuid,
253 withdrawnUUID: result.uuid,
254 balanceAfter: self.balance
255 )
256 }
257 }
258
259 /// deposit takes a Vault and adds its balance to the balance of this Vault
260 ///
261 access(all) fun deposit(from: @{FungibleToken.Vault}) {
262 // Assert that the concrete type of the deposited vault is the same
263 // as the vault that is accepting the deposit
264 pre {
265 from.isInstance(self.getType()):
266 "FungibleToken.Vault.deposit: Cannot deposit tokens! "
267 .concat("The type of the deposited tokens <")
268 .concat(from.getType().identifier)
269 .concat("> has to be the same type as the Vault being deposited into <")
270 .concat(self.getType().identifier)
271 .concat(">. Check that you are withdrawing and depositing to the correct paths in the sender and receiver accounts ")
272 .concat("and that those paths hold the same Vault types.")
273 }
274 post {
275 emit Deposited(
276 type: before(from.getType().identifier),
277 amount: before(from.balance),
278 to: self.owner?.address,
279 toUUID: self.uuid,
280 depositedUUID: before(from.uuid),
281 balanceAfter: self.balance
282 )
283 self.balance == before(self.balance) + before(from.balance):
284 "FungibleToken.Vault.deposit: Cannot deposit tokens! "
285 .concat("The receiver's balance after the deposit (")
286 .concat(self.balance.toString())
287 .concat(") must be the sum of the previous balance (").concat(before(self.balance.toString()))
288 .concat(") and the amount deposited (").concat(before(from.balance).toString()).concat(")")
289 }
290 }
291
292 /// createEmptyVault allows any user to create a new Vault that has a zero balance
293 ///
294 /// @return A Vault of the same type that has a balance of zero
295 access(all) fun createEmptyVault(): @{Vault} {
296 post {
297 result.balance == 0.0:
298 "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! "
299 .concat("The newly created Vault must have zero balance but it has a balance of ")
300 .concat(result.balance.toString())
301
302 result.getType() == self.getType():
303 "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! "
304 .concat("The type of the new Vault <")
305 .concat(result.getType().identifier)
306 .concat("> has to be the same type as the Vault that created it <")
307 .concat(self.getType().identifier)
308 .concat(">.")
309 }
310 }
311 }
312
313 /// createEmptyVault allows any user to create a new Vault that has a zero balance
314 ///
315 /// @return A Vault of the requested type that has a balance of zero
316 access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} {
317 post {
318 result.balance == 0.0:
319 "FungibleToken.createEmptyVault: Empty Vault creation failed! "
320 .concat("The newly created Vault must have zero balance but it has a balance of (")
321 .concat(result.balance.toString()).concat(")")
322
323 result.getType() == vaultType:
324 "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! "
325 .concat("The type of the new Vault <")
326 .concat(result.getType().identifier)
327 .concat("> has to be the same as the type that was requested <")
328 .concat(vaultType.identifier)
329 .concat(">.")
330 }
331 }
332}