Skip to main content

Tutorial: Upgrading ATS Contracts

This comprehensive guide covers upgrading facets and configurations in the Asset Tokenization Studio (ATS) smart contract system without disrupting existing tokens.

Table of Contents​

Overview​

The ATS upgrade system uses the Diamond Pattern (EIP-2535) with a Business Logic Resolver (BLR) to enable safe, versioned upgrades of facet implementations without requiring token redeployment.

Key Concepts​

Business Logic Resolver (BLR)

  • Central registry mapping resolver keys → facet addresses
  • Maintains global version counter across all facets
  • Manages configurations defining facet sets for token types

Global Versioning

  • Single latestVersion counter incremented on any facet registration
  • All previously registered facets must be re-registered together
  • Ensures atomic updates across the system

Configurations

  • Define which facets compose a token type (Equity, Bond)
  • Each configuration has independent version history
  • Tokens can pin to specific config versions or auto-update

Resolver Proxy Pattern

  • Each token is a proxy contract
  • Routes function calls to facets via BLR resolution
  • Can use pinned version or always-latest version

Understanding Versioning​

Three-Level Version System​

┌─────────────────────────────────────────┐
│ BLR Global Version (latestVersion) │ ← Increments on any facet registration
│ Current: 5 │
└─────────────────────────────────────────┘
│
├─ Facet Version Histories
│ ├─ AccessControlFacet: v1, v2, v3, v4, v5
│ ├─ BondFacet: v1, v2, v3, v4, v5
│ └─ RewardsFacet: (none), (none), (none), v4, v5
│
└─ Configuration Versions
├─ Equity Config: v1, v2, v3
│ └─ v3 uses facet global version 5
└─ Bond Config: v1, v2
└─ v2 uses facet global version 4

Version Resolution Modes​

Mode 1: Pinned Version (Recommended for Production)

// Token configuration
configurationVersion = 2; // Fixed to version 2

// Resolution: Always uses exact configuration version 2
// Upgrades require explicit update transaction

Mode 2: Auto-Update (Development/Testing)

// Token configuration
configurationVersion = 0; // or LATEST_VERSION

// Resolution: Always resolves to latest configuration version
// Upgrades happen automatically on next function call

Upgrade Scenarios​

Scenario 1: Bug Fix in Single Facet​

Situation: Critical bug found in BondFacet

Impact: Only affects bonds using latest version

Approach:

  1. Deploy fixed BondFacet v2
  2. Register all facets (v2 for Bond, existing versions for others)
  3. Create new Bond configuration v2
  4. Coordinate bond token upgrades (or auto-update if using version 0)

Scenario 2: New Feature Facet​

Situation: Adding new RewardsFacet

Impact: Available only to new configurations

Approach:

  1. Deploy new RewardsFacet
  2. Register all facets including new RewardsFacet
  3. Create Equity configuration v2 including rewards
  4. New tokens use v2, existing tokens remain on v1

Scenario 3: Multiple Facet Updates​

Situation: Major upgrade affecting 5+ facets

Impact: Requires comprehensive testing

Approach:

  1. Deploy all updated facets
  2. Register all facets together (atomic update)
  3. Create new configurations
  4. Staged rollout to production tokens

Scenario 4: Infrastructure Upgrade (BLR or Factory)​

Situation: Upgrading BLR implementation contract

Impact: Affects entire system

Approach:

  1. Deploy new BLR implementation
  2. Use ProxyAdmin.upgradeAndCall() on BLR proxy
  3. Test thoroughly before facet updates
  4. No token-level changes required (proxy pattern)

Safe Upgrade Process​

Pre-Upgrade Checklist​

  • Code review completed
  • Unit tests passing (100% coverage for changes)
  • Integration tests passing
  • Gas analysis performed
  • Security audit completed (for major changes)
  • Testnet deployment successful
  • Rollback plan documented
  • Stakeholder notification prepared

Upgrade Phases​

Phase 1: Development

  • Implement facet changes
  • Write comprehensive tests
  • Document breaking changes

Phase 2: Testing

  • Deploy to local network
  • Deploy to testnet (previewnet/testnet)
  • Perform integration testing
  • Load testing (if applicable)

Phase 3: Staging

  • Deploy to staging environment
  • Mirror production configuration
  • Perform final validation

Phase 4: Production

  • Deploy during maintenance window
  • Monitor for issues
  • Gradual rollout if possible

Phase 5: Post-Upgrade

  • Verify all functionality
  • Monitor events and transactions
  • Stakeholder communication

Step-by-Step Guides​

Guide 1: Upgrading a Single Facet​

Scenario: Fix bug in BondFacet

Step 1: Prepare New Implementation​

cd packages/ats/contracts

# Make your changes to contracts/layer_2/bond/Bond.sol
# Update version in comments/NatSpec

Step 2: Compile and Test​

# Compile contracts
npm run compile

# Run specific tests
npm run test -- test/layer_2/bond/Bond.test.ts

# Run all tests
npm run test

Step 3: Deploy New Facet to Testnet​

Create deployment script: scripts/maintenance/upgradeBondFacet.ts

import { ethers } from "hardhat";
import { BusinessLogicResolver } from "../../typechain-types";
import { registerFacets } from "../infrastructure/operations/registerFacets";
import { atsRegistry } from "../domain/atsRegistry";

async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying BondFacet upgrade from:", deployer.address);

// Deploy new BondFacet implementation
const BondFacetFactory = await ethers.getContractFactory("BondFacet");
const newBondFacet = await BondFacetFactory.deploy();
await newBondFacet.waitForDeployment();
const newBondAddress = await newBondFacet.getAddress();

console.log("New BondFacet deployed to:", newBondAddress);

// Get BLR instance
const blrAddress = process.env.BLR_PROXY_ADDRESS;
const blr = await ethers.getContractAt("BusinessLogicResolver", blrAddress);

// Get current facet addresses
const currentVersion = await blr.getLatestVersion();
console.log("Current BLR version:", currentVersion);

// Prepare registration data - MUST include ALL facets
const facetsToRegister = [
{
name: "AccessControlFacet",
address: process.env.ACCESS_CONTROL_FACET_ADDRESS, // Existing
resolverKey: atsRegistry.getFacetDefinition("AccessControlFacet").resolverKey.value,
},
{
name: "BondFacet",
address: newBondAddress, // NEW VERSION
resolverKey: atsRegistry.getFacetDefinition("BondFacet").resolverKey.value,
},
// ... ALL other facets with existing addresses
];

// Register all facets (increments version)
console.log("Registering facets...");
const result = await registerFacets(blr, facetsToRegister, {
confirmations: 2,
});

console.log("Registration complete!");
console.log("New BLR version:", await blr.getLatestVersion());
console.log("Transaction hash:", result.transactionHash);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Step 4: Execute Deployment​

# Set environment variables
export BLR_PROXY_ADDRESS=0x123...
export ACCESS_CONTROL_FACET_ADDRESS=0x456...
# ... all other facet addresses

# Deploy to testnet
npx hardhat run scripts/maintenance/upgradeBondFacet.ts --network hedera-testnet

Step 5: Create New Configuration​

// scripts/maintenance/createBondConfigV2.ts
import { ethers } from "hardhat";
import { createBondConfiguration } from "../domain/bond/createConfiguration";

async function main() {
const blr = await ethers.getContractAt("BusinessLogicResolver", process.env.BLR_PROXY_ADDRESS);

// Get all facet addresses (now includes new BondFacet)
const facetAddresses = new Map([
["BondFacet", process.env.NEW_BOND_FACET_ADDRESS],
// ... all other facets
]);

// Create Bond configuration v2
console.log("Creating Bond configuration v2...");
const result = await createBondConfiguration(blr, facetAddresses, {
batchSize: 15,
confirmations: 2,
});

console.log("Bond configuration v2 created!");
console.log("Config ID:", result.configurationId);
console.log("Version:", result.version);
console.log("Facet count:", result.facetCount);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
npx hardhat run scripts/maintenance/createBondConfigV2.ts --network hedera-testnet

Step 6: Verify Upgrade​

npx hardhat console --network hedera-testnet
> const blr = await ethers.getContractAt('BusinessLogicResolver', '0x123...')

> // Check latest version
> await blr.getLatestVersion()
2n // Incremented from 1

> // Check Bond configuration version
> await blr.getConfigurationVersion('0x0000000000000000000000000000000000000000000000000000000000000002')
2n // New configuration version

> // Verify BondFacet address
> await blr.resolveLatestBusinessLogic(RESOLVER_KEY_BOND)
'0xNEW_BOND_FACET_ADDRESS'

Guide 2: Adding a New Facet to Existing Configuration​

Scenario: Add RewardsFacet to Equity tokens

Step 1: Deploy New Facet​

// Assuming RewardsFacet already developed (see adding-facets.md)
const RewardsFacetFactory = await ethers.getContractFactory("RewardsFacet");
const rewardsFacet = await RewardsFacetFactory.deploy();
await rewardsFacet.waitForDeployment();

Step 2: Register All Facets (Including New One)​

const facetsToRegister = [
// ... all existing facets
{
name: "RewardsFacet",
address: await rewardsFacet.getAddress(),
resolverKey: RESOLVER_KEY_REWARDS,
},
];

await registerFacets(blr, facetsToRegister);

Step 3: Create Equity Configuration v2 with Rewards​

Update scripts/domain/equity/createConfiguration.ts:

export async function createEquityConfiguration(
blr: BusinessLogicResolver,
facetAddresses: Map<string, string>,
options?: CreateConfigurationOptions,
): Promise<CreateConfigurationResult> {
const facetConfigurations: FacetConfiguration[] = [
// ... existing 43 facets
{
facetName: "RewardsFacet", // NEW FACET
resolverKey: atsRegistry.getFacetDefinition("RewardsFacet").resolverKey.value,
address: facetAddresses.get("RewardsFacet")!,
},
];

// Create configuration with 44 facets
const result = await createBatchConfiguration(blr, EQUITY_CONFIG_ID, facetConfigurations, options);

return result;
}

Step 4: Deploy New Equity Tokens​

// New equity tokens automatically get RewardsFacet
const factory = await ethers.getContractAt("TREXFactory", factoryAddress);

await factory.createEquityToken(
EQUITY_CONFIG_ID,
0, // Use latest version (now includes rewards)
equityInitData,
);

Step 5: Existing Tokens (Optional Upgrade)​

Existing equity tokens on v1 (without rewards) can stay on v1 or upgrade:

Option A: Stay on v1 (No rewards)

  • No action required
  • Tokens continue using v1 configuration

Option B: Upgrade to v2 (Get rewards)

  • Requires governance approval (if ownership allows)
  • Call upgradeConfiguration() on proxy (if implemented)

Guide 3: Upgrading Infrastructure Contracts (BLR)​

Scenario: Fix critical bug in BLR implementation

Step 1: Deploy New BLR Implementation​

const NewBLRFactory = await ethers.getContractFactory("BusinessLogicResolverV2");
const newBLRImpl = await NewBLRFactory.deploy();
await newBLRImpl.waitForDeployment();

console.log("New BLR implementation:", await newBLRImpl.getAddress());

Step 2: Prepare Upgrade via ProxyAdmin​

import { upgradeProxy } from "../infrastructure/operations/upgradeProxy";

const proxyAdmin = await ethers.getContractAt("ProxyAdmin", process.env.PROXY_ADMIN_ADDRESS);

const blrProxy = process.env.BLR_PROXY_ADDRESS;
const newBLRImpl = process.env.NEW_BLR_IMPL_ADDRESS;

// Option 1: Simple upgrade (no reinitialization)
await upgradeProxy(proxyAdmin, {
proxyAddress: blrProxy,
newImplementationAddress: newBLRImpl,
});

// Option 2: Upgrade with reinitialization
const initData = newBLRImpl.interface.encodeFunctionData("reinitialize", [
/* reinit params */
]);

await upgradeProxy(proxyAdmin, {
proxyAddress: blrProxy,
newImplementationAddress: newBLRImpl,
initData: initData,
});

Step 3: Verify Upgrade​

> const blr = await ethers.getContractAt('BusinessLogicResolverV2', blrProxyAddress)

> // Verify implementation address
> const implSlot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
> const impl = await ethers.provider.getStorage(blrProxyAddress, implSlot)
> console.log('Implementation:', ethers.getAddress('0x' + impl.slice(26)))

> // Test new functionality
> await blr.newFunctionFromV2()

Guide 4: Configuration Selector Blacklisting​

Scenario: Deprecate specific function across all tokens

Step 1: Identify Function Selector​

const deprecatedSelector = ethers.id("deprecatedFunction(uint256)").slice(0, 10);
console.log("Selector to blacklist:", deprecatedSelector);
// Output: 0x12345678

Step 2: Blacklist Selector in Configuration​

const blr = await ethers.getContractAt("BusinessLogicResolver", blrAddress);

// Blacklist for Equity configuration
await blr.addSelectorsToBlacklist(EQUITY_CONFIG_ID, [deprecatedSelector]);

console.log("Selector blacklisted for Equity tokens");

Step 3: Verify Blacklisting​

> const equityToken = await ethers.getContractAt('IEquity', tokenAddress)

> // Attempt to call deprecated function
> await equityToken.deprecatedFunction(123)
// Error: Function selector is blacklisted

Testing Upgrades​

Local Testing​

# Start local Hardhat network
npx hardhat node

# In separate terminal, run upgrade script
npx hardhat run scripts/maintenance/upgradeBondFacet.ts --network localhost

Testnet Testing​

# Deploy complete system to testnet
npm run deploy:hedera:testnet

# Create test tokens
npx hardhat run scripts/domain/factory/deployBondToken.ts --network hedera-testnet

# Perform upgrade
npx hardhat run scripts/maintenance/upgradeBondFacet.ts --network hedera-testnet

# Test upgraded functionality
npx hardhat run scripts/test/testBondUpgrade.ts --network hedera-testnet

Upgrade Test Script Example​

// scripts/test/testBondUpgrade.ts
import { ethers } from "hardhat";

async function main() {
const bondTokenAddress = process.env.BOND_TOKEN_ADDRESS;
const bond = await ethers.getContractAt("IBond", bondTokenAddress);

console.log("Testing upgraded Bond functionality...");

// Test 1: Old functionality still works
const bondDetails = await bond.getBondDetails();
console.log("✓ getBondDetails() works");

// Test 2: New functionality (if added)
if (bond.interface.hasFunction("newBondFunction")) {
await bond.newBondFunction(/* params */);
console.log("✓ newBondFunction() works");
}

// Test 3: Bug fix verification
// ... test specific bug fix

console.log("All upgrade tests passed!");
}

main().catch(console.error);

Production Deployment​

Pre-Deployment​

  1. Announce maintenance window

    Maintenance Window: 2025-01-15 02:00-04:00 UTC
    Expected downtime: None (upgrade is seamless)
    Affected tokens: All Bond tokens (auto-update enabled)
  2. Verify all prerequisites

    # Check deployer balance
    npx hardhat console --network hedera-mainnet
    > (await ethers.provider.getBalance(deployerAddress)).toString()

    # Verify current state
    > const blr = await ethers.getContractAt('BusinessLogicResolver', blrAddress)
    > await blr.getLatestVersion()

Deployment​

# Set production environment
export NETWORK=hedera-mainnet
export BLR_PROXY_ADDRESS=0xPRODUCTION_BLR
# ... other production addresses

# Execute upgrade
npx hardhat run scripts/maintenance/upgradeBondFacet.ts --network hedera-mainnet

# Monitor transaction
# Use Hedera Mirror Node Explorer

Post-Deployment​

  1. Verify upgrade

    > const blr = await ethers.getContractAt('BusinessLogicResolver', blrAddress)
    > await blr.getLatestVersion() // Should be incremented
  2. Test live tokens

    > const bondToken = await ethers.getContractAt('IBond', productionBondAddress)
    > await bondToken.getBondDetails() // Should use new implementation
  3. Monitor events

    # Check for errors in recent transactions
    curl "https://mainnet.mirrornode.hedera.com/api/v1/contracts/0.0.12345678/results?limit=10"
  4. Stakeholder notification

    Upgrade Complete: Bond facet v2 deployed
    - New version: 2
    - All bond tokens updated automatically
    - No user action required

Rollback Procedures​

Scenario: Critical Bug in New Facet​

Option 1: Deploy Previous Version Again​

// Register old facet address again (creates new version)
const facetsToRegister = [
{
name: "BondFacet",
address: previousBondFacetAddress, // Old working version
resolverKey: RESOLVER_KEY_BOND,
},
// ... all other facets
];

await registerFacets(blr, facetsToRegister);

// Create new configuration using old facet
await createBondConfiguration(blr, facetAddresses);

Option 2: Pin Tokens to Previous Configuration​

// If using versioned configurations, pin to previous version
// (Requires governance or admin action on token)
await bondToken.updateConfigurationVersion(previousVersion);

Option 3: Emergency Pause​

// Pause affected facet functionality
const pauseFacet = await ethers.getContractAt("IPause", tokenAddress);
await pauseFacet.pause();

console.log("Token paused while investigating issue");

Best Practices​

Version Management​

  1. Document every version: Maintain changelog with version details
  2. Semantic versioning: Follow semver for major/minor/patch
  3. Git tags: Tag repository with version numbers
  4. Configuration matrix: Track which tokens use which versions

Testing Standards​

  1. 100% test coverage: For modified facets
  2. Integration tests: Test facet interactions
  3. Gas benchmarks: Compare before/after upgrade
  4. Load testing: For performance-critical upgrades

Communication​

  1. Advance notice: 7+ days for major upgrades
  2. Detailed changelogs: Publish comprehensive notes
  3. Migration guides: If breaking changes
  4. Support availability: During and after upgrade

Monitoring​

  1. Event tracking: Monitor upgrade-related events
  2. Error alerts: Set up alerts for failures
  3. Performance metrics: Track gas usage changes
  4. User impact: Monitor transaction success rates

Security​

  1. Multi-sig for production: Require multiple approvals
  2. Time locks: Enforce delay between proposal and execution
  3. Emergency procedures: Document emergency contacts
  4. Insurance: Consider smart contract insurance

Admin Account Assumptions for Resolver Swaps​

The Diamond proxy exposes updateResolver, updateConfig and updateConfigVersion, gated by DEFAULT_ADMIN_ROLE. These calls redirect all proxy function calls to a new implementation in a single transaction, with no on-chain timelock and no user exit window.

This is an intentional design choice: the contracts assume the DEFAULT_ADMIN_ROLE is held by a hardened account — a multisig, governance contract, or equivalent — whose own approval workflow already provides the delay, review, and accountability surface required for such a sensitive operation. Adding an in-contract timelock would only duplicate controls that must exist at the admin-account level.

When deploying to production, issuers MUST:

  • Assign DEFAULT_ADMIN_ROLE to a multisig or governance contract, never to an EOA.
  • Document the off-chain approval process (signers, thresholds, review SLA) used before any resolver or configuration change is executed.
  • Treat any direct EOA holding DEFAULT_ADMIN_ROLE as a configuration defect, not a supported deployment mode.

Documentation​

Every upgrade should document:

  • Version number and date
  • Changes made (bug fixes, features, optimizations)
  • Breaking changes
  • Migration steps (if applicable)
  • Test results
  • Deployment transactions

Example Upgrade Checklist​

# Upgrade Checklist: BondFacet v1.2.3

## Pre-Upgrade

- [x] Code review completed (2025-01-10)
- [x] Unit tests: 100% coverage
- [x] Integration tests: Passed
- [x] Security audit: Completed (no critical issues)
- [x] Testnet deployment: Successful (0.0.12345678)
- [x] Gas analysis: -5% reduction in redemption
- [x] Stakeholder notification: Sent (2025-01-08)

## Deployment

- [x] Deployed new BondFacet: 0.0.12345679
- [x] Registered facets: Version 3
- [x] Created Bond config v3: Success
- [x] Transaction hash: 0xabcd...

## Post-Deployment

- [x] Verified BLR version: 3
- [x] Tested live bond token: Passed
- [x] Monitored for 24h: No issues
- [x] Documentation updated
- [x] Final notification sent: 2025-01-15

## Rollback Plan

- Previous BondFacet: 0.0.12345670
- Previous config version: 2
- Emergency contact: admin@example.com

Support​

For upgrade assistance: