Created
June 27, 2021 11:54
-
-
Save AnshuJalan/2c6a94a18db28194e5550d882e62615a to your computer and use it in GitHub Desktop.
A modified Tezos FA12 token which records the balances at varying levels. This primarily assists token voting mechanisms not requiring locking up of tokens.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import smartpy as sp | |
# The metadata below is just an example, it serves as a base, | |
# the contents are used to build the metadata JSON that users | |
# can copy and upload to IPFS. | |
TZIP16_Metadata_Base = { | |
"name": "Sample FA1.2 Token", | |
"description": "FA1.2 based token supporting level based snapshots, to assist token voting.", | |
"authors": ["Anshu Jalan (anshujalan206@gmail.com)"], | |
"interfaces": ["TZIP-007-2021-04-17", "TZIP-016-2021-04-17"], | |
} | |
# A collection of error messages used in the contract. | |
class FA12_Error: | |
def make(s): | |
return "FA1.2_" + s | |
NotAdmin = make("NotAdmin") | |
InsufficientBalance = make("InsufficientBalance") | |
UnsafeAllowanceChange = make("UnsafeAllowanceChange") | |
Paused = make("Paused") | |
NotAllowed = make("NotAllowed") | |
## | |
## ## Meta-Programming Configuration | |
## | |
## The `FA12_config` class holds the meta-programming configuration. | |
## | |
class FA12_config: | |
def __init__( | |
self, | |
support_upgradable_metadata=False, | |
use_token_metadata_offchain_view=True, | |
): | |
self.support_upgradable_metadata = support_upgradable_metadata | |
# Whether the contract metadata can be upgradable or not. | |
# When True a new entrypoint `change_metadata` will be added. | |
self.use_token_metadata_offchain_view = use_token_metadata_offchain_view | |
# Include offchain view for accessing the token metadata (requires TZIP-016 contract metadata) | |
class FA12_common: | |
def normalize_metadata(self, metadata): | |
""" | |
Helper function to build metadata JSON (string => bytes). | |
""" | |
for key in metadata: | |
metadata[key] = sp.utils.bytes_of_string(metadata[key]) | |
return metadata | |
class FA12_core(sp.Contract, FA12_common): | |
def __init__(self, config, **extra_storage): | |
self.config = config | |
# Type for a checkpoint (based on Block Level) | |
checkpoint = sp.TRecord(level=sp.TNat, value=sp.TNat) | |
self.init( | |
balances=sp.big_map( | |
tvalue=sp.TRecord( | |
approvals=sp.TMap(sp.TAddress, sp.TNat), | |
balance=sp.TNat, | |
) | |
), | |
# Stores the balance of an address at varying level | |
balanceSnapshots=sp.big_map(tkey=sp.TAddress, tvalue=sp.TMap(sp.TNat, checkpoint)), | |
totalSupply=0, | |
**extra_storage | |
) | |
@sp.entry_point | |
def transfer(self, params): | |
sp.set_type( | |
params, | |
sp.TRecord(from_=sp.TAddress, to_=sp.TAddress, value=sp.TNat).layout( | |
("from_ as from", ("to_ as to", "value")) | |
), | |
) | |
sp.verify( | |
self.is_administrator(sp.sender) | |
| ( | |
~self.is_paused() | |
& ( | |
(params.from_ == sp.sender) | |
| (self.data.balances[params.from_].approvals[sp.sender] >= params.value) | |
) | |
), | |
FA12_Error.NotAllowed, | |
) | |
self.addAddressIfNecessary(params.from_) | |
self.addAddressIfNecessary(params.to_) | |
sp.verify( | |
self.data.balances[params.from_].balance >= params.value, FA12_Error.InsufficientBalance | |
) | |
self.data.balances[params.from_].balance = sp.as_nat( | |
self.data.balances[params.from_].balance - params.value | |
) | |
# Take from_ balance snapshot | |
self.addBalanceSnapshot(address=params.from_, level=sp.level) | |
self.data.balances[params.to_].balance += params.value | |
# Take to_ balance snapshot | |
self.addBalanceSnapshot(address=params.to_, level=sp.level) | |
sp.if (params.from_ != sp.sender) & (~self.is_administrator(sp.sender)): | |
self.data.balances[params.from_].approvals[sp.sender] = sp.as_nat( | |
self.data.balances[params.from_].approvals[sp.sender] - params.value | |
) | |
@sp.entry_point | |
def approve(self, params): | |
sp.set_type(params, sp.TRecord(spender=sp.TAddress, value=sp.TNat).layout(("spender", "value"))) | |
self.addAddressIfNecessary(sp.sender) | |
sp.verify(~self.is_paused(), FA12_Error.Paused) | |
alreadyApproved = self.data.balances[sp.sender].approvals.get(params.spender, 0) | |
sp.verify((alreadyApproved == 0) | (params.value == 0), FA12_Error.UnsafeAllowanceChange) | |
self.data.balances[sp.sender].approvals[params.spender] = params.value | |
def addAddressIfNecessary(self, address): | |
sp.if ~self.data.balances.contains(address): | |
self.data.balances[address] = sp.record(balance=0, approvals={}) | |
# Add the address to the snapshots big_map | |
self.data.balanceSnapshots[address] = { 0: sp.record(level = 0, value = 0) } | |
# Adds the token balance snapshot at the specfied level | |
def addBalanceSnapshot(self, address, level): | |
snapshots_map = self.data.balanceSnapshots[address] | |
balance_value = self.data.balances[address].balance | |
# Incase there is a previous snapshot in the same level, delete that and add fresh | |
sp.if (sp.len(snapshots_map) > 0) & (snapshots_map[sp.as_nat(sp.len(snapshots_map) - 1)].level == level): | |
del snapshots_map[sp.as_nat(sp.len(snapshots_map) - 1)] | |
snapshots_map[sp.len(snapshots_map)] = sp.record(level=level, value=balance_value) | |
@sp.utils.view(sp.TNat) | |
def getBalance(self, params): | |
sp.if self.data.balances.contains(params): | |
sp.result(self.data.balances[params].balance) | |
sp.else: | |
sp.result(sp.nat(0)) | |
# Retrieves the balance closest to the specified level | |
@sp.utils.view(sp.TNat) | |
def getBalanceAt(self, params): | |
sp.set_type(params, sp.TRecord(address=sp.TAddress, level=sp.TNat)) | |
sp.if self.data.balanceSnapshots.contains(params.address): | |
# Find the appropriate level using binary search | |
checkpoints = self.data.balanceSnapshots[params.address] | |
low = sp.local("low", 0) | |
high = sp.local("high", sp.as_nat(sp.len(checkpoints) - 2)) | |
mid = sp.local("mid", 0) | |
sp.while (low.value < high.value) & (checkpoints[mid.value].level != params.level): | |
mid.value = (low.value + high.value + 1) // 2 | |
sp.if checkpoints[mid.value].level < params.level: | |
low.value = mid.value | |
sp.if checkpoints[mid.value].level > params.level: | |
high.value = sp.as_nat(mid.value - 1) | |
sp.if checkpoints[mid.value].level == params.level: | |
sp.result(checkpoints[mid.value].value) | |
sp.else: | |
sp.result(checkpoints[low.value].value) | |
sp.else: | |
sp.result(sp.nat(0)) | |
@sp.utils.view(sp.TNat) | |
def getAllowance(self, params): | |
sp.if self.data.balances.contains(params.owner): | |
sp.result(self.data.balances[params.owner].approvals.get(params.spender, 0)) | |
sp.else: | |
sp.result(sp.nat(0)) | |
@sp.utils.view(sp.TNat) | |
def getTotalSupply(self, params): | |
sp.set_type(params, sp.TUnit) | |
sp.result(self.data.totalSupply) | |
# this is not part of the standard but can be supported through inheritance. | |
def is_paused(self): | |
return sp.bool(False) | |
# this is not part of the standard but can be supported through inheritance. | |
def is_administrator(self, sender): | |
return sp.bool(False) | |
class FA12_mint_burn(FA12_core): | |
@sp.entry_point | |
def mint(self, params): | |
sp.set_type(params, sp.TRecord(address=sp.TAddress, value=sp.TNat)) | |
sp.verify(self.is_administrator(sp.sender), FA12_Error.NotAdmin) | |
self.addAddressIfNecessary(params.address) | |
self.data.balances[params.address].balance += params.value | |
# Add to level snapshot | |
self.addBalanceSnapshot(address=params.address, level=sp.level) | |
self.data.totalSupply += params.value | |
@sp.entry_point | |
def burn(self, params): | |
sp.set_type(params, sp.TRecord(address=sp.TAddress, value=sp.TNat)) | |
sp.verify(self.is_administrator(sp.sender), FA12_Error.NotAdmin) | |
sp.verify( | |
self.data.balances[params.address].balance >= params.value, | |
FA12_Error.InsufficientBalance, | |
) | |
self.data.balances[params.address].balance = sp.as_nat( | |
self.data.balances[params.address].balance - params.value | |
) | |
# Add to level snapshot | |
self.addBalanceSnapshot(address=params.address, level=sp.level) | |
self.data.totalSupply = sp.as_nat(self.data.totalSupply - params.value) | |
class FA12_administrator(FA12_core): | |
def is_administrator(self, sender): | |
return sender == self.data.administrator | |
@sp.entry_point | |
def setAdministrator(self, params): | |
sp.set_type(params, sp.TAddress) | |
sp.verify(self.is_administrator(sp.sender), FA12_Error.NotAdmin) | |
self.data.administrator = params | |
@sp.utils.view(sp.TAddress) | |
def getAdministrator(self, params): | |
sp.set_type(params, sp.TUnit) | |
sp.result(self.data.administrator) | |
class FA12_pause(FA12_core): | |
def is_paused(self): | |
return self.data.paused | |
@sp.entry_point | |
def setPause(self, params): | |
sp.set_type(params, sp.TBool) | |
sp.verify(self.is_administrator(sp.sender), FA12_Error.NotAdmin) | |
self.data.paused = params | |
class FA12_token_metadata(FA12_core): | |
def set_token_metadata(self, metadata): | |
""" | |
Store the token_metadata values in a big-map annotated %token_metadata | |
of type (big_map nat (pair (nat %token_id) (map %token_info string bytes))). | |
""" | |
self.update_initial_storage( | |
token_metadata=sp.big_map( | |
{0: sp.record(token_id=0, token_info=self.normalize_metadata(metadata))}, | |
tkey=sp.TNat, | |
tvalue=sp.TRecord(token_id=sp.TNat, token_info=sp.TMap(sp.TString, sp.TBytes)), | |
) | |
) | |
class FA12_contract_metadata(FA12_core): | |
def generate_tzip16_metadata(self): | |
views = [] | |
def token_metadata(self, token_id): | |
sp.set_type(token_id, sp.TNat) | |
sp.result(self.data.token_metadata[token_id]) | |
if self.usingTokenMetadata and self.config.use_token_metadata_offchain_view: | |
self.token_metadata = sp.offchain_view(pure=True, doc="Get Token Metadata")(token_metadata) | |
views += [self.token_metadata] | |
metadata = {**TZIP16_Metadata_Base, "views": views} | |
self.init_metadata("metadata", metadata) | |
def set_contract_metadata(self, metadata): | |
""" | |
Set contract metadata | |
""" | |
self.update_initial_storage(metadata=sp.big_map(self.normalize_metadata(metadata))) | |
if self.config.support_upgradable_metadata: | |
def update_metadata(self, key, value): | |
""" | |
An entry-point to allow the contract metadata to be updated. | |
Can be removed with `FA12_config(support_upgradable_metadata = False, ...)` | |
""" | |
sp.verify(self.is_administrator(sp.sender), FA12_Error.NotAdmin) | |
self.data.metadata[key] = value | |
self.update_metadata = sp.entry_point(update_metadata) | |
class FA12( | |
FA12_mint_burn, | |
FA12_administrator, | |
FA12_pause, | |
FA12_token_metadata, | |
FA12_contract_metadata, | |
FA12_core, | |
): | |
def __init__(self, admin, config, token_metadata=None, contract_metadata=None): | |
FA12_core.__init__(self, config, paused=False, administrator=admin) | |
if token_metadata is None and contract_metadata is None: | |
raise Exception( | |
"""\n | |
Contract must contain at least of the following: | |
\t- TZIP-016 %metadata big-map, | |
\t- Token-specific-metadata through the %token_metadata big-map | |
More info: https://gitlab.com/tzip/tzip/blob/master/proposals/tzip-7/tzip-7.md#token-metadata | |
""" | |
) | |
self.usingTokenMetadata = False | |
if token_metadata is not None: | |
self.usingTokenMetadata = True | |
self.set_token_metadata(token_metadata) | |
if contract_metadata is not None: | |
self.set_contract_metadata(contract_metadata) | |
# This is only an helper, it produces metadata in the output panel | |
# that users can copy and upload to IPFS. | |
self.generate_tzip16_metadata() | |
class Viewer(sp.Contract): | |
def __init__(self, t): | |
self.init(last=sp.none) | |
self.init_type(sp.TRecord(last=sp.TOption(t))) | |
@sp.entry_point | |
def target(self, params): | |
self.data.last = sp.some(params) | |
# Used to test offchain views | |
class TestOffchainView(sp.Contract): | |
def __init__(self, f): | |
self.f = f.f | |
self.init(result=sp.none) | |
@sp.entry_point | |
def compute(self, data, params): | |
b = sp.bind_block() | |
with b: | |
self.f(sp.record(data=data), params) | |
self.data.result = sp.some(b.value) | |
if "templates" not in __name__: | |
@sp.add_test(name="FA12") | |
def test(): | |
scenario = sp.test_scenario() | |
scenario.h1("FA1.2 template - Fungible assets") | |
scenario.table_of_contents() | |
# sp.test_account generates ED25519 key-pairs deterministically: | |
admin = sp.test_account("Administrator") | |
alice = sp.test_account("Alice") | |
bob = sp.test_account("Robert") | |
# Let's display the accounts: | |
scenario.h1("Accounts") | |
scenario.show([admin, alice, bob]) | |
scenario.h1("Contract") | |
token_metadata = { | |
"decimals": "18", # Mandatory by the spec | |
"name": "My Great Token", # Recommended | |
"symbol": "MGT", # Recommended | |
# Extra fields | |
"icon": "https://smartpy.io/static/img/logo-only.svg", | |
} | |
contract_metadata = { | |
"": "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd", | |
} | |
c1 = FA12( | |
admin.address, | |
config=FA12_config(support_upgradable_metadata=True), | |
token_metadata=token_metadata, | |
contract_metadata=contract_metadata, | |
) | |
scenario += c1 | |
scenario.h1("Entry points") | |
scenario.h2("Admin mints a few coins") | |
c1.mint(address=alice.address, value=12).run(sender=admin, level = 1) | |
c1.mint(address=alice.address, value=3).run(sender=admin, level = 1) | |
c1.mint(address=alice.address, value=3).run(sender=admin, level = 1) | |
scenario.h2("Alice transfers to Bob") | |
c1.transfer(from_=alice.address, to_=bob.address, value=4).run(sender=alice, level=2) | |
scenario.verify(c1.data.balances[alice.address].balance == 14) | |
scenario.h2("Bob tries to transfer from Alice but he doesn't have her approval") | |
c1.transfer(from_=alice.address, to_=bob.address, value=4).run( | |
sender=bob, valid=False, level=3 | |
) | |
scenario.h2("Alice approves Bob and Bob transfers") | |
c1.approve(spender=bob.address, value=5).run(sender=alice) | |
c1.transfer(from_=alice.address, to_=bob.address, value=4).run(sender=bob, level=4) | |
scenario.h2("Bob tries to over-transfer from Alice") | |
c1.transfer(from_=alice.address, to_=bob.address, value=4).run(sender=bob, valid=False) | |
scenario.h2("Admin burns Bob token") | |
c1.burn(address=bob.address, value=1).run(sender=admin, level = 5) | |
scenario.verify(c1.data.balances[alice.address].balance == 10) | |
scenario.h2("Alice tries to burn Bob token") | |
c1.burn(address=bob.address, value=1).run(sender=alice, valid=False) | |
scenario.h2("Admin pauses the contract and Alice cannot transfer anymore") | |
c1.setPause(True).run(sender=admin) | |
c1.transfer(from_=alice.address, to_=bob.address, value=4).run(sender=alice, valid=False) | |
scenario.verify(c1.data.balances[alice.address].balance == 10) | |
scenario.h2("Admin transfers while on pause") | |
c1.transfer(from_=alice.address, to_=bob.address, value=1).run(sender=admin, level=8) | |
scenario.h2("Admin unpauses the contract and transferts are allowed") | |
c1.setPause(False).run(sender=admin) | |
scenario.verify(c1.data.balances[alice.address].balance == 9) | |
c1.transfer(from_=alice.address, to_=bob.address, value=1).run(sender=alice, level=14) | |
scenario.verify(c1.data.totalSupply == 17) | |
scenario.verify(c1.data.balances[alice.address].balance == 8) | |
scenario.verify(c1.data.balances[bob.address].balance == 9) | |
scenario.h1("Views") | |
scenario.h2("Balance") | |
view_balance = Viewer(sp.TNat) | |
scenario += view_balance | |
c1.getBalance((alice.address, view_balance.typed.target)) | |
scenario.verify_equal(view_balance.data.last, sp.some(8)) | |
scenario.h2("Administrator") | |
view_administrator = Viewer(sp.TAddress) | |
scenario += view_administrator | |
c1.getAdministrator((sp.unit, view_administrator.typed.target)) | |
scenario.verify_equal(view_administrator.data.last, sp.some(admin.address)) | |
scenario.h2("Total Supply") | |
view_totalSupply = Viewer(sp.TNat) | |
scenario += view_totalSupply | |
c1.getTotalSupply((sp.unit, view_totalSupply.typed.target)) | |
scenario.verify_equal(view_totalSupply.data.last, sp.some(17)) | |
scenario.h2("Allowance") | |
view_allowance = Viewer(sp.TNat) | |
scenario += view_allowance | |
c1.getAllowance( | |
(sp.record(owner=alice.address, spender=bob.address), view_allowance.typed.target) | |
) | |
scenario.verify_equal(view_allowance.data.last, sp.some(1)) | |
scenario.h2("Balance at Level 10") | |
view_balanceAt = Viewer(sp.TNat) | |
scenario += view_balanceAt | |
c1.getBalanceAt((sp.record(address=alice.address, level=10), view_balanceAt.typed.target)) | |
scenario.verify_equal(view_balanceAt.data.last, sp.some(9)) | |
sp.add_compilation_target( | |
"FA1_2", | |
FA12( | |
admin=sp.address("tz1M9CMEtsXm3QxA7FmMU2Qh7xzsuGXVbcDr"), | |
config=FA12_config( | |
support_upgradable_metadata=True, use_token_metadata_offchain_view=True | |
), | |
token_metadata={ | |
"decimals": "18", # Mandatory by the spec | |
"name": "My Great Token", # Recommended | |
"symbol": "MGT", # Recommended | |
# Extra fields | |
"icon": "https://smartpy.io/static/img/logo-only.svg", | |
}, | |
contract_metadata={ | |
"": "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd", | |
}, | |
), | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment