Upgrade scripts can register markets with an invalid quote token
Upgrade scripts can register markets with an invalid quote token. Final disposition: Fixed.
Tracker: issue #26 · Status: Fixed · Verified at: audit-v3-rereview-handoff
Affected files
script/DeployBase.s.sol#L130-L154 script/UpgradeSettlement.s.sol#L92-L118 script/UpgradeSpecialMarket.s.sol#L74-L106
Description
Both upgrade scripts read USDC_ADDRESS and pass it directly to a new market without validating it. Unlike DeployBase, they do not call _assertQuoteTokenLive(). The shared market constructor only rejects the zero address, stores the quote token immutably, and the upgrade scripts immediately register the new market as a trusted badge operator.
Consequently, a stale address from another network can produce a registered market whose quote token has no code. The existing DeployBase guard is also incomplete because it checks only code presence and six decimals, not that the address is the canonical token for the active chain or that it supports the required EIP-3009 receiveWithAuthorization() flow.
Vulnerable Scenario:
An operator runs UpgradeSettlement with a wrong-network USDC_ADDRESS. The upgrade succeeds, permanently stores the codeless address as quoteCurrency, and registers the unusable market. Save the following test as test/poc/UpgradeAcceptsCodelessQuoteInline.t.sol and run forge test --match-path test/poc/UpgradeAcceptsCodelessQuoteInline.t.sol -vv:
// SPDX-License-Identifier: MITpragma solidity 0.8.30;import {Test, Vm} from "forge-std/Test.sol";import {UpgradeSettlement} from "../../script/UpgradeSettlement.s.sol";import {PlakxioBadge} from "../../src/PlakxioBadge.sol";import {PlakxioSettlement} from "../../src/PlakxioSettlement.sol";contract UpgradeAcceptsCodelessQuoteInline is Test {event SettlementContractSet(address indexed settlement, bool allowed, uint256 classMask);function test_upgradeWiresAQuoteTokenWithNoCode() public {uint256 deployerKey = 0xD00D;address deployer = vm.addr(deployerKey);address codelessQuote = address(0xDeaD00000000000000000000000000000000BEEf);vm.prank(deployer);PlakxioBadge badge = new PlakxioBadge(deployer, "uri", 0, 120);vm.setEnv("BADGE_ADDRESS", vm.toString(address(badge)));vm.setEnv("USDC_ADDRESS", vm.toString(codelessQuote));vm.setEnv("TREASURY_ADDRESS", vm.toString(makeAddr("treasury")));vm.setEnv("RELAYER_ADDRESS", vm.toString(makeAddr("relayer")));vm.setEnv("DEPLOYER_PRIVATE_KEY", vm.toString(deployerKey));assertEq(codelessQuote.code.length, 0);vm.recordLogs();new UpgradeSettlement().run();Vm.Log[] memory logs = vm.getRecordedLogs();address settlement;for (uint256 i; i < logs.length; ++i) {if (logs[i].emitter == address(badge) && logs[i].topics[0] == SettlementContractSet.selector) {settlement = address(uint160(uint256(logs[i].topics[1])));}}assertNotEq(settlement, address(0));assertEq(address(PlakxioSettlement(settlement).quoteCurrency()), codelessQuote);assertTrue(badge.isSettlement(settlement));}}
Observed output (gas and timing omitted):
Ran 1 test for test/poc/UpgradeAcceptsCodelessQuoteInline.t.sol:UpgradeAcceptsCodelessQuoteInline[PASS] test_upgradeWiresAQuoteTokenWithNoCode()Suite result: ok. 1 passed; 0 failed; 0 skipped
The test executes the real upgrade script and confirms that it accepts and registers the codeless quote token.
Impact
The newly registered ordinary or Special market cannot complete trades because both payment paths require receiveWithAuthorization() and subsequent ERC-20 transfers through the immutable quote-token address. Restoring service requires deploying and registering another market. Replacing the ordinary market also invalidates resting order signatures because they bind to its EIP-712 domain, while replacing the Special market requires its floor and term state to be reconstructed or migrated.
Failed trades revert atomically, so this causes a trading outage and recovery cost rather than partial payment or badge loss.
Recommendations
Create one shared quote-token preflight and call it from DeployBase, UpgradeSettlement, and UpgradeSpecialMarket before broadcasting. For each supported chain, require the configured address to equal Circle's canonical token address, then retain the code-presence and six-decimal checks as diagnostics. The exact-address check is what establishes support for the EIP-3009 flow the markets require.
Add regressions that reject a wrong-chain codeless address and a six-decimal token without EIP-3009 support, together with a positive control showing that the canonical token still completes a representative signed trade.
Remediation
Fixed, and beyond what the finding asked. The preflight checks the canonical address for the chain, code presence, ERC-20 shape, exactly 6 decimals, and probes for EIP-3009. It also refuses a treasury equal to the quote token and a treasury at an address the run is about to deploy to.
Verification provenance
Final disposition: Fixed. Verified at audit-v3-rereview-handoff, commit f020448976eb7c4895e1626ef3228876cff2b7d2. This earlier remediation disposition is carried forward from the prior re-review, not independently re-verified in the generation-d pass. The report-wide scope and evidence limitations remain applicable; see the final report overview.