skills/bap578-upgrade/SKILL.md
Use this skill when planning, implementing, testing, or executing safe BAP-578 contract upgrades with the UUPS proxy pattern, including storage layout discipline, V2 implementations, deployment, and rollback planning.
npx skillsauth add chatandbuild/skills-repo BAP-578 Contract UpgradeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill to plan, implement, test, and execute safe upgrades to deployed BAP-578 contracts using the UUPS (Universal Upgradeable Proxy Standard) pattern. This skill covers storage layout discipline, writing V2+ implementations, upgrade testing strategies, deployment procedures, and rollback planning.
The upgrade mechanism is the evolution layer for the contract. It allows the contract logic to change while keeping the same proxy address, the same token IDs, the same balances, and the same metadata. Users interact with the same address before and after an upgrade — their agents, funds, and identities are preserved.
Proxy state is preserved across upgrades because storage lives in the proxy contract, not the implementation. All existing data — agent metadata, balances, ownership, free mint records, treasury address — remains intact. The new implementation simply provides updated logic for reading and writing that same storage.
Upgrades can add new features, fix bugs, optimize gas usage, and extend the contract's capabilities. Specific examples:
Upgrades are restricted to the contract owner via _authorizeUpgrade. Trust is maintained by:
User → Proxy Contract (fixed address) → Implementation Contract (replaceable)
↓ ↓
Storage lives here Logic lives here
(never changes address) (can be swapped)
The proxy delegates all calls to the current implementation using delegatecall. This means the implementation's code runs in the context of the proxy's storage. When you upgrade, you deploy a new implementation and tell the proxy to point to it.
_authorizeUpgrade: function that gates who can trigger upgradesUUPS puts the upgrade logic in the implementation, not the proxy. Benefits:
The single most critical rule for safe upgrades:
NEVER reorder, rename, or remove existing state variables. Only append new variables at the end.
// V1 storage
contract NonFungibleAgentsV1 {
uint256 private _totalSupply;
mapping(uint256 => AgentState) private _agents;
mapping(uint256 => AgentMetadata) private _metadata;
address private _treasury;
bool private _paused;
}
// V2 storage — SAFE: only appended new variables
contract NonFungibleAgentsV2 is NonFungibleAgentsV1 {
// New variables appended after all V1 variables
uint256 private _royaltyBps;
mapping(uint256 => uint256) private _agentReputation;
}
// V2 storage — DANGEROUS: reordered variables
contract NonFungibleAgentsV2 {
bool private _paused; // MOVED — was 5th, now 1st
uint256 private _totalSupply; // SHIFTED — storage slot changed
// This will corrupt ALL existing data
}
Reserve storage slots for future use in the base contract:
contract NonFungibleAgentsV1 {
uint256 private _totalSupply;
mapping(uint256 => AgentState) private _agents;
// ... other variables
// Reserve 50 slots for future upgrades
uint256[50] private __gap;
}
When adding new variables in V2, reduce the gap:
contract NonFungibleAgentsV2 is NonFungibleAgentsV1 {
uint256 private _royaltyBps;
// Gap reduced by 1 (from 50 to 49)
uint256[49] private __gap;
}
1. Inherit from V1:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./NonFungibleAgentsV1.sol";
contract NonFungibleAgentsV2 is NonFungibleAgentsV1 {
// New state variables (appended only)
uint256 private _royaltyBps;
// New events
event RoyaltyUpdated(uint256 newBps);
// Reinitializer for V2-specific setup
function initializeV2(uint256 royaltyBps) external reinitializer(2) {
_royaltyBps = royaltyBps;
}
// New functions
function setRoyalty(uint256 bps) external onlyOwner {
require(bps <= 1000, "Max 10%");
_royaltyBps = bps;
emit RoyaltyUpdated(bps);
}
function getRoyalty() external view returns (uint256) {
return _royaltyBps;
}
// Override existing functions if needed
// Always call super for inherited behavior
}
2. Use reinitializer (not initializer):
The initializer modifier can only be called once (for V1). For subsequent versions, use reinitializer(version):
function initializeV2(uint256 param) external reinitializer(2) {
// V2-specific initialization
}
function initializeV3(uint256 param) external reinitializer(3) {
// V3-specific initialization
}
3. Disable initializers in constructor:
constructor() {
_disableInitializers();
}
_authorizeUpgrade is still owner-only.const { ethers, upgrades } = require("hardhat");
const { expect } = require("chai");
describe("Upgrade V1 → V2", function () {
let nfa, nfaV2, owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
// Deploy V1
const V1 = await ethers.getContractFactory("NonFungibleAgentsV1");
nfa = await upgrades.deployProxy(V1, [
"Non-Fungible Agents",
"NFA",
owner.address, // treasury
], { kind: "uups" });
// Create an agent in V1
await nfa.createAgent(
user.address,
ethers.ZeroAddress,
"ipfs://metadata",
{
persona: '{"name":"Test"}',
experience: "Test agent",
voiceHash: "",
animationURI: "",
vaultURI: "",
vaultHash: ethers.ZeroHash,
}
);
// Upgrade to V2
const V2 = await ethers.getContractFactory("NonFungibleAgentsV2");
nfaV2 = await upgrades.upgradeProxy(nfa.target, V2, {
call: { fn: "initializeV2", args: [250] }, // 2.5% royalty
});
});
it("preserves V1 state after upgrade", async function () {
// Token still exists
expect(await nfaV2.ownerOf(1)).to.equal(user.address);
// Metadata intact
const meta = await nfaV2.getAgentMetadata(1);
expect(meta.experience).to.equal("Test agent");
// Total supply unchanged
expect(await nfaV2.getTotalSupply()).to.equal(1);
});
it("new V2 functions work", async function () {
expect(await nfaV2.getRoyalty()).to.equal(250);
await nfaV2.setRoyalty(500);
expect(await nfaV2.getRoyalty()).to.equal(500);
});
it("V1 functions still work", async function () {
// Can still mint
await nfaV2.createAgent(
user.address,
ethers.ZeroAddress,
"ipfs://metadata2",
{
persona: '{"name":"Test2"}',
experience: "Another agent",
voiceHash: "",
animationURI: "",
vaultURI: "",
vaultHash: ethers.ZeroHash,
}
);
expect(await nfaV2.getTotalSupply()).to.equal(2);
});
it("only owner can upgrade", async function () {
const V2Again = await ethers.getContractFactory("NonFungibleAgentsV2", user);
await expect(
upgrades.upgradeProxy(nfaV2.target, V2Again)
).to.be.reverted;
});
});
Use Hardhat's upgrade plugin to validate storage compatibility:
npx hardhat run scripts/validate-upgrade.js
const { upgrades } = require("hardhat");
async function main() {
const V1 = await ethers.getContractFactory("NonFungibleAgentsV1");
const V2 = await ethers.getContractFactory("NonFungibleAgentsV2");
// This will throw if storage layout is incompatible
await upgrades.validateUpgrade(V1, V2, { kind: "uups" });
console.log("Storage layout is compatible ✓");
}
_authorizeUpgrade is still owner-only in V2# 1. Deploy new implementation
npx hardhat run scripts/deploy-v2.js --network bsc
# 2. Verify on explorer
npx hardhat verify --network bsc NEW_IMPL_ADDRESS
# 3. Execute upgrade (via script or multisig)
npx hardhat run scripts/upgrade-to-v2.js --network bsc
owner() returns correct addressSimply redeploy V1 as a new implementation and upgrade back.
This is a critical scenario. Mitigations:
Upgraded events.contract NonFungibleAgentsV2 is NonFungibleAgentsV1, IERC2981 {
uint256 private _royaltyBps;
address private _royaltyReceiver;
function initializeV2(address receiver, uint256 bps) external reinitializer(2) {
_royaltyReceiver = receiver;
_royaltyBps = bps;
}
function royaltyInfo(uint256, uint256 salePrice) external view returns (address, uint256) {
uint256 royaltyAmount = (salePrice * _royaltyBps) / 10000;
return (_royaltyReceiver, royaltyAmount);
}
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
}
}
Storage impact: 2 new variables appended. Safe.
contract NonFungibleAgentsV2 is NonFungibleAgentsV1 {
uint256 private _tier1Fee;
uint256 private _tier2Fee;
uint256 private _tier1Threshold;
function initializeV2(uint256 t1Fee, uint256 t2Fee, uint256 threshold) external reinitializer(2) {
_tier1Fee = t1Fee;
_tier2Fee = t2Fee;
_tier1Threshold = threshold;
}
function getMintFee() public view returns (uint256) {
if (getTotalSupply() < _tier1Threshold) return _tier1Fee;
return _tier2Fee;
}
}
Storage impact: 3 new variables appended. Safe.
Adding events requires no storage changes — events are log entries, not state. Simply add the event declaration and emit it in the appropriate function:
event AgentBurned(uint256 indexed tokenId, address indexed owner);
function burnAgent(uint256 tokenId) external {
require(ownerOf(tokenId) == msg.sender, "Not token owner");
require(getAgentState(tokenId).balance == 0, "Withdraw funds first");
_burn(tokenId);
emit AgentBurned(tokenId, msg.sender);
}
Storage impact: None. Safe.
If a bug is found in an existing function, create V2 that overrides the function with the fix:
contract NonFungibleAgentsV2 is NonFungibleAgentsV1 {
function initializeV2() external reinitializer(2) {}
// Override the buggy function with the fix
function withdrawFromAgent(uint256 tokenId, uint256 amount) external override nonReentrant {
require(ownerOf(tokenId) == msg.sender, "Not token owner");
require(amount > 0, "Amount must be > 0"); // NEW: added zero check
AgentState storage agent = _agents[tokenId];
require(amount <= agent.balance, "Insufficient balance");
agent.balance -= amount;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
emit AgentWithdraw(tokenId, msg.sender, amount);
}
}
For production deployments, add a timelock to prevent instant upgrades:
This can be implemented via OpenZeppelin's TimelockController as the contract owner.
Use a multisig wallet (e.g., Safe) as the contract owner:
Before any mainnet upgrade:
The initializer was already called. Use reinitializer(version) for V2+ initialization.
The new implementation doesn't inherit UUPSUpgradeable or doesn't implement _authorizeUpgrade. Verify the V2 contract inherits correctly.
The OpenZeppelin upgrade plugin detected a storage collision. Review the variable ordering in V2 and ensure it matches V1 with only appended variables.
Attempted to call an upgrade function on the implementation directly. Always interact via the proxy address.
When asked for upgrade help, respond with:
bap578bap578-testingbap578-security-auditdocumentation
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
development
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
devops
Deploy applications and infrastructure to Cloudflare using Workers, Pages, and related platform services. Use when the user asks to deploy, host, publish, or set up a project on Cloudflare.
tools
Use this skill when designing and building durable command-line tools from API docs, OpenAPI specs, SDKs, curl examples, admin tools, web apps, or local scripts, especially when the CLI should expose composable commands, stable JSON output, auth/config handling, install-on-PATH behavior, and a companion skill.