DAOs functioning on a token voting mechanism (using either FA1.2 or FA2) mostly require the locking up of tokens for the entirety of the voting period essentially to prevent double-voting or any form of Sybil attack.
Certain DAOs on Ethereum utilise balance snapshots by recording the blocks (like checkpoints) where the balance has changed. This helps in bypassing the need to lock the tokens and voting can be done on the basis of the balance at a certain checkpoint.
I believe this hasn't been explored on Tezos yet. We are thinking of utilising this feature for one of our projects, so I have tweaked the SmartPy FA1.2 template & added in this level based snapshot feature.
I request the community for suggestions on this implementation and the scope of improvements.
Storage:
# Type of a checkpoint
# level - The current block level
# value - The balance at that level
checkpoint = sp.TRecord(level=sp.TNat, value=sp.TNat)
# An additional BIGMAP in the storage to hold the snapshots
balanceSnapshots=sp.big_map(tkey=sp.TAddress, tvalue=sp.TMap(sp.TNat, checkpoint))
Utility to add snapshots during transfer, mint & burn:
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)
A view to retrieve the balance closest to a certain level:
This is done using binary search. Since Tezos lists don't have random access, a MAP is used with appropriate indexing.
@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))
Link to the entire contract: https://smartpy.io/ide?cid=QmbFs8Tq97BWSz35Bvtt1EXHH8Bm55LNXscamoFKtCRhkG&k=47bebcb99eedfc4758b1
Gist: https://gist.github.com/AnshuJalan/2c6a94a18db28194e5550d882e62615a
O(size(map))
, notO(1)
, which may lead to arbitrary amounts of gas being consumed. We've updated the contract. If anyone else finds my answer, please make sure you pull in the latest commits after PR #13 and #14!