From 21134eaac4357ac70ca68941ad7133cf670ecbb2 Mon Sep 17 00:00:00 2001 From: greypixel Date: Mon, 18 May 2026 15:05:15 +0100 Subject: [PATCH 1/7] first pass --- foundry.toml | 1 + script/public-round-test-deploy.s.sol | 3 +- src/CertificateUriBuilder.sol | 140 +++++++----------- src/CyberCertPrinter.sol | 13 +- src/CyberCorpFactory.sol | 4 +- src/DealManager.sol | 4 +- src/IssuanceManager.sol | 16 +- src/interfaces/ICyberCertPrinter.sol | 5 +- src/interfaces/IIssuanceManager.sol | 5 +- src/interfaces/IUriBuilder.sol | 1 + src/storage/CyberCertPrinterStorage.sol | 26 ++-- src/storage/RoundManagerStorage.sol | 4 +- .../extensions/ICertificateExtension.sol | 7 + test/CyberCorpTest.t.sol | 45 ++++-- test/CyberCorpUpgradeabilityTest.t.sol | 3 +- test/CyberScripUpgradeTest.t.sol | 12 +- test/FactoryArbitraryErc20RoundPOC.t.sol | 3 +- test/IssuanceManagerConversionTest.t.sol | 9 +- test/RoundManagerTest.t.sol | 24 ++- test/ScripPOC.t.sol | 3 +- test/ShareExtensionTest.t.sol | 3 +- test/UpgradeLegacyCyberCorpsTest.t.sol | 9 +- test/UpgradePublicRoundsTest.t.sol | 6 +- ...pgradeRoundManagerTokenWhitelistTest.t.sol | 3 +- 24 files changed, 208 insertions(+), 141 deletions(-) diff --git a/foundry.toml b/foundry.toml index aeb1b570..b3ab94ca 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,6 +4,7 @@ out = "out" libs = ["lib", "dependencies"] optimizer = true optimizer_runs = 15 +via_ir = true fs_permissions = [{ access = "read", path = "./script/res" }] [fmt] diff --git a/script/public-round-test-deploy.s.sol b/script/public-round-test-deploy.s.sol index 5aad936d..84b0f1d1 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/CertificateUriBuilder.sol b/src/CertificateUriBuilder.sol index 13e257ff..e9e9daee 100644 --- a/src/CertificateUriBuilder.sol +++ b/src/CertificateUriBuilder.sol @@ -46,6 +46,7 @@ import "./CyberCorpConstants.sol"; import "./interfaces/ICyberAgreementRegistry.sol"; import "./interfaces/ICertificateImageBuilder.sol"; import "./storage/extensions/ICertificateExtension.sol"; +import "./interfaces/ICyberCertPrinter.sol"; import "./libs/auth.sol"; contract CertificateUriBuilder is UUPSUpgradeable, BorgAuthACL { @@ -442,85 +443,26 @@ struct CertificateDetails { 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), - '", "legalDetails": "', details.legalDetails, - '"' + string memory json = buildCertificateUriNotEncoded( + cyberCORPName, + cyberCORPType, + cyberCORPJurisdiction, + cyberCORPContactDetails, + securityType, + securitySeries, + certificateUri, + certLegend, + details, + endorsements, + owner, + registry, + agreementId, + tokenId, + contractAddress, + extension ); - - //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": ', arrayToJsonString(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( @@ -543,7 +485,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); @@ -585,6 +527,7 @@ struct CertificateDetails { '"' ); + // Add certificate details json = string.concat(json, ', "signingOfficerName": "', details.signingOfficerName, @@ -597,8 +540,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 @@ -617,11 +563,39 @@ 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; } + 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( + ICertificateExtensionV2.getExtensionURI.selector, + printerExtensionData, + certificateExtensionData + ) + ); + + if (success) { + return abi.decode(result, (string)); + } + + // Fall back to `getExtensionUri` without certPrinter data + return ICertificateExtension(extension).getExtensionURI(certificateExtensionData); + } + function _authorizeUpgrade( address newImplementation ) internal virtual override onlyOwner {} diff --git a/src/CyberCertPrinter.sol b/src/CyberCertPrinter.sol index 81d95bc8..f82a88f7 100644 --- a/src/CyberCertPrinter.sol +++ b/src/CyberCertPrinter.sol @@ -91,6 +91,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(bytes32 indexed dataHash); modifier onlyIssuanceManager() { @@ -103,7 +104,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(); @@ -115,6 +116,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { s.certificateUri = _certificateUri; s.endorsementRequired = true; s.extension = _extension; + s.printerExtensionData = _printerExtensionData; } function updateIssuanceManager(address _issuanceManager) external onlyIssuanceManager { @@ -491,6 +493,15 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { return CyberCertPrinterStorage._getExtensionData(tokenId); } + function setPrinterExtensionData(bytes calldata data) external onlyIssuanceManager { + CyberCertPrinterStorage.cyberCertStorage().printerExtensionData = data; + emit PrinterExtensionDataSet(keccak256(data)); + } + + 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/CyberCorpFactory.sol b/src/CyberCorpFactory.sol index f5d79942..5a66b8c8 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 55ca5301..22aba92e 100644 --- a/src/DealManager.sol +++ b/src/DealManager.sol @@ -70,6 +70,7 @@ contract DealManager is Initializable, BorgAuthACL, LexScroWLite, UUPSUpgradeabl SecuritySeries securitySeries; address extension; string[] defaultLegend; + bytes printerExtensionData; } error ZeroAddress(); @@ -559,7 +560,8 @@ contract DealManager is Initializable, BorgAuthACL, LexScroWLite, UUPSUpgradeabl _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/IssuanceManager.sol b/src/IssuanceManager.sol index b02e58df..569d8704 100644 --- a/src/IssuanceManager.sol +++ b/src/IssuanceManager.sol @@ -112,6 +112,8 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { uint256 indexed id, bool isWhitelisted ); + event CertPrinterExtensionDataSet(address indexed certPrinter, bytes32 dataHash); + event CyberScripDeployed( address indexed certPrinterAddress, address indexed cyberScripAddress, @@ -208,7 +210,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) { bytes32 salt = keccak256( abi.encodePacked( @@ -226,7 +229,8 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address(this), _securityType, _securitySeries, - _extension + _extension, + _printerExtensionData ); emit CertPrinterCreated( newCert, @@ -671,6 +675,14 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { certificate.setTokenTransferable(tokenId, value); } + function setCertPrinterExtensionData( + address certPrinter, + bytes calldata data + ) external onlyOwner { + ICyberCertPrinter(certPrinter).setPrinterExtensionData(data); + emit CertPrinterExtensionDataSet(certPrinter, keccak256(data)); + } + /// @notice Sets the minimum scrip amount required to convert back into certs /// @dev Only callable by owner; set to 0 to disable the minimum /// @param certAddress Address of the certificate printer contract diff --git a/src/interfaces/ICyberCertPrinter.sol b/src/interfaces/ICyberCertPrinter.sol index 87e85556..117a6636 100644 --- a/src/interfaces/ICyberCertPrinter.sol +++ b/src/interfaces/ICyberCertPrinter.sol @@ -54,7 +54,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); @@ -136,4 +137,6 @@ interface ICyberCertPrinter is IERC721 { function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); function legalOwnerOf(uint256 tokenId) external view returns (address); function setTokenTransferable(uint256 tokenId, bool value) external; + function setPrinterExtensionData(bytes calldata data) external; + function getPrinterExtensionData() external view returns (bytes memory); } diff --git a/src/interfaces/IIssuanceManager.sol b/src/interfaces/IIssuanceManager.sol index e7a49397..ed388b90 100644 --- a/src/interfaces/IIssuanceManager.sol +++ b/src/interfaces/IIssuanceManager.sol @@ -93,7 +93,8 @@ interface IIssuanceManager { string memory _certificateUri, SecurityClass _securityType, SecuritySeries _securitySeries, - address _extension + address _extension, + bytes memory _printerExtensionData ) external returns (address); function createCert( @@ -247,6 +248,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 b693ef72..e178b718 100644 --- a/src/interfaces/IUriBuilder.sol +++ b/src/interfaces/IUriBuilder.sol @@ -83,3 +83,4 @@ interface IUriBuilder { address extension ) external view returns (string memory); } + diff --git a/src/storage/CyberCertPrinterStorage.sol b/src/storage/CyberCertPrinterStorage.sol index 99e35817..87b84d72 100644 --- a/src/storage/CyberCertPrinterStorage.sol +++ b/src/storage/CyberCertPrinterStorage.sol @@ -100,6 +100,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; } @@ -114,12 +115,7 @@ library CyberCertPrinterStorage { // URI storage functionality function tokenURI(uint256 tokenId) external view returns (string memory) { CyberCertPrinterStorage.CyberCertStorage storage s = cyberCertStorage(); - string[] memory certLegend = s.certLegend[tokenId]; ICyberCorp corp = ICyberCorp(IIssuanceManager(s.issuanceManager).CORP()); - CertificateDetails memory effectiveDetails = getCertificateDetails( - tokenId - ); - // Get registry and agreementId from first endorsement if it exists address registry = address(0); bytes32 agreementId = bytes32(0); @@ -129,7 +125,11 @@ library CyberCertPrinterStorage { agreementId = firstEndorsement.agreementId; } - return IUriBuilder(IIssuanceManager(s.issuanceManager).uriBuilder()).buildCertificateUri( + IUriBuilder builder = IUriBuilder(IIssuanceManager(s.issuanceManager).uriBuilder()); + string[] memory certLegend = s.certLegend[tokenId]; + Endorsement[] memory endorsements = s.endorsements[tokenId]; + OwnerDetails memory owner = s.owners[tokenId]; + return builder.buildCertificateUri( corp.cyberCORPName(), corp.cyberCORPType(), corp.cyberCORPJurisdiction(), @@ -138,9 +138,9 @@ library CyberCertPrinterStorage { s.securitySeries, s.certificateUri, certLegend, - effectiveDetails, - s.endorsements[tokenId], - s.owners[tokenId], + getCertificateDetails(tokenId), + endorsements, + owner, registry, agreementId, tokenId, @@ -260,4 +260,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/RoundManagerStorage.sol b/src/storage/RoundManagerStorage.sol index 64d917f7..e0f667d0 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/ICertificateExtension.sol b/src/storage/extensions/ICertificateExtension.sol index 3e158cfa..a4ef7fda 100644 --- a/src/storage/extensions/ICertificateExtension.sol +++ b/src/storage/extensions/ICertificateExtension.sol @@ -44,4 +44,11 @@ pragma solidity 0.8.28; interface ICertificateExtension { function supportsExtensionType(bytes32 extensionType) external pure returns (bool); function getExtensionURI(bytes memory data) external view returns (string memory); +} + +interface ICertificateExtensionV2 { + function getExtensionURI( + bytes memory printerExtensionData, + bytes memory certificateExtensionData + ) external view returns (string memory); } \ No newline at end of file diff --git a/test/CyberCorpTest.t.sol b/test/CyberCorpTest.t.sol index b2c26f06..d1c69a2e 100644 --- a/test/CyberCorpTest.t.sol +++ b/test/CyberCorpTest.t.sol @@ -133,7 +133,8 @@ contract CyberCorpTest 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; @@ -650,7 +651,8 @@ contract CyberCorpTest is Test { securityClass: SecurityClass.SAFE, securitySeries: SecuritySeries.SeriesPreSeed, extension: address(0), - defaultLegend: defaultLegends[0] + defaultLegend: defaultLegends[0], + printerExtensionData: hex"" }); certData[1] = CyberCorpFactory.CyberCertData({ @@ -660,7 +662,8 @@ contract CyberCorpTest is Test { securityClass: SecurityClass.TokenWarrant, securitySeries: SecuritySeries.SeriesPreSeed, extension: address(0), - defaultLegend: defaultLegends[1] + defaultLegend: defaultLegends[1], + printerExtensionData: hex"" }); vm.startPrank(testAddress); @@ -2689,7 +2692,8 @@ contract CyberCorpTest is Test { securityClass: SecurityClass.SAFE, securitySeries: SecuritySeries.SeriesPreSeed, extension: address(0), - defaultLegend: defaultLegend + defaultLegend: defaultLegend, + printerExtensionData: hex"" }); ( @@ -3623,7 +3627,8 @@ contract CyberCorpTest is Test { "ipfs://test", SecurityClass.SAFE, SecuritySeries.SeriesPreSeed, - address(0) + address(0), + hex"" ); // Deploy new implementation @@ -3754,7 +3759,8 @@ contract CyberCorpTest is Test { "ipfs://test", SecurityClass.SAFE, SecuritySeries.SeriesPreSeed, - address(0) + address(0), + hex"" ); // Deploy new implementation @@ -4160,7 +4166,8 @@ contract CyberCorpTest is Test { securityClass: SecurityClass.SAFE, securitySeries: SecuritySeries.SeriesPreSeed, extension: address(0), - defaultLegend: defaultLegend + defaultLegend: defaultLegend, + printerExtensionData: hex"" }); // Prepare deal parameters @@ -4322,7 +4329,8 @@ contract CyberCorpTest is Test { securityClass: SecurityClass.SAFE, securitySeries: SecuritySeries.SeriesPreSeed, extension: address(0), - defaultLegend: safeLegend + defaultLegend: safeLegend, + printerExtensionData: hex"" }); certData[1] = DealManager.CyberCertData({ name: "Token Warrant", @@ -4331,7 +4339,8 @@ contract CyberCorpTest is Test { securityClass: SecurityClass.TokenWarrant, securitySeries: SecuritySeries.SeriesPreSeed, extension: address(0), - defaultLegend: warrantLegend + defaultLegend: warrantLegend, + printerExtensionData: hex"" }); // Prepare deal parameters @@ -4518,7 +4527,8 @@ contract CyberCorpTest is Test { securityClass: SecurityClass.SAFE, securitySeries: SecuritySeries.SeriesPreSeed, extension: address(0), - defaultLegend: defaultLegend + defaultLegend: defaultLegend, + printerExtensionData: hex"" }); bytes32 templateId = bytes32(uint256(1)); @@ -5692,7 +5702,8 @@ contract CyberCorpTest 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 @@ -5819,7 +5830,8 @@ contract CyberCorpTest is Test { "ipfs://test", SecurityClass.SAFE, SecuritySeries.SeriesSeed, - address(0) + address(0), + hex"" ); CertificateDetails memory cd = CertificateDetails({ @@ -5954,7 +5966,8 @@ contract CyberCorpTest is Test { "ipfs://uri", SecurityClass.SAFE, SecuritySeries.SeriesSeed, - address(0) + address(0), + hex"" ); CertificateDetails memory cd = CertificateDetails({ signingOfficerName: "", @@ -6061,7 +6074,8 @@ contract CyberCorpTest is Test { "ipfs://uri", SecurityClass.SAFE, SecuritySeries.SeriesSeed, - address(0) + address(0), + hex"" ); CertificateDetails memory cd = CertificateDetails({ signingOfficerName: "", @@ -6143,7 +6157,8 @@ contract CyberCorpTest is Test { "ipfs://uri", SecurityClass.SAFE, SecuritySeries.SeriesSeed, - address(0) + address(0), + hex"" ); CertificateDetails memory cd = CertificateDetails({ signingOfficerName: "", diff --git a/test/CyberCorpUpgradeabilityTest.t.sol b/test/CyberCorpUpgradeabilityTest.t.sol index f2f87984..c06c3452 100644 --- a/test/CyberCorpUpgradeabilityTest.t.sol +++ b/test/CyberCorpUpgradeabilityTest.t.sol @@ -241,7 +241,8 @@ contract CyberCorpUpgradeabilityTest 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/CyberScripUpgradeTest.t.sol b/test/CyberScripUpgradeTest.t.sol index fe4f118b..74ecc69e 100644 --- a/test/CyberScripUpgradeTest.t.sol +++ b/test/CyberScripUpgradeTest.t.sol @@ -174,7 +174,8 @@ contract CyberScripUpgradeTest 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"); @@ -252,7 +253,8 @@ contract CyberScripUpgradeTest 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); @@ -1452,7 +1454,8 @@ contract CyberScripUpgradeTest 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); @@ -1509,7 +1512,8 @@ contract CyberScripUpgradeTest is Test { "uri://cert", SecurityClass.CommonStock, SecuritySeries.SeriesA, - address(0) + address(0), + hex"" ) ); } diff --git a/test/FactoryArbitraryErc20RoundPOC.t.sol b/test/FactoryArbitraryErc20RoundPOC.t.sol index b754a1bb..9fb4203e 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 35f0797a..a8bda51d 100644 --- a/test/IssuanceManagerConversionTest.t.sol +++ b/test/IssuanceManagerConversionTest.t.sol @@ -298,7 +298,8 @@ contract IssuanceManagerConversionTest is Test { "uri://safe", SecurityClass.SAFT, SecuritySeries.NA, - address(0) + address(0), + hex"" ) ); equityPrinter = ICyberCertPrinter( @@ -309,7 +310,8 @@ contract IssuanceManagerConversionTest is Test { "uri://eq", SecurityClass.PreferredStock, SecuritySeries.SeriesA, - address(0) + address(0), + hex"" ) ); @@ -1017,7 +1019,8 @@ contract IssuanceManagerConversionTest is Test { "uri://cert", SecurityClass.CommonStock, SecuritySeries.SeriesA, - address(0) + address(0), + hex"" ) ); } diff --git a/test/RoundManagerTest.t.sol b/test/RoundManagerTest.t.sol index 3d6d6736..cb779359 100644 --- a/test/RoundManagerTest.t.sol +++ b/test/RoundManagerTest.t.sol @@ -327,7 +327,8 @@ library CyberCorpHelper { securityClass: SecurityClass.CommonStock, securitySeries: SecuritySeries.NA, extension: address(0), - defaultLegend: defaultLegend + defaultLegend: defaultLegend, + printerExtensionData: hex"" }); string[] memory roundPartyValues = new string[](2); @@ -409,7 +410,8 @@ library CyberCorpHelper { securityClass: SecurityClass.CommonStock, securitySeries: SecuritySeries.NA, extension: address(0), - defaultLegend: defaultLegend + defaultLegend: defaultLegend, + printerExtensionData: hex"" }); string[] memory roundPartyValues = new string[](2); @@ -891,7 +893,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( @@ -2121,7 +2124,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 @@ -2478,7 +2482,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", @@ -2487,7 +2492,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 @@ -2737,7 +2743,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"; @@ -3972,7 +3979,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/ScripPOC.t.sol b/test/ScripPOC.t.sol index 0820ae4f..499cf5da 100644 --- a/test/ScripPOC.t.sol +++ b/test/ScripPOC.t.sol @@ -596,7 +596,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/ShareExtensionTest.t.sol b/test/ShareExtensionTest.t.sol index 6899615a..36bec331 100644 --- a/test/ShareExtensionTest.t.sol +++ b/test/ShareExtensionTest.t.sol @@ -418,7 +418,8 @@ contract ShareExtensionTest 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 index 30ebb8f2..3b465e58 100644 --- a/test/UpgradeLegacyCyberCorpsTest.t.sol +++ b/test/UpgradeLegacyCyberCorpsTest.t.sol @@ -152,7 +152,8 @@ contract UpgradeLegacyCyberCorpsTest is Test { securityClass: SecurityClass.CommonStock, securitySeries: SecuritySeries.NA, extension: address(0), - defaultLegend: new string[](0) + defaultLegend: new string[](0), + printerExtensionData: hex"" }); } @@ -310,7 +311,8 @@ contract UpgradeLegacyCyberCorpsTest is Test { defaultCertData[j].uri, defaultCertData[j].securityClass, defaultCertData[j].securitySeries, - defaultCertData[j].extension + defaultCertData[j].extension, + defaultCertData[j].printerExtensionData ); } @@ -614,7 +616,8 @@ contract UpgradeLegacyCyberCorpsTest is Test { defaultCertData[j].uri, defaultCertData[j].securityClass, defaultCertData[j].securitySeries, - defaultCertData[j].extension + defaultCertData[j].extension, + defaultCertData[j].printerExtensionData ); } diff --git a/test/UpgradePublicRoundsTest.t.sol b/test/UpgradePublicRoundsTest.t.sol index f3034390..fe607b44 100644 --- a/test/UpgradePublicRoundsTest.t.sol +++ b/test/UpgradePublicRoundsTest.t.sol @@ -143,7 +143,8 @@ contract UpgradePublicRoundsTest 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); @@ -531,7 +532,8 @@ contract UpgradePublicRoundsTest 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 index 6fd5a54b..c851e173 100644 --- a/test/UpgradeRoundManagerTokenWhitelistTest.t.sol +++ b/test/UpgradeRoundManagerTokenWhitelistTest.t.sol @@ -158,7 +158,8 @@ contract UpgradeRoundManagerTokenWhitelistTest is Test { securityClass: SecurityClass.CommonStock, securitySeries: SecuritySeries.NA, extension: address(0), - defaultLegend: defaultLegend + defaultLegend: defaultLegend, + printerExtensionData: hex"" }); address[] memory conditions = new address[](1); From 61643676217c7e94bffeeceac4039ebfce7201f2 Mon Sep 17 00:00:00 2001 From: greypixel Date: Wed, 20 May 2026 10:36:43 +0100 Subject: [PATCH 2/7] AI gen'd test --- test/CertificateExtensionDataTest.t.sol | 171 ++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 test/CertificateExtensionDataTest.t.sol diff --git a/test/CertificateExtensionDataTest.t.sol b/test/CertificateExtensionDataTest.t.sol new file mode 100644 index 00000000..3fbd10d4 --- /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; + } +} From 1cf9fd92aae1c2118df94cf784438fd6d66a96c9 Mon Sep 17 00:00:00 2001 From: greypixel Date: Wed, 20 May 2026 11:53:29 +0100 Subject: [PATCH 3/7] Add empty data guard --- src/CertificateUriBuilder.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CertificateUriBuilder.sol b/src/CertificateUriBuilder.sol index e9e9daee..c5969626 100644 --- a/src/CertificateUriBuilder.sol +++ b/src/CertificateUriBuilder.sol @@ -592,6 +592,8 @@ struct CertificateDetails { return abi.decode(result, (string)); } + if (certificateExtensionData.length == 0) return ""; + // Fall back to `getExtensionUri` without certPrinter data return ICertificateExtension(extension).getExtensionURI(certificateExtensionData); } From 13f5dd2e186e069c26645439b1239bb2d0ae0896 Mon Sep 17 00:00:00 2001 From: greypixel Date: Thu, 21 May 2026 18:01:42 +0100 Subject: [PATCH 4/7] interim --- src/CyberCertPrinter.sol | 4 +- src/IssuanceManager.sol | 4 +- src/storage/extensions/ShareExtension.sol | 203 ++++++++++++++++-- .../extensions/ShareExtensionLogic.sol | 12 -- test/ShareExtensionTest.t.sol | 98 +++++++-- 5 files changed, 272 insertions(+), 49 deletions(-) diff --git a/src/CyberCertPrinter.sol b/src/CyberCertPrinter.sol index f82a88f7..589a68ed 100644 --- a/src/CyberCertPrinter.sol +++ b/src/CyberCertPrinter.sol @@ -91,7 +91,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(bytes32 indexed dataHash); + event PrinterExtensionDataSet(); modifier onlyIssuanceManager() { @@ -495,7 +495,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { function setPrinterExtensionData(bytes calldata data) external onlyIssuanceManager { CyberCertPrinterStorage.cyberCertStorage().printerExtensionData = data; - emit PrinterExtensionDataSet(keccak256(data)); + emit PrinterExtensionDataSet(); } function getPrinterExtensionData() external view returns (bytes memory) { diff --git a/src/IssuanceManager.sol b/src/IssuanceManager.sol index 569d8704..cde57282 100644 --- a/src/IssuanceManager.sol +++ b/src/IssuanceManager.sol @@ -112,7 +112,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { uint256 indexed id, bool isWhitelisted ); - event CertPrinterExtensionDataSet(address indexed certPrinter, bytes32 dataHash); + event CertPrinterExtensionDataSet(address indexed certPrinter); event CyberScripDeployed( address indexed certPrinterAddress, @@ -680,7 +680,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { bytes calldata data ) external onlyOwner { ICyberCertPrinter(certPrinter).setPrinterExtensionData(data); - emit CertPrinterExtensionDataSet(certPrinter, keccak256(data)); + emit CertPrinterExtensionDataSet(certPrinter); } /// @notice Sets the minimum scrip amount required to convert back into certs diff --git a/src/storage/extensions/ShareExtension.sol b/src/storage/extensions/ShareExtension.sol index 067e252d..7bd3999e 100644 --- a/src/storage/extensions/ShareExtension.sol +++ b/src/storage/extensions/ShareExtension.sol @@ -194,6 +194,32 @@ struct CertificateData { bool holdingPeriodTackingApplied; } +struct ShareExtensionDataSplitProposal { + string[] moveToPrinterExtensionData; + string[] keepInCertificateExtensionData; + string[] removeBecauseCoveredByRoundManager; +} + +struct SharePrinterExtensionData { + SeriesTerms terms; + MandatoryConversionTrigger[] mandatoryConversionTriggers; + SpecialVotingRight[] specialVotingRights; + TransferRestriction[] transferRestrictions; + SplitRecord[] splitHistory; + ShareExtensionDataSplitProposal dataSplitProposal; +} + +struct ShareCertificateData { + uint256 certificateNumber; + uint256 issueDate; + bool isPartlyPaid; + uint256 amountPaid; + string sourceAuthorityURI; + ShareRepresentationType representationType; + uint256 holdingPeriodStartDate; + bool holdingPeriodTackingApplied; +} + struct ShareCertData { SeriesTerms terms; CertificateData certificateData; @@ -201,11 +227,9 @@ struct ShareCertData { SpecialVotingRight[] specialVotingRights; TransferRestriction[] transferRestrictions; SplitRecord[] splitHistory; - string issuerName; - string stateOfIncorporation; } -contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { +contract ShareExtension is UUPSUpgradeable, ICertificateExtension, ICertificateExtensionV2, BorgAuthACL { bytes32 public constant EXTENSION_TYPE = keccak256("SHARE"); uint256 public constant PERCENTAGE_PRECISION = 10 ** 4; uint256 public constant PRICE_PRECISION = 10 ** 18; @@ -231,21 +255,78 @@ contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { return abi.encode(data); } + function decodePrinterExtensionData(bytes memory data) external pure returns (SharePrinterExtensionData memory) { + return abi.decode(data, (SharePrinterExtensionData)); + } + + function encodePrinterExtensionData(SharePrinterExtensionData memory data) external pure returns (bytes memory) { + return abi.encode(data); + } + + function decodeCertificateExtensionData(bytes memory data) external pure returns (ShareCertificateData memory) { + return abi.decode(data, (ShareCertificateData)); + } + + function encodeCertificateExtensionData(ShareCertificateData 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 ""; + return _buildExtensionURI("", data); + } + + function getExtensionURI( + bytes memory printerExtensionData, + bytes memory certificateExtensionData + ) external pure override returns (string memory) { + return _buildExtensionURI(printerExtensionData, certificateExtensionData); + } + + function _buildExtensionURI( + bytes memory printerExtensionData, + bytes memory certificateExtensionData + ) internal pure returns (string memory) { + if (printerExtensionData.length == 0) { + if (certificateExtensionData.length == 0) return ""; + + ShareCertData memory legacyShare = abi.decode(certificateExtensionData, (ShareCertData)); + return string( + abi.encodePacked( + ', "shareDetails": {', + _buildSeriesJson(legacyShare.terms, legacyShare.certificateData), + _buildCertificateJson(legacyShare.certificateData), + _buildDerivedJson(legacyShare), + '"}' + ) + ); + } + + SharePrinterExtensionData memory printerData = abi.decode(printerExtensionData, (SharePrinterExtensionData)); + + if (certificateExtensionData.length == 0) { + return string( + abi.encodePacked( + ', "shareDetails": {', + _buildSeriesJson(printerData.terms), + _buildPrinterDerivedJson(printerData), + '"}' + ) + ); + } - ShareCertData memory share = abi.decode(data, (ShareCertData)); + ShareCertificateData memory certificateData = abi.decode(certificateExtensionData, (ShareCertificateData)); + CertificateData memory cert = _toCertificateData(certificateData); return string( abi.encodePacked( ', "shareDetails": {', - _buildSeriesJson(share.terms, share.certificateData), - _buildCertificateJson(share.certificateData), - _buildDerivedJson(share), + _buildSeriesJson(printerData.terms), + _buildCertificateJson(certificateData), + _buildDerivedJson(printerData, cert), '"}' ) ); @@ -254,18 +335,43 @@ contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { function _buildSeriesJson( SeriesTerms memory terms, CertificateData memory cert + ) internal pure returns (string memory) { + return string.concat( + _buildSeriesJsonPartOne(terms, Strings.toHexString(uint256(cert.seriesId), 32)), + _buildSeriesJsonPartTwo(terms) + ); + } + + function _buildSeriesJson(SeriesTerms memory terms) internal pure returns (string memory) { + return string.concat( + _buildSeriesJsonPartOne(terms, ""), + _buildSeriesJsonPartTwo(terms) + ); + } + + function _buildSeriesJsonPartOne( + SeriesTerms memory terms, + string memory seriesId ) internal pure returns (string memory) { return string( abi.encodePacked( '"shareClassKey": "', _shareClassKeyToString(terms.shareClassKey), '", "seriesName": "', terms.seriesName, - '", "seriesId": "', Strings.toHexString(uint256(cert.seriesId), 32), + bytes(seriesId).length == 0 ? "" : string.concat('", "seriesId": "', seriesId), '", "authorizedShares": "', Strings.toString(terms.authorizedShares), '", "parValue": "', Strings.toString(terms.parValue), '", "originalIssuePrice": "', Strings.toString(terms.originalIssuePrice), '", "liquidationPreferenceMultiple": "', Strings.toString(terms.liquidationPreferenceMultiple), '", "liquidationPreferenceType": "', _liquidationPreferenceTypeToString(terms.liquidationPreferenceType), - '", "dividendType": "', _dividendTypeToString(terms.dividendType), + '", ' + ) + ); + } + + function _buildSeriesJsonPartTwo(SeriesTerms memory terms) internal pure returns (string memory) { + return string( + abi.encodePacked( + '"dividendType": "', _dividendTypeToString(terms.dividendType), '", "dividendRate": "', Strings.toString(terms.dividendRate), '", "isConvertible": "', _boolToString(terms.isConvertible), '", "conversionPrice": "', Strings.toString(terms.conversionPrice), @@ -297,6 +403,21 @@ contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { ); } + function _buildCertificateJson(ShareCertificateData memory cert) internal pure returns (string memory) { + return string( + abi.encodePacked( + '"certificateNumber": "', Strings.toString(cert.certificateNumber), + '", "issueDate": "', Strings.toString(cert.issueDate), + '", "isPartlyPaid": "', _boolToString(cert.isPartlyPaid), + '", "amountPaid": "', Strings.toString(cert.amountPaid), + '", "representationType": "', _representationTypeToString(cert.representationType), + '", "holdingPeriodStartDate": "', Strings.toString(cert.holdingPeriodStartDate), + '", "holdingPeriodTackingApplied": "', _boolToString(cert.holdingPeriodTackingApplied), + '", ' + ) + ); + } + function _buildDerivedJson(ShareCertData memory share) internal pure returns (string memory) { return string( abi.encodePacked( @@ -306,13 +427,71 @@ contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { '", "splitHistoryCount": "', Strings.toString(share.splitHistory.length), '", "paymentPercentage": "', Strings.toString(_getPaymentPercentage(share.certificateData)), '", "conversionRatio": "', Strings.toString(_getConversionRatio(share.terms)), - '", "issuerName": "', share.issuerName, - '", "stateOfIncorporation": "', share.stateOfIncorporation, '"' ) ); } + function _buildDerivedJson( + SharePrinterExtensionData memory printerData, + CertificateData memory cert + ) internal pure returns (string memory) { + return string( + abi.encodePacked( + _buildPrinterDerivedJson(printerData), + ', "paymentPercentage": "', Strings.toString(_getPaymentPercentage(cert)), + '"' + ) + ); + } + + function _buildPrinterDerivedJson( + SharePrinterExtensionData memory printerData + ) internal pure returns (string memory) { + return string( + abi.encodePacked( + '"mandatoryConversionTriggerCount": "', Strings.toString(printerData.mandatoryConversionTriggers.length), + '", "specialVotingRightCount": "', Strings.toString(printerData.specialVotingRights.length), + '", "transferRestrictionCount": "', Strings.toString(printerData.transferRestrictions.length), + '", "splitHistoryCount": "', Strings.toString(printerData.splitHistory.length), + '", "conversionRatio": "', Strings.toString(_getConversionRatio(printerData.terms)), + '"' + ) + ); + } + + function _toCertificateData( + ShareCertificateData memory data + ) internal pure returns (CertificateData memory certificateData) { + certificateData = CertificateData({ + seriesId: bytes32(0), + certificateNumber: data.certificateNumber, + numberOfShares: 0, + issueDate: data.issueDate, + isPartlyPaid: data.isPartlyPaid, + amountPaid: data.amountPaid, + totalConsideration: 0, + sourceAuthorityURI: data.sourceAuthorityURI, + representationType: data.representationType, + holdingPeriodStartDate: data.holdingPeriodStartDate, + holdingPeriodTackingApplied: data.holdingPeriodTackingApplied + }); + } + + function _emptyCertificateData() internal pure returns (CertificateData memory certificateData) {} + + function _bytesToHexString(bytes memory data) internal pure returns (string memory) { + bytes16 symbols = "0123456789abcdef"; + bytes memory buffer = new bytes(2 + data.length * 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 0; i < data.length; i++) { + buffer[2 + i * 2] = symbols[uint8(data[i] >> 4)]; + buffer[3 + i * 2] = symbols[uint8(data[i] & 0x0f)]; + } + return string(buffer); + } + function _shareClassKeyToString(bytes32 key) internal pure returns (string memory) { if (key == keccak256("COMMON")) return "Common"; if (key == keccak256("PREFERRED")) return "Preferred"; diff --git a/src/storage/extensions/ShareExtensionLogic.sol b/src/storage/extensions/ShareExtensionLogic.sol index 2eec9f7e..3df63f35 100644 --- a/src/storage/extensions/ShareExtensionLogic.sol +++ b/src/storage/extensions/ShareExtensionLogic.sol @@ -114,18 +114,6 @@ contract ShareExtensionLogic { return abi.encode(share); } - function setIssuerName(bytes memory data, string memory newIssuerName) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.issuerName = newIssuerName; - return abi.encode(share); - } - - function setStateOfIncorporation(bytes memory data, string memory newState) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.stateOfIncorporation = newState; - return abi.encode(share); - } - function addConversionTrigger( bytes memory data, MandatoryConversionTrigger memory conversionTrigger diff --git a/test/ShareExtensionTest.t.sol b/test/ShareExtensionTest.t.sol index 36bec331..ee585021 100644 --- a/test/ShareExtensionTest.t.sol +++ b/test/ShareExtensionTest.t.sol @@ -28,6 +28,9 @@ import {CyberCertData, EOI, LexChexDetails, MintRequest} from "../src/storage/Ro import { ShareExtension, ShareCertData, + SharePrinterExtensionData, + ShareCertificateData, + ShareExtensionDataSplitProposal, SeriesTerms, CertificateData, MandatoryConversionTrigger, @@ -55,6 +58,12 @@ contract MockPaymentToken is ERC20 { } } +contract MockLexChex { + function hasValidLexCheX(address) external pure returns (bool) { + return true; + } +} + contract ShareExtensionTest is Test { using RoundLib for Round; @@ -88,7 +97,9 @@ contract ShareExtensionTest is Test { bytes32 internal roundId; bytes32 internal agreementId; uint256 internal tokenId; - bytes internal initialShareData; + bytes internal initialLegacyShareData; + bytes internal initialCertificateShareData; + bytes internal initialPrinterShareData; function setUp() public { officer = vm.addr(officerPrivKey); @@ -106,7 +117,7 @@ contract ShareExtensionTest is Test { function testSetUp_MintsSeriesAShareCertThroughFactoryRoundFlow() public { assertEq(certPrinter.totalSupply(), 1); assertEq(certPrinter.getExtension(tokenId), address(shareExtension)); - assertEq(keccak256(certPrinter.getExtensionData(tokenId)), keccak256(initialShareData)); + assertEq(keccak256(certPrinter.getExtensionData(tokenId)), keccak256(initialCertificateShareData)); CertificateDetails memory details = certPrinter.getCertificateDetails(tokenId); assertEq(details.signingOfficerName, "Series A Officer"); @@ -116,15 +127,18 @@ contract ShareExtensionTest is Test { assertEq(details.unitsRepresented, (OFFER_AMOUNT * 1e18) / PRICE_PER_SHARE); assertEq(details.legalDetails, "Series A Preferred Stock Certificate"); - string memory shareJson = shareExtension.getExtensionURI(details.extensionData); + string memory shareJson = shareExtension.getExtensionURI(certPrinter.getPrinterExtensionData(), details.extensionData); assertTrue(_contains(shareJson, '"shareDetails": {')); assertTrue(_contains(shareJson, '"seriesName": "Series A Preferred"')); - assertTrue(_contains(shareJson, '"issuerName": "Test CyberCorp"')); - assertTrue(_contains(certPrinter.tokenURI(tokenId), "data:application/json;base64,")); + assertTrue(_contains(shareJson, '"certificateNumber": "1001"')); + assertTrue(_contains(shareJson, '"conversionRatio": "1000000000000000000"')); + string memory tokenUri = certPrinter.tokenURI(tokenId); + assertTrue(_contains(tokenUri, "data:application/json;base64,")); + assertEq(keccak256(certPrinter.getPrinterExtensionData()), keccak256(initialPrinterShareData)); } function testLogic_DecodeAndValidateLiveSharePayload() public { - bytes memory storedData = certPrinter.getExtensionData(tokenId); + bytes memory storedData = initialLegacyShareData; ShareCertData memory formatterDecoded = shareExtension.decodeExtensionData(storedData); ShareCertData memory logicDecoded = shareLogic.decodeExtensionData(storedData); @@ -133,7 +147,6 @@ contract ShareExtensionTest is Test { assertTrue(valid); assertEq(error, ""); assertEq(formatterDecoded.terms.seriesName, "Series A Preferred"); - assertEq(logicDecoded.issuerName, "Test CyberCorp"); assertEq(logicDecoded.certificateData.certificateNumber, 1001); assertEq(logicDecoded.transferRestrictions.length, 1); assertEq(logicDecoded.specialVotingRights.length, 1); @@ -141,7 +154,7 @@ contract ShareExtensionTest is Test { } function testLogic_UpdateConversionPriceAndRatio() public { - bytes memory updatedData = shareLogic.updateConversionPrice(certPrinter.getExtensionData(tokenId), 8e18); + bytes memory updatedData = shareLogic.updateConversionPrice(initialLegacyShareData, 8e18); ShareCertData memory decoded = shareLogic.decodeExtensionData(updatedData); uint256 ratio = shareLogic.getConversionRatio(updatedData); @@ -154,7 +167,7 @@ contract ShareExtensionTest is Test { function testLogic_RecordStockSplitAdjustsSharePayload() public { bytes memory updatedData = - shareLogic.recordStockSplit(certPrinter.getExtensionData(tokenId), 2, 1, "ipfs://split-board-consent", block.timestamp); + shareLogic.recordStockSplit(initialLegacyShareData, 2, 1, "ipfs://split-board-consent", block.timestamp); ShareCertData memory decoded = shareLogic.decodeExtensionData(updatedData); @@ -171,13 +184,13 @@ contract ShareExtensionTest is Test { } function testLogic_ComputesAccruedDividendsForCumulativeSeries() public { - uint256 accrued = shareLogic.computeAccruedDividends(certPrinter.getExtensionData(tokenId), block.timestamp + 365 days); + uint256 accrued = shareLogic.computeAccruedDividends(initialLegacyShareData, block.timestamp + 365 days); assertEq(accrued, 10_000e18); } function testLogic_ManagesDynamicArrayPayloadSections() public { - bytes memory workingData = certPrinter.getExtensionData(tokenId); + bytes memory workingData = initialLegacyShareData; MandatoryConversionTrigger memory trigger = MandatoryConversionTrigger({ triggerType: MandatoryConversionTriggerType.Custom, @@ -230,7 +243,7 @@ contract ShareExtensionTest is Test { } function testLogic_RejectsInvalidSeriesTerms() public { - ShareCertData memory shareData = shareLogic.decodeExtensionData(certPrinter.getExtensionData(tokenId)); + ShareCertData memory shareData = shareLogic.decodeExtensionData(initialLegacyShareData); shareData.terms.isConvertible = false; shareData.terms.conversionPrice = 1e18; shareData.terms.targetConversionSeriesId = bytes32(0); @@ -240,8 +253,9 @@ contract ShareExtensionTest is Test { assertFalse(valid); assertEq(error, "ShareExtensionLogic: conversionPrice must be 0 when not convertible"); + bytes memory storedData = initialLegacyShareData; vm.expectRevert(bytes("ShareExtensionLogic: conversionPrice must be 0 when not convertible")); - shareLogic.updateSeriesTerms(certPrinter.getExtensionData(tokenId), shareData.terms); + shareLogic.updateSeriesTerms(storedData, shareData.terms); } function _deployFactories() internal { @@ -358,9 +372,10 @@ contract ShareExtensionTest is Test { address issuanceManagerAddress; address dealManagerAddress; address roundManagerAddress; + address authAddress; ( corpAddress, - , + authAddress, issuanceManagerAddress, dealManagerAddress, roundManagerAddress @@ -378,6 +393,11 @@ contract ShareExtensionTest is Test { issuanceManager = IIssuanceManager(issuanceManagerAddress); roundManager = RoundManager(roundManagerAddress); + vm.startPrank(address(corpFactory)); + BorgAuth(authAddress).updateRole(address(this), BorgAuth(authAddress).OWNER_ROLE()); + vm.stopPrank(); + roundManager.setLexChex(address(new MockLexChex())); + dealManagerAddress; } @@ -385,8 +405,10 @@ contract ShareExtensionTest is Test { paymentToken = new MockPaymentToken(); paymentToken.mint(investor, OFFER_AMOUNT * 2); - initialShareData = _buildInitialShareData(); - roundId = _createSeriesARound(initialShareData); + initialLegacyShareData = _buildInitialShareData(); + initialCertificateShareData = _buildInitialCertificateShareData(); + initialPrinterShareData = _buildInitialPrinterShareData(); + roundId = _createSeriesARound(initialCertificateShareData); vm.startPrank(investor); paymentToken.approve(address(roundManager), type(uint256).max); @@ -419,7 +441,7 @@ contract ShareExtensionTest is Test { securitySeries: SecuritySeries.SeriesA, extension: address(shareExtension), defaultLegend: legend, - printerExtensionData: hex"" + printerExtensionData: initialPrinterShareData }); string[] memory legalDetails = new string[](1); @@ -556,20 +578,54 @@ contract ShareExtensionTest is Test { totalConsideration: 100_000e18, sourceAuthorityURI: "ipfs://board-approval", representationType: ShareRepresentationType.Certificated, - holdingPeriodStartDate: block.timestamp - 30 days, + holdingPeriodStartDate: block.timestamp > 30 days ? block.timestamp - 30 days : 0, holdingPeriodTackingApplied: false }), mandatoryConversionTriggers: conversionTriggers, specialVotingRights: votingRights, transferRestrictions: restrictions, - splitHistory: splitHistory, - issuerName: "Test CyberCorp", - stateOfIncorporation: "Delaware" + splitHistory: splitHistory }); return shareExtension.encodeExtensionData(shareData); } + function _buildInitialPrinterShareData() internal view returns (bytes memory) { + ShareCertData memory legacy = shareExtension.decodeExtensionData(initialLegacyShareData); + ShareExtensionDataSplitProposal memory proposal = ShareExtensionDataSplitProposal({ + moveToPrinterExtensionData: new string[](0), + keepInCertificateExtensionData: new string[](0), + removeBecauseCoveredByRoundManager: new string[](0) + }); + SharePrinterExtensionData memory printerData = SharePrinterExtensionData({ + terms: legacy.terms, + mandatoryConversionTriggers: legacy.mandatoryConversionTriggers, + specialVotingRights: legacy.specialVotingRights, + transferRestrictions: legacy.transferRestrictions, + splitHistory: legacy.splitHistory, + dataSplitProposal: proposal + }); + + return shareExtension.encodePrinterExtensionData(printerData); + } + + function _buildInitialCertificateShareData() internal view returns (bytes memory) { + ShareCertData memory legacy = shareExtension.decodeExtensionData(initialLegacyShareData); + CertificateData memory legacyCert = legacy.certificateData; + ShareCertificateData memory certificateData = ShareCertificateData({ + certificateNumber: legacyCert.certificateNumber, + issueDate: legacyCert.issueDate, + isPartlyPaid: legacyCert.isPartlyPaid, + amountPaid: legacyCert.amountPaid, + sourceAuthorityURI: legacyCert.sourceAuthorityURI, + representationType: legacyCert.representationType, + holdingPeriodStartDate: legacyCert.holdingPeriodStartDate, + holdingPeriodTackingApplied: legacyCert.holdingPeriodTackingApplied + }); + + return shareExtension.encodeCertificateExtensionData(certificateData); + } + function _buildEoi() internal view returns (EOI memory) { return EOI({ name: "Series A Investor", From 4c206affa77eb66d4271c25144b24bcc7d923acf Mon Sep 17 00:00:00 2001 From: greypixel Date: Fri, 22 May 2026 16:04:04 +0100 Subject: [PATCH 5/7] upgrades --- .../deploy-pump-factory-full-lifecycle.s.sol | 3 +- src/CertificateUriBuilder.sol | 6 +- src/PumpCorpFactory.sol | 4 +- src/storage/IssuanceManagerStorage.sol | 6 +- src/storage/extensions/ACESAFEExtension.sol | 2 +- .../extensions/ICertificateExtension.sol | 6 +- src/storage/extensions/SAFEExtension.sol | 2 +- src/storage/extensions/SAFTEExtension.sol | 2 +- src/storage/extensions/SAFTEExtensionV2.sol | 2 +- src/storage/extensions/SAFTExtension.sol | 2 +- src/storage/extensions/SAFTExtensionV2.sol | 2 +- src/storage/extensions/ShareExtension.sol | 105 +---- .../extensions/ShareExtensionLogic.sol | 433 ------------------ .../extensions/TokenWarrantExtension.sol | 2 +- .../extensions/TokenWarrantExtensionV2.sol | 2 +- test/CyberScripUpgradeExistingV4.t.sol | 3 +- test/LegalOwnerBugFix.t.sol | 3 +- test/PumpCorpFactory.t.sol | 9 +- test/RoundManagerForkTest.t.sol | 3 +- test/RoundManagerTest.v3.next.t.sol | 9 +- test/ShareExtensionTest.t.sol | 192 +------- ...pgradeRoundManagerTokenWhitelistTest.t.sol | 1 + 22 files changed, 65 insertions(+), 734 deletions(-) delete mode 100644 src/storage/extensions/ShareExtensionLogic.sol diff --git a/script/deploy-pump-factory-full-lifecycle.s.sol b/script/deploy-pump-factory-full-lifecycle.s.sol index d80d423f..ffa2ba4e 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/src/CertificateUriBuilder.sol b/src/CertificateUriBuilder.sol index 3bb5237c..91df10ff 100644 --- a/src/CertificateUriBuilder.sol +++ b/src/CertificateUriBuilder.sol @@ -583,7 +583,7 @@ struct CertificateDetails { ) internal view returns (string memory) { (bool success, bytes memory result) = extension.staticcall( abi.encodeWithSelector( - ICertificateExtensionV2.getExtensionURI.selector, + ICertificateExtension.getExtensionURI.selector, printerExtensionData, certificateExtensionData ) @@ -595,8 +595,8 @@ struct CertificateDetails { if (certificateExtensionData.length == 0) return ""; - // Fall back to `getExtensionUri` without certPrinter data - return ICertificateExtension(extension).getExtensionURI(certificateExtensionData); + // Fall back to legacy one-blob extensions without certPrinter data. + return ILegacyCertificateExtension(extension).getExtensionURI(certificateExtensionData); } function _authorizeUpgrade( diff --git a/src/PumpCorpFactory.sol b/src/PumpCorpFactory.sol index 42b49522..f3d232f1 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/storage/IssuanceManagerStorage.sol b/src/storage/IssuanceManagerStorage.sol index 9102109a..fed90e26 100644 --- a/src/storage/IssuanceManagerStorage.sol +++ b/src/storage/IssuanceManagerStorage.sol @@ -531,7 +531,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()); @@ -544,7 +545,8 @@ library IssuanceManagerStorage { address(this), securityType, securitySeries, - extension + extension, + printerExtensionData ); emit CertPrinterCreated( newCert, diff --git a/src/storage/extensions/ACESAFEExtension.sol b/src/storage/extensions/ACESAFEExtension.sol index 161afabc..aac0116a 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/ICertificateExtension.sol b/src/storage/extensions/ICertificateExtension.sol index a4ef7fda..35747cd8 100644 --- a/src/storage/extensions/ICertificateExtension.sol +++ b/src/storage/extensions/ICertificateExtension.sol @@ -41,12 +41,14 @@ 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 ICertificateExtensionV2 { +interface ICertificateExtension { + function supportsExtensionType(bytes32 extensionType) external pure returns (bool); + function getExtensionURI( bytes memory printerExtensionData, bytes memory certificateExtensionData diff --git a/src/storage/extensions/SAFEExtension.sol b/src/storage/extensions/SAFEExtension.sol index 203ded65..f9e43236 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 71659de5..5de4b17b 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 57bb18bb..f0be613d 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 032afb64..12826ecf 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 33ba19b2..20694066 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 5a546bf2..95255842 100644 --- a/src/storage/extensions/ShareExtension.sol +++ b/src/storage/extensions/ShareExtension.sol @@ -181,7 +181,6 @@ struct SeriesTerms { } struct CertificateData { - bytes32 seriesId; uint256 certificateNumber; uint256 numberOfShares; uint256 issueDate; @@ -220,21 +219,7 @@ struct ShareCertificateData { bool holdingPeriodTackingApplied; } -struct ShareCertData { - SeriesTerms terms; - CertificateData certificateData; - MandatoryConversionTrigger[] mandatoryConversionTriggers; - SpecialVotingRight[] specialVotingRights; - TransferRestriction[] transferRestrictions; - SplitRecord[] splitHistory; -} - -contract ShareExtension is - UUPSUpgradeable, - ICertificateExtension, - ICertificateExtensionV2, - BorgAuthACL -{ +contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { bytes32 public constant EXTENSION_TYPE = keccak256("SHARE"); uint256 public constant PERCENTAGE_PRECISION = 10 ** 4; uint256 public constant PRICE_PRECISION = 10 ** 18; @@ -252,18 +237,6 @@ contract ShareExtension is __BorgAuthACL_init(_auth); } - function decodeExtensionData( - bytes memory data - ) external pure returns (ShareCertData memory) { - return abi.decode(data, (ShareCertData)); - } - - function encodeExtensionData( - ShareCertData memory data - ) external pure returns (bytes memory) { - return abi.encode(data); - } - function decodePrinterExtensionData( bytes memory data ) external pure returns (SharePrinterExtensionData memory) { @@ -294,12 +267,6 @@ contract ShareExtension is return extensionType == EXTENSION_TYPE; } - function getExtensionURI( - bytes memory data - ) external pure override returns (string memory) { - return _buildExtensionURI("", data); - } - function getExtensionURI( bytes memory printerExtensionData, bytes memory certificateExtensionData @@ -312,27 +279,7 @@ contract ShareExtension is bytes memory printerExtensionData, bytes memory certificateExtensionData ) internal pure returns (string memory) { - if (printerExtensionData.length == 0) { - if (certificateExtensionData.length == 0) return ""; - - ShareCertData memory legacyShare = abi.decode( - certificateExtensionData, - (ShareCertData) - ); - return - string( - abi.encodePacked( - ', "shareDetails": {', - _buildSeriesJson( - legacyShare.terms, - legacyShare.certificateData - ), - _buildCertificateJson(legacyShare.certificateData), - _buildDerivedJson(legacyShare), - '"}' - ) - ); - } + if (printerExtensionData.length == 0) return ""; SharePrinterExtensionData memory printerData = abi.decode( printerExtensionData, @@ -369,33 +316,18 @@ contract ShareExtension is ); } - function _buildSeriesJson( - SeriesTerms memory terms, - CertificateData memory cert - ) internal pure returns (string memory) { - return - string.concat( - _buildSeriesJsonPartOne( - terms, - Strings.toHexString(uint256(cert.seriesId), 32) - ), - _buildSeriesJsonPartTwo(terms) - ); - } - function _buildSeriesJson( SeriesTerms memory terms ) internal pure returns (string memory) { return string.concat( - _buildSeriesJsonPartOne(terms, ""), + _buildSeriesJsonPartOne(terms), _buildSeriesJsonPartTwo(terms) ); } function _buildSeriesJsonPartOne( - SeriesTerms memory terms, - string memory seriesId + SeriesTerms memory terms ) internal pure returns (string memory) { return string( @@ -404,9 +336,6 @@ contract ShareExtension is _shareClassKeyToString(terms.shareClassKey), '", "seriesName": "', terms.seriesName, - bytes(seriesId).length == 0 - ? "" - : string.concat('", "seriesId": "', seriesId), '", "authorizedShares": "', Strings.toString(terms.authorizedShares), '", "parValue": "', @@ -509,31 +438,6 @@ contract ShareExtension is ); } - function _buildDerivedJson( - ShareCertData memory share - ) internal pure returns (string memory) { - return - string( - abi.encodePacked( - '"mandatoryConversionTriggerCount": "', - Strings.toString(share.mandatoryConversionTriggers.length), - '", "specialVotingRightCount": "', - Strings.toString(share.specialVotingRights.length), - '", "transferRestrictionCount": "', - Strings.toString(share.transferRestrictions.length), - '", "splitHistoryCount": "', - Strings.toString(share.splitHistory.length), - '", "paymentPercentage": "', - Strings.toString( - _getPaymentPercentage(share.certificateData) - ), - '", "conversionRatio": "', - Strings.toString(_getConversionRatio(share.terms)), - '"' - ) - ); - } - function _buildDerivedJson( SharePrinterExtensionData memory printerData, CertificateData memory cert @@ -576,7 +480,6 @@ contract ShareExtension is ShareCertificateData memory data ) internal pure returns (CertificateData memory certificateData) { certificateData = CertificateData({ - seriesId: bytes32(0), certificateNumber: data.certificateNumber, numberOfShares: 0, issueDate: data.issueDate, diff --git a/src/storage/extensions/ShareExtensionLogic.sol b/src/storage/extensions/ShareExtensionLogic.sol deleted file mode 100644 index 3df63f35..00000000 --- a/src/storage/extensions/ShareExtensionLogic.sol +++ /dev/null @@ -1,433 +0,0 @@ -/* .o. - .888. - .8"888. - .8' `888. - .88ooo8888. - .8' `888. -o88o o8888o - - - -ooo ooooo . ooooo ooooooo ooooo -`88. .888' .o8 `888' `8888 d8' - 888b d'888 .ooooo. .o888oo .oooo. 888 .ooooo. Y888..8P - 8 Y88. .P 888 d88' `88b 888 `P )88b 888 d88' `88b `8888' - 8 `888' 888 888ooo888 888 .oP"888 888 888ooo888 .8PY888. - 8 Y 888 888 .o 888 . d8( 888 888 o 888 .o d8' `888b -o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o88888o - - - - .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 -888 `88..8' 888 888 888ooo888 888 888 888 888 888 888 888 -`88b ooo `888' 888 888 888 .o 888 `88b ooo 888 888 888 888 888 .o. - `Y8bood8P' .8' `Y8bod8P' `Y8bod8P' d888b `Y8bood8P' `Y8bod8P' d888b 888bod8P' Y8P - .o..P' 888 - `Y8P' o888o -_______________________________________________________________________________________________________ - -All software, documentation and other files and information in this repository (collectively, the "Software") -are copyright MetaLeX Labs, Inc., a Delaware corporation. - -All rights reserved. - -The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, -distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or -mechanical, including photocopying, recording, or by any information storage and retrieval system, -except with the express prior written permission of the copyright holder.*/ - -pragma solidity 0.8.28; - -import "./ShareExtension.sol"; - -contract ShareExtensionLogic { - uint256 public constant PERCENTAGE_PRECISION = 10 ** 4; - uint256 public constant PRICE_PRECISION = 10 ** 18; - - function decodeExtensionData(bytes memory data) external pure returns (ShareCertData memory) { - return abi.decode(data, (ShareCertData)); - } - - function encodeExtensionData(ShareCertData memory data) external pure returns (bytes memory) { - return abi.encode(data); - } - - function validateShareData(bytes memory data) external pure returns (bool valid, string memory error) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - return _validateShareDataInternal(share); - } - - function validateSeriesTerms(SeriesTerms memory terms) external pure returns (bool valid, string memory error) { - return _validateSeriesTermsInternal(terms); - } - - function validateCertificateData( - CertificateData memory certificateData - ) external pure returns (bool valid, string memory error) { - return _validateCertificateDataInternal(certificateData); - } - - function updateSeriesTerms(bytes memory data, SeriesTerms memory terms) external pure returns (bytes memory) { - (bool valid, string memory error) = _validateSeriesTermsInternal(terms); - require(valid, error); - - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.terms = terms; - return abi.encode(share); - } - - function updateCertificateData( - bytes memory data, - CertificateData memory certificateData - ) external pure returns (bytes memory) { - (bool valid, string memory error) = _validateCertificateDataInternal(certificateData); - require(valid, error); - - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.certificateData = certificateData; - return abi.encode(share); - } - - function updateConversionPrice(bytes memory data, uint256 newPrice) external pure returns (bytes memory) { - require(newPrice > 0, "ShareExtensionLogic: conversionPrice must be > 0"); - - ShareCertData memory share = abi.decode(data, (ShareCertData)); - require(share.terms.isConvertible, "ShareExtensionLogic: series is not convertible"); - share.terms.conversionPrice = newPrice; - return abi.encode(share); - } - - function updateAuthorizedShares(bytes memory data, uint256 newAmount) external pure returns (bytes memory) { - require(newAmount > 0, "ShareExtensionLogic: authorizedShares must be > 0"); - - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.terms.authorizedShares = newAmount; - return abi.encode(share); - } - - function updateSeriesName(bytes memory data, string memory newName) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.terms.seriesName = newName; - return abi.encode(share); - } - - function addConversionTrigger( - bytes memory data, - MandatoryConversionTrigger memory conversionTrigger - ) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.mandatoryConversionTriggers = _appendConversionTrigger( - share.mandatoryConversionTriggers, - conversionTrigger - ); - return abi.encode(share); - } - - function removeConversionTrigger(bytes memory data, uint256 index) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.mandatoryConversionTriggers = _removeConversionTrigger(share.mandatoryConversionTriggers, index); - return abi.encode(share); - } - - function addSpecialVotingRight( - bytes memory data, - SpecialVotingRight memory votingRight - ) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.specialVotingRights = _appendSpecialVotingRight(share.specialVotingRights, votingRight); - return abi.encode(share); - } - - function removeSpecialVotingRight(bytes memory data, uint256 index) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.specialVotingRights = _removeSpecialVotingRight(share.specialVotingRights, index); - return abi.encode(share); - } - - function addTransferRestriction( - bytes memory data, - TransferRestriction memory restriction - ) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.transferRestrictions = _appendTransferRestriction(share.transferRestrictions, restriction); - return abi.encode(share); - } - - function removeTransferRestriction(bytes memory data, uint256 index) external pure returns (bytes memory) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - share.transferRestrictions = _removeTransferRestriction(share.transferRestrictions, index); - return abi.encode(share); - } - - function recordStockSplit( - bytes memory data, - uint256 splitNumerator, - uint256 splitDenominator, - string memory sourceAuthorityURI, - uint256 timestamp - ) external pure returns (bytes memory) { - require(splitNumerator > 0 && splitDenominator > 0, "ShareExtensionLogic: split ratio must be non-zero"); - require(splitNumerator != splitDenominator, "ShareExtensionLogic: split ratio must differ from 1:1"); - - ShareCertData memory share = abi.decode(data, (ShareCertData)); - - share.terms.originalIssuePrice = (share.terms.originalIssuePrice * splitDenominator) / splitNumerator; - share.terms.parValue = (share.terms.parValue * splitDenominator) / splitNumerator; - if (share.terms.conversionPrice > 0) { - share.terms.conversionPrice = (share.terms.conversionPrice * splitDenominator) / splitNumerator; - } - if (share.terms.redemptionPrice > 0) { - share.terms.redemptionPrice = (share.terms.redemptionPrice * splitDenominator) / splitNumerator; - } - - share.terms.authorizedShares = (share.terms.authorizedShares * splitNumerator) / splitDenominator; - share.certificateData.numberOfShares = - (share.certificateData.numberOfShares * splitNumerator) / splitDenominator; - - for (uint256 i = 0; i < share.mandatoryConversionTriggers.length; i++) { - if ( - share.mandatoryConversionTriggers[i].triggerType == MandatoryConversionTriggerType.QualifiedIPO - && share.mandatoryConversionTriggers[i].primaryThreshold > 0 - ) { - share.mandatoryConversionTriggers[i].primaryThreshold = - (share.mandatoryConversionTriggers[i].primaryThreshold * splitDenominator) / splitNumerator; - } - } - - share.splitHistory = _appendSplitRecord( - share.splitHistory, - SplitRecord({ - numerator: splitNumerator, - denominator: splitDenominator, - timestamp: timestamp, - sourceAuthorityURI: sourceAuthorityURI - }) - ); - - return abi.encode(share); - } - - function getConversionRatio(bytes memory data) external pure returns (uint256 ratio) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - if (!share.terms.isConvertible || share.terms.conversionPrice == 0) return 0; - ratio = (share.terms.originalIssuePrice * PRICE_PRECISION) / share.terms.conversionPrice; - } - - function getPaymentPercentage(bytes memory data) external pure returns (uint256 percentage) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - if (!share.certificateData.isPartlyPaid || share.certificateData.totalConsideration == 0) { - return PERCENTAGE_PRECISION; - } - percentage = - (share.certificateData.amountPaid * PERCENTAGE_PRECISION) / share.certificateData.totalConsideration; - } - - function computeAccruedDividends(bytes memory data, uint256 asOfTimestamp) external pure returns (uint256 accrued) { - ShareCertData memory share = abi.decode(data, (ShareCertData)); - SeriesTerms memory terms = share.terms; - CertificateData memory cert = share.certificateData; - - if (terms.dividendType != DividendType.Cumulative) return 0; - if (asOfTimestamp <= terms.dividendAccrualStartDate) return 0; - - uint256 elapsed = asOfTimestamp - terms.dividendAccrualStartDate; - uint256 principal = terms.originalIssuePrice * cert.numberOfShares; - - if (!terms.dividendCompounding) { - accrued = (terms.dividendRate * principal * elapsed) / (365 days * PRICE_PRECISION); - return accrued; - } - - uint256 fullYears = elapsed / 365 days; - uint256 remainder = elapsed % 365 days; - uint256 compounded = principal; - - for (uint256 i = 0; i < fullYears; i++) { - compounded = (compounded * (PRICE_PRECISION + terms.dividendRate)) / PRICE_PRECISION; - } - - if (remainder > 0) { - compounded += (compounded * terms.dividendRate * remainder) / (365 days * PRICE_PRECISION); - } - - accrued = compounded - principal; - } - - function _validateShareDataInternal(ShareCertData memory share) internal pure returns (bool, string memory) { - (bool valid, string memory error) = _validateSeriesTermsInternal(share.terms); - if (!valid) return (false, error); - - return _validateCertificateDataInternal(share.certificateData); - } - - function _validateSeriesTermsInternal(SeriesTerms memory terms) internal pure returns (bool, string memory) { - if (terms.authorizedShares == 0) return (false, "ShareExtensionLogic: authorizedShares must be > 0"); - if (terms.parValue == 0) return (false, "ShareExtensionLogic: parValue must be > 0"); - - if (!terms.isConvertible) { - if (terms.conversionPrice != 0) { - return (false, "ShareExtensionLogic: conversionPrice must be 0 when not convertible"); - } - if (terms.targetConversionSeriesId != bytes32(0)) { - return (false, "ShareExtensionLogic: targetConversionSeriesId must be zero when not convertible"); - } - if (terms.hasMandatoryConversion) { - return (false, "ShareExtensionLogic: hasMandatoryConversion must be false when not convertible"); - } - } - - if (terms.isConvertible) { - if (terms.conversionPrice == 0) { - return (false, "ShareExtensionLogic: conversionPrice must be > 0 when convertible"); - } - if (terms.targetConversionSeriesId == bytes32(0)) { - return (false, "ShareExtensionLogic: targetConversionSeriesId must be non-zero when convertible"); - } - } - - if (terms.dividendType == DividendType.None && terms.dividendRate != 0) { - return (false, "ShareExtensionLogic: dividendRate must be 0 when dividendType is None"); - } - - if (terms.dividendType == DividendType.Cumulative && terms.dividendAccrualStartDate == 0) { - return (false, "ShareExtensionLogic: dividendAccrualStartDate should be non-zero for Cumulative"); - } - - if (terms.liquidationPreferenceType != LiquidationPreferenceType.CappedParticipating) { - if (terms.participationCap != 0) { - return (false, "ShareExtensionLogic: participationCap must be 0 when not capped"); - } - } else if (terms.participationCap == 0) { - return (false, "ShareExtensionLogic: participationCap must be > 0 when capped"); - } - - if (!terms.isRedeemable) { - if (terms.redemptionPrice != 0) { - return (false, "ShareExtensionLogic: redemptionPrice must be 0 when not redeemable"); - } - if (terms.redemptionType != RedemptionType.None) { - return (false, "ShareExtensionLogic: redemptionType must be None when not redeemable"); - } - } - - return (true, ""); - } - - function _validateCertificateDataInternal( - CertificateData memory certificateData - ) internal pure returns (bool, string memory) { - if (certificateData.seriesId == bytes32(0)) { - return (false, "ShareExtensionLogic: seriesId must be non-zero"); - } - if (certificateData.numberOfShares == 0) { - return (false, "ShareExtensionLogic: numberOfShares must be > 0"); - } - - if (certificateData.isPartlyPaid) { - if (certificateData.totalConsideration == 0) { - return (false, "ShareExtensionLogic: totalConsideration must be > 0 when partly paid"); - } - if (certificateData.amountPaid >= certificateData.totalConsideration) { - return (false, "ShareExtensionLogic: amountPaid must be < totalConsideration when partly paid"); - } - } else { - if (certificateData.amountPaid != 0) { - return (false, "ShareExtensionLogic: amountPaid must be 0 when not partly paid"); - } - if (certificateData.totalConsideration != 0) { - return (false, "ShareExtensionLogic: totalConsideration must be 0 when not partly paid"); - } - } - - return (true, ""); - } - - function _appendConversionTrigger( - MandatoryConversionTrigger[] memory items, - MandatoryConversionTrigger memory item - ) internal pure returns (MandatoryConversionTrigger[] memory updated) { - updated = new MandatoryConversionTrigger[](items.length + 1); - for (uint256 i = 0; i < items.length; i++) { - updated[i] = items[i]; - } - updated[items.length] = item; - } - - function _removeConversionTrigger( - MandatoryConversionTrigger[] memory items, - uint256 index - ) internal pure returns (MandatoryConversionTrigger[] memory updated) { - require(index < items.length, "ShareExtensionLogic: trigger index out of bounds"); - updated = new MandatoryConversionTrigger[](items.length - 1); - for (uint256 i = 0; i < index; i++) { - updated[i] = items[i]; - } - for (uint256 i = index + 1; i < items.length; i++) { - updated[i - 1] = items[i]; - } - } - - function _appendSpecialVotingRight( - SpecialVotingRight[] memory items, - SpecialVotingRight memory item - ) internal pure returns (SpecialVotingRight[] memory updated) { - updated = new SpecialVotingRight[](items.length + 1); - for (uint256 i = 0; i < items.length; i++) { - updated[i] = items[i]; - } - updated[items.length] = item; - } - - function _removeSpecialVotingRight( - SpecialVotingRight[] memory items, - uint256 index - ) internal pure returns (SpecialVotingRight[] memory updated) { - require(index < items.length, "ShareExtensionLogic: voting right index out of bounds"); - updated = new SpecialVotingRight[](items.length - 1); - for (uint256 i = 0; i < index; i++) { - updated[i] = items[i]; - } - for (uint256 i = index + 1; i < items.length; i++) { - updated[i - 1] = items[i]; - } - } - - function _appendTransferRestriction( - TransferRestriction[] memory items, - TransferRestriction memory item - ) internal pure returns (TransferRestriction[] memory updated) { - updated = new TransferRestriction[](items.length + 1); - for (uint256 i = 0; i < items.length; i++) { - updated[i] = items[i]; - } - updated[items.length] = item; - } - - function _removeTransferRestriction( - TransferRestriction[] memory items, - uint256 index - ) internal pure returns (TransferRestriction[] memory updated) { - require(index < items.length, "ShareExtensionLogic: restriction index out of bounds"); - updated = new TransferRestriction[](items.length - 1); - for (uint256 i = 0; i < index; i++) { - updated[i] = items[i]; - } - for (uint256 i = index + 1; i < items.length; i++) { - updated[i - 1] = items[i]; - } - } - - function _appendSplitRecord( - SplitRecord[] memory items, - SplitRecord memory item - ) internal pure returns (SplitRecord[] memory updated) { - updated = new SplitRecord[](items.length + 1); - for (uint256 i = 0; i < items.length; i++) { - updated[i] = items[i]; - } - updated[items.length] = item; - } -} diff --git a/src/storage/extensions/TokenWarrantExtension.sol b/src/storage/extensions/TokenWarrantExtension.sol index e94aadd6..9ba12b38 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 0c2ef2e6..042ff2c1 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/CyberScripUpgradeExistingV4.t.sol b/test/CyberScripUpgradeExistingV4.t.sol index 161ebee6..6102863d 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/LegalOwnerBugFix.t.sol b/test/LegalOwnerBugFix.t.sol index d9fb0058..c602443f 100644 --- a/test/LegalOwnerBugFix.t.sol +++ b/test/LegalOwnerBugFix.t.sol @@ -132,7 +132,8 @@ contract LegalOwnerBugPOCTest is Test { "uri://cert", SecurityClass.CommonStock, SecuritySeries.SeriesA, - address(0) + address(0), + "" ) ); } diff --git a/test/PumpCorpFactory.t.sol b/test/PumpCorpFactory.t.sol index f8e38a54..c5b14a5c 100644 --- a/test/PumpCorpFactory.t.sol +++ b/test/PumpCorpFactory.t.sol @@ -153,7 +153,8 @@ contract PumpCorpFactoryForkTest is Test { securityClass: SecurityClass.SAFE, securitySeries: SecuritySeries.SeriesSeed, extension: address(0), - defaultLegend: legend + defaultLegend: legend, + printerExtensionData: bytes("") })); legalDetails = new string[](1); @@ -1351,7 +1352,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. @@ -1415,7 +1417,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 abc67107..3ed96fbb 100644 --- a/test/RoundManagerForkTest.t.sol +++ b/test/RoundManagerForkTest.t.sol @@ -847,7 +847,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.v3.next.t.sol b/test/RoundManagerTest.v3.next.t.sol index 6fa2f8f7..9f3e4c47 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/ShareExtensionTest.t.sol b/test/ShareExtensionTest.t.sol index ee585021..c9e0b857 100644 --- a/test/ShareExtensionTest.t.sol +++ b/test/ShareExtensionTest.t.sol @@ -27,12 +27,10 @@ import {CertificateDetails} from "../src/storage/CyberCertPrinterStorage.sol"; import {CyberCertData, EOI, LexChexDetails, MintRequest} from "../src/storage/RoundManagerStorage.sol"; import { ShareExtension, - ShareCertData, SharePrinterExtensionData, ShareCertificateData, ShareExtensionDataSplitProposal, SeriesTerms, - CertificateData, MandatoryConversionTrigger, MandatoryConversionTriggerType, SpecialVotingRight, @@ -47,7 +45,6 @@ import { VotingScope, ShareRepresentationType } from "../src/storage/extensions/ShareExtension.sol"; -import {ShareExtensionLogic} from "../src/storage/extensions/ShareExtensionLogic.sol"; import {CyberAgreementUtils} from "./libs/CyberAgreementUtils.sol"; contract MockPaymentToken is ERC20 { @@ -90,14 +87,12 @@ contract ShareExtensionTest is Test { CyberCertPrinter internal certPrinter; CertificateUriBuilder internal uriBuilder; ShareExtension internal shareExtension; - ShareExtensionLogic internal shareLogic; MockPaymentToken internal paymentToken; address internal corpAddress; bytes32 internal roundId; bytes32 internal agreementId; uint256 internal tokenId; - bytes internal initialLegacyShareData; bytes internal initialCertificateShareData; bytes internal initialPrinterShareData; @@ -137,127 +132,6 @@ contract ShareExtensionTest is Test { assertEq(keccak256(certPrinter.getPrinterExtensionData()), keccak256(initialPrinterShareData)); } - function testLogic_DecodeAndValidateLiveSharePayload() public { - bytes memory storedData = initialLegacyShareData; - - ShareCertData memory formatterDecoded = shareExtension.decodeExtensionData(storedData); - ShareCertData memory logicDecoded = shareLogic.decodeExtensionData(storedData); - (bool valid, string memory error) = shareLogic.validateShareData(storedData); - - assertTrue(valid); - assertEq(error, ""); - assertEq(formatterDecoded.terms.seriesName, "Series A Preferred"); - assertEq(logicDecoded.certificateData.certificateNumber, 1001); - assertEq(logicDecoded.transferRestrictions.length, 1); - assertEq(logicDecoded.specialVotingRights.length, 1); - assertEq(logicDecoded.mandatoryConversionTriggers.length, 1); - } - - function testLogic_UpdateConversionPriceAndRatio() public { - bytes memory updatedData = shareLogic.updateConversionPrice(initialLegacyShareData, 8e18); - ShareCertData memory decoded = shareLogic.decodeExtensionData(updatedData); - uint256 ratio = shareLogic.getConversionRatio(updatedData); - - assertEq(decoded.terms.conversionPrice, 8e18); - assertEq(ratio, (decoded.terms.originalIssuePrice * 1e18) / 8e18); - - string memory json = shareExtension.getExtensionURI(updatedData); - assertTrue(_contains(json, '"conversionPrice": "8000000000000000000"')); - } - - function testLogic_RecordStockSplitAdjustsSharePayload() public { - bytes memory updatedData = - shareLogic.recordStockSplit(initialLegacyShareData, 2, 1, "ipfs://split-board-consent", block.timestamp); - - ShareCertData memory decoded = shareLogic.decodeExtensionData(updatedData); - - assertEq(decoded.terms.authorizedShares, 20_000_000); - assertEq(decoded.certificateData.numberOfShares, 20_000); - assertEq(decoded.terms.originalIssuePrice, 5e18); - assertEq(decoded.terms.parValue, 5e15); - assertEq(decoded.terms.conversionPrice, 5e18); - assertEq(decoded.terms.redemptionPrice, 6e18); - assertEq(decoded.mandatoryConversionTriggers[0].primaryThreshold, 6e18); - assertEq(decoded.splitHistory.length, 1); - assertEq(decoded.splitHistory[0].numerator, 2); - assertEq(decoded.splitHistory[0].denominator, 1); - } - - function testLogic_ComputesAccruedDividendsForCumulativeSeries() public { - uint256 accrued = shareLogic.computeAccruedDividends(initialLegacyShareData, block.timestamp + 365 days); - - assertEq(accrued, 10_000e18); - } - - function testLogic_ManagesDynamicArrayPayloadSections() public { - bytes memory workingData = initialLegacyShareData; - - MandatoryConversionTrigger memory trigger = MandatoryConversionTrigger({ - triggerType: MandatoryConversionTriggerType.Custom, - primaryThreshold: 0, - secondaryThreshold: 0, - additionalConditions: "Board approved recapitalization", - description: "Automatic conversion on recapitalization" - }); - workingData = shareLogic.addConversionTrigger(workingData, trigger); - - SpecialVotingRight memory votingRight = SpecialVotingRight({ - matterType: keccak256("NEW_BOARD_SEAT"), - votesPerShare: 2e18, - threshold: 6000, - isVetoRight: false, - scope: VotingScope.SeriesSpecific, - description: "Series vote to add a board seat" - }); - workingData = shareLogic.addSpecialVotingRight(workingData, votingRight); - - TransferRestrictionException[] memory exceptions = new TransferRestrictionException[](1); - exceptions[0] = TransferRestrictionException({ - exceptionType: keccak256("AFFILIATE_TRANSFER"), - exceptionText: "Transfers to affiliates are permitted", - requiresEvidence: true - }); - - TransferRestriction memory restriction = TransferRestriction({ - restrictionType: TransferRestrictionType.CustomRestriction, - restrictionText: "Subject to side letter transfer limits", - sourceAgreement: "Side Letter", - isRemovable: true, - exceptions: exceptions - }); - workingData = shareLogic.addTransferRestriction(workingData, restriction); - - ShareCertData memory expanded = shareLogic.decodeExtensionData(workingData); - assertEq(expanded.mandatoryConversionTriggers.length, 2); - assertEq(expanded.specialVotingRights.length, 2); - assertEq(expanded.transferRestrictions.length, 2); - - workingData = shareLogic.removeConversionTrigger(workingData, 1); - workingData = shareLogic.removeSpecialVotingRight(workingData, 1); - workingData = shareLogic.removeTransferRestriction(workingData, 1); - - ShareCertData memory collapsed = shareLogic.decodeExtensionData(workingData); - assertEq(collapsed.mandatoryConversionTriggers.length, 1); - assertEq(collapsed.specialVotingRights.length, 1); - assertEq(collapsed.transferRestrictions.length, 1); - } - - function testLogic_RejectsInvalidSeriesTerms() public { - ShareCertData memory shareData = shareLogic.decodeExtensionData(initialLegacyShareData); - shareData.terms.isConvertible = false; - shareData.terms.conversionPrice = 1e18; - shareData.terms.targetConversionSeriesId = bytes32(0); - shareData.terms.hasMandatoryConversion = false; - - (bool valid, string memory error) = shareLogic.validateSeriesTerms(shareData.terms); - assertFalse(valid); - assertEq(error, "ShareExtensionLogic: conversionPrice must be 0 when not convertible"); - - bytes memory storedData = initialLegacyShareData; - vm.expectRevert(bytes("ShareExtensionLogic: conversionPrice must be 0 when not convertible")); - shareLogic.updateSeriesTerms(storedData, shareData.terms); - } - function _deployFactories() internal { bootstrapAuth = new BorgAuth(address(this)); @@ -349,7 +223,6 @@ contract ShareExtensionTest is Test { extensionAuth = new BorgAuth(address(this)); shareExtension = new ShareExtension(); shareExtension.initialize(address(extensionAuth)); - shareLogic = new ShareExtensionLogic(); } function _createTemplate() internal { @@ -405,9 +278,8 @@ contract ShareExtensionTest is Test { paymentToken = new MockPaymentToken(); paymentToken.mint(investor, OFFER_AMOUNT * 2); - initialLegacyShareData = _buildInitialShareData(); - initialCertificateShareData = _buildInitialCertificateShareData(); initialPrinterShareData = _buildInitialPrinterShareData(); + initialCertificateShareData = _buildInitialCertificateShareData(); roundId = _createSeriesARound(initialCertificateShareData); vm.startPrank(investor); @@ -465,6 +337,7 @@ contract ShareExtensionTest is Test { RoundType.FCFS, false, true, + false, RAISE_CAP, OFFER_AMOUNT, OFFER_AMOUNT, @@ -488,7 +361,7 @@ contract ShareExtensionTest is Test { ); } - function _buildInitialShareData() internal view returns (bytes memory) { + function _buildInitialPrinterShareData() internal view returns (bytes memory) { TransferRestrictionException[] memory exceptions = new TransferRestrictionException[](1); exceptions[0] = TransferRestrictionException({ exceptionType: keccak256("ESTATE_PLANNING_TRANSFER"), @@ -524,9 +397,13 @@ contract ShareExtensionTest is Test { description: "Series A veto on merger approval" }); - SplitRecord[] memory splitHistory = new SplitRecord[](0); + ShareExtensionDataSplitProposal memory proposal = ShareExtensionDataSplitProposal({ + moveToPrinterExtensionData: new string[](0), + keepInCertificateExtensionData: new string[](0), + removeBecauseCoveredByRoundManager: new string[](0) + }); - ShareCertData memory shareData = ShareCertData({ + SharePrinterExtensionData memory printerData = SharePrinterExtensionData({ terms: SeriesTerms({ shareClassKey: keccak256("PREFERRED"), seriesName: "Series A Preferred", @@ -568,41 +445,10 @@ contract ShareExtensionTest is Test { hasDragAlongRights: true, dragAlongTermsURI: "ipfs://drag-along" }), - certificateData: CertificateData({ - seriesId: bytes32("SERIES_A_2026"), - certificateNumber: 1001, - numberOfShares: 10_000, - issueDate: block.timestamp, - isPartlyPaid: true, - amountPaid: 50_000e18, - totalConsideration: 100_000e18, - sourceAuthorityURI: "ipfs://board-approval", - representationType: ShareRepresentationType.Certificated, - holdingPeriodStartDate: block.timestamp > 30 days ? block.timestamp - 30 days : 0, - holdingPeriodTackingApplied: false - }), mandatoryConversionTriggers: conversionTriggers, specialVotingRights: votingRights, transferRestrictions: restrictions, - splitHistory: splitHistory - }); - - return shareExtension.encodeExtensionData(shareData); - } - - function _buildInitialPrinterShareData() internal view returns (bytes memory) { - ShareCertData memory legacy = shareExtension.decodeExtensionData(initialLegacyShareData); - ShareExtensionDataSplitProposal memory proposal = ShareExtensionDataSplitProposal({ - moveToPrinterExtensionData: new string[](0), - keepInCertificateExtensionData: new string[](0), - removeBecauseCoveredByRoundManager: new string[](0) - }); - SharePrinterExtensionData memory printerData = SharePrinterExtensionData({ - terms: legacy.terms, - mandatoryConversionTriggers: legacy.mandatoryConversionTriggers, - specialVotingRights: legacy.specialVotingRights, - transferRestrictions: legacy.transferRestrictions, - splitHistory: legacy.splitHistory, + splitHistory: new SplitRecord[](0), dataSplitProposal: proposal }); @@ -610,17 +456,15 @@ contract ShareExtensionTest is Test { } function _buildInitialCertificateShareData() internal view returns (bytes memory) { - ShareCertData memory legacy = shareExtension.decodeExtensionData(initialLegacyShareData); - CertificateData memory legacyCert = legacy.certificateData; ShareCertificateData memory certificateData = ShareCertificateData({ - certificateNumber: legacyCert.certificateNumber, - issueDate: legacyCert.issueDate, - isPartlyPaid: legacyCert.isPartlyPaid, - amountPaid: legacyCert.amountPaid, - sourceAuthorityURI: legacyCert.sourceAuthorityURI, - representationType: legacyCert.representationType, - holdingPeriodStartDate: legacyCert.holdingPeriodStartDate, - holdingPeriodTackingApplied: legacyCert.holdingPeriodTackingApplied + certificateNumber: 1001, + issueDate: block.timestamp, + isPartlyPaid: true, + amountPaid: 50_000e18, + sourceAuthorityURI: "ipfs://board-approval", + representationType: ShareRepresentationType.Certificated, + holdingPeriodStartDate: block.timestamp > 30 days ? block.timestamp - 30 days : 0, + holdingPeriodTackingApplied: false }); return shareExtension.encodeCertificateExtensionData(certificateData); diff --git a/test/UpgradeRoundManagerTokenWhitelistTest.t.sol b/test/UpgradeRoundManagerTokenWhitelistTest.t.sol index c851e173..f3bcc233 100644 --- a/test/UpgradeRoundManagerTokenWhitelistTest.t.sol +++ b/test/UpgradeRoundManagerTokenWhitelistTest.t.sol @@ -198,6 +198,7 @@ contract UpgradeRoundManagerTokenWhitelistTest is Test { roundType, true, // publicRound true, // allowTimedOffers + false, // restrictEndTimeReduction raiseCap, minTicket, maxTicket, From 732cf755766f7493ccfd844de231da193b9a5140 Mon Sep 17 00:00:00 2001 From: 0xPrepop <6125373+merisman@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:34:05 -0400 Subject: [PATCH 6/7] New SVG Image --- src/CertificateImageBuilder.sol | 186 +++++----- src/CertificateImageBuilderContract.sol | 457 +----------------------- src/CertificateImageContentBuilder.sol | 434 ++++++++++++---------- src/CertificateUriBuilder.sol | 18 +- src/CyberCorpConstants.sol | 6 + test/CertificateImageForkTest.t.sol | 442 +++++++++++++++++++++++ 6 files changed, 814 insertions(+), 729 deletions(-) create mode 100644 test/CertificateImageForkTest.t.sol diff --git a/src/CertificateImageBuilder.sol b/src/CertificateImageBuilder.sol index d582a4ef..4789f1e5 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 ba8ed0a8..f707ec97 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 81665cec..c0f5243e 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 91df10ff..ba003646 100644 --- a/src/CertificateUriBuilder.sol +++ b/src/CertificateUriBuilder.sol @@ -501,7 +501,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: certLegend, + isVoided: _isCertVoided(contractAddress, tokenId) }); string memory svg = ICertificateImageBuilder(imageBuilder).buildCertificateSVG(svgParams, certTimestamp); @@ -567,6 +573,16 @@ struct CertificateDetails { 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) { diff --git a/src/CyberCorpConstants.sol b/src/CyberCorpConstants.sol index 534ce81e..5c7ac6d0 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/test/CertificateImageForkTest.t.sol b/test/CertificateImageForkTest.t.sol new file mode 100644 index 00000000..3b0ff02c --- /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; + } +} From 9b0bf74f797ea32597e26c7b3f4f2548c9b9e6ee Mon Sep 17 00:00:00 2001 From: 0xPrepop <6125373+merisman@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:46:34 -0400 Subject: [PATCH 7/7] Compiles but still reverts --- src/CertificateUriBuilder.sol | 14 ++++- src/DealManager.sol | 3 +- src/interfaces/ICyberCertPrinter.sol | 1 + src/storage/CyberCertPrinterStorage.sol | 15 +++--- src/storage/DealManagerStorage.sol | 1 + .../extensions/FundInterestExtension.sol | 7 ++- src/storage/extensions/ShareExtension.sol | 15 +++--- test/CyberCertPrinterTest.t.sol | 3 +- ...erSecondaryTradeExemptionPathwayTest.t.sol | 3 +- ...DealManagerSecondaryTradeIndexerTest.t.sol | 3 +- test/DealManagerSecondaryTradeTest.t.sol | 3 +- ...IssuanceManagerSecondaryTransferTest.t.sol | 5 +- test/IssuanceManagerTest.t.sol | 3 +- test/LifeCycleGasLimitTest.t.sol | 3 +- test/ShareExtensionForkTest.t.sol | 9 ++-- test/UpgradeLegacyCyberCorpsTest.t.sol | 53 +++++++++++++++++-- test/helpers/IssuanceManagerWithMigration.sol | 33 ++++++++++++ 17 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 test/helpers/IssuanceManagerWithMigration.sol diff --git a/src/CertificateUriBuilder.sol b/src/CertificateUriBuilder.sol index a5e835a0..7889c5c9 100644 --- a/src/CertificateUriBuilder.sol +++ b/src/CertificateUriBuilder.sol @@ -158,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++) { @@ -619,7 +631,7 @@ struct CertificateDetails { ownerAddress: owner.ownerAddress, consideration: details.investmentAmountUSD, blockNumber: block.number, - transferRestrictions: certLegend, + transferRestrictions: restrictiveLegendsToStringArray(certLegend), isVoided: _isCertVoided(contractAddress, tokenId) }); diff --git a/src/DealManager.sol b/src/DealManager.sol index f7b8f196..661d968c 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/interfaces/ICyberCertPrinter.sol b/src/interfaces/ICyberCertPrinter.sol index 48b9142f..4356d7f1 100644 --- a/src/interfaces/ICyberCertPrinter.sol +++ b/src/interfaces/ICyberCertPrinter.sol @@ -194,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/storage/CyberCertPrinterStorage.sol b/src/storage/CyberCertPrinterStorage.sol index 7d7f828b..b37dca46 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,6 +135,8 @@ library CyberCertPrinterStorage { CyberCertPrinterStorage.CyberCertStorage storage s = cyberCertStorage(); RestrictiveLegend[] memory certLegend = getEffectiveRestrictiveLegends(tokenId); ICyberCorp corp = ICyberCorp(IIssuanceManager(s.issuanceManager).CORP()); + CertificateDetails memory effectiveDetails = getCertificateDetails(tokenId); + // Get registry and agreementId from first endorsement if it exists address registry = address(0); bytes32 agreementId = bytes32(0); @@ -143,11 +146,7 @@ library CyberCertPrinterStorage { agreementId = firstEndorsement.agreementId; } - IUriBuilder builder = IUriBuilder(IIssuanceManager(s.issuanceManager).uriBuilder()); - string[] memory certLegend = s.certLegend[tokenId]; - Endorsement[] memory endorsements = s.endorsements[tokenId]; - OwnerDetails memory owner = s.owners[tokenId]; - return builder.buildCertificateUri( + return IUriBuilder(IIssuanceManager(s.issuanceManager).uriBuilder()).buildCertificateUri( corp.cyberCORPName(), corp.cyberCORPType(), corp.cyberCORPJurisdiction(), @@ -156,9 +155,9 @@ library CyberCertPrinterStorage { s.securitySeries, s.certificateUri, certLegend, - getCertificateDetails(tokenId), - endorsements, - owner, + effectiveDetails, + s.endorsements[tokenId], + s.owners[tokenId], registry, agreementId, tokenId, diff --git a/src/storage/DealManagerStorage.sol b/src/storage/DealManagerStorage.sol index 9b81898f..24a2c3b6 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/extensions/FundInterestExtension.sol b/src/storage/extensions/FundInterestExtension.sol index e4743a99..dc5ff623 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/ShareExtension.sol b/src/storage/extensions/ShareExtension.sol index 21429f96..9b714757 100644 --- a/src/storage/extensions/ShareExtension.sol +++ b/src/storage/extensions/ShareExtension.sol @@ -249,12 +249,12 @@ contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { function decodeCertificateExtensionData( bytes memory data - ) external pure returns (ShareCertificateData memory) { - return abi.decode(data, (ShareCertificateData)); + ) external pure returns (CertificateData memory) { + return abi.decode(data, (CertificateData)); } function encodeCertificateExtensionData( - ShareCertificateData memory data + CertificateData memory data ) external pure returns (bytes memory) { return abi.encode(data); } @@ -265,10 +265,13 @@ contract ShareExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { 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/test/CyberCertPrinterTest.t.sol b/test/CyberCertPrinterTest.t.sol index 8d4bd60b..7f76f563 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/DealManagerSecondaryTradeExemptionPathwayTest.t.sol b/test/DealManagerSecondaryTradeExemptionPathwayTest.t.sol index f97853de..163bb223 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 f938e62e..2fe7b214 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 3a8dc6fd..03d384c1 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/IssuanceManagerSecondaryTransferTest.t.sol b/test/IssuanceManagerSecondaryTransferTest.t.sol index 392fd483..f4994837 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 c5520ac7..9db7316e 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/LifeCycleGasLimitTest.t.sol b/test/LifeCycleGasLimitTest.t.sol index af2e27f0..1837ae4a 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/ShareExtensionForkTest.t.sol b/test/ShareExtensionForkTest.t.sol index 8835c9c0..ff61eda8 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 index 3b465e58..130b0b6e 100644 --- a/test/UpgradeLegacyCyberCorpsTest.t.sol +++ b/test/UpgradeLegacyCyberCorpsTest.t.sol @@ -9,7 +9,6 @@ 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 {UpgradeLegacyIssuanceManagersScript} from "../script/upgrade-legacy-issuance-managers.s.sol"; import {ILegacyFactory} from "../script/interfaces/ILegacyFactory.sol"; import {ILegacyIssuanceManagerFactory} from "../script/interfaces/ILegacyIssuanceManagerFactory.sol"; import {GnosisTransaction} from "../script/libs/safe.sol"; @@ -27,7 +26,8 @@ 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 {IssuanceManagerWithMigration} from "../src/IssuanceManagerWithMigration.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"; @@ -718,8 +718,8 @@ contract UpgradeLegacyCyberCorpsTest is Test { // Run scripts to upgrade all legacy DealManagers (new UpgradeLegacyDealManagersScript()).runWithArgs(legacyAddressesCount); - // Run scripts to upgrade all legacy IssuanceManagers - (new UpgradeLegacyIssuanceManagersScript()).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++) { @@ -734,6 +734,51 @@ contract UpgradeLegacyCyberCorpsTest is Test { } } + 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(), diff --git a/test/helpers/IssuanceManagerWithMigration.sol b/test/helpers/IssuanceManagerWithMigration.sol new file mode 100644 index 00000000..6700cba1 --- /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); + } + } +}