Follow these steps to set up your local environment for development:
- Install foundry
- Install dependencies:
forge install
- Install pre-commit
- Install pre commit hooks:
pre-commit install
Follow the installation steps to enable pre-commit hooks. To ensure consistency in our formatting pre-commit
is used to check whether code was formatted properly and the documentation is up to date. Whenever a commit does not meet the checks implemented by pre-commit, the commit will fail and the pre-commit checks will modify the files to make the commits pass. Include these changes in your commit for the next commit attempt to succeed. On pull requests the CI checks whether all pre-commit hooks were run correctly.
This repo includes the following pre-commit hooks that are defined in the .pre-commit-config.yaml
:
mixed-line-ending
: This hook ensures that all files have the same line endings (LF).format
: This hook usesforge fmt
to format all Solidity files.prettier
: All remaining files are formatted using prettier.
In order for a PR to be merged, it must pass the following requirements:
- All commits within the PR must be signed
- CI must pass (tests, linting, etc.)
- New features must be merged with associated tests
- Bug fixes must have a corresponding test that fails without the fix
- The PR must be approved by at least one maintainer
- The PR must be approved by 2+ maintainers if the PR is a new feature or > 100 LOC changed
Run git submodule add https://github.com/Uniswap/<repository-name> src/pkgs/<repository-name>
In the foundry.toml
file, first create a profile with the compiler settings.
additional_compiler_profiles = [
...
{ name = "<repository-name>", optimizer_runs = <optimizer-runs>, via_ir = <via_ir>, ... },
]
Next, add the compilation restrictions to use the new profile. Compilation restrictions define what compiler profiles can be used to compile individual files. The restrictions should be defined in a way so that the main contracts used for deployments from the new repository can only be compiled with the newly added profile from this step to ensure consistent deployments.
compilation_restrictions = [
...
{ paths = "src/pkgs/<repository-name>/src/**", version = "<version>", optimizer_runs = <optimizer-runs>, via_ir = <via_ir>, evm_version = <evm_version> },
]
The version
should be fixed (e.g., =0.8.29
) to ensure that foundry does not compile the package with a different version.
Should other packages depend on interfaces of the new package, to ensure that the interfaces can also be compiled with other versions, the path should exclude interfaces from the compilation restrictions. E.g., paths = "src/pkgs/<repository-name>/src/**/[!i]*.sol"
Should the package include libraries other packages depend on, multiple compilation restrictions should be added to ensure that the compilation restrictions do not interfere with the compilation restrictions of other packages. E.g.,
{ paths = "src/pkgs/<repository-name>/src/**/libraries/**", version = "<0.9.0", optimizer_runs = <optimizer_runs>, via_ir = <via_ir> },
{ paths = "src/pkgs/<repository-name>/src/*.sol", version = "0.8.26", ... },
In the remappings.txt file, add the remappings for the dependencies of the new package.
src/pkgs/<repository-name>:dependency1=src/pkgs/<repository-name>/lib/dependency1
src/pkgs/<repository-name>:dependency2=src/pkgs/<repository-name>/lib/dependency2
Run ./script/util/create_briefcase.sh
to generate the briefcase files for the new package.
Create a new deployer in the src/briefcase/deployers
folder.
The file should be located in the directory of the package of the contract. The name of the file should be the name of the contract with the Deployer.sol
suffix:
src/briefcase/deployers/<package-name>/<contract-name>Deployer.sol
The structure of the file should be as follows:
// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0;
import {<ContractInterface>} from '../../protocols/<package-name>/interfaces/<ContractInterface>.sol';
library <ContractName>Deployer {
function deploy(address <arg1>, uint256 <arg2>) internal returns (<ContractInterface> contract) {
bytes memory args = abi.encode(<arg1>, <arg2>);
bytes memory initcode_ = abi.encodePacked(initcode(), args);
assembly {
contract := create(0, add(initcode_, 32), mload(initcode_))
}
}
/**
* @dev autogenerated - run `./script/util/create_briefcase.sh` to generate current initcode
*
* @notice This initcode is generated from the following contract:
* - Source Contract: src/pkgs/<package-name>/src/<contract-name>.sol
*/
function initcode() internal pure returns (bytes memory) {
return hex'';
}
}
The deploy function can run arbitrary logic to deploy the contract, e.g., deploying via create, create2, using a factory, or a proxy. The deployer function should return the address and interface of the deployed contract so it can be called in subsequent steps.
After creating the deployer file, it's important to update the Source Contract
path in the comment above the initcode
function. This ensures that the correct contract is used for the initcode.
Finally, run ./script/util/create_briefcase.sh
to generate the initcode for the deployer and populate the bytecode in the initcode function.
Modify the script/deploy/tasks/task_template.json
file to add the new contract to the task template. The contract can either be added to a new protocol or to an existing protocol where appropriate.
"protocols": {
...,
"<protocol-name>": {
"name": "Permit 2", // The name that will be displayed in the deploy-cli for that protocol
"deploy": false, // deploy is false by default
"contracts": {
... // contracts that can be deployed for that protocol
},
}
}
"contracts": {
"<contract-name>": {
"deploy": false, // deploy is false by default
"address": null, // address is null by default
"params": {
... // parameters for the contract deployment
},
"lookup": {
... // optional, lookup information for the contract
},
"dependencies": [
... // optional, dependencies for the contract
]
}
}
Params
The params object allows the deployer tool to pass arguments to the deployment.
"params": {
"<arg-name>": {
"type": "<type>", // e.g., uint256, address, bool, etc.
"name": "<name>", // optional, if provided it will be displayed to the user instead of the arg-name
"value": "<value>", // optional, if provided it will be prompted to the user as a default value
"pointer": "protocols.<protocol-name>.contracts.<contract-name>" // optional, if provided it will be used to resolve the value at runtime
}
}
If a value is provided, it will be displayed to the user as a default value, the user can then press enter to use the default value or provide a new value.
A pointer allows to dynamically resolve the value of the argument at runtime. This is primarily used to resolve the address of contracts that are deployed in the same run in prior steps. For example, when deploying Uniswap v2, the address of the Uniswap v2 factory needs to be resolved at runtime within the deployment of the Uniswap v2 router. If the v2 factory is deployed in the same run, the pointer would then point to the address of the newly deployed factory. If the v2 factory is not deployed, the user will be prompted to provide the address of the factory.
Lookup
The lookup object allows the deployer tool to find past deployments of the contract in the deployment logs located in deployments/json/<chain-id>.json
. If a contract has been found there, the deployer tool will display the address to the user as a quick selection option for convenience. For example, when deploying the UniversalRouter, where Permit2 is a constructor argument, the lookup object can provide the location of past Permit2 deployments to the user for that chain.
It can either point at the address of the latest deployment of the contract or a point in time it was used as a constructor argument in the past.
"lookup": {
"latest": "<contract-name>",
"history": ["<other-contract>.input.constructor.params.<param-name>"]
}
Dependencies
The dependencies array specifies other external contracts that are required to deploy the current contract but are not deployable by the deployer tool (e.g., WETH).
Add the deployer to the deployment script used by the deploy-cli.
script/deploy/Deploy-all.s.sol
function deploy<ProtocolName>() private {
if (!config.readBoolOr('.protocols.<protocol-name>.deploy', false)) return;
console.log('deploying <ContractName>');
<ContractName>Deployer.deploy(<params>);
}
Create a new protocol section in the config file, create a new function in the deployment script that will deploy the contracts for that protocol. This function should then be called from the run
function in the deployment script.
Within the protocol section the to be deployed contract is defined in, read the arguments for that contract from the config file and pass them to the deployer library for that contract.
Should the contract have dependencies that need to be resolved at runtime (e.g., the factory when deploying a router), ensure that the dependency contracts are deployed before the current contract.
Follow these steps from the README and launch the deploy-cli
to deploy contracts.
Select the Create Deployment Config
option from the deploy-cli
menu. Follow the prompts to select protocols and contracts to deploy as well as enter all required parameters for the deployment.
After this step is completed, a new deployment task file is created under deployments/tasks/<chain-id>/task-pending.json
. This file can be used to verify the deployment and the parameters and will be used by the deploy-cli
in the next step to execute the deployment.
The generated task file should be pushed to the repository. After the deployment is executed, the task will be renamed to task-<timestamp>.json
.
Select the Deploy from Config
option from the deploy-cli
menu to execute the deployment. Enter the chain id of the chain where the deployment should be executed. Additionally, select an RPC URL, an explorer for automatic verification and the private key of the account that will be used to execute the deployment.
After the deployment is executed, deployment logs are generated automatically and added to the deployments/json/<chain-id>.json
file. A human readable summary of the deployment logs is added to the deployments/<chain-id>.md
file. These deployment logs together with the foundry broadcast files should be pushed to the repository.