From 8baa95f6c6a5d0dfbf503590ac9617f8e89cd8a4 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:42:45 -0300 Subject: [PATCH] feat: update claim_contract README and Makefile --- claim_contracts/Makefile | 99 +++++--- claim_contracts/README.md | 219 ++++++++++-------- .../config.base-sepolia.example.json | 5 + .../script-config/config.sepolia.example.json | 5 + 4 files changed, 205 insertions(+), 123 deletions(-) create mode 100644 claim_contracts/script-config/config.base-sepolia.example.json create mode 100644 claim_contracts/script-config/config.sepolia.example.json diff --git a/claim_contracts/Makefile b/claim_contracts/Makefile index c1ae58d0bc..1be3597241 100644 --- a/claim_contracts/Makefile +++ b/claim_contracts/Makefile @@ -1,5 +1,41 @@ -.PHONY: help deploy-aligned-token-implementation deploy-aligned-token-proxy deploy-claimable-airdrop-implementation deploy-claimable-airdrop-proxy upgrade-aligned-token-implementation aligned-token-proxy-deploy-data aligned-token-init-data aligned-token-upgrade-data aligned-token-create2 aligned-token-proxy-create2 - +.PHONY: help \ + calldata-update-merkle-root calldata-update-limit-timestamp calldata-approve-spending calldata-unpause calldata-pause \ + deploy-token deploy-token-sepolia deploy-token-mainnet \ + deploy-claimable-local deploy-claimable-sepolia deploy-claimable-base-sepolia deploy-claimable-mainnet \ + update_token_proxy upgrade-token enable-claimability \ + approve-claimable claimable-update-root claimable-get-root claimable-update-timestamp claimable-get-timestamp claimable-pause claimable-unpause \ + upgrade-aligned-token-implementation \ + test-token test-claim test-claimed test-airdrop \ + deploy-example + + +# ============================================================================ +# Configuration — override any of these on the command line, e.g. +# make deploy-claimable-sepolia DEPLOYER_PRIVATE_KEY=0x... ETHERSCAN_API_KEY=... +# (ETHERSCAN_API_KEY, KEYSTORE_PATH, MERKLE_ROOT, TIMESTAMP, CLAIM_PROXY_ADDRESS, +# LIMIT_TIMESTAMP, etc. have no default and are passed in per invocation.) +# ============================================================================ + +# RPC endpoints (per network) +RPC_URL ?= http://localhost:8545 +SEPOLIA_RPC_URL ?= https://ethereum-sepolia-rpc.publicnode.com +BASE_SEPOLIA_RPC_URL ?= https://sepolia.base.org +MAINNET_RPC_URL ?= https://ethereum-rpc.publicnode.com + +# Signing keys (anvil defaults, for local use) +DEPLOYER_PRIVATE_KEY ?= 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a +DISTRIBUTOR_PRIVATE_KEY ?= 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d +OWNER_PRIVATE_KEY ?= 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a + +# Deploy config + contract addresses (anvil defaults) +CONFIG ?= example +AIRDROP ?= 0xBC9129Dc0487fc2E169941C75aABC539f208fb01 +TOKEN ?= 0x2E983A1Ba5e8b38AAAeC4B440B9dDcFBf72E15d1 +APPROVE_AMOUNT ?= 2600000000000000000000000000 + +# Test inputs — require the proof API running on localhost:4000 +AMOUNT_TO_CLAIM = $(shell curl -S -H "Content-Type: application/json" http://localhost:4000/api/proof/\$(CLAIMER) | jq -r .amount) +MERKLE_PROOF_TO_CLAIM = $(shell curl -S -H "Content-Type: application/json" http://localhost:4000/api/proof/\$(CLAIMER) | jq .proof | tr -d '"\n ') help: ## 📚 Show help for each of the Makefile recipes @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @@ -23,9 +59,6 @@ calldata-pause: ## 💾 Calldata for the method `pause` to use in a transaction # Deployments -RPC_URL?=http://localhost:8545 -DEPLOYER_PRIVATE_KEY?=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a -CONFIG?=example deploy-token: ## 🚀 Deploy the token contract cd script && \ forge script DeployAlignedToken.s.sol \ @@ -33,26 +66,25 @@ deploy-token: ## 🚀 Deploy the token contract $(CONFIG) \ --private-key $(DEPLOYER_PRIVATE_KEY) \ --rpc-url $(RPC_URL) \ - --broadcast \ - --verbosity 3 + --broadcast -deploy-token-testnet: ## 🚀 Deploy the token contract +deploy-token-sepolia: ## 🚀 Deploy the token contract in Sepolia cd script && \ forge script DeployAlignedToken.s.sol \ --sig "run(string)" sepolia \ --private-key $(DEPLOYER_PRIVATE_KEY) \ - --rpc-url $(RPC_URL) \ + --rpc-url $(SEPOLIA_RPC_URL) \ --broadcast \ --verify \ --etherscan-api-key $(ETHERSCAN_API_KEY) -deploy-token-prod: ## 🚀 Deploy the token contract +deploy-token-mainnet: ## 🚀 Deploy the token contract in Mainnet cd script && \ forge script DeployAlignedToken.s.sol \ --sig "run(string)" \ - $(PROD_CONFIG) \ + mainnet \ --keystore $(KEYSTORE_PATH) \ - --rpc-url $(PROD_RPC_URL) \ + --rpc-url $(MAINNET_RPC_URL) \ --broadcast \ --verbosity 3 @@ -64,12 +96,22 @@ deploy-claimable-local: ## 🚀 Deploy the airdrop contract in localnet --rpc-url http://localhost:8545 \ --broadcast -deploy-claimable-testnet: ## 🚀 Deploy the airdrop contract in Sepolia +deploy-claimable-sepolia: ## 🚀 Deploy the airdrop contract in Sepolia cd script && \ forge script DeployClaimableAirdrop.s.sol \ --sig "run(string)" sepolia \ --private-key $(DEPLOYER_PRIVATE_KEY) \ - --rpc-url $(RPC_URL) \ + --rpc-url $(SEPOLIA_RPC_URL) \ + --broadcast \ + --verify \ + --etherscan-api-key $(ETHERSCAN_API_KEY) + +deploy-claimable-base-sepolia: ## 🚀 Deploy the airdrop contract in Base Sepolia + cd script && \ + forge script DeployClaimableAirdrop.s.sol \ + --sig "run(string)" base-sepolia \ + --private-key $(DEPLOYER_PRIVATE_KEY) \ + --rpc-url $(BASE_SEPOLIA_RPC_URL) \ --broadcast \ --verify \ --etherscan-api-key $(ETHERSCAN_API_KEY) @@ -79,40 +121,41 @@ deploy-claimable-mainnet: ## 🚀 Deploy the airdrop contract in Mainnet forge script DeployClaimableAirdrop.s.sol \ --sig "run(string)" mainnet \ --keystore $(KEYSTORE_PATH) \ - --rpc-url https://eth.llamarpc.com \ + --rpc-url $(MAINNET_RPC_URL) \ --broadcast \ --verify \ --etherscan-api-key $(ETHERSCAN_API_KEY) +# TODO: broken — reads script-out/deployed_token_addresses.json (never generated by the deploy +# scripts) and treats $(CONFIG) as a file path. Fix or remove. update_token_proxy: @NEW_TOKEN_PROXY=$$(jq -r '.tokenProxy' "script-out/deployed_token_addresses.json") && \ jq --arg new_proxy "$$NEW_TOKEN_PROXY" '.tokenProxy = $$new_proxy' $(CONFIG) > $(CONFIG).tmp \ && mv $(CONFIG).tmp $(CONFIG) +# TODO: broken — runs UpgradeToken.s.sol, which does not exist in script/. Fix or remove +# (see the README "Upgrades" section, which expects the upgrade script to be written ad-hoc). upgrade-token: ## 🚀 Upgrade the token contract cd script && \ forge script UpgradeToken.s.sol \ --sig "run(string)" \ $(CONFIG) \ - --private-key $(PRIVATE_KEY) \ + --private-key $(DEPLOYER_PRIVATE_KEY) \ --rpc-url $(RPC_URL) \ --broadcast \ --verbosity 3 # Miscellaneous -DISTRIBUTOR_PRIVATE_KEY?=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -OWNER_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a -AIRDROP=0xBC9129Dc0487fc2E169941C75aABC539f208fb01 -TOKEN=0x2E983A1Ba5e8b38AAAeC4B440B9dDcFBf72E15d1 +enable-claimability: claimable-update-root claimable-update-timestamp approve-claimable claimable-unpause ## 🚀 Run every tx needed to start claiming (set root, set deadline, approve, unpause) approve-claimable: ## 🚀 Approve the ClaimableAirdrop contract to spend the token cast send \ --rpc-url $(RPC_URL) \ --private-key $(DISTRIBUTOR_PRIVATE_KEY) \ $(TOKEN) \ - 'approve(address,uint256)' $(AIRDROP) 2600000000000000000000000000 + 'approve(address,uint256)' $(AIRDROP) $(APPROVE_AMOUNT) claimable-update-root: ## 🚀 Update the merkle root of the ClaimableAirdrop contract cast send \ @@ -154,13 +197,15 @@ claimable-unpause: # Upgrades +# TODO: broken — script/aligned_token/UpgradeAlignedTokenImplementation.s.sol doesn't exist and +# --sig has a stray leading "function". Fix or remove. upgrade-aligned-token-implementation: ## 🚀 Upgrade the AlignedToken implementation contract cd script/aligned_token && \ forge script UpgradeAlignedTokenImplementation.s.sol \ --sig "function run(address,address,uint256,address,address,address,address,uint256)" \ $(PROXY) $(IMPLEMENTATION) $(VERSION) $(OWNER) $(BENEFICIARY1) $(BENEFICIARY2) $(BENEFICIARY3) $(MINT)\ --rpc-url $(RPC_URL) \ - --private-key $(PRIVATE_KEY) \ + --private-key $(DEPLOYER_PRIVATE_KEY) \ --broadcast # Test targets @@ -170,21 +215,19 @@ test-token: cast call $(ADDRESS) "symbol()(string)" --rpc-url $(RPC_URL) cast call $(ADDRESS) "totalSupply()(uint256)" --rpc-url $(RPC_URL) -# The following target needs the proof API running on localhost:4000 -AMOUNT_TO_CLAIM=$(shell curl -S -H "Content-Type: application/json" http://localhost:4000/api/proof/\$(CLAIMER) | jq -r .amount) -MERKLE_PROOF_TO_CLAIM=$(shell curl -S -H "Content-Type: application/json" http://localhost:4000/api/proof/\$(CLAIMER) | jq .proof | tr -d '"\n ') +# TODO: outdated — claim is now claim(uint256 amount,uint256 validFrom,bytes32[] proof) and the +# proof comes from the web app's /api/wallets/
, not /api/proof/. Fix. test-claim: cast send $(AIRDROP) --private-key $(CLAIMER_PRIVATE_KEY) "claim(uint256,bytes32[])" $(AMOUNT_TO_CLAIM) "$(MERKLE_PROOF_TO_CLAIM)" --rpc-url $(RPC_URL) +# TODO: outdated — hasClaimed is now hasClaimed(bytes32 leaf), not hasClaimed(address). Fix. test-claimed: cast call $(AIRDROP) "hasClaimed(address)(bool)" $(CLAIMER) --rpc-url $(RPC_URL) cast balance --erc20 $(TOKEN) $(CLAIMER) --rpc-url $(RPC_URL) -OWNER_PRIVATE_KEY?=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 - test-airdrop: cast call $(AIRDROP) "paused()(bool)" --rpc-url $(RPC_URL) cast call $(AIRDROP) "owner()(address)" --rpc-url $(RPC_URL) # Requires MERKLE_ROOT, TIMESTAMP -deploy-example: deploy-token deploy-claimable-local claimable-update-root claimable-update-timestamp approve-claimable claimable-unpause +deploy-example: deploy-token deploy-claimable-local enable-claimability diff --git a/claim_contracts/README.md b/claim_contracts/README.md index 65c2ba6a13..52dfc736ce 100644 --- a/claim_contracts/README.md +++ b/claim_contracts/README.md @@ -1,112 +1,141 @@ -# +# ALIGN Claim Contracts -## Local +This repo deploys the **AlignedToken** (ALIGN) and the **ClaimableAirdrop** contract (both behind +Transparent proxies). The airdrop is split across **Ethereum and Base** — each network has its own +claim contract and its own merkle root (the `ethereum` root and the `base` root produced by the +merkle generator in `aligned_airdrop_web`). -### Requisites +Each environment below is a single flow: **deploy → enable claiming**. -- Foundry +## Prerequisites -### Run +- [Foundry](https://book.getfoundry.sh/getting-started/installation). +- A funded deployer account private key (a keystore for mainnet). +- An Etherscan API key to verify the deployed contract (the same key works for Base via the + Etherscan v2 API). -1. Run anvil in one terminal: - ``` - anvil - ``` -2. Deploy the token - ``` - make deploy-token - ``` -3. Write down the token proxy address that is printed in the console output. Do this in the `config.example.json` file, under the `tokenProxy` key. -4. Deploy the claimable contract - ``` - make deploy-claimable-local - ``` -5. Write down the claimable contract proxy address that is printed in the console output. +## Local (anvil) -## Testnet (Sepolia) +A single anvil chain that deploys the token + claim contract and turns claiming on. -### Requisites +One-shot (deploy everything and enable claiming): -- Foundry -- Etherscan API key +``` +make deploy-example MERKLE_ROOT= TIMESTAMP= +``` + +This runs `deploy-token` → `deploy-claimable-local` → set root → set deadline → approve → unpause. + +Or step by step, if you need the deployed addresses along the way: + +1. Start anvil in another terminal: `anvil` +2. Deploy the token: `make deploy-token` +3. Copy the printed token proxy address into `script-config/config.example.json`, under `tokenProxy`. +4. Deploy the claim contract: `make deploy-claimable-local` +5. Enable claiming: `make enable-claimability MERKLE_ROOT= TIMESTAMP=` + +## Sepolia (testnet) + +> [!NOTE] +> The ALIGN token is already deployed on both testnets (Ethereum Sepolia +> `0xd2Fd114f098b355321cB3424400f3CC6a0d75C9A`, Base Sepolia +> `0x4AAcFbc2C31598a560b285dB20966E00B73F9F81`) and the configs already point `tokenProxy` at it, +> so you only deploy and enable `ClaimableAirdrop` — on **both** chains. +> +> Need to (re)deploy the token? On Ethereum Sepolia run +> `make deploy-token-sepolia DEPLOYER_PRIVATE_KEY= ETHERSCAN_API_KEY=`; on Base Sepolia the +> L2 token is created through the OP bridge factory, not forge (see [`base/`](base/README.md)). Then +> put the new address under `tokenProxy` in the config. + +### 1. Deploy -### Run +**Ethereum Sepolia** — fill `foundation` (contract owner) and `tokenDistributor` (the account +holding ALIGN to distribute) in `script-config/config.sepolia.json`, then: -1. Create a file `script-config/config.sepolia.json` following the example in `script-config/config.sepolia.example.json`. -2. Deploy the token - ``` - make deploy-token-testnet RPC_URL= PRIVATE_KEY= - ``` -3. Write down the `token-proxy-address` that is printed in the console output. Do this in the `config.sepolia.json` file, under the `tokenProxy` key. -4. Deploy the claimable contract - ``` - make deploy-claimable-testnet RPC_URL= DEPLOYER_PRIVATE_KEY= ETHERSCAN_API_KEY= - ``` -5. Write down the `claimable-proxy-address` that is printed in the console output. +``` +make deploy-claimable-sepolia DEPLOYER_PRIVATE_KEY= ETHERSCAN_API_KEY= +``` + +**Base Sepolia** — bridge ALIGN to your `tokenDistributor` first (see [`base/`](base/README.md)), +fill `foundation` and `tokenDistributor` in `script-config/config.base-sepolia.json`, then: + +``` +make deploy-claimable-base-sepolia DEPLOYER_PRIVATE_KEY= ETHERSCAN_API_KEY= +``` + +Note the claimable proxy address printed for each. + +### 2. Enable claiming -## Enabling Claimability +Run once per network (the owner/distributor are plain accounts on testnet): -### By Calldata +``` +make enable-claimability \ + AIRDROP= TOKEN= \ + MERKLE_ROOT= TIMESTAMP= \ + OWNER_PRIVATE_KEY= DISTRIBUTOR_PRIVATE_KEY= \ + RPC_URL= +``` + +> [!IMPORTANT] +> Use the **ethereum** root on the Sepolia contract and the **base** root on the Base Sepolia +> contract. + +This runs, in order: `updateMerkleRoot` → `extendClaimPeriod` → `approve` (2.6B by default, +override with `APPROVE_AMOUNT=`) → `unpause`. The contract must be paused for the first two steps, +which it is right after deployment. Each step is also available as its own target +(`claimable-update-root`, `claimable-update-timestamp`, `approve-claimable`, `claimable-unpause`). + +## Mainnet + +Covers Ethereum mainnet today. **Base mainnet: to be added later** (no claim target yet). + +### 1. Deploy (Ethereum mainnet) + +Fill `script-config/config.mainnet.json` with `foundation`, `tokenDistributor`, and `tokenProxy` +(the mainnet ALIGN token), then: + +``` +make deploy-claimable-mainnet KEYSTORE_PATH= ETHERSCAN_API_KEY= +``` + +Note the claimable proxy address printed in the output. + +> [!NOTE] +> The mainnet ALIGN token already exists — use its address as `tokenProxy`. To deploy the token +> from scratch on Ethereum: `make deploy-token-mainnet KEYSTORE_PATH=`. + +### 2. Enable claiming (foundation multisig) + +On mainnet the owner is the foundation safe, so you generate the calldata for each step and execute +it from the multisig rather than sending the transactions directly. > [!IMPORTANT] > -> - This step-by-step **assumes** that the claimable proxy contract **is already deployed** and that **is already paused**. If it is not paused, the first transaction should be to pause it using this calldata `cast calldata "pause()"`. -> - This method **only** generates the necessary calldata to call the methods through transactions. It does **not** actually call the methods. This method is useful for copy-pasting the calldata into a multisig wallet. -> - Steps 1, 2, and 4 can be batched into a single transaction in a multisig wallet. This multisig must be the `ClaimableAirdrop` contract owner. -> - Step 3 must be done by the token distributor multisig as it is the one that has the tokens to be claimed. +> - This assumes the claim proxy is **already deployed** and **paused** (it is right after deploy). +> If it is not paused, pause it first with `make calldata-pause`. +> - These targets only **generate calldata** to copy into a multisig transaction; they do not send +> anything. +> - Steps 1, 2 and 4 are owner actions and can be batched in one multisig transaction. Step 3 must +> be done by the token-distributor safe (it holds the tokens). > [!WARNING] -> - Double-check the data you passing into the commands, any mistake can lead to undesired behavior. - -1. Update the merkle root - ``` - // Example merkle_root = 0x97619aea42a289b94acc9fb98f5030576fa7449f1dd6701275815a6e99441927 - cast calldata "updateMerkleRoot(bytes32)" - ``` -2. Update the claim time limit - ``` - // Example timestamp = 2733427549 - cast calldata "extendClaimPeriod(uint256)" - ``` -3. Approve the claimable proxy contract to spend the token from the distributor (_2.6B, taking into account the 18 decimals_) - ``` - // Example claim_proxy_address = 0x0234947ce63d1a5E731e5700b911FB32ec54C3c6 - cast calldata "approve(address,uint256)" 2600000000000000000000000000 - ``` -4. Unpause the claimable contract (it is paused by default) - ``` - cast calldata "unpause()" - ``` - -### Local - -1. Deploy the claimable contract as explained above. -2. Set the correct merkle root - ``` - make claimable-update-root MERKLE_ROOT= - ``` -3. Set the correct claim time limit - ``` - make claimable-update-timestamp TIMESTAMP=2733427549 - ``` -4. Approve the claimable contract to spend the token from the distributor - ``` - make approve-claimable - ``` -5. Unpause the claimable contract - ``` - make claimable-unpause - ``` - -or - -``` -make deploy-example MERKLE_ROOT= TIMESTAMP=2733427549 -``` - -# Contract upgrade instructions - -To upgrade a contract, first make sure you pause the contract if it's not paused already (NOTE: the erc20 cannot be paused, the claim contract can though). Once that's done, clone the `aligned_layer` repo and go into the `claim_contracts` directory: +> Double-check the data you pass into these commands — any mistake can lead to undesired behavior. + +1. Merkle root (use the **ethereum** root for the mainnet contract): + `make calldata-update-merkle-root MERKLE_ROOT=` +2. Claim deadline: `make calldata-update-limit-timestamp LIMIT_TIMESTAMP=` +3. Approve spending, run by the token-distributor safe: + `make calldata-approve-spending CLAIM_PROXY_ADDRESS=` +4. Unpause: `make calldata-unpause` + +Submit each piece of calldata as a transaction from the appropriate safe. The same per-network root +mapping (ethereum root on the Ethereum contract, base root on the Base contract) applies once Base +mainnet is added. + +## Upgrades + +To upgrade a contract, first make sure you pause the contract if it's not paused already. Once that's done, clone the `aligned_layer` repo and go into the `claim_contracts` directory: > [!NOTE] > The ERC20 cannot be paused. Only the claimable airdrop proxy can be paused. @@ -115,7 +144,7 @@ To upgrade a contract, first make sure you pause the contract if it's not paused git clone git@github.com:yetanotherco/aligned_layer.git && cd aligned_layer/claim_contracts ``` -## Write the new contract implementation +### Write the new contract implementation This implementation will most likely be a copy paste of the old implementation, only with one or few changes. In addition to that, there is one thing that MUST be done on this new contract: @@ -134,7 +163,7 @@ function reinitialize() public reinitializer(2) { Put the new implementation in a file inside the `src` directory with an appropriate name. -## Write the deployment script +### Write the deployment script Under the `script` directory, create a new forge script (with the `.s.sol` extension) with a name like `UpgradeContract.s.sol`, with this code in it: @@ -216,7 +245,7 @@ Go into the `config.mainnet.json` file inside the `script-config` directory and - `foundation` is the address of the foundation safe. - `contractProxy` is the address of the contract proxy to upgrade. -## Run the deployment script +### Run the deployment script Run the script with diff --git a/claim_contracts/script-config/config.base-sepolia.example.json b/claim_contracts/script-config/config.base-sepolia.example.json new file mode 100644 index 0000000000..dfbb1985c1 --- /dev/null +++ b/claim_contracts/script-config/config.base-sepolia.example.json @@ -0,0 +1,5 @@ +{ + "foundation": "", + "tokenDistributor": "", + "tokenProxy": "0x4AAcFbc2C31598a560b285dB20966E00B73F9F81" +} diff --git a/claim_contracts/script-config/config.sepolia.example.json b/claim_contracts/script-config/config.sepolia.example.json new file mode 100644 index 0000000000..50ac064946 --- /dev/null +++ b/claim_contracts/script-config/config.sepolia.example.json @@ -0,0 +1,5 @@ +{ + "foundation": "", + "tokenDistributor": "", + "tokenProxy": "0xd2Fd114f098b355321cB3424400f3CC6a0d75C9A" +}