-
Notifications
You must be signed in to change notification settings - Fork 140
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[mainnet] Transfer & Delegate OP tokens (#124)
* Transfer OP tokens and delegate * Post checks * Comment * Comments and runbook * Delegate to treasury wallet so we can redelegate from there * Add validation step for tenderly sim * Add more tenderly validation steps to README * Organize new validation steps * Add event details to validation steps * Remove event details with errant tx hash * Fix step count * Cleanup * Extra check * Nadirs comments --------- Co-authored-by: katzman <steve.katzman@coinbase.com>
- Loading branch information
1 parent
781f11a
commit 5ef0d79
Showing
6 changed files
with
424 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
OPTIMISM_RPC_URL=https://mainnet.optimism.io | ||
|
||
OP_COMMIT=3580bf1b41d80fcb2b895d5610836bfad27fc989 | ||
BASE_CONTRACTS_COMMIT=a147139671c09923f78ae46a6ebedc91209bb076 | ||
|
||
OP_TOKEN=0x4200000000000000000000000000000000000042 | ||
NESTED_SAFE=0x0a7361e734cf3f0394B0FC4a45C74E7a4Ec70940 | ||
OP_MULTISIG=0x2501c477D0A35545a387Aa4A3EEe4292A9a8B3F0 | ||
CB_MULTISIG=0x6e1DFd5C1E22A4677663A81D24C6BA03561ef0f6 | ||
SMART_ESCROW_CONTRACT=0x143F5773CFE5613ca94196d557c889134F47CB77 | ||
ALLIGATOR_PROXY=0x7f08F3095530B67CdF8466B7a923607944136Df0 | ||
UPFRONT_GRANT_TOKENS=10737418000000000000000000 | ||
TOKENS_TO_TRANSFER=26843545000000000000000000 | ||
BENEFICIARY=0x635Fb974F09B269Bc750bF96338c29cF41430125 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
include ../../Makefile | ||
include ../.env | ||
include .env | ||
|
||
install-agora: | ||
forge install --no-git git@github.com:voteagora/optimism-gov.git | ||
|
||
.PHONY: sign-cb | ||
sign-cb: | ||
$(GOPATH)/bin/eip712sign --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" -- \ | ||
forge script --rpc-url $(OPTIMISM_RPC_URL) TransferAndDelegateOPTokens \ | ||
--sig "sign(address)" $(CB_MULTISIG) | ||
|
||
.PHONY: sign-op | ||
sign-op: | ||
$(GOPATH)/bin/eip712sign --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" -- \ | ||
forge script --rpc-url $(OPTIMISM_RPC_URL) TransferAndDelegateOPTokens \ | ||
--sig "sign(address)" $(OP_MULTISIG) | ||
|
||
|
||
.PHONY: approve-cb | ||
approve-cb: | ||
forge script --rpc-url $(OPTIMISM_RPC_URL) TransferAndDelegateOPTokens \ | ||
--sig "approve(address,bytes)" $(CB_MULTISIG) $(SIGNATURES) \ | ||
--ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" | ||
|
||
.PHONY: approve-op | ||
approve-op: | ||
forge script --rpc-url $(OPTIMISM_RPC_URL) TransferAndDelegateOPTokens \ | ||
--sig "approve(address,bytes)" $(OP_MULTISIG) $(SIGNATURES) \ | ||
--ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" | ||
|
||
.PHONY: execute | ||
execute: | ||
forge script --rpc-url $(OPTIMISM_RPC_URL) TransferAndDelegateOPTokens \ | ||
--sig "run()" --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
# Transfer and Delegate OP Tokens | ||
|
||
Status: DRAFT, NOT READY TO SIGN | ||
|
||
> [!IMPORTANT] !!! DO NOT SIGN using this playbook yet, as it has not | ||
> been approved by all stakeholders | ||
## Objective | ||
|
||
When Base launched, Optimism granted a share of OP tokens to Coinbase. The token distribution occurs onchain, and vests over a period of 6 years, with the first vesting event in July 2024. A smart contract handles the logic to make the tokens available for distribution as they vest. This smart contract (the "Smart Escrow Contract") also receives OP tokens 1 year before they vest and stores them until vesting. However, since the Smart Escrow contract was not ready upon Base launch, the existing OP tokens are being stored in a 2-of-2 multisig with CB & OP signers (identical to the multisig used for Base's upgrade keys, except on the Optimism network). | ||
|
||
Now that the Smart Escrow contract is live, this task moves any existing OP tokens from the 2-of-2 multisig to the Smart Escrow contract. | ||
|
||
The one caveat is that some amount of the tokens (the "upfront grant") is available to be used by Coinbase for governance purposes, and since the Smart Escrow contract does not support governance use cases, these tokens will remain in the 2-of-2 and be sent directly to Coinbase upon vesting in July 2024. In the meantime, we will do a 1 time delegation of these tokens to a Coinbase owned address, so they can be used for governance purposes prior to July. This delegation event is also handled in this signing task. | ||
|
||
## Approving the transaction | ||
|
||
### 1. Update repo and move to the appropriate folder: | ||
|
||
``` | ||
cd contract-deployments | ||
git pull | ||
cd mainnet/2024-02-23-transfer-op | ||
make deps | ||
``` | ||
|
||
### 2. Setup Ledger | ||
|
||
Your Ledger needs to be connected and unlocked. The Ethereum | ||
application needs to be opened on Ledger with the message "Application | ||
is ready". | ||
|
||
### 3. Simulate and validate the transaction | ||
|
||
Make sure your ledger is still unlocked and run the following. | ||
|
||
``` shell | ||
make sign-op # or make sign-cb for Coinbase signers | ||
``` | ||
|
||
You will see a "Simulation link" from the output. | ||
|
||
Paste this URL in your browser. A prompt may ask you to choose a | ||
project, any project will do. You can create one if necessary. | ||
|
||
Click "Simulate Transaction". | ||
|
||
We will be performing 3 validations and then we'll extract the domain hash and | ||
message hash to approve on your Ledger then verify completion: | ||
|
||
1. Validate integrity of the simulation. | ||
2. Validate correctness of the state diff. | ||
3. Validate correctness of the events emitted | ||
4. Validate and extract domain hash and message hash to approve. | ||
5. Validate that the transaction completed successfully | ||
|
||
|
||
#### 3.1. Validate integrity of the simulation. | ||
|
||
Make sure you are on the "Overview" tab of the tenderly simulation, to | ||
validate integrity of the simulation, we need to check the following: | ||
|
||
1. "Network": Check the network is Optimism Mainnet. | ||
2. "Timestamp": Check the simulation is performed on a block with a | ||
recent timestamp (i.e. close to when you run the script). | ||
3. "Sender": Check the address shown is your signer account. If not, | ||
you will need to determine which “number” it is in the list of | ||
addresses on your ledger. | ||
4. "Success" with a green check mark | ||
|
||
|
||
#### 3.2. Validate correctness of the state diff. | ||
|
||
Now click on the "State" tab. Verify that: | ||
|
||
1. Verify that the state change for token balances is reflected. | ||
|
||
``` | ||
0x0a7361e734cf3f0394b0fc4a45c74e7a4ec70940 37580963000000000000000000 -> 10737418000000000000000000 | ||
0x143f5773cfe5613ca94196d557c889134f47cb77 0 -> 26843545000000000000000000 | ||
``` | ||
|
||
|
||
#### 3.3. Validate correctness of the events emitted | ||
|
||
Now click on the "Events" tab. Verify that: | ||
|
||
|
||
2. Check that the `Transfer` event was emitted for the expected balance `from` the Nested Multisig `to` the Smart Escrow contract with details: | ||
```json | ||
{ | ||
"from": "0x0a7361e734cf3f0394b0fc4a45c74e7a4ec70940", | ||
"to": "0x143f5773cfe5613ca94196d557c889134f47cb77", | ||
"value": "26843545000000000000000000" | ||
} | ||
``` | ||
2. Verify that the call emitted the `DelegateChanged` event, establishing a new address as the delegate for the Nested Multisig with details: | ||
```json | ||
{ | ||
"delegator": "0x0a7361e734cf3f0394b0fc4a45c74e7a4ec70940", | ||
"fromDelegate": "0x0000000000000000000000000000000000000000", | ||
"toDelegate": "0x85e870a853a55c312bbfdb16c1f64d36916b6629" | ||
} | ||
``` | ||
3. Verify that the call emitted `DelegateVotesChanged` with a new balance of `10737418000000000000000000` with details: | ||
```json | ||
{ | ||
"delegate": "0x85e870a853a55c312bbfdb16c1f64d36916b6629", | ||
"previousBalance": "0", | ||
"newBalance": "10737418000000000000000000" | ||
} | ||
``` | ||
4. Verify that the call emitted `SubDelegation`, specifying an allowance of `10737418000000000000000000` with details: | ||
```json | ||
{ | ||
"from": "0x0a7361e734cf3f0394b0fc4a45c74e7a4ec70940", | ||
"to": "0x635fb974f09b269bc750bf96338c29cf41430125", | ||
"subdelegationRules": { | ||
"maxRedelegations": 1, | ||
"blocksBeforeVoteCloses": 0, | ||
"notValidBefore": 0, | ||
"notValidAfter": 0, | ||
"customRule": "0x0000000000000000000000000000000000000000", | ||
"allowanceType": 0, | ||
"allowance": "10737418000000000000000000" | ||
} | ||
} | ||
``` | ||
5. Verify that the call emitted `ExecutionSuccess` | ||
|
||
#### 3.4. Extract the domain hash and the message hash to approve. | ||
|
||
Now that we have verified the transaction performs the right | ||
operation, we need to extract the domain hash and the message hash to | ||
approve. | ||
|
||
Go back to the "Overview" tab, and find the | ||
`GnosisSafeL2.checkSignatures` call. This call's `data` parameter | ||
contains both the domain hash and the message hash that will show up | ||
in your Ledger. | ||
|
||
Here is an example screenshot. Note that the hash value may be | ||
different: | ||
|
||
> TODO | ||
It will be a concatenation of `0x1901`, the domain hash, and the | ||
message hash: `0x1901[domain hash][message hash]`. | ||
|
||
Note down this value. You will need to compare it with the ones | ||
displayed on the Ledger screen at signing. | ||
|
||
### 4. Approve the signature on your ledger | ||
|
||
Once the validations are done, it's time to actually sign the | ||
transaction. Make sure your ledger is still unlocked and run the | ||
following: | ||
|
||
``` shell | ||
make sign-op # or make sign-cb for Coinbase signers | ||
``` | ||
|
||
> [!IMPORTANT] This is the most security critical part of the | ||
> playbook: make sure the domain hash and message hash in the | ||
> following two places match: | ||
1. on your Ledger screen. | ||
2. in the Tenderly simulation. You should use the same Tenderly | ||
simulation as the one you used to verify the state diffs, instead | ||
of opening the new one printed in the console. | ||
|
||
There is no need to verify anything printed in the console. There is | ||
no need to open the new Tenderly simulation link either. | ||
|
||
After verification, sign the transaction. You will see the `Data`, | ||
`Signer` and `Signature` printed in the console. Format should be | ||
something like this: | ||
|
||
``` | ||
Data: <DATA> | ||
Signer: <ADDRESS> | ||
Signature: <SIGNATURE> | ||
``` | ||
|
||
Double check the signer address is the right one. | ||
|
||
### 5. Send the output to Facilitator(s) | ||
|
||
Nothing has occurred onchain - these are offchain signatures which | ||
will be collected by Facilitators for execution. Execution can occur | ||
by anyone once a threshold of signatures are collected, so a | ||
Facilitator will do the final execution for convenience. | ||
|
||
Share the `Data`, `Signer` and `Signature` with the Facilitator, and | ||
congrats, you are done! | ||
|
||
## [For Facilitator ONLY] How to execute the rehearsal | ||
|
||
### [After the rehearsal] Execute the output | ||
|
||
1. Collect outputs from all participating signers. | ||
2. Concatenate all signatures and export it as the `SIGNATURES` | ||
environment variable, i.e. `export | ||
SIGNATURES="0x[SIGNATURE1][SIGNATURE2]..."`. | ||
3. Run `make approve-cb` with Coinbase signer signatures. | ||
4. Run `make approve-op` with Optimism signer signatures. | ||
4. Run `make run` to execute the transaction onchain. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
[profile.default] | ||
src = 'src' | ||
out = 'out' | ||
libs = ['lib'] | ||
broadcast = 'records' | ||
fs_permissions = [ {access = "read-write", path = "./"} ] | ||
optimizer = true | ||
optimizer_runs = 999999 | ||
solc_version = "0.8.19" | ||
via-ir = true | ||
remappings = [ | ||
'@eth-optimism-bedrock/=lib/optimism/packages/contracts-bedrock/', | ||
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts', | ||
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts', | ||
'@rari-capital/solmate/=lib/solmate/', | ||
'@base-contracts/=lib/base-contracts', | ||
'solady/=lib/solady/src/', | ||
'@agora=lib/optimism-gov/src', | ||
] | ||
|
||
# See more config options https://github.com/foundry-rs/foundry/tree/master/config |
97 changes: 97 additions & 0 deletions
97
mainnet/2024-02-23-transfer-op/script/TransferAndDelegateOPTokens.s.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.19; | ||
|
||
import "@base-contracts/script/universal/NestedMultisigBuilder.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; | ||
import "@agora/structs/RulesV3.sol"; | ||
import "@agora/structs/AllowanceType.sol"; | ||
import "@agora/alligator/AlligatorOP_V5.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
|
||
contract TransferAndDelegateOPTokens is NestedMultisigBuilder { | ||
IERC20 internal OP_TOKEN = IERC20(vm.envAddress("OP_TOKEN")); | ||
|
||
address internal NESTED_SAFE = vm.envAddress("NESTED_SAFE"); | ||
address internal SMART_ESCROW = vm.envAddress("SMART_ESCROW_CONTRACT"); | ||
address internal ALLIGATOR_PROXY = vm.envAddress("ALLIGATOR_PROXY"); // Agora address which will allow for subdeletation | ||
address internal BENEFICIARY = vm.envAddress("BENEFICIARY"); | ||
uint256 internal UPFRONT_GRANT_TOKENS = vm.envUint("UPFRONT_GRANT_TOKENS"); | ||
uint256 internal TOKENS_TO_TRANSFER = vm.envUint("TOKENS_TO_TRANSFER"); | ||
|
||
function _postCheck() internal override view { | ||
require( | ||
OP_TOKEN.balanceOf(SMART_ESCROW) >= TOKENS_TO_TRANSFER, | ||
"TransferAndDelegateOPTokens: tokens not transferred to smart escrow" | ||
); | ||
require( | ||
OP_TOKEN.balanceOf(NESTED_SAFE) >= UPFRONT_GRANT_TOKENS, | ||
"TransferAndDelegateOPTokens: number of remaining tokens in nested safe is incorrect" | ||
); | ||
} | ||
|
||
function _buildCalls() internal override view returns (IMulticall3.Call3[] memory) { | ||
IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](3); | ||
|
||
// Double check that there are enough tokens to transfer | ||
uint256 remainingTokens = OP_TOKEN.balanceOf(NESTED_SAFE) - TOKENS_TO_TRANSFER; | ||
require( | ||
remainingTokens >= UPFRONT_GRANT_TOKENS, | ||
"TransferAndDelegateOPTokens: not enough tokens to transfer" | ||
); | ||
|
||
// Transfer collaboration grant tokens which are in the nested safe to smart escrow | ||
// Tokens are sent to the contract 1 year before they vest, so this will be some subset of the | ||
// total collaboration grant tokens | ||
calls[0] = IMulticall3.Call3({ | ||
target: address(OP_TOKEN), | ||
allowFailure: false, | ||
callData: abi.encodeCall( | ||
IERC20.transfer, | ||
(SMART_ESCROW, TOKENS_TO_TRANSFER) | ||
) | ||
}); | ||
// Delegate governance tokens for initial grant to Agora's Alligator proxy, | ||
// which will allow for subdelegations | ||
calls[1] = IMulticall3.Call3({ | ||
target: address(OP_TOKEN), | ||
allowFailure: false, | ||
callData: abi.encodeCall( | ||
ERC20Votes.delegate, | ||
(AlligatorOPV5(ALLIGATOR_PROXY).proxyAddress(NESTED_SAFE)) | ||
) | ||
}); | ||
|
||
// Setup subdelegation rules | ||
// The intended functionality here is that the wallet we delegate to should be able to redelegate, | ||
// but the wallet(s) it redelegates to should not be able to redelegate further. | ||
// In addition, we want to delegate an absolute amount, since only the Upfront Grant tokens are eligible | ||
// to be voted with before the 1 year vesting. There shouldn't be any additional OP tokens sent to this wallet, | ||
// but if there are, we should not delegate them. | ||
// The rest of the rules are set to the defaults and are not relevant for our use case. | ||
SubdelegationRules memory subdelegationRules = SubdelegationRules({ | ||
maxRedelegations: 1, | ||
blocksBeforeVoteCloses: 0, | ||
notValidBefore: 0, | ||
notValidAfter: 0, | ||
customRule: address(0), | ||
allowanceType: AllowanceType.Absolute, | ||
allowance: UPFRONT_GRANT_TOKENS | ||
}); | ||
|
||
// Delegate the tokens to the Coinbase owned address | ||
calls[2] = IMulticall3.Call3({ | ||
target: ALLIGATOR_PROXY, | ||
allowFailure: false, | ||
callData: abi.encodeCall( | ||
AlligatorOPV5.subdelegate, | ||
(BENEFICIARY, subdelegationRules) | ||
) | ||
}); | ||
|
||
return calls; | ||
} | ||
|
||
function _ownerSafe() internal override view returns (address) { | ||
return NESTED_SAFE; | ||
} | ||
} |
Oops, something went wrong.