Skip to content

Commit d3bfe80

Browse files
authored
chore: fund mainnet ledgers (#333)
* chore: fund mainnet ledgers * ready to sign * chore: merge scripts * chore: impl review comments
1 parent d7ef505 commit d3bfe80

9 files changed

Lines changed: 537 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
OP_COMMIT=6f68dc35e103278e366d2b8ba178ca87bbaacb0c # Not really used, only needed for the compilation to succeed.
2+
BASE_CONTRACTS_COMMIT=838514551165f03360f52b2b9ceca69844a98f48
3+
4+
SAFE=0x14536667Cd30e52C0b458BaACcB9faDA7046E056
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
include ../../Makefile
2+
include ../.env
3+
include .env
4+
5+
ifndef LEDGER_ACCOUNT
6+
override LEDGER_ACCOUNT = 0
7+
endif
8+
9+
.PHONY: sign-ethereum
10+
sign-ethereum:
11+
$(GOPATH)/bin/eip712sign --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" -- \
12+
forge script --rpc-url $(L1_RPC_URL) FundScript \
13+
--sig "sign(address[])" "[]" --sender 0x969ffD102fbF304d4e401999333FE9397DaC653D
14+
15+
.PHONY: execute-ethereum
16+
execute-ethereum:
17+
forge script --rpc-url $(L1_RPC_URL) FundScript \
18+
--sig "run(bytes)" $(SIGNATURES) --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --broadcast -vvvv
19+
20+
21+
.PHONY: sign-base
22+
sign-base:
23+
$(GOPATH)/bin/eip712sign --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" -- \
24+
forge script --rpc-url $(L2_RPC_URL) FundScript \
25+
--sig "sign(address[])" "[]" --sender 0x969ffD102fbF304d4e401999333FE9397DaC653D
26+
27+
.PHONY: execute-base
28+
execute-base:
29+
forge script --rpc-url $(L2_RPC_URL) FundScript \
30+
--sig "run(bytes)" $(SIGNATURES) --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --broadcast -vvvv
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Funding
2+
3+
Status: READY TO SIGN
4+
5+
## Description
6+
7+
This task contains a single script that can be used to fund addresses from a Gnosis Safe.
8+
9+
## Procedure
10+
11+
### 1. Update repo:
12+
13+
```bash
14+
cd contract-deployments
15+
git pull
16+
cd mainnet/2025-05-06-fund-ledgers
17+
make deps
18+
```
19+
20+
### 2. Setup Ledger
21+
22+
Your Ledger needs to be connected and unlocked. The Ethereum
23+
application needs to be opened on Ledger with the message "Application
24+
is ready".
25+
26+
### 3. Simulate, Validate, and Sign
27+
28+
#### 3.1. Simulate and validate the transaction
29+
30+
Make sure your ledger is still unlocked and run the following commands for both Ethereum and Base:
31+
32+
```bash
33+
make sign-ethereum
34+
```
35+
36+
```bash
37+
make sign-base
38+
```
39+
40+
For each run, you will see a "Simulation link" from the output.
41+
42+
Paste this URL in your browser. A prompt may ask you to choose a
43+
project, any project will do. You can create one if necessary.
44+
45+
Click "Simulate Transaction".
46+
47+
We will be performing 3 validations and extract the domain hash and
48+
message hash to approve on your Ledger:
49+
50+
1. Validate integrity of the simulation.
51+
2. Validate correctness of the state diff.
52+
3. Validate and extract domain hash and message hash to approve.
53+
54+
##### 3.1.1. Validate integrity of the simulation.
55+
56+
Make sure you are on the "Overview" tab of the tenderly simulation, to
57+
validate integrity of the simulation, we need to check the following:
58+
59+
1. "Network": Check the network is `Mainnet` (when running `make sign-ethereum`) or `Base` (when running `make sign-base`).
60+
2. "Timestamp": Check the simulation is performed on a block with a
61+
recent timestamp (i.e. close to when you run the script).
62+
63+
##### 3.1.2. Validate correctness of the state diff.
64+
65+
Now click on the "State" tab, and refer to the [Ethereum State](./validations/ethereum.md) or [Base State](./validations/base.md) validations instructions for the transaction you are signing. Once complete return to this document to complete the signing.
66+
67+
### 4. Extract the domain hash and the message hash to approve.
68+
69+
Now that we have verified the transaction performs the right
70+
operation, we need to extract the domain hash and the message hash to
71+
approve.
72+
73+
Go back to the "Overview" tab, and find the
74+
`GnosisSafe.checkSignatures` call. This call's `data` parameter
75+
contains both the domain hash and the message hash that will show up
76+
in your Ledger.
77+
78+
It will be a concatenation of `0x1901`, the domain hash, and the
79+
message hash: `0x1901[domain hash][message hash]`.
80+
81+
Note down this value. You will need to compare it with the ones
82+
displayed on the Ledger screen at signing.
83+
84+
Once the validations are done, it's time to actually sign the
85+
transaction.
86+
87+
> [!WARNING]
88+
> This is the most security critical part of the playbook: make sure the
89+
> domain hash and message hash in the following three places match:
90+
>
91+
> 1. On your Ledger screen.
92+
> 2. In the terminal output.
93+
> 3. In the Tenderly simulation. You should use the same Tenderly
94+
> simulation as the one you used to verify the state diffs, instead
95+
> of opening the new one printed in the console.
96+
>
97+
98+
After verification, sign the transaction. You will see the `Data`,
99+
`Signer` and `Signature` printed in the console. Format should be
100+
something like this:
101+
102+
```shell
103+
Data: <DATA>
104+
Signer: <ADDRESS>
105+
Signature: <SIGNATURE>
106+
```
107+
108+
Double check the signer address is the right one.
109+
110+
#### 4.1. Send the output to Facilitator(s)
111+
112+
Nothing has occurred onchain - these are offchain signatures which
113+
will be collected by Facilitators for execution. Execution can occur
114+
by anyone once a threshold of signatures are collected, so a
115+
Facilitator will do the final execution for convenience.
116+
117+
Share the `Data`, `Signer` and `Signature` with the Facilitator, and
118+
congrats, you are done!
119+
120+
### [For Facilitator ONLY] How to execute
121+
122+
#### Execute the transaction
123+
124+
1. Collect outputs from all participating signers.
125+
1. Concatenate all signatures and export it as the `SIGNATURES`
126+
environment variable, i.e. `export
127+
SIGNATURES="[SIGNATURE1][SIGNATURE2]..."`.
128+
1. Run the `make execute-ethereum` or `make execute-base` command as described below to execute the transaction.
129+
130+
For example, if the quorum is 2 and you get the following outputs:
131+
132+
```shell
133+
Data: 0xDEADBEEF
134+
Signer: 0xC0FFEE01
135+
Signature: AAAA
136+
```
137+
138+
```shell
139+
Data: 0xDEADBEEF
140+
Signer: 0xC0FFEE02
141+
Signature: BBBB
142+
```
143+
144+
Then you should run:
145+
146+
Coinbase facilitator:
147+
148+
```bash
149+
SIGNATURES=AAAABBBB make execute
150+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[profile.default]
2+
src = 'src'
3+
out = 'out'
4+
libs = ['lib']
5+
broadcast = 'records'
6+
fs_permissions = [{ access = "read-write", path = "./" }]
7+
optimizer = true
8+
optimizer_runs = 999999
9+
solc_version = "0.8.15"
10+
via-ir = false
11+
remappings = [
12+
'@eth-optimism-bedrock/=lib/optimism/packages/contracts-bedrock/',
13+
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts',
14+
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts',
15+
'@rari-capital/solmate/=lib/solmate/',
16+
'@base-contracts/=lib/base-contracts',
17+
'@solady/=lib/solady/src/',
18+
]
19+
20+
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"recipients": [
3+
"0x7Ad8E6B7B1f6D66F49559f20053Cef8a7b6c488E",
4+
"0x644e3DedB0e4F83Bfcf8F9992964d240224B74dc",
5+
"0x24c3AE1AeDB8142D32BB6d3B988f5910F272D53b",
6+
"0x9bf96dcf51959915c8c343a3e50820ad069a1859"
7+
],
8+
"funds": [
9+
100000000000000000,
10+
100000000000000000,
11+
100000000000000000,
12+
100000000000000000
13+
]
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"recipients": [
3+
"0x7Ad8E6B7B1f6D66F49559f20053Cef8a7b6c488E",
4+
"0x644e3DedB0e4F83Bfcf8F9992964d240224B74dc",
5+
"0x24c3AE1AeDB8142D32BB6d3B988f5910F272D53b",
6+
"0x9bf96dcf51959915c8c343a3e50820ad069a1859"
7+
],
8+
"funds": [
9+
1000000000000000000,
10+
1000000000000000000,
11+
1000000000000000000,
12+
1000000000000000000
13+
]
14+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.15;
3+
4+
import {Vm} from "forge-std/Vm.sol";
5+
import {console} from "forge-std/console.sol";
6+
import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol";
7+
8+
import {MultisigScript} from "@base-contracts/script/universal/MultisigScript.sol";
9+
import {Simulation} from "@base-contracts/script/universal/Simulation.sol";
10+
11+
contract FundScript is MultisigScript {
12+
address internal immutable SAFE;
13+
14+
uint256 internal immutable SAFE_BALANCE_BEFORE;
15+
uint256 internal immutable TOTAL_FUNDS;
16+
17+
address[] internal RECIPIENTS;
18+
uint256[] internal FUNDS;
19+
uint256[] internal RECIPIENT_BALANCES_BEFORE;
20+
21+
constructor() {
22+
Chain memory chain = getChain(block.chainid);
23+
console.log("Deploying on chain: %s", chain.name);
24+
25+
SAFE = vm.envAddress("SAFE");
26+
27+
string memory funding;
28+
if (chain.chainId == 1) {
29+
funding = vm.readFile("./funding-ethereum.json");
30+
} else if (chain.chainId == 8453) {
31+
funding = vm.readFile("./funding-base.json");
32+
} else {
33+
revert(string.concat("Unsupported chain: ", chain.name, " (", vm.toString(block.chainid), ")"));
34+
}
35+
36+
RECIPIENTS = vm.parseJsonAddressArray(funding, ".recipients");
37+
FUNDS = vm.parseJsonUintArray(funding, ".funds");
38+
39+
uint256 totalFunds = 0;
40+
RECIPIENT_BALANCES_BEFORE = new uint256[](RECIPIENTS.length);
41+
for (uint256 i; i < RECIPIENTS.length; i++) {
42+
RECIPIENT_BALANCES_BEFORE[i] = RECIPIENTS[i].balance;
43+
totalFunds += FUNDS[i];
44+
}
45+
46+
SAFE_BALANCE_BEFORE = SAFE.balance;
47+
TOTAL_FUNDS = totalFunds;
48+
}
49+
50+
function setUp() public view {
51+
_precheck();
52+
}
53+
54+
function _precheck() internal view {
55+
require(RECIPIENTS.length == FUNDS.length, "RECIPIENTS and FUNDS not same length");
56+
require(RECIPIENTS.length > 0, "RECIPIENTS and FUNDS empty");
57+
require(SAFE.balance >= TOTAL_FUNDS, "SAFE not enough balance");
58+
}
59+
60+
function _buildCalls() internal view override returns (IMulticall3.Call3Value[] memory) {
61+
IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](RECIPIENTS.length);
62+
for (uint256 i; i < RECIPIENTS.length; i++) {
63+
calls[i] =
64+
IMulticall3.Call3Value({target: RECIPIENTS[i], allowFailure: false, callData: "", value: FUNDS[i]});
65+
}
66+
67+
return calls;
68+
}
69+
70+
function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal view override {
71+
for (uint256 i; i < RECIPIENTS.length; i++) {
72+
vm.assertEq(
73+
RECIPIENTS[i].balance, RECIPIENT_BALANCES_BEFORE[i] + FUNDS[i], "Recipient balance is not correct"
74+
);
75+
}
76+
77+
vm.assertEq(SAFE.balance, SAFE_BALANCE_BEFORE - TOTAL_FUNDS, "Owner safe balance is not correct");
78+
}
79+
80+
function _ownerSafe() internal view override returns (address) {
81+
return SAFE;
82+
}
83+
}

0 commit comments

Comments
 (0)