Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/CertificateImageBuilderContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ contract CertificateImageBuilderContract is ICertificateImageBuilder {
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 "";
}

Expand Down
1 change: 1 addition & 0 deletions src/CertificateImageContentBuilder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ library CertificateImageContentBuilder {
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 "";
}

Expand Down
94 changes: 29 additions & 65 deletions src/CyberCertPrinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
error NotIssuanceManager();
error TokenNotTransferable();
error TokenDoesNotExist();
error HolderLimitExceeded(uint256 maxHolderCount);
error InvalidTokenId();
error URIQueryForNonexistentToken();
error URISetForNonexistentToken();
Expand Down Expand Up @@ -91,6 +92,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 MaxLegalHolderCountUpdated(uint256 maxLegalHolderCount);


modifier onlyIssuanceManager() {
Expand Down Expand Up @@ -143,7 +145,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
address to,
CertificateDetails memory details
) external onlyIssuanceManager returns (uint256) {

CyberCertPrinterStorage.updateLegalHolder(tokenId, to);
_safeMint(to, tokenId);
CyberCertPrinterStorage.cyberCertStorage().certLegend[tokenId] = CyberCertPrinterStorage.cyberCertStorage().defaultLegend;
CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId] = details;
Expand All @@ -162,6 +164,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
CertificateDetails memory details,
string memory investorName
) external onlyIssuanceManager returns (uint256) {
CyberCertPrinterStorage.updateLegalHolder(tokenId, to);
_safeMint(to, tokenId);
CyberCertPrinterStorage.cyberCertStorage().certLegend[tokenId] = CyberCertPrinterStorage.cyberCertStorage().defaultLegend;
// Store agreement details
Expand All @@ -183,6 +186,7 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
CertificateDetails memory details
) external onlyIssuanceManager returns (uint256) {
if(ownerOf(tokenId) != from) revert InvalidTokenId();
CyberCertPrinterStorage.updateLegalHolder(tokenId, to);
CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId] = details;
CyberCertPrinterStorage.cyberCertStorage().owners[tokenId] = OwnerDetails(
"",
Expand Down Expand Up @@ -231,81 +235,21 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {

// Restricted burning
function burn(uint256 tokenId) external onlyIssuanceManager {
CyberCertPrinterStorage.updateLegalHolder(tokenId, address(0));
_burn(tokenId);

// Clear agreement details
delete CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId];
delete CyberCertPrinterStorage.cyberCertStorage().issuerSignatures[tokenId];
delete CyberCertPrinterStorage.cyberCertStorage().owners[tokenId];
}

/**
* @dev Override _update to enforce transferability restrictions
* This function is called for all token transfers, mints, and burns
*/
function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address) {
address from = _ownerOf(tokenId);

// Skip restriction checks for minting (from == address(0)) and burning (to == address(0))
if (from != address(0) && to != address(0)) {
// This is a transfer, check built-in transferability flag and per-token override
bool globalTransferable = CyberCertPrinterStorage.cyberCertStorage().transferable;
bool tokenTransferable = CyberCertPrinterStorage.isTokenTransferable(tokenId);
if (!globalTransferable && !tokenTransferable && from != ICyberCorp(IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).CORP()).dealManager() && from != ICyberCorp(IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).CORP()).roundManager()) revert TokenNotTransferable();

// Check security type-specific hook if it exists
/* ITransferRestrictionHook typeHook = CyberCertPrinterStorage.cyberCertStorage().restrictionHooksById[tokenId];

if (address(typeHook) != address(0)) {
(bool allowed, string memory reason) = typeHook.checkTransferRestriction(
from, to, tokenId, ""
);
if (!allowed) revert TransferRestricted(reason);
}*/

// Check global hook if it exists
if (address(CyberCertPrinterStorage.cyberCertStorage().globalRestrictionHook) != address(0)) {
(bool allowed, string memory reason) = CyberCertPrinterStorage.cyberCertStorage().globalRestrictionHook.checkTransferRestriction(
from, to, tokenId, ""
);
if (!allowed) revert TransferRestricted(reason);
}

address ownerAddress = CyberCertPrinterStorage.cyberCertStorage().owners[tokenId].ownerAddress;
//check endorsement and update owners
if(from == ownerAddress) {
if(!CyberCertPrinterStorage.cyberCertStorage().endorsementRequired) {
emit CertificateAssigned(tokenId, to, "", IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).companyName());
CyberCertPrinterStorage.cyberCertStorage().owners[tokenId] = OwnerDetails("", to);
}
else if(CyberCertPrinterStorage.cyberCertStorage().endorsements[tokenId].length > 0) {
Endorsement memory endorsement = CyberCertPrinterStorage.cyberCertStorage().endorsements[tokenId][CyberCertPrinterStorage.cyberCertStorage().endorsements[tokenId].length - 1];
if (endorsement.endorsee == to) {
// Endorsement exists; ownership will be updated
emit CertificateAssigned(tokenId, to, endorsement.endorseeName, IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).companyName());
CyberCertPrinterStorage.cyberCertStorage().owners[tokenId] = OwnerDetails(endorsement.endorseeName, endorsement.endorsee);
}
}
// NOTE: we don't revert in this block: Owner is able to transfer to another address without an endorsement, but it does not update the owner
}
else if(CyberCertPrinterStorage.cyberCertStorage().endorsements[tokenId].length > 0) {
// Token is not being transferred from the current owner. It can only be transferrred to the latest endorsee, or the current owner
Endorsement memory endorsement = CyberCertPrinterStorage.cyberCertStorage().endorsements[tokenId][CyberCertPrinterStorage.cyberCertStorage().endorsements[tokenId].length - 1];
if(endorsement.endorsee != to && ownerAddress != to) revert EndorsementNotSignedOrInvalid();

emit CertificateAssigned(tokenId, to, endorsement.endorseeName, IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).companyName());
CyberCertPrinterStorage.cyberCertStorage().owners[tokenId] = OwnerDetails(endorsement.endorseeName, endorsement.endorsee);
}
else revert EndorsementNotSignedOrInvalid();

CyberCertPrinterStorage.validateAndUpdateTransfer(tokenId, from, to);
}
// Emit custom transfer event for indexing
emit CyberCertTransfer(
from,
to,
tokenId
);

// Call the parent implementation to handle the actual transfer
emit CyberCertTransfer(from, to, tokenId);
return super._update(to, tokenId, auth);
}

Expand Down Expand Up @@ -511,6 +455,26 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable {
return CyberCertPrinterStorage.cyberCertStorage().tokenTransferable[tokenId];
}

function setMaxLegalHolderCount(uint256 maxHolders) external onlyIssuanceManager {
CyberCertPrinterStorage.cyberCertStorage().maxLegalHolderCount = maxHolders;
emit MaxLegalHolderCountUpdated(maxHolders);
}

function legalHolderCount() external view returns (uint256) {
return CyberCertPrinterStorage.cyberCertStorage().legalHolderCount;
}

function maxLegalHolderCount() external view returns (uint256) {
return CyberCertPrinterStorage.cyberCertStorage().maxLegalHolderCount;
}

function remainingLegalHolderSlots() external view returns (uint256) {
CyberCertPrinterStorage.CyberCertStorage storage s = CyberCertPrinterStorage.cyberCertStorage();
if (s.maxLegalHolderCount == 0) return type(uint256).max;
if (s.legalHolderCount >= s.maxLegalHolderCount) return 0;
return s.maxLegalHolderCount - s.legalHolderCount;
}

function legalOwnerOf(uint256 tokenId) external view returns (address) {
if (!_exists(tokenId)) revert TokenDoesNotExist();
return CyberCertPrinterStorage.cyberCertStorage().owners[tokenId].ownerAddress;
Expand Down
3 changes: 2 additions & 1 deletion src/CyberCorpConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ enum SecuritySeries {
SeriesD,
SeriesE,
SeriesF,
NA
NA,
ACE
}

enum SecurityStatus {
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/ICyberCertPrinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,8 @@ 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 legalHolderCount() external view returns (uint256);
function maxLegalHolderCount() external view returns (uint256);
function setMaxLegalHolderCount(uint256 max) external;
function remainingLegalHolderSlots() external view returns (uint256);
}
80 changes: 78 additions & 2 deletions src/storage/CyberCertPrinterStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ struct OwnerDetails {
address ownerAddress;
}

event CertificateAssigned(uint256 indexed tokenId, address indexed newOwner, string newOwnerName, string issuerName);

error TokenNotTransferable();
error TransferRestricted(string reason);
error EndorsementNotSignedOrInvalid();
error HolderLimitExceeded(uint256 maxHolderCount);

library CyberCertPrinterStorage {
// Storage slot for our struct
bytes32 constant STORAGE_POSITION = keccak256("cybercorp.cert.printer.storage.v1");
Expand Down Expand Up @@ -100,7 +107,9 @@ library CyberCertPrinterStorage {
// New variables must be appended below to preserve storage layout for upgrades
mapping(uint256 => bool) tokenTransferable;
mapping(uint256 => bytes[]) issuerSignatures;

uint256 legalHolderCount;
uint256 maxLegalHolderCount;
mapping(address => uint256) legalHolderTokenCount;
}

// Returns the storage layout
Expand Down Expand Up @@ -260,4 +269,71 @@ library CyberCertPrinterStorage {
return cyberCertStorage().certificateDetails[tokenId].extensionData;
}

}
function _updateLegalHolder(uint256 tokenId, address newOwner) internal {
CyberCertStorage storage s = cyberCertStorage();
address oldOwner = s.owners[tokenId].ownerAddress;
if (oldOwner == newOwner) return;

if (newOwner != address(0) && s.maxLegalHolderCount > 0 && s.legalHolderTokenCount[newOwner] == 0) {
if (s.legalHolderCount >= s.maxLegalHolderCount) revert HolderLimitExceeded(s.maxLegalHolderCount);
}

if (oldOwner != address(0) && s.legalHolderTokenCount[oldOwner] > 0) {
s.legalHolderTokenCount[oldOwner] -= 1;
if (s.legalHolderTokenCount[oldOwner] == 0) s.legalHolderCount -= 1;
}

if (newOwner != address(0)) {
if (s.legalHolderTokenCount[newOwner] == 0) s.legalHolderCount += 1;
s.legalHolderTokenCount[newOwner] += 1;
}
}

function updateLegalHolder(uint256 tokenId, address newOwner) external {
_updateLegalHolder(tokenId, newOwner);
}

function validateAndUpdateTransfer(uint256 tokenId, address from, address to) external {
CyberCertStorage storage s = cyberCertStorage();

ICyberCorp corp = ICyberCorp(IIssuanceManager(s.issuanceManager).CORP());
if (!s.transferable && !s.tokenTransferable[tokenId]
&& from != corp.dealManager() && from != corp.roundManager()) {
revert TokenNotTransferable();
}

if (address(s.globalRestrictionHook) != address(0)) {
(bool allowed, string memory reason) =
s.globalRestrictionHook.checkTransferRestriction(from, to, tokenId, "");
if (!allowed) revert TransferRestricted(reason);
}

address ownerAddress = s.owners[tokenId].ownerAddress;
string memory issuerName = IIssuanceManager(s.issuanceManager).companyName();

if (from == ownerAddress) {
if (!s.endorsementRequired) {
_updateLegalHolder(tokenId, to);
emit CertificateAssigned(tokenId, to, "", issuerName);
s.owners[tokenId] = OwnerDetails("", to);
} else if (s.endorsements[tokenId].length > 0) {
Endorsement memory e = s.endorsements[tokenId][s.endorsements[tokenId].length - 1];
if (e.endorsee == to) {
_updateLegalHolder(tokenId, e.endorsee);
emit CertificateAssigned(tokenId, to, e.endorseeName, issuerName);
s.owners[tokenId] = OwnerDetails(e.endorseeName, e.endorsee);
}
}
// NOTE: no revert — owner may transfer without updating legal record
} else if (s.endorsements[tokenId].length > 0) {
Endorsement memory e = s.endorsements[tokenId][s.endorsements[tokenId].length - 1];
if (e.endorsee != to && ownerAddress != to) revert EndorsementNotSignedOrInvalid();
_updateLegalHolder(tokenId, e.endorsee);
emit CertificateAssigned(tokenId, to, e.endorseeName, issuerName);
s.owners[tokenId] = OwnerDetails(e.endorseeName, e.endorsee);
} else {
revert EndorsementNotSignedOrInvalid();
}
}

}
Loading
Loading