Files
chia-blockchain/chia/_tests/wallet/dao_wallet/test_dao_clvm.py
T
6c90a76b56 Tighten up ruff ignore list (#18837)
* Tighten up ruff ignore list

* Okay that fix was indeed unsafe

* enable type-name-incorrect-variance

* enable literal-membership

* enable non-augmented-assignment

* enable useless-return

* enable global-variable-not-assigned

* -

* fixup

* Clean up roff.toml from annotations

* use ignore instead of explicit re-export

* use more descriptive names

---------

Co-authored-by: Kyle Altendorf <sda@fstab.net>
2024-11-12 12:19:31 -07:00

1331 lines
49 KiB
Python

from __future__ import annotations
from typing import Any, Optional
import pytest
from chia_rs import AugSchemeMPL
from clvm.casts import int_to_bytes
from chia._tests.util.spend_sim import SimClient, SpendSim, sim_and_client
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import INFINITE_COST, Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import make_spend
from chia.types.condition_opcodes import ConditionOpcode
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.util.condition_tools import conditions_dict_for_solution
from chia.util.errors import Err
from chia.util.hash import std_hash
from chia.util.ints import uint32, uint64
from chia.wallet.cat_wallet.cat_utils import CAT_MOD
from chia.wallet.dao_wallet.dao_info import DAORules
from chia.wallet.dao_wallet.dao_utils import curry_singleton, get_p2_singleton_puzhash, get_treasury_puzzle
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.singleton import create_singleton_puzzle_hash
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
CAT_MOD_HASH: bytes32 = CAT_MOD.get_tree_hash()
SINGLETON_MOD: Program = load_clvm("singleton_top_layer_v1_1.clsp")
SINGLETON_MOD_HASH: bytes32 = SINGLETON_MOD.get_tree_hash()
SINGLETON_LAUNCHER: Program = load_clvm("singleton_launcher.clsp")
SINGLETON_LAUNCHER_HASH: bytes32 = SINGLETON_LAUNCHER.get_tree_hash()
DAO_LOCKUP_MOD: Program = load_clvm("dao_lockup.clsp")
DAO_LOCKUP_MOD_HASH: bytes32 = DAO_LOCKUP_MOD.get_tree_hash()
DAO_PROPOSAL_TIMER_MOD: Program = load_clvm("dao_proposal_timer.clsp")
DAO_PROPOSAL_TIMER_MOD_HASH: bytes32 = DAO_PROPOSAL_TIMER_MOD.get_tree_hash()
DAO_PROPOSAL_MOD: Program = load_clvm("dao_proposal.clsp")
DAO_PROPOSAL_MOD_HASH: bytes32 = DAO_PROPOSAL_MOD.get_tree_hash()
DAO_PROPOSAL_VALIDATOR_MOD: Program = load_clvm("dao_proposal_validator.clsp")
DAO_PROPOSAL_VALIDATOR_MOD_HASH: bytes32 = DAO_PROPOSAL_VALIDATOR_MOD.get_tree_hash()
DAO_TREASURY_MOD: Program = load_clvm("dao_treasury.clsp")
DAO_TREASURY_MOD_HASH: bytes32 = DAO_TREASURY_MOD.get_tree_hash()
SPEND_P2_SINGLETON_MOD: Program = load_clvm("dao_spend_p2_singleton_v2.clsp")
SPEND_P2_SINGLETON_MOD_HASH: bytes32 = SPEND_P2_SINGLETON_MOD.get_tree_hash()
DAO_FINISHED_STATE: Program = load_clvm("dao_finished_state.clsp")
DAO_FINISHED_STATE_HASH: bytes32 = DAO_FINISHED_STATE.get_tree_hash()
DAO_CAT_TAIL: Program = load_clvm(
"genesis_by_coin_id_or_singleton.clsp", package_or_requirement="chia.wallet.cat_wallet.puzzles"
)
DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash()
P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp")
P2_SINGLETON_MOD_HASH: bytes32 = P2_SINGLETON_MOD.get_tree_hash()
P2_SINGLETON_AGGREGATOR_MOD: Program = load_clvm("p2_singleton_aggregator.clsp")
P2_SINGLETON_AGGREGATOR_MOD_HASH: bytes32 = P2_SINGLETON_AGGREGATOR_MOD.get_tree_hash()
DAO_UPDATE_MOD: Program = load_clvm("dao_update_proposal.clsp")
DAO_UPDATE_MOD_HASH: bytes32 = DAO_UPDATE_MOD.get_tree_hash()
def test_finished_state() -> None:
"""
Once a proposal has closed, it becomes a 'beacon' singleton which announces
its proposal ID. This is referred to as the finished state and is used to
confirm that a proposal has closed in order to release voting CATs from
the lockup puzzle.
"""
proposal_id = Program.to("proposal_id").get_tree_hash()
singleton_struct: Program = Program.to(
(SINGLETON_MOD.get_tree_hash(), (proposal_id, SINGLETON_LAUNCHER.get_tree_hash()))
)
finished_inner_puz = DAO_FINISHED_STATE.curry(singleton_struct, DAO_FINISHED_STATE_HASH)
finished_full_puz = SINGLETON_MOD.curry(singleton_struct, finished_inner_puz)
inner_sol = Program.to([1])
conds = finished_inner_puz.run(inner_sol).as_python()
assert conds[0][1] == finished_full_puz.get_tree_hash()
assert conds[2][1] == finished_inner_puz.get_tree_hash()
lineage = Program.to([proposal_id, finished_inner_puz.get_tree_hash(), 1])
full_sol = Program.to([lineage, 1, inner_sol])
conds = conditions_dict_for_solution(finished_full_puz, full_sol, INFINITE_COST)
assert conds[ConditionOpcode.ASSERT_MY_PUZZLEHASH][0].vars[0] == finished_full_puz.get_tree_hash()
assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == finished_full_puz.get_tree_hash()
def test_proposal() -> None:
"""
This test covers the three paths for closing a proposal:
- Close a passed proposal
- Close a failed proposal
- Self-destruct a broken proposal
"""
proposal_pass_percentage: uint64 = uint64(5100)
CAT_TAIL_HASH = Program.to("tail").get_tree_hash()
treasury_id = Program.to("treasury").get_tree_hash()
singleton_id = Program.to("singleton_id").get_tree_hash()
singleton_struct: Program = Program.to(
(SINGLETON_MOD.get_tree_hash(), (singleton_id, SINGLETON_LAUNCHER.get_tree_hash()))
)
self_destruct_time = 1000 # number of blocks
oracle_spend_delay = 10
active_votes_list = [0xFADEDDAB] # are the ids of previously voted on proposals?
acs: Program = Program.to(1)
acs_ph: bytes32 = acs.get_tree_hash()
dao_lockup_self = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
)
proposal_curry_one = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
dao_lockup_self.get_tree_hash(),
CAT_TAIL_HASH,
treasury_id,
)
# make a lockup puz for the dao cat
lockup_puz = dao_lockup_self.curry(
dao_lockup_self.get_tree_hash(),
active_votes_list,
acs, # innerpuz
)
dao_cat_puz: Program = CAT_MOD.curry(CAT_MOD_HASH, CAT_TAIL_HASH, lockup_puz)
dao_cat_puzhash: bytes32 = dao_cat_puz.get_tree_hash()
# Test Voting
current_yes_votes = 20
current_total_votes = 100
full_proposal: Program = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
singleton_id,
acs_ph,
current_yes_votes,
current_total_votes,
)
vote_amount = 10
vote_type = 1 # yes vote
vote_coin_id = Program.to("vote_coin").get_tree_hash()
solution: Program = Program.to(
[
[vote_amount], # vote amounts
vote_type, # vote type (yes)
[vote_coin_id], # vote coin ids
[active_votes_list], # previous votes
[acs_ph], # lockup inner puz hash
0, # inner puz reveal
0, # soft close len
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
# Run the proposal and check its conditions
conditions = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
# Puzzle Announcement of vote_coin_ids
assert bytes32(conditions[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT][0].vars[0]) == vote_coin_id
# Assert puzzle announcement from dao_cat of proposal_id and all vote details
apa_msg = Program.to([singleton_id, vote_amount, vote_type, vote_coin_id]).get_tree_hash()
assert conditions[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == std_hash(dao_cat_puzhash + apa_msg)
# Check that the proposal recreates itself with updated vote amounts
next_proposal: Program = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
singleton_id,
acs_ph,
current_yes_votes + vote_amount,
current_total_votes + vote_amount,
)
assert bytes32(conditions[ConditionOpcode.CREATE_COIN][0].vars[0]) == next_proposal.get_tree_hash()
assert conditions[ConditionOpcode.CREATE_COIN][0].vars[1] == int_to_bytes(1)
# Try to vote using multiple coin ids
vote_coin_id_1 = Program.to("vote_coin_1").get_tree_hash()
vote_coin_id_2 = Program.to("vote_coin_2").get_tree_hash()
repeat_solution_1: Program = Program.to(
[
[vote_amount, 20], # vote amounts
vote_type, # vote type (yes)
[vote_coin_id_1, vote_coin_id_2], # vote coin ids
[active_votes_list, 0], # previous votes
[acs_ph, acs_ph], # lockup inner puz hash
0, # inner puz reveal
0, # soft close len
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
conds_repeated = conditions_dict_for_solution(full_proposal, repeat_solution_1, INFINITE_COST)
assert len(conds_repeated) == 5
# Try to vote using repeated coin ids
repeat_solution_2: Program = Program.to(
[
[vote_amount, vote_amount, 20], # vote amounts
vote_type, # vote type (yes)
[vote_coin_id_1, vote_coin_id_1, vote_coin_id_2], # vote coin ids
[active_votes_list], # previous votes
[acs_ph], # lockup inner puz hash
0, # inner puz reveal
0, # soft close len
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
with pytest.raises(ValueError) as e_info:
conditions_dict_for_solution(full_proposal, repeat_solution_2, INFINITE_COST)
assert "clvm raise" in e_info.value.args[0]
# Test Launch
current_yes_votes = 0
current_total_votes = 0
launch_proposal: Program = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
singleton_id,
acs_ph,
current_yes_votes,
current_total_votes,
)
vote_amount = 10
vote_type = 1 # yes vote
vote_coin_id = Program.to("vote_coin").get_tree_hash()
solution = Program.to(
[
[vote_amount], # vote amounts
vote_type, # vote type (yes)
[vote_coin_id], # vote coin ids
# TODO: Check whether previous votes should be 0 in the first spend since
# proposal looks at (f previous_votes) during loop_over_vote_coins
[0], # previous votes
[acs_ph], # lockup inner puz hash
acs, # inner puz reveal
0, # soft close len
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
# Run the proposal and check its conditions
conditions = conditions_dict_for_solution(launch_proposal, solution, INFINITE_COST)
# check that the timer is created
timer_puz = DAO_PROPOSAL_TIMER_MOD.curry(
proposal_curry_one.get_tree_hash(),
singleton_struct,
)
timer_puzhash = timer_puz.get_tree_hash()
assert conditions[ConditionOpcode.CREATE_COIN][1].vars[0] == timer_puzhash
# Test exits
# Test attempt to close a passing proposal
current_yes_votes = 200
current_total_votes = 350
full_proposal = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
singleton_id,
acs_ph,
current_yes_votes,
current_total_votes,
)
attendance_required = 200
proposal_timelock = 20
soft_close_length = 5
solution = Program.to(
[
Program.to("validator_hash").get_tree_hash(),
0,
# Program.to("receiver_hash").get_tree_hash(), # not needed anymore?
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
conds = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
# make a matching treasury puzzle for the APA
treasury_inner: Program = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
Program.to("validator_hash"),
proposal_timelock,
soft_close_length,
attendance_required,
proposal_pass_percentage,
self_destruct_time,
oracle_spend_delay,
)
treasury: Program = SINGLETON_MOD.curry(
Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH))),
treasury_inner,
)
treasury_puzhash = treasury.get_tree_hash()
apa_msg_bytes = singleton_id
timer_apa = std_hash(timer_puzhash + singleton_id)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == timer_apa
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][1].vars[0] == std_hash(treasury_puzhash + apa_msg_bytes)
# close a failed proposal
full_proposal = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
singleton_id,
acs_ph,
20, # failing number of yes votes
current_total_votes,
)
solution = Program.to(
[
Program.to("validator_hash").get_tree_hash(),
0,
# Program.to("receiver_hash").get_tree_hash(), # not needed anymore?
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
conds = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
apa_msg_int = int_to_bytes(0)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][1].vars[0] == std_hash(treasury_puzhash + apa_msg_int)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == timer_apa
finished_puz = DAO_FINISHED_STATE.curry(singleton_struct, DAO_FINISHED_STATE_HASH)
assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == finished_puz.get_tree_hash()
# self destruct a proposal
attendance_required = 200
solution = Program.to(
[
Program.to("validator_hash").get_tree_hash(),
0,
# Program.to("receiver_hash").get_tree_hash(), # not needed anymore?
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
1,
1,
]
)
conds = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == std_hash(treasury_puzhash + apa_msg_int)
assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == finished_puz.get_tree_hash()
def test_proposal_timer() -> None:
"""
The timer puzzle is created at the same time as a proposal, and enforces a relative time condition on proposals
The closing time is passed in via the timer solution and confirmed via announcement from the proposal.
It creates/asserts announcements to pair it with the finishing spend of a proposal.
The timer puzzle only has one spend path so there is only one test case for this puzzle.
"""
CAT_TAIL_HASH = Program.to("tail").get_tree_hash()
treasury_id = Program.to("treasury").get_tree_hash()
singleton_id = Program.to("singleton_id").get_tree_hash()
singleton_struct: Program = Program.to(
(SINGLETON_MOD.get_tree_hash(), (singleton_id, SINGLETON_LAUNCHER.get_tree_hash()))
)
dao_lockup_self = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
)
proposal_curry_one = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
dao_lockup_self.get_tree_hash(),
CAT_TAIL_HASH,
treasury_id,
)
proposal_timer_full: Program = DAO_PROPOSAL_TIMER_MOD.curry(
proposal_curry_one.get_tree_hash(),
singleton_struct,
)
timelock = int_to_bytes(101)
parent_parent_id = Program.to("parent_parent").get_tree_hash()
parent_amount = 2000
solution: Program = Program.to(
[
140, # yes votes
180, # total votes
Program.to(1).get_tree_hash(), # proposal innerpuz
timelock,
parent_parent_id,
parent_amount,
]
)
# run the timer puzzle.
conds = conditions_dict_for_solution(proposal_timer_full, solution, INFINITE_COST)
assert len(conds) == 4
# Validate the output conditions
# Check the timelock is present
assert conds[ConditionOpcode.ASSERT_HEIGHT_RELATIVE][0].vars[0] == timelock
# Check the proposal id is announced by the timer puz
assert conds[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT][0].vars[0] == singleton_id
# Check the proposal puz announces the timelock
expected_proposal_puzhash: bytes32 = create_singleton_puzzle_hash(
proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(), singleton_id, Program.to(1).get_tree_hash(), 140, 180
).get_tree_hash(),
singleton_id,
)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == std_hash(
expected_proposal_puzhash + timelock
)
# Check the parent is a proposal
expected_parent_puzhash: bytes32 = create_singleton_puzzle_hash(
proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
singleton_id,
Program.to(1).get_tree_hash(),
0,
0,
).get_tree_hash(),
singleton_id,
)
parent_id = std_hash(parent_parent_id + expected_parent_puzhash + int_to_bytes(parent_amount))
assert conds[ConditionOpcode.ASSERT_MY_PARENT_ID][0].vars[0] == parent_id
def test_validator() -> None:
"""
The proposal validator is run by the treasury when a passing proposal is closed.
Its main purpose is to check that the proposal's vote amounts adehere to
the DAO rules contained in the treasury (which are passed in from the
treasury as Truth values). It creates a puzzle announcement of the
proposal ID, that the proposal itself asserts. It also spends the value
held in the proposal to the excess payout puzhash.
The test cases covered are:
- Executing a spend proposal in which the validator executes the spend of a
`spend_p2_singleton` coin. This is just a proposal that spends some the treasury
- Executing an update proposal that changes the DAO rules.
"""
# Setup the treasury
treasury_id = Program.to("treasury_id").get_tree_hash()
treasury_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
# Setup the proposal
proposal_id = Program.to("proposal_id").get_tree_hash()
proposal_struct: Program = Program.to((SINGLETON_MOD.get_tree_hash(), (proposal_id, SINGLETON_LAUNCHER_HASH)))
CAT_TAIL_HASH = Program.to("tail").get_tree_hash()
acs: Program = Program.to(1)
acs_ph: bytes32 = acs.get_tree_hash()
p2_singleton = P2_SINGLETON_MOD.curry(treasury_struct, P2_SINGLETON_AGGREGATOR_MOD)
p2_singleton_puzhash = p2_singleton.get_tree_hash()
parent_id = Program.to("parent").get_tree_hash()
locked_amount = 100000
spend_amount = 1100
conditions = [[51, 0xDABBAD00, 1000], [51, 0xCAFEF00D, 100]]
# Setup the validator
minimum_amt = 1
excess_puzhash = bytes32(b"1" * 32)
dao_lockup_self = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
)
proposal_curry_one = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
dao_lockup_self.get_tree_hash(),
CAT_TAIL_HASH,
treasury_id,
)
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
treasury_struct,
proposal_curry_one.get_tree_hash(),
minimum_amt,
excess_puzhash,
)
# Can now create the treasury inner puz
treasury_inner = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
10, # proposal len
5, # soft close
1000, # attendance
5100, # pass margin
20, # self_destruct len
3, # oracle delay
)
# Setup the spend_p2_singleton (proposal inner puz)
spend_p2_singleton = SPEND_P2_SINGLETON_MOD.curry(
treasury_struct,
CAT_MOD_HASH,
conditions,
[],
p2_singleton_puzhash, # tailhash conds
)
spend_p2_singleton_puzhash = spend_p2_singleton.get_tree_hash()
parent_amt_list = [[parent_id, locked_amount]]
cat_parent_amt_list: list[Optional[Any]] = []
spend_p2_singleton_solution = Program.to([parent_amt_list, cat_parent_amt_list, treasury_inner.get_tree_hash()])
output_conds = spend_p2_singleton.run(spend_p2_singleton_solution)
proposal: Program = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
proposal_id,
spend_p2_singleton_puzhash,
950,
1200,
)
full_proposal = SINGLETON_MOD.curry(proposal_struct, proposal)
proposal_amt = uint64(10)
proposal_coin_id = Coin(parent_id, full_proposal.get_tree_hash(), proposal_amt).name()
solution = Program.to(
[
1000,
5100,
[proposal_coin_id, spend_p2_singleton_puzhash, 0],
[proposal_id, 1200, 950, parent_id, proposal_amt],
output_conds,
]
)
conds: Program = proposal_validator.run(solution)
assert len(conds.as_python()) == 7 + len(conditions)
# test update
proposal = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
proposal_id,
acs_ph,
950,
1200,
)
full_proposal = SINGLETON_MOD.curry(proposal_struct, proposal)
proposal_coin_id = Coin(parent_id, full_proposal.get_tree_hash(), proposal_amt).name()
solution = Program.to(
[
1000,
5100,
[proposal_coin_id, acs_ph, 0],
[proposal_id, 1200, 950, parent_id, proposal_amt],
[[51, 0xCAFEF00D, spend_amount]],
]
)
conds = proposal_validator.run(solution)
assert len(conds.as_python()) == 3
def test_spend_p2_singleton() -> None:
# Curried values
singleton_id = Program.to("singleton_id").get_tree_hash()
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (singleton_id, SINGLETON_LAUNCHER_HASH)))
p2_singleton_puzhash = P2_SINGLETON_MOD.curry(singleton_struct, P2_SINGLETON_AGGREGATOR_MOD).get_tree_hash()
cat_tail_1 = Program.to("cat_tail_1").get_tree_hash()
cat_tail_2 = Program.to("cat_tail_2").get_tree_hash()
conditions = [[51, 0xCAFEF00D, 100], [51, 0xFEEDBEEF, 200]]
list_of_tailhash_conds = [
[cat_tail_1, [[51, 0x8BADF00D, 123], [51, 0xF00DF00D, 321]]],
[cat_tail_2, [[51, 0x8BADF00D, 123], [51, 0xF00DF00D, 321]]],
]
# Solution Values
xch_parent_amt_list = [[b"x" * 32, 10], [b"y" * 32, 100]]
cat_parent_amt_list = [
[cat_tail_1, [["b" * 32, 100], [b"c" * 32, 400]]],
[cat_tail_2, [[b"e" * 32, 100], [b"f" * 32, 400]]],
]
# cat_parent_amt_list = []
treasury_inner_puzhash = Program.to("treasury_inner").get_tree_hash()
# Puzzle
spend_p2_puz = SPEND_P2_SINGLETON_MOD.curry(
singleton_struct, CAT_MOD_HASH, conditions, list_of_tailhash_conds, p2_singleton_puzhash
)
# Solution
spend_p2_sol = Program.to([xch_parent_amt_list, cat_parent_amt_list, treasury_inner_puzhash])
conds = spend_p2_puz.run(spend_p2_sol)
assert conds
# spend only cats
conditions = []
list_of_tailhash_conds = [
[cat_tail_1, [[51, b"q" * 32, 123], [51, b"w" * 32, 321]]],
[cat_tail_2, [[51, b"e" * 32, 123], [51, b"r" * 32, 321]]],
]
xch_parent_amt_list = []
cat_parent_amt_list = [
[cat_tail_1, [[b"b" * 32, 100], [b"c" * 32, 400]]],
[cat_tail_2, [[b"e" * 32, 100], [b"f" * 32, 400]]],
]
treasury_inner_puzhash = Program.to("treasury_inner").get_tree_hash()
# Puzzle
spend_p2_puz = SPEND_P2_SINGLETON_MOD.curry(
singleton_struct, CAT_MOD_HASH, conditions, list_of_tailhash_conds, p2_singleton_puzhash
)
# Solution
spend_p2_sol = Program.to([xch_parent_amt_list, cat_parent_amt_list, treasury_inner_puzhash])
conds = spend_p2_puz.run(spend_p2_sol)
assert conds
# test deduplicate cat_parent_amount_list
cat_parent_amt_list = [
[cat_tail_1, [[b"b" * 32, 100], [b"c" * 32, 400], [b"b" * 32, 100], [b"b" * 32, 100]]],
[cat_tail_2, [[b"e" * 32, 100], [b"f" * 32, 400], [b"f" * 32, 400], [b"e" * 32, 100]]],
]
spend_p2_sol = Program.to([xch_parent_amt_list, cat_parent_amt_list, treasury_inner_puzhash])
dupe_conds = spend_p2_puz.run(spend_p2_sol)
assert dupe_conds == conds
def test_merge_p2_singleton() -> None:
"""
The treasury funds are held by `p2_singleton_via_delegated` puzzles.
Because a DAO can have a large number of these coins, it's possible to
merge them together without requiring a treasury spend.
There are two cases tested:
- For the merge coins that do not create the single output coin, and
- For the coin that does create the output.
"""
# Setup a singleton struct
singleton_inner: Program = Program.to(1)
singleton_id = Program.to("singleton_id").get_tree_hash()
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (singleton_id, SINGLETON_LAUNCHER_HASH)))
# Setup p2_singleton_via_delegated puz
my_id = Program.to("my_id").get_tree_hash()
p2_singleton = P2_SINGLETON_MOD.curry(singleton_struct, P2_SINGLETON_AGGREGATOR_MOD)
my_puzhash = p2_singleton.get_tree_hash()
# Spend to delegated puz
delegated_puz = Program.to(1)
delegated_sol = Program.to([[51, 0xCAFEF00D, 300]])
solution = Program.to([0, singleton_inner.get_tree_hash(), delegated_puz, delegated_sol, my_id])
conds = conditions_dict_for_solution(p2_singleton, solution, INFINITE_COST)
apa = std_hash(
SINGLETON_MOD.curry(singleton_struct, singleton_inner).get_tree_hash()
+ Program.to([my_id, delegated_puz.get_tree_hash()]).get_tree_hash()
)
assert len(conds) == 4
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == apa
assert conds[ConditionOpcode.CREATE_COIN][0].vars[1] == int_to_bytes(300)
# Merge Spend (not output creator)
output_parent_id = Program.to("output_parent").get_tree_hash()
output_coin_amount = uint64(100)
aggregator_sol = Program.to([my_id, my_puzhash, 300, 0, [output_parent_id, output_coin_amount]])
merge_p2_singleton_sol = Program.to([aggregator_sol, 0, 0, 0, 0])
conds = conditions_dict_for_solution(p2_singleton, merge_p2_singleton_sol, INFINITE_COST)
assert len(conds) == 5
assert conds[ConditionOpcode.ASSERT_MY_PUZZLEHASH][0].vars[0] == my_puzhash
assert conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0] == int_to_bytes(0)
# Merge Spend (output creator)
fake_parent_id = Program.to("fake_parent").get_tree_hash()
merged_coin_id = Coin(fake_parent_id, my_puzhash, uint64(200)).name()
merge_sol = Program.to([[my_id, my_puzhash, 100, [[fake_parent_id, my_puzhash, 200]], 0]])
conds = conditions_dict_for_solution(p2_singleton, merge_sol, INFINITE_COST)
assert len(conds) == 7
assert conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT][0].vars[0] == std_hash(merged_coin_id)
assert conds[ConditionOpcode.CREATE_COIN][0].vars[1] == int_to_bytes(300)
# Test merge and aggregate announcements match up
parent_ids = [
Program.to("fake_parent_1").get_tree_hash(),
Program.to("fake_parent_2").get_tree_hash(),
Program.to("fake_parent_3").get_tree_hash(),
]
amounts = [uint64(1000), uint64(2000), uint64(3000)]
parent_puzhash_amounts = []
merge_coin_ids: list[bytes32] = []
for pid, amt in zip(parent_ids, amounts):
parent_puzhash_amounts.append([pid, my_puzhash, amt])
merge_coin_ids.append(Coin(pid, my_puzhash, amt).name())
output_parent_amount = [output_parent_id, output_coin_amount]
output_coin_id = Coin(output_parent_id, my_puzhash, output_coin_amount).name()
agg_sol = Program.to([[output_coin_id, my_puzhash, output_coin_amount, parent_puzhash_amounts, 0]])
agg_conds = conditions_dict_for_solution(p2_singleton, agg_sol, INFINITE_COST)
# aggregator coin announces merge coin IDs
agg_ccas = [std_hash(output_coin_id + x.vars[0]) for x in agg_conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT]]
# aggregator coin asserts 0 from merge coins
agg_acas = [x.vars[0] for x in agg_conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT]]
for coin_id, ppa in zip(merge_coin_ids, parent_puzhash_amounts):
sol = Program.to([[coin_id, ppa[1], ppa[2], 0, output_parent_amount]])
merge_conds = conditions_dict_for_solution(p2_singleton, sol, INFINITE_COST)
# merge coin announces 0
cca = std_hash(coin_id + merge_conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0])
# merge coin asserts my_id from aggregator
aca = merge_conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT][0].vars[0]
assert aca in agg_ccas
assert cca in agg_acas
assert merge_conds[ConditionOpcode.ASSERT_MY_COIN_ID][0].vars[0] == coin_id
def test_treasury() -> None:
"""
The treasury has two spend paths:
- Proposal Path: when a proposal is being closed the treasury spend runs the
validator and the actual proposed code (if passed)
- Oracle Path: The treasury can make announcements about itself that are
used to close invalid proposals
"""
# Setup the treasury
treasury_id = Program.to("treasury_id").get_tree_hash()
treasury_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
CAT_TAIL_HASH = Program.to("tail").get_tree_hash()
proposal_id = Program.to("singleton_id").get_tree_hash()
proposal_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
p2_singleton = P2_SINGLETON_MOD.curry(treasury_struct, P2_SINGLETON_AGGREGATOR_MOD)
p2_singleton_puzhash = p2_singleton.get_tree_hash()
parent_id = Program.to("parent").get_tree_hash()
locked_amount = 100000
oracle_spend_delay = 10
self_destruct_time = 1000
proposal_length = 40
soft_close_length = 5
attendance = 1000
pass_margin = 5100
conditions = [[51, 0xDABBAD00, 1000], [51, 0xCAFEF00D, 100]]
# Setup the validator
minimum_amt = 1
excess_puzhash = bytes32(b"1" * 32)
dao_lockup_self = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
)
proposal_curry_one = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
dao_lockup_self.get_tree_hash(),
CAT_TAIL_HASH,
treasury_id,
)
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
treasury_struct,
proposal_curry_one.get_tree_hash(),
minimum_amt,
excess_puzhash,
)
# Can now create the treasury inner puz
treasury_inner = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
proposal_length,
soft_close_length,
attendance,
pass_margin,
self_destruct_time,
oracle_spend_delay,
)
# Setup the spend_p2_singleton (proposal inner puz)
spend_p2_singleton = SPEND_P2_SINGLETON_MOD.curry(
treasury_struct,
CAT_MOD_HASH,
conditions,
[],
p2_singleton_puzhash, # tailhash conds
)
spend_p2_singleton_puzhash = spend_p2_singleton.get_tree_hash()
parent_amt_list = [[parent_id, locked_amount]]
cat_parent_amt_list: list[Optional[Any]] = []
spend_p2_singleton_solution = Program.to([parent_amt_list, cat_parent_amt_list, treasury_inner.get_tree_hash()])
proposal: Program = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
proposal_id,
spend_p2_singleton_puzhash,
950,
1200,
)
full_proposal = SINGLETON_MOD.curry(proposal_struct, proposal)
# Oracle spend
solution: Program = Program.to([0, 0, 0, 0, 0, treasury_struct])
conds: Program = treasury_inner.run(solution)
assert len(conds.as_python()) == 3
# Proposal Spend
proposal_amt = uint64(10)
proposal_coin_id = Coin(parent_id, full_proposal.get_tree_hash(), proposal_amt).name()
solution = Program.to(
[
[proposal_coin_id, spend_p2_singleton_puzhash, 0, "s"],
[proposal_id, 1200, 950, parent_id, proposal_amt],
spend_p2_singleton,
spend_p2_singleton_solution,
]
)
conds = treasury_inner.run(solution)
assert len(conds.as_python()) == 10 + len(conditions)
def test_lockup() -> None:
"""
The lockup puzzle tracks the voting records of DAO CATs. When a proposal is
voted on, the proposal ID is added to a list, against which, future votes
are checked.
This test checks the addition of new votes to the lockup, and that you can't
re-vote on a proposal twice.
"""
CAT_TAIL_HASH = Program.to("tail").get_tree_hash()
INNERPUZ = Program.to(1)
previous_votes = [0xFADEDDAB]
dao_lockup_self = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
)
full_lockup_puz: Program = dao_lockup_self.curry(
dao_lockup_self.get_tree_hash(),
previous_votes,
INNERPUZ,
)
my_id = Program.to("my_id").get_tree_hash()
lockup_coin_amount = 20
# Test adding vote
new_proposal = 0xBADDADAB
new_vote_list = [new_proposal, 0xFADEDDAB]
child_puzhash = dao_lockup_self.curry(
dao_lockup_self.get_tree_hash(),
new_vote_list,
INNERPUZ,
).get_tree_hash()
message = Program.to([new_proposal, lockup_coin_amount, 1, my_id]).get_tree_hash()
generated_conditions = [[51, child_puzhash, lockup_coin_amount], [62, message]]
solution: Program = Program.to(
[
my_id,
generated_conditions,
20,
new_proposal,
INNERPUZ.get_tree_hash(), # fake proposal curry vals
1,
20,
child_puzhash,
0,
]
)
conds: Program = full_lockup_puz.run(solution)
assert len(conds.as_python()) == 6
# Test Re-voting on same proposal fails
new_proposal = 0xBADDADAB
new_vote_list = [new_proposal, 0xBADDADAB]
child_puzhash = dao_lockup_self.curry(
dao_lockup_self.get_tree_hash(),
new_vote_list,
INNERPUZ,
).get_tree_hash()
message = Program.to([new_proposal, lockup_coin_amount, 1, my_id]).get_tree_hash()
generated_conditions = [[51, child_puzhash, lockup_coin_amount], [62, message]]
revote_solution: Program = Program.to(
[
my_id,
generated_conditions,
20,
new_proposal,
INNERPUZ.get_tree_hash(), # fake proposal curry vals
1,
20,
child_puzhash,
0,
]
)
with pytest.raises(ValueError) as e_info:
conds = full_lockup_puz.run(revote_solution)
assert "clvm raise" in e_info.value.args[0]
# Test vote removal
solution = Program.to(
[
0,
generated_conditions,
20,
[0xFADEDDAB],
INNERPUZ.get_tree_hash(),
0,
0,
0,
0,
]
)
conds = full_lockup_puz.run(solution)
assert len(conds.as_python()) == 3
new_innerpuz = Program.to("new_inner")
new_innerpuzhash = new_innerpuz.get_tree_hash()
child_lockup = dao_lockup_self.curry(
dao_lockup_self.get_tree_hash(),
previous_votes,
new_innerpuz,
).get_tree_hash()
message = Program.to([0, 0, 0, my_id]).get_tree_hash()
spend_conds = [[51, child_lockup, lockup_coin_amount], [62, message]]
transfer_sol = Program.to(
[
my_id,
spend_conds,
lockup_coin_amount,
0,
INNERPUZ.get_tree_hash(), # fake proposal curry vals
0,
0,
INNERPUZ.get_tree_hash(),
new_innerpuzhash,
]
)
conds = full_lockup_puz.run(transfer_sol)
assert conds.at("rrrrfrf").as_atom() == child_lockup
def test_proposal_lifecycle() -> None:
"""
This test covers the whole lifecycle of a proposal and treasury.
Its main function is to check that the announcement pairs between treasury
and proposal are accurate.
It covers the spend proposal and update proposal types.
"""
proposal_pass_percentage: uint64 = uint64(5100)
attendance_required: uint64 = uint64(1000)
proposal_timelock: uint64 = uint64(40)
soft_close_length: uint64 = uint64(5)
self_destruct_time: uint64 = uint64(1000)
oracle_spend_delay: uint64 = uint64(10)
min_amt: uint64 = uint64(1)
CAT_TAIL_HASH = Program.to("tail").get_tree_hash()
dao_rules = DAORules(
proposal_timelock=proposal_timelock,
soft_close_length=soft_close_length,
attendance_required=attendance_required,
pass_percentage=proposal_pass_percentage,
self_destruct_length=self_destruct_time,
oracle_spend_delay=oracle_spend_delay,
proposal_minimum_amount=min_amt,
)
# Setup the treasury
treasury_id = Program.to("treasury_id").get_tree_hash()
treasury_singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
treasury_amount = 1
# setup the p2_singleton
p2_singleton = P2_SINGLETON_MOD.curry(treasury_singleton_struct, P2_SINGLETON_AGGREGATOR_MOD)
p2_singleton_puzhash = p2_singleton.get_tree_hash()
parent_id = Program.to("parent").get_tree_hash()
locked_amount = 100000
conditions = [[51, 0xDABBAD00, 1000], [51, 0xCAFEF00D, 100]]
excess_puzhash = get_p2_singleton_puzhash(treasury_id)
dao_lockup_self = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
)
proposal_curry_one = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
dao_lockup_self.get_tree_hash(),
CAT_TAIL_HASH,
treasury_id,
)
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
treasury_singleton_struct,
proposal_curry_one.get_tree_hash(),
min_amt,
excess_puzhash,
)
treasury_inner_puz: Program = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
proposal_timelock,
soft_close_length,
attendance_required,
proposal_pass_percentage,
self_destruct_time,
oracle_spend_delay,
)
treasury_inner_puzhash = treasury_inner_puz.get_tree_hash()
calculated_treasury_puzhash = get_treasury_puzzle(dao_rules, treasury_id, CAT_TAIL_HASH).get_tree_hash()
assert treasury_inner_puzhash == calculated_treasury_puzhash
full_treasury_puz = SINGLETON_MOD.curry(treasury_singleton_struct, treasury_inner_puz)
full_treasury_puzhash = full_treasury_puz.get_tree_hash()
# Setup the spend_p2_singleton (proposal inner puz)
spend_p2_singleton = SPEND_P2_SINGLETON_MOD.curry(
treasury_singleton_struct,
CAT_MOD_HASH,
conditions,
[],
p2_singleton_puzhash, # tailhash conds
)
spend_p2_singleton_puzhash = spend_p2_singleton.get_tree_hash()
parent_amt_list = [[parent_id, locked_amount]]
cat_parent_amt_list: list[Optional[Any]] = []
spend_p2_singleton_solution = Program.to([parent_amt_list, cat_parent_amt_list, treasury_inner_puzhash])
# Setup Proposal
proposal_id = Program.to("proposal_id").get_tree_hash()
proposal_singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
current_votes = 1200
yes_votes = 950
proposal: Program = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
proposal_id,
spend_p2_singleton_puzhash,
yes_votes,
current_votes,
)
full_proposal: Program = SINGLETON_MOD.curry(proposal_singleton_struct, proposal)
full_proposal_puzhash: bytes32 = full_proposal.get_tree_hash()
proposal_amt = uint64(11)
proposal_coin_id = Coin(parent_id, full_proposal_puzhash, proposal_amt).name()
treasury_solution: Program = Program.to(
[
[proposal_coin_id, spend_p2_singleton_puzhash, 0],
[proposal_id, current_votes, yes_votes, parent_id, proposal_amt],
spend_p2_singleton,
spend_p2_singleton_solution,
]
)
proposal_solution = Program.to(
[
proposal_validator.get_tree_hash(),
0,
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
proposal_amt,
]
)
# lineage_proof my_amount inner_solution
lineage_proof = [treasury_id, treasury_inner_puzhash, treasury_amount]
full_treasury_solution = Program.to([lineage_proof, treasury_amount, treasury_solution])
full_proposal_solution = Program.to([lineage_proof, proposal_amt, proposal_solution])
# Run the puzzles
treasury_conds = conditions_dict_for_solution(full_treasury_puz, full_treasury_solution, INFINITE_COST)
proposal_conds = conditions_dict_for_solution(full_proposal, full_proposal_solution, INFINITE_COST)
# Announcements
treasury_aca = treasury_conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT][0].vars[0]
proposal_cca = proposal_conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0]
assert std_hash(proposal_coin_id + proposal_cca) == treasury_aca
treasury_cpas = [
std_hash(full_treasury_puzhash + cond.vars[0])
for cond in treasury_conds[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT]
]
proposal_apas = [cond.vars[0] for cond in proposal_conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT]]
assert treasury_cpas[1] == proposal_apas[1]
# Test Proposal to update treasury
# Set up new treasury params
new_proposal_pass_percentage: uint64 = uint64(2500)
new_attendance_required: uint64 = uint64(500)
new_proposal_timelock: uint64 = uint64(900)
new_soft_close_length: uint64 = uint64(10)
new_self_destruct_time: uint64 = uint64(1000)
new_oracle_spend_delay: uint64 = uint64(20)
new_minimum_amount: uint64 = uint64(10)
proposal_excess_puzhash: bytes32 = get_p2_singleton_puzhash(treasury_id)
new_dao_rules = DAORules(
proposal_timelock=new_proposal_timelock,
soft_close_length=new_soft_close_length,
attendance_required=new_attendance_required,
pass_percentage=new_proposal_pass_percentage,
self_destruct_length=new_self_destruct_time,
oracle_spend_delay=new_oracle_spend_delay,
proposal_minimum_amount=new_minimum_amount,
)
update_proposal = DAO_UPDATE_MOD.curry(
DAO_TREASURY_MOD_HASH,
DAO_PROPOSAL_VALIDATOR_MOD_HASH,
treasury_singleton_struct,
proposal_curry_one.get_tree_hash(),
new_minimum_amount,
proposal_excess_puzhash,
new_proposal_timelock,
new_soft_close_length,
new_attendance_required,
new_proposal_pass_percentage,
new_self_destruct_time,
new_oracle_spend_delay,
)
update_proposal_puzhash = update_proposal.get_tree_hash()
update_proposal_sol = Program.to([])
proposal = proposal_curry_one.curry(
proposal_curry_one.get_tree_hash(),
proposal_id,
update_proposal_puzhash,
yes_votes,
current_votes,
)
full_proposal = SINGLETON_MOD.curry(proposal_singleton_struct, proposal)
full_proposal_puzhash = full_proposal.get_tree_hash()
proposal_coin_id = Coin(parent_id, full_proposal_puzhash, proposal_amt).name()
treasury_solution = Program.to(
[
[proposal_coin_id, update_proposal_puzhash, 0, "u"],
[proposal_id, current_votes, yes_votes, parent_id, proposal_amt],
update_proposal,
update_proposal_sol,
]
)
proposal_solution = Program.to(
[
proposal_validator.get_tree_hash(),
0,
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
proposal_amt,
]
)
lineage_proof = [treasury_id, treasury_inner_puzhash, treasury_amount]
full_treasury_solution = Program.to([lineage_proof, treasury_amount, treasury_solution])
full_proposal_solution = Program.to([lineage_proof, proposal_amt, proposal_solution])
treasury_conds = conditions_dict_for_solution(full_treasury_puz, full_treasury_solution, INFINITE_COST)
proposal_conds = conditions_dict_for_solution(full_proposal, full_proposal_solution, INFINITE_COST)
treasury_aca = treasury_conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT][0].vars[0]
proposal_cca = proposal_conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0]
assert std_hash(proposal_coin_id + proposal_cca) == treasury_aca
treasury_cpas = [
std_hash(full_treasury_puzhash + cond.vars[0])
for cond in treasury_conds[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT]
]
proposal_apas = [cond.vars[0] for cond in proposal_conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT]]
assert treasury_cpas[1] == proposal_apas[1]
new_treasury_inner = update_proposal.run(update_proposal_sol).at("frf").as_atom()
expected_treasury_inner = get_treasury_puzzle(new_dao_rules, treasury_id, CAT_TAIL_HASH)
assert new_treasury_inner == expected_treasury_inner.get_tree_hash()
expected_treasury_hash = curry_singleton(treasury_id, expected_treasury_inner).get_tree_hash()
assert treasury_conds[ConditionOpcode.CREATE_COIN][1].vars[0] == expected_treasury_hash
async def do_spend(
sim: SpendSim,
sim_client: SimClient,
coins: list[Coin],
puzzles: list[Program],
solutions: list[Program],
) -> tuple[MempoolInclusionStatus, Optional[Err]]:
spends = []
for coin, puzzle, solution in zip(coins, puzzles, solutions):
spends.append(make_spend(coin, puzzle, solution))
spend_bundle = WalletSpendBundle(spends, AugSchemeMPL.aggregate([]))
result = await sim_client.push_tx(spend_bundle)
await sim.farm_block()
return result
@pytest.mark.anyio
async def test_singleton_aggregator() -> None:
async with sim_and_client() as (sim, sim_client):
aggregator = P2_SINGLETON_AGGREGATOR_MOD
aggregator_hash = aggregator.get_tree_hash()
await sim.farm_block(aggregator_hash)
await sim.farm_block(aggregator_hash)
for i in range(5):
await sim.farm_block()
coin_records = await sim_client.get_coin_records_by_puzzle_hash(aggregator_hash)
coins = [c.coin for c in coin_records]
output_coin = coins[0]
output_sol = Program.to(
[
output_coin.name(),
output_coin.puzzle_hash,
output_coin.amount,
[[c.parent_coin_info, c.puzzle_hash, c.amount] for c in coins[1:]],
]
)
merge_sols = [
Program.to([c.name(), c.puzzle_hash, c.amount, [], [output_coin.parent_coin_info, output_coin.amount]])
for c in coins[1:]
]
res = await do_spend(sim, sim_client, coins, [aggregator] * 4, [output_sol, *merge_sols])
assert res[0] == MempoolInclusionStatus.SUCCESS
await sim.rewind(uint32(sim.block_height - 1))
# Spend a merge coin with empty output details
output_sol = Program.to(
[
output_coin.name(),
output_coin.puzzle_hash,
output_coin.amount,
[],
[],
]
)
res = await do_spend(sim, sim_client, [output_coin], [aggregator], [output_sol])
assert res[0] == MempoolInclusionStatus.FAILED
# Try to steal treasury coins with a phoney output
acs = Program.to(1)
acs_ph = acs.get_tree_hash()
await sim.farm_block(acs_ph)
bad_coin = (await sim_client.get_coin_records_by_puzzle_hash(acs_ph))[0].coin
bad_sol = Program.to(
[
[ConditionOpcode.CREATE_COIN, acs_ph, sum(c.amount for c in coins)],
*[[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, std_hash(c.name())] for c in coins],
[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, 0],
]
)
merge_sols = [
Program.to([c.name(), c.puzzle_hash, c.amount, [], [bad_coin.parent_coin_info, bad_coin.amount]])
for c in coins
]
res = await do_spend(sim, sim_client, [bad_coin, *coins], [acs] + [aggregator] * 4, [bad_sol, *merge_sols])
assert res[0] == MempoolInclusionStatus.FAILED