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

Add L1 & L2 Farm Proxies #1

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
21f73d7
Add farm proxies
May 22, 2024
c8febe2
Add missing approval
May 22, 2024
afb473c
Add e2e test & deploy scripts
May 24, 2024
2a65e8c
Update new-bridge
May 24, 2024
79246a0
Add default maxGas estimate
May 31, 2024
45669be
Add unit tests
May 31, 2024
4ef9be8
Add vest setup to init script
May 31, 2024
5f42034
Add maxGas estimation script
Jun 5, 2024
4cede7b
Add minReward to L1Proxy
Jun 13, 2024
f88061e
Revert when reward is too small on L1
Jun 14, 2024
d98c65a
Add estimateDepositCost view func
Jun 14, 2024
e1c107b
Apply suggestions from code review
telome Jun 19, 2024
b498cd9
Update arbitrum-token-bridge
Jun 24, 2024
ad7048c
Apply suggestions from code review
telome Jun 24, 2024
c86e45e
File gas params in spell
Jun 24, 2024
c2eb5ab
Update arbitrum-token-bridge
Jun 25, 2024
f371a1a
Add more sanity checks
Jun 25, 2024
9b4847b
Assume dss-vest already inited
Jun 25, 2024
63e4ca2
Update chainlog
Jun 25, 2024
84e2b6a
Make L2 spell reusable across L2 proxies
Jun 25, 2024
6044525
Add farm sanity checks
Jun 25, 2024
87a2147
Add more farm checks
Jun 25, 2024
cf292ac
Fix nits
Jun 25, 2024
8fb5adf
Apply suggestions from code review
telome Jun 25, 2024
71829f9
Add Deploy.s.sol
Jun 26, 2024
214a78a
Add Init.s.sol
Jun 26, 2024
9e2b461
Fix tests and apply correct alias to feeRecipient
Jun 27, 2024
6ba7328
Move feeRecipient aliasing to init lib
Jun 27, 2024
2229c3a
Move dealiasing to L1FarmProxy
Jun 27, 2024
2093296
Add deploy test scripts and fix nits
Jun 27, 2024
1441c87
Update script/output/11155111/deployed-latest.json
telome Jun 27, 2024
8202396
Fix Estimate script
Jul 1, 2024
7777054
Use minThreshold
Jul 2, 2024
84680b8
emit RewardAdded from L1 proxy
Jul 5, 2024
1376638
Complete README
Jul 9, 2024
a8c1570
Remove vestMgr
Jul 9, 2024
d356c1d
Update arbitrum-token-bridge
Jul 9, 2024
e36873e
Check vest mgr is still 0 after spell
Jul 10, 2024
25c8104
Add token recovery funcs
Jul 10, 2024
c4d9f08
Recommend L2FarmProxy.rewardThreshold <= L1FarmProxy.rewardThreshold
Jul 10, 2024
99af088
Use single rewardThreshold in init lib
Jul 10, 2024
fbe197f
Fix L2 spell and add L2 spell tests
Jul 10, 2024
2960565
Fix alignment
Jul 10, 2024
f74d47a
Fix spell revert msg
Jul 10, 2024
7226d12
Fix nits
Jul 11, 2024
78aa99d
Fix excess fee collection logic
Jul 13, 2024
a45c297
Remove old Deploy.s.sol
Jul 13, 2024
880c283
Remove extra space
Jul 15, 2024
3e6f5c7
Merge pull request #2 from telome/fee-forwarder
telome Jul 16, 2024
d8204ad
Update README for env vars to be exported
Jul 16, 2024
09ce9d8
Use single deploy proxy script with multiple deployers
Jul 18, 2024
702e425
Remove unnecessary command
Jul 18, 2024
fa18c20
Update deps
Jul 18, 2024
38476c1
Remove outdated files
Jul 18, 2024
69b539f
Add CS report (#3)
telome Aug 23, 2024
8cae0ca
Update deps (#4)
telome Aug 23, 2024
b879549
Update CI
Aug 23, 2024
370f3da
Upgrade bridge dependency + minor adjustment (#5)
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 @@
FOUNDRY_SCRIPT_DEPS=deployed
FOUNDRY_EXPORTS_OVERWRITE_LATEST=true
L1="sepolia"
L2="arbitrum_one_sepolia"
ETH_RPC_URL=
ARBITRUM_ONE_RPC_URL=
SEPOLIA_RPC_URL=
ARBITRUM_ONE_SEPOLIA_RPC_URL=
DEPLOYER=
PRIVATE_KEY=$(cat /path/to/pkey)
ETHERSCAN_KEY=
ARBISCAN_KEY=
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

# Docs
docs/

# Dotenv file
.env

deployed-[0-9]*.json
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "lib/dss-test"]
path = lib/dss-test
url = https://github.com/makerdao/dss-test
[submodule "lib/arbitrum-token-bridge"]
path = lib/arbitrum-token-bridge
url = https://github.com/makerdao/arbitrum-token-bridge
[submodule "lib/endgame-toolkit"]
path = lib/endgame-toolkit
url = https://github.com/makerdao/endgame-toolkit
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Arbitrum 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 Arbitrum. It uses the [Arbitrum Token Bridge](https://github.com/makerdao/arbitrum-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 [Arbitrum Token Bridge](https://github.com/makerdao/arbitrum-token-bridge) is used to bridge the tokens from L1 to L2.
- The [escrow contract](https://etherscan.io/address/0xA10c7CE4b876998858b1a9E12b10092229539400#code) is used by the Arbitrum 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://etherscan.io/address/0x9ba25c289e351779E0D481Ba37489317c34A899d#code) & [`L2GovernanceRelay`](https://arbiscan.io/address/0x10E6593CDda8c58a1d0f14C5164B376352a55f2F#code) allow governance to exert admin control over the deployed L2 contracts. These contracts have been previously deployed to control the Arbitrum Dai Bridge.

## Expected flow

- It is expected that the ether balance of the `L1FarmProxy` is continuously monitored and topped up as needed to ensure the successful operation of the proxy.
- 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` must be sufficiently large to reduce the frequency of cross-chain transfers (thereby also reducing the amount of ether that needs to be provisionned into the `L1FarmProxy`). `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.

oldchili marked this conversation as resolved.
Show resolved Hide resolved
## 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=arbitrum_one
```

Testnet deployment:

```
L1=sepolia
L2=arbitrum_one_sepolia
```

### Deploy the farm L1 & L2 proxies

The deployment assumes that the [arbitrum-token-bridge](https://github.com/makerdao/arbitrum-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. It is assumed that these tokens have been registered with the Arbitrum Token Bridge.

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.

The following command deploys the L1 and L2 farm proxies:

```
forge script script/Deploy.s.sol:Deploy --sender $DEPLOYER --private-key $PRIVATE_KEY --slow --verify --multi --broadcast
```

### Initialize the farm L1 & L2 proxies

On mainnet, the farm proxies should be initialized via the spell process. To determine an adequate value for the `maxGas` storage variable of `L1FarmProxy`, the `Estimate` script can be run:

```
forge script script/Estimate.s.sol:Estimate --sender $DEPLOYER --private-key $PRIVATE_KEY
```

On testnet, the proxies initialization can be performed via the following command:

```
forge script script/Init.s.sol:Init --sender $DEPLOYER --private-key $PRIVATE_KEY --slow --multi --broadcast
```

### Run a test distribution

Run the following command to distribute the vested funds to the L1 proxy:

```
forge script script/Distribute.s.sol:Distribute --sender $DEPLOYER --private-key $PRIVATE_KEY --slow --multi --broadcast
```

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 --sender $DEPLOYER --private-key $PRIVATE_KEY --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 l2Proxy,
address feeRecipient,
address l1Gateway
) internal returns (address l1Proxy) {
l1Proxy = address(new L1FarmProxy(rewardsToken, l2Proxy, feeRecipient, l1Gateway));
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());
}
}
156 changes: 156 additions & 0 deletions deploy/FarmProxyInit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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 l2Proxy() external view returns (address);
function feeRecipient() external view returns (address);
function l1Gateway() 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,
uint256 l1CallValue,
uint256 maxGas,
uint256 gasPriceBid,
uint256 maxSubmissionCost
) external payable;
}

struct MessageParams {
uint256 maxGas;
uint256 gasPriceBid;
uint256 maxSubmissionCost;
}

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 l1Gateway;
uint256 maxGas; // For the L1 proxy
uint256 gasPriceBid; // For the L1 proxy
uint256 rewardThreshold; // For the L1 and L2 proxies
address farm; // The L2 farm
uint256 rewardsDuration; // For the L2 farm
MessageParams xchainMsg; // For the xchain message executing the L2 spell
bytes32 proxyChainlogKey; // Chainlog key for the L1 proxy
bytes32 distrChainlogKey; // Chainlog key for vestedRewardsDistribution
}

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

// 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/rewards-token-mismatch");
require(l1Proxy.l2Proxy() == l2Proxy, "FarmProxyInit/l2-proxy-mismatch");
require(l1Proxy.feeRecipient() == l1GovRelay.l2GovernanceRelay(), "FarmProxyInit/fee-recipient-mismatch");
require(l1Proxy.l1Gateway() == cfg.l1Gateway, "FarmProxyInit/l1-gateway-mismatch");
require(cfg.maxGas <= 10_000_000_000, "FarmProxyInit/max-gas-out-of-bounds");
require(cfg.gasPriceBid <= 10_000 gwei, "FarmProxyInit/gas-price-bid-out-of-bounds");
require(cfg.rewardThreshold <= type(uint128).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("maxGas", cfg.maxGas);
l1Proxy.file("gasPriceBid", cfg.gasPriceBid);
l1Proxy.file("rewardThreshold", cfg.rewardThreshold);

oldchili marked this conversation as resolved.
Show resolved Hide resolved
// setup L2 proxy

uint256 l1CallValue = cfg.xchainMsg.maxSubmissionCost + cfg.xchainMsg.maxGas * cfg.xchainMsg.gasPriceBid;

// not strictly necessary (as the retryable ticket creation would otherwise fail)
// but makes the eth balance requirement more explicit
require(address(l1GovRelay).balance >= l1CallValue, "FarmProxyInit/insufficient-relay-balance");

l1GovRelay.relay({
target: l2Spell,
oldchili marked this conversation as resolved.
Show resolved Hide resolved
targetData: abi.encodeCall(L2FarmProxySpell.init, (
l2Proxy,
cfg.l2RewardsToken,
cfg.stakingToken,
cfg.farm,
cfg.rewardThreshold,
cfg.rewardsDuration
)),
l1CallValue: l1CallValue,
maxGas: cfg.xchainMsg.maxGas,
gasPriceBid: cfg.xchainMsg.gasPriceBid,
maxSubmissionCost: cfg.xchainMsg.maxSubmissionCost
});

// update chainlog

dss.chainlog.setAddress(cfg.proxyChainlogKey, l1Proxy_);
dss.chainlog.setAddress(cfg.distrChainlogKey, cfg.vestedRewardsDistribution);
}
oldchili marked this conversation as resolved.
Show resolved Hide resolved
}
Loading