Skip to content

Commit 2c2d23f

Browse files
authored
Merge pull request #613 from anoma/heueristik/deployment-tests-to-rust
refactor(contracts,crates): move deployments-related tests to rust
2 parents 66b4e6a + 611459d commit 2c2d23f

19 files changed

Lines changed: 2064 additions & 408 deletions

.github/workflows/contracts.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,10 @@ jobs:
6161
- name: Check that bindings are up-to-date
6262
run: just bindings-check
6363

64-
# The deployment promotion gate runs on pull requests into an environment branch, where the recorded
65-
# deployments and the source are meant to agree. Elsewhere the gated tests skip and no chain is forked.
64+
- name: Check that the recorded deployments library is up-to-date
65+
run: just contracts-deployments-check
66+
6667
- name: Run Forge tests
6768
run: just contracts-test
6869
env:
6970
ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
70-
VERIFY_STAGING_DEPLOYMENTS: ${{ github.base_ref == 'staging' }}
71-
VERIFY_PRODUCTION_DEPLOYMENTS: ${{ github.base_ref == 'main' }}

RELEASE_CHECKLIST.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Changes flow one way, `next` → `staging` → `main`, and the promotion pull re
3232
- **`staging`** receives `next`. A pull request into it requires every entry in the staging section to run the source version, checked with `VERIFY_STAGING_DEPLOYMENTS`.
3333
- **`main`** receives `staging`. A pull request into it requires every entry in the production section to run the source version, carry no prerelease suffix, and be owned by a Safe, checked with `VERIFY_PRODUCTION_DEPLOYMENTS`.
3434

35-
The flags gate the deployment tests in the contracts and the crates suites alike; unset, the gated tests skip and no chain is forked.
35+
The flags gate the deployment tests, which live in the bindings crate beside the record they check; unset, they skip and no chain is forked.
3636

3737
Deploy or upgrade **every** chain of an environment before opening its promotion pull request — one chain left behind blocks the promotion for all of them.
3838

@@ -176,7 +176,7 @@ For each chain in the `staging` section of the record:
176176
- [ ] After the last chain, confirm the promotion gate locally by running
177177

178178
```sh
179-
VERIFY_STAGING_DEPLOYMENTS=true just contracts-test bindings-test
179+
VERIFY_STAGING_DEPLOYMENTS=true just bindings-test
180180
```
181181

182182
the same checks the promotion pull request runs.
@@ -359,6 +359,14 @@ For **both**:
359359

360360
The genesis fields pin how the address was derived and cannot be recovered from the chain once the proxy is upgraded. They are written once and never edited.
361361

362+
- [ ] Regenerate the library the deploy script reads the record through with
363+
364+
```sh
365+
just contracts-gen-deployments
366+
```
367+
368+
and commit it alongside the record. The contracts package ships without `deployments.json`, so the deploy script reads the records from the generated [`./contracts/generated/RecordedDeployments.sol`](./contracts/generated/RecordedDeployments.sol); leaving it stale lets a genesis deploy run twice on the same chain. CI reruns the generator and fails on any diff.
369+
362370
- [ ] Bump the `bindings` package version in [`./crates/bindings/Cargo.toml`](./crates/bindings/Cargo.toml) to `A.B.0`, where `A` is the last `MAJOR` version and `B` is the last `MINOR` version number incremented by 1.
363371

364372
- [ ] Run `just bindings-build` and check that the `Cargo.lock` file reflects the version number change, then run the tests with `just bindings-test`.

contracts/foundry.toml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@ via_ir = true
1414
optimizer_runs = 10_000
1515
allow_internal_expect_revert = true
1616

17-
# Let tests read build artifacts (needed by `openzeppelin-foundry-upgrades`) and the
18-
# recorded deployments. Setting `fs_permissions` replaces the implicit artifact access.
19-
fs_permissions = [
20-
{ access = "read", path = "out" },
21-
{ access = "read", path = "../crates/bindings/deployments.json" },
22-
]
17+
# Let tests read build artifacts, which `openzeppelin-foundry-upgrades` needs. Setting
18+
# `fs_permissions` replaces the implicit artifact access.
19+
fs_permissions = [{ access = "read", path = "out" }]
2320

2421
# Settings required by `openzeppelin-foundry-upgrades` for upgrade-safety validation
2522
# (https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades#before-running).
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.30;
3+
4+
/// @title RecordedDeployments
5+
/// @author Anoma Foundation, 2026
6+
/// @notice The protocol adapter deployments each environment records.
7+
/// @dev Generated from `crates/bindings/deployments.json`, the single source of truth, which the bindings crate
8+
/// embeds and checks against the chains. Do not edit by hand: run `just contracts-gen-deployments`, which CI reruns
9+
/// and fails on any diff. The records live with the bindings because that crate publishes them; this library carries
10+
/// them into Solidity so the contracts package reads nothing outside itself.
11+
/// @custom:security-contact security@anoma.foundation
12+
library RecordedDeployments {
13+
/// @notice A recorded protocol adapter proxy. The field `addr` holds the address, which is a reserved word.
14+
/// @dev The genesis fields pin how the address was derived: the creation code and the constructor arguments
15+
/// determine it together with the environment salt, and none of them can be read from the chain once the proxy is
16+
/// upgraded.
17+
struct Proxy {
18+
address addr;
19+
address initialImplementation;
20+
bytes initializerData;
21+
bytes creationCode;
22+
}
23+
24+
/// @notice A recorded protocol adapter deployment.
25+
struct Deployment {
26+
uint256 chainId;
27+
Proxy proxy;
28+
}
29+
30+
/// @notice Returns whether the environment records a deployment for the chain.
31+
/// @param isProduction Whether to check the production or the staging environment.
32+
/// @param chainId The chain ID to look for.
33+
/// @return recorded Whether the environment records a deployment for the chain.
34+
function isRecorded(bool isProduction, uint256 chainId) internal pure returns (bool recorded) {
35+
Deployment[] memory deployments = isProduction ? production() : staging();
36+
37+
for (uint256 i = 0; i < deployments.length; ++i) {
38+
if (deployments[i].chainId == chainId) {
39+
return true;
40+
}
41+
}
42+
}
43+
44+
/// @notice Returns the deployments the staging environment records.
45+
/// @return deployments The recorded staging deployments.
46+
function staging() internal pure returns (Deployment[] memory deployments) {
47+
deployments = new Deployment[](2);
48+
deployments[0] = Deployment({
49+
chainId: 11155111,
50+
proxy: Proxy({
51+
addr: 0xe23d3b3FC0944cB0c1184C6e04b9d26Ed50CeC51,
52+
initialImplementation: 0x4d68d7A738FBAF0E81aB508F573B1C6A45a376d7,
53+
initializerData: hex"c4d66de800000000000000000000000061462be56782568376f9cb069382efa72764a407",
54+
creationCode: hex"6080604052610284803803806100148161016e565b9283398101604082820312610156578151916001600160a01b03831690818403610156576020810151906001600160401b038211610156570182601f82011215610156578051906001600160401b03821161015a5761007c601f8301601f191660200161016e565b938285526020838301011161015657815f9260208093018387015e8401015281511561014757823b15610135577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a280511561011e5761010e91610193565b505b604051606490816102208239f35b505034156101105763b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b6330a289cf60e21b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b0381118382101761015a57604052565b905f8091602081519101845af4808061020c575b156101c75750506040513d81523d5f602083013e60203d82010160405290565b156101ec57639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b3d156101fd576040513d5f823e3d90fd5b63d6bda27560e01b5f5260045ffd5b503d1515806101a75750813b15156101a756fe60806040525f8073ffffffffffffffffffffffffffffffffffffffff7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5416368280378136915af43d5f803e156053573d5ff35b3d5ffdfea164736f6c6343000824000a"
55+
})
56+
});
57+
deployments[1] = Deployment({
58+
chainId: 84532,
59+
proxy: Proxy({
60+
addr: 0xb5A5a52Af29dA0c8801D9caf4D75a4d6C3895f0A,
61+
initialImplementation: 0xd29F13e08C3DAe49Dc99F65Bc0C51EBF6310682f,
62+
initializerData: hex"c4d66de800000000000000000000000061462be56782568376f9cb069382efa72764a407",
63+
creationCode: hex"6080604052610284803803806100148161016e565b9283398101604082820312610156578151916001600160a01b03831690818403610156576020810151906001600160401b038211610156570182601f82011215610156578051906001600160401b03821161015a5761007c601f8301601f191660200161016e565b938285526020838301011161015657815f9260208093018387015e8401015281511561014757823b15610135577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a280511561011e5761010e91610193565b505b604051606490816102208239f35b505034156101105763b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b6330a289cf60e21b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b0381118382101761015a57604052565b905f8091602081519101845af4808061020c575b156101c75750506040513d81523d5f602083013e60203d82010160405290565b156101ec57639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b3d156101fd576040513d5f823e3d90fd5b63d6bda27560e01b5f5260045ffd5b503d1515806101a75750813b15156101a756fe60806040525f8073ffffffffffffffffffffffffffffffffffffffff7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5416368280378136915af43d5f803e156053573d5ff35b3d5ffdfea164736f6c6343000824000a"
64+
})
65+
});
66+
}
67+
68+
/// @notice Returns the deployments the production environment records.
69+
/// @return deployments The recorded production deployments.
70+
function production() internal pure returns (Deployment[] memory deployments) {
71+
deployments = new Deployment[](0);
72+
}
73+
}

contracts/script/DeployProtocolAdapterImplementation.s.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {Options} from "openzeppelin-foundry-upgrades-0.4.2/src/Options.sol";
88
import {Upgrades} from "openzeppelin-foundry-upgrades-0.4.2/src/Upgrades.sol";
99

1010
import {ProtocolAdapter} from "../src/ProtocolAdapter.sol";
11+
import {Parameters} from "./Parameters.sol";
1112

1213
/// @title DeployProtocolAdapterImplementation
1314
/// @author Anoma Foundation, 2026
@@ -17,7 +18,7 @@ import {ProtocolAdapter} from "../src/ProtocolAdapter.sol";
1718
/// @custom:security-contact security@anoma.foundation
1819
contract DeployProtocolAdapterImplementation is SupportedNetworks, Script {
1920
/// @notice The CREATE2 salt for the implementation deployment, shared by the staging and production environments.
20-
bytes32 public constant IMPLEMENTATION_SALT = "ProtocolAdapterImpl";
21+
bytes32 public constant IMPLEMENTATION_SALT = Parameters.IMPLEMENTATION_SALT;
2122

2223
/// @notice The initialization data to pass to `upgradeToAndCall` when upgrading a proxy to this implementation —
2324
/// empty because the current version requires no reinitialization.

contracts/script/DeployProtocolAdapterProxy.s.sol

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ pragma solidity ^0.8.30;
44
import {ERC1967Proxy} from "@openzeppelin-contracts-5.7.0/proxy/ERC1967/ERC1967Proxy.sol";
55
import {Script} from "forge-std-1.16.2/src/Script.sol";
66

7+
import {RecordedDeployments} from "../generated/RecordedDeployments.sol";
78
import {ProtocolAdapter} from "../src/ProtocolAdapter.sol";
89
import {DeployProtocolAdapterImplementation} from "./DeployProtocolAdapterImplementation.s.sol";
10+
import {Parameters} from "./Parameters.sol";
911

1012
/// @title DeployProtocolAdapterProxy
1113
/// @author Anoma Foundation, 2026
@@ -14,19 +16,16 @@ import {DeployProtocolAdapterImplementation} from "./DeployProtocolAdapterImplem
1416
/// @custom:security-contact security@anoma.foundation
1517
contract DeployProtocolAdapterProxy is Script {
1618
/// @notice The CREATE2 salt for the staging environment proxy deployment.
17-
bytes32 public constant PROXY_SALT_STAGING = "ProtocolAdapterProxyStaging";
19+
bytes32 public constant PROXY_SALT_STAGING = Parameters.PROXY_SALT_STAGING;
1820

1921
/// @notice The CREATE2 salt for the production environment proxy deployment.
20-
bytes32 public constant PROXY_SALT_PRODUCTION = "ProtocolAdapterProxyProduction";
22+
bytes32 public constant PROXY_SALT_PRODUCTION = Parameters.PROXY_SALT_PRODUCTION;
2123

2224
/// @notice The staging environment proxy owner — the deployment wallet, upgrading instantly.
23-
address public constant PROXY_OWNER_STAGING = 0x61462bE56782568376f9cB069382EFa72764a407;
25+
address public constant PROXY_OWNER_STAGING = Parameters.PROXY_OWNER_STAGING;
2426

2527
/// @notice The production environment proxy owner — the Safe multisig queueing upgrades.
26-
address public constant PROXY_OWNER_PRODUCTION = 0xE9082Ac8Aa2Fb27DEfDBAC604921C196b884Da10;
27-
28-
/// @notice The deployments recorded per environment, relative to the Foundry root.
29-
string internal constant _DEPLOYMENTS_PATH = "../crates/bindings/deployments.json";
28+
address public constant PROXY_OWNER_PRODUCTION = Parameters.PROXY_OWNER_PRODUCTION;
3029

3130
/// @notice Thrown if the environment already has a deployment recorded for this chain.
3231
error DeploymentAlreadyRecorded(string environment, uint256 chainId);
@@ -54,7 +53,10 @@ contract DeployProtocolAdapterProxy is Script {
5453

5554
// Checks
5655
{
57-
_requireUnrecorded(isProduction);
56+
require(
57+
!RecordedDeployments.isRecorded({isProduction: isProduction, chainId: block.chainid}),
58+
DeploymentAlreadyRecorded(environmentName(isProduction), block.chainid)
59+
);
5860

5961
// forge-lint: disable-next-line(unused-return)
6062
(implementation,) = implementationDeployScript.predict();
@@ -94,28 +96,6 @@ contract DeployProtocolAdapterProxy is Script {
9496
name = isProduction ? "production" : "staging";
9597
}
9698

97-
/// @notice Checks that the environment has no deployment recorded for this chain yet.
98-
/// @param isProduction Whether to check the production or the staging environment.
99-
function _requireUnrecorded(bool isProduction) internal view {
100-
// `fs_permissions` scopes the read to the recorded deployments.
101-
// forge-lint: disable-next-line(unsafe-cheatcode)
102-
string memory json = vm.readFile(_DEPLOYMENTS_PATH);
103-
string memory environment = environmentName(isProduction);
104-
105-
for (uint256 i = 0;; ++i) {
106-
// solhint-disable-next-line func-named-parameters
107-
string memory entry = string.concat(".", environment, "[", vm.toString(i), "]");
108-
if (!vm.keyExistsJson(json, entry)) {
109-
return;
110-
}
111-
112-
require(
113-
vm.parseJsonUint(json, string.concat(entry, ".chainId")) != block.chainid,
114-
DeploymentAlreadyRecorded(environment, block.chainid)
115-
);
116-
}
117-
}
118-
11999
/// @notice Derives the deterministic proxy address and the constructor arguments it commits to.
120100
/// @param salt The CREATE2 salt of the environment.
121101
/// @param implementation The implementation contract the proxy delegates to.

contracts/script/Parameters.sol

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.30;
3+
4+
/// @title Parameters
5+
/// @author Anoma Foundation, 2026
6+
/// @notice The deterministic deployment parameters — the CREATE2 salts and the environment proxy owners. They fix
7+
/// where a deployment lands and who may upgrade it, so they are held once here and read by the deploy scripts, their
8+
/// tests, and the bindings crate through `DeploymentParameters`.
9+
/// @custom:security-contact security@anoma.foundation
10+
library Parameters {
11+
/// @notice The CREATE2 salt for the staging environment proxy deployment.
12+
bytes32 internal constant PROXY_SALT_STAGING = "ProtocolAdapterProxyStaging";
13+
14+
/// @notice The CREATE2 salt for the production environment proxy deployment.
15+
bytes32 internal constant PROXY_SALT_PRODUCTION = "ProtocolAdapterProxyProduction";
16+
17+
/// @notice The CREATE2 salt for the implementation deployment, shared by the staging and production environments.
18+
bytes32 internal constant IMPLEMENTATION_SALT = "ProtocolAdapterImpl";
19+
20+
/// @notice The staging environment proxy owner — the deployment wallet, upgrading instantly.
21+
address internal constant PROXY_OWNER_STAGING = 0x61462bE56782568376f9cB069382EFa72764a407;
22+
23+
/// @notice The production environment proxy owner — the Safe multisig queueing upgrades.
24+
address internal constant PROXY_OWNER_PRODUCTION = 0xE9082Ac8Aa2Fb27DEfDBAC604921C196b884Da10;
25+
}
26+
27+
/// @title DeploymentParameters
28+
/// @author Anoma Foundation, 2026
29+
/// @notice Exposes the deployment parameters through getters, so consumers outside Solidity — the bindings crate and
30+
/// its tests — read the values this source holds instead of restating them.
31+
/// @custom:security-contact security@anoma.foundation
32+
contract DeploymentParameters {
33+
/// @notice Returns the CREATE2 salt for the staging environment proxy deployment.
34+
/// @return salt The staging proxy salt.
35+
function PROXY_SALT_STAGING() external pure returns (bytes32 salt) {
36+
salt = Parameters.PROXY_SALT_STAGING;
37+
}
38+
39+
/// @notice Returns the CREATE2 salt for the production environment proxy deployment.
40+
/// @return salt The production proxy salt.
41+
function PROXY_SALT_PRODUCTION() external pure returns (bytes32 salt) {
42+
salt = Parameters.PROXY_SALT_PRODUCTION;
43+
}
44+
45+
/// @notice Returns the CREATE2 salt for the implementation deployment.
46+
/// @return salt The implementation salt, shared by both environments.
47+
function IMPLEMENTATION_SALT() external pure returns (bytes32 salt) {
48+
salt = Parameters.IMPLEMENTATION_SALT;
49+
}
50+
51+
/// @notice Returns the staging environment proxy owner.
52+
/// @return owner The deployment wallet upgrading the staging proxies.
53+
function PROXY_OWNER_STAGING() external pure returns (address owner) {
54+
owner = Parameters.PROXY_OWNER_STAGING;
55+
}
56+
57+
/// @notice Returns the production environment proxy owner.
58+
/// @return owner The Safe multisig queueing production upgrades.
59+
function PROXY_OWNER_PRODUCTION() external pure returns (address owner) {
60+
owner = Parameters.PROXY_OWNER_PRODUCTION;
61+
}
62+
}

0 commit comments

Comments
 (0)