Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 3 additions & 6 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ libs = ["lib"]
test = "test"
script = "script"

gas_reports = ["XanV1", "XanV2", "XanV2Forwarder"]
gas_reports = ["XanV1", "XanV2"]

allow_internal_expect_revert = true

Expand All @@ -43,13 +43,10 @@ fail_on_revert = false
fuzz = { runs = 10_000 }
verbosity = 4

[etherscan]
mainnet = { key = "${API_KEY_ETHERSCAN}" }
sepolia = { key = "${API_KEY_ETHERSCAN}" }

[rpc_endpoints]
mainnet = "https://mainnet.infura.io/v3/${API_KEY_INFURA}"
sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}"
mainnet = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
sepolia = "https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
localhost = "http://localhost:8545"

# Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config
94 changes: 94 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,97 @@ check:
@just build
@echo "==> Testing..."
@just test

# --- Deployment ---

# Simulate the XanV1 deployment (dry-run)
deploy-simulate initial-mint-recipient council chain *args:
@echo "Cleaning contracts to ensure reproducible build..."
@just clean
forge script script/DeployXanV1.s.sol:DeployXanV1 \
--sig "run(address,address)" {{ initial-mint-recipient }} {{ council }} \
--rpc-url {{ chain }} {{ args }}

# Deploy XanV1 behind a UUPS proxy
deploy deployer initial-mint-recipient council chain *args:
@echo "Cleaning contracts to ensure reproducible build..."
@just clean
forge script script/DeployXanV1.s.sol:DeployXanV1 \
--sig "run(address,address)" {{ initial-mint-recipient }} {{ council }} \
--broadcast --rpc-url {{ chain }} --account {{ deployer }} {{ args }}

# Simulate scheduling the council upgrade to XanV2 (dry-run)
schedule-council-upgrade-simulate proxy sender chain *args:
@echo "Cleaning contracts to ensure reproducible build..."
@just clean
forge script script/ScheduleCouncilUpgradeToXanV2.s.sol:ScheduleCouncilUpgradeToXanV2 \
--sig "run(address)" {{ proxy }} \
--rpc-url {{ chain }} --sender {{ sender }} {{ args }}

# Schedule the council upgrade to XanV2
schedule-council-upgrade deployer proxy sender chain *args:
@echo "Cleaning contracts to ensure reproducible build..."
@just clean
forge script script/ScheduleCouncilUpgradeToXanV2.s.sol:ScheduleCouncilUpgradeToXanV2 \
--sig "run(address)" {{ proxy }} \
--broadcast --rpc-url {{ chain }} --account {{ deployer }} --sender {{ sender }} {{ args }}

# Simulate the upgrade to XanV2 (dry-run)
upgrade-simulate proxy owner chain *args:
@echo "Cleaning contracts to ensure reproducible build..."
@just clean
forge script script/UpgradeToXanV2.s.sol:UpgradeToXanV2 \
--sig "run(address,address)" {{ proxy }} {{ owner }} \
--rpc-url {{ chain }} --sender {{ owner }} {{ args }}

# Upgrade the proxy to XanV2
upgrade deployer proxy owner chain *args:
@echo "Cleaning contracts to ensure reproducible build..."
@just clean
forge script script/UpgradeToXanV2.s.sol:UpgradeToXanV2 \
--sig "run(address,address)" {{ proxy }} {{ owner }} \
--broadcast --rpc-url {{ chain }} --account {{ deployer }} {{ args }}

# --- Verification ---

# Verify an implementation contract on sourcify (e.g. contract=src/XanV1.sol:XanV1)
verify-impl-sourcify address contract chain *args:
ETHERSCAN_API_KEY="" forge verify-contract {{ address }} {{ contract }} \
--chain {{ chain }} --verifier sourcify --watch {{ args }}

# Verify an implementation contract on etherscan (e.g. contract=src/XanV1.sol:XanV1)
verify-impl-etherscan address contract chain *args:
forge verify-contract {{ address }} {{ contract }} \
--chain {{ chain }} --verifier etherscan --watch {{ args }}

# Verify an implementation contract on a custom explorer
verify-impl-custom address contract chain verifier-url *args:
forge verify-contract {{ address }} {{ contract }} \
--chain {{ chain }} --verifier-url {{ verifier-url }} --watch {{ args }}

# Verify an implementation contract on both sourcify and etherscan
verify-impl address contract chain: (verify-impl-sourcify address contract chain) (verify-impl-etherscan address contract chain)

# Verify the ERC1967 proxy on sourcify (encodes the constructor args from the deploy inputs)
verify-proxy-sourcify proxy implementation initial-mint-recipient council chain *args:
ETHERSCAN_API_KEY="" forge verify-contract {{ proxy }} \
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
--chain {{ chain }} --verifier sourcify --watch \
--constructor-args "$(cast abi-encode 'c(address,bytes)' {{ implementation }} "$(cast calldata 'initializeV1(address,address)' {{ initial-mint-recipient }} {{ council }})")" {{ args }}

# Verify the ERC1967 proxy on etherscan (encodes the constructor args from the deploy inputs)
verify-proxy-etherscan proxy implementation initial-mint-recipient council chain *args:
forge verify-contract {{ proxy }} \
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
--chain {{ chain }} --verifier etherscan --watch \
--constructor-args "$(cast abi-encode 'c(address,bytes)' {{ implementation }} "$(cast calldata 'initializeV1(address,address)' {{ initial-mint-recipient }} {{ council }})")" {{ args }}

# Verify the ERC1967 proxy on a custom explorer (encodes the constructor args from the deploy inputs)
verify-proxy-custom proxy implementation initial-mint-recipient council chain verifier-url *args:
forge verify-contract {{ proxy }} \
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
--chain {{ chain }} --verifier-url {{ verifier-url }} --watch \
--constructor-args "$(cast abi-encode 'c(address,bytes)' {{ implementation }} "$(cast calldata 'initializeV1(address,address)' {{ initial-mint-recipient }} {{ council }})")" {{ args }}

# Verify the ERC1967 proxy on both sourcify and etherscan
verify-proxy proxy implementation initial-mint-recipient council chain: (verify-proxy-sourcify proxy implementation initial-mint-recipient council chain) (verify-proxy-etherscan proxy implementation initial-mint-recipient council chain)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"dependencies": {
"@openzeppelin/upgrades-core": "^1.46.0",
"solhint": "^6.2.2"
"solhint": "^6.2.3"
}
}
24 changes: 24 additions & 0 deletions script/DeployXanV1.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.30;

import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol";

import {Script} from "forge-std/Script.sol";

import {XanV1} from "../src/XanV1.sol";

contract DeployXanV1 is Script {
function run(address initialMintRecipient, address council) public returns (address proxy, address implementation) {
vm.startBroadcast();

proxy = Upgrades.deployUUPSProxy({
contractName: "XanV1.sol:XanV1",
initializerData: abi.encodeCall(XanV1.initializeV1, (initialMintRecipient, council))
});

implementation = XanV1(proxy).implementation();

vm.stopBroadcast();
}
}
30 changes: 30 additions & 0 deletions script/ScheduleCouncilUpgradeToXanV2.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.30;

import {Upgrades, Options} from "@openzeppelin/foundry-upgrades/Upgrades.sol";
import {Script} from "forge-std/Script.sol";

import {Parameters} from "../src/libs/Parameters.sol";
import {XanV1} from "../src/XanV1.sol";

contract ScheduleCouncilUpgradeToXanV2 is Script {
function run(address proxy) public returns (address implV2) {
Options memory opts;

// Bind the owner and vesting schedule into the implementation bytecode at deployment (the trusted step). The
// scheduled implementation address is fixed, so whoever later executes the (permissionless) upgrade cannot
// change these via calldata. Always use the `Parameters` constants so the vesting schedule cannot be picked
// wrong.
opts.constructorData =
abi.encode(Parameters.INITIAL_OWNER, Parameters.VESTING_START, Parameters.VESTING_DURATION);

vm.startBroadcast();

implV2 = Upgrades.prepareUpgrade({contractName: "XanV2.sol:XanV2", opts: opts});

XanV1(proxy).scheduleCouncilUpgrade({impl: implV2});

vm.stopBroadcast();
}
}
36 changes: 0 additions & 36 deletions script/Upgrade.s.sol

This file was deleted.

34 changes: 34 additions & 0 deletions script/UpgradeToXanV2.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.30;

import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
import {UnsafeUpgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol";

import {Script} from "forge-std/Script.sol";

import {XanV1} from "../src/XanV1.sol";
import {XanV2} from "../src/XanV2.sol";

contract UpgradeToXanV2 is Script {
error ZeroImplementationV2NotAllowed();

function run(address proxy) public returns (address implementationV2) {
uint48 endTime;

(implementationV2, endTime) = XanV1(proxy).scheduledCouncilUpgrade();

require(implementationV2 != address(0), ZeroImplementationV2NotAllowed());
require(endTime <= Time.timestamp(), XanV1.DelayPeriodNotEnded({endTime: endTime}));

vm.startBroadcast();

// The owner and vesting start are baked into `implV2` at deployment (see `ScheduleCouncilUpgradeToXanV2`),
// so `reinitializeFromV1` takes no arguments and executing this upgrade cannot influence them.
UnsafeUpgrades.upgradeProxy({
proxy: proxy, newImpl: implementationV2, data: abi.encodeCall(XanV2.reinitializeFromV1, ())
});

vm.stopBroadcast();
}
}
Loading
Loading