Dual-stack contract workspace supporting both Foundry and Hardhat.
src/: shared Solidity sourcestest/foundry/: Forge teststest/hardhat/: Hardhat testsscript/: deployment or maintenance scripts
forge test -C .npm installnpm run test:hardhatnpm run buildnpm run deploy:localhostnpm run deploy:devnetnpm run deploy:keystore:localhostnpm run deploy:keystore:sepolianpm run deploy:keystore:ethereum
- Solidity version pinned to
0.8.24 - Optimizer enabled (
runs=200) - Hardhat source path points to
src/to share the same contracts with Foundry
After deployment, verify contracts on Etherscan:
# Hardhat-native verification (default)
npx hardhat verify-contracts --network <network>
# Or via Makefile (from repo root)
make verify-contracts NETWORK=sepolia
make verify-contracts-ethereum
# Foundry forge verify-contract (alternative)
ETHERSCAN_VERIFICATION_PROVIDER=foundry npx hardhat verify-contracts --network sepoliaAll settings are read from env vars:
ETHERSCAN_KEY: Primary Etherscan API key (required)SEPOLIA_ETHERSCAN_KEY: Override for Sepolia (falls back to ETHERSCAN_KEY)MAINNET_ETHERSCAN_KEY: Override for Ethereum mainnet (falls back to ETHERSCAN_KEY)ETHERSCAN_VERIFICATION_PROVIDER:hardhat(default) orfoundryETHERSCAN_VERIFICATION_MAX_RETRIES: Max retries for Hardhat verification (default: 3)
- Deploy scripts write per-contract metadata (
address,constructorArgs,libraries) to JSON files underdeployments/<network>/. - Verify reads those deployment files from
deployments/<network>/and auto-detects which contracts to verify. - Each contract (including
_Implementationvariants;_Proxycontracts are skipped) is verified via:- Hardhat (default): runs
verify:verifywith automatic retries - Foundry (
ETHERSCAN_VERIFICATION_PROVIDER=foundry): constructs aforge verify-contractcommand with ABI-encoded constructor args viacast abi-encode
- Hardhat (default): runs
Contract types covered:
| Type | Example | Notes |
|---|---|---|
| Standalone | StateManager, Bridge | Deployed and verified directly |
| Proxy | PsyAddressesProvider_Proxy | OpenZeppelin Transparent Proxy; constructor args include (impl, admin, data) |
| Implementation | PsyAddressesProvider_Implementation | Verified against the implementation constructor args only |
Reference: paraspace-core
tasks/dev/verifyContracts.ts→helpers/contracts-helpers.ts:verifyContracts(), supporting both Hardhat and Foundry modes.
KEYSTORE_PATH: encrypted deployer keystore pathWALLET_PASSWORD: keystore passwordLOCALHOST_RPC_URL,SEPOLIA_RPC_URL, orETH_RPC_URL: network RPC URL- Optional per-network WETH envs in
helper-hardhat-config.ts(ETH_WETH,ARB_WETH, ...)
Direct deploy private keys are intentionally disabled. Use scripts/deploy-with-keystore.mjs or the deploy:keystore:* npm scripts.
- Network deploy config is loaded from
config/<network>.json - For non-local networks, do not use placeholder governance addresses (
0x...01,0x...02) adminshould be timelock/proxy-admin owner;proposershould be finalize operator
KEYSTORE_PATH=... WALLET_PASSWORD=... LOCALHOST_RPC_URL=http://127.0.0.1:8545 npm run deploy:keystore:localhost
KEYSTORE_PATH=... WALLET_PASSWORD=... SEPOLIA_RPC_URL=https://... npm run deploy:keystore:sepoliaKEYSTORE_PATH=... WALLET_PASSWORD=... ETH_RPC_URL=https://... npm run deploy:keystore:ethereum
StateManager.bridge == BridgeStateManager.proposer == expected proposerRouter.bridge == BridgeRouter.defaultERC20Gateway == ERC20GatewayRouter.ethGateway == ETHGatewayStateManager.zkVerifier != address(0)
StateManager.appendDeposit: only BridgeStateManager.finalize: only ProposerBridge.recordDeposit: disabled; canonical path is Router -> Gateway -> Bridge.recordDepositFromGatewayBridge.recordDepositFromGateway: caller must match Router-resolved gateway