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
CONTROLLERrole. - 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-facingcallSelectorto an internaldelegateSelector.

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 function | Mutability |
|---|---|---|
cctp_transfer(uint256,uint32,uint64) | transfer | state-mutating |
cctp_setDomainParameters(uint32,bytes32,uint32,uint32) | setDomainParameters | state-mutating |
cctp_toCCTPRateLimitKey() | toCCTPRateLimitKey | pure |
cctp_getToDomainRateLimitKey(uint32) | getToDomainRateLimitKey | pure |
cctp_getDomainParameters(uint32) | getDomainParameters | view |
cctp_VERSION() | VERSION | constant getter |
cctp_DESTINATION_CALLER() | DESTINATION_CALLER | constant getter |
cctp_MIN_FINALITY_THRESHOLD() | MIN_FINALITY_THRESHOLD | constant getter |
cctp_cctp() | cctp | immutable getter |
cctp_usdc() | usdc | immutable 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:
onlyRole(ALLOCATOR_ROLE)queries the shared AccessControls. The role is held by exactly one address, the AdministeredAgent (see role analysis).- Global rate limit
LIMIT_USDC_TO_CCTP.
RateLimits.triggerRateLimitDecreasereverts both when the key was never configured and whenamountexceeds the currently recharged capacity. Limits are set only by the RateLimits admin (governance). - Per-domain rate limit
LIMIT_USDC_TO_DOMAIN[destinationDomain]. A domain with no configured limit is unreachable regardless of the global one. - The domain must be configured:
mintRecipient != 0, writable only through governance-gatedsetDomainParameters. feeCapRatemust 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:mintRecipientis the governance-configured value (the caller cannot choose where funds go),burnTokenis the immutable USDC address,destinationCalleris 0 (anyone may relay, but only to the fixed recipient),maxFeeis bounded by the fee window, andminFinalityThresholdis 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 bySPARK_EXECUTOR) andALLOCATOR_ROLE(held only by the AdministeredAgent). The facet’sonlyRoleand the Controller’sonlyAdminboth resolve here. - RateLimits. Its admin (governance) sets the limits, and its
CONTROLLERrole, granted only to the PAU Controller, may trigger decreases and increases. - ALMProxy. Its
CONTROLLERrole, the grant under review, allowsdoCall. 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) holdsDEFAULT_ADMIN_ROLEin 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 viasetDomainParameters.- PAU Controller holds
CONTROLLERin RateLimits (may trigger limit decreases and increases) andCONTROLLERin ALMProxy (may execute calls from the proxy). Both are only executable through the wired facet functions. - AdministeredAgent holds
ALLOCATOR_ROLEin AccessControls, which allows it to callcctp_transferon the Controller. It holds no other role in any contract. ALM_RELAYER_MULTISIGholds 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_MULTISIGholds the grantor role on the AdministeredAgent and can add actors.ALM_FREEZER_MULTISIGholds 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:
- The controller holds
CONTROLLERon the shared proxy - The facet is wired:
Beacon.setIntegration(id, config)andController.updateIntegrations([id]) - The rate-limit keys for that integration are set above zero on that controller’s
RateLimits - The facet-specific admin config is set (for example CCTP
domainParameters/recipient, ERC4626maxExchangeRate, swapmaxSlippage, LayerZero/CentrifugesetRecipient)
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), thenController.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
RateLimitsand 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_FACETgetConfig(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)
- no roles
- immutable
beacon->SPARK_BEACON - deploy events:
AccessControlsDeployed->PAU_ACCESS_CONTROLS,RateLimitsDeployed->PAU_RATELIMITS,ControllerDeployed->PAU_CONTROLLER
SPARK_ADMINISTERED_AGENT_FACTORY (v1.0.0)
- no roles
- deploy event:
AdministeredAgentDeployed->PAU_ADMINISTERED_AGENT
PAU_ADMINISTERED_AGENT (v1.0.0)
- admin ->
SPARK_EXECUTOR(count 1) - actor ->
ALM_RELAYER_MULTISIG(count 1) - grantor ->
PAU_GRANTOR_MULTISIG(count 1) - revoker ->
ALM_FREEZER_MULTISIG(count 1)
CCTP_FACET (v1.14.0)
- no facet-specific roles
- immutable
cctp->CCTP_TOKEN_MESSENGER(TokenMessengerV2) - immutable
usdc->USDC - matches the Sky mainnet facet (
Ethereum.CCTP_FACET) except for theusdcimmutable and compiler metadata
PAU_ACCESS_CONTROLS (v1.14.0)
DEFAULT_ADMIN_ROLE->SPARK_EXECUTOR(count 1)ALLOCATOR_ROLE->PAU_ADMINISTERED_AGENT(count 1)
PAU_RATELIMITS (v1.14.0)
DEFAULT_ADMIN_ROLE->SPARK_EXECUTOR(count 1)CONTROLLER->PAU_CONTROLLER(count 1)
PAU_CONTROLLER (v1.14.0)
- immutable
beacon()->SPARK_BEACON accessControls()->PAU_ACCESS_CONTROLSproxy()->ALM_PROXYrateLimits()->PAU_RATELIMITSintegrations()-> only 1 integration CCTP_FACET
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_FACETgetConfig(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)
- no roles
- immutable
beacon->SPARK_BEACON - deploy events:
AccessControlsDeployed->PAU_ACCESS_CONTROLS,RateLimitsDeployed->PAU_RATELIMITS,ControllerDeployed->PAU_CONTROLLER
SPARK_ADMINISTERED_AGENT_FACTORY (v1.0.0)
- no roles
- deploy event:
AdministeredAgentDeployed->PAU_ADMINISTERED_AGENT
CCTP_FACET (v1.14.0)
- no facet-specific roles
- immutable
cctp->CCTP_TOKEN_MESSENGER(TokenMessengerV2) - immutable
usdc->USDC - matches the Sky mainnet facet (
Ethereum.CCTP_FACET) except for theusdcimmutable and compiler metadata
PAU_ACCESS_CONTROLS (v1.14.0)
DEFAULT_ADMIN_ROLE->SPARK_EXECUTOR(count 1)ALLOCATOR_ROLE->PAU_ADMINISTERED_AGENT(count 1)
PAU_ADMINISTERED_AGENT (v1.0.0)
- admin ->
SPARK_EXECUTOR(count 1) - actor ->
ALM_RELAYER_MULTISIG(count 1) - grantor ->
PAU_GRANTOR_MULTISIG(count 1) - revoker ->
ALM_FREEZER_MULTISIG(count 1)
PAU_CONTROLLER (v1.14.0)
- immutable
beacon()->SPARK_BEACON accessControls()->PAU_ACCESS_CONTROLSproxy()->ALM_PROXYrateLimits()->PAU_RATELIMITSintegrations()-> only 1 integration CCTP_FACET
PAU_RATELIMITS (v1.14.0)
DEFAULT_ADMIN_ROLE->SPARK_EXECUTOR(count 1)CONTROLLER->PAU_CONTROLLER(count 1)
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