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 ''; + return ''; + } + + /// @notice Static frame: corner motifs, top/bottom bands, stats panel and bottom tiles + function _getSVGFrame(bool voided) private pure returns (string memory) { + return string(abi.encodePacked( + _getCornerMotifs(), + _getTopDecorations(), + _getTopBand(voided), + _getStatsPanel(), + _getBottomBand(), + _getBottomTiles() + )); } - function _getSVGBackground() private pure returns (string memory) { + function _getCornerMotifs() private pure returns (string memory) { return string(abi.encodePacked( - '', - '', - '', - _getSVGDecorativeElements() + '', + '', + '' )); } - function _getSVGDecorativeElements() private pure returns (string memory) { + function _getTopDecorations() private pure returns (string memory) { return string(abi.encodePacked( - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - _generateBottomDecorations(), + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', '' )); } - function _getSVGFooter() private pure returns (string memory) { - return ''; + function _getTopBand(bool voided) private pure returns (string memory) { + return string(abi.encodePacked( + '', + '', + '', + '', + '', + '', + _getStatusPill(voided), + '', + '' + )); + } + + /// @notice Status pill next to the "active"/"voided" label: lime when active, red when voided + function _getStatusPill(bool voided) private pure returns (string memory) { + if (voided) { + return string(abi.encodePacked( + '', + '' + )); + } + return string(abi.encodePacked( + '', + '' + )); + } + + function _getStatsPanel() private pure returns (string memory) { + return string(abi.encodePacked( + '', + '', + '', + '', + '' + )); + } + + function _getBottomBand() private pure returns (string memory) { + return string(abi.encodePacked( + '', + '', + '', + '', + '' + )); + } + + /// @notice Bottom "In code we trust" tile row, defined once and reused via + function _getBottomTiles() private pure returns (string memory) { + return string(abi.encodePacked( + '', + '', + '', + _getBottomTileText(), + '', + '', + '', + '' + )); + } + + /// @notice The "In code we trust" text paths for the bottom tile + function _getBottomTileText() private pure returns (string memory) { + return ''; } function _generateMetaLeXLogo() private pure returns (string memory) { @@ -134,43 +193,6 @@ library CertificateImageBuilder { ); } - function _generateBottomDecorations() private pure returns (string memory) { - return string(abi.encodePacked( - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '' - )); - } - function _generateSVGBody(Accreditation memory acc) private pure returns (string memory) { return string( abi.encodePacked( diff --git a/src/CertificateImageBuilderContract.sol b/src/CertificateImageBuilderContract.sol index ba8ed0a..f707ec9 100644 --- a/src/CertificateImageBuilderContract.sol +++ b/src/CertificateImageBuilderContract.sol @@ -43,466 +43,19 @@ pragma solidity ^0.8.28; import "./CyberCorpConstants.sol"; import "./interfaces/ICertificateImageBuilder.sol"; +import "./CertificateImageBuilder.sol"; /// @title CertificateImageBuilderContract /// @notice Standalone contract for building certificate SVG images -/// @dev Deployed separately to reduce CertificateUriBuilder contract size +/// @dev Deployed separately to reduce CertificateUriBuilder contract size; +/// delegates rendering to the shared CertificateImageBuilder library contract CertificateImageBuilderContract is ICertificateImageBuilder { - + /// @inheritdoc ICertificateImageBuilder function buildCertificateSVG( CertificateSVGParams calldata params, uint256 timestamp ) external pure override returns (string memory) { - return string(abi.encodePacked( - _getSVGHeader(), - _getSVGBackground(), - _buildSVGContent(params, timestamp), - '' - )); - } - - 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); + } + } +}