Voting that cannot be voted twice
Proposals, a deadline, one vote per address, and a result nobody can quietly change afterwards.
On-chain voting is attractive for one reason: the tally is computed from records anyone can re-check, so the count is not something you have to trust anyone about.
That only holds if three things are true:
1. One vote per voter — enforced by storage, not by good manners 2. A deadline that comes from consensus, not from a clock 3. A tally nobody can edit after the fact
Use abi.block_height() for deadlines, not a timestamp where you can help it. Height is a count every node agrees on exactly. Block timestamps are also agreed, but they are permitted to drift within a tolerance, and a miner has some influence over them. If a few minutes of slack would change who wins, height is the safer clock.
Animica blocks average about two minutes (121 seconds measured), so "roughly a day" is about 715 blocks. Write that arithmetic in a comment — the next person to read deadline = start + 715 will not otherwise know whether you meant a day or a week.
Everything on-chain is public, including your vote. A ballot stored in contract storage is readable by anyone forever. If secrecy matters, the pattern is commit-reveal: first submit hash(choice + secret), then reveal the choice after voting closes. That is a substantially harder contract, and worth knowing exists before you promise anyone a private vote.
Your turn
Implement vote(yes) and result(). A voter may vote once — a second attempt reverts b"already_voted" and must not change the tally. Voting after the deadline reverts b"closed". result() returns 1 if yes beats no, 2 if no beats yes, 0 for a tie.
Hints
One way to do it
from stdlib import abi, events, storage
def _uget(key: bytes) -> int:
raw = storage.get(key, b"")
return 0 if raw == b"" else int.from_bytes(raw, "big")
def _uset(key: bytes, value: int) -> None:
v = int(value)
abi.require(v >= 0, b"negative")
if v == 0:
storage.delete(key)
return
storage.set(key, v.to_bytes(max(1, (v.bit_length() + 7) // 8), "big"))
def _flag(key: bytes) -> bool:
return storage.get(key, b"") == b"\x01"
def _set_flag(key: bytes) -> None:
storage.set(key, b"\x01")
K_YES = b"vote:yes"
K_NO = b"vote:no"
K_DEADLINE = b"vote:deadline"
def _k_voted(addr: bytes) -> bytes:
return b"vote:by:" + addr
def init(blocks_open: int) -> None:
_uset(K_DEADLINE, abi.block_height() + int(blocks_open))
def deadline() -> int:
return _uget(K_DEADLINE)
def tally() -> int:
return _uget(K_YES) * 1000 + _uget(K_NO)
def has_voted(addr: bytes) -> bool:
return _flag(_k_voted(bytes(addr)))
def vote(yes: bool) -> int:
# Height is a count every node agrees on exactly.
abi.require(abi.block_height() <= _uget(K_DEADLINE), b"closed")
who = abi.caller()
abi.require(not _flag(_k_voted(who)), b"already_voted")
_set_flag(_k_voted(who))
if yes:
_uset(K_YES, _uget(K_YES) + 1)
else:
_uset(K_NO, _uget(K_NO) + 1)
events.emit(b"Voted", {"who": who, "yes": bool(yes)})
return _uget(K_YES) * 1000 + _uget(K_NO)
def result() -> int:
yes = _uget(K_YES)
no = _uget(K_NO)
if yes > no:
return 1
if no > yes:
return 2
return 0
Claim your 10 ANM
Finish this lesson and claim 10 ANM, once per address. Paid from the Animica treasury in batches — allow a few minutes.
Stuck? Ask
Answered by Animica's own free inference network. It is donated GPU capacity, so give it 20-30 seconds.