Diamond PAU parallel controller with CCTP V2 Security Review

Summary

Spark has deployed a Parallelized Allocation Unit (PAU) stack on Arbitrum and Base - Beacon, PAUFactory, Controller, AccessControls, RateLimits, CCTPFacet, AdministeredAgentFactory, AdministeredAgent - and the onboarding action connects this new Controller to the existing Spark ALMProxy by granting it the CONTROLLER role.

The CONTROLLER role on an ALMProxy is powerful: it allows arbitrary calls to be executed from the proxy, which holds funds. However, in practice (== in the current scope), the impact of this action is narrowly bounded.

The only thing an allocator would be able to do through the PAU Controller after the role is granted is to call functions of facets that governance has explicitly wired in - in the case with this deployment, exactly one - the CCTP facet.

During the security review of this upcoming update, we’ve:

  • Performed deployment validation on both Base and Arbitrum, including bytecode and state verification
  • Performed a full-scope role analysis and evaluated the impact of every configured role
  • Reviewed the consequences of the parallel controller setup, considering worst-case scenarios

Our verdict: the onboarding action is safe to execute and does not introduce any significant risks.

Architecture in brief

The PAU (“Parallelized Allocation Unit”) architecture splits the previous monolithic controller into small, single-purpose contracts. The system now consists of these contracts:

  • Beacon - Spark’s per-chain deployment of Sky’s Beacon registry contract that stores the integration configurations - the selector mappings and facets.
  • Controller - the new diamond proxy. Receives external calls from allocator, dispatches them to the facets via delegateCall, navigated by the config from the Beacon.
  • RateLimits - still a standalone rate-limit engine with time-based refill.
  • AccessControls - once part of the ALM controller logic, now an independent contract that handles role management.
  • ALMProxy - the existing funds-holding contract, unchanged from the current architecture. It executes calls only for addresses holding its CONTROLLER role.
  • AdministeredAgent - an operational contract that holds the ALLOCATOR_ROLE. The allocator multisig is its actor. Separate grantor and revoker roles onboard and offboard actors.
  • Facets - integration modules (here: only CCTPFacet). Each integration is configured with an array of Wires - explicit mappings from an external-facing callSelector to an internal delegateSelector.

Architecture Diagram

What the onboarding action does

The onboarding action creates a single new trust link: the ALMProxy grants its CONTROLLER role to the PAU Controller. This section explains why that grant has limited impact.

The Controller rejects any selector that is not explicitly wired, and in this deployment only one integration is wired: the CCTPFacet. Below we walk through the dispatch mechanism, then the facet code, showing that it exposes just two state-mutating entry points, each with narrow, governance-bounded effects.

Dispatch layer

The Controller has no public functions that touch the proxy.
Every operational call enters through its fallback():

fallback() external payable {
    require(msg.data.length >= 4, InvalidCallDataLength(msg.data.length));

    Dispatch storage dispatch = _getControllerStorage().dispatches[msg.sig];

    address facet = dispatch.facet;

    require(facet != address(0), CallSelectorNotWired(msg.sig));

    // Replace the incoming selector with the delegate selector.
    ( bool success, bytes memory returnData ) = facet.delegatecall(
        abi.encodePacked(dispatch.delegateSelector, msg.data[4:])
    );
    ...
}

Any selector without a dispatch entry reverts. A wired selector is delegatecalled into its facet, so the facet runs in the Controller’s storage context and its role and rate limit checks go through the shared AccessControls and RateLimits contracts.

The dispatch table can only be modified by updateIntegrations() / removeIntegrations(), both gated on DEFAULT_ADMIN_ROLE, held solely by the SPARK_EXECUTOR (governance). And updateIntegrations() can only pull configs that already exist in the Beacon, whose admin is also the SPARK_EXECUTOR. Expanding the Controller’s capabilities therefore takes two governance steps (publish in Beacon, opt in on Controller) and cannot be done by any operational role.

CCTPFacet wiring

The deployment wires ten selectors, all into the CCTPFacet:

Wire (Controller selector)Facet functionMutability
cctp_transfer(uint256,uint32,uint64)transferstate-mutating
cctp_setDomainParameters(uint32,bytes32,uint32,uint32)setDomainParametersstate-mutating
cctp_toCCTPRateLimitKey()toCCTPRateLimitKeypure
cctp_getToDomainRateLimitKey(uint32)getToDomainRateLimitKeypure
cctp_getDomainParameters(uint32)getDomainParametersview
cctp_VERSION()VERSIONconstant getter
cctp_DESTINATION_CALLER()DESTINATION_CALLERconstant getter
cctp_MIN_FINALITY_THRESHOLD()MIN_FINALITY_THRESHOLDconstant getter
cctp_cctp()cctpimmutable getter
cctp_usdc()usdcimmutable getter

Eight of the ten wires are view or pure functions and getters of constant or immutable variables. The compiler guarantees these cannot write storage or produce side effects, so they are out of the impact surface. Everything this action opens reduces to the two remaining functions, transfer and setDomainParameters.

transfer

The only function reachable by the operational path, gated on ALLOCATOR_ROLE:

function transfer(uint256 amount, uint32 destinationDomain, uint64 feeCapRate)
    external
    override
    nonReentrant
    onlyRole(ALLOCATOR_ROLE)
{
    _decreaseRateLimit(toCCTPRateLimitKey(),                       amount);
    _decreaseRateLimit(getToDomainRateLimitKey(destinationDomain), amount);

    DomainParameters storage params = _getFacetStorage().domainParameters[destinationDomain];

    bytes32 recipient     = params.mintRecipient;
    uint32  minFeeCapRate = params.minFeeCapRate;
    uint32  maxFeeCapRate = params.maxFeeCapRate;

    require(recipient != 0,              "CCTPFacet/domain-not-configured");
    require(feeCapRate >= minFeeCapRate, "CCTPFacet/fee-cap-rate-too-low");
    require(feeCapRate <= maxFeeCapRate, "CCTPFacet/fee-cap-rate-too-high");

    // Approve USDC to CCTP from the proxy (assumes the proxy has enough USDC).
    _approve(usdc, cctp, amount);

    // If amount is larger than limit it must be split into multiple calls.
    uint256 burnLimit =
        ICCTPTokenMinterLike(ICCTPLike(cctp).localMinter()).burnLimitsPerMessage(usdc);

    while (amount > 0) {
        uint256 transferAmount = amount > burnLimit ? burnLimit : amount;
        uint256 maxFee         = (transferAmount * feeCapRate) / _ONE_HUNDRED_PERCENT;

        _initiateTransfer(transferAmount, maxFee, recipient, destinationDomain);

        amount -= transferAmount;
    }

    // Clear approvals
    _approve(usdc, cctp, 0);
}

The checks run in this order:

  1. onlyRole(ALLOCATOR_ROLE) queries the shared AccessControls. The role is held by exactly one address, the AdministeredAgent (see role analysis).
  2. Global rate limit LIMIT_USDC_TO_CCTP.
    RateLimits.triggerRateLimitDecrease reverts both when the key was never configured and when amount exceeds the currently recharged capacity. Limits are set only by the RateLimits admin (governance).
  3. Per-domain rate limit LIMIT_USDC_TO_DOMAIN[destinationDomain]. A domain with no configured limit is unreachable regardless of the global one.
  4. The domain must be configured: mintRecipient != 0, writable only through governance-gated setDomainParameters.
  5. feeCapRate must fall inside the governance-set window for the domain.

Only after all five gates pass does the facet touch the proxy, calling only two external functions via ALMProxy.doCall:

  • USDC.approve(cctp, amount), reset to 0 at the end, so no allowance survives the transaction.
  • CCTP.depositForBurn(...) with every argument either caller-bounded or fixed: mintRecipient is the governance-configured value (the caller cannot choose where funds go), burnToken is the immutable USDC address, destinationCaller is 0 (anyone may relay, but only to the fixed recipient), maxFee is bounded by the fee window, and minFinalityThreshold is the constant 2000, so only standard finalized messages are possible.

The loop splits amount into chunks that fit CCTP’s per-message burn limit. The destination and total stay the same.

setDomainParameters

function setDomainParameters(
    uint32  destinationDomain,
    bytes32 recipient,
    uint32  minFeeCapRate,
    uint32  maxFeeCapRate
)
    external
    override
    nonReentrant
    onlyRole(DEFAULT_ADMIN_ROLE)
{
    require(recipient != bytes32(0), "CCTPFacet/zero-recipient");

    require(minFeeCapRate <= maxFeeCapRate,       "CCTPFacet/min-fee-cap-rate-too-high");
    require(maxFeeCapRate < _ONE_HUNDRED_PERCENT, "CCTPFacet/max-fee-cap-rate-too-high");

    _getFacetStorage().domainParameters[destinationDomain] = DomainParameters(
        recipient,
        minFeeCapRate,
        maxFeeCapRate
    );

    emit CCTPDomainParametersSet(destinationDomain, recipient, minFeeCapRate, maxFeeCapRate);
}

A configuration setter callable only by governance (DEFAULT_ADMIN_ROLE).
It makes no external calls, including to the ALMProxy, and moves no funds. It only writes a domain’s mint recipient and fee window, with basic sanity checks. It cannot unset a domain, so routes are disabled by setting their rate limit to zero.
This is how governance pre-approves the destinations that transfer is allowed to use.

Call path summary

ALM relayer multisig  (actor)
        │  call(...)
        ▼
AdministeredAgent  (holds ALLOCATOR_ROLE, holds no funds)
        │  cctp_transfer(amount, destinationDomain, feeCapRate)
        ▼
PAU Controller  (fallback: selector must be wired, else revert)
        │  delegatecall to CCTPFacet.transfer
        ▼
CCTPFacet.transfer enforces, in order:
    1. onlyRole(ALLOCATOR_ROLE)                        (AccessControls)
    2. rate limit decrease: global LIMIT_USDC_TO_CCTP  (reverts if unset/exhausted)
    3. rate limit decrease: LIMIT_USDC_TO_DOMAIN[domain]
    4. domain configured: mintRecipient != 0           (set only by governance)
    5. minFeeCapRate <= feeCapRate <= maxFeeCapRate    (bounds set only by governance)
        │  via ALMProxy.doCall, exactly two call shapes
        ▼
USDC.approve(cctp, amount)
then CCTP.depositForBurn(
    amount, destinationDomain,
    mintRecipient = governance-configured  (caller cannot choose),
    burnToken     = USDC (immutable),
    destinationCaller = 0, maxFee bounded, finality = 2000)

Granting CONTROLLER to the PAU Controller therefore adds just two things: rate-limited USDC bridging via CCTP to governance-approved recipients, and a governance-only configuration setting.

Role analysis

Four access control domains matter for this deployment:

  • AccessControls, shared by the Controller and facets. Defines DEFAULT_ADMIN_ROLE (held by SPARK_EXECUTOR) and ALLOCATOR_ROLE (held only by the AdministeredAgent). The facet’s onlyRole and the Controller’s onlyAdmin both resolve here.
  • RateLimits. Its admin (governance) sets the limits, and its CONTROLLER role, granted only to the PAU Controller, may trigger decreases and increases.
  • ALMProxy. Its CONTROLLER role, the grant under review, allows doCall. The CCTPFacet uses it only for the two call shapes shown above.
  • AdministeredAgent. The indirection in front of ALLOCATOR_ROLE. The agent holds no funds and has no privileges anywhere except that role. It defines its own admin, actor, grantor and revoker roles.

Which roles each entity holds across these domains, and what they allow:

  • SPARK_EXECUTOR (governance) holds DEFAULT_ADMIN_ROLE in AccessControls, the admin of RateLimits, ALMProxy, the Beacon, and the AdministeredAgent. It sets and removes rate limits, wires and integrations on the Controller, publishes configs in the Beacon, and grants or revokes every role listed here. For CCTP facet it configures domain parameters and recipients via setDomainParameters.
  • PAU Controller holds CONTROLLER in RateLimits (may trigger limit decreases and increases) and CONTROLLER in ALMProxy (may execute calls from the proxy). Both are only executable through the wired facet functions.
  • AdministeredAgent holds ALLOCATOR_ROLE in AccessControls, which allows it to call cctp_transfer on the Controller. It holds no other role in any contract.
  • ALM_RELAYER_MULTISIG holds the actor role on the AdministeredAgent, so it can execute calls from the agent, in practice triggering CCTP transfers along the configured routes within the rate limits. It holds no role in AccessControls, RateLimits or the ALMProxy.
  • PAU_GRANTOR_MULTISIG holds the grantor role on the AdministeredAgent and can add actors.
  • ALM_FREEZER_MULTISIG holds the revoker role on the AdministeredAgent and can remove actors.

Worst case analysis

Compromised relayer multisig

On the PAU Controller, the only state-changing call available to the attacker is cctp_transfer(), nothing else is accessible. Funds can only be burned toward the governance-approved mint recipient on governance-enabled domains, within the per-domain and global rate limits.

Impact: Temporary misplacement of USDC between Spark-controlled addresses, plus bounded fee expenditure.

Mitigation: The freezer multisig can revoke the actor immediately.

Note: The same ALM_RELAYER_MULTISIG has the RELAYER role on the ALM Controller, so a malicious relayer can also interact with the other existing integrations through that controller.

Compromised freezer multisig

A compromised freezer multisig can remove any actor from the PAU_ADMINISTERED_AGENT.

Impact: USDC cctp_transfer calls would not be available during that period, but existing funds would remain safe.

Mitigation: Governance revokes the freezer role, and the grantor restores the removed actors.

Compromised grantor multisig

A compromised grantor multisig can add a new actor to the PAU_ADMINISTERED_AGENT.

Impact: Same as in “Compromised relayer multisig,” except that it affects only the PAU Controller.

Mitigation: Governance revokes the grantor role and removes any actors added by the compromised grantor.

Parallel controller architecture security review

We reviewed what changes when two controllers share one proxy. The concerns cover migration mechanics, rate limit accounting, and reentrancy across controllers.

Migration atomicity: opening and closing an integration

An integration is live on a controller only when all of the following hold:

  1. The controller holds CONTROLLER on the shared proxy
  2. The facet is wired: Beacon.setIntegration(id, config) and Controller.updateIntegrations([id])
  3. The rate-limit keys for that integration are set above zero on that controller’s RateLimits
  4. The facet-specific admin config is set (for example CCTP domainParameters/recipient, ERC4626 maxExchangeRate, swap maxSlippage, LayerZero/Centrifuge setRecipient)

Migrating an integration therefore means enabling it on PAU Controller (steps 2 to 4 plus limits) and disabling it on ALM Controller. Because rate limits are per-controller, the enable/disable switch is primarily the rate limit on each side.

If open and close are not done atomically, one of two bad states results:

Failure mode A: overlap (open before or without close)

If the integration is open on both controllers at once, the limits are additive because they live in separate RateLimits contracts, while both controllers draw from the same proxy funds. A vault meant to be capped at 50M can end up with 100M if both sides hold a 50M limit at the same time.

Failure mode B: gap (close before onboarded on PAU)

If ALM limits are closed before PAU limits are set up, the integration works nowhere. Calls revert, so no funds are lost directly, but funds already sitting in the integration cannot be moved or withdrawn until a new spell restores access. If the position starts losing value during that gap, the delay can turn into a real loss.

Recommendation

We recommend never splitting a migration across spells. Attach a checklist to every migration spell:

  • set every relevant ALM Controller rate limit key to zero on ALM_RATE_LIMITS
  • wire on PAU Controller: Beacon.setIntegration(id, config), then Controller.updateIntegrations([id])
  • set facet admin config on PAU Controller
  • set PAU Controller rate limits on PAU_RATE_LIMITS
  • check that the integration’s keys are above zero on exactly one RateLimits and zero on the other

Setting rate limits for a position the proxy already holds

Take an ERC4626 vault as the example. The deposit rate limit stores maxAmount, the ceiling, and lastAmount, the remaining allowance. On deposit _decreaseRateLimit lowers lastAmount, and on withdraw it is raised back. So when governance sets maxAmount = 50M, once 50M has been deposited, lastAmount reaches 0 and further deposits revert, keeping at most 50M in that vault.

The vault position is owned by the proxy, so migrating the integration does not move any funds. But the previous deposits were recorded in ALM Controller’s RateLimits, and PAU Controller starts with a fresh counter that knows nothing about them. The 3-arg setRateLimitData sets that counter to the full maxAmount, as if nothing was ever deposited:

function setRateLimitData(bytes32 key, uint256 maxAmount, uint256 slope) external {
    setRateLimitData(key, maxAmount, slope, maxAmount, block.timestamp);  // lastAmount = maxAmount = FULL
}

Suppose ALM Controller deposited 40M into a vault with a 50M cap, so the vault holds 40M. The spell onboards PAU Controller with the 3-arg form at 50M. PAU Controller believes 50M of room remains and lets the allocator deposit another 50M. The vault ends up at 90M against an intended 50M cap.

Recommendation

We recommend using the 4-arg form and setting the remaining allowance by hand:

// remaining allowance = maxAmount - currentExposure = 50M - 40M = 10M
setRateLimitData(key, 50M, slope, 10M, block.timestamp);

Reentrancy across the two controllers

The guard is per-controller, the proxy has none

The nonReentrant guard is per controller. ALM and PAU Controllers have separate guard slots, and the shared ALMProxy has no guard at all. A call that entered through one controller is not blocked from reentering through the other.

Why it is not exploitable today

With only the CCTP facet wired, the external calls are USDC.approve and depositForBurn, both trusted and non-reentrant.

Which future facets could make this exploitable

The risk appears with the first facet whose external calls can pass control to a third party. Many controller functions read the proxy balance, make an external call, read the balance again, and apply the rate limit to the difference. A third party gaining control inside that window can reenter through the sibling controller and move proxy funds, so the measured difference no longer matches what the function actually moved.

The sharpest targets are the legacy functions marked !!! Rate limited at end of function !!!: withdrawPSM(), redeemERC4626(), withdrawAave(). They withdraw first and call triggerRateLimitDecrease at the end on the measured difference. A reentering deposit into the same position shrinks that difference, so the limit is decreased by less than what was withdrawn.

Onboarding any future facet should therefore include a reentrancy review, checking whether its external calls can pass control to a third party.

Deployments verification: Arbitrum

Bytecode verification

Deployed bytecode matches source at the pinned releases (immutables and metadata aside):

  • Beacon (0x86036CE5d2f792367C0AA43164e688d13c5A60A8) – diamond-pau v1.14.0
  • PAUFactory (0x3968a022D955Bbb7927cc011A48601B65a33F346) – diamond-pau v1.14.0
  • CCTPFacet (0xeCCA0D296Cb133081d41E9772B60D57F5fd2798E) – diamond-pau v1.14.0
  • AccessControls (0x8386f819860D54B1180539Ff4852E4CAECef8A1D) – diamond-pau v1.14.0
  • RateLimits (0x4824C4336a1a11979068A544958dCe5D49B42752) – diamond-pau v1.14.0
  • Controller (0x04ACB9e9bbd64A425677edC535D6B30cfD74E42f) – diamond-pau v1.14.0
  • AdministeredAgent (0x0745aae633E8318a063D383791bCc0d8C82F46C6) – pau-administered-agent v1.0.0
  • AdministeredAgentFactory (0xCBA0C0a2a0B6Bb11233ec4EA85C5bFfea33e724d) – pau-administered-agent v1.0.0

Roles & configuration

SPARK_BEACON (v1.14.0)

  • DEFAULT_ADMIN_ROLE -> SPARK_EXECUTOR (count 1)
  • integrations() -> only 1 integration CCTP_FACET
  • getConfig(bytes32("CCTP_FACET")) -> (CCTP_FACET, 10 wires)
  • matches the Sky PAU Beacon on Ethereum mainnet (Ethereum.BEACON), same 10 wire pairs

SPARK_PAU_FACTORY (v1.14.0)

SPARK_ADMINISTERED_AGENT_FACTORY (v1.0.0)

PAU_ADMINISTERED_AGENT (v1.0.0)

CCTP_FACET (v1.14.0)

PAU_ACCESS_CONTROLS (v1.14.0)

PAU_RATELIMITS (v1.14.0)

PAU_CONTROLLER (v1.14.0)

Wire tables

CCTP_FACET

Integration id: bytes32("CCTP_FACET") = 0x434354505f464143455400000000000000000000000000000000000000000000

  • cctp_setDomainParameters(uint32,bytes32,uint32,uint32) -> setDomainParameters(uint32,bytes32,uint32,uint32)
    • 0x6969ad0b -> 0x0f32bb10
  • cctp_transfer(uint256,uint32,uint64) -> transfer(uint256,uint32,uint64)
    • 0xd82592c1 -> 0xb9bbdcf2
  • cctp_toCCTPRateLimitKey() -> toCCTPRateLimitKey()
    • 0x9bbb03f2 -> 0x127febbd
  • cctp_getDomainParameters(uint32) -> getDomainParameters(uint32)
    • 0x3c7020e9 -> 0xa24e0ef5
  • cctp_getToDomainRateLimitKey(uint32) -> getToDomainRateLimitKey(uint32)
    • 0xb2a4d06f -> 0x845da79b
  • cctp_VERSION() -> VERSION()
    • 0x87bd715a -> 0xffa1ad74
  • cctp_DESTINATION_CALLER() -> DESTINATION_CALLER()
    • 0x2c7389a0 -> 0x62cef4b9
  • cctp_MIN_FINALITY_THRESHOLD() -> MIN_FINALITY_THRESHOLD()
    • 0x75ef4015 -> 0x5bd0f214
  • cctp_cctp() -> cctp()
    • 0xf9b8d11c -> 0xe3329e32
  • cctp_usdc() -> usdc()
    • 0x3520c914 -> 0x3e413bee

Deployments verification: Base

Bytecode verification

Deployed bytecode matches source at the pinned releases (immutables and metadata aside):

  • Beacon (0x7ac96180C4d6b2A328D3a19ac059D0E7Fc3C6d41) – diamond-pau v1.14.0
  • PAUFactory (0x011A115b5498B85b3d12245A3a7296F77325B5C3) – diamond-pau v1.14.0
  • CCTPFacet (0xb22d50c393c6E1D13E3e05B172448dD8BF8DdC32) – diamond-pau v1.14.0
  • AccessControls (0xE593c8c6a31d88cab100244Afd352efb127f9a16) – diamond-pau v1.14.0
  • RateLimits (0x5E311e8BCe95F4e8d4920E70985ED4aC122a838A) – diamond-pau v1.14.0
  • Controller (0xd864bF1Ea2f78Dc2013E3FC7e4C474383BE9d456) – diamond-pau v1.14.0
  • AdministeredAgent (0x70E46bAf2E3F27a119757D7b796c641C8bc087cE) – pau-administered-agent v1.0.0
  • AdministeredAgentFactory (0xD711DbfD937a45e2C89CA0D4781cfa5BAb32e752) – pau-administered-agent v1.0.0

Roles & configuration

SPARK_BEACON (v1.14.0)

  • DEFAULT_ADMIN_ROLE -> SPARK_EXECUTOR (count 1)
  • integrations() -> only 1 integration CCTP_FACET
  • getConfig(bytes32("CCTP_FACET")) -> (CCTP_FACET, 10 wires)
  • matches the Sky PAU Beacon on Ethereum mainnet (Ethereum.BEACON), same 10 wire pairs

SPARK_PAU_FACTORY (v1.14.0)

SPARK_ADMINISTERED_AGENT_FACTORY (v1.0.0)

CCTP_FACET (v1.14.0)

PAU_ACCESS_CONTROLS (v1.14.0)

PAU_ADMINISTERED_AGENT (v1.0.0)

PAU_CONTROLLER (v1.14.0)

PAU_RATELIMITS (v1.14.0)

Wire tables

CCTP_FACET

Integration id: bytes32("CCTP_FACET") = 0x434354505f464143455400000000000000000000000000000000000000000000

  • cctp_setDomainParameters(uint32,bytes32,uint32,uint32) -> setDomainParameters(uint32,bytes32,uint32,uint32)
    • 0x6969ad0b -> 0x0f32bb10
  • cctp_transfer(uint256,uint32,uint64) -> transfer(uint256,uint32,uint64)
    • 0xd82592c1 -> 0xb9bbdcf2
  • cctp_toCCTPRateLimitKey() -> toCCTPRateLimitKey()
    • 0x9bbb03f2 -> 0x127febbd
  • cctp_getDomainParameters(uint32) -> getDomainParameters(uint32)
    • 0x3c7020e9 -> 0xa24e0ef5
  • cctp_getToDomainRateLimitKey(uint32) -> getToDomainRateLimitKey(uint32)
    • 0xb2a4d06f -> 0x845da79b
  • cctp_VERSION() -> VERSION()
    • 0x87bd715a -> 0xffa1ad74
  • cctp_DESTINATION_CALLER() -> DESTINATION_CALLER()
    • 0x2c7389a0 -> 0x62cef4b9
  • cctp_MIN_FINALITY_THRESHOLD() -> MIN_FINALITY_THRESHOLD()
    • 0x75ef4015 -> 0x5bd0f214
  • cctp_cctp() -> cctp()
    • 0xf9b8d11c -> 0xe3329e32
  • cctp_usdc() -> usdc()
    • 0x3520c914 -> 0x3e413bee

Appendix: Bytecode verification commands

Methodology: Beacon, PAUFactory, and AdministeredAgentFactory were verified via forge verify-bytecode. The remaining contracts were deployed by these verified factories, as confirmed via cast logs.

Arbitrum

❯ forge verify-bytecode 0x86036CE5d2f792367C0AA43164e688d13c5A60A8 \
    lib/diamond-pau/src/Beacon.sol:Beacon \
    --rpc-url $ARBITRUM_RPC_URL \
    --encoded-constructor-args $(cast abi-encode "constructor(address)" 0xC758519Ace14E884fdbA9ccE25F2DbE81b7e136f)
Verifying bytecode for contract Beacon at address 0x86036CE5d2f792367C0AA43164e688d13c5A60A8
Creation code matched with status full
Runtime code matched with status full

❯ forge verify-bytecode 0x3968a022D955Bbb7927cc011A48601B65a33F346 \
    lib/diamond-pau/src/PAUFactory.sol:PAUFactory \
    --rpc-url $ARBITRUM_RPC_URL \
    --encoded-constructor-args $(cast abi-encode "constructor(address)" 0x86036CE5d2f792367C0AA43164e688d13c5A60A8)
Verifying bytecode for contract PAUFactory at address 0x3968a022D955Bbb7927cc011A48601B65a33F346
Creation code matched with status full
Runtime code matched with status full

❯ forge verify-bytecode 0xCBA0C0a2a0B6Bb11233ec4EA85C5bFfea33e724d \
    lib/pau-administered-agent/src/AdministeredAgentFactory.sol:AdministeredAgentFactory \
    --rpc-url $ARBITRUM_RPC_URL
Verifying bytecode for contract AdministeredAgentFactory at address 0xCBA0C0a2a0B6Bb11233ec4EA85C5bFfea33e724d
Creation code matched with status full
Runtime code matched with status full

❯ FROM=506440750; TO=506440800
❯ cast logs --rpc-url $ARBITRUM_RPC_URL --address 0x3968a022D955Bbb7927cc011A48601B65a33F346 \
    --from-block $FROM --to-block $TO "AccessControlsDeployed(address indexed accessControls)"
- address: 0x3968a022D955Bbb7927cc011A48601B65a33F346
  blockHash: 0xece579665127a28229de74a5dc4d2b6d2a445df29d65bcd6e7ab63772c81dcbf
  blockNumber: 506440776
  data: 0x
  logIndex: 6
  removed: false
  topics: [
        0x0beb1af00f5ca15b209e2fdbe3bf168c3e6af933b2c466225243687ccd443d0a
        0x0000000000000000000000008386f819860d54b1180539ff4852e4caecef8a1d
  ]
  transactionHash: 0xeb410a03bad6c4084807f43dfb3b523ee6a8566e130bfecb3e356714dc684f22
  transactionIndex: 2
 
❯ cast logs --rpc-url $ARBITRUM_RPC_URL --address 0x3968a022D955Bbb7927cc011A48601B65a33F346 \
    --from-block $FROM --to-block $TO "RateLimitsDeployed(address indexed rateLimits)"
- address: 0x3968a022D955Bbb7927cc011A48601B65a33F346
  blockHash: 0x6d0be3e3157f4b37fd2e487ec0b7fd3f1be0824ac0950f4ff825a1cd7adefe60
  blockNumber: 506440780
  data: 0x
  logIndex: 19
  removed: false
  topics: [
        0x32042273d1658350d79d35cb5c869a9ec9331ec670e123faf6b4bc1e9dd84d5c
        0x0000000000000000000000004824c4336a1a11979068a544958dce5d49b42752
  ]
  transactionHash: 0x7bfc18fef9d27b3fb973141d41942cc818ca21fd1ae4da5d2e4f182a3dc81b6d
  transactionIndex: 8

❯ cast logs --rpc-url $ARBITRUM_RPC_URL --address 0x3968a022D955Bbb7927cc011A48601B65a33F346 \
    --from-block $FROM --to-block $TO "ControllerDeployed(address indexed controller, address accessControls, address proxy, address rateLimits)"
- address: 0x3968a022D955Bbb7927cc011A48601B65a33F346
  blockHash: 0xeb93614b336ba4c9b9ac7b3ab9a66249484d08952b36dca6fe44914e6e8a83a6
  blockNumber: 506440784
  data: 0x0000000000000000000000008386f819860d54b1180539ff4852e4caecef8a1d00000000000000000000000092afd6f2385a90e44da3a8b60fe36f6cbe1d87090000000000000000000000004824c4336a1a11979068a544958dce5d49b42752
  logIndex: 19
  removed: false
  topics: [
        0xeedc799f1549d06e41f68e520636a7e754aa8b5b11a80f5d678a576785be9324
        0x00000000000000000000000004acb9e9bbd64a425677edc535d6b30cfd74e42f
  ]
  transactionHash: 0x73bf66b91491dcee12e53723b19a7622e6d7b29ca54d9fe80a45db5971434371
  transactionIndex: 13

❯ cast logs --rpc-url $ARBITRUM_RPC_URL --address 0xCBA0C0a2a0B6Bb11233ec4EA85C5bFfea33e724d \
    --from-block $FROM --to-block $TO "AdministeredAgentDeployed(address indexed administeredAgent)"
- address: 0xCBA0C0a2a0B6Bb11233ec4EA85C5bFfea33e724d
  blockHash: 0x36d6f7f16f6f92e4e167159200787ad9de1dbe29070c1759956551ed4148df76
  blockNumber: 506440788
  data: 0x
  logIndex: 13
  removed: false
  topics: [
        0xf403b4c002ecf92eb862794f17686c0a7d2dd1ed273fb106e877672f12ba71a3
        0x0000000000000000000000000745aae633e8318a063d383791bcc0d8c82f46c6
  ]
  transactionHash: 0x08a393425c2492d9de42391a23ed0b6a8eb1a80ce45aabb89708235650608f76
  transactionIndex: 8

Base

❯ forge verify-bytecode 0x7ac96180C4d6b2A328D3a19ac059D0E7Fc3C6d41 \
    lib/diamond-pau/src/Beacon.sol:Beacon \
    --rpc-url $BASE_RPC_URL \
    --encoded-constructor-args $(cast abi-encode "constructor(address)" 0xC758519Ace14E884fdbA9ccE25F2DbE81b7e136f)
Verifying bytecode for contract Beacon at address 0x7ac96180C4d6b2A328D3a19ac059D0E7Fc3C6d41
Creation code matched with status full
Runtime code matched with status full

❯ forge verify-bytecode 0x011A115b5498B85b3d12245A3a7296F77325B5C3 \
    lib/diamond-pau/src/PAUFactory.sol:PAUFactory \
    --rpc-url $BASE_RPC_URL \
    --encoded-constructor-args $(cast abi-encode "constructor(address)" 0x7ac96180C4d6b2A328D3a19ac059D0E7Fc3C6d41)
Verifying bytecode for contract PAUFactory at address 0x011A115b5498B85b3d12245A3a7296F77325B5C3
Creation code matched with status full
Runtime code matched with status full

❯ forge verify-bytecode 0xD711DbfD937a45e2C89CA0D4781cfa5BAb32e752 \
    lib/pau-administered-agent/src/AdministeredAgentFactory.sol:AdministeredAgentFactory \
    --rpc-url $BASE_RPC_URL
Verifying bytecode for contract AdministeredAgentFactory at address 0xD711DbfD937a45e2C89CA0D4781cfa5BAb32e752
Creation code matched with status full
Runtime code matched with status full

❯ B=51606638
❯ cast logs --rpc-url $BASE_RPC_URL --address 0x011A115b5498B85b3d12245A3a7296F77325B5C3 \
    --from-block $B --to-block $B "AccessControlsDeployed(address indexed accessControls)"
- address: 0x011A115b5498B85b3d12245A3a7296F77325B5C3
  blockHash: 0x385164b6f63e06ce06269d1fcd2864456e3d1c2618ec25dfd022341179d9a917
  blockNumber: 51606638
  data: 0x
  logIndex: 1019
  removed: false
  topics: [
        0x0beb1af00f5ca15b209e2fdbe3bf168c3e6af933b2c466225243687ccd443d0a
        0x000000000000000000000000e593c8c6a31d88cab100244afd352efb127f9a16
  ]
  transactionHash: 0x39ff9fc3d3fafc6a8eddd1d04442790653e14dbf8bcdfa12e7fdcbfd812065d4
  transactionIndex: 362

❯ cast logs --rpc-url $BASE_RPC_URL --address 0x011A115b5498B85b3d12245A3a7296F77325B5C3 \
    --from-block $B --to-block $B "RateLimitsDeployed(address indexed rateLimits)"
- address: 0x011A115b5498B85b3d12245A3a7296F77325B5C3
  blockHash: 0x385164b6f63e06ce06269d1fcd2864456e3d1c2618ec25dfd022341179d9a917
  blockNumber: 51606638
  data: 0x
  logIndex: 1021
  removed: false
  topics: [
        0x32042273d1658350d79d35cb5c869a9ec9331ec670e123faf6b4bc1e9dd84d5c
        0x0000000000000000000000005e311e8bce95f4e8d4920e70985ed4ac122a838a
  ]
  transactionHash: 0x29c3c86806ee2fcb60e23309cba933dcec92d0d1487e59ae3108a900e7a3c32c
  transactionIndex: 363

❯ cast logs --rpc-url $BASE_RPC_URL --address 0x011A115b5498B85b3d12245A3a7296F77325B5C3 \
    --from-block $B --to-block $B "ControllerDeployed(address indexed controller, address accessControls, address proxy, address rateLimits)"
- address: 0x011A115b5498B85b3d12245A3a7296F77325B5C3
  blockHash: 0x385164b6f63e06ce06269d1fcd2864456e3d1c2618ec25dfd022341179d9a917
  blockNumber: 51606638
  data: 0x000000000000000000000000e593c8c6a31d88cab100244afd352efb127f9a160000000000000000000000002917956eff0b5eaf030abdb4ef4296df775009ca0000000000000000000000005e311e8bce95f4e8d4920e70985ed4ac122a838a
  logIndex: 1023
  removed: false
  topics: [
        0xeedc799f1549d06e41f68e520636a7e754aa8b5b11a80f5d678a576785be9324
        0x000000000000000000000000d864bf1ea2f78dc2013e3fc7e4c474383be9d456
  ]
  transactionHash: 0x9f0cd3752b09380b2d571b698c43d58fa282ec7bdfb9c433187ed7176f5200a9
  transactionIndex: 364

❯ cast logs --rpc-url $BASE_RPC_URL --address 0xD711DbfD937a45e2C89CA0D4781cfa5BAb32e752 \
    --from-block $B --to-block $B "AdministeredAgentDeployed(address indexed administeredAgent)"
- address: 0xD711DbfD937a45e2C89CA0D4781cfa5BAb32e752
  blockHash: 0x385164b6f63e06ce06269d1fcd2864456e3d1c2618ec25dfd022341179d9a917
  blockNumber: 51606638
  data: 0x
  logIndex: 1025
  removed: false
  topics: [
        0xf403b4c002ecf92eb862794f17686c0a7d2dd1ed273fb106e877672f12ba71a3
        0x00000000000000000000000070e46baf2e3f27a119757d7b796c641c8bc087ce
  ]
  transactionHash: 0x37be1ab9d2d7894e68a675c3ca266020cf2b57f96b201c1c94d2f1a5995897eb
  transactionIndex: 365