diff --git a/foundry.toml b/foundry.toml
index 5f469c8..656742c 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -4,10 +4,11 @@ out = "out"
libs = ["lib", "dependencies"]
optimizer = true
optimizer_runs = 15
+via_ir = true
extra_output_files = ['abi']
fs_permissions = [
- { access = "read", path = "./script/res" },
- { access = "read", path = "./test/res" }
+ { access = "read", path = "./script/res" },
+ { access = "read", path = "./test/res" },
]
[rpc_endpoints]
diff --git a/script/deploy-pump-factory-full-lifecycle.s.sol b/script/deploy-pump-factory-full-lifecycle.s.sol
index d80d423..ffa2ba4 100644
--- a/script/deploy-pump-factory-full-lifecycle.s.sol
+++ b/script/deploy-pump-factory-full-lifecycle.s.sol
@@ -136,7 +136,8 @@ contract DeployPumpCorpFactoryFullLifeCycleScript is Script {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: ""
});
string[] memory roundFirstPartyValues = new string[](5);
diff --git a/script/public-round-test-deploy.s.sol b/script/public-round-test-deploy.s.sol
index 358653d..5f99c06 100644
--- a/script/public-round-test-deploy.s.sol
+++ b/script/public-round-test-deploy.s.sol
@@ -210,7 +210,8 @@ contract PublicRoundTestDeploy is Script {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
// SAFE template id 1 requires 5 global fields and 5 party fields
diff --git a/src/CertificateImageBuilder.sol b/src/CertificateImageBuilder.sol
index d582a4e..4789f1e 100644
--- a/src/CertificateImageBuilder.sol
+++ b/src/CertificateImageBuilder.sol
@@ -18,7 +18,7 @@ o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o8888
- .oooooo. .o8 .oooooo.
+ .oooooo. .o8 .oooooo.
d8P' `Y8b "888 d8P' `Y8b
888 oooo ooo 888oooo. .ooooo. oooo d8b 888 .ooooo. oooo d8b oo.ooooo.
888 `88. .8' d88' `88b d88' `88b `888""8P 888 d88' `88b `888""8P 888' `88b
@@ -46,7 +46,7 @@ import "./creds/storage/lexchexStorage.sol";
import "./CertificateImageContentBuilder.sol";
/// @title CertificateImageBuilder
-/// @notice Minimal SVG builder used by CertificateUriBuilder; mirrors LeXcheX styling
+/// @notice SVG builder used by CertificateUriBuilder; renders the "Ledger Entry Token" certificate
library CertificateImageBuilder {
function buildLexChexSVG(Accreditation memory acc) internal pure returns (string memory) {
return string(abi.encodePacked(
@@ -61,67 +61,126 @@ library CertificateImageBuilder {
function buildCertificateSVG(
CertificateSVGParams memory params,
uint256 timestamp
- ) internal view returns (string memory) {
+ ) internal pure returns (string memory) {
return string(abi.encodePacked(
_getSVGHeader(),
- _getSVGBackground(),
+ _getSVGFrame(params.isVoided),
+ '',
CertificateImageContentBuilder.buildSVGContent(params, timestamp),
- _getSVGFooter()
+ ''
));
}
function _getSVGHeader() private pure returns (string memory) {
- return ''
- ));
- }
-
- function _getSVGHeader() private pure returns (string memory) {
- return '';
- }
-
- function _getSVGBackground() private pure returns (string memory) {
- return string(abi.encodePacked(
- _getBackgroundPaths(),
- _getSVGDecorativeElements()
- ));
- }
-
- function _getBackgroundPaths() private pure returns (string memory) {
- return string(abi.encodePacked(
- '',
- '',
- ''
- ));
- }
-
- function _getSVGDecorativeElements() private pure returns (string memory) {
- return string(abi.encodePacked(
- _getTopDecorations(),
- _getMidDecorations(),
- _generateBottomDecorations(),
- ''
- ));
- }
-
- function _getTopDecorations() private pure returns (string memory) {
- return string(abi.encodePacked(
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- ''
- ));
- }
-
- function _getMidDecorations() private pure returns (string memory) {
- return string(abi.encodePacked(
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- ''
- ));
- }
-
- /// @notice Generates bottom decorations using SVG pattern for size optimization
- /// @dev Uses a single pattern definition that tiles across the bottom row
- function _generateBottomDecorations() private pure returns (string memory) {
- return string(abi.encodePacked(
- _getBottomPattern(),
- ''
- ));
- }
-
- /// @notice Defines the repeating pattern for bottom decorations
- function _getBottomPattern() private pure returns (string memory) {
- return string(abi.encodePacked(
- '',
- '',
- '',
- _getBottomPatternText(),
- '',
- ''
- ));
- }
-
- /// @notice The "In code we trust" text paths for the bottom pattern
- function _getBottomPatternText() private pure returns (string memory) {
- return '';
- }
-
- function _buildSVGContent(
- CertificateSVGParams calldata params,
- uint256 timestamp
- ) private pure returns (string memory) {
- (string memory day, string memory month, string memory year) = _getDateComponents(timestamp);
-
- return string(abi.encodePacked(
- _buildDefs(),
- _buildHeader(params),
- _buildMiddleSection(params),
- _buildFooter(params, day, month, year)
- ));
- }
-
- function _buildDefs() private pure returns (string memory) {
- return string(abi.encodePacked(
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- ''
- ));
- }
-
- function _buildHeader(CertificateSVGParams calldata params) private pure returns (string memory) {
- string memory formattedUnits = _formatUnits(params.units, params.securityType);
- return string(abi.encodePacked(
- '', params.corpName, '',
- 'Token ID',
- '#', _uintToString(params.tokenId), '',
- '', _getBaseUnit(params.securityType), '',
- '', formattedUnits, '',
- '', _securitySeriesToString(params.securitySeries), ' ', _securityClassToString(params.securityType), ''
- ));
- }
-
- function _buildMiddleSection(CertificateSVGParams calldata params) private pure returns (string memory) {
- if (_isConvertible(params.securityType)) {
- return _buildMiddleConvertible(params);
- }
- return _buildMiddleShares(params);
- }
-
- function _buildMiddleConvertible(CertificateSVGParams calldata params) private pure returns (string memory) {
- string memory formattedUnits = _formatUnits(params.units, params.securityType);
- string memory securityFullName = _getSecurityFullName(params.securityType);
- return string(abi.encodePacked(
- 'This Certifies that ',
- '', params.ownerName, '',
- '',
- '',
- 'is the registered holder of',
- '1',
- '', securityFullName, '',
- _buildMiddleConvertiblePart2(params, formattedUnits)
- ));
- }
-
- function _buildMiddleConvertiblePart2(CertificateSVGParams calldata params, string memory formattedUnits) private pure returns (string memory) {
- return string(abi.encodePacked(
- '',
- 'of',
- '', params.corpName, '',
- '',
- 'purchased from the said Entity for',
- '', formattedUnits, '',
- '',
- 'and transferable only in ',
- 'accordance with the terms and conditions thereof and any other applicable agreements',
- ' between or involving or applicable to the said Entity and the said registered Holder.'
- ));
- }
-
- function _buildMiddleShares(CertificateSVGParams calldata params) private pure returns (string memory) {
- string memory formattedUnits = _formatUnits(params.units, params.securityType);
- string memory unitType = _buildUnitType(params.securityType, params.securitySeries);
- return string(abi.encodePacked(
- 'This Certifies That',
- '',
- '', params.ownerName, '',
- 'is the registered holder of',
- '', formattedUnits, '',
- '',
- _buildMiddleSharesPart2(params, unitType)
- ));
- }
-
- function _buildMiddleSharesPart2(CertificateSVGParams calldata params, string memory unitType) private pure returns (string memory) {
- return string(abi.encodePacked(
- '', unitType, '',
- '',
- '', params.corpName, '',
- '',
- 'of',
- 'transferable only on the books of the Corporation by the holder hereof in person or by',
- 'Attorney upon surrender of this Certificate properly endorsed.'
- ));
- }
-
- function _buildFooter(
- CertificateSVGParams calldata params,
- string memory day,
- string memory month,
- string memory year
- ) private pure returns (string memory) {
- return string(abi.encodePacked(
- 'In Witness Whereof',
- ', the said Entity has caused this Certificate to be signed by its duly',
- 'authorized officer(s)',
- _buildFooterDate(day, month, year),
- _buildFooterSignature(params)
- ));
- }
-
- function _buildFooterDate(string memory day, string memory month, string memory year) private pure returns (string memory) {
- return string(abi.encodePacked(
- 'This',
- '', day, '',
- '',
- 'day of',
- '', month, '',
- '',
- 'A.D.',
- '', year, '',
- ''
- ));
- }
-
- function _buildFooterSignature(CertificateSVGParams calldata params) private pure returns (string memory) {
- return string(abi.encodePacked(
- '', params.officerName, '',
- '', params.officerTitle, '',
- 'Link to full certificate: ', params.certificateUri, ''
- ));
- }
-
- // Helper functions
-
- function _formatUnits(uint256 units, SecurityClass securityType) private pure returns (string memory) {
- // Convert from 18 decimals to display with 2 decimal places (cents)
- string memory formattedUnits = _format18DecimalsWithCents(units);
- if (_isDollarBased(securityType)) {
- return string(abi.encodePacked("$", formattedUnits));
- }
- return formattedUnits;
- }
-
- /// @notice Formats an 18-decimal value with 2 decimal places (e.g., 1000.50)
- /// @param value The 18-decimal value
- /// @return Formatted string with commas and 2 decimal places
- function _format18DecimalsWithCents(uint256 value) private pure returns (string memory) {
- // Divide by 1e16 to get value in cents (2 decimal places)
- uint256 valueInCents = value / 1e16;
- uint256 wholePart = valueInCents / 100;
- uint256 centsPart = valueInCents % 100;
-
- string memory wholeStr = _formatNumberWithCommas(wholePart);
-
- // Format cents with leading zero if needed
- string memory centsStr;
- if (centsPart < 10) {
- centsStr = string(abi.encodePacked("0", _uintToString(centsPart)));
- } else {
- centsStr = _uintToString(centsPart);
- }
-
- return string(abi.encodePacked(wholeStr, ".", centsStr));
- }
-
- /// @notice Formats a number with commas as thousand separators (e.g., 1,000,000)
- function _formatNumberWithCommas(uint256 _i) private pure returns (string memory) {
- if (_i == 0) return "0";
-
- // First get the plain number string
- uint256 j = _i;
- uint256 digitCount;
- while (j != 0) {
- digitCount++;
- j /= 10;
- }
-
- // Calculate how many commas we need
- uint256 commaCount = (digitCount - 1) / 3;
- uint256 totalLength = digitCount + commaCount;
-
- bytes memory result = new bytes(totalLength);
- uint256 pos = totalLength;
- uint256 digitsSinceComma = 0;
-
- while (_i != 0) {
- // Add comma before every 3rd digit (except at the start)
- if (digitsSinceComma == 3) {
- pos--;
- result[pos] = ",";
- digitsSinceComma = 0;
- }
-
- pos--;
- result[pos] = bytes1(uint8(48 + (_i % 10)));
- _i /= 10;
- digitsSinceComma++;
- }
-
- return string(result);
- }
-
- function _isDollarBased(SecurityClass _class) private pure returns (bool) {
- return _class == SecurityClass.SAFE || _class == SecurityClass.SAFT || _class == SecurityClass.SAFTE || _class == SecurityClass.TokenWarrant;
- }
-
- function _isConvertible(SecurityClass _class) private pure returns (bool) {
- return _class == SecurityClass.SAFE ||
- _class == SecurityClass.SAFT ||
- _class == SecurityClass.SAFTE ||
- _class == SecurityClass.ConvertibleNote ||
- _class == SecurityClass.TokenWarrant ||
- _class == SecurityClass.TokenPurchaseAgreement;
- }
-
- function _securityClassToString(SecurityClass _class) private pure returns (string memory) {
- if (_class == SecurityClass.SAFE) return "SAFE";
- if (_class == SecurityClass.SAFT) return "SAFT";
- if (_class == SecurityClass.SAFTE) return "SAFTE";
- if (_class == SecurityClass.TokenPurchaseAgreement) return "Token Purchase Agreement";
- if (_class == SecurityClass.TokenWarrant) return "Token Warrant";
- if (_class == SecurityClass.CommonStock) return "Common Stock";
- if (_class == SecurityClass.StockOption) return "Stock Option";
- if (_class == SecurityClass.PreferredStock) return "Preferred Stock";
- return "Unknown";
- }
-
- function _securitySeriesToString(SecuritySeries _series) private pure returns (string memory) {
- if (_series == SecuritySeries.SeriesPreSeed) return "Pre-Seed";
- if (_series == SecuritySeries.SeriesSeed) return "Series Seed";
- if (_series == SecuritySeries.SeriesA) return "Series A";
- if (_series == SecuritySeries.SeriesB) return "Series B";
- if (_series == SecuritySeries.SeriesC) return "Series C";
- if (_series == SecuritySeries.SeriesD) return "Series D";
- if (_series == SecuritySeries.SeriesE) return "Series E";
- if (_series == SecuritySeries.SeriesF) return "Series F";
- if (_series == SecuritySeries.NA) return "";
- if (_series == SecuritySeries.ACE) return "ACE";
- return "";
- }
-
- function _getBaseUnit(SecurityClass _class) private pure returns (string memory) {
- if (_class == SecurityClass.SAFE) return "Dollars";
- if (_class == SecurityClass.SAFT) return "Dollars";
- if (_class == SecurityClass.SAFTE) return "Dollars";
- if (_class == SecurityClass.TokenPurchaseAgreement) return "Tokens";
- if (_class == SecurityClass.TokenWarrant) return "Tokens";
- if (_class == SecurityClass.ConvertibleNote) return "Notes";
- if (_class == SecurityClass.CommonStock) return "Shares";
- if (_class == SecurityClass.StockOption) return "Shares";
- if (_class == SecurityClass.PreferredStock) return "Shares";
- if (_class == SecurityClass.RestrictedStockPurchaseAgreement) return "Units";
- if (_class == SecurityClass.RestrictedStockUnit) return "Units";
- if (_class == SecurityClass.RestrictedTokenPurchaseAgreement) return "Units";
- if (_class == SecurityClass.RestrictedTokenUnit) return "Units";
- return "Unknown";
- }
-
- function _buildUnitType(SecurityClass _class, SecuritySeries _series) private pure returns (string memory) {
- return _getBaseUnit(_class);
- }
-
- function _getSecurityFullName(SecurityClass _class) private pure returns (string memory) {
- if (_class == SecurityClass.SAFE) return "Simple Agreement for Future Equity";
- if (_class == SecurityClass.SAFT) return "Simple Agreement for Future Tokens";
- if (_class == SecurityClass.SAFTE) return "Simple Agreement for Future Tokens or Equity";
- if (_class == SecurityClass.TokenPurchaseAgreement) return "Token Purchase Agreement";
- if (_class == SecurityClass.TokenWarrant) return "Token Warrant";
- if (_class == SecurityClass.CommonStock) return "Common Stock";
- if (_class == SecurityClass.StockOption) return "Stock Option";
- if (_class == SecurityClass.PreferredStock) return "Preferred Stock";
- return "Unknown";
- }
-
- function _getDateComponents(uint256 timestamp) private pure returns (string memory day, string memory month, string memory year) {
- uint256 totalDays = timestamp / 86400;
- uint256 y = 1970;
- uint256 daysRemaining = totalDays;
-
- while (daysRemaining >= (_isLeapYear(y) ? 366 : 365)) {
- daysRemaining -= _isLeapYear(y) ? 366 : 365;
- y++;
- }
-
- uint8[12] memory daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
- if (_isLeapYear(y)) {
- daysInMonth[1] = 29;
- }
-
- uint256 m = 0;
- while (m < 12 && daysRemaining >= daysInMonth[m]) {
- daysRemaining -= daysInMonth[m];
- m++;
- }
-
- day = _uintToString(daysRemaining + 1);
- month = _getMonthName(m + 1);
- year = _uintToString(y);
- }
-
- function _isLeapYear(uint256 y) private pure returns (bool) {
- return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
- }
-
- function _getMonthName(uint256 month) private pure returns (string memory) {
- if (month == 1) return "January";
- if (month == 2) return "February";
- if (month == 3) return "March";
- if (month == 4) return "April";
- if (month == 5) return "May";
- if (month == 6) return "June";
- if (month == 7) return "July";
- if (month == 8) return "August";
- if (month == 9) return "September";
- if (month == 10) return "October";
- if (month == 11) return "November";
- if (month == 12) return "December";
- return "Unknown";
- }
-
- function _uintToString(uint256 _i) private pure returns (string memory) {
- if (_i == 0) return "0";
- uint256 j = _i;
- uint256 length;
- while (j != 0) {
- length++;
- j /= 10;
- }
- bytes memory bstr = new bytes(length);
- uint256 k = length;
- while (_i != 0) {
- k--;
- bstr[k] = bytes1(uint8(48 + uint256(_i % 10)));
- _i /= 10;
- }
- return string(bstr);
+ return CertificateImageBuilder.buildCertificateSVG(params, timestamp);
}
}
-
diff --git a/src/CertificateImageContentBuilder.sol b/src/CertificateImageContentBuilder.sol
index 81665ce..c0f5243 100644
--- a/src/CertificateImageContentBuilder.sol
+++ b/src/CertificateImageContentBuilder.sol
@@ -44,33 +44,147 @@ pragma solidity ^0.8.28;
import "./CyberCorpConstants.sol";
/// @title CertificateImageContentBuilder
-/// @notice Helper library for generating SVG content
+/// @notice Helper library generating the gradient defs and dynamic text content
+/// of the "Ledger Entry Token" certificate SVG
library CertificateImageContentBuilder {
function buildSVGContent(
CertificateSVGParams memory params,
uint256 timestamp
) internal pure returns (string memory) {
- string memory securityType = _securityClassToString(params.securityType);
- string memory unitType = _buildUnitType(params.securityType, params.securitySeries);
- (string memory day, string memory month, string memory year) = _getDateComponents(timestamp);
+ string memory dateStr = _formatDate(timestamp);
+ return string(abi.encodePacked(
+ _buildDefs(),
+ '',
+ '',
+ _buildHeaderText(params),
+ _buildPartiesText(params),
+ _buildStatsText(params, dateStr),
+ _buildOfficerText(params, dateStr),
+ _buildRestrictionsText(params.transferRestrictions),
+ unicode'Recorded on the MetaLeX Labs, Inc. Tokenized Stock Ledger pursuant to DGCL §§219, 224.',
+ '',
+ _buildVoidedStamp(params.isVoided)
+ ));
+ }
+
+ /// @notice Rotated "VOIDED" stamp overlaid on the certificate when it has been voided
+ function _buildVoidedStamp(bool voided) private pure returns (string memory) {
+ if (!voided) return "";
+ return string(abi.encodePacked(
+ '',
+ '',
+ 'VOIDED',
+ ''
+ ));
+ }
+
+ function _buildDefs() private pure returns (string memory) {
return string(abi.encodePacked(
'',
- '',
+ '',
'',
- '',
+ '',
'',
- '',
+ '',
'',
- '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ _buildDefsPart2()
+ ));
+ }
+
+ function _buildDefsPart2() private pure returns (string memory) {
+ return string(abi.encodePacked(
+ '',
+ '',
+ '',
'',
- '',
+ '',
'',
- '',
- _buildTextContent(params, securityType, unitType, day, month, year)
+ '',
+ '',
+ '',
+ '',
+ '',
+ ''
+ ));
+ }
+
+ function _buildHeaderText(CertificateSVGParams memory params) private pure returns (string memory) {
+ return string(abi.encodePacked(
+ '', params.corpName, '',
+ 'Token ID',
+ '#', _uintToString(params.tokenId), '',
+ '', _getBaseUnit(params.securityType), '',
+ '', _formatUnits(params), '',
+ params.isVoided
+ ? 'voided'
+ : 'active',
+ 'Ledger Entry Token'
+ ));
+ }
+
+ function _buildPartiesText(CertificateSVGParams memory params) private pure returns (string memory) {
+ return string(abi.encodePacked(
+ 'Issuer ',
+ '', params.corpName, ' ',
+ '', _truncatedAddress(params.issuerAddress), '',
+ 'Registered Owner ',
+ '', params.ownerName, ' ',
+ '', _truncatedAddress(params.ownerAddress), '',
+ 'Class',
+ 'Series',
+ '', _securityClassToString(params.securityType), ' ', _securitySeriesToString(params.securitySeries), ''
+ ));
+ }
+
+ function _buildStatsText(
+ CertificateSVGParams memory params,
+ string memory dateStr
+ ) private pure returns (string memory) {
+ return string(abi.encodePacked(
+ 'Units',
+ '', _formatUnits(params), '',
+ 'Consideration',
+ '', _formatConsideration(params), '',
+ 'Issue Date',
+ '', dateStr, ''
));
}
+ function _buildOfficerText(
+ CertificateSVGParams memory params,
+ string memory dateStr
+ ) private pure returns (string memory) {
+ return string(abi.encodePacked(
+ 'Authorizing Officer',
+ '', params.officerName, '',
+ 'block ', _uintToString(params.blockNumber), ' | ', dateStr, ''
+ ));
+ }
+
+ function _buildRestrictionsText(string[] memory restrictions) private pure returns (string memory) {
+ string memory result = 'Transfer Restrictions:';
+ for (uint256 i = 0; i < restrictions.length; i++) {
+ result = string(abi.encodePacked(
+ result,
+ '[', _uintToString(i + 1), '] ', restrictions[i], ' '
+ ));
+ }
+ return result;
+ }
+
+ // ------------------------------------------------------------------
+ // Formatting helpers
+ // ------------------------------------------------------------------
+
function _securityClassToString(SecurityClass _class) private pure returns (string memory) {
if (_class == SecurityClass.SAFE) return "SAFE";
if (_class == SecurityClass.SAFT) return "SAFT";
@@ -88,17 +202,18 @@ library CertificateImageContentBuilder {
return "Unknown";
}
- function _buildUnitType(SecurityClass _class, SecuritySeries _series) private pure returns (string memory) {
- string memory baseUnit = _getBaseUnit(_class);
-
- // For dollar-based securities, add series and class info
- if (_class == SecurityClass.SAFE || _class == SecurityClass.SAFT || _class == SecurityClass.SAFTE) {
- string memory seriesStr = _securitySeriesToString(_series);
- string memory classStr = _securityClassToString(_class);
- return string(abi.encodePacked("Dollars of the ", seriesStr, " ", classStr));
- }
-
- return baseUnit;
+ function _securitySeriesToString(SecuritySeries _series) private pure returns (string memory) {
+ if (_series == SecuritySeries.SeriesPreSeed) return "Pre-Seed";
+ if (_series == SecuritySeries.SeriesSeed) return "Series Seed";
+ if (_series == SecuritySeries.SeriesA) return "Series A";
+ if (_series == SecuritySeries.SeriesB) return "Series B";
+ if (_series == SecuritySeries.SeriesC) return "Series C";
+ if (_series == SecuritySeries.SeriesD) return "Series D";
+ if (_series == SecuritySeries.SeriesE) return "Series E";
+ if (_series == SecuritySeries.SeriesF) return "Series F";
+ if (_series == SecuritySeries.NA) return "";
+ if (_series == SecuritySeries.ACE) return "ACE";
+ return "";
}
function _getBaseUnit(SecurityClass _class) private pure returns (string memory) {
@@ -118,215 +233,146 @@ library CertificateImageContentBuilder {
return "Unknown";
}
- function _securitySeriesToString(SecuritySeries _series) private pure returns (string memory) {
- if (_series == SecuritySeries.SeriesPreSeed) return "Pre-Seed";
- if (_series == SecuritySeries.SeriesSeed) return "Series Seed";
- if (_series == SecuritySeries.SeriesA) return "Series A";
- if (_series == SecuritySeries.SeriesB) return "Series B";
- if (_series == SecuritySeries.SeriesC) return "Series C";
- if (_series == SecuritySeries.SeriesD) return "Series D";
- if (_series == SecuritySeries.SeriesE) return "Series E";
- if (_series == SecuritySeries.SeriesF) return "Series F";
- if (_series == SecuritySeries.NA) return "";
- if (_series == SecuritySeries.ACE) return "ACE";
- return "";
- }
-
- function _isDollarBased(SecurityClass _class) private pure returns (bool) {
- return _class == SecurityClass.SAFE || _class == SecurityClass.SAFT || _class == SecurityClass.SAFTE;
- }
-
+ /// @notice Convertible securities (SAFEs/SAFTs/SAFTEs etc.) represent a single instrument
function _isConvertible(SecurityClass _class) private pure returns (bool) {
- return _class == SecurityClass.SAFE ||
- _class == SecurityClass.SAFT ||
- _class == SecurityClass.SAFTE ||
+ return _class == SecurityClass.SAFE ||
+ _class == SecurityClass.SAFT ||
+ _class == SecurityClass.SAFTE ||
_class == SecurityClass.ConvertibleNote ||
_class == SecurityClass.TokenWarrant ||
_class == SecurityClass.TokenPurchaseAgreement;
}
- function _getSecurityFullName(SecurityClass _class) private pure returns (string memory) {
- if (_class == SecurityClass.SAFE) return "Simple Agreement for Future Equity";
- if (_class == SecurityClass.SAFT) return "Simple Agreement for Future Tokens";
- if (_class == SecurityClass.SAFTE) return "Simple Agreement for Future Tokens or Equity";
- if (_class == SecurityClass.TokenPurchaseAgreement) return "Token Purchase Agreement";
- if (_class == SecurityClass.TokenWarrant) return "Token Warrant";
- if (_class == SecurityClass.ConvertibleNote) return "Convertible Note";
- if (_class == SecurityClass.CommonStock) return "Common Stock";
- if (_class == SecurityClass.StockOption) return "Stock Option";
- if (_class == SecurityClass.PreferredStock) return "Preferred Stock";
- if (_class == SecurityClass.RestrictedStockPurchaseAgreement) return "Restricted Stock Purchase Agreement";
- if (_class == SecurityClass.RestrictedStockUnit) return "Restricted Stock Unit";
- if (_class == SecurityClass.RestrictedTokenPurchaseAgreement) return "Restricted Token Purchase Agreement";
- if (_class == SecurityClass.RestrictedTokenUnit) return "Restricted Token Unit";
- return "Unknown";
+ /// @notice Units displayed: unitsRepresented for shares/units, always 1 for convertibles
+ function _formatUnits(CertificateSVGParams memory params) private pure returns (string memory) {
+ if (_isConvertible(params.securityType)) {
+ return "1";
+ }
+ return _formatNumberWithCommas(params.units / 1e18);
+ }
+
+ /// @notice Consideration displayed: price per share for shares/units (e.g. "0.0000001/sh"),
+ /// total dollar amount for convertibles (e.g. "$100,000")
+ function _formatConsideration(CertificateSVGParams memory params) private pure returns (string memory) {
+ if (_isConvertible(params.securityType)) {
+ return string(abi.encodePacked("$", _formatDecimal18(params.consideration)));
+ }
+ if (params.units == 0) {
+ return string(abi.encodePacked(_formatDecimal18(params.consideration), "/sh"));
+ }
+ uint256 perUnit = (params.consideration * 1e18) / params.units;
+ return string(abi.encodePacked(_formatDecimal18(perUnit), "/sh"));
+ }
+
+ /// @notice Formats an 18-decimal value trimming trailing fractional zeros (max 9 decimals shown)
+ function _formatDecimal18(uint256 value) private pure returns (string memory) {
+ uint256 wholePart = value / 1e18;
+ // Keep at most 9 fractional digits
+ uint256 frac = (value % 1e18) / 1e9;
+ if (frac == 0) {
+ return _formatNumberWithCommas(wholePart);
+ }
+
+ bytes memory fracDigits = new bytes(9);
+ uint256 f = frac;
+ for (uint256 i = 9; i > 0; i--) {
+ fracDigits[i - 1] = bytes1(uint8(48 + (f % 10)));
+ f /= 10;
+ }
+ uint256 len = 9;
+ while (len > 0 && fracDigits[len - 1] == "0") {
+ len--;
+ }
+ bytes memory trimmed = new bytes(len);
+ for (uint256 i = 0; i < len; i++) {
+ trimmed[i] = fracDigits[i];
+ }
+ return string(abi.encodePacked(_formatNumberWithCommas(wholePart), ".", trimmed));
}
- function _getDateComponents(uint256 timestamp) private pure returns (string memory day, string memory month, string memory year) {
- // Convert timestamp to days since epoch
+ /// @notice Formats a timestamp as "M-D-YYYY" (e.g. "7-1-2026")
+ function _formatDate(uint256 timestamp) private pure returns (string memory) {
uint256 totalDays = timestamp / 86400;
-
- // Calculate year
+
uint256 y = 1970;
uint256 daysRemaining = totalDays;
-
while (daysRemaining >= (_isLeapYear(y) ? 366 : 365)) {
daysRemaining -= _isLeapYear(y) ? 366 : 365;
y++;
}
-
- // Days in each month (non-leap year)
+
uint8[12] memory daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (_isLeapYear(y)) {
- daysInMonth[1] = 29; // February has 29 days in leap year
+ daysInMonth[1] = 29;
}
-
- // Calculate month
+
uint256 m = 0;
while (m < 12 && daysRemaining >= daysInMonth[m]) {
daysRemaining -= daysInMonth[m];
m++;
}
-
- day = _uintToString(daysRemaining + 1);
- month = _getMonthName(m + 1);
- year = _uintToString(y);
+
+ return string(abi.encodePacked(
+ _uintToString(m + 1), "-", _uintToString(daysRemaining + 1), "-", _uintToString(y)
+ ));
}
function _isLeapYear(uint256 y) private pure returns (bool) {
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
- function _getMonthName(uint256 month) private pure returns (string memory) {
- if (month == 1) return "January";
- if (month == 2) return "February";
- if (month == 3) return "March";
- if (month == 4) return "April";
- if (month == 5) return "May";
- if (month == 6) return "June";
- if (month == 7) return "July";
- if (month == 8) return "August";
- if (month == 9) return "September";
- if (month == 10) return "October";
- if (month == 11) return "November";
- if (month == 12) return "December";
- return "Unknown";
+ /// @notice Truncates an address to "0x12345678...1234567890" form
+ function _truncatedAddress(address addr) private pure returns (string memory) {
+ bytes memory hexChars = "0123456789abcdef";
+ bytes memory full = new bytes(40);
+ uint160 value = uint160(addr);
+ for (uint256 i = 0; i < 20; i++) {
+ uint8 b = uint8(value >> (8 * (19 - i)));
+ full[i * 2] = hexChars[b >> 4];
+ full[i * 2 + 1] = hexChars[b & 0x0f];
+ }
+ bytes memory head = new bytes(8);
+ for (uint256 i = 0; i < 8; i++) {
+ head[i] = full[i];
+ }
+ bytes memory tail = new bytes(10);
+ for (uint256 i = 0; i < 10; i++) {
+ tail[i] = full[30 + i];
+ }
+ return string(abi.encodePacked("0x", head, "...", tail));
}
- function _buildTextContent(
- CertificateSVGParams memory params,
- string memory securityType,
- string memory unitType,
- string memory day,
- string memory month,
- string memory year
- ) private pure returns (string memory) {
- string memory formattedUnits = _uintToString(params.units);
- // Add $ prefix for dollar-based securities (SAFE, SAFT, SAFTE)
- if (_isDollarBased(params.securityType)) {
- formattedUnits = string(abi.encodePacked("$", formattedUnits));
- }
+ /// @notice Formats a number with commas as thousand separators (e.g. 2,500,000)
+ function _formatNumberWithCommas(uint256 _i) private pure returns (string memory) {
+ if (_i == 0) return "0";
- string memory middleSection;
- if (_isConvertible(params.securityType)) {
- middleSection = _buildMiddleConvertible(params, formattedUnits);
- } else {
- middleSection = _buildMiddleShares(params, unitType, formattedUnits);
+ uint256 j = _i;
+ uint256 digitCount;
+ while (j != 0) {
+ digitCount++;
+ j /= 10;
}
- return string(abi.encodePacked(
- _buildHeader(params, securityType, formattedUnits),
- middleSection,
- _buildFooter(params, day, month, year)
- ));
- }
+ uint256 commaCount = (digitCount - 1) / 3;
+ uint256 totalLength = digitCount + commaCount;
- function _buildHeader(
- CertificateSVGParams memory params,
- string memory securityType,
- string memory formattedUnits
- ) private pure returns (string memory) {
- return string(abi.encodePacked(
- '', params.corpName, '',
- 'Token ID',
- '#', _uintToString(params.tokenId), '',
- '', _getBaseUnit(params.securityType), '',
- '', formattedUnits, '',
- '', _securitySeriesToString(params.securitySeries), ' ', securityType, ''
- ));
- }
+ bytes memory result = new bytes(totalLength);
+ uint256 pos = totalLength;
+ uint256 digitsSinceComma = 0;
- function _buildMiddleConvertible(
- CertificateSVGParams memory params,
- string memory formattedUnits
- ) private pure returns (string memory) {
- string memory securityFullName = _getSecurityFullName(params.securityType);
- return string(abi.encodePacked(
- 'This Certifies that ',
- '', params.ownerName, '',
- '',
- '',
- 'is the registered holder of',
- '1',
- '', securityFullName, '',
- '',
- 'of',
- '', params.corpName, '',
- '',
- 'purchased from the said Entity for',
- '', formattedUnits, '',
- '',
- 'and transferable only in ',
- 'accordance with the terms and conditions thereof and any other applicable agreements',
- ' between or involving or applicable to the said Entity and the said registered Holder.'
- ));
- }
-
- function _buildMiddleShares(
- CertificateSVGParams memory params,
- string memory unitType,
- string memory formattedUnits
- ) private pure returns (string memory) {
- return string(abi.encodePacked(
- 'This Certifies That',
- '',
- '', params.ownerName, '',
- 'is the registered holder of',
- '', formattedUnits, '',
- '',
- '', unitType, '',
- '',
- '', params.corpName, '',
- '',
- 'of',
- 'transferable only on the books of the Corporation by the holder hereof in person or by',
- 'Attorney upon surrender of this Certificate properly endorsed.'
- ));
- }
+ while (_i != 0) {
+ if (digitsSinceComma == 3) {
+ pos--;
+ result[pos] = ",";
+ digitsSinceComma = 0;
+ }
+ pos--;
+ result[pos] = bytes1(uint8(48 + (_i % 10)));
+ _i /= 10;
+ digitsSinceComma++;
+ }
- function _buildFooter(
- CertificateSVGParams memory params,
- string memory day,
- string memory month,
- string memory year
- ) private pure returns (string memory) {
- return string(abi.encodePacked(
- 'In Witness Whereof',
- ', the said Entity has caused this Certificate to be signed by its duly',
- 'authorized officer(s)',
- 'This',
- '', day, '',
- '',
- 'day of',
- '', month, '',
- '',
- 'A.D.',
- '', year, '',
- '',
- '', params.officerName, '',
- '', params.officerTitle, '',
- 'Link to full certificate: ', params.certificateUri, ''
- ));
+ return string(result);
}
function _uintToString(uint256 _i) private pure returns (string memory) {
diff --git a/src/CertificateUriBuilder.sol b/src/CertificateUriBuilder.sol
index b781d8a..7889c5c 100644
--- a/src/CertificateUriBuilder.sol
+++ b/src/CertificateUriBuilder.sol
@@ -47,6 +47,7 @@ import "./interfaces/ICyberAgreementRegistry.sol";
import "./interfaces/ICertificateImageBuilder.sol";
import {RestrictionType, RestrictiveLegend} from "./interfaces/ICyberCertPrinter.sol";
import "./storage/extensions/ICertificateExtension.sol";
+import "./interfaces/ICyberCertPrinter.sol";
import "./libs/auth.sol";
interface ICertificateUnitsReserved {
@@ -157,6 +158,18 @@ contract CertificateUriBuilder is UUPSUpgradeable, BorgAuthACL {
}
}
+ function restrictiveLegendsToStringArray(
+ RestrictiveLegend[] memory legends
+ ) public pure returns (string[] memory texts) {
+ texts = new string[](legends.length);
+ for (uint256 i = 0; i < legends.length; i++) {
+ texts[i] = legends[i].text;
+ if (bytes(texts[i]).length == 0) {
+ texts[i] = legends[i].title;
+ }
+ }
+ }
+
function restrictiveLegendsToJson(RestrictiveLegend[] memory arr) public pure returns (string memory) {
string memory json = "[";
for (uint256 i = 0; i < arr.length; i++) {
@@ -518,7 +531,7 @@ struct CertificateDetails {
address contractAddress,
address extension
) public view returns (string memory) {
- return buildCertificateUri(
+ string memory json = buildCertificateUriNotEncoded(
cyberCORPName,
cyberCORPType,
cyberCORPJurisdiction,
@@ -526,7 +539,7 @@ struct CertificateDetails {
securityType,
securitySeries,
certificateUri,
- legacyLegendsToRestrictiveLegends(certLegend),
+ certLegend,
details,
endorsements,
owner,
@@ -536,106 +549,8 @@ struct CertificateDetails {
contractAddress,
extension
);
- }
-
- function buildCertificateUri(
- string memory cyberCORPName,
- string memory cyberCORPType,
- string memory cyberCORPJurisdiction,
- string memory cyberCORPContactDetails,
- SecurityClass securityType,
- SecuritySeries securitySeries,
- string memory certificateUri,
- RestrictiveLegend[] memory certLegend,
- CertificateDetails memory details,
- Endorsement[] memory endorsements,
- OwnerDetails memory owner,
- address registry,
- bytes32 agreementId,
- uint256 tokenId,
- address contractAddress,
- address extension
- ) public view returns (string memory) {
- // Start building the JSON string with ERC-721 metadata standard format
- // Build on-chain SVG image using the image builder
-
- // Fetch timestamp from registry in scoped block to reduce stack pressure
- uint256 certTimestamp = _getAgreementTimestamp(registry, agreementId);
-
- CertificateSVGParams memory svgParams = CertificateSVGParams({
- corpName: cyberCORPName,
- securityType: securityType,
- securitySeries: securitySeries,
- officerName: details.signingOfficerName,
- officerTitle: details.signingOfficerTitle,
- units: details.unitsRepresented,
- valuation: details.issuerUSDValuationAtTimeOfInvestment,
- jurisdiction: cyberCORPJurisdiction,
- ownerName: owner.name,
- tokenId: tokenId,
- certificateUri: certificateUri
- });
-
- string memory svg = ICertificateImageBuilder(imageBuilder).buildCertificateSVG(svgParams, certTimestamp);
- string memory imageDataUri = string(abi.encodePacked('data:image/svg+xml;base64,', Base64.encode(bytes(svg))));
-
- string memory json = string(abi.encodePacked(
- '{"title": "MetaLeX Tokenized Certificate",',
- '"type": "', securityClassToString(securityType),
- '", "image": "', imageDataUri, '",',
- '"attributes": [', buildAttributes(owner, details, cyberCORPName, cyberCORPType, cyberCORPJurisdiction, cyberCORPContactDetails, securityType, securitySeries, certificateUri),
- '],'
- ));
-
- // Add all existing properties at root level
- json = string.concat(
- json,
- '"cyberCORPName": "', cyberCORPName,
- '", "cyberCORPType": "', cyberCORPType,
- '", "cyberCORPJurisdiction": "', cyberCORPJurisdiction,
- '", "cyberCORPContactDetails": "', cyberCORPContactDetails,
- '", "securityType": "', securityClassToString(securityType),
- '", "securitySeries": "', securitySeriesToString(securitySeries),
- '", "certificateUri": "', certificateUri,
- '"'
- );
-
- // Add certificate details
- json = string.concat(json,
- ', "signingOfficerName": "', details.signingOfficerName,
- '", "signingOfficerTitle": "', details.signingOfficerTitle,
- '", "investmentAmountUSD": "', from18DecimalsToString(details.investmentAmountUSD),
- '", "issuerUSDValuationAtTimeOfInvestment": "', from18DecimalsToString(details.issuerUSDValuationAtTimeOfInvestment),
- '", "unitsRepresented": "', from18DecimalsToString(details.unitsRepresented),
- '", "unitsReserved": "', unitsReservedToString(contractAddress, tokenId),
- '", "legalDetails": "', details.legalDetails,
- '"'
- );
-
- //add extensionData
- if (extension != address(0) && details.extensionData.length > 0) {
- json = string.concat(json, ICertificateExtension(extension).getExtensionURI(details.extensionData));
- }
-
- // Add endorsement history
- json = string.concat(json, ', "endorsementHistory": ', buildEndorsementHistory(endorsements, registry, agreementId));
-
- // Add current owner details
- json = string.concat(json,
- ', "currentOwner": {',
- '"name": "', owner.name,
- '", "ownerAddress": "', addressToString(owner.ownerAddress),
- '"}'
- );
-
- // Add restrictive legends at the end
- json = string.concat(json, ', "restrictiveLegends": ', restrictiveLegendsToJson(certLegend));
-
- // Close the main JSON object
- json = string.concat(json, '}');
- json = Base64.encode(bytes(string(json)));
- json = string(abi.encodePacked('data:application/json;base64,', json));
- return json;
+ json = Base64.encode(bytes(json));
+ return string(abi.encodePacked('data:application/json;base64,', json));
}
function buildCertificateUriNotEncoded(
@@ -696,7 +611,7 @@ struct CertificateDetails {
) public view returns (string memory) {
// Start building the JSON string with ERC-721 metadata standard format
// Build on-chain SVG image using the image builder
-
+
// Fetch timestamp from registry in scoped block to reduce stack pressure
uint256 certTimestamp = _getAgreementTimestamp(registry, agreementId);
@@ -711,7 +626,13 @@ struct CertificateDetails {
jurisdiction: cyberCORPJurisdiction,
ownerName: owner.name,
tokenId: tokenId,
- certificateUri: certificateUri
+ certificateUri: certificateUri,
+ issuerAddress: contractAddress,
+ ownerAddress: owner.ownerAddress,
+ consideration: details.investmentAmountUSD,
+ blockNumber: block.number,
+ transferRestrictions: restrictiveLegendsToStringArray(certLegend),
+ isVoided: _isCertVoided(contractAddress, tokenId)
});
string memory svg = ICertificateImageBuilder(imageBuilder).buildCertificateSVG(svgParams, certTimestamp);
@@ -738,6 +659,7 @@ struct CertificateDetails {
'"'
);
+
// Add certificate details
json = string.concat(json,
', "signingOfficerName": "', details.signingOfficerName,
@@ -751,8 +673,11 @@ struct CertificateDetails {
);
//add extensionData
- if (extension != address(0) && details.extensionData.length > 0) {
- json = string.concat(json, ICertificateExtension(extension).getExtensionURI(details.extensionData));
+ if (extension != address(0)) {
+ bytes memory printerExtensionData = _getPrinterExtensionData(contractAddress);
+ if (details.extensionData.length > 0 || printerExtensionData.length > 0) {
+ json = string.concat(json, _getExtensionURI(extension, printerExtensionData, details.extensionData));
+ }
}
// Add endorsement history
@@ -771,11 +696,51 @@ struct CertificateDetails {
// Close the main JSON object
json = string.concat(json, '}');
- //json = Base64.encode(bytes(string(json)));
- //json = string(abi.encodePacked('data:application/json;base64,', json));
return json;
}
+ /// @notice Checks whether the certificate has been voided on the printer contract
+ function _isCertVoided(address certPrinter, uint256 tokenId) internal view returns (bool) {
+ if (certPrinter == address(0)) return false;
+ try ICyberCertPrinter(certPrinter).isVoided(tokenId) returns (bool voided) {
+ return voided;
+ } catch {
+ return false;
+ }
+ }
+
+ function _getPrinterExtensionData(address certPrinter) internal view returns (bytes memory) {
+ if (certPrinter == address(0)) return "";
+ try ICyberCertPrinter(certPrinter).getPrinterExtensionData() returns (bytes memory data) {
+ return data;
+ } catch {
+ return "";
+ }
+ }
+
+ function _getExtensionURI(
+ address extension,
+ bytes memory printerExtensionData,
+ bytes memory certificateExtensionData
+ ) internal view returns (string memory) {
+ (bool success, bytes memory result) = extension.staticcall(
+ abi.encodeWithSelector(
+ ICertificateExtension.getExtensionURI.selector,
+ printerExtensionData,
+ certificateExtensionData
+ )
+ );
+
+ if (success) {
+ return abi.decode(result, (string));
+ }
+
+ if (certificateExtensionData.length == 0) return "";
+
+ // Fall back to legacy one-blob extensions without certPrinter data.
+ return ILegacyCertificateExtension(extension).getExtensionURI(certificateExtensionData);
+ }
+
function _authorizeUpgrade(
address newImplementation
) internal virtual override onlyOwner {}
diff --git a/src/CyberCertPrinter.sol b/src/CyberCertPrinter.sol
index 6f1905d..b7e7800 100644
--- a/src/CyberCertPrinter.sol
+++ b/src/CyberCertPrinter.sol
@@ -98,6 +98,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
event RestrictionHookSet(uint256 indexed id, address indexed hookAddress);
event GlobalRestrictionHookSet(address indexed hookAddress);
event GlobalTransferableSet(bool indexed transferable);
+ event PrinterExtensionDataSet();
// Emitted from the storage library via delegatecall; declared here for the ABI
event UnitsReservedUpdated(uint256 indexed tokenId, uint256 unitsReserved);
@@ -112,7 +113,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
}
// Called by proxy on deployment (if needed)
- function initialize(string[] memory _defaultLegend, string memory name, string memory ticker, string memory _certificateUri, address _issuanceManager, SecurityClass _securityType, SecuritySeries _securitySeries, address _extension) external initializer {
+ function initialize(string[] memory _defaultLegend, string memory name, string memory ticker, string memory _certificateUri, address _issuanceManager, SecurityClass _securityType, SecuritySeries _securitySeries, address _extension, bytes memory _printerExtensionData) external initializer {
__ERC721_init(name, ticker);
__ERC721Enumerable_init_unchained();
@@ -124,6 +125,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
s.certificateUri = _certificateUri;
s.endorsementRequired = true;
s.extension = _extension;
+ s.printerExtensionData = _printerExtensionData;
}
function updateIssuanceManager(address _issuanceManager) external onlyIssuanceManager {
@@ -487,6 +489,15 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
return CyberCertPrinterStorage._getExtensionData(tokenId);
}
+ function setPrinterExtensionData(bytes calldata data) external onlyIssuanceManager {
+ CyberCertPrinterStorage.cyberCertStorage().printerExtensionData = data;
+ emit PrinterExtensionDataSet();
+ }
+
+ function getPrinterExtensionData() external view returns (bytes memory) {
+ return CyberCertPrinterStorage.cyberCertStorage().printerExtensionData;
+ }
+
function setExtension(uint256 tokenId, address extension) external onlyIssuanceManager {
CyberCertPrinterStorage.cyberCertStorage().extension = extension;
}
diff --git a/src/CyberCorpConstants.sol b/src/CyberCorpConstants.sol
index 534ce81..5c7ac6d 100644
--- a/src/CyberCorpConstants.sol
+++ b/src/CyberCorpConstants.sol
@@ -120,6 +120,12 @@ struct CertificateSVGParams {
string ownerName;
uint256 tokenId;
string certificateUri;
+ address issuerAddress;
+ address ownerAddress;
+ uint256 consideration;
+ uint256 blockNumber;
+ string[] transferRestrictions;
+ bool isVoided;
}
diff --git a/src/CyberCorpFactory.sol b/src/CyberCorpFactory.sol
index d8843f5..de074e1 100644
--- a/src/CyberCorpFactory.sol
+++ b/src/CyberCorpFactory.sol
@@ -104,6 +104,7 @@ contract CyberCorpFactory is UUPSUpgradeable, BorgAuthACL {
SecuritySeries securitySeries;
address extension;
string[] defaultLegend;
+ bytes printerExtensionData;
}
event CyberCorpDeployed(
@@ -358,7 +359,8 @@ contract CyberCorpFactory is UUPSUpgradeable, BorgAuthACL {
_certData[i].uri,
_certData[i].securityClass,
_certData[i].securitySeries,
- _certData[i].extension
+ _certData[i].extension,
+ _certData[i].printerExtensionData
)
);
certPrinterAddress[i] = address(certPrinter);
diff --git a/src/DealManager.sol b/src/DealManager.sol
index f7b8f19..661d968 100644
--- a/src/DealManager.sol
+++ b/src/DealManager.sol
@@ -425,7 +425,8 @@ contract DealManager is
_certData[i].uri,
_certData[i].securityClass,
_certData[i].securitySeries,
- _certData[i].extension
+ _certData[i].extension,
+ _certData[i].printerExtensionData
);
}
}
diff --git a/src/IssuanceManager.sol b/src/IssuanceManager.sol
index 0435af0..c70b903 100644
--- a/src/IssuanceManager.sol
+++ b/src/IssuanceManager.sol
@@ -110,6 +110,8 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable {
uint256 indexed id,
bool isWhitelisted
);
+ event CertPrinterExtensionDataSet(address indexed certPrinter);
+
event CyberScripDeployed(
address indexed certPrinterAddress,
address indexed cyberScripAddress,
@@ -216,7 +218,8 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable {
string memory _certificateUri,
SecurityClass _securityType,
SecuritySeries _securitySeries,
- address _extension
+ address _extension,
+ bytes memory _printerExtensionData
) public onlyOwner returns (address) {
return
IssuanceManagerStorage.executeCreateCertPrinter(
@@ -226,7 +229,8 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable {
_certificateUri,
_securityType,
_securitySeries,
- _extension
+ _extension,
+ _printerExtensionData
);
}
diff --git a/src/PumpCorpFactory.sol b/src/PumpCorpFactory.sol
index 42b4952..f3d232f 100644
--- a/src/PumpCorpFactory.sol
+++ b/src/PumpCorpFactory.sol
@@ -163,6 +163,7 @@ contract PumpCorpFactory is UUPSUpgradeable, BorgAuthACL {
SecuritySeries securitySeries;
address extension;
string[] defaultLegend;
+ bytes printerExtensionData;
}
event CyberCorpDeployed(
@@ -473,7 +474,8 @@ contract PumpCorpFactory is UUPSUpgradeable, BorgAuthACL {
_certData[i].uri,
_certData[i].securityClass,
_certData[i].securitySeries,
- _certData[i].extension
+ _certData[i].extension,
+ _certData[i].printerExtensionData
)
);
certPrinterAddress[i] = address(certPrinter);
diff --git a/src/interfaces/ICyberCertPrinter.sol b/src/interfaces/ICyberCertPrinter.sol
index 3ff3130..4356d7f 100644
--- a/src/interfaces/ICyberCertPrinter.sol
+++ b/src/interfaces/ICyberCertPrinter.sol
@@ -100,7 +100,8 @@ interface ICyberCertPrinter is IERC721 {
address _issuanceManager,
SecurityClass _securityType,
SecuritySeries _securitySeries,
- address _extension
+ address _extension,
+ bytes memory _printerExtensionData
) external;
function name() external view returns (string memory);
function symbol() external view returns (string memory);
@@ -193,6 +194,7 @@ interface ICyberCertPrinter is IERC721 {
address endorsee
);
function tokenURI(uint256 tokenId) external view returns (string memory);
+ function getPrinterExtensionData() external view returns (bytes memory);
function certificateUri() external view returns (string memory);
function holderCount() external view returns (uint256);
function totalSupply() external view returns (uint256);
diff --git a/src/interfaces/IIssuanceManager.sol b/src/interfaces/IIssuanceManager.sol
index be45adf..bb7b9e6 100644
--- a/src/interfaces/IIssuanceManager.sol
+++ b/src/interfaces/IIssuanceManager.sol
@@ -115,7 +115,8 @@ interface IIssuanceManager {
string memory _certificateUri,
SecurityClass _securityType,
SecuritySeries _securitySeries,
- address _extension
+ address _extension,
+ bytes memory _printerExtensionData
) external returns (address);
function createCert(
@@ -317,6 +318,8 @@ interface IIssuanceManager {
uint256 minimum
) external;
+ function setCertPrinterExtensionData(address certPrinter, bytes calldata data) external;
+
function getScripToCertMinimum(
address certAddress
) external view returns (uint256);
diff --git a/src/interfaces/IUriBuilder.sol b/src/interfaces/IUriBuilder.sol
index 6d00354..0a8b4c2 100644
--- a/src/interfaces/IUriBuilder.sol
+++ b/src/interfaces/IUriBuilder.sol
@@ -126,3 +126,4 @@ interface IUriBuilder {
address extension
) external view returns (string memory);
}
+
diff --git a/src/storage/CyberCertPrinterStorage.sol b/src/storage/CyberCertPrinterStorage.sol
index ca8d8dc..b37dca4 100644
--- a/src/storage/CyberCertPrinterStorage.sol
+++ b/src/storage/CyberCertPrinterStorage.sol
@@ -107,6 +107,7 @@ library CyberCertPrinterStorage {
// New variables must be appended below to preserve storage layout for upgrades
mapping(uint256 => bool) tokenTransferable;
mapping(uint256 => bytes[]) issuerSignatures;
+ bytes printerExtensionData;
// Units locked in a pending deal/loan; always <= certificateDetails[tokenId].unitsRepresented
mapping(uint256 => uint256) unitsReserved;
mapping(uint256 => RestrictiveLegend[]) certLegendsV2;
@@ -134,9 +135,7 @@ library CyberCertPrinterStorage {
CyberCertPrinterStorage.CyberCertStorage storage s = cyberCertStorage();
RestrictiveLegend[] memory certLegend = getEffectiveRestrictiveLegends(tokenId);
ICyberCorp corp = ICyberCorp(IIssuanceManager(s.issuanceManager).CORP());
- CertificateDetails memory effectiveDetails = getCertificateDetails(
- tokenId
- );
+ CertificateDetails memory effectiveDetails = getCertificateDetails(tokenId);
// Get registry and agreementId from first endorsement if it exists
address registry = address(0);
@@ -592,4 +591,12 @@ library CyberCertPrinterStorage {
return cyberCertStorage().certificateDetails[tokenId].extensionData;
}
+ function setPrinterExtensionData(bytes memory data) internal {
+ cyberCertStorage().printerExtensionData = data;
+ }
+
+ function getPrinterExtensionData() internal view returns (bytes memory) {
+ return cyberCertStorage().printerExtensionData;
+ }
+
}
\ No newline at end of file
diff --git a/src/storage/DealManagerStorage.sol b/src/storage/DealManagerStorage.sol
index 9b81898..24a2c3b 100644
--- a/src/storage/DealManagerStorage.sol
+++ b/src/storage/DealManagerStorage.sol
@@ -80,6 +80,7 @@ library DealManagerStorage {
SecuritySeries securitySeries;
address extension;
string[] defaultLegend;
+ bytes printerExtensionData;
}
/// @notice Main storage layout struct that holds all deal manager data
diff --git a/src/storage/IssuanceManagerStorage.sol b/src/storage/IssuanceManagerStorage.sol
index 05a370a..90b9bb1 100644
--- a/src/storage/IssuanceManagerStorage.sol
+++ b/src/storage/IssuanceManagerStorage.sol
@@ -576,7 +576,8 @@ library IssuanceManagerStorage {
string memory certificateUri,
SecurityClass securityType,
SecuritySeries securitySeries,
- address extension
+ address extension,
+ bytes memory printerExtensionData
) external returns (address newCert) {
bytes32 salt = keccak256(abi.encodePacked(getPrinters().length, address(this)));
newCert = Create2.deploy(0, salt, _getBytecodeCertPrinter());
@@ -589,7 +590,8 @@ library IssuanceManagerStorage {
address(this),
securityType,
securitySeries,
- extension
+ extension,
+ printerExtensionData
);
emit CertPrinterCreated(
newCert,
diff --git a/src/storage/RoundManagerStorage.sol b/src/storage/RoundManagerStorage.sol
index afcae0c..f10607c 100644
--- a/src/storage/RoundManagerStorage.sol
+++ b/src/storage/RoundManagerStorage.sol
@@ -76,6 +76,7 @@ struct CyberCertData {
SecuritySeries securitySeries;
address extension;
string[] defaultLegend;
+ bytes printerExtensionData;
}
struct EOI {
@@ -174,7 +175,8 @@ library RoundManagerStorage {
certData[i].uri,
certData[i].securityClass,
certData[i].securitySeries,
- certData[i].extension
+ certData[i].extension,
+ certData[i].printerExtensionData
)
);
certPrinterAddresses[i] = address(certPrinter);
diff --git a/src/storage/extensions/ACESAFEExtension.sol b/src/storage/extensions/ACESAFEExtension.sol
index 161afab..aac0116 100644
--- a/src/storage/extensions/ACESAFEExtension.sol
+++ b/src/storage/extensions/ACESAFEExtension.sol
@@ -52,7 +52,7 @@ struct ACESAFEData {
}
-contract ACESAFEExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract ACESAFEExtension is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("ACE_SAFE");
//ofset to leave for future upgrades
diff --git a/src/storage/extensions/FundInterestExtension.sol b/src/storage/extensions/FundInterestExtension.sol
index e4743a9..dc5ff62 100644
--- a/src/storage/extensions/FundInterestExtension.sol
+++ b/src/storage/extensions/FundInterestExtension.sol
@@ -64,8 +64,11 @@ contract FundInterestExtension is UUPSUpgradeable, ICertificateExtension, BorgAu
return extensionType == EXTENSION_TYPE;
}
- function getExtensionURI(bytes memory data) external view override returns (string memory) {
- FundInterestData memory decoded = abi.decode(data, (FundInterestData));
+ function getExtensionURI(
+ bytes memory /* printerExtensionData */,
+ bytes memory certificateExtensionData
+ ) external view override returns (string memory) {
+ FundInterestData memory decoded = abi.decode(certificateExtensionData, (FundInterestData));
return string(
abi.encodePacked(
diff --git a/src/storage/extensions/ICertificateExtension.sol b/src/storage/extensions/ICertificateExtension.sol
index 3e158cf..35747cd 100644
--- a/src/storage/extensions/ICertificateExtension.sol
+++ b/src/storage/extensions/ICertificateExtension.sol
@@ -41,7 +41,16 @@ except with the express prior written permission of the copyright holder.*/
pragma solidity 0.8.28;
-interface ICertificateExtension {
+interface ILegacyCertificateExtension {
function supportsExtensionType(bytes32 extensionType) external pure returns (bool);
function getExtensionURI(bytes memory data) external view returns (string memory);
+}
+
+interface ICertificateExtension {
+ function supportsExtensionType(bytes32 extensionType) external pure returns (bool);
+
+ function getExtensionURI(
+ bytes memory printerExtensionData,
+ bytes memory certificateExtensionData
+ ) external view returns (string memory);
}
\ No newline at end of file
diff --git a/src/storage/extensions/SAFEExtension.sol b/src/storage/extensions/SAFEExtension.sol
index 203ded6..f9e4323 100644
--- a/src/storage/extensions/SAFEExtension.sol
+++ b/src/storage/extensions/SAFEExtension.sol
@@ -49,7 +49,7 @@ struct SAFEData {
string customProvisions;
}
-contract SAFEExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract SAFEExtension is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("SAFE");
//ofset to leave for future upgrades
diff --git a/src/storage/extensions/SAFTEExtension.sol b/src/storage/extensions/SAFTEExtension.sol
index 71659de..5de4b17 100644
--- a/src/storage/extensions/SAFTEExtension.sol
+++ b/src/storage/extensions/SAFTEExtension.sol
@@ -59,7 +59,7 @@ struct SAFTEData {
uint256 protocolUSDValuationAtTimeofInvestment;
}
-contract SAFTEExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract SAFTEExtension is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("SAFTE");
uint256 public constant PERCENTAGE_PRECISION = 10 ** 4;
diff --git a/src/storage/extensions/SAFTEExtensionV2.sol b/src/storage/extensions/SAFTEExtensionV2.sol
index 57bb18b..f0be613 100644
--- a/src/storage/extensions/SAFTEExtensionV2.sol
+++ b/src/storage/extensions/SAFTEExtensionV2.sol
@@ -60,7 +60,7 @@ struct SAFTEDataV2 {
string customProvisions;
}
-contract SAFTEExtensionV2 is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract SAFTEExtensionV2 is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("SAFTE_V2");
uint256 public constant PERCENTAGE_PRECISION = 10 ** 4;
diff --git a/src/storage/extensions/SAFTExtension.sol b/src/storage/extensions/SAFTExtension.sol
index 032afb6..12826ec 100644
--- a/src/storage/extensions/SAFTExtension.sol
+++ b/src/storage/extensions/SAFTExtension.sol
@@ -55,7 +55,7 @@ struct SAFTData {
UnlockingIntervalType unlockingIntervalType;
}
-contract SAFTExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract SAFTExtension is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("SAFT");
uint256 public constant PERCENTAGE_PRECISION = 10 ** 4;
diff --git a/src/storage/extensions/SAFTExtensionV2.sol b/src/storage/extensions/SAFTExtensionV2.sol
index 33ba19b..2069406 100644
--- a/src/storage/extensions/SAFTExtensionV2.sol
+++ b/src/storage/extensions/SAFTExtensionV2.sol
@@ -56,7 +56,7 @@ struct SAFTDataV2 {
string customProvisions;
}
-contract SAFTExtensionV2 is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract SAFTExtensionV2 is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("SAFT_V2");
uint256 public constant PERCENTAGE_PRECISION = 10 ** 4;
diff --git a/src/storage/extensions/ShareExtension.sol b/src/storage/extensions/ShareExtension.sol
index 30be681..9b71475 100644
--- a/src/storage/extensions/ShareExtension.sol
+++ b/src/storage/extensions/ShareExtension.sol
@@ -247,14 +247,31 @@ contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
return abi.encode(data);
}
- function supportsExtensionType(bytes32 extensionType) external pure override returns (bool) {
+ function decodeCertificateExtensionData(
+ bytes memory data
+ ) external pure returns (CertificateData memory) {
+ return abi.decode(data, (CertificateData));
+ }
+
+ function encodeCertificateExtensionData(
+ CertificateData memory data
+ ) external pure returns (bytes memory) {
+ return abi.encode(data);
+ }
+
+ function supportsExtensionType(
+ bytes32 extensionType
+ ) external pure override returns (bool) {
return extensionType == EXTENSION_TYPE;
}
- function getExtensionURI(bytes memory data) external pure override returns (string memory) {
- if (data.length == 0) return "";
+ function getExtensionURI(
+ bytes memory /* printerExtensionData */,
+ bytes memory certificateExtensionData
+ ) external pure override returns (string memory) {
+ if (certificateExtensionData.length == 0) return "";
- ShareCertData memory share = abi.decode(data, (ShareCertData));
+ ShareCertData memory share = abi.decode(certificateExtensionData, (ShareCertData));
return string(
abi.encodePacked(
diff --git a/src/storage/extensions/TokenWarrantExtension.sol b/src/storage/extensions/TokenWarrantExtension.sol
index e94aadd..9ba12b3 100644
--- a/src/storage/extensions/TokenWarrantExtension.sol
+++ b/src/storage/extensions/TokenWarrantExtension.sol
@@ -61,7 +61,7 @@ struct TokenWarrantData {
uint256 tokenPremiumMultiplier; //multiplier of network valuation over company equity valuation, to be used within equityProRataToTokenSupply method (set to 0 if no premium)
}
-contract TokenWarrantExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract TokenWarrantExtension is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("TOKEN_WARRANT");
uint256 public constant PERCENTAGE_PRECISION = 10 ** 4;
diff --git a/src/storage/extensions/TokenWarrantExtensionV2.sol b/src/storage/extensions/TokenWarrantExtensionV2.sol
index 0c2ef2e..042ff2c 100644
--- a/src/storage/extensions/TokenWarrantExtensionV2.sol
+++ b/src/storage/extensions/TokenWarrantExtensionV2.sol
@@ -62,7 +62,7 @@ struct TokenWarrantDataV2 {
string customProvisions;
}
-contract TokenWarrantExtensionV2 is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
+contract TokenWarrantExtensionV2 is UUPSUpgradeable, ILegacyCertificateExtension, BorgAuthACL {
bytes32 public constant EXTENSION_TYPE = keccak256("TOKEN_WARRANT_V2");
uint256 public constant PERCENTAGE_PRECISION = 10 ** 4;
diff --git a/test/CertificateExtensionDataTest.t.sol b/test/CertificateExtensionDataTest.t.sol
new file mode 100644
index 0000000..3fbd10d
--- /dev/null
+++ b/test/CertificateExtensionDataTest.t.sol
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import "forge-std/Test.sol";
+import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol";
+
+import {BorgAuth} from "../src/libs/auth.sol";
+import {CertificateImageBuilderContract} from "../src/CertificateImageBuilderContract.sol";
+import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol";
+import {SecurityClass, SecuritySeries} from "../src/CyberCorpConstants.sol";
+
+contract MockPrinterExtensionData {
+ bytes internal data;
+
+ function setPrinterExtensionData(bytes memory newData) external {
+ data = newData;
+ }
+
+ function getPrinterExtensionData() external view returns (bytes memory) {
+ return data;
+ }
+}
+
+contract MockLegacyCertificateExtension {
+ function getExtensionURI(bytes memory certificateExtensionData) external pure returns (string memory) {
+ if (certificateExtensionData.length == 0) return "";
+ return string.concat(', "legacyCertificateData": "', abi.decode(certificateExtensionData, (string)), '"');
+ }
+}
+
+contract MockV2CertificateExtension {
+ function getExtensionURI(bytes memory certificateExtensionData) external pure returns (string memory) {
+ if (certificateExtensionData.length == 0) return "";
+ return string.concat(', "legacyCertificateData": "', abi.decode(certificateExtensionData, (string)), '"');
+ }
+
+ function getExtensionURI(
+ bytes memory printerExtensionData,
+ bytes memory certificateExtensionData
+ ) external pure returns (string memory) {
+ string memory shared = printerExtensionData.length == 0 ? "" : abi.decode(printerExtensionData, (string));
+ string memory certificate = certificateExtensionData.length == 0 ? "" : abi.decode(certificateExtensionData, (string));
+ return string.concat(
+ ', "sharedFields": {',
+ '"series": "', shared,
+ '", "certificate": "', certificate,
+ '"}'
+ );
+ }
+}
+
+contract CertificateExtensionDataTest is Test {
+ CertificateUriBuilder internal uriBuilder;
+ MockPrinterExtensionData internal printer;
+ MockLegacyCertificateExtension internal legacyExtension;
+ MockV2CertificateExtension internal v2Extension;
+
+ function setUp() public {
+ BorgAuth auth = new BorgAuth(address(this));
+ uriBuilder = CertificateUriBuilder(
+ address(
+ new ERC1967Proxy(
+ address(new CertificateUriBuilder()),
+ abi.encodeWithSelector(CertificateUriBuilder.initialize.selector, address(auth))
+ )
+ )
+ );
+ uriBuilder.setImageBuilder(address(new CertificateImageBuilderContract()));
+
+ printer = new MockPrinterExtensionData();
+ legacyExtension = new MockLegacyCertificateExtension();
+ v2Extension = new MockV2CertificateExtension();
+ }
+
+ function testLegacyExtensionUriUnchangedWhenPrinterDataIsSetButExtensionNotUpgraded() public {
+ CertificateUriBuilder.CertificateDetails memory details = _details("certificate-only-data");
+
+ string memory beforePrinterData = _json(details, address(legacyExtension), address(printer));
+
+ printer.setPrinterExtensionData(abi.encode("shared data unavailable to legacy extension"));
+
+ assertEq(_json(details, address(legacyExtension), address(printer)), beforePrinterData);
+ }
+
+ function testV2ExtensionUriIncludesSharedPrinterAndCertificateFields() public {
+ printer.setPrinterExtensionData(abi.encode("Series A shared terms"));
+
+ string memory json = _json(_details("Certificate 1001 terms"), address(v2Extension), address(printer));
+
+ assertTrue(_contains(json, '"sharedFields": {'));
+ assertTrue(_contains(json, '"series": "Series A shared terms"'));
+ assertTrue(_contains(json, '"certificate": "Certificate 1001 terms"'));
+ }
+
+ function testV2ExtensionUriIncludesSharedFieldsWhenCertificateDataIsEmpty() public {
+ printer.setPrinterExtensionData(abi.encode("Series A shared terms"));
+
+ string memory json = _json(_detailsBytes(hex""), address(v2Extension), address(printer));
+
+ assertTrue(_contains(json, '"sharedFields": {'));
+ assertTrue(_contains(json, '"series": "Series A shared terms"'));
+ assertTrue(_contains(json, '"certificate": ""'));
+ }
+
+ function _json(
+ CertificateUriBuilder.CertificateDetails memory details,
+ address extension,
+ address printerAddress
+ ) internal view returns (string memory) {
+ string[] memory legend = new string[](1);
+ legend[0] = "Legend";
+ CertificateUriBuilder.Endorsement[] memory endorsements = new CertificateUriBuilder.Endorsement[](0);
+ CertificateUriBuilder.OwnerDetails memory owner =
+ CertificateUriBuilder.OwnerDetails({name: "Investor", ownerAddress: address(0xB0B)});
+
+ return uriBuilder.buildCertificateUriNotEncoded(
+ "Test CyberCorp",
+ "corporation",
+ "DE",
+ "contact@test.com",
+ SecurityClass.PreferredStock,
+ SecuritySeries.SeriesA,
+ "ipfs://series-a-cert",
+ legend,
+ details,
+ endorsements,
+ owner,
+ address(0),
+ bytes32(0),
+ 1,
+ printerAddress,
+ extension
+ );
+ }
+
+ function _details(string memory extensionData) internal pure returns (CertificateUriBuilder.CertificateDetails memory) {
+ return _detailsBytes(abi.encode(extensionData));
+ }
+
+ function _detailsBytes(bytes memory extensionData) internal pure returns (CertificateUriBuilder.CertificateDetails memory) {
+ return CertificateUriBuilder.CertificateDetails({
+ signingOfficerName: "Officer",
+ signingOfficerTitle: "CEO",
+ investmentAmountUSD: 100_000e18,
+ issuerUSDValuationAtTimeOfInvestment: 25_000_000e18,
+ unitsRepresented: 10_000e18,
+ legalDetails: "Legal details",
+ extensionData: extensionData
+ });
+ }
+
+ function _contains(string memory haystack, string memory needle) internal pure returns (bool) {
+ bytes memory h = bytes(haystack);
+ bytes memory n = bytes(needle);
+ if (n.length == 0) return true;
+ if (n.length > h.length) return false;
+
+ for (uint256 i = 0; i <= h.length - n.length; i++) {
+ bool found = true;
+ for (uint256 j = 0; j < n.length; j++) {
+ if (h[i + j] != n[j]) {
+ found = false;
+ break;
+ }
+ }
+ if (found) return true;
+ }
+
+ return false;
+ }
+}
diff --git a/test/CertificateImageForkTest.t.sol b/test/CertificateImageForkTest.t.sol
new file mode 100644
index 0000000..3b0ff02
--- /dev/null
+++ b/test/CertificateImageForkTest.t.sol
@@ -0,0 +1,442 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity 0.8.28;
+
+import {Test, console2, stdStorage, StdStorage} from "forge-std/Test.sol";
+import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
+
+import {CertificateImageBuilderContract} from "../src/CertificateImageBuilderContract.sol";
+import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol";
+import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol";
+import {CyberCertPrinter} from "../src/CyberCertPrinter.sol";
+import {CompanyOfficer, SecurityClass, SecuritySeries} from "../src/CyberCorpConstants.sol";
+import {CyberCorp} from "../src/CyberCorp.sol";
+import {CyberCorpFactory} from "../src/CyberCorpFactory.sol";
+import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol";
+import {CyberScrip} from "../src/CyberScrip.sol";
+import {DealManager} from "../src/DealManager.sol";
+import {DealManagerFactory} from "../src/DealManagerFactory.sol";
+import {IssuanceManager} from "../src/IssuanceManager.sol";
+import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol";
+import {RoundManager} from "../src/RoundManager.sol";
+import {RoundManagerFactory} from "../src/RoundManagerFactory.sol";
+import {BorgAuth} from "../src/libs/auth.sol";
+import {CertificateDetails} from "../src/storage/CyberCertPrinterStorage.sol";
+import {DeploymentConstants} from "../script/libs/DeploymentConstants.sol";
+import {CyberAgreementUtils} from "./libs/CyberAgreementUtils.sol";
+
+interface IUUPS {
+ function upgradeToAndCall(address newImplementation, bytes calldata data) external payable;
+}
+
+/// @notice End-to-end fork test against the deployed CyberCorps V2 stack (deployment-addresses.md):
+/// 1. upgrades the deployed CertificateUriBuilder proxy to the local implementation and
+/// points it at a freshly deployed CertificateImageBuilderContract,
+/// 2. deploys a CyberCorp with a security offer through the deployed CyberCorpFactory,
+/// 3. has an investor accept the deal (sign + pay + finalize) minting a CyberCertPrinter NFT,
+/// 4. prints the tokenURI and verifies every stored field is displayed in the SVG image.
+contract CertificateImageForkTest is Test {
+ using stdStorage for StdStorage;
+
+ bytes32 internal constant TEMPLATE_ID = keccak256("CertificateImageForkTest.template");
+
+ string internal constant CORP_NAME = "Test CyberCorp, LLC";
+ string internal constant OWNER_NAME = "Mr. Prepop";
+ string internal constant OFFICER_NAME = "Gabriel Shapiro";
+
+ uint256 internal constant UNITS = 2_500_000e18;
+ uint256 internal constant INVESTMENT_USD = 250_000e18; // 0.1 per unit
+ uint256 internal constant VALUATION_USD = 25_000_000e18;
+
+ DeploymentConstants.CoreDeployment internal net;
+ CyberCorpFactory internal factory;
+ CyberAgreementRegistry internal registry;
+
+ uint256 internal founderKey;
+ address internal founder;
+ uint256 internal investorKey;
+ address internal investor;
+
+ function setUp() public {
+ vm.createSelectFork("base_sepolia");
+
+ net = DeploymentConstants.coreV2(block.chainid);
+ factory = CyberCorpFactory(net.cyberCorpFactory);
+ registry = CyberAgreementRegistry(net.cyberAgreementRegistry);
+
+ (founder, founderKey) = makeAddrAndKey("cert-image-founder");
+ (investor, investorKey) = makeAddrAndKey("cert-image-investor");
+
+ _upgradeDeployedStack();
+ _createTemplate();
+ }
+
+ /// @dev Mirrors script/upgrade-core-stack.s.sol on the fork: upgrades the deployed
+ /// CyberCorpFactory + CertificateUriBuilder proxies to the local implementations and
+ /// updates all factory reference implementations, so newly deployed corps run local code.
+ function _upgradeDeployedStack() internal {
+ address upgrader = makeAddr("cert-image-upgrader");
+ CyberCorpSingleFactory singleFactory = CyberCorpSingleFactory(factory.cyberCorpSingleFactory());
+ IssuanceManagerFactory imFactory = IssuanceManagerFactory(factory.issuanceManagerFactory());
+ DealManagerFactory dmFactory = DealManagerFactory(factory.dealManagerFactory());
+ RoundManagerFactory rmFactory = RoundManagerFactory(factory.roundManagerFactory());
+
+ _grantOwnerRole(address(factory.AUTH()), upgrader);
+ _grantOwnerRole(address(CertificateUriBuilder(net.uriBuilder).AUTH()), upgrader);
+ _grantOwnerRole(address(singleFactory.AUTH()), upgrader);
+ _grantOwnerRole(address(imFactory.AUTH()), upgrader);
+ _grantOwnerRole(address(dmFactory.AUTH()), upgrader);
+ _grantOwnerRole(address(rmFactory.AUTH()), upgrader);
+
+ vm.startPrank(upgrader);
+
+ // Factory + uri builder proxies
+ IUUPS(address(factory)).upgradeToAndCall(address(new CyberCorpFactory()), "");
+ IUUPS(net.uriBuilder).upgradeToAndCall(address(new CertificateUriBuilder()), "");
+ CertificateUriBuilder(net.uriBuilder).setImageBuilder(address(new CertificateImageBuilderContract()));
+
+ // Reference implementations used when deploying a new corp stack
+ singleFactory.setRefImplementation(address(new CyberCorp()));
+ imFactory.setRefImplementation(address(new IssuanceManager()));
+ imFactory.setCyberCertPrinterRefImplementation(address(new CyberCertPrinter()));
+ imFactory.setCyberScripRefImplementation(address(new CyberScrip()));
+ dmFactory.setRefImplementation(address(new DealManager()));
+ rmFactory.setRefImplementation(address(new RoundManager()));
+
+ // Make sure the factory has a stable payment token configured
+ if (factory.stable() == address(0)) {
+ factory.setStable(DeploymentConstants.deps(block.chainid).usdc);
+ }
+
+ vm.stopPrank();
+ }
+
+ /// @dev Grants OWNER_ROLE (99) on a BorgAuth instance directly via storage
+ function _grantOwnerRole(address auth, address user) internal {
+ if (BorgAuth(auth).userRoles(user) >= 99) return;
+ stdstore.target(auth).sig("userRoles(address)").with_key(user).checked_write(uint256(99));
+ }
+
+ function _createTemplate() internal {
+ string[] memory globalFields = new string[](1);
+ globalFields[0] = "Jurisdiction";
+ string[] memory partyFields = new string[](1);
+ partyFields[0] = "Party Name";
+
+ vm.prank(net.metalexSafe);
+ registry.createTemplate(
+ TEMPLATE_ID,
+ "Certificate Image Fork Test Template",
+ "ipfs://cert-image-fork-template",
+ globalFields,
+ partyFields
+ );
+ }
+
+ function test_ForkDeal_MintsCertAndDisplaysAllFieldsInImage() public {
+ // ------------------------------------------------------------------
+ // 1. Founder deploys a CyberCorp and creates a Common Stock offer
+ // ------------------------------------------------------------------
+ string[] memory defaultLegend = new string[](2);
+ defaultLegend[0] = "Board Consent Required";
+ defaultLegend[1] = "Securities Act Restriction";
+
+ CyberCorpFactory.CyberCertData[] memory certData = new CyberCorpFactory.CyberCertData[](1);
+ certData[0] = CyberCorpFactory.CyberCertData({
+ name: "Common Stock",
+ symbol: "TCC-CS",
+ uri: "ipfs://cert-image-fork-cert",
+ securityClass: SecurityClass.CommonStock,
+ securitySeries: SecuritySeries.SeriesSeed,
+ extension: address(0),
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
+ });
+
+ CertificateDetails[] memory certDetails = new CertificateDetails[](1);
+ certDetails[0] = CertificateDetails({
+ signingOfficerName: OFFICER_NAME,
+ signingOfficerTitle: "CEO",
+ investmentAmountUSD: INVESTMENT_USD,
+ issuerUSDValuationAtTimeOfInvestment: VALUATION_USD,
+ unitsRepresented: UNITS,
+ legalDetails: "Founder restricted stock purchase",
+ extensionData: ""
+ });
+
+ string[] memory globalValues = new string[](1);
+ globalValues[0] = "DE";
+
+ address[] memory parties = new address[](2);
+ parties[0] = founder;
+ parties[1] = investor;
+
+ string[][] memory partyValues = new string[][](2);
+ partyValues[0] = new string[](1);
+ partyValues[0][0] = OFFICER_NAME;
+ partyValues[1] = new string[](1);
+ partyValues[1][0] = OWNER_NAME;
+
+ ERC20 stable = ERC20(factory.stable());
+ uint256 paymentAmount = 250_000 * (10 ** stable.decimals());
+ uint256 dealSalt = uint256(keccak256("CertificateImageForkTest.deal"));
+ bytes32 expectedAgreementId = keccak256(abi.encode(TEMPLATE_ID, dealSalt, globalValues, parties));
+
+ (string memory templateUri, , string[] memory globalFields, string[] memory partyFields) =
+ registry.getTemplateDetails(TEMPLATE_ID);
+
+ vm.startPrank(founder);
+ (
+ ,
+ ,
+ ,
+ address dealManagerAddr,
+ ,
+ address[] memory certPrinters,
+ bytes32 agreementId,
+ uint256[] memory certIds
+ ) = factory.deployCyberCorpAndCreateOffer(
+ dealSalt,
+ CORP_NAME,
+ "Limited Liability Company",
+ "DE",
+ "contact@testcybercorp.xyz",
+ "Arbitration",
+ founder,
+ CompanyOfficer({eoa: founder, name: OFFICER_NAME, contact: "officer@testcybercorp.xyz", title: "CEO"}),
+ certData,
+ TEMPLATE_ID,
+ globalValues,
+ parties,
+ paymentAmount,
+ partyValues,
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ expectedAgreementId,
+ templateUri,
+ globalFields,
+ partyFields,
+ globalValues,
+ partyValues[0],
+ founderKey
+ ),
+ certDetails,
+ new address[](0),
+ bytes32(0),
+ block.timestamp + 30 days
+ );
+ vm.stopPrank();
+
+ assertEq(agreementId, expectedAgreementId, "unexpected agreement id");
+
+ // ------------------------------------------------------------------
+ // 2. Investor accepts the deal: sign + pay, then finalize
+ // ------------------------------------------------------------------
+ deal(address(stable), investor, paymentAmount);
+
+ vm.startPrank(investor);
+ stable.approve(dealManagerAddr, paymentAmount);
+ DealManager(dealManagerAddr).signDealAndPay(
+ investor,
+ agreementId,
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ agreementId,
+ templateUri,
+ globalFields,
+ partyFields,
+ globalValues,
+ partyValues[1],
+ investorKey
+ ),
+ partyValues[1],
+ true,
+ OWNER_NAME,
+ ""
+ );
+ vm.stopPrank();
+
+ DealManager(dealManagerAddr).finalizeDeal(agreementId);
+
+ CyberCertPrinter printer = CyberCertPrinter(certPrinters[0]);
+ uint256 tokenId = certIds[0];
+ assertEq(printer.ownerOf(tokenId), investor, "investor should own the certificate NFT");
+
+ // ------------------------------------------------------------------
+ // 3. Print the tokenURI and verify all stored fields render in the SVG
+ // ------------------------------------------------------------------
+ string memory uri = printer.tokenURI(tokenId);
+ console2.log("=== tokenURI ===");
+ console2.log(uri);
+
+ string memory json = _decodeJsonDataUri(uri);
+ string memory svg = _extractSvg(json);
+ console2.log("=== decoded SVG ===");
+ console2.log(svg);
+
+ // Corp name (header and issuer block)
+ assertTrue(_contains(svg, string.concat(">", CORP_NAME, "")), "corp name missing");
+ // Token ID
+ assertTrue(_contains(svg, string.concat(">#", vm.toString(tokenId), "")), "token id missing");
+ // Unit label from securityClass
+ assertTrue(_contains(svg, ">Shares"), "base unit label missing");
+ // unitsRepresented (header counter + stats panel)
+ assertTrue(_contains(svg, ">2,500,000"), "units missing");
+ // Consideration = investmentAmountUSD / unitsRepresented for stock
+ assertTrue(_contains(svg, ">0.1/sh"), "per-share consideration missing");
+ // securityClass + securitySeries
+ assertTrue(_contains(svg, ">Common Stock Series Seed"), "class/series missing");
+ // Registered owner (name from signDealAndPay + truncated address)
+ assertTrue(_contains(svg, string.concat(">", OWNER_NAME, " ")), "owner name missing");
+ assertTrue(_contains(svg, string.concat(">", _truncatedAddress(investor), "")), "owner address missing");
+ // Issuer (cert printer contract) truncated address
+ assertTrue(_contains(svg, string.concat(">", _truncatedAddress(address(printer)), "")), "issuer address missing");
+ // Signing officer
+ assertTrue(_contains(svg, string.concat(">", OFFICER_NAME, "")), "officer name missing");
+ // Block number of the render
+ assertTrue(_contains(svg, string.concat(">block ", vm.toString(block.number), " | ")), "block number missing");
+ // Certificate legend -> transfer restrictions
+ assertTrue(_contains(svg, "[1] Board Consent Required"), "restriction 1 missing");
+ assertTrue(_contains(svg, "[2] Securities Act Restriction"), "restriction 2 missing");
+ // Static chrome
+ assertTrue(_contains(svg, ">active"), "status chip missing");
+ assertTrue(_contains(svg, "Ledger Entry Token"), "ledger label missing");
+ assertTrue(_contains(svg, "Tokenized Stock Ledger pursuant to DGCL"), "footer missing");
+
+ // JSON metadata sanity: all stored numeric fields present
+ assertTrue(_contains(json, string.concat('"cyberCORPName": "', CORP_NAME, '"')), "json corp name missing");
+ assertTrue(_contains(json, '"signingOfficerName": "Gabriel Shapiro"'), "json officer missing");
+ assertTrue(_contains(json, '"investmentAmountUSD": "250000.00"'), "json investment missing");
+ assertTrue(_contains(json, '"issuerUSDValuationAtTimeOfInvestment": "25000000.00"'), "json valuation missing");
+ assertTrue(_contains(json, '"unitsRepresented": "2500000.00"'), "json units missing");
+
+ // ------------------------------------------------------------------
+ // 4. Void the certificate and verify the red "voided" badge renders
+ // ------------------------------------------------------------------
+ vm.prank(printer.issuanceManager());
+ printer.voidCert(tokenId);
+
+ string memory voidedSvg = _extractSvg(_decodeJsonDataUri(printer.tokenURI(tokenId)));
+ assertTrue(_contains(voidedSvg, ">voided"), "voided label missing");
+ assertTrue(_contains(voidedSvg, 'fill="url(#paint4_linear_void)"'), "voided pill missing");
+ assertTrue(_contains(voidedSvg, ">VOIDED"), "VOIDED stamp missing");
+ assertFalse(_contains(voidedSvg, ">active"), "active label should be gone");
+ }
+
+ // ------------------------------------------------------------------
+ // Data URI helpers
+ // ------------------------------------------------------------------
+
+ function _decodeJsonDataUri(string memory uri) internal pure returns (string memory) {
+ bytes memory u = bytes(uri);
+ bytes memory prefix = bytes("data:application/json;base64,");
+ require(u.length > prefix.length, "uri too short");
+ for (uint256 i = 0; i < prefix.length; i++) {
+ require(u[i] == prefix[i], "unexpected uri prefix");
+ }
+ return string(_base64Decode(_slice(u, prefix.length, u.length)));
+ }
+
+ function _extractSvg(string memory json) internal pure returns (string memory) {
+ bytes memory marker = bytes('"image": "data:image/svg+xml;base64,');
+ bytes memory j = bytes(json);
+ int256 start = _indexOf(j, marker, 0);
+ require(start >= 0, "image data uri not found in json");
+ uint256 b64Start = uint256(start) + marker.length;
+ uint256 b64End = b64Start;
+ while (b64End < j.length && j[b64End] != '"') {
+ b64End++;
+ }
+ return string(_base64Decode(_slice(j, b64Start, b64End)));
+ }
+
+ function _base64Decode(bytes memory data) internal pure returns (bytes memory) {
+ require(data.length % 4 == 0, "invalid base64 length");
+ if (data.length == 0) return "";
+
+ uint256 padding = 0;
+ if (data[data.length - 1] == "=") padding++;
+ if (data[data.length - 2] == "=") padding++;
+
+ uint256 outLen = (data.length / 4) * 3 - padding;
+ bytes memory result = new bytes(outLen);
+ uint256 outIdx = 0;
+
+ for (uint256 i = 0; i < data.length; i += 4) {
+ uint256 chunk = (_base64Value(data[i]) << 18)
+ | (_base64Value(data[i + 1]) << 12)
+ | (_base64Value(data[i + 2]) << 6)
+ | _base64Value(data[i + 3]);
+ if (outIdx < outLen) result[outIdx++] = bytes1(uint8(chunk >> 16));
+ if (outIdx < outLen) result[outIdx++] = bytes1(uint8(chunk >> 8));
+ if (outIdx < outLen) result[outIdx++] = bytes1(uint8(chunk));
+ }
+ return result;
+ }
+
+ function _base64Value(bytes1 c) internal pure returns (uint256) {
+ uint8 v = uint8(c);
+ if (v >= 65 && v <= 90) return v - 65; // A-Z
+ if (v >= 97 && v <= 122) return v - 71; // a-z
+ if (v >= 48 && v <= 57) return v + 4; // 0-9
+ if (v == 43) return 62; // +
+ if (v == 47) return 63; // /
+ return 0; // '=' padding
+ }
+
+ // ------------------------------------------------------------------
+ // String helpers
+ // ------------------------------------------------------------------
+
+ /// @dev Mirrors the SVG's truncated address format: 0x + first 8 + "..." + last 10 hex chars (lowercase)
+ function _truncatedAddress(address addr) internal pure returns (string memory) {
+ bytes memory hexChars = "0123456789abcdef";
+ bytes memory full = new bytes(40);
+ uint160 value = uint160(addr);
+ for (uint256 i = 0; i < 20; i++) {
+ uint8 b = uint8(value >> (8 * (19 - i)));
+ full[i * 2] = hexChars[b >> 4];
+ full[i * 2 + 1] = hexChars[b & 0x0f];
+ }
+ bytes memory head = new bytes(8);
+ for (uint256 i = 0; i < 8; i++) {
+ head[i] = full[i];
+ }
+ bytes memory tail = new bytes(10);
+ for (uint256 i = 0; i < 10; i++) {
+ tail[i] = full[30 + i];
+ }
+ return string(abi.encodePacked("0x", head, "...", tail));
+ }
+
+ function _contains(string memory haystack, string memory needle) internal pure returns (bool) {
+ return _indexOf(bytes(haystack), bytes(needle), 0) >= 0;
+ }
+
+ function _indexOf(bytes memory haystack, bytes memory needle, uint256 from) internal pure returns (int256) {
+ if (needle.length == 0) return int256(from);
+ if (needle.length > haystack.length) return -1;
+ for (uint256 i = from; i <= haystack.length - needle.length; i++) {
+ bool found = true;
+ for (uint256 j = 0; j < needle.length; j++) {
+ if (haystack[i + j] != needle[j]) {
+ found = false;
+ break;
+ }
+ }
+ if (found) return int256(i);
+ }
+ return -1;
+ }
+
+ function _slice(bytes memory data, uint256 start, uint256 end) internal pure returns (bytes memory) {
+ bytes memory result = new bytes(end - start);
+ for (uint256 i = start; i < end; i++) {
+ result[i - start] = data[i];
+ }
+ return result;
+ }
+}
diff --git a/test/CyberCertPrinterTest.t.sol b/test/CyberCertPrinterTest.t.sol
index 8d4bd60..7f76f56 100644
--- a/test/CyberCertPrinterTest.t.sol
+++ b/test/CyberCertPrinterTest.t.sol
@@ -218,7 +218,8 @@ contract CyberCertPrinterTest is Test {
address(issuanceManager),
SecurityClass.PreferredStock,
SecuritySeries.SeriesA,
- initialExtension
+ initialExtension,
+ hex""
)
);
printer = CyberCertPrinter(address(new ERC1967Proxy(address(implementation), initData)));
diff --git a/test/CyberCorpTest.t.sol b/test/CyberCorpTest.t.sol
index 7d72fbb..587bee3 100644
--- a/test/CyberCorpTest.t.sol
+++ b/test/CyberCorpTest.t.sol
@@ -137,7 +137,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: _dataDefaultString
+ defaultLegend: _dataDefaultString,
+ printerExtensionData: hex""
});
certData = new CyberCorpFactory.CyberCertData[](1);
certData[0] = _certData;
@@ -654,7 +655,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: defaultLegends[0]
+ defaultLegend: defaultLegends[0],
+ printerExtensionData: hex""
});
certData[1] = CyberCorpFactory.CyberCertData({
@@ -664,7 +666,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.TokenWarrant,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: defaultLegends[1]
+ defaultLegend: defaultLegends[1],
+ printerExtensionData: hex""
});
vm.startPrank(testAddress);
@@ -2695,7 +2698,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
(
@@ -3629,7 +3633,8 @@ contract CyberCorpForkTest is Test {
"ipfs://test",
SecurityClass.SAFE,
SecuritySeries.SeriesPreSeed,
- address(0)
+ address(0),
+ hex""
);
// Deploy new implementation
@@ -3760,7 +3765,8 @@ contract CyberCorpForkTest is Test {
"ipfs://test",
SecurityClass.SAFE,
SecuritySeries.SeriesPreSeed,
- address(0)
+ address(0),
+ hex""
);
// Deploy new implementation
@@ -4152,7 +4158,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
// Prepare deal parameters
@@ -4314,7 +4321,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: safeLegend
+ defaultLegend: safeLegend,
+ printerExtensionData: hex""
});
certData[1] = DealManagerStorage.CyberCertData({
name: "Token Warrant",
@@ -4323,7 +4331,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.TokenWarrant,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: warrantLegend
+ defaultLegend: warrantLegend,
+ printerExtensionData: hex""
});
// Prepare deal parameters
@@ -4510,7 +4519,8 @@ contract CyberCorpForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
bytes32 templateId = bytes32(uint256(1));
@@ -5687,7 +5697,8 @@ contract CyberCorpForkTest is Test {
"ipfs://test",
SecurityClass.SAFE,
SecuritySeries.SeriesPreSeed,
- address(0)
+ address(0),
+ hex""
);
// Deploy and initialize the toggle hook with the corp's AUTH used by issuanceManager
@@ -5816,7 +5827,8 @@ contract CyberCorpForkTest is Test {
"ipfs://test",
SecurityClass.SAFE,
SecuritySeries.SeriesSeed,
- address(0)
+ address(0),
+ hex""
);
CertificateDetails memory cd = CertificateDetails({
@@ -5955,7 +5967,8 @@ contract CyberCorpForkTest is Test {
"ipfs://uri",
SecurityClass.SAFE,
SecuritySeries.SeriesSeed,
- address(0)
+ address(0),
+ hex""
);
CertificateDetails memory cd = CertificateDetails({
signingOfficerName: "",
@@ -6064,7 +6077,8 @@ contract CyberCorpForkTest is Test {
"ipfs://uri",
SecurityClass.SAFE,
SecuritySeries.SeriesSeed,
- address(0)
+ address(0),
+ hex""
);
CertificateDetails memory cd = CertificateDetails({
signingOfficerName: "",
@@ -6146,7 +6160,8 @@ contract CyberCorpForkTest is Test {
"ipfs://uri",
SecurityClass.SAFE,
SecuritySeries.SeriesSeed,
- address(0)
+ address(0),
+ hex""
);
CertificateDetails memory cd = CertificateDetails({
signingOfficerName: "",
diff --git a/test/CyberCorpUpgradeabilityForkTest.t.sol b/test/CyberCorpUpgradeabilityForkTest.t.sol
index fab2fd0..5b22088 100644
--- a/test/CyberCorpUpgradeabilityForkTest.t.sol
+++ b/test/CyberCorpUpgradeabilityForkTest.t.sol
@@ -247,7 +247,8 @@ contract CyberCorpUpgradeabilityForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesPreSeed,
extension: address(0),
- defaultLegend: new string[](0)
+ defaultLegend: new string[](0),
+ printerExtensionData: hex""
});
CertificateDetails[] memory certDetails = new CertificateDetails[](1);
diff --git a/test/CyberScripUpgradeExistingV4.t.sol b/test/CyberScripUpgradeExistingV4.t.sol
index 161ebee..6102863 100644
--- a/test/CyberScripUpgradeExistingV4.t.sol
+++ b/test/CyberScripUpgradeExistingV4.t.sol
@@ -633,7 +633,8 @@ contract CyberScripUpgradeExistingV4ForkTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ ""
)
);
}
diff --git a/test/CyberScripUpgradeTest.t.sol b/test/CyberScripUpgradeTest.t.sol
index 8c3c0cb..6d2e305 100644
--- a/test/CyberScripUpgradeTest.t.sol
+++ b/test/CyberScripUpgradeTest.t.sol
@@ -187,7 +187,8 @@ contract CyberScripUpgradeForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesA,
extension: address(0),
- defaultLegend: new string[](0)
+ defaultLegend: new string[](0),
+ printerExtensionData: hex""
});
string[] memory offerGlobalValues = _strings("100", "5000000");
@@ -265,7 +266,8 @@ contract CyberScripUpgradeForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesA,
extension: address(0),
- defaultLegend: new string[](0)
+ defaultLegend: new string[](0),
+ printerExtensionData: hex""
});
string[] memory legalDetails = new string[](1);
@@ -1446,7 +1448,8 @@ contract CyberScripUpgradeForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesA,
extension: address(0),
- defaultLegend: new string[](0)
+ defaultLegend: new string[](0),
+ printerExtensionData: hex""
});
string[] memory legalDetails = new string[](1);
@@ -1503,7 +1506,8 @@ contract CyberScripUpgradeForkTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
}
diff --git a/test/DealManagerSecondaryTradeExemptionPathwayTest.t.sol b/test/DealManagerSecondaryTradeExemptionPathwayTest.t.sol
index f97853d..163bb22 100644
--- a/test/DealManagerSecondaryTradeExemptionPathwayTest.t.sol
+++ b/test/DealManagerSecondaryTradeExemptionPathwayTest.t.sol
@@ -234,7 +234,8 @@ contract DealManagerSecondaryTradeExemptionPathwayTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
sellerTokenId = im.createCertAndAssign(address(certPrinter), seller, _sellerCertDetails());
diff --git a/test/DealManagerSecondaryTradeIndexerTest.t.sol b/test/DealManagerSecondaryTradeIndexerTest.t.sol
index f938e62..2fe7b21 100644
--- a/test/DealManagerSecondaryTradeIndexerTest.t.sol
+++ b/test/DealManagerSecondaryTradeIndexerTest.t.sol
@@ -307,7 +307,8 @@ contract DealManagerSecondaryTradeIndexerTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
sellerTokenId =
diff --git a/test/DealManagerSecondaryTradeTest.t.sol b/test/DealManagerSecondaryTradeTest.t.sol
index 3a8dc6f..03d384c 100644
--- a/test/DealManagerSecondaryTradeTest.t.sol
+++ b/test/DealManagerSecondaryTradeTest.t.sol
@@ -298,7 +298,8 @@ contract DealManagerSecondaryTradeTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
sellerTokenId = im.createCertAndAssign(address(certPrinter), seller, _sellerCertDetails(UNITS));
diff --git a/test/FactoryArbitraryErc20RoundPOC.t.sol b/test/FactoryArbitraryErc20RoundPOC.t.sol
index 0017c3e..d6d2b98 100644
--- a/test/FactoryArbitraryErc20RoundPOC.t.sol
+++ b/test/FactoryArbitraryErc20RoundPOC.t.sol
@@ -99,7 +99,8 @@ contract FactoryArbitraryErc20RoundPOCTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesSeed,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
string[] memory roundPartyValues = new string[](2);
diff --git a/test/IssuanceManagerConversionTest.t.sol b/test/IssuanceManagerConversionTest.t.sol
index 156992b..47586aa 100644
--- a/test/IssuanceManagerConversionTest.t.sol
+++ b/test/IssuanceManagerConversionTest.t.sol
@@ -340,7 +340,8 @@ contract IssuanceManagerConversionTest is Test {
"uri://safe",
SecurityClass.SAFT,
SecuritySeries.NA,
- address(0)
+ address(0),
+ hex""
)
);
equityPrinter = ICyberCertPrinter(
@@ -351,7 +352,8 @@ contract IssuanceManagerConversionTest is Test {
"uri://eq",
SecurityClass.PreferredStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
@@ -1971,7 +1973,8 @@ contract IssuanceManagerConversionTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
}
diff --git a/test/IssuanceManagerSecondaryTransferTest.t.sol b/test/IssuanceManagerSecondaryTransferTest.t.sol
index 392fd48..f499483 100644
--- a/test/IssuanceManagerSecondaryTransferTest.t.sol
+++ b/test/IssuanceManagerSecondaryTransferTest.t.sol
@@ -146,7 +146,7 @@ contract IssuanceManagerSecondaryTransferTest is Test {
ICyberCertPrinter cert = ICyberCertPrinter(
issuanceManager.createCertPrinter(
new string[](0), "Cert", "CERT", "uri://cert",
- SecurityClass.CommonStock, SecuritySeries.SeriesA, address(0)
+ SecurityClass.CommonStock, SecuritySeries.SeriesA, address(0), hex""
)
);
CertificateDetails memory details = CertificateDetails({
@@ -280,7 +280,8 @@ contract IssuanceManagerSecondaryTransferTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
CertificateDetails memory details = CertificateDetails({
diff --git a/test/IssuanceManagerTest.t.sol b/test/IssuanceManagerTest.t.sol
index c5520ac..9db7316 100644
--- a/test/IssuanceManagerTest.t.sol
+++ b/test/IssuanceManagerTest.t.sol
@@ -222,7 +222,8 @@ contract IssuanceManagerTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
)
);
}
diff --git a/test/LegalOwnerBugFix.t.sol b/test/LegalOwnerBugFix.t.sol
index 3853970..65e67ac 100644
--- a/test/LegalOwnerBugFix.t.sol
+++ b/test/LegalOwnerBugFix.t.sol
@@ -175,7 +175,8 @@ contract LegalOwnerBugPOCTest is Test {
"uri://cert",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ ""
)
);
}
diff --git a/test/LifeCycleGasLimitTest.t.sol b/test/LifeCycleGasLimitTest.t.sol
index af2e27f..1837ae4 100644
--- a/test/LifeCycleGasLimitTest.t.sol
+++ b/test/LifeCycleGasLimitTest.t.sol
@@ -398,7 +398,8 @@ contract LifeCycleGasLimitTest is Test {
securityClass: SecurityClass.PreferredStock,
securitySeries: SecuritySeries.SeriesSeed,
extension: address(shareExtension),
- defaultLegend: legend
+ defaultLegend: legend,
+ printerExtensionData: hex""
});
}
diff --git a/test/PumpCorpFactory.t.sol b/test/PumpCorpFactory.t.sol
index 46e8e42..b434587 100644
--- a/test/PumpCorpFactory.t.sol
+++ b/test/PumpCorpFactory.t.sol
@@ -154,7 +154,8 @@ contract PumpCorpFactoryForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesSeed,
extension: address(0),
- defaultLegend: legend
+ defaultLegend: legend,
+ printerExtensionData: bytes("")
}));
legalDetails = new string[](1);
@@ -1352,7 +1353,8 @@ contract PumpCorpFactoryForkTest is Test {
securityClass: SecurityClass.CommonStock, // ← different from signed
securitySeries: SecuritySeries.SeriesA, // ← different from signed
extension: address(0),
- defaultLegend: legend
+ defaultLegend: legend,
+ printerExtensionData: bytes("")
});
// Attacker forges meta sig with their own key → signer != officer.eoa → revert.
@@ -1416,7 +1418,8 @@ contract PumpCorpFactoryForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesSeed,
extension: address(0),
- defaultLegend: legend
+ defaultLegend: legend,
+ printerExtensionData: bytes("")
});
// Attacker forges meta sig with their own key → signer != officer.eoa → revert.
diff --git a/test/RoundManagerForkTest.t.sol b/test/RoundManagerForkTest.t.sol
index bd8e50a..c656478 100644
--- a/test/RoundManagerForkTest.t.sol
+++ b/test/RoundManagerForkTest.t.sol
@@ -848,7 +848,8 @@ contract RoundManagerFCFSForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesSeed,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: bytes("")
});
string[] memory roundPartyValues = new string[](2);
diff --git a/test/RoundManagerTest.t.sol b/test/RoundManagerTest.t.sol
index 569346b..c5c205c 100644
--- a/test/RoundManagerTest.t.sol
+++ b/test/RoundManagerTest.t.sol
@@ -344,7 +344,8 @@ library CyberCorpHelper {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
string[] memory roundPartyValues = new string[](2);
@@ -427,7 +428,8 @@ library CyberCorpHelper {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
string[] memory roundPartyValues = new string[](2);
@@ -884,7 +886,8 @@ contract RoundManagerTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
(bytes memory signature, ) = CyberCorpHelper.computeEscrowSignature(
@@ -2115,7 +2118,8 @@ contract RoundManagerTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
bytes32 roundIdFuture;
// Compute escrowed signature for the future round
@@ -2473,7 +2477,8 @@ contract RoundManagerTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: legendA
+ defaultLegend: legendA,
+ printerExtensionData: hex""
});
certData[1] = CyberCertData({
name: "Equity B",
@@ -2482,7 +2487,8 @@ contract RoundManagerTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: legendB
+ defaultLegend: legendB,
+ printerExtensionData: hex""
});
// Round agreement arrays must match certData length
@@ -2753,7 +2759,8 @@ contract RoundManagerFCFSTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
string[] memory roundPartyValues = new string[](2);
roundPartyValues[0] = "Officer";
@@ -3269,7 +3276,8 @@ contract CyberCorpFactoryPublicRoundTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
string[] memory roundPartyValues = new string[](2);
diff --git a/test/RoundManagerTest.v3.next.t.sol b/test/RoundManagerTest.v3.next.t.sol
index 6fa2f8f..9f3e4c4 100644
--- a/test/RoundManagerTest.v3.next.t.sol
+++ b/test/RoundManagerTest.v3.next.t.sol
@@ -57,7 +57,8 @@ library CyberCorpHelperV3 {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: bytes("")
});
string[] memory roundPartyValues = new string[](2);
@@ -333,7 +334,8 @@ contract RoundManagerV3NextForkTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: bytes("")
});
uint256 maxEndTime = type(uint256).max;
@@ -441,7 +443,8 @@ contract RoundManagerV3NextForkTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: bytes("")
});
(bytes memory sig, ) = CyberCorpHelper.computeEscrowSignature(
diff --git a/test/ScripPOC.t.sol b/test/ScripPOC.t.sol
index 48aa04e..dd17067 100644
--- a/test/ScripPOC.t.sol
+++ b/test/ScripPOC.t.sol
@@ -604,7 +604,8 @@ contract ScripPOCTest is Test {
"uri://real",
SecurityClass.CommonStock,
SecuritySeries.SeriesA,
- address(0)
+ address(0),
+ hex""
);
// Avoid IssuanceManager.createCert in this PoC: it fetches tokenURI, which
diff --git a/test/ShareExtensionForkTest.t.sol b/test/ShareExtensionForkTest.t.sol
index 8835c9c..ff61eda 100644
--- a/test/ShareExtensionForkTest.t.sol
+++ b/test/ShareExtensionForkTest.t.sol
@@ -130,7 +130,7 @@ contract ShareExtensionForkTest is Test {
assertEq(details.unitsRepresented, (OFFER_AMOUNT * 1e18) / PRICE_PER_SHARE);
assertEq(details.legalDetails, "Series A Preferred Stock Certificate");
- string memory shareJson = _toJson(shareExtension.getExtensionURI(details.extensionData));
+ string memory shareJson = _toJson(shareExtension.getExtensionURI(hex"", details.extensionData));
assertEq(vm.parseJsonString(shareJson, ".shareDetails.terms.seriesName"), "Series A Preferred");
assertEq(vm.parseJsonString(shareJson, ".shareDetails.conversionRatio"), "1.00");
assertEq(vm.parseJsonString(shareJson, ".shareDetails.terms.hasPayToPlay"), "true");
@@ -167,7 +167,7 @@ contract ShareExtensionForkTest is Test {
assertEq(decoded.terms.conversionPrice, 8e18);
assertEq(ratio, (decoded.terms.originalIssuePrice * 1e18) / 8e18);
- string memory json = _toJson(shareExtension.getExtensionURI(updatedData));
+ string memory json = _toJson(shareExtension.getExtensionURI(hex"", updatedData));
assertEq(vm.parseJsonString(json, ".shareDetails.terms.conversionPrice"), "8.00");
}
@@ -243,7 +243,7 @@ contract ShareExtensionForkTest is Test {
function testJson_GetExtensionURIIsValidAndParseable() public {
bytes memory data = certPrinter.getExtensionData(tokenId);
- string memory json = _toJson(shareExtension.getExtensionURI(data));
+ string memory json = _toJson(shareExtension.getExtensionURI(hex"", data));
// Structural validity: reverts on malformed JSON
vm.parseJson(json);
@@ -484,7 +484,8 @@ contract ShareExtensionForkTest is Test {
securityClass: SecurityClass.PreferredStock,
securitySeries: SecuritySeries.SeriesA,
extension: address(shareExtension),
- defaultLegend: legend
+ defaultLegend: legend,
+ printerExtensionData: hex""
});
string[] memory legalDetails = new string[](1);
diff --git a/test/UpgradeLegacyCyberCorpsTest.t.sol b/test/UpgradeLegacyCyberCorpsTest.t.sol
new file mode 100644
index 0000000..130b0b6
--- /dev/null
+++ b/test/UpgradeLegacyCyberCorpsTest.t.sol
@@ -0,0 +1,794 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {Test, console2} from "forge-std/Test.sol";
+import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
+import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol";
+import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import {CyberAgreementUtils} from "./libs/CyberAgreementUtils.sol";
+import {UpgradePublicRoundsScript} from "../script/upgrade-public-rounds.s.sol";
+import {UpgradeLegacyCyberCorpsScript} from "../script/upgrade-legacy-cybercorps.s.sol";
+import {UpgradeLegacyDealManagersScript} from "../script/upgrade-legacy-deal-managers.s.sol";
+import {ILegacyFactory} from "../script/interfaces/ILegacyFactory.sol";
+import {ILegacyIssuanceManagerFactory} from "../script/interfaces/ILegacyIssuanceManagerFactory.sol";
+import {GnosisTransaction} from "../script/libs/safe.sol";
+import {KnownAddressesLoader} from "../script/libs/KnownAddressesLoader.sol";
+import {SecurityClass, SecuritySeries, CompanyOfficer} from "../src/CyberCorpConstants.sol";
+import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol";
+import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol";
+import {CyberCorpFactory} from "../src/CyberCorpFactory.sol";
+import {CyberCorp} from "../src/CyberCorp.sol";
+import {CyberCorpWithMigration} from "../src/CyberCorpWithMigration.sol";
+import {DealManager} from "../src/DealManager.sol";
+import {DealManagerWithMigration} from "../src/DealManagerWithMigration.sol";
+import {DealManagerFactory} from "../src/DealManagerFactory.sol";
+import {RoundManager} from "../src/RoundManager.sol";
+import {RoundManagerFactory} from "../src/RoundManagerFactory.sol";
+import {CertificateDetails} from "../src/storage/CyberCertPrinterStorage.sol";
+import {IIssuanceManager} from "../src/interfaces/IIssuanceManager.sol";
+import {IssuanceManager} from "../src/IssuanceManager.sol";
+import {IssuanceManagerWithMigration} from "./helpers/IssuanceManagerWithMigration.sol";
+import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol";
+import {BorgAuth} from "../src/libs/auth.sol";
+import {ERC1967ProxyLib} from "./libs/ERC1967ProxyLib.sol";
+
+contract MockImplVTest is UUPSUpgradeable {
+ string public constant DEPLOY_VERSION = "test";
+
+ // UUPS upgrade authorization
+ function _authorizeUpgrade(
+ address newImplementation
+ ) internal override {}
+}
+
+contract UpgradeLegacyCyberCorpsTest is Test {
+ using ERC1967ProxyLib for address;
+
+ address metalexSafe = 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C;
+
+ // Universal registry address
+ CyberAgreementRegistry registry = CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134);
+ CyberCorpFactory cyberCorpFactory = CyberCorpFactory(0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2);
+ BorgAuth deployedLexChexAddrAuth = BorgAuth(0xeAdeaD5C4A6747D4959489742c143bCDb95a01c2);
+ ILegacyFactory legacyCyberCorpSingleFactory = ILegacyFactory(0xc8e084D3f8B3b326FCc894C7afD28F4904196406);
+ ILegacyFactory legacyDealManagerFactory = ILegacyFactory(0x975df8A99C895d04ae158F8C91Ba562Fce3ECDA3);
+ ILegacyIssuanceManagerFactory legacyIssuanceManagerFactory = ILegacyIssuanceManagerFactory(0xA32547aAdAA4975082D729c79e79dBaE4385EBCf);
+
+ address paymentToken = 0x036CbD53842c5426634e7929541eC2318f3dCF7e; // USDC @ Base Sepolia
+
+ uint256 legacyAddressesCount = 3; // Limit the number of legacy addresses we migrate during tests so it won't stress the RPC endpoints too much
+
+ address[] knownCyberCorps;
+
+ // Randomly generated to avoid contaminated common test addresses
+ uint256 privateKeySalt = 0xe6fc9058b04996425a6f0e6479e6e06f7177a6c61043b10857eb0a72339853e0;
+
+ uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY_MAIN");
+ address deployer = vm.addr(deployerPrivateKey);
+ uint256 alicePrivateKey = 0xa11ce + privateKeySalt;
+ address alice = vm.addr(alicePrivateKey);
+ uint256 bobPrivateKey = 0xb0b + privateKeySalt;
+ address bob = vm.addr(bobPrivateKey);
+
+ CyberCorpSingleFactory newCyberCorpSingleFactory;
+ DealManagerFactory newDmFactory;
+ IssuanceManagerFactory newImFactory;
+ RoundManagerFactory newRmFactory;
+
+ // Deal test related
+ bytes32 templateId = bytes32(uint256(10000));
+ string contractUri = "ipfs.io/ipfs/[cid]";
+ string[] globalFields;
+ string[] partyFields;
+ address[] defaultParties = new address[](2);
+ string[] defaultGlobalValues = new string[](1);
+ string[][] defaultPartyValues = new string[][](2);
+ CyberCorpFactory.CyberCertData[] defaultCertData = new CyberCorpFactory.CyberCertData[](1);
+ CertificateDetails[] defaultCertDetails = new CertificateDetails[](1);
+
+ function setUp() public {
+ //
+ // Prepare for upgrades
+ //
+
+ // For future-proof: some networks may have upgraded already. In such case we will roll back to a known block before the upgrades
+ if (block.chainid == 84532) {
+ console2.log("Existing deployment has been upgraded, rolling back to a known block before it...");
+ vm.rollFork(33920951);
+ }
+
+ // Load all known managers
+ knownCyberCorps = KnownAddressesLoader.load(block.chainid, "/script/res/known-cyber-corps.json", legacyAddressesCount);
+
+ // Simulate granting the test deployer admin access so it can perform upgrades
+ vm.startPrank(metalexSafe);
+ registry.AUTH().updateRole(deployer, registry.AUTH().OWNER_ROLE());
+ deployedLexChexAddrAuth.updateRole(deployer, deployedLexChexAddrAuth.OWNER_ROLE()); // so deployer can grant cyberCorpFactory permissions to it
+ vm.stopPrank();
+
+ //
+ // Prepare for deal tests
+ //
+
+ globalFields = new string[](1);
+ globalFields[0] = "Global Field 1";
+ partyFields = new string[](1);
+ partyFields[0] = "Party Field 1";
+
+ vm.startPrank(metalexSafe);
+ registry.createTemplate(
+ templateId,
+ "Test",
+ contractUri,
+ globalFields,
+ partyFields
+ );
+ vm.stopPrank();
+
+ defaultParties[0] = alice;
+ defaultParties[1] = bob;
+
+ defaultGlobalValues[0] = "Test global 0";
+
+ defaultPartyValues[0] = new string[](1);
+ defaultPartyValues[0][0] = "Test party 0-0";
+ defaultPartyValues[1] = new string[](1);
+ defaultPartyValues[1][0] = "Test party 1-0";
+
+ defaultCertDetails[0] = CertificateDetails({
+ signingOfficerName: "Alice",
+ signingOfficerTitle: "CEO",
+ investmentAmountUSD: 100,
+ issuerUSDValuationAtTimeOfInvestment: 10000,
+ unitsRepresented: 0,
+ legalDetails: "test legal details",
+ extensionData: ""
+ });
+
+ defaultCertData[0] = CyberCorpFactory.CyberCertData({
+ name: "Test",
+ symbol: "TEST",
+ uri: "ipfs://test",
+ securityClass: SecurityClass.CommonStock,
+ securitySeries: SecuritySeries.NA,
+ extension: address(0),
+ defaultLegend: new string[](0),
+ printerExtensionData: hex""
+ });
+ }
+
+ function test_SanityCheck() public {
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ // Script might've done it, but we'll do it again just in case
+ assertEq(
+ legacyCyberCorpSingleFactory.getBeaconImplementation(),
+ newCyberCorpSingleFactory.getRefImplementation(),
+ "legacy CyberCorp beacon implementation should be upgraded by now"
+ );
+ assertEq(
+ legacyDealManagerFactory.getBeaconImplementation(),
+ newDmFactory.getRefImplementation(),
+ "legacy DealManager beacon implementation should be upgraded by now"
+ );
+ assertEq(
+ legacyIssuanceManagerFactory.getBeaconImplementation(),
+ newImFactory.getRefImplementation(),
+ "legacy IssuanceManager beacon implementation should be upgraded by now"
+ );
+ }
+
+ function test_LegacyCyberCorpIntegrity() public {
+ // Snapshot slot contents before upgrade
+ BorgAuth[] memory expectedAuths = new BorgAuth[](knownCyberCorps.length);
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ expectedAuths[i] = CyberCorp(knownCyberCorps[i]).AUTH();
+ }
+
+ // Perform upgrades
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ // Verify integrity
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ // New CyberCorp should implement new methods
+ assertEq(CyberCorp(knownCyberCorps[i]).DEPLOY_VERSION(), "3", string(abi.encodePacked("unexpected DEPLOY_VERSION() for CyberCorp: ", vm.toString(knownCyberCorps[i]))));
+ assertNotEq(CyberCorpSingleFactory(CyberCorp(knownCyberCorps[i]).upgradeFactory()).getRefImplementation(), address(0), "upgraded CyberCorp should be able to find reference implementation");
+
+ // Check for slot conflicts
+ assertEq(address(CyberCorp(knownCyberCorps[i]).AUTH()), address(expectedAuths[i]), string(abi.encodePacked("AUTH should not change for CyberCorp: ", vm.toString(knownCyberCorps[i]))));
+
+ // Should have RoundManager now
+ assertEq(CyberCorp(knownCyberCorps[i]).roundManager().getErc1967Implementation(), newRmFactory.getRefImplementation(), "should have up-to-date RoundManager");
+
+ // Should still be able to verify that this legacy contract is a BeaconProxy (for front-end to differentiate legacy vs v3 contracts)
+ assertEq(knownCyberCorps[i].getErc1967Beacon(), legacyCyberCorpSingleFactory.beacon(), "should be able to get beacon address");
+ }
+
+ // Legacy CyberCorpSingleFactory should be able to unilaterally upgrade its beacon
+ vm.startPrank(metalexSafe);
+ legacyCyberCorpSingleFactory.upgradeImplementation(address(new MockImplVTest()));
+ vm.stopPrank();
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ assertEq(CyberCorp(knownCyberCorps[i]).DEPLOY_VERSION(), "test", "CyberCorp should be upgraded again");
+ }
+ }
+
+ function test_LegacyDealManagerIntegrity() public {
+ // Snapshot slot contents before upgrade
+ BorgAuth[] memory expectedAuths = new BorgAuth[](knownCyberCorps.length);
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ expectedAuths[i] = DealManager(CyberCorp(knownCyberCorps[i]).dealManager()).AUTH();
+ }
+
+ // Perform upgrades
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ // Verify integrity
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ address dmAddr = CyberCorp(knownCyberCorps[i]).dealManager();
+ // New DealManager should implement new methods
+ assertEq(DealManager(dmAddr).DEPLOY_VERSION(), "3", string(abi.encodePacked("unexpected DEPLOY_VERSION() for DealManager: ", vm.toString(dmAddr))));
+ assertEq(DealManager(dmAddr).computeFee(1 ether), 0.003 ether, "upgraded DealManager should support fee calculation");
+ assertEq(DealManager(dmAddr).getPlatformPayable(), metalexSafe, "upgraded DealManager should support fee payable");
+
+ // Check for slot conflicts
+ assertEq(address(DealManager(dmAddr).AUTH()), address(expectedAuths[i]), string(abi.encodePacked("AUTH should not change for DealManager: ", vm.toString(dmAddr))));
+
+ // Should still be able to verify that this legacy contract is a BeaconProxy (for front-end to differentiate legacy vs v3 contracts)
+ assertEq(dmAddr.getErc1967Beacon(), legacyDealManagerFactory.beacon(), "should be able to get beacon address");
+ }
+
+ // Legacy DealManagerFactory should be able to unilaterally upgrade its beacon
+ vm.startPrank(metalexSafe);
+ legacyDealManagerFactory.upgradeImplementation(address(new MockImplVTest()));
+ vm.stopPrank();
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ assertEq(DealManager(CyberCorp(knownCyberCorps[i]).dealManager()).DEPLOY_VERSION(), "test", "DealManager should be upgraded again");
+ }
+ }
+
+ function test_LegacyIssuanceManagerIntegrity() public {
+ // Snapshot slot contents before upgrade
+ BorgAuth[] memory expectedAuths = new BorgAuth[](knownCyberCorps.length);
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ expectedAuths[i] = BorgAuth(IIssuanceManager(CyberCorp(knownCyberCorps[i]).issuanceManager()).AUTH());
+ }
+
+ // Perform upgrades
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ // Verify integrity
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ address imAddr = CyberCorp(knownCyberCorps[i]).issuanceManager();
+ // New IssuanceManager should implement new methods
+ assertEq(IIssuanceManager(imAddr).DEPLOY_VERSION(), "3", string(abi.encodePacked("unexpected DEPLOY_VERSION() for IssuanceManager: ", vm.toString(imAddr))));
+ assertNotEq(IssuanceManagerFactory(IIssuanceManager(imAddr).getUpgradeFactory()).getRefImplementation(), address(0), "upgraded IssuanceManager should be able to find reference implementation");
+
+ // Check for slot conflicts
+ assertEq(address(IIssuanceManager(imAddr).AUTH()), address(expectedAuths[i]), string(abi.encodePacked("AUTH should not change for IssuanceManager: ", vm.toString(imAddr))));
+
+ // its beacon implementations should be upgraded to the reference implementations
+ _assertIssuanceManagerBeacons(imAddr);
+
+ // IssuanceManagerFactory should be no longer able to unilaterally upgrade CyberCertPrinter
+ vm.startPrank(metalexSafe);
+ // Because `upgradeBeaconImplementation()` is no longer implemented in the new IssuanceManager;
+ // furthermore, it will no longer allow the factory to unilaterally upgrade CyberCertPrinter
+ vm.expectRevert();
+ ILegacyIssuanceManagerFactory(legacyIssuanceManagerFactory).upgradePrinterBeaconAt(
+ imAddr,
+ address(0) // no-op
+ );
+ vm.stopPrank();
+
+ // Should still be able to verify that this legacy contract is a BeaconProxy (for front-end to differentiate legacy vs v3 contracts)
+ assertEq(imAddr.getErc1967Beacon(), legacyIssuanceManagerFactory.beacon(), "should be able to get beacon address");
+ }
+
+ // Legacy IssuanceManagerFactory should be able to unilaterally upgrade its beacon
+ vm.startPrank(metalexSafe);
+ legacyIssuanceManagerFactory.upgradeImplementation(address(new MockImplVTest()));
+ vm.stopPrank();
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ assertEq(IIssuanceManager(CyberCorp(knownCyberCorps[i]).issuanceManager()).DEPLOY_VERSION(), "test", "IssuanceManager should be upgraded again");
+ }
+ }
+
+ function test_LegacyDealManagerProposeDeal() public {
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ address dmAddr = CyberCorp(knownCyberCorps[i]).dealManager();
+ address cyberCorp = DealManager(dmAddr).issuanceManager().CORP();
+ vm.startPrank(cyberCorp);
+
+ address[] memory certPrinterAddress = new address[](1);
+ for (uint256 j = 0; j < defaultCertData.length; j++) {
+ certPrinterAddress[j] = IIssuanceManager(DealManager(dmAddr).issuanceManager()).createCertPrinter(
+ defaultCertData[j].defaultLegend,
+ string.concat("TestCorp", defaultCertData[j].name),
+ defaultCertData[j].symbol,
+ defaultCertData[j].uri,
+ defaultCertData[j].securityClass,
+ defaultCertData[j].securitySeries,
+ defaultCertData[j].extension,
+ defaultCertData[j].printerExtensionData
+ );
+ }
+
+ uint256 agreementSalt = block.timestamp + i;
+
+ // Create and sign deal
+ (
+ bytes32 agreementId,
+ uint256[] memory certIds
+ ) = DealManager(dmAddr).proposeAndSignDeal(
+ certPrinterAddress,
+ paymentToken,
+ 100e6,
+ templateId,
+ agreementSalt,
+ defaultGlobalValues,
+ defaultParties,
+ defaultCertDetails,
+ alice,
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ keccak256(abi.encode(
+ templateId,
+ agreementSalt,
+ defaultGlobalValues,
+ defaultParties
+ )),
+ contractUri,
+ globalFields,
+ partyFields,
+ defaultGlobalValues,
+ defaultPartyValues[0],
+ alicePrivateKey
+ ),
+ defaultPartyValues,
+ new address[](0),
+ bytes32(0),
+ block.timestamp + 1000000
+ );
+
+ vm.stopPrank();
+
+ // Simulate counter-sign
+ deal(address(paymentToken), bob, 100e6);
+ uint256 companyPayableBalanceBefore = ERC20(paymentToken).balanceOf(CyberCorp(cyberCorp).companyPayable());
+
+ vm.startPrank(bob);
+ ERC20(paymentToken).approve(dmAddr, 100e6);
+
+ DealManager(dmAddr).signAndFinalizeDeal(
+ bob,
+ agreementId,
+ defaultPartyValues[1],
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ agreementId,
+ contractUri,
+ globalFields,
+ partyFields,
+ defaultGlobalValues,
+ defaultPartyValues[1],
+ bobPrivateKey
+ ),
+ true,
+ "Bob",
+ ""
+ );
+ vm.stopPrank();
+
+ assertEq(ERC20(paymentToken).balanceOf(CyberCorp(cyberCorp).companyPayable()) - companyPayableBalanceBefore, 100e6 - 0.3e6, "alice should receive payment minus fees");
+ }
+ }
+
+ /// @notice After migration, legacy Corp should have RoundManager retrofitted. Since the RoundManager is a new deployment,
+ /// it is of pure UUPSUpgradeable + ERC1967Proxy architecture and has the full upgrade-by-co-approval features.
+ /// We will verify the upgradeability here
+ function test_LegacyRetrofittedRoundManagerIntegrity() public {
+ // Perform upgrades
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ // Verify integrity
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ address rmAddr = CyberCorp(knownCyberCorps[i]).roundManager();
+ // New RoundManager should implement new methods
+ assertEq(RoundManager(rmAddr).DEPLOY_VERSION(), "3", string(abi.encodePacked("unexpected DEPLOY_VERSION() for RoundManager: ", vm.toString(rmAddr))));
+ assertEq(RoundManager(rmAddr).computeFee(1 ether), 0.003 ether, "upgraded RoundManager should support fee calculation with no fees");
+ assertEq(RoundManager(rmAddr).getPlatformPayable(), metalexSafe, "upgraded RoundManager should support fee payable");
+
+ // Should be able to upgrade it by co-approval
+ address testNewRmImpl = address(new MockImplVTest());
+ vm.prank(metalexSafe);
+ newRmFactory.setRefImplementation(testNewRmImpl);
+ vm.prank(knownCyberCorps[i]);
+ RoundManager(rmAddr).upgradeToAndCall(testNewRmImpl, "");
+ assertEq(RoundManager(rmAddr).DEPLOY_VERSION(), "test", "RoundManager should be upgraded again");
+ }
+ }
+
+ function test_NewDealManagerIntegrity() public {
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ // Deploy a new DealManager
+ DealManager dm = DealManager(newDmFactory.deployDealManager(bytes32(keccak256("test_NewDealManagerIntegrity"))));
+
+ // Simulate initialize() like CyberCorpFactory would do
+ dm.initialize(
+ address(1), // No-op
+ address(1), // No-op
+ address(1), // No-op
+ address(1), // No-op
+ address(newDmFactory)
+ );
+
+ // New DealManager should implement new methods
+ assertEq(dm.DEPLOY_VERSION(), "3", "unexpected DEPLOY_VERSION()");
+ assertEq(dm.computeFee(1 ether), 0.003 ether, "new DealManager should support fee calculation with no fees");
+ assertEq(dm.getPlatformPayable(), metalexSafe, "new DealManager should support fee payable");
+ }
+
+ function test_DeployNewCyberCorp() public {
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ vm.startPrank(alice);
+ (
+ address cyberCorpAddress,
+ ,
+ address issuanceManagerAddress,
+ address dealManagerAddress,
+ address roundManagerAddress,
+ address[] memory certPrinterAddress,
+ ,
+
+ ) = cyberCorpFactory.deployCyberCorpAndCreateOffer(
+ block.timestamp,
+ "TestCorp",
+ "Limited Liability Company",
+ "Juris",
+ "Contact Details",
+ "Dispute Res",
+ alice,
+ CompanyOfficer({
+ eoa: alice,
+ name: "Alice",
+ contact: "test@example.com",
+ title: "CEO"
+ }),
+ defaultCertData,
+ templateId,
+ defaultGlobalValues,
+ defaultParties,
+ 100e6,
+ defaultPartyValues,
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ keccak256(abi.encode(
+ templateId,
+ block.timestamp,
+ defaultGlobalValues,
+ defaultParties
+ )),
+ contractUri,
+ globalFields,
+ partyFields,
+ defaultGlobalValues,
+ defaultPartyValues[0],
+ alicePrivateKey
+ ),
+ defaultCertDetails,
+ new address[](0),
+ bytes32(0),
+ block.timestamp + 1000000
+ );
+ vm.stopPrank();
+
+ // Verify that new deployments are UUPSUpgradeable (so front-end can differentiate from from legacy contracts)
+ assertEq(cyberCorpAddress.getErc1967Beacon(), address(0), "new CyberCorp should not be a BeaconProxy");
+ assertEq(issuanceManagerAddress.getErc1967Beacon(), address(0), "new IssuanceManager should not be a BeaconProxy");
+ assertEq(dealManagerAddress.getErc1967Beacon(), address(0), "new DealManager should not be a BeaconProxy");
+ assertEq(roundManagerAddress.getErc1967Beacon(), address(0), "new RoundManager should not be a BeaconProxy");
+ assertEq(certPrinterAddress[0].getErc1967Beacon(), address(IIssuanceManager(issuanceManagerAddress).cyberCertPrinterBeacon()), "new CyberCertPrinter should still be a BeaconProxy");
+ }
+
+ function test_EnableFeesNewCorp() public {
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ vm.startPrank(metalexSafe);
+ newDmFactory.setPlatformPayable(address(metalexSafe));
+ newDmFactory.setDefaultFeeRatio(25);
+ vm.stopPrank();
+
+ vm.startPrank(alice);
+ (
+ address cyberCorp,
+ address auth,
+ address issuanceManager,
+ address dm,
+ address rm,
+ address[] memory cyberCertPrinterAddr,
+ bytes32 agreementId,
+ uint256[] memory certIds
+ ) = cyberCorpFactory.deployCyberCorpAndCreateOffer(
+ block.timestamp,
+ "TestCorp",
+ "Limited Liability Company",
+ "Juris",
+ "Contact Details",
+ "Dispute Res",
+ alice,
+ CompanyOfficer({
+ eoa: alice,
+ name: "Alice",
+ contact: "test@example.com",
+ title: "CEO"
+ }),
+ defaultCertData,
+ templateId,
+ defaultGlobalValues,
+ defaultParties,
+ 100e6,
+ defaultPartyValues,
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ keccak256(abi.encode(
+ templateId,
+ block.timestamp,
+ defaultGlobalValues,
+ defaultParties
+ )),
+ contractUri,
+ globalFields,
+ partyFields,
+ defaultGlobalValues,
+ defaultPartyValues[0],
+ alicePrivateKey
+ ),
+ defaultCertDetails,
+ new address[](0),
+ bytes32(0),
+ block.timestamp + 1000000
+ );
+ vm.stopPrank();
+
+ deal(address(paymentToken), bob, 100e6);
+ uint256 metalexSafeBalanceBefore = ERC20(paymentToken).balanceOf(metalexSafe);
+
+ vm.startPrank(bob);
+ ERC20(paymentToken).approve(dm, 100e6);
+
+ DealManager(dm).signAndFinalizeDeal(
+ bob,
+ agreementId,
+ defaultPartyValues[1],
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ agreementId,
+ contractUri,
+ globalFields,
+ partyFields,
+ defaultGlobalValues,
+ defaultPartyValues[1],
+ bobPrivateKey
+ ),
+ true,
+ "Bob",
+ ""
+ );
+ vm.stopPrank();
+
+ assertEq(ERC20(paymentToken).balanceOf(alice), 100e6 - 0.25e6, "alice should receive payment minus fees");
+ assertEq(ERC20(paymentToken).balanceOf(metalexSafe) - metalexSafeBalanceBefore, 0.25e6, "MetaLex should receive fees");
+ }
+
+ function test_EnableFeesExistingCorp() public {
+ _upgradeFactoryAndLegacyCyberCorps();
+
+ vm.startPrank(metalexSafe);
+ newDmFactory.setPlatformPayable(address(metalexSafe));
+ newDmFactory.setDefaultFeeRatio(25);
+ vm.stopPrank();
+
+ address dmAddr = CyberCorp(knownCyberCorps[0]).dealManager();
+ address cyberCorp = DealManager(dmAddr).issuanceManager().CORP();
+ vm.startPrank(cyberCorp);
+
+ address[] memory certPrinterAddress = new address[](1);
+ for (uint256 j = 0; j < defaultCertData.length; j++) {
+ certPrinterAddress[j] = IIssuanceManager(DealManager(dmAddr).issuanceManager()).createCertPrinter(
+ defaultCertData[j].defaultLegend,
+ string.concat("TestCorp", defaultCertData[j].name),
+ defaultCertData[j].symbol,
+ defaultCertData[j].uri,
+ defaultCertData[j].securityClass,
+ defaultCertData[j].securitySeries,
+ defaultCertData[j].extension,
+ defaultCertData[j].printerExtensionData
+ );
+ }
+
+ uint256 agreementSalt = block.timestamp;
+
+ // Create and sign deal
+ (bytes32 agreementId, ) = DealManager(dmAddr).proposeAndSignDeal(
+ certPrinterAddress,
+ paymentToken,
+ 100e6,
+ templateId,
+ agreementSalt,
+ defaultGlobalValues,
+ defaultParties,
+ defaultCertDetails,
+ alice,
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ keccak256(abi.encode(
+ templateId,
+ agreementSalt,
+ defaultGlobalValues,
+ defaultParties
+ )),
+ contractUri,
+ globalFields,
+ partyFields,
+ defaultGlobalValues,
+ defaultPartyValues[0],
+ alicePrivateKey
+ ),
+ defaultPartyValues,
+ new address[](0),
+ bytes32(0),
+ block.timestamp + 1000000
+ );
+
+ vm.stopPrank();
+
+ deal(address(paymentToken), bob, 100e6);
+ uint256 companyPayableBalanceBefore = ERC20(paymentToken).balanceOf(CyberCorp(cyberCorp).companyPayable());
+ uint256 metalexSafeBalanceBefore = ERC20(paymentToken).balanceOf(metalexSafe);
+
+ vm.startPrank(bob);
+ ERC20(paymentToken).approve(dmAddr, 100e6);
+
+ DealManager(dmAddr).signAndFinalizeDeal(
+ bob,
+ agreementId,
+ defaultPartyValues[1],
+ CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ registry.DOMAIN_SEPARATOR(),
+ registry.SIGNATUREDATA_TYPEHASH(),
+ agreementId,
+ contractUri,
+ globalFields,
+ partyFields,
+ defaultGlobalValues,
+ defaultPartyValues[1],
+ bobPrivateKey
+ ),
+ true,
+ "Bob",
+ ""
+ );
+ vm.stopPrank();
+
+ assertEq(ERC20(paymentToken).balanceOf(CyberCorp(cyberCorp).companyPayable()) - companyPayableBalanceBefore, 100e6 - 0.25e6, "alice should receive payment minus fees");
+ assertEq(ERC20(paymentToken).balanceOf(metalexSafe) - metalexSafeBalanceBefore, 0.25e6, "MetaLex should receive fees");
+ }
+
+ function _upgradeFactoryAndLegacyCyberCorps() internal {
+ //
+ // Simulate upgrades
+ //
+
+ // Simulate granting the test deployer admin access so it can perform upgrades
+ vm.startPrank(metalexSafe);
+ CyberAgreementRegistry(registry).AUTH().updateRole(
+ deployer,
+ CyberAgreementRegistry(registry).AUTH().OWNER_ROLE()
+ );
+ vm.stopPrank();
+
+ // Upgrade all factories and dependencies
+ (new UpgradePublicRoundsScript()).run();
+ newCyberCorpSingleFactory = CyberCorpSingleFactory(cyberCorpFactory.cyberCorpSingleFactory());
+ newDmFactory = DealManagerFactory(cyberCorpFactory.dealManagerFactory());
+ newImFactory = IssuanceManagerFactory(cyberCorpFactory.issuanceManagerFactory());
+ newRmFactory = RoundManagerFactory(cyberCorpFactory.roundManagerFactory());
+
+ // Run scripts to upgrade all legacy CyberCorps
+ (new UpgradeLegacyCyberCorpsScript()).runWithArgs(legacyAddressesCount);
+
+ // Run scripts to upgrade all legacy DealManagers
+ (new UpgradeLegacyDealManagersScript()).runWithArgs(legacyAddressesCount);
+
+ // Upgrade legacy IssuanceManagers (script commented out in src/ due to contract size; inlined here for tests)
+ _upgradeLegacyIssuanceManagers();
+
+ // Simulate all legacy corp owners accept the new CyberCorpPrinter
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ address imAddr = CyberCorp(knownCyberCorps[i]).issuanceManager();
+ // Accept new CyberCertPrinter release
+ vm.startPrank(imAddr);
+ IIssuanceManager(imAddr).upgradeCertPrinterBeaconImplementation(
+ IssuanceManagerFactory(cyberCorpFactory.issuanceManagerFactory()).getCyberCertPrinterRefImplementation()
+ );
+ vm.stopPrank();
+ console2.log("CyberCertPrinter beacon implementation was accepted by IssuanceManager: %s", imAddr);
+ }
+ }
+
+ function _upgradeLegacyIssuanceManagers() internal {
+ IssuanceManagerWithMigration imWithMigrationImpl = new IssuanceManagerWithMigration();
+
+ assertEq(
+ cyberCorpFactory.issuanceManagerFactory(),
+ imWithMigrationImpl.NEW_UPGRADE_FACTORY(),
+ "new issuanceManagerFactory address has changed, update it in IssuanceManagerWithMigration"
+ );
+
+ legacyIssuanceManagerFactory.upgradeImplementation(address(imWithMigrationImpl));
+ assertEq(
+ legacyIssuanceManagerFactory.getBeaconImplementation(),
+ address(imWithMigrationImpl),
+ "beacon implementation should be upgraded with migration features by now"
+ );
+
+ for (uint256 i = 0; i < knownCyberCorps.length; i++) {
+ address imAddr = CyberCorp(knownCyberCorps[i]).issuanceManager();
+ IssuanceManagerWithMigration(imAddr).migrateUpgradeFactory();
+ assertNotEq(
+ IssuanceManagerFactory(IssuanceManager(imAddr).getUpgradeFactory()).getRefImplementation(),
+ address(0),
+ "should be able to lookup reference implementation now"
+ );
+ assertEq(
+ IssuanceManager(imAddr).getCertPrinterBeaconImplementation(),
+ newImFactory.getCyberCertPrinterRefImplementation(),
+ "should point CyberCertPrinter implementation to reference now"
+ );
+ assertEq(
+ IssuanceManager(imAddr).getScripBeaconImplementation(),
+ newImFactory.getCyberScripRefImplementation(),
+ "should point CyberScrip implementation to reference now"
+ );
+ }
+
+ address refImplementation = newImFactory.getRefImplementation();
+ legacyIssuanceManagerFactory.upgradeImplementation(refImplementation);
+ assertEq(
+ legacyIssuanceManagerFactory.getBeaconImplementation(),
+ refImplementation,
+ "beacon implementation should be upgraded without migration features by now"
+ );
+ }
+
+ function _assertIssuanceManagerBeacons(address imAddr) internal {
+ assertEq(
+ IIssuanceManager(imAddr).getCertPrinterBeaconImplementation(),
+ newImFactory.getCyberCertPrinterRefImplementation(),
+ string(abi.encodePacked("unexpected CertPrinterBeaconImplementation for IssuanceManager: ", vm.toString(imAddr)))
+ );
+ assertEq(
+ IIssuanceManager(imAddr).getScripBeaconImplementation(),
+ newImFactory.getCyberScripRefImplementation(),
+ string(abi.encodePacked("unexpected ScripBeaconImplementation for IssuanceManager: ", vm.toString(imAddr)))
+ );
+ }
+}
diff --git a/test/UpgradePublicRoundsTest.t.sol b/test/UpgradePublicRoundsTest.t.sol
index a18bdf0..899a334 100644
--- a/test/UpgradePublicRoundsTest.t.sol
+++ b/test/UpgradePublicRoundsTest.t.sol
@@ -150,7 +150,8 @@ contract UpgradePublicRoundsForkTest is Test {
securityClass: SecurityClass.SAFE,
securitySeries: SecuritySeries.SeriesA,
extension: address(0),
- defaultLegend: new string[](0)
+ defaultLegend: new string[](0),
+ printerExtensionData: hex""
});
string[] memory roundPartyValues = new string[](5);
@@ -540,7 +541,8 @@ contract UpgradePublicRoundsForkTest is Test {
securityClass: SecurityClass.CommonStock,
securitySeries: SecuritySeries.NA,
extension: address(0),
- defaultLegend: defaultLegend
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
});
address[] memory conditions = new address[](1);
diff --git a/test/UpgradeRoundManagerTokenWhitelistTest.t.sol b/test/UpgradeRoundManagerTokenWhitelistTest.t.sol
new file mode 100644
index 0000000..f3bcc23
--- /dev/null
+++ b/test/UpgradeRoundManagerTokenWhitelistTest.t.sol
@@ -0,0 +1,331 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+import {Test, console2} from "forge-std/Test.sol";
+import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
+import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import {CyberCorpHelper} from "./RoundManagerTest.t.sol";
+import {UpgradeRoundManagerTokenWhitelistScript} from "../script/upgrade-round-manager-token-whitelist.s.sol";
+import {KnownAddressesLoader} from "../script/libs/KnownAddressesLoader.sol";
+import {SecuritySeries, SecurityClass} from "../src/CyberCorpConstants.sol";
+import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol";
+import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol";
+import {CyberCorpFactory} from "../src/CyberCorpFactory.sol";
+import {CyberCorp} from "../src/CyberCorp.sol";
+import {RoundManager} from "../src/RoundManager.sol";
+import {RoundManagerFactory} from "../src/RoundManagerFactory.sol";
+import {LeXcheXMinter, LeXcheX} from "../src/creds/lexchexMinter.sol";
+import {CyberCertData, EOI, LexChexDetails, MintRequest} from "../src/storage/RoundManagerStorage.sol";
+import {BorgAuth} from "../src/libs/auth.sol";
+import {RoundLib, Round, RoundType} from "../src/libs/RoundLib.sol";
+import {ERC1967ProxyLib} from "./libs/ERC1967ProxyLib.sol";
+import {CyberAgreementUtils} from "./libs/CyberAgreementUtils.sol";
+
+/// @notice This is for testing upgrading base-sepolia, which has been upgraded to a dev version of v3 before, to the current version of v3
+contract UpgradeRoundManagerTokenWhitelistTest is Test {
+ using RoundLib for Round;
+ using ERC1967ProxyLib for address;
+
+ address metalexSafe = 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C;
+
+ // Known addresses
+ CyberAgreementRegistry registry = CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134);
+ CyberCorpFactory cyberCorpFactory = CyberCorpFactory(0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2);
+ BorgAuth deployedLexChexAddrAuth = BorgAuth(0xeAdeaD5C4A6747D4959489742c143bCDb95a01c2);
+ address lexchexConditionAddress = 0x4a08547d57C8d01e59bA8F884aB90CEe0d6d5b42;
+ LeXcheXMinter leXcheXMinter = LeXcheXMinter(0x0dD1a2a89eC172ac322B6a7a6c869180CBD0F960);
+ ERC20 stable = ERC20(0x036CbD53842c5426634e7929541eC2318f3dCF7e); // Base Sepolia
+
+ RoundManagerFactory rmFactory;
+
+ address paymentToken = 0x036CbD53842c5426634e7929541eC2318f3dCF7e; // USDC @ Base Sepolia
+
+ uint256 legacyAddressesCount = 3; // Limit the number of legacy addresses we migrate during tests so it won't stress the RPC endpoints too much
+
+ address[] knownLegacyCorps;
+
+ address deployer;
+ uint256 deployerPrivateKey;
+ address alice;
+ uint256 alicePrivateKey;
+ address bob;
+ uint256 bobPrivateKey;
+
+ function setUp() public {
+ (deployer, deployerPrivateKey) = makeAddrAndKey("deployer");
+ (alice, alicePrivateKey) = makeAddrAndKey("alice");
+ (bob, bobPrivateKey) = makeAddrAndKey("bob");
+
+ // Lock in specific chain ID and fork block (after previous dev public-round migration and after LexCheX Minter is setup)
+ assertEq(block.chainid, 84532, "This test is meant for only Base Sepolia @ 34732511");
+ vm.rollFork(34732511);
+
+ rmFactory = RoundManagerFactory(cyberCorpFactory.roundManagerFactory());
+
+ // Load a limit number of known legacy cyber corps for tests
+ knownLegacyCorps = KnownAddressesLoader.load(block.chainid, "/script/res/known-cyber-corps.json", legacyAddressesCount);
+
+ // Simulate granting the test deployer admin access so it can perform upgrades
+ vm.startPrank(metalexSafe);
+ registry.AUTH().updateRole(deployer, registry.AUTH().OWNER_ROLE());
+ vm.stopPrank();
+
+ // Run scripts to upgrade RoundManager and its factory
+ (new UpgradeRoundManagerTokenWhitelistScript()).runWithArgs(deployerPrivateKey);
+
+ // Sanity check
+ assertTrue(rmFactory.isWhitelistedToken(address(stable)), "stable should have been whitelisted");
+
+ // Run scripts to accept the new RoundManager for the legacy corps (assuming they have all been migrated by now)
+ for (uint256 i = 0; i < knownLegacyCorps.length; i++) {
+ CyberCorp corp = CyberCorp(knownLegacyCorps[i]);
+ RoundManager rm = RoundManager(corp.roundManager());
+ assertNotEq(
+ address(rm),
+ address(0),
+ string(abi.encodePacked("legacy CyberCorp: ", vm.toString(address(corp)), " should have RoundManager by now"))
+ );
+
+ address refRmImpl = rmFactory.getRefImplementation();
+ if (address(rm).getErc1967Implementation() != refRmImpl) {
+ vm.prank(address(corp));
+ rm.upgradeToAndCall(refRmImpl, "");
+ console2.log("CyberCorp: %s, RoundManager: %s accepted upgrade to implementation: %s", address(corp), address(rm), refRmImpl);
+ }
+
+ // Simulate admin to retro-authorize legacy corp's RoundManager to LexCheX minter
+ vm.startPrank(metalexSafe); // In practice, SAFE can temporarily grant the deployer owner-role to do the followings
+ deployedLexChexAddrAuth.updateRole(
+ address(rm),
+ deployedLexChexAddrAuth.OWNER_ROLE()
+ );
+ console2.log("CyberCorp: %s, RoundManager: %s is assigned owner role for LexCheX Minter", address(corp), address(rm));
+ vm.stopPrank();
+ }
+ }
+
+ function test_SanityCheck() public {
+ for (uint256 i = 0; i < knownLegacyCorps.length; i++) {
+ CyberCorp corp = CyberCorp(knownLegacyCorps[i]);
+ RoundManager rm = RoundManager(corp.roundManager());
+ // RoundManager should be at current implementation
+ assertEq(address(rm).getErc1967Implementation(), rmFactory.getRefImplementation(), string(abi.encodePacked("CyberCorp: ", vm.toString(address(corp)), " should have up-to-date implementation for its roundManager")));
+ }
+ }
+
+ /// @notice Now that legacy corps have the most current RoundManager, it should be able to create a LeXCheX round
+ function test_CreateLexchexRound() public {
+ // Create template for tests
+
+ bytes32 templateId = keccak256("test_CreateLexchexRound");
+ string[] memory globalFields = new string[](1);
+ globalFields[0] = "Global Field";
+ string[] memory partyFields = new string[](2);
+ partyFields[0] = "Officer Name";
+ partyFields[1] = "Officer Title";
+ vm.prank(metalexSafe);
+ registry.createTemplate(
+ templateId,
+ "Test",
+ "ipfs://template",
+ globalFields,
+ partyFields
+ );
+
+ // Test data for agreement
+
+ SecuritySeries series = SecuritySeries.SeriesSeed;
+ RoundType roundType = RoundType.FCFS;
+ uint256 raiseCap = 1_000_000 * (10 ** stable.decimals());
+ uint256 minTicket = 2_000 * (10 ** stable.decimals());
+ uint256 maxTicket = 300_000 * (10 ** stable.decimals());
+ uint256 startTime = block.timestamp;
+ uint256 endTime = block.timestamp + 30 days;
+ uint256 pricePerUnit = 10 * (10 ** stable.decimals());
+ uint256 valuation = 10_000_000;
+
+ string[] memory roundPartyValues = new string[](2);
+ roundPartyValues[0] = "Alice Officer";
+ roundPartyValues[1] = "CEO";
+
+ string[] memory defaultLegend = new string[](1);
+ defaultLegend[0] = "Legend";
+ CyberCertData[] memory certData = new CyberCertData[](1);
+ certData[0] = CyberCertData({
+ name: "Equity",
+ symbol: "EQ",
+ uri: "ipfs://eq",
+ securityClass: SecurityClass.CommonStock,
+ securitySeries: SecuritySeries.NA,
+ extension: address(0),
+ defaultLegend: defaultLegend,
+ printerExtensionData: hex""
+ });
+
+ address[] memory conditions = new address[](1);
+ conditions[0] = lexchexConditionAddress;
+
+ // Test each known corp
+
+ for (uint256 i = 0; i < knownLegacyCorps.length; i++) {
+ CyberCorp corp = CyberCorp(knownLegacyCorps[i]);
+ RoundManager rm = RoundManager(corp.roundManager());
+
+ // Create round
+
+ (bytes memory escrowedSig, ) = CyberCorpHelper.computeEscrowSignature(
+ address(rm),
+ series,
+ raiseCap,
+ minTicket,
+ maxTicket,
+ roundType,
+ startTime,
+ endTime,
+ templateId,
+ paymentToken,
+ pricePerUnit,
+ valuation,
+ alicePrivateKey,
+ address(corp)
+ );
+
+ vm.startPrank(address(corp));
+ bytes32 roundId = rm.createRound(
+ RoundLib.draft()
+ .setTickets(
+ series,
+ roundType,
+ true, // publicRound
+ true, // allowTimedOffers
+ false, // restrictEndTimeReduction
+ raiseCap,
+ minTicket,
+ maxTicket,
+ address(stable),
+ pricePerUnit,
+ valuation,
+ startTime,
+ endTime
+ )
+ .setAgreement(
+ templateId,
+ alice,
+ "Officer",
+ "CEO",
+ new string[](certData.length),
+ roundPartyValues,
+ new bytes[](certData.length),
+ conditions,
+ escrowedSig
+ ),
+ certData
+ );
+ vm.stopPrank();
+
+ // Submit EOI
+
+ uint256 salt = uint256(keccak256("test_CreateLexchexRound.EOI")) + i;
+
+ EOI memory eoi = EOI({
+ name: "High Roller",
+ investorType: "Individual",
+ jurisdiction: "US",
+ contact: "email",
+ minAmount: 200_000 * (10 ** stable.decimals()),
+ maxAmount: 250_000 * (10 ** stable.decimals()),
+ expiry: block.timestamp + 7 days,
+ naturalPerson: true,
+ lexchexDetails: CyberCorpHelper.emptyLex()
+ });
+
+ // Attach a valid LeXcheX mint payload aligned to template 400 so auto-mint can succeed
+ {
+ CyberAgreementRegistry lxRegistry = CyberAgreementRegistry(leXcheXMinter.dealRegistry());
+
+ bytes32 lxTemplateId = bytes32(uint256(400));
+ uint256 lxSalt = block.timestamp;
+ (string memory legalUri, , string[] memory lxGlFields, string[] memory lxPartyFields) = lxRegistry.getTemplateDetails(lxTemplateId);
+
+ string[] memory lxGlobalValues = new string[](1);
+ lxGlobalValues[0] = "2029-01-01";
+
+ address[] memory lxParties = new address[](1);
+ lxParties[0] = bob;
+
+ string[][] memory lxPartyValues = new string[][](1);
+ lxPartyValues[0] = new string[](4);
+ lxPartyValues[0][0] = eoi.name;
+ lxPartyValues[0][1] = eoi.investorType;
+ lxPartyValues[0][2] = eoi.jurisdiction;
+ lxPartyValues[0][3] = eoi.contact;
+
+ bytes32 lxContractId = keccak256(abi.encode(lxTemplateId, lxSalt, lxGlobalValues, lxParties));
+ bytes memory lxSig = CyberAgreementUtils.signAgreementTypedData(
+ vm,
+ lxRegistry.DOMAIN_SEPARATOR(),
+ lxRegistry.SIGNATUREDATA_TYPEHASH(),
+ lxContractId,
+ legalUri,
+ lxGlFields,
+ lxPartyFields,
+ lxGlobalValues,
+ lxPartyValues[0],
+ bobPrivateKey
+ );
+
+ eoi.lexchexDetails = LexChexDetails({
+ request: MintRequest({
+ uuid: 1,
+ owner: bob,
+ investorName: eoi.name,
+ investorType: eoi.investorType,
+ investorJurisdiction: eoi.jurisdiction,
+ investorContact: eoi.contact,
+ mintPrice: 0,
+ expiry: block.timestamp + 30 days,
+ paymentToken: address(stable)
+ }),
+ templateId: lxTemplateId,
+ salt: uint256(lxSalt),
+ globalValues: lxGlobalValues,
+ parties: lxParties,
+ partyValues: lxPartyValues,
+ agreementSignature: lxSig
+ });
+ }
+
+ string[] memory globalValues = new string[](1);
+ globalValues[0] = "g";
+ string[] memory partyValues = new string[](2);
+ partyValues[0] = "Officer";
+ partyValues[1] = "CEO";
+
+ deal(address(stable), bob, 300_000 * (10 ** stable.decimals()));
+
+ vm.startPrank(bob);
+ stable.approve(address(rm), 300_000 * (10 ** stable.decimals()));
+ rm.submitEOI(
+ roundId,
+ eoi,
+ globalValues,
+ partyValues,
+ CyberCorpHelper.computeEOISignature(
+ registry,
+ templateId,
+ salt,
+ globalValues,
+ partyValues,
+ alice,
+ bobPrivateKey
+ ),
+ salt,
+ new address[](0),
+ bytes32(0)
+ );
+ vm.stopPrank();
+
+ assertEq(LeXcheX(CyberCorpHelper.LEXCHEX_ADDRESS).balanceOf(bob), 1, "LexChex should be minted for bob");
+ }
+ }
+}
diff --git a/test/helpers/IssuanceManagerWithMigration.sol b/test/helpers/IssuanceManagerWithMigration.sol
new file mode 100644
index 0000000..6700cba
--- /dev/null
+++ b/test/helpers/IssuanceManagerWithMigration.sol
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity 0.8.28;
+
+import {UpgradeableBeacon} from "openzeppelin-contracts/proxy/beacon/UpgradeableBeacon.sol";
+import {IssuanceManager} from "../../src/IssuanceManager.sol";
+import {IssuanceManagerStorage} from "../../src/storage/IssuanceManagerStorage.sol";
+import {IIssuanceManagerFactory} from "../../src/interfaces/IIssuanceManagerFactory.sol";
+
+/// @dev Test-only migration helper; kept out of src/ because IssuanceManager is at the EIP-170 size limit.
+contract IssuanceManagerWithMigration is IssuanceManager {
+ address public constant NEW_UPGRADE_FACTORY = 0xD353972D7955F421d94d0eA8c42c88c417F7155A;
+
+ /// @notice Migrate legacy contracts and set upgradeFactory to the known new contract (for reference implementation lookup)
+ /// Also migrate its beacons for CyberCertPrinter and CyberScrip to new reference implementations
+ function migrateUpgradeFactory() public {
+ IssuanceManagerStorage.setUpgradeFactory(NEW_UPGRADE_FACTORY);
+
+ address cyberCertPrinterRefImpl = IIssuanceManagerFactory(NEW_UPGRADE_FACTORY)
+ .getCyberCertPrinterRefImplementation();
+ IssuanceManagerStorage.upgradeCertPrinterBeaconImplementation(cyberCertPrinterRefImpl);
+ emit CertPrinterBeaconImplementationUpgraded(cyberCertPrinterRefImpl);
+
+ address cyberScripRefImpl = IIssuanceManagerFactory(NEW_UPGRADE_FACTORY).getCyberScripRefImplementation();
+ if (address(IssuanceManagerStorage.getCyberScripBeacon()) == address(0)) {
+ UpgradeableBeacon beaconScrip = new UpgradeableBeacon(cyberScripRefImpl, address(this));
+ IssuanceManagerStorage.setCyberScripBeacon(beaconScrip);
+ emit ScripBeaconImplementationUpgraded(cyberScripRefImpl);
+ } else {
+ IssuanceManagerStorage.updateScripBeaconImplementation(cyberScripRefImpl);
+ emit ScripBeaconImplementationUpgraded(cyberScripRefImpl);
+ }
+ }
+}