Introduction
Last month, we at Unvariant audited Diamond PAU, Sky’s new architecture for allocator actions. The upgrade stood out to us not just as a Sky-specific improvement, but as a set of design patterns more DeFi teams should study closely.
The reason is simple: in liquidity management protocols, failures often start inside something the system already trusts. A bridge signs the wrong message. A multisig is compromised. An integration starts behaving in a way no one planned for. The risk is not only that a strategy fails or a contract has a bug, but that a trusted path suddenly gives an external component more reach than expected.
This risk is especially relevant for Sky, whose ecosystem is split into autonomous units called Stars. Spark and Grove, Sky’s two Stars, each operate their own liquidity management system for moving and allocating capital across the ecosystem: the Spark Liquidity Layer, or SLL, and the Grove Liquidity Layer, or GLL. Both share the same monolithic architecture, and both are hitting the same scaling wall.
Sky’s response was Diamond PAU, or Parallelized Allocation Unit: a ground-up redesign that replaces per-product monoliths with a single, modular, diamond-based operating system for allocator actions.
The old architecture: Spark Liquidity Layer
To understand why Diamond PAU exists, it's important to understand what it replaced.
The SLL was built around a monolithic controller - a single MainnetController contract that contained every protocol integration inline.
Depositing into Aave, swapping through Curve, bridging USDC via CCTP, managing Ethena positions, providing Uniswap V4 liquidity - all lived in one contract, behind one role (RELAYER), with all rate limit keys hardcoded.

The architecture was straightforward: the Relayer calls MainnetController, which checks roles and rate limits, then forwards the actual token operations through ALMProxy.
A separate ForeignController handled foreign chains with a subset of the same integrations.
While this design is fairly simple and reliable, it comes with a set of problems:
- No upgradeability.
If Spark wanted to make a change to any integration that would require them to edit, audit and redeploy theMainnetControlleron every change, as well as migrate all controller-internal storage state (roles, max slippage params, max exchange rate params, mint recipients, OTC configs, tick limits, etc.).
Not very handy for new integrations coming every now and then or a tweak of a hardcoded parameter. - Selector and storage collisions at scale.
With 30+ external functions in a single contract, every new integration risked colliding with existing function selectors or stepping on shared state. - Size concerns.
MainnetControllerwas already getting pretty large (bytecode size ~22.8kb), and with new integration and logic coming it there was a risk of bypassing the smart contract bytecode size limit (24kb) - a measure necessary to ensure the EVMs are not DOSed by calls to ambiguously large contracts.
Spark's team shared with us that they had previously hit bytecode size limit over five times and had to refactor each time we had to go to production.
Apart from these, the motivation behind the update was also based on the desire to join Spark, Grove and other (and future) Sky Stars under one architecture, allowing Sky the necessary governance above different systems.
At this stage, however, both Spark and Grove were forks of the initial Sky repo being developed, audited and updated separately.
The new design: Diamond PAU
Diamond PAU replaces the monolith with a modular architecture built on the diamond proxy pattern (EIP-2535).
However, that's not all (and not exactly interesting by itself).
What seems actually novel is the approach to the diamond pattern chosen by Sky - using it as a constrained operating system for allocator (new role that replaced the relayer one) actions, where each facet is a sandboxed integration module that can be independently deployed, wired, and removed.

The core system now consists of five contracts:
- Beacon - a Sky ecosystem-wide registry of integration configurations that stores the selector mappings.
- Controller - the new diamond proxy. Receives external calls from
Allocator, dispatches them to the facets viadelegateCall, navigated by the config from the Beacon. - ALMProxy still holds the funds and remains unchanged - it's main purpose is to execute the external calls on behalf of the Controller.
- RateLimits - still a standalone rate-limit engine with time-based refill.
- AccessControls - once part of the MainnetController logic, now an independent contract that handles role management.
A PAUFactory deploys this full stack in a single transaction, wires all the roles, and revokes its own admin access - leaving a clean and minimal trust setup.
Now let's talk about the specific design choices that might serve as an inspiration for other teams across the DeFi space.
Selector wiring: Remapping to avoid collisions
The classic diamond problem is selector collisions. If two facets both define a deposit(uint256) function, they share the same 4-byte selector, and the diamond can only route it to one. The standard approach is to rename functions to avoid collisions - which pollutes the external interface with ugly prefixed names like aaveDeposit(), curveDeposit(), etc.
Sky's approach is different - each integration is configured with an array of Wires - explicit mappings from an external-facing callSelector to an internal delegateSelector:

Beacon model
A naive diamond would store its facet routing directly and apply changes immediately. Diamond PAU separates configuration from execution through a two-contract model:
- Beacon: Stores the canonical integration definitions (facet address + wire mappings). Managed by Sky DAO's governance.
- Controller: The actual diamond proxy that handles calls.
Maintains its own local copy of the dispatch table.
This way, updating the Beacon does not automatically update any Controller. Instead, a Controller admin must explicitly call updateIntegrations() to pull specific integrations from the Beacon:
function updateIntegrations(bytes32[] calldata ids) external onlyAdmin nonReentrant {
Config[] memory configs = IBeacon(beacon).getConfigs(ids);
for (uint256 i = 0; i < ids.length; ++i) {
// Validate, then rebuild local dispatch table
if (!$.integrationIds.add(id)) {
_deleteConfigAndDispatches(id); // Remove old wiring
}
_setConfigAndDispatches(id, config); // Set new wiring
}
}
Which creates a pull-based upgrade model:

Which is cool for a few reasons:
- Adding a new integration to the Beacon doesn't affect any running Controller until its admin explicitly opts in.
This prevents a governance mistake from cascading across all deployed instances. - A Controller can sync only the integrations it needs.
A mainnet Controller might wire Aave, Curve, and PSM facets, while a foreign-chain Controller might only need CCTP and ERC4626. - If a newly synced integration has issues, the Controller admin can call
removeIntegrations()without touching the Beacon, instantly un-wiring the problematic facet. - Multiple Controllers share the same Beacon (and hence the same audited facet code) while each maintaining independent dispatch tables, rate limits, and proxy contracts. This is how Spark's SLL and Grove's GLL are unified - they use the same Beacon with different Controller instances.
Role management and trust assumptions
Lots of DeFi protocols draw a binary trust line: you're either an admin with full power or an untrusted user. The privileged roles (multisig owners, operators, relayers) are assumed to be honest, and if they're compromised, the protocol has no defense.
In the Diamond PAU, the allocator role is assumed to be compromisable.
The system is designed so that even a fully compromised allocator cannot cause major loss of protocol funds by performing unlimited malicious actions.
This protection is enforced through a time-based refill rate limit system.
Every material action an allocator can take - depositing into Aave, swapping through a PSM, bridging USDC - is gated by a rate limit that caps how much value can flow through in a given time window.
This creates a damage ceiling. If an allocator key is compromised, the attacker’s impact is capped by the configured rate limits.
Meanwhile, the governance can react - revoke the allocator role, adjust limits, or even remove the affected integration entirely.
Rate limits are not per-allocator but per-action-per-integration, with composite keys derived from the action type and relevant addresses:
// A deposit into Aave with a specific token gets its own rate limit
bytes32 key = makeAddressKey(LIMIT_AAVE_DEPOSIT, aToken);
// A CCTP bridge to a specific domain gets its own rate limit
bytes32 key = makeAddressUint32Key(LIMIT_CCTP_TRANSFER, usdc, destinationDomain);
This granularity means governance can set tight limits on risky integrations while allowing higher throughput on well-understood ones.
It also ensures that a compromised allocator cannot concentrate all available bandwidth on a single vulnerable integration path, since each path is governed by its own independent budget.
Trust model for external protocols
The allocator isn't the only entity Diamond PAU refuses to blindly trust. External protocol integrations are treated in a similar way.
For example, take their balance checks.
A common DeFi pattern is to trust the return value of an external call:
// Trusting pattern (what most protocols do):
uint256 shares = vault.deposit(amount, receiver); // trust the return
Diamond PAU systematically replaces this with balance-before/after measurements:
// Diamond PAU pattern
uint256 startingShares = IERC20(token).balanceOf(proxy);
IALMProxy(proxy).doCall(token, abi.encodeCall(IERC4626.deposit, (amount, proxy)));
shares = IERC20(token).balanceOf(proxy) - startingShares;
This pattern appears in virtually every facet — Aave, Curve, Pendle and so on.
The protocol measures the actual change in token balances rather than trusting what the external contract claims happened.
Why? Because external protocols are upgradeable proxies.
A compromised or buggy upgrade could return inflated values while transferring less (or nothing).
By measuring actual balances, Diamond PAU's rate limits accurately reflect real value flows regardless of what external contracts report.
Immutable addresses instead of dynamic lookups
Another trust issue involves address resolution.
Many DeFi integrations involve nested contracts - you call contract A, which internally references contract B. If you dynamically query A for B's address on every call, and A is upgradeable, a compromised A can redirect you to an attacker-controlled B.
Diamond PAU's general approach is to resolve addresses once at construction and store them as immutable:
contract PSMFacet is Facet {
address public immutable dai;
address public immutable daiUSDS;
address public immutable psm;
address public immutable usdc;
address public immutable usds;
constructor(address dai_, address daiUSDS_, address psm_, address usdc_, address usds_) {
dai = dai_;
daiUSDS = daiUSDS_;
// ...
}
}
This pattern is used across 10+ facets for all critical token and protocol addresses. Once the facet is deployed, it can never be tricked into interacting with a substitute contract.
Our finding that inspired a broader reassessment
During our audit of Diamond PAU, we identified a case where this principle wasn't fully applied. The WEETHFacet dynamically resolved the eETH token address from the external weETH contract on every call:
function deposit(uint256 amount, ...) external ... {
address eeth = IWEETHLike(weeth).eETH(); // Dynamic lookup on each call
address liquidityPool = IEETHLike(eeth).liquidityPool();
// ...
ApproveLib.approve(eeth, proxy, weeth, eethAmount); // Approves potentially arbitrary token
}
If weETH (an upgradeable proxy) were compromised, the attacker could make eETH() return an arbitrary address, causing the ALM proxy to approve any token to any spender - bypassing all rate limits and approving assets unrelated to the integration.
We recommended resolving eETH once as an immutable.
Spark team chose an alternative mitigation that we found equally elegant: they encoded the dynamically-fetched addresses into the rate limit key itself:
function getDepositRateLimitKey(address eeth, address liquidityPool)
public pure returns (bytes32)
{
return makeAddressAddressKey(LIMIT_DEPOSIT, eeth, liquidityPool);
}
Now if weETH returns a compromised eeth address, the rate limit key changes, and the _decreaseRateLimit() call reverts because no rate limit is configured for that key (maxAmount == 0).
The rate limit system itself becomes an address whitelist.
Conclusion
Diamond PAU demonstrates that the diamond proxy pattern can be more than an upgradeability mechanism - it can serve as an architecture for building constrained, modular, security-conscious DeFi systems.
In our dark times, when nothing feels secure enough anymore and a major hack happens every month, minimizing trust is the only reasonable approach to engineering.
Any DeFi protocol that manages funds across multiple external integrations can benefit from thinking this way.
Don't trust that operators won't be compromised - rate-limit them.
Don't trust that external protocols will behave - measure actual balances before and after transactions.
Want to secure your protocol?
Let's talk.