Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
12f4454
Redesign ShareExtension.sol: split series terms from certificate data
claude Mar 9, 2026
b35b5fa
Add foundry.lock from dependency installation
claude Mar 9, 2026
803f8a9
Distinguish class-wide vs series-specific voting rights
claude Mar 9, 2026
04e6459
Fix V1 backward compat: preserve TransferRestrictionType enum ordinals
claude Mar 9, 2026
cdf8145
Revert V1 enum ordinal preservation — clean up TransferRestrictionType
claude Mar 9, 2026
31b6cea
Remove V1 backward compatibility layer — no prior deployment exists
claude Mar 9, 2026
9dcc268
Implement full ShareExtension redesign per revised evaluation plan
claude Mar 9, 2026
de048da
Reject zero conversionPrice for convertible series
claude Mar 9, 2026
2763da1
Only scale price-denominated triggers during stock splits
claude Mar 9, 2026
8bef176
Extract _recordStockSplit internal to fix batch self-call revert
claude Mar 9, 2026
6235595
Backport legend hash tracking, events, and audit trail to CyberCertPr…
claude Mar 9, 2026
0dcd787
Delegate legend management from ShareExtension to CyberCertPrinter
claude Mar 9, 2026
7c871a1
Extract shared StringUtils library for uint256ToString
claude Mar 9, 2026
8bbd1da
Honor dividendCompounding flag in computeAccruedDividends
claude Mar 9, 2026
d974920
Preserve custom shareClassKey identity in URI rendering
claude Mar 9, 2026
fccc4a2
Condense URI output by omitting inactive sections for simple share cl…
claude Mar 9, 2026
c5f8c7e
Add lazy legend-hash backfill for pre-upgrade proxies
claude Mar 9, 2026
205cef4
Backfill legend hashes before appending in add functions
claude Mar 9, 2026
ce5b61b
Seed series transfer-restriction legends automatically at mint time
claude Mar 9, 2026
dbbf422
Wire setCertPrinter on extension during cert printer creation
claude Mar 9, 2026
f37ce4a
Remove deprecated legacy storage slots and annotate no-prod-history
claude Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"dependencies/forge-std": {
"rev": "8ba9031ffcbe25aa0d1224d3ca263a995026e477"
},
"dependencies/openzeppelin-contracts": {
"rev": "fda6b85f2c65d146b86d513a604554d15abd6679"
},
"dependencies/openzeppelin-contracts-upgradeable": {
"rev": "36ec7079af1a68bd866f6b9f4cf2f4dddee1e7bc"
}
}
97 changes: 84 additions & 13 deletions src/CyberCertPrinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,24 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
event RestrictionHookSet(uint256 indexed id, address indexed hookAddress);
event GlobalRestrictionHookSet(address indexed hookAddress);
event GlobalTransferableSet(bool indexed transferable);
event LegendAdded(uint256 indexed tokenId, uint256 legendIndex, bytes32 legendHash);
event LegendRemoved(uint256 indexed tokenId, uint256 legendIndex, bytes32 legendHash);
event LegendRemovalRequested(uint256 indexed tokenId, uint256 legendIndex, string justification);
event DefaultLegendAdded(uint256 legendIndex, bytes32 legendHash);
event DefaultLegendRemoved(uint256 legendIndex, bytes32 legendHash);


modifier onlyIssuanceManager() {
if (msg.sender != CyberCertPrinterStorage.cyberCertStorage().issuanceManager) revert NotIssuanceManager();
_;
}

modifier onlyIssuanceManagerOrExtension() {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
if (msg.sender != s.issuanceManager && msg.sender != s.extension) revert NotIssuanceManager();
_;
}

constructor() {
_disableInitializers();
}
Expand All @@ -109,6 +120,9 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
s.issuanceManager = _issuanceManager;
s.defaultLegend = _defaultLegend;
for (uint256 i = 0; i < _defaultLegend.length; i++) {
s.defaultLegendHashes.push(keccak256(bytes(_defaultLegend[i])));
}
s.securityType = _securityType;
s.securitySeries = _securitySeries;
s.certificateUri = _certificateUri;
Expand Down Expand Up @@ -144,22 +158,28 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
) external onlyIssuanceManager returns (uint256) {

_safeMint(to, tokenId);
CyberCertPrinterStorage.cyberCertStorage().certLegend[tokenId] = CyberCertPrinterStorage.cyberCertStorage().defaultLegend;
CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId] = details;
_ensureDefaultLegendHashes();
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
s.certLegend[tokenId] = s.defaultLegend;
s.certLegendHashes[tokenId] = s.defaultLegendHashes;
s.certificateDetails[tokenId] = details;
emit CyberCertPrinter_CertificateCreated(tokenId);
return tokenId;
}

// Restricted minting with full agreement details
function safeMintAndAssign(
address to,
address to,
uint256 tokenId,
CertificateDetails memory details
) external onlyIssuanceManager returns (uint256) {
_safeMint(to, tokenId);
CyberCertPrinterStorage.cyberCertStorage().certLegend[tokenId] = CyberCertPrinterStorage.cyberCertStorage().defaultLegend;
_ensureDefaultLegendHashes();
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
s.certLegend[tokenId] = s.defaultLegend;
s.certLegendHashes[tokenId] = s.defaultLegendHashes;
// Store agreement details
CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId] = details;
s.certificateDetails[tokenId] = details;
string memory issuerName = IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).companyName();
emit CyberCertPrinter_CertificateCreated(tokenId);
return tokenId;
Expand Down Expand Up @@ -383,22 +403,53 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
return CyberCertPrinterStorage.cyberCertStorage().endorsementRequired;
}

/// @dev Backfill defaultLegendHashes for proxies deployed before hash tracking was added.
function _ensureDefaultLegendHashes() internal {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
uint256 textLen = s.defaultLegend.length;
uint256 hashLen = s.defaultLegendHashes.length;
if (hashLen < textLen) {
for (uint256 i = hashLen; i < textLen; i++) {
s.defaultLegendHashes.push(keccak256(bytes(s.defaultLegend[i])));
}
}
}

/// @dev Backfill certLegendHashes for tokens minted before hash tracking was added.
function _ensureCertLegendHashes(uint256 tokenId) internal {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
uint256 textLen = s.certLegend[tokenId].length;
uint256 hashLen = s.certLegendHashes[tokenId].length;
if (hashLen < textLen) {
for (uint256 i = hashLen; i < textLen; i++) {
s.certLegendHashes[tokenId].push(keccak256(bytes(s.certLegend[tokenId][i])));
}
}
}

function addDefaultLegend(string memory newLegend) external onlyIssuanceManager {
_ensureDefaultLegendHashes();
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
bytes32 h = keccak256(bytes(newLegend));
s.defaultLegend.push(newLegend);
s.defaultLegendHashes.push(h);
emit DefaultLegendAdded(s.defaultLegend.length - 1, h);
}

function removeDefaultLegendAt(uint256 index) external onlyIssuanceManager {
_ensureDefaultLegendHashes();
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
if (index >= s.defaultLegend.length) revert InvalidLegendIndex();

// Move the last element to the index being removed (if it's not the last element)
// and then pop the last element
bytes32 removedHash = s.defaultLegendHashes[index];
uint256 lastIndex = s.defaultLegend.length - 1;
if (index != lastIndex) {
s.defaultLegend[index] = s.defaultLegend[lastIndex];
s.defaultLegendHashes[index] = s.defaultLegendHashes[lastIndex];
}
s.defaultLegend.pop();
s.defaultLegendHashes.pop();
emit DefaultLegendRemoved(index, removedHash);
}

function getDefaultLegendAt(uint256 index) external view returns (string memory) {
Expand All @@ -412,35 +463,55 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
return CyberCertPrinterStorage.cyberCertStorage().defaultLegend.length;
}

function addCertLegend(uint256 tokenId, string memory newLegend) external onlyIssuanceManager {
function addCertLegend(uint256 tokenId, string memory newLegend) external onlyIssuanceManagerOrExtension {
_ensureCertLegendHashes(tokenId);
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
bytes32 h = keccak256(bytes(newLegend));
s.certLegend[tokenId].push(newLegend);
s.certLegendHashes[tokenId].push(h);
emit LegendAdded(tokenId, s.certLegend[tokenId].length - 1, h);
}

function removeCertLegendAt(uint256 tokenId, uint256 index) external onlyIssuanceManager {
_ensureCertLegendHashes(tokenId);
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
if (index >= s.certLegend[tokenId].length) revert InvalidLegendIndex();

// Move the last element to the index being removed (if it's not the last element)
// and then pop the last element
bytes32 removedHash = s.certLegendHashes[tokenId][index];
uint256 lastIndex = s.certLegend[tokenId].length - 1;
if (index != lastIndex) {
s.certLegend[tokenId][index] = s.certLegend[tokenId][lastIndex];
s.certLegendHashes[tokenId][index] = s.certLegendHashes[tokenId][lastIndex];
}
s.certLegend[tokenId].pop();
}
s.certLegendHashes[tokenId].pop();
emit LegendRemoved(tokenId, index, removedHash);
}

/// @notice Request legend removal (informational audit trail; actual removal requires removeCertLegendAt)
function requestCertLegendRemoval(uint256 tokenId, uint256 legendIndex, string calldata justification) external {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
if (legendIndex >= s.certLegend[tokenId].length) revert InvalidLegendIndex();
emit LegendRemovalRequested(tokenId, legendIndex, justification);
}

function getCertLegendAt(uint256 tokenId, uint256 index) external view returns (string memory) {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
if (index >= s.certLegend[tokenId].length) revert InvalidLegendIndex();

return s.certLegend[tokenId][index];
}
}

function getCertLegendCount(uint256 tokenId) external view returns (uint256) {
return CyberCertPrinterStorage.cyberCertStorage().certLegend[tokenId].length;
}

/// @notice Get all legends and their hashes for a certificate
function getCertLegends(uint256 tokenId) external view returns (string[] memory texts, bytes32[] memory hashes) {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
return (s.certLegend[tokenId], s.certLegendHashes[tokenId]);
}

function getExtension(uint256 tokenId) external view returns (address) {
return CyberCertPrinterStorage.cyberCertStorage().extension;
}
Expand Down
26 changes: 26 additions & 0 deletions src/IssuanceManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ import "./interfaces/ICertificateConverter.sol";
import "./interfaces/IIssuanceManagerFactory.sol";
import "./storage/IssuanceManagerStorage.sol";

/// @dev Minimal interface for extension legend seeding at mint time.
interface ILegendSeeder {
function initializeLegends(uint256 tokenId, bytes32 seriesId) external;
function setCertPrinter(address _certPrinter) external;
}

/// @title IssuanceManager
/// @notice Manages the issuance and lifecycle of digital certificates representing securities and more
/// @dev Implements UUPS upgradeable pattern and BorgAuth access control
Expand Down Expand Up @@ -197,6 +203,9 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable {
_securitySeries,
_extension
);
if (_extension != address(0)) {
try ILegendSeeder(_extension).setCertPrinter(newCert) {} catch {}
}
emit CertPrinterCreated(
newCert,
IssuanceManagerStorage.getCORP(),
Expand Down Expand Up @@ -224,6 +233,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable {
ICyberCertPrinter cert = ICyberCertPrinter(certAddress);
uint256 tokenId = cert.totalSupply();
uint256 id = cert.safeMint(tokenId, to, _details);
_seedSeriesLegends(certAddress, tokenId, _details.extensionData);
string memory tokenURI = cert.tokenURI(tokenId);
emit CertificateCreated(
tokenId,
Expand Down Expand Up @@ -273,6 +283,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable {
tokenId = cert.totalSupply();

cert.safeMintAndAssign(investor, tokenId, _details);
_seedSeriesLegends(certAddress, tokenId, _details.extensionData);
string memory tokenURI = cert.tokenURI(tokenId);
emit CertificateCreated(
tokenId,
Expand All @@ -285,6 +296,21 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable {
return tokenId;
}

/// @dev If the cert has a share extension with series-specific transfer restrictions,
/// seed those restriction texts as certificate legends. Silently skips if the
/// extension doesn't support initializeLegends (e.g. SAFT, TokenWarrant).
function _seedSeriesLegends(address certAddress, uint256 tokenId, bytes memory extensionData) internal {
if (extensionData.length == 0) return;
address ext = ICyberCertPrinter(certAddress).getExtension(tokenId);
if (ext == address(0)) return;
// First 32 bytes of the ABI-encoded CertificateData is the seriesId
bytes32 seriesId;
// solhint-disable-next-line no-inline-assembly
assembly { seriesId := mload(add(extensionData, 32)) }
if (seriesId == bytes32(0)) return;
try ILegendSeeder(ext).initializeLegends(tokenId, seriesId) {} catch {}
}

/// @notice Adds an issuer's signature to a certificate
/// @dev Only callable by admin, requires valid signature URI
/// @param certAddress Address of the certificate printer contract
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/ICyberCertPrinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,5 @@ interface ICyberCertPrinter is IERC721 {
function totalSupply() external view returns (uint256);
function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);
function setTokenTransferable(uint256 tokenId, bool value) external;
function getExtension(uint256 tokenId) external view returns (address);
}
27 changes: 27 additions & 0 deletions src/libs/StringUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
pragma solidity 0.8.28;

/// @title StringUtils
/// @notice Shared string conversion utilities for CyberCorps extensions
library StringUtils {
function uint256ToString(uint256 _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint256 j = _i;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = uint8(48 + (_i % 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
}
3 changes: 2 additions & 1 deletion src/storage/CyberCertPrinterStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ library CyberCertPrinterStorage {
bool endorsementRequired;
// New variables must be appended below to preserve storage layout for upgrades
mapping(uint256 => bool) tokenTransferable;

mapping(uint256 => bytes32[]) certLegendHashes;
bytes32[] defaultLegendHashes;
}

// Returns the storage layout
Expand Down
40 changes: 10 additions & 30 deletions src/storage/extensions/SAFTEExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "./ICertificateExtension.sol";
import "../../CyberCorpConstants.sol";
import "../../libs/auth.sol";
import "../../libs/StringUtils.sol";

struct SAFTEData {
UnlockStartTimeType unlockStartTimeType;
Expand All @@ -60,6 +61,8 @@ struct SAFTEData {
}

contract SAFTEExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {
using StringUtils for uint256;

bytes32 public constant EXTENSION_TYPE = keccak256("SAFTE");
uint256 public constant PERCENTAGE_PRECISION = 10 ** 4;

Expand Down Expand Up @@ -88,45 +91,22 @@ contract SAFTEExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL {

string memory json = string(abi.encodePacked(
', "SAFTEDetails": {',
'"protocolUSDValuationAtTimeofInvestment": "', uint256ToString(decoded.protocolUSDValuationAtTimeofInvestment),
'"protocolUSDValuationAtTimeofInvestment": "', decoded.protocolUSDValuationAtTimeofInvestment.uint256ToString(),
'", "unlockStartTimeType": "', UnlockStartTimeTypeToString(decoded.unlockStartTimeType),
'", "unlockStartTime": "', uint256ToString(decoded.unlockStartTime),
'", "unlockingPeriod": "', uint256ToString(decoded.unlockingPeriod),
'", "unlockingCliffPeriod": "', uint256ToString(decoded.unlockingCliffPeriod),
'", "unlockingCliffPercentage": "', uint256ToString(decoded.unlockingCliffPercentage),
'", "unlockStartTime": "', decoded.unlockStartTime.uint256ToString(),
'", "unlockingPeriod": "', decoded.unlockingPeriod.uint256ToString(),
'", "unlockingCliffPeriod": "', decoded.unlockingCliffPeriod.uint256ToString(),
'", "unlockingCliffPercentage": "', decoded.unlockingCliffPercentage.uint256ToString(),
'", "unlockingIntervalType": "', UnlockingIntervalTypeToString(decoded.unlockingIntervalType),
'", "tokenCalculationMethod": "', conversionTypeToString(decoded.tokenCalculationMethod),
'", "minCompanyReserve": "', uint256ToString(decoded.minCompanyReserve),
'", "tokenPremiumMultiplier": "', uint256ToString(decoded.tokenPremiumMultiplier),
'", "minCompanyReserve": "', decoded.minCompanyReserve.uint256ToString(),
'", "tokenPremiumMultiplier": "', decoded.tokenPremiumMultiplier.uint256ToString(),
'"}'
));

return json;
}

// Helper function to convert uint256 to string
function uint256ToString(uint256 _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint256 j = _i;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (_i != 0) {
k = k-1;
uint8 temp = uint8(48 + (_i % 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}

function conversionTypeToString(TokenCalculationMethod _type) internal pure returns (string memory) {
if (_type == TokenCalculationMethod.equityProRataToCompanyReserve) return "equityProRataToCompanyReserve";
if (_type == TokenCalculationMethod.equityProRataToTokenSupply) return "equityProRataToTokenSupply";
Expand Down
Loading
Loading