# Ekx Openzeppelin Upgrades

> Upgradeable contract patterns with OpenZeppelin — UUPS proxies, initializers, storage layout safety, access control, and merkle-tree allowlists. Use when writing an upgradeable contract, adding a storage variable to a deployed one, running the upgrade-safe validations, or building a merkle allowlist.

- Skill: `ekinoxis-evm/ekx-openzeppelin-upgrades` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ekinoxis-evm/ekx-openzeppelin-upgrades`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ekinoxis-evm/ekx-openzeppelin-upgrades/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Ekinoxis-evm (https://skillmd.com/u/ekinoxis-evm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ekinoxis-evm/ekx-openzeppelin-upgrades

---


# OpenZeppelin (upgrades + utilities)

`@openzeppelin/upgrades-core`, `upgrade-safe-transpiler`, `merkle-tree` and
`docs-utils` are in all three of our Foundry contract suites.

Docs: https://docs.openzeppelin.com/contracts

---

## Upgradeable contracts

We use **UUPS**, not Transparent — the upgrade logic lives in the implementation, so
the proxy is cheaper to deploy, and we deploy one proxy per property.

```solidity
contract AuctionManager is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    function initialize(address usdc_, address owner_) public initializer {
        __Ownable_init(owner_);
        __UUPSUpgradeable_init();
        usdc = IERC20(usdc_);
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() { _disableInitializers(); }
}
```

Four things that are not optional:

1. **`_disableInitializers()` in the constructor.** Without it, anyone can initialize the *implementation* contract directly and, with UUPS, call `upgradeTo` on it — a known way to brick the proxy.
2. **`initialize`, not a constructor.** Constructor code never runs in the proxy's context, so any state set there is invisible.
3. **`_authorizeUpgrade` must be access-controlled.** An empty override with no modifier makes the contract upgradeable by anyone.
4. **Use the `Upgradeable` variants** of every OZ base (`OwnableUpgradeable`, not `Ownable`). Mixing them corrupts storage.

---

## Storage layout — the rule you cannot break

Upgrades keep the proxy's storage. You may **append** variables. You may never
insert, reorder, remove, or change the type of an existing one.

```solidity
contract V1 { uint256 a; address b; }
contract V2 { uint256 a; address b; uint256 c; }   // ✅ appended
contract V2 { uint256 a; uint256 c; address b; }   // ❌ b and c now read each other's bytes
```

Leave a gap in base contracts meant to be extended:

```solidity
uint256[50] private __gap;
```

Validate before every upgrade — this is what `upgrades-core` is for:

```bash
npx @openzeppelin/upgrades-core validate out/build-info
```

Run it in CI. A storage-layout mistake is not recoverable after the fact; the funds
are already misread.

---

## Merkle allowlists

`@openzeppelin/merkle-tree` for whitelists (course access, qualified bidders) — one
32-byte root onchain instead of N addresses.

```ts
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
const tree = StandardMerkleTree.of(entries.map(a => [a]), ["address"]);
// publish tree.root onchain, serve proofs from the API
```

```solidity
if (!MerkleProof.verify(proof, root, keccak256(bytes.concat(keccak256(abi.encode(msg.sender)))))) revert NotAllowed();
```

The double-hash is `StandardMerkleTree`'s leaf encoding. Single-hashing it is the
usual reason a proof verifies in the JS test and fails onchain.

---

## Other OZ we rely on

- **`SafeERC20`** for every USDC movement — see [`../ekx-circle-usdc/SKILL.md`](../ekx-circle-usdc/SKILL.md).
- **`ReentrancyGuard`** on anything that transfers out then updates state.
- **`AccessControl`** over `Ownable` once there is more than one privileged role.
- **`Pausable`** as a circuit breaker on contracts holding user funds.

---

## Gotchas

1. **Missing `_disableInitializers()`** — implementation hijack.
2. **Storage layout change** — unrecoverable corruption.
3. **Non-`Upgradeable` base contracts** mixed in.
4. **Unprotected `_authorizeUpgrade`.**
5. **Single-hashed merkle leaves.**
6. **`initializer` vs `reinitializer(2)`** — a V2 adding new state needs `reinitializer`, and it must be callable exactly once.
7. **OZ v5 changed `Ownable`'s constructor** to require an owner argument. v4 code does not compile against v5.

