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']}