Skip to content

Uniswap/govkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

82 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GovKit

A lightweight kit for writing Uniswap Governance Proposals

At a Glance

import {Uniswap} from "lib/govkit/src/types/Uniswap.sol";
import {Proposal} from "lib/govkit/src/types/Proposal.sol";
import {Call, LibCall} from "lib/govkit/src/types/Call.sol";

// -----------------------------------------------------------------------------
// Initialize Uniswap protocol addresses.
//
Uniswap internal uniswap;
uniswap.loadLatest();

// -----------------------------------------------------------------------------
// Build proposal.
//
Proposal memory proposal = Proposal({
    description: "# Activate Fee Switch V2+V3 on Ethereum ....",
    calls: LibCall.newCalls([
        Call({
            target: uniswap.ethereum.v2Factory,
            value: 0,
            data: abi.encodeCall(
                IUniswapV2Factory.setFeeTo,
                (uniswap.ethereum.tokenJar)
            )
        }),
        Call({
            target: uniswap.ethereum.v3Factory,
            value: 0,
            data: abi.encodeCall(
                IUniswapV3Factory.setOwner,
                (uniswap.ethereum.v3OpenFeeAdapter)
            )
        })
    ])
});

// -----------------------------------------------------------------------------
// Export proposal to Governance Seatbelt.
//
string memory json = GovernanceSeatbelt.toJson({
    proposal: proposal,
    governorBravo: uniswap.ethereum.governorBravo
});

vm.writeFile("./seatbelt-example.json", json);

// -----------------------------------------------------------------------------
// Send proposal on GovernorBravo.
//
(
    address[] memory targets,
    uint256[] memory values,
    string[] memory signatures,
    bytes[] memory datas,
) = proposal.toGovernorBravoInputs();

IGovernorBravo(uniswap.ethereum.governorBravo).propose(
    targets,
    values,
    signatures,
    datas,
    description
);

Core Structures & Functionality

Proposal

Each proposal requires a series of calls, each containing a target, value, and data. GovernorBravo takes each of these as separate arrays, making proposal specification unwieldy. We define a data type which encapsulates these actions more cleanly, as well as provide a function to transform it into the input that GovernorBravo can accept.

Usage:

import {Proposal} from "lib/govkit/src/types/Proposal.sol";
import {Call, LibCall} from "lib/govkit/src/types/Call.sol";

Proposal memory proposal = Proposal({
    description: "Transfer <token> to <receiver>.",
    calls: LibCall.newCalls([
        Call({
            target: token,
            value: 0,
            data: abi.encodeCall(ERC20.transfer, (receiver, amount))
        })
    ])
});

(
    address[] memory targets,
    uint256[] memory values,
    string[] memory signatures,
    bytes[] memory datas,
    string memory description
) = actions.toGovernorBravoInputs();

Contract Addresses

Each action requires contract addresses across networks for calling. At the time of writing, this is fragmented with no importable source of truth. We implement a collection of data types and constants which scope, by name, contracts across networks in the protocol; which gives us portability across proposals instead of re-hard coding them each proposal.

Using uniswap.<network>.<name> returns the contract with the given <name> on the given <network>.

Using uniswap.ethereum.bridge.<target_network> returns the bridge sender on Ethereum that sends messages to the given <target_network>.

Usage:

import {Uniswap} from "lib/govkit/src/types/Uniswap.sol";
import {Proposal} from "lib/govkit/src/types/Proposal.sol";
import {Call, LibCall} from "lib/govkit/src/types/Call.sol";

Uniswap internal uniswap;
uniswap.loadLatest();

Proposal memory proposal = Proposal({
    description: "# Activate Fee Switch V2+V3 on Ethereum ....",
    calls: LibCall.newCalls([
        Call({
            target: uniswap.ethereum.v2Factory,
            value: 0,
            data: abi.encodeCall(
                IUniswapV2Factory.setFeeTo,
                (uniswap.ethereum.tokenJar)
            )
        }),
        Call({
            target: uniswap.ethereum.v3Factory,
            value: 0,
            data: abi.encodeCall(
                IUniswapV3Factory.setOwner,
                (uniswap.ethereum.v3OpenFeeAdapter)
            )
        })
    ])
});

This also makes testing environments more flexible for protocol mocking.

import {Uniswap} from "lib/govkit/src/types/Uniswap.sol";

contract MockGovernorBravo {
    // ...
}

Uniswap internal uniswap;

uniswap.ethereum.governorBravo = address(new MockGovernorBravo());

Chain Identifiers and Encoders

Each action that interacts with remote chains requires the target chain ID, the bridge used to send the message to the target chain, the target chain infrastructure to receive the message, and in some cases, it requires bridge-defined "chain identifier" values, which are unrelated to the "chain ID". We define constants and encoders for each bridge type.

Chain ID in the EIP-155 sense (the intuitive sense) are define together in a ChainId library, while bridge protocols' chain identifiers are defined together in their respective libraries as necessary. While each bridge's chain identifier has transformer functions to move between their chain identifiers and the canonical EIP-155 chain ID's, we will only expose the ChainId values in the top-level API. The appropriate transformations happen internally.

We define encoders for the following:

Network Library
Arbitrum src/bridges/InboxEncoder.sol
Avalanche src/bridges/WormholeEncoder.sol
Base src/bridges/L1CrossDomainMessengerEncoder.sol
Blast src/bridges/L1CrossDomainMessengerEncoder.sol
BnbChain src/bridges/WormholeEncoder.sol
Celo src/bridges/L1CrossDomainMessengerEncoder.sol
Ink See Below.
MegaETH src/bridges/WormholeEncoder.sol
Linea !! UNIMPLEMENTED !!
Monad src/bridges/WormholeEncoder.sol
Optimism src/bridges/L1CrossDomainMessengerEncoder.sol
Polygon src/bridges/FxRootEncoder.sol
RootStock src/bridges/WormholeEncoder.sol
Soneium src/bridges/L1CrossDomainMessengerEncoder.sol
Tempo src/bridges/WormholeEncoder.sol
UniChain src/bridges/OptimismPortal2Encoder.sol
WorldChain See Below.
XLayer See Below.
Zora src/bridges/L1CrossDomainMessengerEncoder.sol

Regarding Ink, WorldChain, and XLayer: On these networks, most of the protocol is owned by the OP-stack-aliased Timelock (from L1), which uses the OptimismPortal2 low level bridge for ownership. However, when the fee infrastructure was deployed, the V3OpenFeeAdapter becomes the owner of the UniswapV3Factory, but this adapter is owned by a newly deployed CrossChainAccount which uses the high level bridge on OP-stack chains.

So for these networks, use L1CrossDomainMessengerEncoder for the V3OpenFeeAdapter ONLY and use OptimismPortal2Encoder for the rest of the protocol.

Chain ID usage:

import {ChainId} from "lib/govkit/src/constants/ChainId.sol";
import {WormholeChainId} from "lib/govkit/src/constants/WormholeChainId.sol";

// EIP-155
//
// ChainId.Ethereum = 1
vm.chainId(ChainId.Ethereum);

// Wormhole's chain ID
//
// WormholeChainId.Ethereum = 2
WormholeChainId.Ethereum;

// Converters
assertEq(
    WormholeChainId.chainIdToWormholeChainId(
        ChainId.Ethereum
    ),
    WormholeChainId.Ethereum
);

assertEq(
    WormholeChainId.wormholeChainIdtoChainId(
        WormholeChainId.Ethereum
    ),
    ChainId.Ethereum
);

Encoder Usage:

Proposal memory proposal = Proposal({
    description: description,
    calls: LibCall.newCalls([
        // ---------------------------------------------------------------------
        // Set feeToSetter on Uniswap V2 factory on Celo.
        //
        L1CrossDomainMessengerEncoder.encode({
            l1CrossDomainMessenger: uniswap.ethereum.bridge.celo,
            crossChainAccount: uniswap.celo.crossChainAccount,
            remoteCall: Call({
                target: uniswap.celo.v2Factory,
                value: 0,
                data: abi.encodeCall(
                    IUniswapV2Factory.setFeeTo,
                    (uniswap.celo.tokenJar)
                )
            })
        }),
        // ---------------------------------------------------------------------
        // Set owner on Uniswap V3 factory on Celo.
        //
        L1CrossDomainMessengerEncoder.encode({
            l1CrossDomainMessenger: uniswap.ethereum.bridge.celo,
            crossChainAccount: uniswap.celo.crossChainAccount,
            remoteCall: Call({
                target: uniswap.celo.v3Factory,
                value: 0,
                data: abi.encodeCall(
                    IUniswapV3Factory.setOwner,
                    (uniswap.celo.v3OpenFeeAdapter)
                )
            })
        })
    ])
});

Seatbelt Handoff

Each action requires a handoff to Uniswap Foundation's "Governance Seatbelt" system, which performs rich multichain call validation. We implement an exporter which produces JSON that can be imported into the seatbelt program to be interpreted in a more uniform and automated way.

Usage:

import {Uniswap} from "lib/govkit/src/types/Uniswap.sol";
import {Proposal} from "lib/govkit/src/types/Proposal.sol";
import {GovernanceSeatbelt} from "lib/govkit/src/forge/GovernanceSeatbelt.sol";

Uniswap internal uniswap;
uniswap.loadLatest();
Proposal memory proposal = Proposal({
    description: "Burn 20 UNI.",
    calls: LibCall.newCalls([
        Call({
            target: uniswap.ethereum.uni,
            value: 0,
            data: abi.encodeCall(ERC20.transfer, (address(0xdead), 20e18))
        })
    ])
});

vm.writeJson(
    "./prop-100.json",
    GovernanceSeatbelt.toJson(proposal, uniswap.ethereum.governorBravo)
);

Output (./prop-100.json):

{
    "type": "new",
    "daoName": "Uniswap",
    "governorAddress": "0x408ED6354d4973f66138C91495F2f2FCbd8724C3",
    "governorType": "bravo",
    "targets": [
        "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
    ],
    "values": [
        0
    ],
    "signatures": [
        ""
    ],
    "calldatas": [
        "0xa9059cbb000000000000000000000000000000000000000000000000000000000000dead000000000000000000000000000000000000000000000001158e460913d00000"
    ],
    "description": "Burn 20 UNI.",
}

Contract Logging

Some actions require prerequisite deployments and configurations before the proposal can run. So far we have primarily relied purely on Foundry's scripting output files (broadcast/) or Foundry's console logging contract to get newly deployed addresses. In short, Foundry's native broadcast handling is fragile, unwieldy, and its internal API for interacting with it is broken. We implement a data type which can record contract addresses, names, and chain ID's to disk in a clean and easy to interpret JSON file, stored in .records/.

We also optionally allow for a debugMode, which performs excessive logging before any VM-related action which may fail. This makes it clearer where things fail if they do and should fill in contextual gaps for proposal writers in the future.

Usage:

import {Recorder} from "lib/govkit/src/forge/Recorder.sol";

Recorder internal recorder;

// Initialize with script name.
//
// recorder.debugMode uses excessive STDOUT logging in case things go wrong.
recorder.initialize({
    scriptName: "MyScript",
    debugMode: true
});

// Deploy the contract and record it.
//
// `write` returns the address as a passthrough for ergonomics.
address myContract = recorder.write(
    ChainId.Ethereum,
    "MyContract", 
    address(new MyContract())
);

// Load the contract's address.
//
// This can be done in any subsequent script.
//
address loadedMyContract = recorder.read(ChainId.Ethereum, "MyContract");

// (Optional): Clear the record.
//
recorder.clear();

Output (.records/MyScript.json):

{
    "1": {
        "MyContract": "0x.."
    }
}

We can also use this to conditionally deploy contracts, which helps with mid-script failure recovery.

Usage:

import {Recorder} from "lib/govkit/src/forge/Recorder.sol";

Recorder internal recorder;

if (!recorder.exists(ChainId.Ethereum, "MyContract")) {
    recorder.write(
        ChainId.Ethereum,
        "MyContract", 
        address(new MyContract())
    );
}

ERC1967 Proxy

At times, proxy contracts must be used and dealt with, primarily those of ERC-1967. We provide a lightweight utility for handling these. The ERC1967 library is independent of Foundry, it contains only the constants, while ERC1967Reader is a Foundry-specific reader, using its Vm to load the relevant addresses.

Usage:

import {Uniswap} from "lib/govkit/src/types/Uniswap.sol";
import {ERC1967Reader} from "lib/govkit/src/forge/ERC1967Reader.sol";

Uniswap internal uniswap;
uniswap.loadLatest();

address proxy = uniswap.ethereum.bridge.bnbChain;

address admin = ERC1967Reader.admin(proxy);
address beacon = ERC1967Reader.beacon(proxy);
address implementation = ERC1967Reader.implementation(proxy);

About

Lightweight Solidity Kit for Governance Proposals

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors