Upgrade Authorization

Access control mechanisms that restrict who can upgrade proxy implementation contracts and under what conditions.

Upgrade authorization controls who can modify the implementation contract that a proxy delegates to. Since proxy upgrades can completely change contract behavior, proper authorization is critical to prevent unauthorized takeovers and ensure legitimate upgrades follow proper governance processes.

Types of Upgrade Authorization

Single Owner Model

The simplest but most centralized approach where one address controls upgrades:

1contract ProxyAdmin is Ownable {
2 mapping(address => address) public implementations;
3
4 function upgrade(TransparentUpgradeableProxy proxy, address newImplementation)
5 external onlyOwner {
6 proxy.upgradeTo(newImplementation);
7 implementations[address(proxy)] = newImplementation;
8 emit Upgraded(address(proxy), newImplementation);
9 }
10}

Pros: Simple, fast upgrades Cons: Single point of failure, centralization risk

Multisig Authorization

Requires multiple signatures to authorize upgrades:

1contract MultisigProxyAdmin {
2 address[] public owners;
3 uint256 public required;
4 mapping(uint256 => Transaction) public transactions;
5 mapping(uint256 => mapping(address => bool)) public confirmations;
6
7 struct Transaction {
8 address proxy;
9 address implementation;
10 bool executed;
11 uint256 confirmationCount;
12 }
13
14 function submitUpgrade(address proxy, address implementation)
15 external onlyOwner returns (uint256) {
16 uint256 txId = transactions.length;
17 transactions.push(Transaction({
18 proxy: proxy,
19 implementation: implementation,
20 executed: false,
21 confirmationCount: 0
22 }));
23
24 return txId;
25 }
26
27 function confirmUpgrade(uint256 txId) external onlyOwner {
28 require(!confirmations[txId][msg.sender], "Already confirmed");
29
30 confirmations[txId][msg.sender] = true;
31 transactions[txId].confirmationCount += 1;
32
33 if (transactions[txId].confirmationCount >= required) {
34 executeUpgrade(txId);
35 }
36 }
37}

Timelock Authorization

Introduces mandatory delays between upgrade proposal and execution:

1contract TimelockProxyAdmin {
2 uint256 public constant UPGRADE_DELAY = 2 days;
3 mapping(bytes32 => uint256) public queuedUpgrades;
4
5 event UpgradeScheduled(
6 address indexed proxy,
7 address indexed implementation,
8 uint256 executeAfter
9 );
10
11 function scheduleUpgrade(address proxy, address implementation)
12 external onlyOwner {
13 bytes32 upgradeHash = keccak256(abi.encode(proxy, implementation));
14 uint256 executeAfter = block.timestamp + UPGRADE_DELAY;
15
16 queuedUpgrades[upgradeHash] = executeAfter;
17 emit UpgradeScheduled(proxy, implementation, executeAfter);
18 }
19
20 function executeUpgrade(address proxy, address implementation)
21 external {
22 bytes32 upgradeHash = keccak256(abi.encode(proxy, implementation));
23 uint256 executeAfter = queuedUpgrades[upgradeHash];
24
25 require(executeAfter != 0, "Upgrade not scheduled");
26 require(block.timestamp >= executeAfter, "Timelock not expired");
27
28 delete queuedUpgrades[upgradeHash];
29 TransparentUpgradeableProxy(payable(proxy)).upgradeTo(implementation);
30 }
31}

DAO Governance Authorization

Community-controlled upgrades through voting mechanisms:

1contract DAOProxyAdmin {
2 IGovernor public governor;
3
4 function upgrade(address proxy, address implementation)
5 external {
6 require(
7 governor.hasRole(EXECUTOR_ROLE, msg.sender),
8 "Only governor can upgrade"
9 );
10
11 TransparentUpgradeableProxy(payable(proxy)).upgradeTo(implementation);
12 }
13
14 // Called by governance after successful proposal
15 function executeGovernanceUpgrade(
16 address proxy,
17 address implementation,
18 bytes32 proposalId
19 ) external {
20 require(
21 governor.getProposalState(proposalId) == IGovernor.ProposalState.Executed,
22 "Proposal not executed"
23 );
24
25 TransparentUpgradeableProxy(payable(proxy)).upgradeTo(implementation);
26 }
27}

Security Vulnerabilities in Authorization

Missing Access Control

The most critical vulnerability - anyone can upgrade the contract:

1// CRITICAL VULNERABILITY
2contract VulnerableProxy {
3 address public implementation;
4
5 function upgradeTo(address newImplementation) external {
6 implementation = newImplementation; // No access control!
7 }
8}

Impact: Complete contract takeover Mitigation: Always implement proper access control

Weak Access Control

Using simple ownership that can be easily compromised:

1// VULNERABLE: Single point of failure
2contract WeakProxy is Ownable {
3 function upgradeTo(address newImplementation) external onlyOwner {
4 // Owner key compromise = total loss
5 }
6}

Impact: Single private key compromise leads to total loss Mitigation: Use multisig or DAO governance

Bypassing Authorization

Implementation contracts that allow self-upgrade without going through proxy admin:

1// VULNERABLE: Implementation can upgrade itself
2contract BadImplementation is UUPSUpgradeable {
3 function _authorizeUpgrade(address) internal override {
4 // No authorization check - anyone can upgrade!
5 }
6}

Race Conditions in Authorization

Time gaps between authorization checks and execution:

1// VULNERABLE: Race condition
2contract RacyAdmin {
3 mapping(address => bool) public authorized;
4
5 function authorizeUpgrade(address user) external onlyOwner {
6 authorized[user] = true;
7 }
8
9 function executeUpgrade(address proxy, address impl) external {
10 require(authorized[msg.sender], "Not authorized");
11 // Gap here - authorization could be revoked
12 TransparentUpgradeableProxy(payable(proxy)).upgradeTo(impl);
13 }
14}

Best Practices for Upgrade Authorization

1. Multi-Layer Authorization

Combine multiple authorization mechanisms:

1contract SecureProxyAdmin {
2 address public owner;
3 address[] public guardians; // Emergency pause powers
4 uint256 public timelockDelay = 2 days;
5 bool public paused;
6
7 modifier onlyOwner() {
8 require(msg.sender == owner, "Not owner");
9 _;
10 }
11
12 modifier notPaused() {
13 require(!paused, "Contract paused");
14 _;
15 }
16
17 modifier timelockExpired(bytes32 upgradeHash) {
18 require(
19 block.timestamp >= queuedUpgrades[upgradeHash] + timelockDelay,
20 "Timelock not expired"
21 );
22 _;
23 }
24}

2. Immutable Authorization Logic

Deploy authorization contracts that cannot be modified:

1contract ImmutableProxyAdmin {
2 // These cannot be changed after deployment
3 address public immutable multisig;
4 uint256 public immutable timelockDelay;
5
6 constructor(address _multisig, uint256 _delay) {
7 multisig = _multisig;
8 timelockDelay = _delay;
9 }
10}

3. Emergency Pause Mechanisms

Allow trusted parties to pause upgrades during emergencies:

1contract PausableProxyAdmin {
2 bool public upgradesPaused;
3 address[] public emergencyPausers;
4
5 function emergencyPause() external {
6 require(isEmergencyPauser(msg.sender), "Not pauser");
7 upgradesPaused = true;
8 emit EmergencyPause(msg.sender);
9 }
10
11 function executeUpgrade(address proxy, address impl)
12 external
13 notPaused {
14 // Upgrade logic
15 }
16}

4. Upgrade Validation

Implement checks to validate new implementations:

1contract ValidatingProxyAdmin {
2 function upgrade(address proxy, address newImpl) external onlyAuthorized {
3 // Validate new implementation
4 require(newImpl.code.length > 0, "Not a contract");
5 require(
6 IERC165(newImpl).supportsInterface(REQUIRED_INTERFACE),
7 "Missing required interface"
8 );
9
10 // Check storage layout compatibility
11 require(
12 IStorageLayout(newImpl).isCompatibleWith(currentImpl),
13 "Incompatible storage layout"
14 );
15
16 TransparentUpgradeableProxy(payable(proxy)).upgradeTo(newImpl);
17 }
18}

Authorization Patterns by Protocol

OpenZeppelin Pattern

  • ProxyAdmin contract controls upgrades
  • Separate admin rights from user interactions
  • Supports timelock and multisig

UUPS Pattern

  • Implementation controls its own upgrades
  • Lower gas costs
  • Higher risk if authorization logic has bugs

Diamond Pattern

  • Facet cuts controlled by diamond owner
  • Granular authorization per facet
  • Complex but flexible authorization

Testing Authorization

1describe("Upgrade Authorization", function() {
2 it("should prevent unauthorized upgrades", async function() {
3 await expect(
4 proxy.connect(attacker).upgradeTo(maliciousImpl.address)
5 ).to.be.revertedWith("Unauthorized");
6 });
7
8 it("should require multiple signatures", async function() {
9 // Single signature should fail
10 await expect(
11 multisigAdmin.connect(owner1).upgrade(proxy.address, newImpl.address)
12 ).to.be.revertedWith("Insufficient signatures");
13
14 // Multiple signatures should succeed
15 await multisigAdmin.connect(owner1).confirmUpgrade(0);
16 await multisigAdmin.connect(owner2).confirmUpgrade(0);
17 // Should execute automatically
18 });
19
20 it("should enforce timelock delay", async function() {
21 await admin.scheduleUpgrade(proxy.address, newImpl.address);
22
23 await expect(
24 admin.executeUpgrade(proxy.address, newImpl.address)
25 ).to.be.revertedWith("Timelock not expired");
26
27 // Fast forward time
28 await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]);
29 await admin.executeUpgrade(proxy.address, newImpl.address);
30 });
31});

Proper upgrade authorization is the cornerstone of proxy security. Without robust authorization mechanisms, proxy patterns become a liability rather than a feature, opening up devastating attack vectors for malicious actors.

Need expert guidance on Upgrade Authorization?

Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.

Get a Quote

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx