templates.fa1_2

  1# Fungible Assets - FA12
  2# Inspired by https://gitlab.com/tzip/tzip/blob/master/A/FA1.2.md
  3
  4import smartpy as sp
  5
  6
  7# The metadata below is just an example, it serves as a base,
  8# the contents are used to build the metadata JSON that users
  9# can copy and upload to IPFS.
 10TZIP16_Metadata_Base = {
 11    "name": "SmartPy FA1.2 Token Template",
 12    "description": "Example Template for an FA1.2 Contract from SmartPy",
 13    "authors": ["SmartPy Dev Team <email@domain.com>"],
 14    "homepage": "https://smartpy.io",
 15    "interfaces": ["TZIP-007-2021-04-17", "TZIP-016-2021-04-17"],
 16}
 17
 18
 19@sp.module
 20def m():
 21    class AdminInterface(sp.Contract):
 22        @sp.private(with_storage="read-only")
 23        def is_administrator_(self, sender):
 24            sp.cast(sp.sender, sp.address)
 25            """Not standard, may be re-defined through inheritance."""
 26            return True
 27
 28    class CommonInterface(AdminInterface):
 29        def __init__(self):
 30            AdminInterface.__init__(self)
 31            self.data.balances = sp.cast(
 32                sp.big_map(),
 33                sp.big_map[
 34                    sp.address,
 35                    sp.record(approvals=sp.map[sp.address, sp.nat], balance=sp.nat),
 36                ],
 37            )
 38            self.data.total_supply = 0
 39            self.data.token_metadata = sp.cast(
 40                sp.big_map(),
 41                sp.big_map[
 42                    sp.nat,
 43                    sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]),
 44                ],
 45            )
 46            self.data.metadata = sp.cast(
 47                sp.big_map(),
 48                sp.big_map[sp.string, sp.bytes],
 49            )
 50            self.data.balances = sp.cast(
 51                sp.big_map(),
 52                sp.big_map[
 53                    sp.address,
 54                    sp.record(approvals=sp.map[sp.address, sp.nat], balance=sp.nat),
 55                ],
 56            )
 57            self.data.total_supply = 0
 58            self.data.token_metadata = sp.cast(
 59                sp.big_map(),
 60                sp.big_map[
 61                    sp.nat,
 62                    sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]),
 63                ],
 64            )
 65            self.data.metadata = sp.cast(
 66                sp.big_map(),
 67                sp.big_map[sp.string, sp.bytes],
 68            )
 69
 70        @sp.private(with_storage="read-only")
 71        def is_paused_(self):
 72            """Not standard, may be re-defined through inheritance."""
 73            return False
 74
 75    class Fa1_2(CommonInterface):
 76        def __init__(self, metadata, ledger, token_metadata):
 77            """
 78            token_metadata spec: https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-12/tzip-12.md#token-metadata
 79            Token-specific metadata is stored/presented as a Michelson value of type (map string bytes).
 80            A few of the keys are reserved and predefined:
 81
 82            - ""          : Should correspond to a TZIP-016 URI which points to a JSON representation of the token metadata.
 83            - "name"      : Should be a UTF-8 string giving a “display name” to the token.
 84            - "symbol"    : Should be a UTF-8 string for the short identifier of the token (e.g. XTZ, EUR, …).
 85            - "decimals"  : Should be an integer (converted to a UTF-8 string in decimal)
 86                which defines the position of the decimal point in token balances for display purposes.
 87
 88            contract_metadata spec: https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-16/tzip-16.md
 89            """
 90            CommonInterface.__init__(self)
 91            self.data.metadata = metadata
 92            self.data.token_metadata = sp.big_map(
 93                {0: sp.record(token_id=0, token_info=token_metadata)}
 94            )
 95
 96            for owner in ledger.items():
 97                self.data.balances[owner.key] = owner.value
 98                self.data.total_supply += owner.value.balance
 99
100            # TODO: Activate when this feature is implemented.
101            # self.init_metadata("metadata", metadata)
102
103        @sp.entrypoint
104        def transfer(self, param):
105            sp.cast(
106                param,
107                sp.record(from_=sp.address, to_=sp.address, value=sp.nat).layout(
108                    ("from_ as from", ("to_ as to", "value"))
109                ),
110            )
111            balance_from = self.data.balances.get(
112                param.from_, default=sp.record(balance=0, approvals={})
113            )
114            balance_to = self.data.balances.get(
115                param.to_, default=sp.record(balance=0, approvals={})
116            )
117            balance_from.balance = sp.as_nat(
118                balance_from.balance - param.value, error="FA1.2_InsufficientBalance"
119            )
120            balance_to.balance += param.value
121            if not self.is_administrator_(sp.sender):
122                assert not self.is_paused_(), "FA1.2_Paused"
123                if param.from_ != sp.sender:
124                    balance_from.approvals[sp.sender] = sp.as_nat(
125                        balance_from.approvals[sp.sender] - param.value,
126                        error="FA1.2_NotAllowed",
127                    )
128            self.data.balances[param.from_] = balance_from
129            self.data.balances[param.to_] = balance_to
130
131        @sp.entrypoint
132        def approve(self, param):
133            sp.cast(
134                param,
135                sp.record(spender=sp.address, value=sp.nat).layout(
136                    ("spender", "value")
137                ),
138            )
139            assert not self.is_paused_(), "FA1.2_Paused"
140            spender_balance = self.data.balances.get(
141                sp.sender, default=sp.record(balance=0, approvals={})
142            )
143            alreadyApproved = spender_balance.approvals.get(param.spender, default=0)
144            assert (
145                alreadyApproved == 0 or param.value == 0
146            ), "FA1.2_UnsafeAllowanceChange"
147            spender_balance.approvals[param.spender] = param.value
148            self.data.balances[sp.sender] = spender_balance
149
150        @sp.entrypoint
151        def getBalance(self, param):
152            (address, callback) = param
153            result = self.data.balances.get(
154                address, default=sp.record(balance=0, approvals={})
155            ).balance
156            sp.transfer(result, sp.tez(0), callback)
157
158        @sp.entrypoint
159        def getAllowance(self, param):
160            (args, callback) = param
161            result = self.data.balances.get(
162                args.owner, default=sp.record(balance=0, approvals={})
163            ).approvals.get(args.spender, default=0)
164            sp.transfer(result, sp.tez(0), callback)
165
166        @sp.entrypoint
167        def getTotalSupply(self, param):
168            sp.cast(param, sp.pair[sp.unit, sp.contract[sp.nat]])
169            sp.transfer(self.data.total_supply, sp.tez(0), sp.snd(param))
170
171        @sp.offchain_view()
172        def token_metadata(self, token_id):
173            """Return the token-metadata URI for the given token. (token_id must be 0)."""
174            sp.cast(token_id, sp.nat)
175            return self.data.token_metadata[token_id]
176
177    ##########
178    # Mixins #
179    ##########
180
181    class Admin(sp.Contract):
182        def __init__(self, administrator):
183            self.data.administrator = administrator
184
185        @sp.private(with_storage="read-only")
186        def is_administrator_(self, sender):
187            return sender == self.data.administrator
188
189        @sp.entrypoint
190        def setAdministrator(self, params):
191            sp.cast(params, sp.address)
192            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
193            self.data.administrator = params
194
195        @sp.entrypoint()
196        def getAdministrator(self, param):
197            sp.cast(param, sp.pair[sp.unit, sp.contract[sp.address]])
198            sp.transfer(self.data.administrator, sp.tez(0), sp.snd(param))
199
200        @sp.onchain_view()
201        def get_administrator(self):
202            return self.data.administrator
203
204    class Pause(AdminInterface):
205        def __init__(self):
206            AdminInterface.__init__(self)
207            self.data.paused = False
208
209        @sp.private(with_storage="read-only")
210        def is_paused_(self):
211            return self.data.paused
212
213        @sp.entrypoint
214        def setPause(self, param):
215            sp.cast(param, sp.bool)
216            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
217            self.data.paused = param
218
219    class Mint(CommonInterface):
220        def __init__(self):
221            CommonInterface.__init__(self)
222
223        @sp.entrypoint
224        def mint(self, param):
225            sp.cast(param, sp.record(address=sp.address, value=sp.nat))
226            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
227            receiver_balance = self.data.balances.get(
228                param.address, default=sp.record(balance=0, approvals={})
229            )
230            receiver_balance.balance += param.value
231            self.data.balances[param.address] = receiver_balance
232            self.data.total_supply += param.value
233
234    class Burn(CommonInterface):
235        def __init__(self):
236            CommonInterface.__init__(self)
237
238        @sp.entrypoint
239        def burn(self, param):
240            sp.cast(param, sp.record(address=sp.address, value=sp.nat))
241            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
242            receiver_balance = self.data.balances.get(
243                param.address, default=sp.record(balance=0, approvals={})
244            )
245            receiver_balance.balance = sp.as_nat(
246                receiver_balance.balance - param.value,
247                error="FA1.2_InsufficientBalance",
248            )
249            self.data.balances[param.address] = receiver_balance
250            self.data.total_supply = sp.as_nat(self.data.total_supply - param.value)
251
252    class ChangeMetadata(CommonInterface):
253        def __init__(self):
254            CommonInterface.__init__(self)
255
256        @sp.entrypoint
257        def update_metadata(self, key, value):
258            """An entrypoint to allow the contract metadata to be updated."""
259            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
260            self.data.metadata[key] = value
261
262    ##########
263    # Tests #
264    ##########
265
266    class Fa1_2TestFull(Admin, Pause, Fa1_2, Mint, Burn, ChangeMetadata):
267        def __init__(self, administrator, metadata, ledger, token_metadata):
268            ChangeMetadata.__init__(self)
269            Burn.__init__(self)
270            Mint.__init__(self)
271            Fa1_2.__init__(self, metadata, ledger, token_metadata)
272            Pause.__init__(self)
273            Admin.__init__(self, administrator)
274
275    class Viewer_nat(sp.Contract):
276        def __init__(self):
277            self.data.last = sp.cast(None, sp.option[sp.nat])
278
279        @sp.entrypoint
280        def target(self, params):
281            self.data.last = sp.Some(params)
282
283    class Viewer_address(sp.Contract):
284        def __init__(self):
285            self.data.last = sp.cast(None, sp.option[sp.address])
286
287        @sp.entrypoint
288        def target(self, params):
289            self.data.last = sp.Some(params)
290
291
292if "main" in __name__:
293
294    @sp.add_test()
295    def test():
296        sc = sp.test_scenario("FA12", m)
297        sc.h1("FA1.2 template - Fungible assets")
298
299        # sp.test_account generates ED25519 key-pairs deterministically:
300        admin = sp.test_account("Administrator")
301        alice = sp.test_account("Alice")
302        bob = sp.test_account("Robert")
303
304        # Let's display the accounts:
305        sc.h1("Accounts")
306        sc.show([admin, alice, bob])
307
308        sc.h1("Contract")
309        token_metadata = {
310            "decimals": sp.scenario_utils.bytes_of_string(
311                "18"
312            ),  # Mandatory by the spec
313            "name": sp.scenario_utils.bytes_of_string("My Great Token"),  # Recommended
314            "symbol": sp.scenario_utils.bytes_of_string("MGT"),  # Recommended
315            # Extra fields
316            "icon": sp.scenario_utils.bytes_of_string(
317                "https://smartpy.io/static/img/logo-only.svg"
318            ),
319        }
320        contract_metadata = sp.scenario_utils.metadata_of_url(
321            "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd"
322        )
323
324        c1 = m.Fa1_2TestFull(
325            administrator=admin.address,
326            metadata=contract_metadata,
327            token_metadata=token_metadata,
328            ledger={},
329        )
330        sc += c1
331
332        sc.h1("Offchain view - token_metadata")
333        sc.verify_equal(
334            sp.View(c1, "token_metadata")(0),
335            sp.record(
336                token_id=0,
337                token_info=sp.map(
338                    {
339                        "decimals": sp.scenario_utils.bytes_of_string("18"),
340                        "name": sp.scenario_utils.bytes_of_string("My Great Token"),
341                        "symbol": sp.scenario_utils.bytes_of_string("MGT"),
342                        "icon": sp.scenario_utils.bytes_of_string(
343                            "https://smartpy.io/static/img/logo-only.svg"
344                        ),
345                    }
346                ),
347            ),
348        )
349
350        sc.h1("Attempt to update metadata")
351        sc.verify(
352            c1.data.metadata[""]
353            == sp.scenario_utils.bytes_of_string(
354                "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd"
355            )
356        )
357        c1.update_metadata(key="", value=sp.bytes("0x00"), _sender=admin)
358        sc.verify(c1.data.metadata[""] == sp.bytes("0x00"))
359
360        sc.h1("Entrypoints")
361        sc.h2("Admin mints a few coins")
362        c1.mint(address=alice.address, value=12, _sender=admin)
363        c1.mint(address=alice.address, value=3, _sender=admin)
364        c1.mint(address=alice.address, value=3, _sender=admin)
365        sc.h2("Alice transfers to Bob")
366        c1.transfer(from_=alice.address, to_=bob.address, value=4, _sender=alice)
367        sc.verify(c1.data.balances[alice.address].balance == 14)
368        sc.h2("Bob tries to transfer from Alice but he doesn't have her approval")
369        c1.transfer(
370            from_=alice.address, to_=bob.address, value=4, _sender=bob, _valid=False
371        )
372        sc.h2("Alice approves Bob and Bob transfers")
373        c1.approve(spender=bob.address, value=5, _sender=alice)
374        c1.transfer(from_=alice.address, to_=bob.address, value=4, _sender=bob)
375        sc.h2("Bob tries to over-transfer from Alice")
376        c1.transfer(
377            from_=alice.address, to_=bob.address, value=4, _sender=bob, _valid=False
378        )
379        sc.h2("Admin burns Bob token")
380        c1.burn(address=bob.address, value=1, _sender=admin)
381        sc.verify(c1.data.balances[alice.address].balance == 10)
382        sc.h2("Alice tries to burn Bob token")
383        c1.burn(address=bob.address, value=1, _sender=alice, _valid=False)
384        sc.h2("Admin pauses the contract and Alice cannot transfer anymore")
385        c1.setPause(True, _sender=admin)
386        c1.transfer(
387            from_=alice.address, to_=bob.address, value=4, _sender=alice, _valid=False
388        )
389        sc.verify(c1.data.balances[alice.address].balance == 10)
390        sc.h2("Admin transfers while on pause")
391        c1.transfer(from_=alice.address, to_=bob.address, value=1, _sender=admin)
392        sc.h2("Admin unpauses the contract and transfers are allowed")
393        c1.setPause(False, _sender=admin)
394        sc.verify(c1.data.balances[alice.address].balance == 9)
395        c1.transfer(from_=alice.address, to_=bob.address, value=1, _sender=alice)
396
397        sc.verify(c1.data.total_supply == 17)
398        sc.verify(c1.data.balances[alice.address].balance == 8)
399        sc.verify(c1.data.balances[bob.address].balance == 9)
400
401        sc.h1("Views")
402        sc.h2("Balance")
403        view_balance = m.Viewer_nat()
404        sc += view_balance
405        target = sp.contract(sp.nat, view_balance.address, "target").unwrap_some()
406        c1.getBalance((alice.address, target))
407        sc.verify_equal(view_balance.data.last, sp.Some(8))
408
409        sc.h2("Administrator")
410        view_administrator = m.Viewer_address()
411        sc += view_administrator
412        target = sp.contract(
413            sp.address, view_administrator.address, "target"
414        ).unwrap_some()
415        c1.getAdministrator((sp.unit, target))
416        sc.verify_equal(view_administrator.data.last, sp.Some(admin.address))
417
418        sc.h2("Total Supply")
419        view_totalSupply = m.Viewer_nat()
420        sc += view_totalSupply
421        target = sp.contract(sp.nat, view_totalSupply.address, "target").unwrap_some()
422        c1.getTotalSupply((sp.unit, target))
423        sc.verify_equal(view_totalSupply.data.last, sp.Some(17))
424
425        sc.h2("Allowance")
426        view_allowance = m.Viewer_nat()
427        sc += view_allowance
428        target = sp.contract(sp.nat, view_allowance.address, "target").unwrap_some()
429        c1.getAllowance((sp.record(owner=alice.address, spender=bob.address), target))
430        sc.verify_equal(view_allowance.data.last, sp.Some(1))
TZIP16_Metadata_Base = {'name': 'SmartPy FA1.2 Token Template', 'description': 'Example Template for an FA1.2 Contract from SmartPy', 'authors': ['SmartPy Dev Team <email@domain.com>'], 'homepage': 'https://smartpy.io', 'interfaces': ['TZIP-007-2021-04-17', 'TZIP-016-2021-04-17']}