1. Purpose
Define safe, governance‑controlled contract upgrade mechanisms for TLaaS (LEX) components (e.g., LicenseMarketplace
, PaymentOrchestrator
, RoyaltyRoutingEngine
). Upgrades must preserve storage layout, enforce role‑gated authorization, emit auditable events, and integrate with TLAAS (DLA) and DAL controls. Primary patterns: EIP‑1967 Transparent, UUPS (ERC‑1822), and Beacon. Non‑proxy patterns (EIP‑1167 minimal clones) are covered for factory deployments.
2. Design Principles
- Deterministic Storage: Strict layout discipline;
- Least Privilege: Only a governance executor (timelock + Safe) may authorize upgrades.
- Explicit Initializers: No constructors in logic contracts; use
- Upgrade Simulation: Pre‑deploy storage diff checks; rollback testing; dry‑run on fork.
- Observability: Emit
3. Pattern Selection
- UUPS (ERC‑1822): Preferred for app contracts; lower gas; upgrade code in implementation with
- Transparent (EIP‑1967): Useful where an
- Beacon: Many proxies share a single implementation pointer; suitable for mass upgrades of homogeneous instances.
- Clones (EIP‑1167): For factories minting many lightweight instances; pair with Beacon for bulk logic updates.
4. Storage Layout Discipline
- Append‑only storage; never change order or type of existing fields.
- Reserve
- Track layout with
Example Layout Manifest (snippet)
{
"contracts/PaymentOrchestrator.sol:PaymentOrchestrator": {
"storage": [
{"label":"WETH","type":"t_address","slot":"0","offset":0},
{"label":"lexTreasury","type":"t_address","slot":"1","offset":0}
]
}
}
5. UUPS Implementation (Recommended)
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
contract PaymentOrchestratorV1 is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE");
address public lexTreasury;
address public router;
uint256[48] private __gap; // storage gap
function initialize(address _treasury, address _router) public initializer {
__AccessControl_init();
__UUPSUpgradeable_init();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(GOVERNANCE_ROLE, msg.sender);
lexTreasury = _treasury;
router = _router;
}
function _authorizeUpgrade(address newImplementation) internal override onlyRole(GOVERNANCE_ROLE) {}
}
Proxy Deployment (UUPS)
// deploy_uups.ts
import { ethers, upgrades } from "hardhat";
async function main() {
const Impl = await ethers.getContractFactory("PaymentOrchestratorV1");
const proxy = await upgrades.deployProxy(Impl, [process.env.TREASURY!, process.env.ROUTER!], { kind: 'uups' });
await proxy.waitForDeployment();
console.log('Proxy:', await proxy.getAddress());
}
main();
Upgrade Execution
// upgrade_uups.ts
import { ethers, upgrades } from "hardhat";
async function main() {
const proxyAddr = process.env.PROXY!;
const NewImpl = await ethers.getContractFactory("PaymentOrchestratorV2");
await upgrades.upgradeProxy(proxyAddr, NewImpl);
}
main();
6. Transparent Proxy (EIP‑1967) Variant
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
contract Deploy1967 {
ProxyAdmin public admin;
TransparentUpgradeableProxy public proxy;
constructor(address impl, bytes memory initData) {
admin = new ProxyAdmin();
proxy = new TransparentUpgradeableProxy(impl, address(admin), initData);
}
}
Admin Upgrade Call
// admin_upgrade.ts
import { ethers } from 'hardhat';
import { ProxyAdmin } from '../typechain';
async function main(){
const admin = await ethers.getContractAt('ProxyAdmin', process.env.ADMIN!);
await admin.upgradeAndCall(process.env.PROXY!, process.env.NEW_IMPL!, '0x');
}
main();
7. Beacon Pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
contract BeaconDeployer {
UpgradeableBeacon public beacon;
constructor(address impl, address owner){
beacon = new UpgradeableBeacon(impl);
beacon.transferOwnership(owner);
}
function newInstance(bytes memory initData) external returns (address){
BeaconProxy p = new BeaconProxy(address(beacon), initData);
return address(p);
}
}
8. Initialization & Versioning
- Use
- Guard against re‑init with
- Store
Versioned Initializer
function initializeV2(/* new params */) public reinitializer(2) {
// add new fields safely
}
9. Authorization & Governance Integration
- UUPS
- Transparent/Beacon upgrades restricted to
- Record DAL proposal id in an on‑chain registry before allowing
Timelock Gate (concept)
interface ITimelock { function isOperationReady(bytes32 id) external view returns (bool); }
contract UpgradeGate {
ITimelock public timelock;
mapping(address => bytes32) public requiredProposal; // proxy => proposalId
function canUpgrade(address proxy) external view returns (bool) {
return timelock.isOperationReady(requiredProposal[proxy]);
}
}
10. Safety Checks & Tooling
- Storage Layout Diff:
- Rollback Test: upgrade → call → upgrade back; verify state and behavior unchanged.
- Access Controls: fuzz tests on
- Pause Before Upgrade: optional emergency pause via
Hardhat Test (excerpt)
it('prevents non-governance upgrade', async () => {
const v2 = await ethers.getContractFactory('PaymentOrchestratorV2');
await expect(upgrades.upgradeProxy(proxy.address, v2.connect(attacker))).to.be.reverted;
});
11. Diamond Pattern (Optional)
If modular facets are needed (e.g., Marketplace facets), use EIP‑2535 with caution: higher complexity, selector collisions, and tooling overhead. Prefer UUPS for simpler governance.
12. Clones & Factories
For per‑issuer SublicenseModule
instances:
- Deploy EIP‑1167 minimal proxies from a factory.
- Optionally point clones to a Beacon for coordinated upgrades.
Factory (EIP‑1167)
library Clones { function clone(address impl) internal returns (address instance); }
contract SublicenseFactory {
address public impl;
event Instance(address indexed owner, address proxy);
constructor(address _impl){ impl = _impl; }
function create(address owner, bytes memory init) external returns (address p){
p = Clones.clone(impl);
(bool ok,) = p.call(init); require(ok, 'init failed');
emit Instance(owner, p);
}
}
13. Operational Runbook
- Pre‑Upgrade:
- Freeze window announced; submit DAL proposal; generate storage manifest; run fuzz/invariants.
- Execute:
- Timelock executes upgrade;
- Post‑Upgrade:
- Monitor key metrics; run sanity txs; publish release notes (SBOM, audit delta, storage diff).
14. Acceptance Criteria
- Storage layout unchanged for existing slots; gaps preserved.
- Only governance executor can upgrade; DAL gate enforced.
- Rollback test passes; invariants hold; no critical static‑analysis findings.
- All proxies emit expected events with implementation address fingerprints.
Next Article: On‑Chain vs IPFS/Arweave Storage — Data Storage Strategies