Skip to content

Commit 6efdbff

Browse files
authored
docs: update README (#2289)
1 parent 72eee1e commit 6efdbff

5 files changed

Lines changed: 232 additions & 33 deletions

File tree

claim_contracts/Makefile

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.PHONY: help \
22
calldata-update-merkle-root calldata-update-limit-timestamp calldata-approve-spending calldata-unpause calldata-pause \
33
deploy-token deploy-token-sepolia deploy-token-mainnet \
4-
deploy-claimable-local deploy-claimable-sepolia deploy-claimable-base-sepolia deploy-claimable-mainnet \
4+
deploy-claimable-local deploy-claimable-sepolia deploy-claimable-base-sepolia deploy-claimable-mainnet deploy-claimable-base-mainnet \
55
update_token_proxy upgrade-token enable-claimability \
66
approve-claimable claimable-update-root claimable-get-root claimable-update-timestamp claimable-get-timestamp claimable-pause claimable-unpause \
77
upgrade-aligned-token-implementation \
@@ -12,15 +12,18 @@
1212
# ============================================================================
1313
# Configuration — override any of these on the command line, e.g.
1414
# make deploy-claimable-sepolia DEPLOYER_PRIVATE_KEY=0x... ETHERSCAN_API_KEY=...
15-
# (ETHERSCAN_API_KEY, KEYSTORE_PATH, MERKLE_ROOT, TIMESTAMP, CLAIM_PROXY_ADDRESS,
15+
# (ETHERSCAN_API_KEY, MERKLE_ROOT, TIMESTAMP, CLAIM_PROXY_ADDRESS,
1616
# LIMIT_TIMESTAMP, etc. have no default and are passed in per invocation.)
17+
# The mainnet deploy targets sign with --interactive, so they prompt for the key instead of
18+
# reading it from a variable or a keystore path.
1719
# ============================================================================
1820

1921
# RPC endpoints (per network)
2022
RPC_URL ?= http://localhost:8545
2123
SEPOLIA_RPC_URL ?= https://ethereum-sepolia-rpc.publicnode.com
2224
BASE_SEPOLIA_RPC_URL ?= https://sepolia.base.org
2325
MAINNET_RPC_URL ?= https://ethereum-rpc.publicnode.com
26+
BASE_MAINNET_RPC_URL ?= https://mainnet.base.org
2427

2528
# Signing keys (anvil defaults, for local use)
2629
DEPLOYER_PRIVATE_KEY ?= 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
@@ -48,8 +51,8 @@ calldata-update-merkle-root: ## 💾 Calldata for the method `updateMerkleRoot`
4851
calldata-update-limit-timestamp: ## 💾 Calldata for the method `extendClaimPeriod` to use in a transaction
4952
cast calldata "extendClaimPeriod(uint256)" $(LIMIT_TIMESTAMP)
5053

51-
calldata-approve-spending: ## 💾 Calldata for the method `approve` to use in a transaction
52-
cast calldata "approve(address,uint256)" $(CLAIM_PROXY_ADDRESS) 2600000000000000000000000000
54+
calldata-approve-spending: ## 💾 Calldata for the method `approve` to use in a transaction (override APPROVE_AMOUNT)
55+
cast calldata "approve(address,uint256)" $(CLAIM_PROXY_ADDRESS) $(APPROVE_AMOUNT)
5356

5457
calldata-unpause: ## 💾 Calldata for the method `unpause` to use in a transaction
5558
cast calldata "unpause()"
@@ -83,9 +86,11 @@ deploy-token-mainnet: ## 🚀 Deploy the token contract in Mainnet
8386
forge script DeployAlignedToken.s.sol \
8487
--sig "run(string)" \
8588
mainnet \
86-
--keystore $(KEYSTORE_PATH) \
89+
--interactive \
8790
--rpc-url $(MAINNET_RPC_URL) \
8891
--broadcast \
92+
--verify \
93+
--etherscan-api-key $(ETHERSCAN_API_KEY) \
8994
--verbosity 3
9095

9196
deploy-claimable-local: ## 🚀 Deploy the airdrop contract in localnet
@@ -120,12 +125,22 @@ deploy-claimable-mainnet: ## 🚀 Deploy the airdrop contract in Mainnet
120125
cd script && \
121126
forge script DeployClaimableAirdrop.s.sol \
122127
--sig "run(string)" mainnet \
123-
--keystore $(KEYSTORE_PATH) \
128+
--interactive \
124129
--rpc-url $(MAINNET_RPC_URL) \
125130
--broadcast \
126131
--verify \
127132
--etherscan-api-key $(ETHERSCAN_API_KEY)
128133

134+
deploy-claimable-base-mainnet: ## 🚀 Deploy the airdrop contract in Base Mainnet
135+
cd script && \
136+
forge script DeployClaimableAirdrop.s.sol \
137+
--sig "run(string)" base-mainnet \
138+
--interactive \
139+
--rpc-url $(BASE_MAINNET_RPC_URL) \
140+
--broadcast \
141+
--verify \
142+
--etherscan-api-key $(ETHERSCAN_API_KEY)
143+
129144

130145
# TODO: broken — reads script-out/deployed_token_addresses.json (never generated by the deploy
131146
# scripts) and treats $(CONFIG) as a file path. Fix or remove.

claim_contracts/README.md

Lines changed: 127 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,27 @@ Each environment below is a single flow: **deploy → enable claiming**.
1010
## Prerequisites
1111

1212
- [Foundry](https://book.getfoundry.sh/getting-started/installation).
13-
- A funded deployer account private key (a keystore for mainnet).
13+
- A funded deployer account private key. The mainnet targets sign with `--interactive`, so they
14+
prompt for the key rather than reading a variable or a keystore path.
1415
- An Etherscan API key to verify the deployed contract (the same key works for Base via the
1516
Etherscan v2 API).
1617

18+
### Who signs what on mainnet
19+
20+
On mainnet every privileged action goes through a Safe, and **each chain has its own pair of safes**.
21+
The Ethereum foundation safe may not be the Base foundation safe; the same holds for the
22+
distributors. Don't assume they match — fill each chain's config from that chain's safes rather than
23+
copying addresses across.
24+
25+
| | Ethereum mainnet | Base mainnet |
26+
| --- | --- | --- |
27+
| **Foundation** safe — claim contract owner + proxy admin owner | `foundation` in `config.mainnet.json` | `foundation` in `config.base-mainnet.json` |
28+
| **Distributor** safe — holds the tokens, approves the claim contract | `tokenDistributor` in `config.mainnet.json` | `tokenDistributor` in `config.base-mainnet.json` |
29+
30+
Deployment is the one exception: `forge script` signs with an ordinary **hot EOA**, which needs no
31+
privileges at all — ownership is assigned from the config's `foundation` at initialization, not from
32+
the deployer. Don't try to run a deploy from a Safe.
33+
1734
## Local (anvil)
1835

1936
A single anvil chain that deploys the token + claim contract and turns claiming on.
@@ -88,27 +105,90 @@ which it is right after deployment. Each step is also available as its own targe
88105

89106
## Mainnet
90107

91-
Covers Ethereum mainnet today. **Base mainnet: to be added later** (no claim target yet).
108+
Both chains: **Ethereum mainnet** and **Base mainnet**. Each needs its own claim contract, its own
109+
merkle root, and its own pair of safes (see [Who signs what on mainnet](#who-signs-what-on-mainnet)).
110+
The ALIGN token already exists on both — you only deploy and enable `ClaimableAirdrop`.
111+
112+
Run Ethereum first, then Base; the two are independent apart from sharing the same
113+
`START_TIMESTAMP` and claim deadline.
92114

93-
### 1. Deploy (Ethereum mainnet)
115+
### Ethereum mainnet
94116

95-
Fill `script-config/config.mainnet.json` with `foundation`, `tokenDistributor`, and `tokenProxy`
96-
(the mainnet ALIGN token), then:
117+
#### 1. Deploy
118+
119+
Fill `script-config/config.mainnet.json` with the Ethereum `foundation` and `tokenDistributor` safes.
120+
`tokenProxy` is already the mainnet ALIGN token.
97121

98122
```
99-
make deploy-claimable-mainnet KEYSTORE_PATH=<keystore-path> ETHERSCAN_API_KEY=<key>
123+
make deploy-claimable-mainnet ETHERSCAN_API_KEY=<key>
100124
```
101125

102-
Note the claimable proxy address printed in the output.
126+
Prompts for the deployer key. Note the claimable **proxy** address printed in the output (not the
127+
implementation).
103128

104129
> [!NOTE]
105-
> The mainnet ALIGN token already exists — use its address as `tokenProxy`. To deploy the token
106-
> from scratch on Ethereum: `make deploy-token-mainnet KEYSTORE_PATH=<keystore-path>`.
130+
> To deploy the token from scratch on Ethereum: `make deploy-token-mainnet ETHERSCAN_API_KEY=<key>`.
131+
> You should not need this — the ALIGN token is already deployed.
132+
133+
#### 2. Enable claiming
134+
135+
Follow [Enable claiming from the safes](#enable-claiming-from-the-safes) with:
136+
137+
- `AIRDROP` = the claim proxy you just deployed, `TOKEN` = the mainnet ALIGN token
138+
- `MERKLE_ROOT` = the **ethereum** root
139+
- Owner steps from the **Ethereum foundation** safe, approve from the **Ethereum distributor** safe
140+
141+
### Base mainnet
142+
143+
#### 1. Token — already deployed, but its supply has to be bridged
144+
145+
The Base ALIGN token is an `OptimismMintableERC20` created through the OP factory, not with forge —
146+
see [`base/`](base/README.md). It is pre-filled as `tokenProxy` in
147+
`script-config/config.base-mainnet.json`.
148+
149+
Its balance on Base only exists to the extent it has been **bridged from Ethereum**. Claims call
150+
`safeTransferFrom` on the distributor, so before enabling claiming the **Base distributor safe** must
151+
hold at least the total claimable on Base.
152+
153+
#### 2. Fund the Base distributor safe
154+
155+
The **Ethereum distributor** safe bridges to the **Base distributor** safe. Since the two may be
156+
different addresses, the deposit has to name the destination explicitly — otherwise the tokens land
157+
at whatever address matches the sender on Base, which may be one nobody controls. Full walkthrough in
158+
[`base/`](base/README.md#bridge-l1---base).
159+
160+
Takes ~20 minutes to land. Verify before continuing:
161+
162+
```
163+
cast call <base-align-token> 'balanceOf(address)(uint256)' <base-distributor-safe> \
164+
--rpc-url https://mainnet.base.org
165+
```
166+
167+
#### 3. Deploy
168+
169+
Fill `script-config/config.base-mainnet.json` with the **Base** `foundation` and `tokenDistributor`
170+
safes, then:
171+
172+
```
173+
make deploy-claimable-base-mainnet ETHERSCAN_API_KEY=<key>
174+
```
175+
176+
Note the claimable proxy address printed in the output.
177+
178+
#### 4. Enable claiming
179+
180+
Follow [Enable claiming from the safes](#enable-claiming-from-the-safes) with:
107181

108-
### 2. Enable claiming (foundation multisig)
182+
- `AIRDROP` = the Base claim proxy, `TOKEN` = the Base ALIGN token
183+
- `MERKLE_ROOT` = the **base** root
184+
- Owner steps from the **Base foundation** safe, approve from the **Base distributor** safe
185+
- `APPROVE_AMOUNT` = the amount actually bridged, not the 2.6B default
109186

110-
On mainnet the owner is the foundation safe, so you generate the calldata for each step and execute
111-
it from the multisig rather than sending the transactions directly.
187+
### Enable claiming from the safes
188+
189+
The owner is a Safe, so you generate the calldata for each step and execute it from the multisig
190+
rather than sending transactions directly. This section applies to **either** chain — use that
191+
chain's safes, root, token and claim proxy throughout.
112192

113193
> [!IMPORTANT]
114194
>
@@ -118,20 +198,38 @@ it from the multisig rather than sending the transactions directly.
118198
> anything.
119199
> - Steps 1, 2 and 4 are owner actions and can be batched in one multisig transaction. Step 3 must
120200
> be done by the token-distributor safe (it holds the tokens).
201+
> - `unpause` goes **last**: `updateMerkleRoot` and `extendClaimPeriod` are `whenPaused`.
121202
122203
> [!WARNING]
123204
> Double-check the data you pass into these commands — any mistake can lead to undesired behavior.
205+
> In particular, use the **ethereum** root on the Ethereum contract and the **base** root on the
206+
> Base contract. Crossed roots deploy and enable without complaint and fail only when a user claims,
207+
> with `Invalid Merkle proof`.
208+
209+
1. Merkle root, from the foundation safe:
210+
`make calldata-update-merkle-root MERKLE_ROOT=<that-chain's-root>`
211+
2. Claim deadline, from the foundation safe:
212+
`make calldata-update-limit-timestamp LIMIT_TIMESTAMP=<timestamp>`
213+
3. Approve spending, from the token-distributor safe, **sent to that chain's token contract**:
214+
`make calldata-approve-spending CLAIM_PROXY_ADDRESS=<claimable-proxy> APPROVE_AMOUNT=<amount>`
215+
(`APPROVE_AMOUNT` defaults to 2.6B; on Base pass the bridged amount instead)
216+
4. Unpause, from the foundation safe: `make calldata-unpause`
124217

125-
1. Merkle root (use the **ethereum** root for the mainnet contract):
126-
`make calldata-update-merkle-root MERKLE_ROOT=<root>`
127-
2. Claim deadline: `make calldata-update-limit-timestamp LIMIT_TIMESTAMP=<timestamp>`
128-
3. Approve spending, run by the token-distributor safe:
129-
`make calldata-approve-spending CLAIM_PROXY_ADDRESS=<claimable-proxy>`
130-
4. Unpause: `make calldata-unpause`
218+
Submit each piece of calldata as a transaction from the appropriate safe.
131219

132-
Submit each piece of calldata as a transaction from the appropriate safe. The same per-network root
133-
mapping (ethereum root on the Ethereum contract, base root on the Base contract) applies once Base
134-
mainnet is added.
220+
### Verify (either chain, read-only)
221+
222+
```
223+
make claimable-get-root AIRDROP=<claim-proxy> RPC_URL=<rpc> # that chain's root
224+
make claimable-get-timestamp AIRDROP=<claim-proxy> RPC_URL=<rpc> # the claim deadline
225+
make test-airdrop AIRDROP=<claim-proxy> RPC_URL=<rpc> # paused()=false, owner()=safe
226+
227+
cast call <token> 'allowance(address,address)(uint256)' \
228+
<distributor-safe> <claim-proxy> --rpc-url <rpc>
229+
```
230+
231+
`RPC_URL` has no mainnet default — pass `https://ethereum-rpc.publicnode.com` or
232+
`https://mainnet.base.org` explicitly, or the read hits `http://localhost:8545`.
135233

136234
## Upgrades
137235

@@ -140,6 +238,14 @@ To upgrade a contract, first make sure you pause the contract if it's not paused
140238
> [!NOTE]
141239
> The ERC20 cannot be paused. Only the claimable airdrop proxy can be paused.
142240
241+
> [!IMPORTANT]
242+
> Upgrades are **per chain**. Ethereum mainnet and Base mainnet have separate proxies, separate proxy
243+
> admins and separate foundation safes, so an upgrade has to be repeated on each chain with that
244+
> chain's config (`config.mainnet.json` / `config.base-mainnet.json`), and the final transaction must
245+
> be submitted from that chain's foundation safe. Note the upgrade script below reads a different
246+
> shape from the deploy scripts — `foundation` plus `contractProxy` — so don't overwrite the
247+
> `tokenDistributor`/`tokenProxy` values a redeploy would need.
248+
143249
```
144250
git clone git@github.com:yetanotherco/aligned_layer.git && cd aligned_layer/claim_contracts
145251
```

0 commit comments

Comments
 (0)