1

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

1
  • 1
    Just a note that since this question was asked we identified a potential resource exhaustion issue in the kDAO token (specifically an issue like S3 found here drive.google.com/file/d/1mXZj6MjzB_jzi4fmVOqo_zv2-ltU4Nrd/view). Specifically, reads and writes to a map in Michelson are O(size(map)), not O(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! Jul 7 at 22:48
1

We've implemented a version of this for Murmuration DAO, and I think it's a pretty valid and cool idea.

Our code is here: https://github.com/Hover-Labs/murmuration/blob/main/smart_contracts/token.py

You can see an example in dao.py that shows how you can read data out of the token snapshots.

2
  • Thanks, Keefer! Just went through the code. I got a question- is there a performance (or gas-related) advantage in keeping a separate storage item to track the number of checkpoints instead of using sp.len(..) to index the map storing the checkpoint? Jun 28 at 5:36
  • 1
    No reason - other than that I didn't realize you could use sp.len on map types (you can't on big_maps). This is also the closest to how the compound contract worked, and that was what I was most interested in mirroring. Jun 28 at 18:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.