diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..9dae7b65 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/chainlink-evm"] + path = lib/chainlink-evm + url = https://github.com/smartcontractkit/chainlink-evm +[submodule "lib/save"] + path = lib/save + url = https://github.com/llama-risk/save diff --git a/contracts-save-cre/.gitkeep b/contracts-save-cre/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 00000000..19b2994a --- /dev/null +++ b/foundry.toml @@ -0,0 +1,11 @@ +# Used by `@nomicfoundation/hardhat-foundry` + `forge config`. Must match +# `paths.sources` in `hardhat.config.ts`. On the save-cre deploy throwaway +# branch this is a dummy directory; real Solidity is compiled from +# `lib/save/...` via `hardhat-dependency-compiler` and flat remappings in +# `remappings.txt` (global `@openzeppelin` → OZ v5 in lib/). + +[profile.default] +src = "contracts-save-cre" +libs = ["lib", "node_modules"] +cache_path = "cache_forge" +solc_version = "0.8.33" diff --git a/hardhat.config.ts b/hardhat.config.ts index e16c1e51..0ae39599 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -14,6 +14,8 @@ import 'solidity-docgen'; import './tasks'; import '@layerzerolabs/toolbox-hardhat'; import 'hardhat-tracer'; +import '@nomicfoundation/hardhat-foundry'; +import 'hardhat-dependency-compiler'; import { chainIds, ENV, @@ -48,6 +50,24 @@ const config: HardhatUserConfig = { }, }, }, + { + // PLACEHOLDER optimizer settings — must be reconciled against + // upstream `llama-risk/save`'s `foundry.toml` (`optimizer`, + // `optimizer_runs`, `via_ir`) before mainnet deploy. Tracked in + // docs/superpowers/specs/2026-04-23-save-cre-receiver-proxy-mainnet-deploy-design.md §10. + version: '0.8.33', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + ], + }, + dependencyCompiler: { + paths: [ + 'save/contracts/src/save-cre-receiver-proxy/SaveCreReceiverProxy.sol', ], }, namedAccounts: { @@ -97,6 +117,13 @@ const config: HardhatUserConfig = { runOnCompile: OPTIMIZER, }, paths: { + // Throwaway `save-cre` deploy branch only. Flat remapped deps (OZ v5 in + // `lib/openzeppelin-contracts`) are incompatible with the main `contracts/` + // tree (OZ 4.8.3 in node_modules), so we point `sources` at an empty + // directory and let `hardhat-dependency-compiler` pull + // `save/.../SaveCreReceiverProxy.sol` (forge remapping) in as the only graph. + // Main branch must NOT merge this; restore `sources: 'contracts'`. + sources: 'contracts-save-cre', deploy: 'deploy/', deployments: 'deployments/', }, diff --git a/package.json b/package.json index 73e0023c..878f1d33 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "deploy:axelar:post:set:flowlimit": "yarn hh:run:script scripts/deploy/misc/axelar/set_FlowLimit.ts", "deploy:axelar:post:grant:roles": "yarn hh:run:script scripts/deploy/misc/axelar/grant_Roles.ts", "deploy:axelar:post:revoke:roles": "yarn hh:run:script scripts/deploy/misc/axelar/revoke_Roles.ts", + "deploy:save-cre": "yarn hh:run:script scripts/deploy/misc/save-cre/deploy_SaveCreReceiverProxy.ts", "deploy:post:set:sanctionsList": "yarn hh:run:script scripts/deploy/post-deploy/set_SanctionsList.ts", "deploy:post:set:aaveconfig": "yarn hh:run:script scripts/deploy/post-deploy/set_AaveConfig.ts", "verify:proxy": "yarn hardhat verify-transparent-proxy", @@ -116,6 +117,7 @@ "@layerzerolabs/oft-evm": "3.2.1", "@layerzerolabs/toolbox-hardhat": "0.6.12", "@nomicfoundation/hardhat-chai-matchers": "1.0.4", + "@nomicfoundation/hardhat-foundry": "hh2", "@nomicfoundation/hardhat-network-helpers": "1.0.6", "@nomicfoundation/hardhat-toolbox": "1.0.2", "@nomicfoundation/hardhat-verify": "2.0.14", @@ -145,6 +147,7 @@ "globals": "16.5.0", "hardhat": "2.24.0", "hardhat-contract-sizer": "2.6.1", + "hardhat-dependency-compiler": "1.2.1", "hardhat-deploy": "0.11.19", "hardhat-docgen": "1.3.0", "hardhat-gas-reporter": "1.0.9", diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 00000000..f65c5b75 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,2 @@ +@chainlink/contracts/=lib/chainlink-evm/contracts/ +@openzeppelin/contracts@5.0.2/=lib/save/lib/openzeppelin-contracts/contracts/ diff --git a/scripts/deploy/common/utils.ts b/scripts/deploy/common/utils.ts index 020c073c..60dd5e87 100644 --- a/scripts/deploy/common/utils.ts +++ b/scripts/deploy/common/utils.ts @@ -265,7 +265,8 @@ export const sendAndWaitForCustomTxSign = async ( | 'update-lz-oapp-config' | 'axelar-wire-tokens' | 'axelar-update-config' - | 'deployer'; + | 'deployer' + | 'cre-receiver-owner'; subAction?: | 'add-payment-token' | 'grant-token-roles' diff --git a/scripts/deploy/misc/save-cre/deploy_SaveCreReceiverProxy.ts b/scripts/deploy/misc/save-cre/deploy_SaveCreReceiverProxy.ts new file mode 100644 index 00000000..62d631bb --- /dev/null +++ b/scripts/deploy/misc/save-cre/deploy_SaveCreReceiverProxy.ts @@ -0,0 +1,90 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { etherscanVerify, logDeploy } from '../../../../helpers/utils'; +import { DeployFunction } from '../../common/types'; +import { + getDeployer, + getWalletAddressForAction, + sendAndWaitForCustomTxSign, +} from '../../common/utils'; + +const REGISTRY = '0x2D6e9F608807436DE5D9603B00Abe3FEd1Bc809d'; +const EXPECTED_FORWARDER = '0x0b93082D9b3C7C97fAcd250082899BAcf3af3885'; +const EXPECTED_AUTHOR = '0xFD1ebE54d435bDd9a81A4BF204756a20AeB7Ee71'; + +// bytes10(0x62396534366635623834) — ASCII-hex of `"b9e46f5b84"` = +// sha256("midas_por_attestation_prod")[0:5] re-encoded as 10 ASCII hex chars. +const ATTESTATION_WORKFLOW_NAME = '0x62396534366635623834'; + +// bytes10(0x34346265396532306639) — ASCII-hex of `"44be9e20f9"` = +// sha256("midas_por_verification_prod")[0:5] re-encoded as 10 ASCII hex chars. +const VERIFICATION_WORKFLOW_NAME = '0x34346265396532306639'; + +// Both attestation and verification workflows share `bytes32(0)` per the +// task spec. The constructor seeds this slot with the attestation name; +// the post-deploy `setWorkflowConfig` call below overwrites it with the +// verification name. The DON-side report selector +// (`setAttestation` vs `setVerification`) is what differentiates which +// registry call is made — not the workflow-config name check. +const WORKFLOW_ID = '0x' + '00'.repeat(32); +const IS_REPORT_WRITE_SECURED = true; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const deployer = await getDeployer(hre); + const initialOwner = await getWalletAddressForAction( + hre, + 'cre-receiver-owner', + ); + + console.log('Deployer :', await deployer.getAddress()); + console.log('Initial owner :', initialOwner); + console.log('Registry :', REGISTRY); + console.log('Forwarder :', EXPECTED_FORWARDER); + console.log('Author :', EXPECTED_AUTHOR); + console.log('Att. workflow :', ATTESTATION_WORKFLOW_NAME); + console.log('Ver. workflow :', VERIFICATION_WORKFLOW_NAME); + + const factory = await hre.ethers.getContractFactory( + 'SaveCreReceiverProxy', + deployer, + ); + + const constructorArgs = [ + REGISTRY, + WORKFLOW_ID, + EXPECTED_FORWARDER, + EXPECTED_AUTHOR, + ATTESTATION_WORKFLOW_NAME, + IS_REPORT_WRITE_SECURED, + initialOwner, + ] as const; + + const proxy = await factory.deploy(...constructorArgs); + logDeploy('SaveCreReceiverProxy', undefined, proxy.address); + await proxy.deployTransaction.wait(2); + + await etherscanVerify(hre, proxy.address, ...constructorArgs).catch((e) => { + console.error('Unable to verify SaveCreReceiverProxy. Error: ', e); + }); + + const setWorkflowTx = await proxy.populateTransaction.setWorkflowConfig( + WORKFLOW_ID, + EXPECTED_FORWARDER, + EXPECTED_AUTHOR, + VERIFICATION_WORKFLOW_NAME, + true, + ); + + await sendAndWaitForCustomTxSign( + hre, + { ...setWorkflowTx, to: proxy.address }, + { + action: 'cre-receiver-owner', + comment: 'SaveCreReceiverProxy: register verification workflow', + }, + ); + + console.log('Done. Proxy:', proxy.address); +}; + +export default func; diff --git a/scripts/setup-foundry-submodules.sh b/scripts/setup-foundry-submodules.sh new file mode 100755 index 00000000..2da6e6ec --- /dev/null +++ b/scripts/setup-foundry-submodules.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Resolves the two root-level submodules and nested Foundry deps, then applies +# a small import patch in `lib/save` for Hardhat (HH415). +# +# Why two top-level submodules: +# - `lib/save` — llama-risk/save; OZ v5, forge-std, and a nested chainlink +# copy are pulled with `git submodule update --init --recursive`. +# - `lib/chainlink-evm` — smartcontractkit/chainlink-evm. Required at repo +# root: `remappings.txt` maps `@chainlink/contracts/` to +# `lib/chainlink-evm/contracts/` (IReceiver, etc.); that path is *not* the +# same as `lib/save/lib/chainlink-evm/...` used by save's own build. +# OpenZeppelin for Hardhat: `remappings.txt` points at +# `lib/save/lib/openzeppelin-contracts/contracts/` +# so a separate root `lib/openzeppelin-contracts` is not used. +# +# Run: +# ./scripts/setup-foundry-submodules.sh +# yarn compile +# +# `forge` on PATH is not required to run this script; it is needed for +# `forge remappings` / @nomicfoundation/hardhat-foundry when you compile. +# +# Ref: docs/superpowers/specs/2026-04-23-save-cre-receiver-proxy-mainnet-deploy-design.md +# +# HH415: `IReceiver.sol` imports `@openzeppelin/contracts@5.0.2/...` while +# upstream save uses plain `@openzeppelin/contracts/...`. If both prefixes +# map to the same on-disk file, Hardhat errors with +# "Two different source names ... resolve to the same file". We unify the +# Save .sol files to the `@5.0.2` import style to match IReceiver. + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +if ! command -v forge >/dev/null 2>&1; then + echo "warning: 'forge' not on PATH. Install Foundry for Hardhat compiles: https://getfoundry.sh" >&2 +fi + +mkdir -p lib + +# Register in .gitmodules only if missing. After a normal clone, entries exist +# and we must NOT run `submodule add` again (use update instead). +if ! git config --file .gitmodules --get 'submodule.lib/save.path' &>/dev/null; then + git submodule add --depth=1 \ + https://github.com/llama-risk/save \ + lib/save +fi +if ! git config --file .gitmodules --get 'submodule.lib/chainlink-evm.path' &>/dev/null; then + git submodule add --depth=1 \ + https://github.com/smartcontractkit/chainlink-evm \ + lib/chainlink-evm +fi + +git submodule update --init --recursive + +patch_save_for_hardhat() { + local proxy="lib/save/contracts/src/save-cre-receiver-proxy/SaveCreReceiverProxy.sol" + local abs="lib/save/contracts/src/abstracts/AbstractCreReceiver.sol" + if ! grep -q '@openzeppelin/contracts@5.0.2/access/Ownable2Step' "$proxy" 2>/dev/null; then + if sed --version 2>&1 | grep -q GNU; then + sed -i 's|@openzeppelin/contracts/access/|@openzeppelin/contracts@5.0.2/access/|' "$proxy" + sed -i 's|@openzeppelin/contracts/utils/|@openzeppelin/contracts@5.0.2/utils/|' "$abs" + else + sed -i '' 's|@openzeppelin/contracts/access/|@openzeppelin/contracts@5.0.2/access/|' "$proxy" + sed -i '' 's|@openzeppelin/contracts/utils/|@openzeppelin/contracts@5.0.2/utils/|' "$abs" + fi + echo "Applied Hardhat HH415 import unification patch to lib/save/contracts/..." + else + echo "Patch already applied (import paths use @5.0.2)." + fi +} + +patch_save_for_hardhat + +echo +echo "Done. Verify with: yarn compile" diff --git a/yarn.lock b/yarn.lock index 70c7e6ec..220dd530 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2584,6 +2584,17 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/hardhat-foundry@npm:hh2": + version: 1.2.1 + resolution: "@nomicfoundation/hardhat-foundry@npm:1.2.1" + dependencies: + picocolors: "npm:^1.1.0" + peerDependencies: + hardhat: ^2.26.0 + checksum: 10c0/0cab1137f5aae2f58f61e963445fd300b026fa808b04c172f0e7c6508a880be743d4aca9aca21de90d52350dbb559665369706c8f3fe3f673bfdb7b9c301f322 + languageName: node + linkType: hard + "@nomicfoundation/hardhat-network-helpers@npm:1.0.6": version: 1.0.6 resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.6" @@ -9583,6 +9594,15 @@ __metadata: languageName: node linkType: hard +"hardhat-dependency-compiler@npm:1.2.1": + version: 1.2.1 + resolution: "hardhat-dependency-compiler@npm:1.2.1" + peerDependencies: + hardhat: ^2.0.0 + checksum: 10c0/d4373422849fe8851d06f1151f51fe17d00fb9f3425cc77cce53bd2d0c9f325d4f1520f2aa8eae305b3a0d012b9841b398d57c3f2d5e7a3d29b6dd4aaebc1f6e + languageName: node + linkType: hard + "hardhat-deploy@npm:0.11.19": version: 0.11.19 resolution: "hardhat-deploy@npm:0.11.19" @@ -11769,6 +11789,7 @@ __metadata: "@layerzerolabs/oft-evm": "npm:3.2.1" "@layerzerolabs/toolbox-hardhat": "npm:0.6.12" "@nomicfoundation/hardhat-chai-matchers": "npm:1.0.4" + "@nomicfoundation/hardhat-foundry": "npm:hh2" "@nomicfoundation/hardhat-network-helpers": "npm:1.0.6" "@nomicfoundation/hardhat-toolbox": "npm:1.0.2" "@nomicfoundation/hardhat-verify": "npm:2.0.14" @@ -11798,6 +11819,7 @@ __metadata: globals: "npm:16.5.0" hardhat: "npm:2.24.0" hardhat-contract-sizer: "npm:2.6.1" + hardhat-dependency-compiler: "npm:1.2.1" hardhat-deploy: "npm:0.11.19" hardhat-docgen: "npm:1.3.0" hardhat-gas-reporter: "npm:1.0.9"