-
Notifications
You must be signed in to change notification settings - Fork 2
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
Add proposal id option #63
Changes from 12 commits
cc93ba4
5b2f399
59e6ddf
d82e269
f54e723
a57b84b
25d497f
f6273dd
55c53db
bea0486
51203f3
c96e420
739e281
cb40c8c
9d8e472
8288e48
72c247e
8f09eca
d57485b
a51df2c
7691e48
4097d3d
20a4b3c
218e6df
b644502
9415e04
69891e6
851b574
551dc41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import requests | ||
|
||
from Quorum.utils.chain_enum import Chain | ||
from Quorum.apis.governance.data_models import BGDProposalData, PayloadAddresses | ||
|
||
BASE_BGD_CACHE_REPO = 'https://raw.githubusercontent.com/bgd-labs/v3-governance-cache/refs/heads/main/cache' | ||
PROPOSALS_URL = f'{BASE_BGD_CACHE_REPO}/1/0x9AEE0B04504CeF83A65AC3f0e838D0593BCb2BC7/proposals' | ||
|
||
CHAIN_ID_TO_CHAIN = { | ||
'1': Chain.ETH, | ||
'42161': Chain.ARB, | ||
'43114': Chain.AVAX, | ||
'8453': Chain.BASE, | ||
'56': Chain.BSC, | ||
'100': Chain.GNO, | ||
'10': Chain.OPT, | ||
'137': Chain.POLY, | ||
'534352': Chain.SCROLL, | ||
'324': Chain.ZK, | ||
'59144': Chain.LINEA, | ||
} | ||
|
||
class AaveGovernanceAPI: | ||
""" | ||
A utility class to interact with the BGD governance cache and retrieve | ||
relevant information about Aave proposals and payload addresses. | ||
""" | ||
|
||
def __init__(self) -> None: | ||
self.session = requests.Session() | ||
|
||
def get_proposal_data(self, proposal_id: int) -> BGDProposalData: | ||
""" | ||
Fetches and returns the data for a given proposal. | ||
|
||
Args: | ||
proposal_id: The ID of the proposal to fetch. | ||
|
||
Returns: | ||
A BGDProposalData object. | ||
""" | ||
proposal_data_link = f'{PROPOSALS_URL}/{proposal_id}.json' | ||
resp = self.session.get(proposal_data_link) | ||
resp.raise_for_status() | ||
|
||
raw_json = resp.json() | ||
# Parse into our data model | ||
return BGDProposalData(**raw_json) | ||
|
||
def get_payload_addresses(self, chain_id: str, controller: str, payload_id: int) -> list[str]: | ||
""" | ||
Retrieves a list of payload addresses for a given payload ID, chain, and controller. | ||
|
||
Args: | ||
chain_id: The chain ID for the proposal. | ||
controller: The controller for the proposal. | ||
payload_id: The ID of the payload to fetch. | ||
|
||
Returns: | ||
A list of addresses that are part of the payload. | ||
""" | ||
url = f'{BASE_BGD_CACHE_REPO}/{chain_id}/{controller}/payloads/{payload_id}.json' | ||
resp = self.session.get(url) | ||
resp.raise_for_status() | ||
|
||
payload_data = resp.json() | ||
# We only need the 'target' field from each action | ||
return [a['target'] for a in payload_data['payload']['actions']] | ||
|
||
def get_all_payloads_addresses(self, proposal_id: int) -> list[PayloadAddresses]: | ||
""" | ||
Retrieves all payload addresses for a given proposal. | ||
|
||
Args: | ||
proposal_id: The ID of the proposal to fetch. | ||
|
||
Returns: | ||
A list of PayloadAddresses objects, each containing a chain ID and a list of addresses. | ||
""" | ||
data = self.get_proposal_data(proposal_id) | ||
results = [] | ||
for p in data.proposal.payloads: | ||
addresses = self.get_payload_addresses(p.chain, p.payloads_controller, p.payload_id) | ||
results.append(PayloadAddresses(chain=CHAIN_ID_TO_CHAIN[p.chain], addresses=addresses)) | ||
return results |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from typing import List, Optional | ||
from pydantic import BaseModel, Field | ||
|
||
from Quorum.utils.chain_enum import Chain | ||
|
||
class IPFSData(BaseModel): | ||
title: Optional[str] = None | ||
discussions: Optional[str] = None | ||
|
||
|
||
class PayloadData(BaseModel): | ||
chain: str | ||
payloads_controller: str = Field(alias='payloadsController') | ||
payload_id: int = Field(alias='payloadId') | ||
|
||
class Config: | ||
allow_population_by_alias = True | ||
|
||
|
||
class ProposalData(BaseModel): | ||
payloads: list[PayloadData] = Field(default_factory=list) | ||
votingPortal: Optional[str] = None | ||
ipfsHash: Optional[str] = None | ||
|
||
|
||
class EventArgs(BaseModel): | ||
creator: Optional[str] = None | ||
accessLevel: Optional[int] = None | ||
ipfsHash: Optional[str] = None | ||
|
||
|
||
class EventData(BaseModel): | ||
transactionHash: Optional[str] = None | ||
args: EventArgs = Field(default_factory=EventArgs) | ||
|
||
|
||
class BGDProposalData(BaseModel): | ||
""" | ||
Represents the entire JSON structure returned by the BGD cache | ||
for a given proposal. | ||
""" | ||
ipfs: Optional[IPFSData] = None | ||
proposal: Optional[ProposalData] = None | ||
events: List[EventData] = Field(default_factory=list) | ||
|
||
|
||
class PayloadAddresses(BaseModel): | ||
chain: Chain | ||
addresses: List[str] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,105 @@ | ||
import requests | ||
from dataclasses import dataclass | ||
import json5 as json | ||
from pydantic import BaseModel | ||
from typing import Any, Dict | ||
|
||
from Quorum.apis.governance.aave_governance import AaveGovernanceAPI | ||
from Quorum.apis.governance.data_models import BGDProposalData, IPFSData, ProposalData, EventData | ||
|
||
|
||
BASE_BGD_CACHE_REPO = 'https://raw.githubusercontent.com/bgd-labs/v3-governance-cache/refs/heads/main/cache' | ||
PROPOSALS_URL = f'{BASE_BGD_CACHE_REPO}/1/0x9AEE0B04504CeF83A65AC3f0e838D0593BCb2BC7/proposals' | ||
BASE_SEATBELT_REPO = 'https://github.com/bgd-labs/seatbelt-gov-v3/blob/main/reports' | ||
SEATBELT_PAYLOADS_URL = f'{BASE_SEATBELT_REPO}/payloads' | ||
|
||
|
||
@dataclass | ||
class ChainInfo: | ||
class ChainInfo(BaseModel): | ||
name: str | ||
block_explorer_link: str | ||
|
||
|
||
AAVE_CHAIN_MAPPING = { | ||
'1': ChainInfo('Ethereum', 'https://etherscan.io/address'), | ||
'137': ChainInfo('Polygon', 'https://polygonscan.com/address'), | ||
'43114': ChainInfo('Avalanche', 'https://snowtrace.io/address'), | ||
'8453': ChainInfo('Base', 'https://basescan.org/address'), | ||
'42161': ChainInfo('Arbitrum One', 'https://arbiscan.io/address'), | ||
'1088': ChainInfo('Metis', 'https://explorer.metis.io/address'), | ||
'10': ChainInfo('OP Mainnet', 'https://optimistic.etherscan.io/address'), | ||
'56': ChainInfo('BNB Smart Chain', 'https://bscscan.com/address'), | ||
'100': ChainInfo('Gnosis', 'https://gnosisscan.io/address'), | ||
'534352': ChainInfo('Scroll', 'https://scrollscan.com/address'), | ||
'324': ChainInfo('zkSync Era', 'https://era.zksync.network/address'), | ||
"59144": ChainInfo('Linea', 'https://lineascan.build/'), | ||
'1': ChainInfo(name='Ethereum', block_explorer_link='https://etherscan.io/address'), | ||
'137': ChainInfo(name='Polygon', block_explorer_link='https://polygonscan.com/address'), | ||
'43114': ChainInfo(name='Avalanche', block_explorer_link='https://snowtrace.io/address'), | ||
'8453': ChainInfo(name='Base', block_explorer_link='https://basescan.org/address'), | ||
'42161': ChainInfo(name='Arbitrum One', block_explorer_link='https://arbiscan.io/address'), | ||
'1088': ChainInfo(name='Metis', block_explorer_link='https://explorer.metis.io/address'), | ||
'10': ChainInfo(name='OP Mainnet', block_explorer_link='https://optimistic.etherscan.io/address'), | ||
'56': ChainInfo(name='BNB Smart Chain',block_explorer_link='https://bscscan.com/address'), | ||
'100': ChainInfo(name='Gnosis', block_explorer_link='https://gnosisscan.io/address'), | ||
'534352':ChainInfo(name='Scroll', block_explorer_link='https://scrollscan.com/address'), | ||
'324': ChainInfo(name='zkSync Era', block_explorer_link='https://era.zksync.network/address'), | ||
'59144': ChainInfo(name='Linea', block_explorer_link='https://lineascan.build/') | ||
} | ||
|
||
|
||
def __extract_payload_addresses(session: requests.Session, chain_id: str, controller: str, payload_id: int) -> list[str]: | ||
resp = session.get(f'{BASE_BGD_CACHE_REPO}/{chain_id}/{controller}/payloads/{payload_id}.json') | ||
resp.raise_for_status() | ||
def get_aave_tags(proposal_id: int) -> Dict[str, Any]: | ||
""" | ||
Utility function that orchestrates calls to AaveGovernanceAPI | ||
and compiles the final dictionary of tags for a given proposal. | ||
|
||
Returns: | ||
A dictionary that can be directly rendered by your Jinja2 template. | ||
""" | ||
api = AaveGovernanceAPI() | ||
bgd_data: BGDProposalData = api.get_proposal_data(proposal_id) | ||
|
||
# Safely unwrap fields (some might be None). | ||
ipfs_data: IPFSData = bgd_data.ipfs or IPFSData() | ||
proposal_data: ProposalData = bgd_data.proposal or ProposalData() | ||
create_event: EventData = bgd_data.events[0] if bgd_data.events else EventData() | ||
Comment on lines
+46
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I never seen this syntax. Is it checking if the first expression is None and if so assigns the second expression? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, exactly. But must make sure bgd_data is not None. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's cool There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nivcertora You haven't addressed this yet |
||
|
||
# Construct an empty dictionary for the Jinja2 context | ||
tags: Dict[str, Any] = {} | ||
|
||
# Basic info | ||
tags['proposal_id'] = str(proposal_id) | ||
tags['proposal_title'] = ipfs_data.title if ipfs_data.title else 'N/A' | ||
tags['voting_link'] = f'https://vote.onaave.com/proposal/?proposalId={proposal_id}' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can default the relevant pydantic model to 'N/A'? If we're already leveraging pydantic let's remove all these little checks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a continuation to the comment above, don't we want to stick for similar expressions? |
||
tags['gov_forum_link'] = ipfs_data.discussions if ipfs_data.discussions else 'N/A' | ||
|
||
# Multi-chain references | ||
tags['chain'] = [] | ||
tags['payload_link'] = [] | ||
tags['payload_seatbelt_link'] = [] | ||
|
||
# Go through each payload in the proposal | ||
for p in proposal_data.payloads: | ||
# For each payload, retrieve the addresses from the API | ||
addresses = api.get_payload_addresses( | ||
chain_id = p.chain, | ||
controller = p.payloads_controller, | ||
payload_id = p.payload_id | ||
) | ||
|
||
# For each address, build up the chain/payload references | ||
for i, address in enumerate(addresses, 1): | ||
chain_info = AAVE_CHAIN_MAPPING.get(p.chain) | ||
if not chain_info: | ||
# If chain info is missing, skip | ||
continue | ||
|
||
payload_data = resp.json() | ||
chain_display = chain_info.name + (f' {i}' if i != 1 else '') | ||
tags['chain'].append(chain_display) | ||
|
||
return [a['target'] for a in payload_data['payload']['actions']] | ||
block_explorer_link = f'{chain_info.block_explorer_link}/{address}' | ||
tags['payload_link'].append(block_explorer_link) | ||
|
||
seatbelt_link = f'{SEATBELT_PAYLOADS_URL}/{p.chain}/{p.payloads_controller}/{p.payload_id}.md' | ||
tags['payload_seatbelt_link'].append(seatbelt_link) | ||
|
||
def get_aave_tags(proposal_id: int) -> dict: | ||
with requests.Session() as session: | ||
proposal_data_link = f'{PROPOSALS_URL}/{proposal_id}.json' | ||
resp = session.get(proposal_data_link) | ||
resp.raise_for_status() | ||
# Transaction info | ||
transaction_hash = create_event.transactionHash or 'N/A' | ||
tags['transaction_hash'] = transaction_hash | ||
tags['transaction_link'] = f'https://etherscan.io/tx/{transaction_hash}' | ||
|
||
proposal_data: dict = resp.json() | ||
# Creator + event args | ||
args = create_event.args | ||
tags['creator'] = args.creator if args.creator else 'N/A' | ||
tags['access_level'] = str(args.accessLevel) if args.accessLevel is not None else 'N/A' | ||
tags['ipfs_hash'] = args.ipfsHash if args.ipfsHash else 'N/A' | ||
|
||
ipfs: dict = proposal_data.get('ipfs', {}) | ||
proposal: dict = proposal_data.get('proposal', {}) | ||
create_event: dict = proposal_data.get('events', [{}])[0] # The create event is always the first. | ||
tags['createProposal_parameters_data'] = json.dumps(proposal_data.model_dump(), indent=4) | ||
|
||
tags = {} | ||
tags['proposal_id'] = str(proposal_id) | ||
tags['proposal_title'] = ipfs.get('title', 'N/A') | ||
tags['voting_link'] = f'https://vote.onaave.com/proposal/?proposalId={proposal_id}' | ||
tags['gov_forum_link'] = ipfs.get('discussions', 'N/A') | ||
# seatbelt link for entire proposal | ||
tags['seatbelt_link'] = f'{BASE_SEATBELT_REPO}/proposals/{proposal_id}.md' | ||
|
||
tags['chain'], tags['payload_link'], tags['payload_seatbelt_link'] = [], [], [] | ||
for p in proposal.get('payloads', []): | ||
# These are necessary fields in the payload data to construct the payload fields. | ||
if not all(k in p for k in ['chain', 'payloadsController', 'payloadId']): | ||
continue | ||
addresses = __extract_payload_addresses(session, p['chain'], p['payloadsController'], p['payloadId']) | ||
for i, address in enumerate(addresses, 1): | ||
tags['chain'].append(AAVE_CHAIN_MAPPING[p['chain']].name + (f' {i}' if i != 1 else '')) | ||
tags['payload_link'].append(f'{AAVE_CHAIN_MAPPING[p["chain"]].block_explorer_link}/{address}') | ||
tags['payload_seatbelt_link'].append( | ||
f'{SEATBELT_PAYLOADS_URL}/{p["chain"]}/{p["payloadsController"]}/{p["payloadId"]}.md' | ||
) | ||
|
||
tags['transaction_hash'] = create_event.get('transactionHash', 'N/A') | ||
tags['transaction_link'] = f'https://etherscan.io/tx/{tags["transaction_hash"]}' | ||
|
||
args: dict = create_event.get('args', {}) | ||
tags['creator'] = args.get('creator', 'N/A') | ||
tags['access_level'] = str(args.get('accessLevel', 'N/A')) | ||
tags['ipfs_hash'] = args.get('ipfsHash', 'N/A') | ||
|
||
tags['createProposal_parameters_data'] = json.dumps({k: proposal.get(k, 'N/A') for k | ||
in ['payloads', 'votingPortal', 'ipfsHash']}, indent=4) | ||
|
||
tags['seatbelt_link'] = f'{BASE_SEATBELT_REPO}/proposals/{proposal_id}.md' | ||
|
||
return tags | ||
return tags |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import Quorum.checks as Checks | ||
import Quorum.utils.pretty_printer as pp | ||
from Quorum.utils.chain_enum import Chain | ||
from Quorum.apis.block_explorers.chains_api import ChainAPI | ||
from Quorum.apis.price_feeds.price_feed_utils import PriceFeedProviderBase | ||
|
||
|
||
def proposals_check(customer: str, chain: Chain, proposal_addresses: list[str], providers: list[PriceFeedProviderBase]) -> None: | ||
""" | ||
Check and compare source code files for given proposals. | ||
|
||
This function handles the main logic of fetching source code from the remote repository. | ||
|
||
Args: | ||
customer (str): The customer name or identifier. | ||
chain_name (str): The blockchain chain name. | ||
proposal_addresses (list[str]): List of proposal addresses. | ||
providers (list[PriceFeedProviderInterface]): List of price feed providers. | ||
""" | ||
api = ChainAPI(chain) | ||
|
||
pp.pretty_print(f"Processing customer {customer}, for chain: {chain}", pp.Colors.INFO) | ||
for proposal_address in proposal_addresses: | ||
pp.pretty_print(f"Processing proposal {proposal_address}", pp.Colors.INFO) | ||
|
||
try: | ||
source_codes = api.get_source_code(proposal_address) | ||
except ValueError as e: | ||
error_message = ( | ||
f"Payload address {proposal_address} is not verified on {chain.name} explorer.\n" | ||
"We do not recommend to approve this proposal until the code is approved!\n" | ||
"Try contacting the proposer and ask them to verify the contract.\n" | ||
"No further checks are being performed on this payload." | ||
) | ||
pp.pretty_print(error_message, pp.Colors.FAILURE) | ||
# Skip further checks for this proposal | ||
continue | ||
|
||
# Diff check | ||
missing_files = Checks.DiffCheck(customer, chain, proposal_address, source_codes).find_diffs() | ||
|
||
# Review diff check | ||
Checks.ReviewDiffCheck(customer, chain, proposal_address, missing_files).find_diffs() | ||
|
||
# Global variables check | ||
Checks.GlobalVariableCheck(customer, chain, proposal_address, missing_files).check_global_variables() | ||
|
||
# Feed price check | ||
Checks.PriceFeedCheck(customer, chain, proposal_address, missing_files, providers).verify_price_feed() | ||
|
||
# New listing check | ||
Checks.NewListingCheck(customer, chain, proposal_address, missing_files).new_listing_check() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I don't really like here is that these models are directly tied to Aave's API but are not treated as such.
I would either put the file and
aave_governance.py
underQuorum/apis/governance/aave
or rename this one toaave_data_models.py