11/// A Quint specification following Callum-A's cosmwasm-zero-to-hero tutorial:
2- /// https://github.com/Callum-A/cosmwasm-zero-to-hero
2+ /// https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/03e33968f588b192c04fadb984ae853b7dc5e64e
33///
44/// We refer to the relevant tutorial chapters in comments below.
55
@@ -14,7 +14,7 @@ module vote {
1414 type UUID = str
1515 type Uint64 = int // FIXME: account for under/overflows
1616
17- val None: Addr = ""
17+ pure val None: Addr = ""
1818
1919 type Config = {
2020 admin: Addr,
@@ -76,12 +76,18 @@ module vote {
7676 type QueryPoll = { poll_id: str }
7777 type QueryVote = { poll_id: str, address: Addr }
7878
79+ // 13 - Query
80+
81+ type AllPollsResponse = { polls: List[Poll] }
82+ type PollResponse = { poll: List[Poll] }
83+ type VoteResponse = { vote: List[Ballot] }
84+
7985 /*** Functional layer ***/
8086
8187 // 06 - Instantiate
8288 // https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/main/06%20-%20Instantiate
8389
84- def instantiate(cs: ContractState, info: MessageInfo,
90+ pure def instantiate(cs: ContractState, info: MessageInfo,
8591 msg: InstantiateMsg): ContractState = {
8692 val admin = if (msg.admin != None) msg.admin else info.sender
8793 // TODO: deps.api.addr_validate(&admin)?
@@ -91,7 +97,7 @@ module vote {
9197 // 09 - Execute 1
9298 // https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/main/09%20-%20Execute%201
9399
94- def execute_create_poll(cs: ContractState, info: MessageInfo, poll_id: UUID,
100+ pure def execute_create_poll(cs: ContractState, info: MessageInfo, poll_id: UUID,
95101 question: str, options: List[str]): Result = {
96102 if (options.length() > 10) {
97103 { cs: cs, error: "TooManyOptions" }
@@ -112,7 +118,7 @@ module vote {
112118 // 10 - Execute 2
113119 // https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/main/10%20-%20Execute%202
114120
115- def execute_vote(cs: ContractState, info: MessageInfo, poll_id: UUID,
121+ pure def execute_vote(cs: ContractState, info: MessageInfo, poll_id: UUID,
116122 vote: str): Result = {
117123 // CosmWasm: POLLS.may_load(deps.storage, poll_id.clone())?;
118124 if (not(cs.polls.keys().contains(poll_id))) {
@@ -154,23 +160,27 @@ module vote {
154160 // 13 - Query
155161 // https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/main/13%20-%20Query
156162
157- def query_all_polls(cs: ContractState): List[Poll] = {
158- cs.polls.keys().fold([], (res, poll_id) => res.append(cs.polls.get(poll_id)))
163+ pure def query_all_polls(cs: ContractState): AllPollsResponse = {
164+ val polls = cs.polls.keys().fold([], (res, poll_id) =>
165+ res.append(cs.polls.get(poll_id)))
166+ { polls: polls }
159167 }
160168
161- def query_poll(cs: ContractState, poll_id: UUID): List[Poll] = {
162- cs.polls.keys().fold([], (res, pid) => {
169+ pure def query_poll(cs: ContractState, poll_id: UUID): PollResponse = {
170+ val poll = cs.polls.keys().fold([], (res, pid) => {
163171 if (pid == poll_id) res.append(cs.polls.get(poll_id)) else res
164172 })
173+ { poll: poll }
165174 }
166175
167- def query_vote(cs: ContractState, address: Addr, poll_id: UUID): List[Ballot] = {
176+ pure def query_vote(cs: ContractState, address: Addr, poll_id: UUID): VoteResponse = {
168177 val _key = (address, poll_id)
169- if (cs.ballots.keys().contains(_key)) {
178+ val vote = if (cs.ballots.keys().contains(_key)) {
170179 [ cs.ballots.get(_key) ]
171180 } else {
172181 []
173182 }
183+ { vote: vote }
174184 }
175185}
176186
@@ -179,11 +189,11 @@ module vote {
179189module tests {
180190 import vote.*
181191
182- // 17 - Instantiate Test
192+ // 07 - Instantiate Test
183193 // https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/main/07%20-%20Instantiate%20Test
184194
185- val ADDR1: Addr = "addr1"
186- val ADDR2: Addr = "addr2"
195+ pure val ADDR1: Addr = "addr1"
196+ pure val ADDR2: Addr = "addr2"
187197
188198 // To run:
189199 // quint test --main=tests --match=test_ vote.qnt
@@ -201,6 +211,7 @@ module tests {
201211 run test_execute_create_poll_valid = {
202212 val _cs_init = { config: { admin: "__UNINIT__" }, polls: Map(), ballots: Map() }
203213 val info = { sender: ADDR1 }
214+ // Instantiate the contract
204215 val msg_inst = { admin: None }
205216 val _res_inst = instantiate(_cs_init, info, msg_inst)
206217
@@ -219,6 +230,7 @@ module tests {
219230 run test_execute_create_poll_invalid = {
220231 val _cs_init = { config: { admin: "__UNINIT__" }, polls: Map(), ballots: Map() }
221232 val info = { sender: ADDR1 }
233+ // Instantiate the contract
222234 val msg_inst = { admin: None }
223235 val _res_inst = instantiate(_cs_init, info, msg_inst)
224236
@@ -235,6 +247,7 @@ module tests {
235247 run test_execute_vote_valid = {
236248 val _cs_init = { config: { admin: "__UNINIT__" }, polls: Map(), ballots: Map() }
237249 val info = { sender: ADDR1 }
250+ // Instantiate the contract
238251 val msg_inst = { admin: None }
239252 val _res_inst = instantiate(_cs_init, info, msg_inst)
240253
@@ -264,6 +277,7 @@ module tests {
264277 run test_execute_vote_invalid = {
265278 val _cs_init = { config: { admin: "__UNINIT__" }, polls: Map(), ballots: Map() }
266279 val info = { sender: ADDR1 }
280+ // Instantiate the contract
267281 val msg_inst = { admin: None }
268282 val _res_inst = instantiate(_cs_init, info, msg_inst)
269283
@@ -282,4 +296,221 @@ module tests {
282296 val _res = execute_vote(_res_create.cs, info, msg.poll_id, msg.vote)
283297 assert(_res.error != "")
284298 }
299+
300+ // 14 - Query Tests
301+ // https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/main/14%20-%20Query%20Tests
302+
303+ run test_query_all_polls = {
304+ val _cs_init = { config: { admin: "__UNINIT__" }, polls: Map(), ballots: Map() }
305+ val info = { sender: ADDR1 }
306+ // Instantiate the contract
307+ val msg_inst = { admin: None }
308+ val _res_inst = instantiate(_cs_init, info, msg_inst)
309+
310+ // Create a poll
311+ val msg_create = {
312+ poll_id: "some_id_1",
313+ question: "What's your favourite Cosmos coin?",
314+ options: [ "Cosmos Hub", "Juno", "Osmosis" ]
315+ }
316+ val _res_create = execute_create_poll(_res_inst, info, msg_create.poll_id,
317+ msg_create.question, msg_create.options)
318+
319+ // Create a second poll
320+ val msg_create2 = {
321+ poll_id: "some_id_2",
322+ question: "What's your colour?",
323+ options: [ "Red", "Green", "Blue" ]
324+ }
325+ val _res_create2 = execute_create_poll(_res_create.cs, info, msg_create2.poll_id,
326+ msg_create2.question, msg_create2.options)
327+
328+ // Query
329+ val res = query_all_polls(_res_create2.cs)
330+ assert(res.polls.length() == 2)
331+ }
332+
333+ run test_query_poll = {
334+ val _cs_init = { config: { admin: "__UNINIT__" }, polls: Map(), ballots: Map() }
335+ val info = { sender: ADDR1 }
336+ // Instantiate the contract
337+ val msg_inst = { admin: None }
338+ val _res_inst = instantiate(_cs_init, info, msg_inst)
339+
340+ // Create a poll
341+ val msg_create = {
342+ poll_id: "some_id_1",
343+ question: "What's your favourite Cosmos coin?",
344+ options: [ "Cosmos Hub", "Juno", "Osmosis" ]
345+ }
346+ val _res_create = execute_create_poll(_res_inst, info, msg_create.poll_id,
347+ msg_create.question, msg_create.options)
348+
349+ // Query for the poll that exists
350+ val msg1 = { poll_id: "some_id_1" }
351+ val res1 = query_poll(_res_create.cs, msg1.poll_id)
352+
353+ // Query for the poll that does not exists
354+ val msg2 = { poll_id: "some_id_not_exist" }
355+ val res2 = query_poll(_res_create.cs, msg2.poll_id)
356+
357+ all {
358+ // Expect a poll
359+ assert(res1.poll.length() == 1),
360+ // Expect none
361+ assert(res2.poll.length() == 0)
362+ }
363+ }
364+
365+ run test_query_vote = {
366+ val _cs_init = { config: { admin: "__UNINIT__" }, polls: Map(), ballots: Map() }
367+ val info = { sender: ADDR1 }
368+ val msg_inst = { admin: None }
369+ val _res_inst = instantiate(_cs_init, info, msg_inst)
370+
371+ // Create a poll
372+ val msg_create = {
373+ poll_id: "some_id_1",
374+ question: "What's your favourite Cosmos coin?",
375+ options: [ "Cosmos Hub", "Juno", "Osmosis" ]
376+ }
377+ val _res_create = execute_create_poll(_res_inst, info, msg_create.poll_id,
378+ msg_create.question, msg_create.options)
379+
380+ // Create a vote, first time voting
381+ val msg_vote = { poll_id: "some_id_1", vote: "Juno" }
382+ val _res_vote = execute_vote(_res_create.cs, info, msg_vote.poll_id, msg_vote.vote)
383+
384+ // Query for a vote that exists
385+ val msg1 = { poll_id: "some_id_1", address: ADDR1 }
386+ val res1 = query_vote(_res_vote.cs, msg1.address, msg1.poll_id)
387+
388+ // Query for a vote that does not exists
389+ val msg2 = { poll_id: "some_id_2", address: ADDR2 }
390+ val res2 = query_vote(_res_vote.cs, msg2.address, msg2.poll_id)
391+
392+ all {
393+ // Expect the vote to exist
394+ assert(res1.vote.length() == 1),
395+ // Expect the vote to not exist
396+ assert(res2.vote.length() == 0)
397+ }
398+ }
399+ }
400+
401+ /*** State machine ***/
402+
403+ module state {
404+ import vote.*
405+
406+ var state: ContractState
407+
408+ pure val ADMIN: Addr = "admin"
409+ pure val USER_ADDR = Set("alice", "bob", "charlie", "eve")
410+ pure val ALL_ADDR = USER_ADDR.union(Set(ADMIN))
411+ pure val OPTIONS = Set("o1", "o2", "o3")
412+
413+ action init = {
414+ val pre_init_state = { config: { admin: None }, polls: Map(), ballots: Map() }
415+ val info = { sender: ADMIN }
416+ val msg_inst = { admin: ADMIN }
417+ state' = instantiate(pre_init_state, info, msg_inst)
418+ }
419+
420+ action exec_create_poll(sender: Addr, poll_id: str, question: str, options: List[str]): bool = {
421+ val info = { sender: sender }
422+ val res = execute_create_poll(state, info, poll_id, question, options)
423+ all {
424+ res.error == "",
425+ state' = res.cs
426+ }
427+ }
428+
429+ action exec_vote(sender: Addr, poll_id: UUID, vote: str): bool = {
430+ val info = { sender: sender }
431+ val res = execute_vote(state, info, poll_id, vote)
432+ all {
433+ res.error == "",
434+ state' = res.cs
435+ }
436+ }
437+
438+ action step = {
439+ nondet sender = ALL_ADDR.oneOf()
440+ nondet poll_id = Set("p1", "p2", "p3", "p4").oneOf()
441+ any {
442+ nondet question = Set("q1", "q2", "q3", "q4").oneOf()
443+ nondet options = OPTIONS.powerset().oneOf()
444+ val optionsL = options.fold([], (s, o) => s.append(o))
445+ exec_create_poll(sender, poll_id, question, optionsL),
446+
447+ nondet option = OPTIONS.oneOf()
448+ exec_vote(sender, poll_id, option)
449+ }
450+ }
451+
452+ // Invariant: An admin is always set in the contract's config.
453+ //
454+ // To check:
455+ // quint run --main=state --invariant=alwaysAdmin vote.qnt
456+ val alwaysAdmin = state.config.admin != None
457+
458+ // Return true iff `pred(e)` is true for all elements `e` of `list`.
459+ pure def listForall(list: List[a], pred: a => bool): bool = {
460+ list.foldl(true, (b, elem) => { if (pred(elem)) b else false })
461+ }
462+
463+ // FIXME(#1042): work around binding issue
464+ pure def listForall2(list: List[a], pred: a => bool): bool = {
465+ list.foldl(true, (b, elem) => { if (pred(elem)) b else false })
466+ }
467+
468+ // Invariant: The ballots in `ballots` sum up to the aggregated votes in
469+ // `polls[id].options`.
470+ //
471+ // To check:
472+ // quint run --main=state --invariant=invBallotsMatchPolls vote.qnt
473+ //
474+ // Note: This invariant DOES NOT hold in the current implementation / spec.
475+ // Can you guess why?
476+ // [`execute_vote` allows to replace a poll in `polls` without nuking
477+ // already submitted ballots in `ballots`.]
478+ val invBallotsMatchPolls = {
479+ state.polls.keys().forall(poll_id =>
480+ // sum the votes in `ballots` into a map `question -> num_votes`:
481+ val affected_ballot_keys = state.ballots.keys().filter(k => k._2 == poll_id)
482+ val summed_ballots = affected_ballot_keys.fold(Map(), (m, k) =>
483+ val option = state.ballots.get(k).option
484+ if(m.keys().contains(option)) {
485+ m.setBy(option, old => old + 1)
486+ } else {
487+ m.put(option, 1)
488+ }
489+ )
490+ // assert that aggregated sum in `polls[poll_id]` equals the sum from above
491+ val poll = state.polls.get(poll_id)
492+ poll.options.listForall(option =>
493+ val optionKey = option._1
494+ val optionSum = option._2
495+ // `ballots` only contains entries if there are > 0 votes.
496+ optionSum > 0 implies and {
497+ summed_ballots.keys().contains(optionKey),
498+ summed_ballots.get(optionKey) == optionSum
499+ }
500+ )
501+ )
502+ }
503+
504+ // Invariant: The aggregated votes for all options of all polls are greater 0.
505+ //
506+ // To check:
507+ // quint run --main=state --invariant=invAllBallotsPositive vote.qnt
508+ //
509+ // Note: This invariant DOES NOT hold in the current implementation / spec.
510+ // Can you guess why?
511+ // [`execute_vote` allows to replace a poll in `polls` without nuking
512+ // already submitted ballots in `ballots`, but `execute_vote` adjusts
513+ // the sum differently based on whether such a ballot is present.]
514+ val invAllBallotsPositive = query_all_polls(state).polls.listForall(poll =>
515+ poll.options.listForall(opt => opt._2 >= 0))
285516}
0 commit comments