Audit seed
nemesis / nana-fee-project-deployer-v6 / deployment / dependency-boundary / Codex fresh round
Repos involved
nana-fee-project-deployer-v6
revnet-core-v6
nana-core-v6
Root cause
nana-fee-project-deployer-v6/script/Deploy.s.sol hardcodes fee project 1, but its idempotent already-deployed path treats a project as canonical if:
JBProjects.ownerOf(1) == revnet.basic_deployer
JBDirectory.controllerOf(1) == core.controller
revnet.basic_deployer.hashedEncodedConfigurationOf(1) != bytes32(0)
- the project token symbol is
NANA
It does not verify that the loaded REVDeployer.FEE_REVNET_ID() is also 1, nor that the stored hashedEncodedConfigurationOf(1) equals the expected hash for this script's NANA economics, terminals, auto-issuance, operator, and sucker config.
Relevant code:
// script/Deploy.s.sol
if (address(core.controller.DIRECTORY().controllerOf(feeProjectId)) != address(0)) {
if (!_feeProjectIsCanonical(feeProjectId)) revert DeployScript_FeeProjectNotCanonical(feeProjectId);
return;
}
function _feeProjectIsCanonical(uint256 feeProjectId) internal view returns (bool) {
if (core.projects.ownerOf(feeProjectId) != address(revnet.basic_deployer)) return false;
if (address(core.controller.DIRECTORY().controllerOf(feeProjectId)) != address(core.controller)) return false;
if (revnet.basic_deployer.hashedEncodedConfigurationOf(feeProjectId) == bytes32(0)) return false;
if (!_projectTokenSymbolIs({projectId: feeProjectId, expectedSymbol: SYMBOL})) return false;
return true;
}
REVDeployer stores FEE_REVNET_ID as an immutable, and REVOwner routes cash-out fees through it:
// revnet-core-v6/src/REVOwner.sol
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
Impact
A deploy/resume run can silently accept a wrong fee-project topology. If the loaded revnet dependency was deployed with a different FEE_REVNET_ID, project 1 can look canonical to this deployer while revnet cash-out fees route to another project. Likewise, a previous project 1 deployment with token symbol NANA and any nonzero revnet configuration hash can bypass this script's intended economic, terminal, operator, and sucker configuration checks.
This is not an arbitrary public takeover in the intended production topology where core pre-mints project 1 to the Sphinx safe. The concrete risk is dependency artifact drift, interrupted/wrong deploy or resume state, or accepting an already-configured project whose shape is not the expected NANA fee project.
Proof of concept
A Foundry PoC was added locally at:
nana-fee-project-deployer-v6/test/audit/CodexNemesisCanonicalGuard.t.sol
It mocks:
FEE_REVNET_ID() == 2
hashedEncodedConfigurationOf(1) == bytes32(uint256(1))
- token symbol
NANA
- expected owner/controller checks passing
The exposed _feeProjectIsCanonical(1) still returns true.
Verification command:
forge test --match-path test/audit/CodexNemesisCanonicalGuard.t.sol -vv
Result: 1 test passed.
Full repo verification:
Result: 90 tests passed.
Why this survived self-review
The strongest counterargument is that the canonical core deployment pre-mints project 1 to the Sphinx safe, so an external attacker cannot normally squat and configure project 1 first. That mitigation holds for public squatting, but it does not address this finding: the bug is in the dependency-boundary/idempotence check. The deployer returns early based on weak evidence and never proves that the already-configured project is the exact NANA fee project or that the loaded revnet fee singleton routes fees to project 1.
Recommended fix
Before returning from the already-configured branch, bind all coupled deployment state to the expected values:
if (revnet.basic_deployer.FEE_REVNET_ID() != feeProjectId) {
revert DeployScript_FeeProjectNotCanonical(feeProjectId);
}
if (revnet.basic_deployer.hashedEncodedConfigurationOf(feeProjectId) != expectedNanaConfigurationHash) {
revert DeployScript_FeeProjectNotCanonical(feeProjectId);
}
The expected hash should be derived from the same fields this script passes to deployFor, or REVDeployer should expose a canonical hash helper. The skip guard should also validate the installed terminal list and, where practical, split operator and sucker state.
Audit seed
nemesis / nana-fee-project-deployer-v6 / deployment / dependency-boundary / Codex fresh round
Repos involved
nana-fee-project-deployer-v6revnet-core-v6nana-core-v6Root cause
nana-fee-project-deployer-v6/script/Deploy.s.solhardcodes fee project1, but its idempotent already-deployed path treats a project as canonical if:JBProjects.ownerOf(1) == revnet.basic_deployerJBDirectory.controllerOf(1) == core.controllerrevnet.basic_deployer.hashedEncodedConfigurationOf(1) != bytes32(0)NANAIt does not verify that the loaded
REVDeployer.FEE_REVNET_ID()is also1, nor that the storedhashedEncodedConfigurationOf(1)equals the expected hash for this script's NANA economics, terminals, auto-issuance, operator, and sucker config.Relevant code:
REVDeployerstoresFEE_REVNET_IDas an immutable, andREVOwnerroutes cash-out fees through it:Impact
A deploy/resume run can silently accept a wrong fee-project topology. If the loaded revnet dependency was deployed with a different
FEE_REVNET_ID, project1can look canonical to this deployer while revnet cash-out fees route to another project. Likewise, a previous project1deployment with token symbolNANAand any nonzero revnet configuration hash can bypass this script's intended economic, terminal, operator, and sucker configuration checks.This is not an arbitrary public takeover in the intended production topology where core pre-mints project
1to the Sphinx safe. The concrete risk is dependency artifact drift, interrupted/wrong deploy or resume state, or accepting an already-configured project whose shape is not the expected NANA fee project.Proof of concept
A Foundry PoC was added locally at:
nana-fee-project-deployer-v6/test/audit/CodexNemesisCanonicalGuard.t.solIt mocks:
FEE_REVNET_ID() == 2hashedEncodedConfigurationOf(1) == bytes32(uint256(1))NANAThe exposed
_feeProjectIsCanonical(1)still returns true.Verification command:
forge test --match-path test/audit/CodexNemesisCanonicalGuard.t.sol -vvResult: 1 test passed.
Full repo verification:
forge test --deny notesResult: 90 tests passed.
Why this survived self-review
The strongest counterargument is that the canonical core deployment pre-mints project
1to the Sphinx safe, so an external attacker cannot normally squat and configure project1first. That mitigation holds for public squatting, but it does not address this finding: the bug is in the dependency-boundary/idempotence check. The deployer returns early based on weak evidence and never proves that the already-configured project is the exact NANA fee project or that the loaded revnet fee singleton routes fees to project1.Recommended fix
Before returning from the already-configured branch, bind all coupled deployment state to the expected values:
The expected hash should be derived from the same fields this script passes to
deployFor, orREVDeployershould expose a canonical hash helper. The skip guard should also validate the installed terminal list and, where practical, split operator and sucker state.