Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update snapshot class for matching eip712 specs and data structured #1006

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 118 additions & 48 deletions great_ape_safe/ape_api/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import time
from rich.console import Console

from eth_account import messages
from brownie import interface
from eip712.hashing import hash_message as hash_eip712_message
from eip712.validation import validate_structured_data
from brownie import interface, web3
from helpers.addresses import registry


Expand All @@ -18,8 +19,8 @@ def __init__(self, safe, proposal_id):
self.sign_message_lib = interface.ISignMessageLib(
registry.eth.gnosis.sign_message_lib, owner=self.safe.account
)

self.vote_relayer = "https://relayer.snapshot.org/api/message"
# https://github.com/snapshot-labs/snapshot-relayer/blob/master/src/constants.json#L3
self.vote_relayer = "https://relayer.snapshot.org/api/msg"
self.subgraph = "https://hub.snapshot.org/graphql?"
self.proposal_query = """
query($proposal_id: String) {
Expand All @@ -39,10 +40,69 @@ def __init__(self, safe, proposal_id):
"Content-Type": "application/json",
"Referer": "https://snapshot.org/",
}
self.domain = {
"name": "snapshot",
"version": "0.1.4",
}
# `string` proposal type: https://vote.convexfinance.com/#/proposal/bafkreihkhe75abvrfl67oz7ucxxm42mn3ofxb267z3wyzxj7rcc7e7hq3a
self.eip_712_type = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
],
"Vote": [
{"name": "from", "type": "address"},
{"name": "space", "type": "string"},
{"name": "timestamp", "type": "uint64"},
{"name": "proposal", "type": "string"},
{"name": "choice", "type": "string"},
{"name": "reason", "type": "string"},
{"name": "app", "type": "string"},
{"name": "metadata", "type": "string"},
],
}
# `bytes32` proposal type: https://vote.aura.finance/#/proposal/0x022c66d408c9bccdf4f7e514718415d2717bc22290adea71f1b5261dbeb92f3c
self.eip_712_type_2 = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
],
"Vote": [
{"name": "from", "type": "address"},
{"name": "space", "type": "string"},
{"name": "timestamp", "type": "uint64"},
{"name": "proposal", "type": "bytes32"},
{"name": "choice", "type": "string"},
{"name": "reason", "type": "string"},
{"name": "app", "type": "string"},
{"name": "metadata", "type": "string"},
],
}

self.proposal_id = proposal_id
self.proposal_data = self._get_proposal_data(self.proposal_id)

def post_payload_relayer_api(self, payload):
# https://github.com/snapshot-labs/snapshot-relayer/blob/master/src/api.ts#L63
r = requests.post(
self.vote_relayer,
headers=self.headers,
data=json.dumps(
{
"address": self.safe.address,
"data": payload,
"sig": "0x",
},
separators=(",", ":"),
),
)

self.handle_response(r)

msg_hash = r.json()["id"]

return msg_hash

def handle_response(self, response):
if not response.ok:
console.print(f"Error: {response.text}")
Expand Down Expand Up @@ -84,37 +144,44 @@ def create_payload_hash(
timestamp=None,
choice=None,
proposal=None,
version="0.1.3",
type="vote",
metadata=None,
reason="",
):
# helper method to create message hash from payload and output to console
# can be used externally to verify generated hash
if not payload:
assert all([timestamp, choice])

internal_payload = {}
internal_payload["proposal"] = (
self.proposal_id if not proposal else proposal
)
internal_payload["choice"] = self.format_choice(choice)
if metadata:
internal_payload["metadata"] = metadata
if self.proposal_id.startswith("0x"):
types = self.eip_712_type_2
proposal = web3.toBytes(hexstr=self.proposal_id)
else:
types = self.eip_712_type
proposal = self.proposal_id

payload = {
"version": version,
"timestamp": str(timestamp),
"space": self.proposal_data["space"]["id"],
"type": type,
"payload": internal_payload,
"domain": self.domain,
"message": {
"from": self.safe.address,
"space": self.proposal_data["space"]["id"],
"timestamp": int(timestamp),
"proposal": proposal,
"choice": json.dumps(self.format_choice(choice)),
"reason": reason,
"app": "snapshot",
"metadata": json.dumps({}),
},
"primaryType": "Vote",
"types": types,
}

payload_stringify = json.dumps(payload, separators=(",", ":"))
hash = messages.defunct_hash_message(text=payload_stringify)
console.print(f"msg hash: {hash.hex()}")
return hash, payload_stringify
validate_structured_data(payload)

# https://github.com/ApeWorX/eip712/blob/main/eip712/hashing.py#L261
hash = hash_eip712_message(payload)

return hash, payload

def vote_and_post(self, choice, version="0.1.3", type="vote", metadata=None):
def vote_and_post(self, choice, reason="", metadata=None):
# given a choice, contruct payload, post to vote relayer and post safe tx
# for single vote, pass in choice as str ex: "yes"
# for weighted vote, pass in choice(s) as dict ex: {"80/20 BADGER/WBTC": 1}
Expand All @@ -128,22 +195,33 @@ def vote_and_post(self, choice, version="0.1.3", type="vote", metadata=None):

choice_formatted = self.format_choice(choice)

internal_payload = {}
internal_payload["proposal"] = self.proposal_id
internal_payload["choice"] = choice_formatted
if metadata:
internal_payload["metadata"] = metadata
if self.proposal_id.startswith("0x"):
types = self.eip_712_type_2
proposal = web3.toBytes(hexstr=self.proposal_id)
else:
types = self.eip_712_type
proposal = self.proposal_id

payload = {
"version": version,
"timestamp": str(int(time.time())),
"space": space,
"type": type,
"payload": internal_payload,
"domain": self.domain,
"message": {
"from": self.safe.address,
"space": space,
"timestamp": int(time.time()),
"proposal": proposal,
"choice": json.dumps(choice_formatted, separators=(",", ":")),
"reason": reason,
"app": "snapshot",
"metadata": json.dumps({}) if not metadata else metadata,
},
"primaryType": "Vote",
"types": types,
}

validate_structured_data(payload)

console.print("payload", payload)
hash, payload_stringify = self.create_payload_hash(payload)
hash, _ = self.create_payload_hash(payload)

if is_weighted:
for label, weight in choice_formatted.items():
Expand All @@ -155,20 +233,12 @@ def vote_and_post(self, choice, version="0.1.3", type="vote", metadata=None):

tx_data = self.sign_message_lib.signMessage.encode_input(hash)

response = requests.post(
self.vote_relayer,
headers=self.headers,
data=json.dumps(
{
"address": self.safe.address,
"msg": payload_stringify,
"sig": "0x",
},
separators=(",", ":"),
),
)
# NOTE: remove unused types as per endpoint 500 error
payload["types"].pop("EIP712Domain")
# NOTE: prior to json stringify, convert proposal[bytes -> string]
payload["message"]["proposal"] = self.proposal_id

self.handle_response(response)
self.post_payload_relayer_api(payload)

safe_tx = self.safe.build_multisig_tx(
to=registry.eth.gnosis.sign_message_lib, value=0, data=tx_data, operation=1
Expand Down
2 changes: 2 additions & 0 deletions helpers/addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,8 @@ def get_registry():
return registry.op
elif chain.id == 42:
return registry.kovan
elif chain.id == 5:
return registry.goerli
except:
return registry.eth

Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ def ibbtc_msig():
return GreatApeSafe(registry.eth.badger_wallets.ibbtc_multisig)


@pytest.fixture
def voter_msig():
return GreatApeSafe(registry.eth.badger_wallets.treasury_voter_multisig)


@pytest.fixture
def USDC(safe):
usdc = interface.IFiatTokenV2_1(
Expand Down
22 changes: 22 additions & 0 deletions tests/snapshot/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest

# https://snapshot.org/#/cvx.eth/proposal/0xdded9266cc2c671793d6ee9db2b6328222399e7314a265b5ff50f3381edf48fc
PROPOSAL_SINGLE_CHOICE = (
"0xdded9266cc2c671793d6ee9db2b6328222399e7314a265b5ff50f3381edf48fc"
)
# https://snapshot.org/#/aurafinance.eth/proposal/0xc950294fd8c00b2901026b62b568b01923e62166fca1e96d738f4a27bdf26faf
PROPOSAL_MULTI_CHOICE = (
"0xc950294fd8c00b2901026b62b568b01923e62166fca1e96d738f4a27bdf26faf"
)


@pytest.fixture
def snapshot_single_choice(voter_msig):
voter_msig.init_snapshot(PROPOSAL_SINGLE_CHOICE)
return voter_msig.snapshot


@pytest.fixture
def snapshot_multi_choice(voter_msig):
voter_msig.init_snapshot(PROPOSAL_MULTI_CHOICE)
return voter_msig.snapshot
51 changes: 51 additions & 0 deletions tests/snapshot/test_eip712_hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import time

"""
Reverting means a mismatch on the hash production and relayers may be looking for a different
hash from the safe events and the vote may not show up ever in the snapshot space as result
"""


def test_hash_single_choice(snapshot_single_choice):
ts = time.time()
singe_choice = "yes"
hash_class_generated, payload = snapshot_single_choice.create_payload_hash(
timestamp=ts, choice=singe_choice
)

# debugging eip712 hash produce from https://pypi.org/project/eip712/ package
print("Class generated hash:", hash_class_generated.hex())

# NOTE: remove unused types as per endpoint 500 error
payload["types"].pop("EIP712Domain")
# NOTE: prior to json stringify, convert proposal[bytes -> string]
payload["message"]["proposal"] = snapshot_single_choice.proposal_id
hash_api_generated = snapshot_single_choice.post_payload_relayer_api(payload)

# debugging api `msgHash`
print("API generated hash:", hash_api_generated)

assert hash_class_generated.hex() == hash_api_generated


def test_hash_multi_choice(snapshot_multi_choice):
ts = time.time()
multi_choice = {"80/20 BADGER/WBTC": 1, "40/40/20 WBTC/DIGG/graviAURA": 1}

hash_class_generated, payload = snapshot_multi_choice.create_payload_hash(
timestamp=ts, choice=multi_choice
)

# debugging eip712 hash produce from https://pypi.org/project/eip712/ package
print("Class generated hash:", hash_class_generated.hex())

# NOTE: remove unused types as per endpoint 500 error
payload["types"].pop("EIP712Domain")
# NOTE: prior to json stringify, convert proposal[bytes -> string]
payload["message"]["proposal"] = snapshot_multi_choice.proposal_id
hash_api_generated = snapshot_multi_choice.post_payload_relayer_api(payload)

# debugging api `msgHash`
print("API generated hash:", hash_api_generated)

assert hash_class_generated.hex() == hash_api_generated