Skip to content

Commit

Permalink
CERT 8125 Support Multisig Address Check (#83)
Browse files Browse the repository at this point in the history
* CERT 8125 Support Multisig Address Check

* Fix hashable multisig data

* Add multisig check test for regression

* Address Yoav CR

* Update lock to mitigate the https://github.com/Certora/Quorum/security/dependabot/1 Vulnerable
  • Loading branch information
nivcertora authored Feb 12, 2025
1 parent 7ddc23d commit eb535a3
Show file tree
Hide file tree
Showing 6 changed files with 878 additions and 654 deletions.
1,434 changes: 782 additions & 652 deletions poetry.lock

Large diffs are not rendered by default.

Empty file.
49 changes: 49 additions & 0 deletions src/quorum/apis/multisig/safe_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import requests
from pydantic import BaseModel

from quorum.utils.singleton import singleton


class MultisigData(BaseModel):
"""
MultisigData is a Pydantic model for Safe Multisig data.
"""

class Config:
frozen = True

address: str
owners: list[str]
threshold: int

def __hash__(self) -> int:
return hash((self.address, tuple(self.owners), self.threshold))


@singleton
class SafeAPI:
"""
SafeAPI is a class designed to interact with the Safe Multisig API.
It fetches and stores data for various Safe Multisig contracts.
"""

SAFE_API_URL = "https://safe-transaction-mainnet.safe.global/api/v1/safes"

def __init__(self):
self.session = requests.Session()

def get_multisig_info(self, address: str) -> MultisigData | None:
"""
Get Safe Multisig data for a given address.
Args:
address (str): The contract address of the Safe Multisig.
Returns:
MultisigData: The Safe Multisig data for the specified address, or None if not found.
"""
response = self.session.get(f"{self.SAFE_API_URL}/{address}")
if not response.ok:
return None
data = response.json()
return MultisigData(**data)
25 changes: 24 additions & 1 deletion src/quorum/checks/price_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import quorum.utils.pretty_printer as pp
from quorum.apis.block_explorers.source_code import SourceCode
from quorum.apis.multisig.safe_api import MultisigData, SafeAPI
from quorum.apis.price_feeds import PriceFeedData, PriceFeedProviderBase
from quorum.checks.check import Check
from quorum.utils.chain_enum import Chain
Expand Down Expand Up @@ -36,6 +37,7 @@ def __init__(
providers (list[PriceFeedProviderInterface]): A list of price feed providers to be used for verification.
"""
super().__init__(customer, chain, proposal_address, source_codes)
self.multisig_api = SafeAPI()
self.address_pattern = r"0x[a-fA-F0-9]{40}"
self.price_feed_providers = price_feed_providers
self.token_providers = token_providers
Expand Down Expand Up @@ -83,6 +85,7 @@ def verify_price_feed(self) -> None:
"""
verified_price_feeds: set[PriceFeedCheck.PriceFeedResult] = set()
verified_tokens: set[PriceFeedCheck.PriceFeedResult] = set()
verified_multisigs: set[MultisigData] = set()
unverified_addresses: set[str] = set()

# Iterate through each source code file to find and verify address variables
Expand All @@ -106,14 +109,20 @@ def verify_price_feed(self) -> None:
elif res := self.__check_address(address, self.token_providers):
verified_variables.append(res.price_feed.model_dump())
verified_tokens.add(res)
elif res := self.multisig_api.get_multisig_info(address):
verified_variables.append(res.model_dump())
verified_multisigs.add(res)
else:
unverified_addresses.add(address)

if verified_variables:
self._write_to_file(verified_sources_path, verified_variables)

num_addresses = (
len(verified_price_feeds) + len(verified_tokens) + len(unverified_addresses)
len(verified_price_feeds)
+ len(verified_tokens)
+ len(verified_multisigs)
+ len(unverified_addresses)
)
pp.pprint(
f"{num_addresses} addresses identified in the payload.\n", pp.Colors.INFO
Expand Down Expand Up @@ -148,6 +157,20 @@ def verify_price_feed(self) -> None:
)
pp.pprint(msg, pp.Colors.SUCCESS)

# Print multisig validation
pp.pprint("Multisig Validation", pp.Colors.INFO, pp.Heading.HEADING_3)
msg = (
f"{len(verified_multisigs)}/{num_addresses} "
"were identified as multisig wallets:\n"
)
for i, var_res in enumerate(verified_multisigs, 1):
msg += (
f"\t{i}. {var_res.address}\n"
f"\t Owners: {', '.join(var_res.owners)}\n"
f"\t Threshold: {var_res.threshold} / {len(var_res.owners)}\n"
)
pp.pprint(msg, pp.Colors.SUCCESS)

# Print not found
msg = (
f"{len(unverified_addresses)}/{num_addresses} "
Expand Down
3 changes: 2 additions & 1 deletion src/quorum/tests/regression.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"Aave": {
"Ethereum": {
"Proposals": [
"0xAD6c03BF78A3Ee799b86De5aCE32Bb116eD24637"
"0xAD6c03BF78A3Ee799b86De5aCE32Bb116eD24637",
"0xec89aED62B364816e965D27f658163B8877d7D34" // For testing mutlisig address
]
},
"Arbitrum": {
Expand Down
21 changes: 21 additions & 0 deletions src/quorum/tests/test_mutisig_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from quorum.apis.multisig.safe_api import SafeAPI


def test_safe_api():
api = SafeAPI()
multisig = api.get_multisig_info("0xA1c93D2687f7014Aaf588c764E3Ce80aF016229b")
assert multisig.owners == [
"0x320A4e54e3641A7a9dAF47016a93CDe6F848A340",
"0xb647055A9915bF9c8021a684E175A353525b9890",
"0x6efa225841090Fb54d7bCE4593c700C0f24C4be8",
"0x329c54289Ff5D6B7b7daE13592C6B1EDA1543eD4",
"0x009d13E9bEC94Bf16791098CE4E5C168D27A9f07",
]
assert multisig.threshold == 3
assert multisig.address == "0xA1c93D2687f7014Aaf588c764E3Ce80aF016229b"


def test_non_multisig():
api = SafeAPI()
non_multisig = api.get_multisig_info("0x0000000000000000000000000000000000000000")
assert non_multisig is None

0 comments on commit eb535a3

Please sign in to comment.