Keyrxng

UUPS Upgradeability for Core Contracts

Ubiquity Dollar · 2023 · Migrated seven core smart contracts to OpenZeppelin UUPS pattern after diamond proxy incompatibility. Included comprehensive testing, admin authorization, and deployment script refactoring. Survived external security audit.

So what?
7 core (ERC20/ERC721/ERC1155) contracts migrated · 0 security issues audit findings · 200 WXDAI reward
Role
Solidity Engineer (Second Major Contribution)
Year
2023
Stack
Solidity 0.8.19, OpenZeppelin Contracts Upgradeable 4.9, ERC1967Proxy, UUPS, Foundry, GitHub Actions
Read narrative
7 core (ERC20/ERC721/ERC1155) contracts migrated0 security issues audit findings200 WXDAI reward

Problem

The Ubiquity Dollar protocol faced architectural limitations where seven core contracts (UbiquityDollarToken, UbiquityGovernanceToken, UbiquityCreditToken, CreditNft, StakingShare, ERC20Ubiquity, ERC1155Ubiquity) could not utilize the existing diamond proxy pattern due to function signature clashing. This prevented protocol evolution, bug fixes, and feature additions for critical token contracts while preserving state in a multi-million dollar TVL protocol.

Approach

System diagram

flowchart LR
  User[User] --> D[Diamond]
  D --> P[UUPS Proxy ERC1967]
  P --> V1[Implementation Contract V1]
  V1 --> AC[Access Control Check]
  AC --> EX[Execute Function]
  P -.->|upgrade| V2[Implementation Contract V2]
  V2 --> SP[State Preservation]

Outcome

Constraints

Design choices

Proof

Code excerpt — UUPS pattern for a core token

// packages/contracts/src/dollar/core/UbiquityCreditToken.sol
contract UbiquityCreditToken is ERC20Ubiquity {
  constructor() {
    _disableInitializers();
  }

  function initialize(address manager) public initializer {
    __ERC20Ubiquity_init(manager, "Ubiquity Credit", "uCR");
  }

  function _authorizeUpgrade(address newImplementation)
    internal
    override
    onlyAdmin
  {}
}

Code excerpt — ERC1967Proxy deployment with init payload

// packages/contracts/scripts/deploy/dollar/solidityScripting/02_UbiquityDollarToken.s.sol
bytes memory managerPayload = abi.encodeWithSignature(
  "initialize(address)",
  address(diamond)
);

proxyDollarToken = new ERC1967Proxy(
  address(new UbiquityDollarToken()),
  managerPayload
);
dollarToken = UbiquityDollarToken(address(proxyDollarToken));
IManager.setDollarTokenAddress(address(dollarToken));

Test evidence — comprehensive upgrade, authorization, and initialization testing

function testUUPS_ShouldUpgradeAndCall() external {
  CreditNftUpgraded creditNftUpgraded = new CreditNftUpgraded();
  
  vm.startPrank(admin);
  bytes memory hasUpgradedCall = abi.encodeWithSignature("hasUpgraded()");
  
  // Verify not upgraded initially
  (bool success, ) = address(creditNft).call(hasUpgradedCall);
  assertEq(success, false, "should not have upgraded yet");
  
  creditNft.upgradeTo(address(creditNftUpgraded));
  
  // Verify successful upgrade
  (success, ) = address(creditNft).call(hasUpgradedCall);
  assertEq(success, true, "should have upgraded");
  
  // Ensure initialization protection
  vm.expectRevert();
  creditNft.initialize(address(diamond));
  
  vm.stopPrank();
}

function testUUPS_AdminAuth() external {
  CreditNftUpgraded creditNftUpgraded = new CreditNftUpgraded();
  
  // Non-admin should fail
  vm.expectRevert();
  creditNft.upgradeTo(address(creditNftUpgraded));
  
  // Admin should succeed
  vm.prank(admin);
  creditNft.upgradeTo(address(creditNftUpgraded));
  
  bytes memory hasUpgradedCall = abi.encodeWithSignature("hasUpgraded()");
  (bool success, bytes memory data) = address(creditNft).call(hasUpgradedCall);
  bool hasUpgraded = abi.decode(data, (bool));
  
  assertEq(hasUpgraded, true, "should have upgraded");
}

function testUUPS_InitializedVersion() external {
  // Test version tracking across upgrades
  uint expectedVersion = 1;
  uint baseExpectedVersion = 255;
  
  CreditNftUpgraded creditNftUpgraded = new CreditNftUpgraded();
  
  vm.startPrank(admin);
  creditNft.upgradeTo(address(creditNftUpgraded));
  
  bytes memory getVersionCall = abi.encodeWithSignature("getVersion()");
  (bool success, bytes memory data) = address(creditNft).call(getVersionCall);
  uint8 version = abi.decode(data, (uint8));
  
  assertEq(version, expectedVersion, "should maintain version consistency");
  vm.stopPrank();
}

PR evidence — successful merge with maintainer collaboration

"@Keyrxng pls merge https://github.com/Keyrxng/ubiquity-dollar/pull/5" - rndquu

"The original spec is implemented, core contracts are upgradeable. 
This PR still needs some refactoring (small changes in naming) but I will 
update it in a separate PR since those changes are not really connected 
with the UUPS" - rndquu

rndquu merged commit 919c455 into ubiquity:development (Sep 21, 2023)
24 commits across 40 files
Earned 200 WXDAI bounty

External audit evidence — Sherlock.xyz security review

External security audit completed via Sherlock.xyz
Zero findings related to UUPS upgradeability implementation
Protocol TVL: Multi-million dollars
Audit scope: All core contracts including upgraded UUPS implementations

References