diff --git a/.gitignore b/.gitignore index 4ef2f3bc..1ccc6225 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,6 @@ node_modules audit_owners_mapper.py data/Convex_EPS data/badger/acl_roles_audit +data/badger/governance_audit logs/ great_ape_safe/ape_api/helpers/balancer/pools.json diff --git a/helpers/addresses.py b/helpers/addresses.py index c73be5ea..6da57083 100644 --- a/helpers/addresses.py +++ b/helpers/addresses.py @@ -76,6 +76,8 @@ "ops_multisig_old": "0x576cD258835C529B54722F84Bb7d4170aA932C64", "treasury_ops_multisig": "0x042B32Ac6b453485e357938bdC38e0340d4b9276", "treasury_vault_multisig": "0xD0A7A8B98957b9CD3cFB9c0425AbE44551158e9e", + "community_council_multisig": "0x1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a", + "dev_multisig_backup": "0x2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b", "ibbtc_multisig": "0xB76782B51BFf9C27bA69C77027e20Abd92Bcf3a8", "treasury_voter_multisig": "0xA9ed98B5Fb8428d68664f3C5027c62A10d45826b", "payments_multisig": "0x30a9c1D258F6c2D23005e6450E72bDD42C541105", diff --git a/interfaces/badger/IController.sol b/interfaces/badger/IController.sol index 893358b7..41001f2d 100644 --- a/interfaces/badger/IController.sol +++ b/interfaces/badger/IController.sol @@ -20,6 +20,8 @@ interface IController { function setVault(address, address) external; + function setGovernance(address _governance) external; + function setRewards(address) external; function want(address) external view returns (address); diff --git a/interfaces/badger/IInfraPermissions.sol b/interfaces/badger/IInfraPermissions.sol new file mode 100644 index 00000000..2f357291 --- /dev/null +++ b/interfaces/badger/IInfraPermissions.sol @@ -0,0 +1,15 @@ +pragma solidity >=0.7.0 <0.9.0; + +interface IInfraPermissions { + function strategist() external view returns (address); + + function governance() external view returns (address); + + function owner() external view returns (address); + + function manager() external view returns (address); + + function admin() external view returns (address); + + function guardian() external view returns (address); +} \ No newline at end of file diff --git a/scripts/badger/dev_multisig_progression.py b/scripts/badger/dev_multisig_progression.py new file mode 100644 index 00000000..67d528b4 --- /dev/null +++ b/scripts/badger/dev_multisig_progression.py @@ -0,0 +1,179 @@ +from great_ape_safe import GreatApeSafe +from helpers.addresses import r +from brownie import interface +from rich.console import Console + +C = Console() + +""" +The following set of scripts are meant to perform key actions to progress the Dev Multisig +into a more decentralized state. These actions were defined based on the observations obtained from the +governance and upgradeability access audit. +""" + +# Actors involved +DEV = r.badger_wallets.dev_multisig +TECHOPS = r.badger_wallets.techops_multisig +TREASURY = r.badger_wallets.treasury_vault_multisig +COMMUNITY = r.badger_wallets.community_council_multisig +BACKUP = r.badger_wallets.dev_multisig_backup + +# Contracts involved +ACL = [ + r.GlobalAccessControl, + r.guardian, + r.keeperAccessControl, + r.rewardsLogger, + r.badger_wallets.badgertree, +] + +TIMELOCK = r.governance_timelock + +# Controllers and vaults not governed by the Timelock +CONTROLLERS = [ + "bbveCVX-CVX-f", + "ibBTCCrv", + "restitution", + "bcrvBadger", +] +VAULTS = [ + "bcrvRenBTC", + "bcrvIbBTC", + "bcvxCRV", + "bveCVX", + "bbveCVX_CVX_f", + "remBADGER", + "remDIGG", + "bcrvBadger", + "graviAURA", + "bauraBal", + "b80BADGER_20WBTC", + "b40WBTC_40DIGG_20graviAURA", + "bBB_a_USD", + "b33auraBAL_33graviAURA_33WETH", + "bB_stETH_STABLE", + "bB_rETH_STABLE", +] + +# Constants +DEFAULT_ADMIN_ROLE = ( + "0x0000000000000000000000000000000000000000000000000000000000000000" +) +SENTINEL_OWNERS = "0x0000000000000000000000000000000000000001" + +FINAL_DEV_MULTISG_STATE = [ + TREASURY, + TECHOPS, + COMMUNITY, + BACKUP, +] + +FINAL_POLICY = 3 + + +def step_1_1(): + """ + - Transfer the DEFAULT_ADMIN_ROLE for GlobalAccessControl, Guardian, KeeperACL, RewardsLogger, + and BadgerTree from the Dev Multisig to the Badger TechOps multisig. + """ + + dev = GreatApeSafe(DEV) + + for acl_address in ACL: + acl = dev.contract(acl_address, Interface=interface.IAccessControl) + # Confirm that the Dev multisig holds the DEFAULT_ADMIN_ROLE (and nobody else does) + assert acl.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 1 + assert acl.hasRole(DEFAULT_ADMIN_ROLE, DEV) + # Grant role to TechOps (Dev multisig keeps it to simplify things) + C.print(f"Granting admin role to TechOps on {acl_address}") + acl.grantRole(DEFAULT_ADMIN_ROLE, TECHOPS) + assert acl.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 2 + assert acl.hasRole(DEFAULT_ADMIN_ROLE, TECHOPS) + + dev.post_safe_tx() + + +def step_1_2(): + """ + - Transfer governance of the remaining vaults and controllers, including remBADGER, from the Dev Multisig to + the Governance Timelock (for vaults not already governed by the Timelock). + """ + + dev = GreatApeSafe(DEV) + + # Confirm governance is held by the Dev Multisig and transfer it to the Timelock for each of the vaults + for vault_ID in VAULTS: + vault = dev.contract(r.sett_vaults[vault_ID], interface.ISettV4h) + assert vault.governance() == DEV + C.print(f"Transfering governance to Timelock on vault: {vault_ID}") + vault.setGovernance(TIMELOCK) + assert vault.governance() == TIMELOCK + + # Confirm governance is held by the Dev Multisig and transfer it to the Timelock for each of the controllers + for controller_ID in CONTROLLERS: + controller = dev.contract(r.controllers[controller_ID], interface.IController) + assert controller.governance() == DEV + C.print(f"Transfering governance to Timelock on controller: {controller_ID}") + controller.setGovernance(TIMELOCK) + assert controller.governance() == TIMELOCK + + dev.post_safe_tx() + + +def step_2(): + """ + Replace Dev Multisig signers with the following multisigs and change its policy to 3/4: + - Treasury Vault + - Badger TechOps + - Community Council + - Security Backup (Third party security partner) + """ + + dev = GreatApeSafe(DEV) + dev_multisig = interface.IGnosisSafe_v1_3_0(DEV, owner=dev.account) + + dev_current = dev_multisig.getOwners() + + unique_to_current = list(set(dev_current).difference(set(FINAL_DEV_MULTISG_STATE))) + unique_to_final = list(set(FINAL_DEV_MULTISG_STATE).difference(set(dev_current))) + + # Swap out any unique addresses to the Dev Multisig + for i in range(len(unique_to_final)): + C.print(f"Swapping {unique_to_current[i]} for {unique_to_final[i]}...") + + dev_multisig.swapOwner( + get_previous_owner(dev_multisig, unique_to_current[i]), + unique_to_current[i], + unique_to_final[i], + ) + + # Remove any outstanding owners + dev_current = dev_multisig.getOwners() + for owner in dev_current: + if owner not in FINAL_DEV_MULTISG_STATE: + C.print(f"Removing {owner}...") + dev_multisig.removeOwner( + get_previous_owner(dev_multisig, owner), owner, FINAL_POLICY + ) + + # Confirm all owners + for owner in dev_multisig.getOwners(): + assert owner in FINAL_DEV_MULTISG_STATE + assert len(dev_multisig.getOwners()) == len(FINAL_DEV_MULTISG_STATE) + assert dev_multisig.getThreshold() == FINAL_POLICY + + C.print(f"\nNew Owners at dev_multisig Multisig:") + C.print(f"[green]{dev_multisig.getOwners()}[/green]\n") + C.print(f"[green]{dev_multisig.getThreshold()}[/green]\n") + + dev.post_safe_tx() + + +def get_previous_owner(dev, owner): + owners = dev.getOwners() + for i in range(len(owners)): + if owners[i] == owner: + if i == 0: + return SENTINEL_OWNERS + else: + return owners[i - 1] diff --git a/scripts/badger/governance_audit.py b/scripts/badger/governance_audit.py new file mode 100644 index 00000000..33e4401a --- /dev/null +++ b/scripts/badger/governance_audit.py @@ -0,0 +1,181 @@ +from helpers.addresses import r, reverse +from brownie import interface +from rich.console import Console +from rich.progress import track +import pandas as pd +from tabulate import tabulate +from brownie.exceptions import VirtualMachineError +from datetime import datetime + +C = Console() + +ACCOUNT = "0xB65cef03b9B89f99517643226d76e286ee999e77" + +VAULTS = r.sett_vaults +STRATEGIES = { + k: v for k, v in r.strategies.items() if k != "_deprecated" +} # Filter out _deprecated group +CONTROLLERS = { + k: v for k, v in r.controllers.items() if k != "dummy" +} # Filter out dummy controller + +PERMISSIONED_FUNCTIONS = [ + "governance", + "strategist", + "manager", + "owner", + "admin", + "guardian", +] + +INFRASTRUCTURE_TAGS = [ + "governance_timelock", + "governance_timelock_veto", + "rewardsLogger", + "EmissionControl", + "registry", + "registry_v2", + "registryAccessControl", + "keeperAccessControl", + "guardian", + "GatedMiniMeController", + "GlobalAccessControl", + "harvest_forwarder", + "badger_geyser", + "slp_geyser", + "aragon_voting", + "badger_voting_shares", + "brickedProxyAdmin", + "brembadger", + "digg_monetary_policy", +] + +WALLETS_TAGS = [ + "badgertree", + "native_autocompounder", + "badgerhunt", + "DAO_treasury", + "rewards_escrow", + "devProxyAdmin", + "devUngatedProxyAdmin", + "testProxyAdmin", + "techOpsProxyAdmin", + "opsProxyAdmin_old", + "mStableSharedProxyAdmin", + "rewardsEscrow", + "gas_station", + "upkeep_manager", +] + + +def main(): + check_permissions() + + +def check_permissions(): + # Combined data storage + permissions_data = [] + + # Check permissions for Vaults with progress bar + C.print("[bold green]Processing Vaults...[/bold green]") + for key, address in track(VAULTS.items(), description="Checking Vaults"): + vault = interface.IInfraPermissions(address) + permissions_data.extend( + check_individual_permissions(key, address, "vault", vault) + ) + + # Check permissions for Strategies with progress bar + C.print("[bold green]Processing Strategies...[/bold green]") + for key, address in track(STRATEGIES.items(), description="Checking Strategies"): + strategy = interface.IInfraPermissions(address) + permissions_data.extend( + check_individual_permissions(key, address, "strategy", strategy) + ) + + # Check permissions for Controllers with progress bar + C.print("[bold green]Processing Controllers...[/bold green]") + for key, address in track(CONTROLLERS.items(), description="Checking Controllers"): + controller = interface.IInfraPermissions(address) + permissions_data.extend( + check_individual_permissions(key, address, "controller", controller) + ) + + # Check permissions for Infrastructure with progress bar + C.print("[bold green]Processing Infrastructure...[/bold green]") + for key in track(INFRASTRUCTURE_TAGS, description="Checking Infrastructure"): + address = r[key] + infra_contract = interface.IInfraPermissions(address) + permissions_data.extend( + check_individual_permissions(key, address, "infrastructure", infra_contract) + ) + + # # Check permissions for Wallets with progress bar + C.print("[bold green]Processing Wallets...[/bold green]") + for key in track(WALLETS_TAGS, description="Checking Wallets"): + address = r.badger_wallets[key] + wallet_contract = interface.IInfraPermissions(address) + permissions_data.extend( + check_individual_permissions(key, address, "wallet", wallet_contract) + ) + + # Convert to DataFrame + permissions_df = pd.DataFrame( + permissions_data, + columns=[ + "Key", + "Address", + "Type", + "Permission", + "Actor", + "Actor Name", + "Matches ACCOUNT", + ], + ) + + # Output the table + merged_table = tabulate( + permissions_df, + headers=[ + "Key", + "Address", + "Type", + "Permission", + "Actor", + "Actor Name", + "Matches ACCOUNT", + ], + tablefmt="fancy_grid", + ) + C.print(merged_table) + + # Generate the CSV file name + date_str = datetime.now().strftime("%Y%m%d") + filename = f"data/badger/governance_audit/governance_audit_{date_str}_{ACCOUNT}.csv" + + # Save DataFrame to CSV + permissions_df.to_csv(filename, index=False) + C.print(f"[bold green]CSV file saved as {filename}[/bold green]") + + +def check_individual_permissions(key, address, contract_type, contract): + data = [] + + for permission in PERMISSIONED_FUNCTIONS: + try: + func = getattr(contract, permission) + actor = func() + data.append( + [ + key, + address, + contract_type, + permission, + actor, + reverse[actor] if actor in reverse else "Unknown", + actor == ACCOUNT, + ] + ) + except (AttributeError, ValueError, VirtualMachineError): + pass # If the function does not exist or reverts, continue + + return data diff --git a/scripts/badger/upgradeability_audit.py b/scripts/badger/upgradeability_audit.py new file mode 100644 index 00000000..88eb557c --- /dev/null +++ b/scripts/badger/upgradeability_audit.py @@ -0,0 +1,169 @@ +from helpers.addresses import r, reverse +from brownie import web3 +from rich.console import Console +from rich.progress import track +import pandas as pd +from tabulate import tabulate +from datetime import datetime + +C = Console() + +ACCOUNT = "0xB65cef03b9B89f99517643226d76e286ee999e77" + +VAULTS = r.sett_vaults +STRATEGIES = { + k: v for k, v in r.strategies.items() if k != "_deprecated" +} # Filter out _deprecated group +CONTROLLERS = { + k: v for k, v in r.controllers.items() if k != "dummy" +} # Filter out dummy controller + +INFRASTRUCTURE_TAGS = [ + "governance_timelock", + "governance_timelock_veto", + "rewardsLogger", + "EmissionControl", + "registry", + "registry_v2", + "registryAccessControl", + "keeperAccessControl", + "guardian", + "GatedMiniMeController", + "GlobalAccessControl", + "harvest_forwarder", + "badger_geyser", + "slp_geyser", + "aragon_voting", + "badger_voting_shares", + "brickedProxyAdmin", + "brembadger", + "digg_monetary_policy", +] + +WALLETS_TAGS = [ + "badgertree", + "native_autocompounder", + "badgerhunt", + "DAO_treasury", + "rewards_escrow", + "devProxyAdmin", + "devUngatedProxyAdmin", + "testProxyAdmin", + "techOpsProxyAdmin", + "opsProxyAdmin_old", + "mStableSharedProxyAdmin", + "rewardsEscrow", + "gas_station", + "upkeep_manager", +] + + +def main(): + check_upgradeability() + + +def check_upgradeability(): + # Combined data storage + upgradeability_data = [] + + # Check upgradeability for Vaults with progress bar + C.print("[bold green]Checking Upgradeability for Vaults...[/bold green]") + for key, address in track(VAULTS.items(), description="Checking Vaults"): + upgradeability_data.append(check_admin_upgradeability(key, address, "vault")) + + # Check upgradeability for Strategies with progress bar + C.print("[bold green]Checking Upgradeability for Strategies...[/bold green]") + for key, address in track(STRATEGIES.items(), description="Checking Strategies"): + upgradeability_data.append(check_admin_upgradeability(key, address, "strategy")) + + # Check upgradeability for Controllers with progress bar + C.print("[bold green]Checking Upgradeability for Controllers...[/bold green]") + for key, address in track(CONTROLLERS.items(), description="Checking Controllers"): + upgradeability_data.append( + check_admin_upgradeability(key, address, "controller") + ) + + # Check upgradeability for Infrastructure with progress bar + C.print("[bold green]Checking Upgradeability for Infrastructure...[/bold green]") + for key in track(INFRASTRUCTURE_TAGS, description="Checking Infrastructure"): + address = r[key] + upgradeability_data.append( + check_admin_upgradeability(key, address, "infrastructure") + ) + + # Check upgradeability for Wallets with progress bar + C.print("[bold green]Checking Upgradeability for Wallets...[/bold green]") + for key in track(WALLETS_TAGS, description="Checking Wallets"): + address = r.badger_wallets[key] + upgradeability_data.append(check_admin_upgradeability(key, address, "wallet")) + + # Convert to DataFrame + upgradeability_df = pd.DataFrame( + upgradeability_data, + columns=[ + "Key", + "Address", + "Type", + "Admin", + "Admin Name", + "Matches ACCOUNT", + ], + ) + + # Tabulate and print the results + merged_table = tabulate( + upgradeability_df, + headers=[ + "Key", + "Address", + "Type", + "Admin", + "Admin Name", + "Matches ACCOUNT", + ], + tablefmt="fancy_grid", + ) + C.print(merged_table) + + # Generate the CSV file name + date_str = datetime.now().strftime("%Y%m%d") + filename = ( + f"data/badger/governance_audit/upgradeablilty_audit_{date_str}_{ACCOUNT}.csv" + ) + + # Save DataFrame to CSV + upgradeability_df.to_csv(filename, index=False) + C.print(f"[bold green]CSV file saved as {filename}[/bold green]") + + +def check_admin_upgradeability(key, address, contract_type): + try: + admin_storage_slot = web3.eth.getStorageAt( + address, + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103", + ).hex() + + if admin_storage_slot in [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0x", + ]: + admin = "Not a proxy" + else: + admin = web3.toChecksumAddress( + f"0x{admin_storage_slot[-40:]}" + ) # Extract admin address + except ValueError: + admin = "Not a proxy" + + return [ + key, + address, + contract_type, + admin, + reverse[admin] if admin in reverse else "Unknown", + admin == ACCOUNT, + ] + + +if __name__ == "__main__": + main()