Skip to content
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

Op Farms #1

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0f1676f
chore: forge init
oldchili Jul 23, 2024
fb88fda
forge install: forge-std
oldchili Jul 23, 2024
6bc3351
forge install: dss-test
oldchili Jul 23, 2024
98153df
forge remove forge-std
oldchili Jul 23, 2024
d861e89
Remove Counter files
oldchili Jul 23, 2024
2d00826
Initial implementation
oldchili Jul 23, 2024
51f07cf
Gateway => Bridge
oldchili Jul 23, 2024
1e57f53
forge install: endgame-toolkit
oldchili Jul 23, 2024
1c4f38f
forge install: op-token-bridge
oldchili Jul 23, 2024
8f37ad4
Add init libs and integration test
oldchili Jul 31, 2024
5aab2da
Add scripts - untested
oldchili Jul 31, 2024
a3a6e93
Small changes
oldchili Aug 1, 2024
fecbab3
Script fixes and configs
oldchili Aug 1, 2024
ab03847
Remove unused interface, fix typo
oldchili Aug 5, 2024
73cbd87
Apply suggestions from code review
oldchili Aug 20, 2024
3db60d6
Bond cfg.minGasLimit and cfg.initMinGasLimit by 500M
oldchili Aug 20, 2024
fa068a6
Notes that gas limits need to be tighter in production
oldchili Aug 20, 2024
8902409
Add 20% gas buffer in Distribute.s.sol instructions
oldchili Aug 21, 2024
b08732f
Update dss-test
oldchili Aug 21, 2024
e665e0e
New line at EOF
oldchili Aug 21, 2024
74f503e
Update op-token-bridge
oldchili Aug 22, 2024
b5a90d0
Apply suggestions from code review
oldchili Aug 25, 2024
2212693
Remove redundant line
oldchili Aug 25, 2024
94fc23f
Inlinve some vars in script/Init.s.sol
oldchili Aug 25, 2024
29bc92a
More inlining
oldchili Aug 25, 2024
7aad28b
Update CI
oldchili Aug 25, 2024
632c17f
Minor changes in env example
oldchili Aug 26, 2024
4950996
Change TODO to clarification message
oldchili Aug 26, 2024
f71a30e
Update lib/op-token-bridge
oldchili Aug 26, 2024
119313b
Cantina audit (#2)
oldchili Sep 2, 2024
525f923
Add Cantina Report (#3)
oldchili Sep 10, 2024
6ce7a1e
Readme clarification about distribution rate (#4)
oldchili Sep 10, 2024
62c2dea
Add ChainSecurity Report (#5)
oldchili Sep 16, 2024
a78ed4d
Upgrade bridge dependency
sunbreak1211 Sep 17, 2024
ba2f7c7
Upgrade bridge dependency
sunbreak1211 Sep 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export FOUNDRY_SCRIPT_DEPS=deployed
export FOUNDRY_EXPORTS_OVERWRITE_LATEST=true
export L1="sepolia"
export L2="base_sepolia"
export ETH_RPC_URL=
oldchili marked this conversation as resolved.
Show resolved Hide resolved
export BASE_RPC_URL=
export SEPOLIA_RPC_URL=
export BASE_ONE_SEPOLIA_RPC_URL=
oldchili marked this conversation as resolved.
Show resolved Hide resolved
export L1_PRIVATE_KEY="0x$(cat /path/to/pkey1)"
export L2_PRIVATE_KEY="0x$(cat /path/to/pkey2)"
export ETHERSCAN_KEY=
export BASESCAN_KEY=
37 changes: 37 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: test
sunbreak1211 marked this conversation as resolved.
Show resolved Hide resolved

on: [push, pull_request]

env:
FOUNDRY_PROFILE: ci

jobs:
check:
strategy:
fail-fast: true

name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build

- name: Run Forge tests
run: |
forge test -vvv
id: test
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }}
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
10 changes: 10 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[submodule "lib/dss-test"]
path = lib/dss-test
url = https://github.com/makerdao/dss-test
[submodule "lib/endgame-toolkit"]
path = lib/endgame-toolkit
url = https://github.com/makerdao/endgame-toolkit
[submodule "lib/op-token-bridge"]
path = lib/op-token-bridge
url = https://github.com/makerdao/op-token-bridge
branch = dev
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,92 @@
# op-farms
# Op Farms

## Overview

This repository implements a mechanism to distribute rewards vested in a [DssVest](https://github.com/makerdao/dss-vest) contract on L1 to users staking tokens in a [StakingRewards](https://github.com/makerdao/endgame-toolkit/blob/master/src/synthetix/StakingRewards.sol) farm on an OP stack L2. It uses the [Op Token Bridge](https://github.com/makerdao/op-token-bridge) to transfer the rewards from L1 to L2.

## Contracts

- `L1FarmProxy.sol` - Proxy to the farm on the L1 side. Receives the token reward (expected to come from a [`VestedRewardDistribution`](https://github.com/makerdao/endgame-toolkit/blob/master/src/VestedRewardsDistribution.sol) contract) and transfers it cross-chain to the `L2FarmProxy`. An instance of `L1FarmProxy` must be deployed for each supported pair of staking and rewards token.
- `L2FarmProxy.sol` - Proxy to the farm on the L2 side. Receives the token reward (expected to be bridged from the `L1FarmProxy`) and forwards it to the [StakingRewards](https://github.com/makerdao/endgame-toolkit/blob/master/src/synthetix/StakingRewards.sol) farm where it gets distributed to stakers. An instance of `L2FarmProxy` must be deployed for each supported pair of staking and rewards token.

### External dependencies

- The L2 staking tokens and the L1 and L2 rewards tokens are not provided as part of this repository. It is assumed that only simple, regular ERC20 tokens will be used. In particular, the supported tokens are assumed to revert on failure (instead of returning false) and do not execute any hook on transfer.
- [`DssVest`](https://github.com/makerdao/dss-vest) is used to vest the rewards token on L1.
- [`VestedRewardDistribution`](https://github.com/makerdao/endgame-toolkit/blob/master/src/VestedRewardsDistribution.sol) is used to vest the rewards tokens from `DssVest`, transfer them to the `L1FarmProxy` and trigger the bridging of the tokens.
- The [Op Token Bridge](https://github.com/makerdao/op-token-bridge) is used to bridge the tokens from L1 to L2.
- The [escrow contract](https://github.com/makerdao/op-token-bridge/blob/dev/src/Escrow.sol) is used by the Op Token Bridge to hold the bridged tokens on L1.
- [`StakingRewards`](https://github.com/makerdao/endgame-toolkit/blob/master/src/synthetix/StakingRewards.sol) is used to distribute the bridged rewards to stakers on L2.
- The [`L1GovernanceRelay`](https://github.com/makerdao/op-token-bridge/blob/dev/src/L1GovernanceRelay.sol) & [`L2GovernanceRelay`](https://github.com/makerdao/op-token-bridge/blob/dev/src/L2GovernanceRelay.sol) allow governance to exert admin control over the deployed L2 contracts.

## Expected flow
- Once the vested amount of rewards tokens exceeds `L1FarmProxy.rewardThreshold`, a keeper calls `VestedRewardDistribution.distribute()` to vest the rewards and have them bridged to L2.
- Once the bridged amount of rewards tokens exceeds `L2FarmProxy.rewardThreshold`, anyone (e.g. a keeper or an L2 staker) can call `L2FarmProxy.forwardReward()` to distribute the rewards to the L2 farm.

Note that `L1FarmProxy.rewardThreshold` should be sufficiently large to reduce the frequency of cross-chain transfers (thereby saving keepers gas). `L2FarmProxy.rewardThreshold` must also be sufficiently large to limit the reduction of the farm's rate of rewards distribution. Consider also choosing `L2FarmProxy.rewardThreshold <= L1FarmProxy.rewardThreshold` so that the bridged rewards can be promptly distributed to the farm. In the initialization library, these two variables are assigned the same value.

## Deployment

### Declare env variables

Add the required env variables listed in `.env.example` to your `.env` file, and run `source .env`.

Make sure to set the `L1` and `L2` env variables according to your desired deployment environment.

Mainnet deployment:

```
L1=mainnet
L2=base # in case of using Base as the L2
```

Testnet deployment:

```
L1=sepolia
L2=base_sepolia # in case of using Base as the L2
```

### Deploy the farm L1 & L2 proxies

The deployment assumes that the [op-token-bridge](https://github.com/makerdao/op-token-bridge) has already been deployed and was properly initialized.

Fill in the addresses of the L2 staking token and L1 and L2 rewards tokens in `script/input/{chainId}/config.json` under the `"stakingToken"` and `"rewardsToken"` keys.

Fill in the address of the mainnet DssVest contract in `script/input/1/config.json` under the `vest` key. It is assumed that the vesting contract was properly initialized. On testnet, a mock DssVest contract will automatically be deployed.

Start by deploying the `L2FarmProxySpell` singleton.

```
forge script script/DeploySingletons.s.sol:DeploySingletons --slow --multi --broadcast --verify
```

Next, run the following command to deploy the L1 vested rewards distribution contract, the L2 farm and the L1 and L2 proxies:

```
forge script script/DeployProxy.s.sol:DeployProxy --slow --multi --broadcast --verify
```

### Initialize the farm L1 & L2 proxies

On mainnet, the farm proxies should be initialized via the spell process.
On testnet, the proxies initialization can be performed via the following command:

```
forge script script/Init.s.sol:Init --slow --multi --broadcast
```

### Run a test distribution

Run the following command to distribute the vested funds to the L1 proxy.
We add a buffer to the gas estimation per Optimism's [recommendation](https://docs.optimism.io/builders/app-developers/bridging/messaging#for-l1-to-l2-transactions-1) for L1 => L2 transactions.

```
forge script script/Distribute.s.sol:Distribute --slow --multi --broadcast --gas-estimate-multiplier 120
```

Wait for the transaction to be relayed to L2, then run the following command to forward the bridged funds from the L2 proxy to the farm:

```
forge script script/Forward.s.sol:Forward --slow --multi --broadcast
```
50 changes: 50 additions & 0 deletions deploy/FarmProxyDeploy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: © 2024 Dai Foundation <www.daifoundation.org>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

pragma solidity >=0.8.0;

import { ScriptTools } from "dss-test/ScriptTools.sol";

import { L2FarmProxySpell } from "./L2FarmProxySpell.sol";
import { L1FarmProxy } from "src/L1FarmProxy.sol";
import { L2FarmProxy } from "src/L2FarmProxy.sol";

library FarmProxyDeploy {
function deployL1Proxy(
address deployer,
address owner,
address rewardsToken,
address remoteToken,
address l2Proxy,
address l1Bridge
) internal returns (address l1Proxy) {
l1Proxy = address(new L1FarmProxy(rewardsToken, remoteToken, l2Proxy, l1Bridge));
ScriptTools.switchOwner(l1Proxy, deployer, owner);
}

function deployL2Proxy(
address deployer,
address owner,
address farm
) internal returns (address l2Proxy) {
l2Proxy = address(new L2FarmProxy(farm));
ScriptTools.switchOwner(l2Proxy, deployer, owner);
}

function deployL2ProxySpell() internal returns (address l2Spell) {
l2Spell = address(new L2FarmProxySpell());
}
}
132 changes: 132 additions & 0 deletions deploy/FarmProxyInit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: © 2024 Dai Foundation <www.daifoundation.org>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

pragma solidity >=0.8.0;

import { DssInstance } from "dss-test/MCD.sol";
import { L2FarmProxySpell } from "./L2FarmProxySpell.sol";

interface DssVestLike {
function gem() external view returns (address);
function create(address _usr, uint256 _tot, uint256 _bgn, uint256 _tau, uint256 _eta, address _mgr) external returns (uint256 id);
function restrict(uint256 _id) external;
}

interface VestedRewardsDistributionLike {
function dssVest() external view returns (address);
function stakingRewards() external view returns (address);
function gem() external view returns (address);
function file(bytes32 what, uint256 data) external;
}

interface L1FarmProxyLike {
function rewardsToken() external view returns (address);
function remoteToken() external view returns (address);
function l2Proxy() external view returns (address);
function l1Bridge() external view returns (address);
function file(bytes32 what, uint256 data) external;
}

interface L1RelayLike {
function l2GovernanceRelay() external view returns (address);
function relay(address target, bytes calldata targetData, uint32 minGasLimit) external;
}

struct ProxiesConfig {
address vest; // DssVest, assumed to have been fully init'ed for l1RewardsToken
uint256 vestTot;
uint256 vestBgn;
uint256 vestTau;
address vestedRewardsDistribution;
address l1RewardsToken;
address l2RewardsToken;
address stakingToken;
address l1Bridge;
uint32 minGasLimit; // For filing in the L1 proxy
uint224 rewardThreshold; // For the L1 and L2 proxies
address farm; // The L2 farm
uint256 rewardsDuration; // For the L2 farm
uint32 initMinGasLimit; // For relaying of `init` L2 spell operation
bytes32 proxyChainlogKey; // Chainlog key for the L1 proxy
bytes32 distrChainlogKey; // Chainlog key for vestedRewardsDistribution
}

library FarmProxyInit {
function initProxies(
DssInstance memory dss,
address l1GovRelay,
address l1Proxy_,
address l2Proxy,
address l2Spell,
ProxiesConfig memory cfg
) internal {
L1FarmProxyLike l1Proxy = L1FarmProxyLike(l1Proxy_);
DssVestLike vest = DssVestLike(cfg.vest);
VestedRewardsDistributionLike distribution = VestedRewardsDistributionLike(cfg.vestedRewardsDistribution);

// sanity checks

require(vest.gem() == cfg.l1RewardsToken, "FarmProxyInit/vest-gem-mismatch");
require(distribution.gem() == cfg.l1RewardsToken, "FarmProxyInit/distribution-gem-mismatch");
require(distribution.stakingRewards() == l1Proxy_, "FarmProxyInit/distribution-farm-mismatch");
require(distribution.dssVest() == cfg.vest, "FarmProxyInit/distribution-vest-mismatch");
require(l1Proxy.rewardsToken() == cfg.l1RewardsToken, "FarmProxyInit/rewardsToken-token-mismatch");
require(l1Proxy.l2Proxy() == l2Proxy, "FarmProxyInit/l2-proxy-mismatch");
require(l1Proxy.remoteToken() == cfg.l2RewardsToken, "FarmProxyInit/remote-token-mismatch");
require(l1Proxy.l1Bridge() == cfg.l1Bridge, "FarmProxyInit/l1-bridge-mismatch");
require(cfg.minGasLimit <= 500_000_000, "FarmProxyInit/min-gas-limit-out-of-bounds");
require(cfg.initMinGasLimit <= 500_000_000, "FarmProxyInit/init-min-gas-limit-out-of-bounds");
require(cfg.rewardThreshold <= type(uint224).max, "FarmProxyInit/reward-threshold-out-of-bounds");

// setup vest

uint256 vestId = vest.create({
_usr: cfg.vestedRewardsDistribution,
_tot: cfg.vestTot,
_bgn: cfg.vestBgn,
_tau: cfg.vestTau,
_eta: 0,
_mgr: address(0)
});
vest.restrict(vestId);
distribution.file("vestId", vestId);

// setup L1 proxy

l1Proxy.file("minGasLimit", cfg.minGasLimit);
l1Proxy.file("rewardThreshold", cfg.rewardThreshold);

// setup L2 proxy

L1RelayLike(l1GovRelay).relay({
target: l2Spell,
targetData: abi.encodeCall(L2FarmProxySpell.init, (
l2Proxy,
cfg.l2RewardsToken,
cfg.stakingToken,
cfg.farm,
cfg.rewardThreshold,
cfg.rewardsDuration
)),
minGasLimit: cfg.initMinGasLimit
});

// update chainlog

dss.chainlog.setAddress(cfg.proxyChainlogKey, l1Proxy_);
dss.chainlog.setAddress(cfg.distrChainlogKey, cfg.vestedRewardsDistribution);
}
}
Loading