Skip to content

Commit b26054f

Browse files
Governance additions (vote via legacy delegation, vote via liquid staking) (#4)
* Prepare scripts for governance call (work in progress). * Micro-refactor. * Sketch functions for voting via legacy delegation. * Sketch script for voting via legacy delegation. * Improve voting (on-chain). A bit more robust. * Fix voting for direct staking. * Improve vote via legacy delegation. * Display previous votes etc. * Refactor, adjust getting previous votes. * More robust flow. * Get on-chain delegated votes, apply some checks when voting via legacy delegation. * Simple report on governance (voting). * Adjust readme. * Rename files, cleanup etc. * Fix gas limit. * Add proofs for governance, for liquid staking (delegating votes). * Vote via liquid staking (work in progress). * Adjust report etc. Refactoring. * Fix function name. * Update proofs for governance. * Fix logic around getting past votes. * Fix decoding. * Fix getting timestamp etc. * Fix after self-review (refactoring). * Update governance proofs (Hatom). * Fix after review.
1 parent 3592df6 commit b26054f

File tree

13 files changed

+19821
-148
lines changed

13 files changed

+19821
-148
lines changed

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,30 @@ PYTHONPATH=. python3 ./wizard/prepare_custom_tokens.py --token=WEGLD-a28c59 --ne
117117
PYTHONPATH=. python3 ./wizard/do_transfers.py --network=devnet --wallets=$WALLETS_CONFIG --infile=custom_transfers.json --receiver=${RECEIVER} --auth=$AUTH_REGISTRATION
118118
```
119119

120-
## Vote on governance - outdated
120+
## Governance: direct vote
121121

122122
```
123-
export PROOFS="./proofs.json"
124-
PYTHONPATH=. python3 ./wizard/vote_on_governance.py --network=devnet --wallets=$WALLETS_CONFIG --proofs=${PROOFS} --auth=$AUTH_REGISTRATION
123+
PYTHONPATH=. python3 ./wizard/vote_directly.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote yes --auth=$AUTH_REGISTRATION
125124
```
126125

127-
## Vote on on-chain governance
126+
## Governance: delegated vote (via legacy delegation)
128127

129128
```
130-
PYTHONPATH=. python3 ./wizard/vote_on_onchain_governance.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote <yes/no/abstain/veto> --auth=$AUTH_REGISTRATION
129+
PYTHONPATH=. python3 ./wizard/vote_via_legacy_delegation.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote yes --auth=$AUTH_REGISTRATION
130+
```
131+
132+
## Governance: delegated vote (via liquid staking contracts)
133+
134+
```
135+
export CONTRACT=...
136+
137+
PYTHONPATH=. python3 ./wizard/vote_via_liquid_staking.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce> --vote yes --auth=$AUTH_REGISTRATION --contract=$CONTRACT
138+
```
139+
140+
## Simple report on governance (voting)
141+
142+
```
143+
PYTHONPATH=. python3 ./wizard/voting_report.py --network=devnet --wallets=$WALLETS_CONFIG --proposal <proposal nonce>
131144
```
132145

133146
## Guardians

governance_proofs/mainnet/erd1qqqqqqqqqqqqqpgq2khda0rx207gvlqg92dq5rh0z03a8dqf78ssu0qlcc/1.json

Lines changed: 16832 additions & 0 deletions
Large diffs are not rendered by default.

governance_proofs/mainnet/erd1qqqqqqqqqqqqqpgqdnpmeseu3j5t7grds9dfj8ttt70pev66ah0sydkq9x/1.json

Lines changed: 2472 additions & 0 deletions
Large diffs are not rendered by default.

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
requests>=2.32.0,<3.0.0
22
ledgercomm[hid]
33
rich==13.3.4
4-
multiversx-sdk[ledger]==2.1.0
4+
multiversx-sdk[ledger]==2.3.2
55
pyotp==2.9.0

wizard/configuration.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ class Configuration:
3131
deep_history_url: str
3232
explorer_url: str
3333
legacy_delegation_contract: str
34-
governance_contract: str
34+
system_governance_contract: str
3535
cosigner_url: str
36+
liquid_staking_contracts: list[str]
3637

3738

3839
CONFIGURATIONS = {
@@ -43,8 +44,9 @@ class Configuration:
4344
deep_history_url=ENV_MAINNET_DEEP_HISTORY_URL or DEFAULT_MAINNET_DEEP_HISTORY_URL,
4445
explorer_url="https://explorer.multiversx.com",
4546
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt",
46-
governance_contract="erd1qqqqqqqqqqqqqpgqfn2mu8l0dte34eqh6qtgmpjpxpkhunccrl4sy2sp07",
47+
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
4748
cosigner_url="https://tools.multiversx.com",
49+
liquid_staking_contracts=["erd1qqqqqqqqqqqqqpgq2khda0rx207gvlqg92dq5rh0z03a8dqf78ssu0qlcc", "erd1qqqqqqqqqqqqqpgqdnpmeseu3j5t7grds9dfj8ttt70pev66ah0sydkq9x"]
4850
),
4951
"devnet": Configuration(
5052
chain_id="D",
@@ -53,8 +55,9 @@ class Configuration:
5355
deep_history_url=ENV_DEVNET_DEEP_HISTORY_URL or DEFAULT_DEVNET_DEEP_HISTORY_URL,
5456
explorer_url="https://devnet-explorer.multiversx.com",
5557
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep",
56-
governance_contract="erd1qqqqqqqqqqqqqpgqahutnw3r4s95gxz4keecvlyyl3wlsu2mdthq06swcp",
57-
cosigner_url="https://devnet-tools.multiversx.com"
58+
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
59+
cosigner_url="https://devnet-tools.multiversx.com",
60+
liquid_staking_contracts=["erd1qqqqqqqqqqqqqpgqlavy2909f0pa9yf66es5cwh53m0wue28u7hs79g2m2"],
5861
),
5962
"testnet": Configuration(
6063
chain_id="T",
@@ -63,7 +66,8 @@ class Configuration:
6366
deep_history_url=ENV_TESTNET_DEEP_HISTORY_URL or DEFAULT_TESTNET_DEEP_HISTORY_URL,
6467
explorer_url="https://testnet-explorer.multiversx.com",
6568
legacy_delegation_contract="erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep",
66-
governance_contract="erd1qqqqqqqqqqqqqpgqahutnw3r4s95gxz4keecvlyyl3wlsu2mdthq06swcp",
67-
cosigner_url="https://testnet-tcs-api.multiversx.com"
69+
system_governance_contract="erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla",
70+
cosigner_url="https://testnet-tcs-api.multiversx.com",
71+
liquid_staking_contracts=[],
6872
),
6973
}

wizard/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
TRANSACTION_AWAITING_PATIENCE_IN_MILLISECONDS = 8000
1313
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_CLAIM_REWARDS = 50
1414
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS = 10_000
15+
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_VOTE = 10
1516
MAX_NUM_CUSTOM_TOKENS_TO_FETCH = 10_000
1617
METACHAIN_ID = 4294967295
1718
ONE_QUINTILLION = 1000000000000000000

wizard/entrypoint.py

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import base64
12
import time
3+
from datetime import datetime, timedelta, timezone
24
from multiprocessing.dummy import Pool
35
from typing import Any, Callable, Optional
46

@@ -8,7 +10,8 @@
810
NetworkProviderConfig, NetworkProviderError,
911
ProxyNetworkProvider, Token, TokenTransfer,
1012
Transaction, TransactionOnNetwork, VoteType)
11-
from multiversx_sdk.abi import BigUIntValue, BytesValue, U64Value
13+
from multiversx_sdk.abi import (AddressValue, BigUIntValue, BytesValue,
14+
StringValue, U64Value)
1215
from rich import print
1316

1417
from wizard import ux
@@ -21,7 +24,8 @@
2124
COSIGNER_SIGN_TRANSACTIONS_RETRY_DELAY_IN_SECONDS,
2225
DEFAULT_CHUNK_SIZE_OF_SEND_TRANSACTIONS, MAX_NUM_CUSTOM_TOKENS_TO_FETCH,
2326
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_CLAIM_REWARDS,
24-
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS, METACHAIN_ID,
27+
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_REWARDS,
28+
MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_VOTE, METACHAIN_ID,
2529
NETWORK_PROVIDER_NUM_RETRIES, NETWORK_PROVIDER_TIMEOUT_SECONDS,
2630
NETWORK_PROVIDERS_RETRY_DELAY_IN_SECONDS,
2731
NUM_PARALLEL_GET_GUARDIAN_DATA_REQUESTS, NUM_PARALLEL_GET_NONCE_REQUESTS,
@@ -30,6 +34,7 @@
3034
TRANSACTION_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS)
3135
from wizard.currencies import is_native_currency
3236
from wizard.errors import KnownError, TransientError
37+
from wizard.governance import OnChainVote
3338
from wizard.guardians import (AuthApp, AuthRegistrationEntry, CosignerClient,
3439
GuardianData)
3540
from wizard.rewards import ClaimableRewards, ReceivedRewards, RewardsType
@@ -44,7 +49,7 @@ def __init__(
4449
configuration: Configuration,
4550
use_gas_estimator: Optional[bool] = None,
4651
gas_limit_multiplier: Optional[float] = None
47-
) -> None:
52+
) -> None:
4853
self.configuration = configuration
4954

5055
self.network_entrypoint = NetworkEntrypoint(
@@ -119,12 +124,16 @@ def get_claimable_rewards_legacy(self, delegator: Address) -> int:
119124
return int(amount)
120125

121126
def recall_nonces(self, accounts_wrappers: list[AccountWrapper]):
127+
print("Recalling nonces...")
128+
122129
def recall_nonce(wrapper: AccountWrapper):
123130
wrapper.account.nonce = self.network_entrypoint.recall_account_nonce(wrapper.account.address)
124131

125132
Pool(NUM_PARALLEL_GET_NONCE_REQUESTS).map(recall_nonce, accounts_wrappers)
126133

127134
def recall_guardians(self, accounts: list[AccountWrapper]):
135+
print("Recalling guardians...")
136+
128137
def recall_guardian(wrapper: AccountWrapper):
129138
guardian_data = self.get_guardian_data(wrapper.account.address)
130139
wrapper.guardian = Address.new_from_bech32(guardian_data.active_guardian) if guardian_data.is_guarded else None
@@ -263,19 +272,67 @@ def transfer_funds(self, sender: AccountWrapper, receiver: Address, transfer: To
263272
guardian=sender.guardian
264273
)
265274

266-
def vote_on_governance(self, sender: AccountWrapper, proposal: int, choice: int, power: int, proof: bytes, gas_price: int) -> Transaction:
267-
governance_contract = Address.new_from_bech32(self.configuration.governance_contract)
275+
def get_direct_voting_power(self, voter: Address):
276+
controller = self.network_entrypoint.create_governance_controller()
277+
return controller.get_voting_power(voter)
278+
279+
def vote_directly(self, sender: AccountWrapper, proposal: int, vote: VoteType, gas_price: int) -> Transaction:
280+
controller = self.network_entrypoint.create_governance_controller()
281+
282+
return controller.create_transaction_for_voting(
283+
sender=sender.account,
284+
nonce=sender.account.get_nonce_then_increment(),
285+
proposal_nonce=proposal,
286+
vote=vote,
287+
gas_price=gas_price,
288+
guardian=sender.guardian,
289+
)
290+
291+
def get_voting_power_via_legacy_delegation(self, voter: Address) -> int:
292+
legacy_delegation_contract = Address.new_from_bech32(self.configuration.legacy_delegation_contract)
293+
294+
controller = self.network_entrypoint.create_smart_contract_controller()
295+
[power_encoded] = controller.query(
296+
contract=legacy_delegation_contract,
297+
function="getVotingPower",
298+
arguments=[AddressValue.new_from_address(voter)],
299+
)
300+
301+
power = BigUIntValue()
302+
power.decode_top_level(power_encoded)
303+
return power.value
304+
305+
def vote_via_legacy_delegation(self, sender: AccountWrapper, proposal: int, vote: VoteType, gas_price: int):
306+
legacy_delegation_contract = Address.new_from_bech32(self.configuration.legacy_delegation_contract)
307+
308+
controller = self.network_entrypoint.create_smart_contract_controller()
309+
transaction = controller.create_transaction_for_execute(
310+
sender=sender.account,
311+
nonce=sender.account.get_nonce_then_increment(),
312+
contract=legacy_delegation_contract,
313+
function="delegateVote",
314+
arguments=[U64Value(proposal), StringValue(vote.value)],
315+
# Gas estimator might not work, thus we hard-code a value here.
316+
gas_limit=75_000_000,
317+
gas_price=gas_price,
318+
guardian=sender.guardian
319+
)
320+
321+
return transaction
268322

323+
def vote_via_liquid_staking(self, sender: AccountWrapper, contract: str, proposal: int, vote: VoteType, power: int, proof: bytes, gas_price: int) -> Transaction:
269324
controller = self.network_entrypoint.create_smart_contract_controller()
325+
270326
transaction = controller.create_transaction_for_execute(
271327
sender=sender.account,
272328
nonce=sender.account.get_nonce_then_increment(),
273-
contract=governance_contract,
274-
gas_limit=50_000_000,
275-
function="vote",
329+
contract=Address.new_from_bech32(contract),
330+
# Gas estimator might not work, thus we hard-code a value here.
331+
gas_limit=100_000_000,
332+
function="delegate_vote",
276333
arguments=[
277334
U64Value(proposal),
278-
U64Value(choice),
335+
StringValue(vote.value),
279336
BigUIntValue(power),
280337
BytesValue(proof)
281338
],
@@ -285,16 +342,59 @@ def vote_on_governance(self, sender: AccountWrapper, proposal: int, choice: int,
285342

286343
return transaction
287344

288-
def vote_on_onchain_governance(self, sender: AccountWrapper, proposal: int, vote: VoteType, gas_price: int) -> Transaction:
289-
controller = self.network_entrypoint.create_governance_controller()
290-
return controller.create_transaction_for_voting(
291-
sender=sender.account,
292-
nonce=sender.account.get_nonce_then_increment(),
293-
proposal_nonce=proposal,
294-
vote=vote,
295-
gas_price=gas_price,
296-
guardian=sender.guardian,
297-
)
345+
def get_direct_vote(self, voter: Address, proposal: int) -> Optional[OnChainVote]:
346+
return self._get_past_vote(voter.to_bech32(), self.configuration.system_governance_contract, "vote", "vote", proposal)
347+
348+
def get_vote_via_legacy_delegation(self, voter: Address, proposal: int) -> Optional[OnChainVote]:
349+
return self._get_past_vote(voter.to_bech32(), self.configuration.legacy_delegation_contract, "delegateVote", "delegateVote", proposal)
350+
351+
def get_vote_via_liquid_staking(self, voter: Address, contract: str, proposal: int) -> Optional[OnChainVote]:
352+
return self._get_past_vote(voter.to_bech32(), contract, "delegate_vote", "delegateVote", proposal)
353+
354+
def _get_past_vote(self, voter: str, contract: str, function: str, event_identifier: str, proposal: int) -> Optional[OnChainVote]:
355+
url = f"accounts/{voter}/transactions"
356+
size = MAX_NUM_TRANSACTIONS_TO_FETCH_OF_TYPE_VOTE
357+
reasonably_recent_timestamp = int((datetime.now(timezone.utc) - timedelta(days=30)).timestamp())
358+
359+
transactions = self._api_do_get(url, {
360+
"status": "success",
361+
"receiver": contract,
362+
"function": function,
363+
"withLogs": "true",
364+
"withScResults": "true",
365+
"size": size,
366+
"after": reasonably_recent_timestamp
367+
})
368+
369+
if len(transactions) == size:
370+
print(f"\tRetrieved {size} transactions. [red]There could be more![/red]")
371+
372+
for transaction in transactions:
373+
timestamp = transaction.get("timestamp", 0)
374+
375+
all_events: list[Any] = []
376+
all_events.extend(transaction.get("logs", {}).get("events", []))
377+
378+
for result in transaction.get("results"):
379+
all_events.extend(result.get("logs", {}).get("events", []))
380+
381+
for event in all_events:
382+
if event.get("identifier") != event_identifier:
383+
continue
384+
385+
topics = event.get("topics", [])
386+
387+
event_proposal_base64 = topics[0]
388+
event_proposal_bytes = base64.b64decode(event_proposal_base64)
389+
event_proposal = U64Value()
390+
event_proposal.decode_top_level(event_proposal_bytes)
391+
event_vote_type_base64 = topics[1]
392+
event_vote_type = VoteType(base64.b64decode(event_vote_type_base64).decode())
393+
394+
if event_proposal.value == proposal:
395+
return OnChainVote(voter, proposal, contract, timestamp, event_vote_type)
396+
397+
return None
298398

299399
def get_guardian_data(self, address: Address):
300400
response = self.proxy_network_provider.do_get_generic(f"address/{address.to_bech32()}/guardian-data")

wizard/governance.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

2+
import json
3+
from pathlib import Path
24
from typing import Any
35

4-
from multiversx_sdk import Address
6+
from multiversx_sdk import Address, VoteType
57

68

79
class GovernanceRecord:
@@ -17,3 +19,35 @@ def new_from_dictionary(cls, data: dict[str, Any]):
1719
proof = bytes.fromhex(data["proof"])
1820

1921
return cls(address, power, proof)
22+
23+
@classmethod
24+
def load_many_from_proofs_file(cls, proofs_file: Path):
25+
json_content = proofs_file.read_text()
26+
data = json.loads(json_content)
27+
records = [GovernanceRecord.new_from_dictionary(item) for item in data]
28+
29+
records_by_adresses: dict[str, GovernanceRecord] = {
30+
item.address.to_bech32(): item for item in records
31+
}
32+
33+
return records_by_adresses
34+
35+
36+
class OnChainVote:
37+
def __init__(self, voter: str, proposal: int, contract: str, timestamp: int, vote_type: VoteType) -> None:
38+
self.voter = voter
39+
self.proposal = proposal
40+
self.contract = contract
41+
self.timestamp = timestamp
42+
self.vote_type = vote_type
43+
44+
45+
def convert_string_to_vote_type(input: str) -> VoteType:
46+
input = input.lower()
47+
48+
return {
49+
"yes": VoteType.YES,
50+
"no": VoteType.NO,
51+
"abstain": VoteType.ABSTAIN,
52+
"veto": VoteType.VETO
53+
}[input]

0 commit comments

Comments
 (0)