For the complete documentation index, see llms.txt. This page is also available as Markdown.

Upgrade a cyberCORP

Move an existing cyberCORP to v5 by upgrading its four proxies and two beacons together, then backfill and open its LET contracts

A company deployed through the CyberCorpFactory stack puts its CyberCorp, IssuanceManager, DealManager and RoundManager behind UUPS proxies. Its Ledger Entry Token (LET) contracts and CyberScrip instances use beacon proxies whose beacons the IssuanceManager owns. An upgrade needs both sides: MetaLeX publishes a reference implementation on the factory, and the company's owner opts in. See Upgrade model.

New companies are created on v5. An existing company stays on the version it was deployed with until its owner runs this upgrade. In the cyberCORPs app, an available upgrade appears as the first task in mission control and links to the app's Upgrade page; Run your company covers the app side.

1. Check the company's versions and targets

Read DEPLOY_VERSION() on the CyberCorp, IssuanceManager, DealManager and RoundManager, and on the implementations behind the IssuanceManager's two beacons (getCertPrinterBeaconImplementation(), getScripBeaconImplementation()). Then read the targets from the factory each contract records:

address corpTarget = ICyberCorpSingleFactory(CyberCorp(cyberCorp).upgradeFactory())
    .getRefImplementation();
IIssuanceManagerFactory imf =
    IIssuanceManagerFactory(IIssuanceManager(issuanceManager).getUpgradeFactory());
address imTarget      = imf.getRefImplementation();
address printerTarget = imf.getCyberCertPrinterRefImplementation();
address scripTarget   = imf.getCyberScripRefImplementation();
// DealManager and RoundManager: getRefImplementation() on the
// DealManagerFactory and RoundManagerFactory they were deployed from.

If a target is not what you expected, do not upgrade. The reference-implementation check exists so that neither side can move you to an arbitrary implementation.

2. Upgrade all six contracts in one batch

An existing UUPS company moves to v5 with six calls, made by an owner of the company's BorgAuth (level 99 or above; officers hold 200):

Each call is onlyOwner and reverts NotRefImplementation unless its target equals the published reference. Upgrading a beacon moves every instance of that type under the IssuanceManager at once.

Run the six as one transaction where you can; for a Safe-owned company, that is one Safe batch. Each version calls functions the other version lacks, so a company left part-way (a v5 IssuanceManager next to a v4 DealManager, for example) can see deal, round and issuance calls revert until the rest land. The contracts repository's script/upgrade-v5.s.sol builds this batch for one company (corpUpgradeCalls, and printCorpUpgradeSafeBatch, which prints Safe Transaction Builder JSON) and refuses a company that has no RoundManager.

The app's Upgrade page runs the same calls in the same order. If a Safe that the connected wallet owns holds the owner role, it proposes every call that is not done as one Safe transaction, followed by the LET migrations of step 3 that need no dates. Its gas grows with the holders (one initializeHolderCount each) and the lots (each backfill touches each lot). The page estimates it from bounds measured on a Base fork and does not offer a batch above half the block gas limit; such a company needs a plan with MetaLeX, because running the migrations after the upgrade in a separate transaction reopens the period described in step 3. Otherwise it sends one call at a time from a wallet with the owner role, and only the next call is available. From a wallet, the LET migrations follow in later transactions, and in between the v5-only paths (for example a secondary offer that a buyer accepts and settles with finalizeSecondaryTradeAgreement) can mint a LET to a holder that is not seeded yet. Nothing outside the contracts can stop those calls, so use a Safe batch for a company whose LET contracts have holders from before the upgrade, and read step 3 first. It starts the sequence only for a company with all contracts on a v4 release (any 4.x DEPLOY_VERSION, such as "4", "4.0.1" or "4.1"), UUPS proxies, an IssuanceManager and a DealManager, and v5 references for every call. A v4 company without a RoundManager takes the five other calls; a fork test of a deal and an issuance after those five calls passed before the app allowed it. While a company runs v5 and older contracts side by side, the app refuses new deals, rounds and issuance. See Run your company.

3. Prepare the upgraded LET contracts

The upgraded LET contracts keep their data, but v5 adds storage that starts empty, zero or closed. Work through the steps below on each LET contract as soon as the upgrade lands, because holders can call convertScripToCert at any time. LedgerEntryToken documents each function and the holder-counting rules.

The app's Upgrade page runs these steps from a checklist for each LET contract once the company runs v5. It reads which lots and holders still need each step from the contract's storage. It sends the required calls in this order: holder counters, legal-owner backfill, the badge when the company has a HolderCapCondition configuration, tally backfill. It refuses issuance, deal settlement and scrip conversion on a LET contract until its holder counters, legal-owner index and holder tally are complete. It proposes dates from onchain and company records, and sends them only after the owner confirms them.

Existing LET contracts keep the certificate extension they were created with. New LET contracts can bind the V3 extensions, which need a v5 LET contract and IssuanceManager.

Seed each holder's possession counter

On a LET contract whose lots were minted before it kept a per-wallet possession counter, a holder's counter reads zero and a transfer out of that wallet reverts. An admin seeds each such holder with initializeHolderCount(holder) before anything is minted or transferred to or from it. Seed the DealManager and RoundManager last when they hold lots in escrow: a pending deal or allocation settles by a transfer out of them, which reverts while their counter is zero, so no settlement can reach a holder that is not seeded yet. The app's checklist uses this order.

A mint or transfer that reaches a holder first sets the counter to 1, below the holder's balance. initializeHolderCount then reverts HolderCountAlreadyInitialized, and no deployed function corrects the counter. A holder can cause this without the company: convertScripToCert looks up the holder's lot through the legal-owner enumeration, which is empty until the backfill below. With a recertification approval on file, it then mints a new LET to the holder. An FCFS round that has not ended does the same without the company once it opens: RoundManager.submitEOI is public, and for an FCFS round it calls allocate in the same transaction, which mints a LET to the investor. A new LET also reaches a legacy holder indirectly: if the LET contract is transferable (or a lot has setTokenTransferable on), any holder whose counter is already seeded can transfer possession to a legacy holder without an endorsement. No seeding order prevents this, so turn transfers off (setGlobalTransferable(false) and each lot's override) until every counter is seeded. Seed the counters in the same transaction as the beacon upgrade where you can, as the app's Safe batch does. A batch is built when it is proposed, so a lot minted while it waits for signatures is not in it. So for a wallet upgrade and for a Safe batch, close every FCFS round that has not ended, on every Round Manager the company has used, including one that has not started, with closeRoundNow(roundId), and clear every outstanding approval with clearRecertificationApproval(certAddress, investor), before the upgrade. Also settle or void every pending deal and secondary offer whose lots the DealManager holds: finalizeDeal is public, and processTransfer lets the current DealManager deliver even with transfers off, so a delivery after the batch was encoded reaches a holder that the batch does not seed. Check all of these again right before the Safe executes the batch, and do not issue or propose deals while it waits for signatures; the app's Upgrade page refuses to build or show the batch until they hold. closeRoundNow sets endTime to the current block's timestamp, and submitEOI reverts RoundNotOpen only when block.timestamp > endTime. So wait for a block with a later timestamp before the beacon upgrade. closeRoundNow reverts EndTimeReductionRestricted for a round that restricts it; for such a round, wait until the round ends. Set the approvals again only after the counters are seeded and the legal-owner enumeration is backfilled (see below), because until then a conversion with an approval also draws on the shared vault.

The legal-owner enumeration and the look-through holder tally start empty. Until they are backfilled, existing holders read zero through balanceOfLegalOwner and isLegalHolder, and lookThroughHolderCount() starts at zero, so a configured HolderCapCondition can admit a buyer although the real cap is already full.

Scrip redemption depends on the enumeration too: convertScripToCert finds the holder's lot and draws down the holder's own vault positions through it. Before the backfill it cannot find the lot, and reverts for a missing recertification approval. With an approval on file, it takes the withdrawal from the shared vault instead of the holder's positions. Run these steps before enabling registration or secondary settlement on the LET contract:

  1. If the SPV uses look-through counting, wire the badge first with setLookThroughBadge(badge).

  2. Run backfillLegalOwners(start, count) and backfillLookThroughTally(start, count) in batches over [0, totalSupply()). Both are permissionless and idempotent, so a batch can be re-run safely.

Set acquisition timestamps for Rule 144 and Reg S trades

Each lot's acquisitionTimestamp reads zero for lots minted before the upgrade. HoldingPeriodCondition (Rule 144) and RegSDistributionComplianceCondition fail closed on a zero timestamp, so secondary trades on those pathways stay refused until it is set. Before enabling them:

  1. On a LET contract whose extension supports the fund-interest type, run backfillAcquisitionTimestamps(start, count) in batches. It copies each lot's recorded acquisition date, and does nothing on other LET contracts.

  2. For the remaining lots, an admin sets each known date with setAcquisitionTimestamp(tokenId, ts). The date anchors the holding period, so take it from the company's records.

Record issue dates for the certificate image

v4 LET contracts never stored issueTimestamp, so it reads zero for every lot minted before the upgrade, and the certificate image built by CertificateUriBuilder shows a blank Issue Date for each one. Nothing backfills it automatically. An admin sets each lot's issuance date with setIssueTimestamp(tokenId, ts), taking the date from the company's records (for example the lot's original issuance endorsement). The date fixes the certificate's displayed record and blocks no transfer or trade.

Open the registration gate

The v5 registration gate is new storage and starts closed. Until an admin calls setGlobalLegalTransferable(true) on a LET contract (or setTokenLegalTransferable per lot), assignCert, endorsed transfers and secondary-trade settlement on it revert LegalOwnerNotTransferable. Issuance and the managers' deliveries out of escrow are not affected. Open the gate after the legal-owner backfill.

Who controls the upgrade

  • The company may stay on its current implementation indefinitely.

  • An upgrade to anything other than the factory's published reference reverts.

  • The company's owner makes the upgrade calls. The deploying factory may still hold owner level on your BorgAuth; Access control shows how to check.

Upgrade model has the version and proxy details, and Upgrades and control explains why an upgrade needs both MetaLeX and the company.

Last updated

Was this helpful?