Skip to content

[Audit] [MEDIUM] Fee project deployer can accept mismatched canonical revnet config #101

Description

@mejango

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:

forge test --deny notes

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions