diff --git a/.gitignore b/.gitignore index adb79d2e..ccaa5c00 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ docs/ # IDE /.idea/ /.vscode/ +/.claude/ diff --git a/foundry.toml b/foundry.toml index aeb1b570..6ac991be 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,11 @@ out = "out" libs = ["lib", "dependencies"] optimizer = true optimizer_runs = 15 -fs_permissions = [{ access = "read", path = "./script/res" }] +extra_output_files = ['abi'] +fs_permissions = [ + { access = "read", path = "./script/res" }, + { access = "read", path = "./test/res" } +] [fmt] sort_imports = true diff --git a/script/UpdateCertificate.s.sol b/script/UpdateCertificate.s.sol new file mode 100644 index 00000000..c9eb768a --- /dev/null +++ b/script/UpdateCertificate.s.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {IIssuanceManager} from "../src/interfaces/IIssuanceManager.sol"; +import {CertificateDetails} from "../src/storage/CyberCertPrinterStorage.sol"; + +/// @notice Script to update certificate details via the IssuanceManager +/// @dev Run with: forge script script/UpdateCertificate.s.sol --rpc-url $RPC_URL --broadcast +contract UpdateCertificate is Script { + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY_MAIN"); + address deployer = vm.addr(deployerPrivateKey); + + address issuanceManagerAddress = 0x23c3a16AdB129Da2FCB297C63F6015C201dB2AC1; + address printerAddress = 0xCB00123c91DB928CcF885FCE4f30919B0caB5845; + uint256 tokenId = 0; + + + // Prepare the details struct based on the provided image + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Test Officer", + signingOfficerTitle: "CEO", + investmentAmountUSD: 1000000000000000000, + issuerUSDValuationAtTimeOfInvestment: 100000000000000000000, // 1e20 + unitsRepresented: 1000000000000000000, // 1e20 + legalDetails: "Dispute resolution method: Binding Arbitration\nGoverning law: Delaware", + // Use hex literal for raw byte encoding + extensionData: hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000000004" + }); + + console.log("=== Updating Certificate Details via IssuanceManager ==="); + console.log("IssuanceManager:", issuanceManagerAddress); + console.log("Printer Address:", printerAddress); + console.log("Token ID:", tokenId); + console.log("Caller:", deployer); + + vm.startBroadcast(deployerPrivateKey); + + // Call updateCertificateDetails on the IssuanceManager + // IIssuanceManager(issuanceManagerAddress).updateCertificateDetails(printerAddress, tokenId, details); + + vm.stopBroadcast(); + + console.log("Update call via IssuanceManager broadcasted successfully!"); + } +} diff --git a/script/add-spa-plus-templates.s.sol b/script/add-spa-plus-templates.s.sol new file mode 100644 index 00000000..f4cc2dd6 --- /dev/null +++ b/script/add-spa-plus-templates.s.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import {Script} from "forge-std/Script.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; + +contract AddSpaPlusTemplatesScript is Script { + address internal constant REGISTRY = + 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY_MAIN"); + vm.startBroadcast(deployerPrivateKey); + + string[] memory globalFieldsSafe = _globalFieldsSafe(); + string[] memory globalFieldsSafeTokenWarrant = _globalFieldsSafeTokenWarrant(); + string[] memory globalFieldsSafte = _globalFieldsSafte(); + string[] memory globalFieldsSaft = _globalFieldsSaft(); + string[] memory partyFields = _partyFields(); + + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_safe_reg_d_v1_3")), + "mlx_safe_reg_d_v1_3", + "IPFS://bafybeih7l2kxncjuwrfgv5gnmpcik43dnn4pxpe4it4u7ti2hgfgrlot2a", + globalFieldsSafe, + partyFields + ); + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_safe_reg_s_v1_3")), + "mlx_safe_reg_s_v1_3", + "IPFS://bafybeieh7jn553jmrjmwee3dsvwf5hkedomey2vhubc3mumlewfpumvlae", + globalFieldsSafe, + partyFields + ); + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_safe_tw_reg_d_v1_3")), + "mlx_safe_tw_reg_d_v1_3", + "IPFS://bafybeiaw3pwov3ahg4bk2hte2hu4pwv34nndoguxyk3umq6f5su3kod6ay", + globalFieldsSafeTokenWarrant, + partyFields + ); + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_safe_tw_reg_s_v1_3")), + "mlx_safe_tw_reg_s_v1_3", + "IPFS://bafybeicto2raupsj5ad7snxvhmmll2plwyploqho4fg2cibnn2fuhlm2d4", + globalFieldsSafeTokenWarrant, + partyFields + ); + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_safte_reg_d_v1_3")), + "mlx_safte_reg_d_v1_3", + "IPFS://bafybeiag7xatsusb24evnpyj6ztf62kix36dgbsp3kbazfyvr273ph56ay", + globalFieldsSafte, + partyFields + ); + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_safte_reg_s_v1_3")), + "mlx_safte_reg_s_v1_3", + "IPFS://bafybeia43r7e566s2jlq4gtaasmtybutujy7fuizhw3fycxtwnstfbkeia", + globalFieldsSafte, + partyFields + ); + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_saft_reg_d_v1_3")), + "mlx_saft_reg_d_v1_3", + "IPFS://bafybeieoljri2rwuv35rymjd654sr3u46kbcao7mymseqobfo7x6lxgdcy", + globalFieldsSaft, + partyFields + ); + CyberAgreementRegistry(REGISTRY).createTemplate( + bytes32(bytes("mlx_saft_reg_s_v1_3")), + "mlx_saft_reg_s_v1_3", + "IPFS://bafybeibwrz3rttteguo5ccoh5x7ndwdu6hyhy7i3iraii5c5ml4pfv73t4", + globalFieldsSaft, + partyFields + ); + + vm.stopBroadcast(); + } + + function _globalFieldsSafe() internal pure returns (string[] memory fields) { + fields = new string[](5); + fields[0] = "purchaseAmount"; + fields[1] = "postMoneyValuationCap"; + fields[2] = "expirationTime"; + fields[3] = "governingJurisdiction"; + fields[4] = "disputeResolution"; + } + + function _globalFieldsSafeTokenWarrant() + internal + pure + returns (string[] memory fields) + { + fields = new string[](17); + fields[0] = "purchaseAmount"; + fields[1] = "postMoneyValuationCap"; + fields[2] = "expirationTime"; + fields[3] = "governingJurisdiction"; + fields[4] = "disputeResolution"; + fields[5] = "exercisePriceMethod"; + fields[6] = "exercisePrice"; + fields[7] = "unlockStartTimeType"; + fields[8] = "unlockStartTime"; + fields[9] = "unlockingPeriod"; + fields[10] = "latestExpirationTime"; + fields[11] = "unlockingCliffPeriod"; + fields[12] = "unlockingCliffPercentage"; + fields[13] = "unlockingIntervalType"; + fields[14] = "tokenCalculationMethod"; + fields[15] = "minCompanyReserve"; + fields[16] = "tokenPremiumMultiplier"; + } + + function _globalFieldsSafte() internal pure returns (string[] memory fields) { + fields = new string[](15); + fields[0] = "purchaseAmount"; + fields[1] = "postMoneyValuationCap"; + fields[2] = "protocolUSDValuationAtTimeofInvestment"; + fields[3] = "expirationTime"; + fields[4] = "governingJurisdiction"; + fields[5] = "disputeResolution"; + fields[6] = "unlockStartTimeType"; + fields[7] = "unlockStartTime"; + fields[8] = "unlockingPeriod"; + fields[9] = "unlockingCliffPeriod"; + fields[10] = "unlockingCliffPercentage"; + fields[11] = "unlockingIntervalType"; + fields[12] = "tokenCalculationMethod"; + fields[13] = "minCompanyReserve"; + fields[14] = "tokenPremiumMultiplier"; + } + + function _globalFieldsSaft() internal pure returns (string[] memory fields) { + fields = new string[](10); + fields[0] = "purchaseAmount"; + fields[1] = "protocolValuationCap"; + fields[2] = "governingJurisdiction"; + fields[3] = "disputeResolution"; + fields[4] = "unlockStartTimeType"; + fields[5] = "unlockStartTime"; + fields[6] = "unlockingPeriod"; + fields[7] = "unlockingCliffPeriod"; + fields[8] = "unlockingCliffPercentage"; + fields[9] = "unlockingIntervalType"; + } + + function _partyFields() internal pure returns (string[] memory fields) { + fields = new string[](5); + fields[0] = "name"; + fields[1] = "evmAddress"; + fields[2] = "contactDetails"; + fields[3] = "investorType"; + fields[4] = "investorJurisdiction"; + } +} diff --git a/script/deploy-extensions-v2.s.sol b/script/deploy-extensions-v2.s.sol new file mode 100644 index 00000000..39297f73 --- /dev/null +++ b/script/deploy-extensions-v2.s.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {SAFEExtension} from "../src/storage/extensions/SAFEExtension.sol"; +import {SAFTEExtensionV2} from "../src/storage/extensions/SAFTEExtensionV2.sol"; +import {SAFTExtensionV2} from "../src/storage/extensions/SAFTExtensionV2.sol"; +import {TokenWarrantExtensionV2} from "../src/storage/extensions/TokenWarrantExtensionV2.sol"; + +contract BaseScript is Script { + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY_MAIN"); + vm.startBroadcast(deployerPrivateKey); + + bytes32 salt = bytes32(keccak256("MetaLexCyberCorpLaunchV2.2-Extensions")); + BorgAuth auth = BorgAuth(0x033012a1eDA6e2E00D12CD37c5b63B9440ef5E01); + + address safeExtension = address( + new ERC1967Proxy{salt: salt}( + address(new SAFEExtension{salt: salt}()), + abi.encodeWithSelector(SAFEExtension.initialize.selector, address(auth)) + ) + ); + + address safteExtensionV2 = address( + new ERC1967Proxy{salt: salt}( + address(new SAFTExtensionV2{salt: salt}()), + abi.encodeWithSelector(SAFTExtensionV2.initialize.selector, address(auth)) + ) + ); + + address safteExtensionV2Long = address( + new ERC1967Proxy{salt: salt}( + address(new SAFTEExtensionV2{salt: salt}()), + abi.encodeWithSelector(SAFTEExtensionV2.initialize.selector, address(auth)) + ) + ); + + address tokenWarrantExtensionV2 = address( + new ERC1967Proxy{salt: salt}( + address(new TokenWarrantExtensionV2{salt: salt}()), + abi.encodeWithSelector(TokenWarrantExtensionV2.initialize.selector, address(auth)) + ) + ); + + console.log("SAFEExtension: ", safeExtension); + console.log("SAFTExtensionV2: ", safteExtensionV2); + console.log("SAFTEExtensionV2: ", safteExtensionV2Long); + console.log("TokenWarrantExtensionV2: ", tokenWarrantExtensionV2); + } +} diff --git a/script/deploy-metadao-factory.s.sol b/script/deploy-metadao-factory.s.sol index a4a0f9cd..826c04d4 100644 --- a/script/deploy-metadao-factory.s.sol +++ b/script/deploy-metadao-factory.s.sol @@ -18,8 +18,9 @@ import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; import {CyberScrip} from "../src/CyberScrip.sol"; import {BorgAuth} from "../src/libs/auth.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {CompanyOfficer} from "../src/CyberCorpConstants.sol"; -contract DeployMetaDAOFactoryScript is Script { +contract DeployScript is Script { // Hard-coded since we don't have programmatic access to CyberAgreementRegistry's underlying types string constant DOMAIN_SEPARATOR_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; string constant ESCROW_SIGNATUREDATA_TYPE = "EscrowSignatureData(string legalContractUri,string[] partyFields,string[] partyValues)"; @@ -41,58 +42,35 @@ contract DeployMetaDAOFactoryScript is Script { CyberAgreementRegistry registry, MetaDAOFactory metaDAOFactory ) { - return run( + return runWithArgs( vm.envUint("PRIVATE_KEY_MAIN"), // deployerPrivateKey - // TODO: review needed: is this up to date? - 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C, // multisig - "" // TBD: ask MetaDAO to sign + 0x59026c9A3871505c8E5fb0B021e274a0B28547F6, // corpPayable + 0x76A6168B69f8f1b27E06dC77a30F2D1C92733e7A, // officerAddress + hex"63f62ac9b08c813401a02a16a820a106e525ac65dff992dccfd2cb42e5423db6725bb1b4d6e0244a635665f4965514512253613e3b032491f7ec85c2f657154e1b" // metadaoEscrowSig ); } - function run( + function runWithArgs( uint256 deployerPrivateKey, - address multisig, + address corpPayable, + address officerAddress, bytes memory metadaoEscrowSig ) public returns (CyberAgreementRegistry registry, MetaDAOFactory metaDAOFactory) { // Other configs - string memory metaDAOOfficerName = "MetaDAO Officer"; // TODO TBD - string memory metaDAOOfficerContact = "metadao@example.com"; // TODO TBD - string memory metaDAOOfficerTitle = "CEO"; // TODO TBD + string memory metaDAOOfficerName = "MetaDAO LLC, a Marshall Islands DAO limited liability company"; + string memory metaDAOOfficerContact = "market.governed.civilization@metadao.fi PO Box 852, Long Island Rd, Majuro, Marshall Islands MH 96960"; + string memory metaDAOOfficerTitle = "Director & Management Shareholder"; address deployerAddress = vm.addr(deployerPrivateKey); vm.startBroadcast(deployerPrivateKey); bytes32 salt = bytes32(keccak256("MetaDAOFactory.deploy.v1")); - uint256 currentChainId = block.chainid; - address stable; - - if (currentChainId == 1) { - stable = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // Mainnet USDC - } else if (currentChainId == 42161) { - stable = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; // Arbitrum USDC - } else if (currentChainId == 8453) { - stable = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; // Base USDC - } else if (currentChainId == 84532) { - stable = 0x036CbD53842c5426634e7929541eC2318f3dCF7e; // Base Sepolia USDC - } else if (currentChainId == 11155111) { - stable = 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238; // Sepolia USDC - } else { - revert("Unsupported chain ID"); - } + address stable = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; // USDC @ Base BorgAuth auth = new BorgAuth{salt: salt}(deployerAddress); - address registry = address( - new ERC1967Proxy{salt: salt}( - address(new CyberAgreementRegistry{salt: salt}()), - abi.encodeWithSelector( - CyberAgreementRegistry.initialize.selector, - address(auth) - ) - ) - ); - + address registry = 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134; // Create templates string[] memory globalFields = new string[](8); @@ -110,81 +88,24 @@ contract DeployMetaDAOFactoryScript is Script { // Create template for SegCo string memory segCoAgreementTitle = "MetaDAO Futarchy Governance SPC - SegCo combined v 1.0"; - CyberAgreementRegistry(registry).createTemplate( - keccak256(bytes(segCoAgreementTitle)), - segCoAgreementTitle, - "ipfs://bafybeifpvfwxfmobk7nhflsczqiynp3ca5urvyk3duh7s3rwptcnfzhuje", - globalFields, - partyFields - ); + string memory segCoAgreementUri = "ipfs://bafybeifpvfwxfmobk7nhflsczqiynp3ca5urvyk3duh7s3rwptcnfzhuje"; // Create template for Board Consent string memory boardConsentTitle = "MetaDAO Futarchy Governance SPC - Board Consent - Approval of SegCo v 1.0"; string memory boardConsentUri = "ipfs://bafkreic7dscoigvwjc23vzvkmzophm34kpafu6nrctykq5bif63lqvpuoa"; - CyberAgreementRegistry(registry).createTemplate( - keccak256(bytes(boardConsentTitle)), - boardConsentTitle, - boardConsentUri, - globalFields, - partyFields - ); - address uriBuilder = address( - new ERC1967Proxy{salt: salt}( - address(new CertificateUriBuilder{salt: salt}()), - abi.encodeWithSelector( - CertificateUriBuilder.initialize.selector, - address(auth) - ) - ) - ); - address issuanceManagerImplementation = address(new IssuanceManager{salt: salt}()); - address cyberCertPrinterImplementation = address(new CyberCertPrinter{salt: salt}()); - address cyberCert20Implementation = address(new CyberScrip{salt: salt}()); - address issuanceManagerFactory = address( - new ERC1967Proxy{salt: salt}( - address(new IssuanceManagerFactory{salt: salt}()), - abi.encodeWithSelector( - IssuanceManagerFactory.initialize.selector, - address(auth), - issuanceManagerImplementation, - cyberCertPrinterImplementation, - cyberCert20Implementation - ) - ) - ); + address uriBuilder = 0x5500c095ea7dE6F8a5E15949e24B80604cc670A3; - address cyberCorpSingleFactory = address( - new ERC1967Proxy{salt: salt}( - address(new CyberCorpSingleFactory{salt: salt}()), - abi.encodeWithSelector( - CyberCorpSingleFactory.initialize.selector, - address(auth), - address(new CyberCorp()) - ) - ) - ); - address dealManagerFactory = address( - new ERC1967Proxy{salt: salt}( - address(new DealManagerFactory{salt: salt}()), - abi.encodeWithSelector( - DealManagerFactory.initialize.selector, - address(auth), - address(new DealManager()) - ) - ) - ); - address roundManagerFactory = address( - new ERC1967Proxy{salt: salt}( - address(new RoundManagerFactory{salt: salt}()), - abi.encodeWithSelector( - RoundManagerFactory.initialize.selector, - address(auth), - address(new RoundManager()) - ) - ) - ); + address issuanceManagerFactory = 0xA32547aAdAA4975082D729c79e79dBaE4385EBCf; + + address cyberCorpSingleFactory = 0xc8e084D3f8B3b326FCc894C7afD28F4904196406; + + address dealManagerFactory = 0x975df8A99C895d04ae158F8C91Ba562Fce3ECDA3; + + // upgrade CyberAgreementRegistry + address newAgreementRegistryImplementation = address(new CyberAgreementRegistry{salt: salt}()); + CyberAgreementRegistry(registry).upgradeToAndCall(newAgreementRegistryImplementation, ""); MetaDAOFactory metaDAOFactory = MetaDAOFactory( address( @@ -197,7 +118,7 @@ contract DeployMetaDAOFactoryScript is Script { address(issuanceManagerFactory), address(cyberCorpSingleFactory), address(dealManagerFactory), - address(roundManagerFactory), + address(0), address(uriBuilder), address(stable) ) @@ -206,32 +127,10 @@ contract DeployMetaDAOFactoryScript is Script { ); // Configure MetaDAO officer and escrowed signature BEFORE revoking deployer ownership - metaDAOFactory.setMetaDAOOfficerEOA(multisig); + metaDAOFactory.setMetaDAOOfficerEOA(officerAddress); metaDAOFactory.setMetaDAOOfficerName(metaDAOOfficerName); metaDAOFactory.setMetaDAOOfficerContact(metaDAOOfficerContact); metaDAOFactory.setMetaDAOOfficerTitle(metaDAOOfficerTitle); - - if (metadaoEscrowSig.length > 0) { - // If we have the signature to escrow, set it - metaDAOFactory.setMetaDAOSignatureHash(metadaoEscrowSig); - - } else { - // Otherwise, output the typed data for MetaDAO to sign off-chain - string[] memory partyValues = new string[](2); - partyValues[0] = metaDAOOfficerName; - partyValues[1] = metaDAOOfficerContact; - - console.log("Signature required: have MetaDAO sign the following EIP-712 typed data:"); - console.log(" (can be signed with command `cast wallet sign --data ''`)"); - console.log("==== JSON data start ===="); - console.log(_formatEscrowAgreementTypedDataJson( - CyberAgreementRegistry(registry), - boardConsentUri, - partyFields, - partyValues - )); - console.log("==== JSON data end ===="); - } // Create the parent corp (one-time). Reverts if called again. (address parentCorp, @@ -239,18 +138,19 @@ contract DeployMetaDAOFactoryScript is Script { address parentIssuance, address parentDealMgr, address parentRoundMgr) = metaDAOFactory.createParentCorp( - bytes32(keccak256("MetaDAO.parent.corp.v1")), - "MetaLeX MetaDAO", - "corporation", - "DE", - "contact@metadao.example", - "arbitration", - multisig + bytes32(keccak256("Futarchy Governance SPC")), + "Futarchy Governance SPC", + "segregated portfolio company", + "Cayman Islands", + "market.governed.civilization@metadao.fi", + "binding arbitration", + corpPayable ); // Assign roles and revoke EOA ownership (after setup) - auth.updateRole(address(multisig), auth.OWNER_ROLE()); - auth.zeroOwner(); + auth.updateRole(address(officerAddress), auth.OWNER_ROLE()); + auth.updateRole(address(corpPayable), auth.OWNER_ROLE()); + console.log("Auth:", address(auth)); console.log("CyberAgreementRegistry:", address(registry)); @@ -258,15 +158,16 @@ contract DeployMetaDAOFactoryScript is Script { console.log("IssuanceManagerFactory:", address(issuanceManagerFactory)); console.log("CyberCorpSingleFactory:", address(cyberCorpSingleFactory)); console.log("DealManagerFactory:", address(dealManagerFactory)); - console.log("RoundManagerFactory:", address(roundManagerFactory)); - console.log("CyberCertPrinter Impl:", address(cyberCertPrinterImplementation)); - console.log("CyberScrip Impl:", address(cyberCert20Implementation)); + // console.log("RoundManagerFactory:", address(roundManagerFactory)); + //console.log("CyberCertPrinter Impl:", address(cyberCertPrinterImplementation)); + // console.log("CyberScrip Impl:", address(cyberCert20Implementation)); console.log("MetaDAOFactory (proxy):", address(metaDAOFactory)); console.log("ParentCorp:", parentCorp); console.log("ParentAuth:", parentAuth); console.log("ParentIssuance:", parentIssuance); console.log("ParentDealMgr:", parentDealMgr); - console.log("ParentRoundMgr:", parentRoundMgr); + console.log("NewAgreementRegistryImplementation:", address(newAgreementRegistryImplementation)); + //console.log("ParentRoundMgr:", parentRoundMgr); vm.stopBroadcast(); @@ -304,4 +205,4 @@ contract DeployMetaDAOFactoryScript is Script { vm.serializeString("outputKey", "primaryType", "EscrowSignatureData"); return vm.serializeString("outputKey", "types", "{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"EscrowSignatureData\":[{\"name\":\"legalContractUri\",\"type\":\"string\"},{\"name\":\"partyFields\",\"type\":\"string[]\"},{\"name\":\"partyValues\",\"type\":\"string[]\"}]}"); } -} +} \ No newline at end of file diff --git a/script/deploy-non-us-zkpassport-condition.s.sol b/script/deploy-non-us-zkpassport-condition.s.sol new file mode 100644 index 00000000..52414941 --- /dev/null +++ b/script/deploy-non-us-zkpassport-condition.s.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {NonUSNationalityCondition} from "../src/libs/conditions/NonUSNationalityCondition.sol"; +import {OrCondition} from "../src/libs/conditions/OrCondition.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {DeploymentConstants} from "./libs/DeploymentConstants.sol"; + +contract DeployNonUsZkPassportConditionScript is Script { + function run() public returns (BorgAuth zkpassportAuth, NonUSNationalityCondition zkpassportCondition) { + return runWithArgs( + // Production + "MetaLexCyberCorp.NonUSNationalityZkpassport.V1.0.0", + vm.envUint("PRIVATE_KEY_MAIN"), + "ace.metalex.tech", + "non-us-non-sanctioned", + 2592000, // 3600 * 24 * 30 days + DeploymentConstants.BASE + +// // Staging +// "MetaLexCyberCorp.NonUSNationalityZkpassport.V1.0.0.staging.dev1", +// vm.envUint("PRIVATE_KEY_MAIN"), +// "staging.ace.metalex.tech", +// "non-us-non-sanctioned", +// 2592000, // 3600 * 24 * 30 days +// DeploymentConstants.BASE + ); + } + + function runWithArgs( + string memory saltStr, + uint256 deployerPrivateKey, + string memory expectedDomain, + string memory expectedScope, + uint256 maxValidityPeriod, + uint256 chainId + ) public returns (BorgAuth zkpassportAuth, NonUSNationalityCondition zkpassportCondition) { + + bytes32 salt = keccak256(bytes(saltStr)); + + address deployerAddress = vm.addr(deployerPrivateKey); + + DeploymentConstants.CoreDeployment memory deployment = DeploymentConstants + .coreV2(chainId); + + string[] memory outCountries = new string[](9); + outCountries[0] = "IRN"; + outCountries[1] = "IRQ"; + outCountries[2] = "LBY"; + outCountries[3] = "PRK"; + outCountries[4] = "SDN"; + outCountries[5] = "SOM"; + outCountries[6] = "SYR"; + outCountries[7] = "USA"; + outCountries[8] = "YEM"; + + vm.startBroadcast(deployerPrivateKey); + + zkpassportAuth = new BorgAuth{salt: salt}(deployerAddress); + + zkpassportCondition = new NonUSNationalityCondition{salt: salt}( + address(zkpassportAuth), + expectedDomain, + expectedScope, + address(0), // verifier (use default) + maxValidityPeriod, + outCountries + ); + + // Deploy OrCondition (zkPassport || LexChex) + address[] memory orAddrs = new address[](2); + orAddrs[0] = address(zkpassportCondition); + orAddrs[1] = deployment.lexchexCondition; + OrCondition orCondition = new OrCondition(orAddrs); + + vm.stopBroadcast(); + + console2.log("==== Configs ===="); + console2.log("chainId: %d", chainId); + console2.log("salt string: %s", saltStr); + console2.log("deployer: %s", deployerAddress); + console2.log("LexChexCondition:", address(deployment.lexchexCondition)); + console2.log("Expected domain:", expectedDomain); + console2.log("Expected scope:", expectedScope); + console2.log(""); + + console2.log("==== Deployed ===="); + console2.log("zkpassportAuth:", address(zkpassportAuth)); + console2.log("NonUSNationalityCondition:", address(zkpassportCondition)); + console2.log("OrCondition(zkPassport || lexchex):", address(orCondition)); + console2.log(""); + } +} diff --git a/script/deploy-parentco-factory.s.sol b/script/deploy-parentco-factory.s.sol new file mode 100644 index 00000000..3699526e --- /dev/null +++ b/script/deploy-parentco-factory.s.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {ParentCoFactory} from "../src/ParentCoFactory.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {DeploymentConstants} from "./libs/DeploymentConstants.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {CyberAgreementUtils} from "../test/libs/CyberAgreementUtils.sol"; +import {CompanyOfficer} from "../src/CyberCorpConstants.sol"; + +contract DeployParentCoFactoryScript is Script { + uint256 internal constant BASE_SEPOLIA_CHAIN_ID = 84532; + address internal constant BASE_USDC = + 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + + function run() public returns (ParentCoFactory parentCoFactory) { + return + runWithArgs( + vm.envUint("PRIVATE_KEY_MAIN"), + 0x42069BaBe92462393FaFdc653A88F958B64EC9A3, // test corp payable + 0x42069BaBe92462393FaFdc653A88F958B64EC9A3 // test officer EOA + ); + } + + function runWithArgs( + uint256 deployerPrivateKey, + address corpPayable, + address officerAddress + ) public returns (ParentCoFactory parentCoFactory) { + string memory parentCoOfficerName = "Test ParentCo Officer"; + string memory parentCoOfficerContact = "test@parentco.example"; + string memory parentCoOfficerTitle = "Director"; + + DeploymentConstants.CoreDeployment memory deployment = DeploymentConstants + .coreV2(BASE_SEPOLIA_CHAIN_ID); + + address deployerAddress = vm.addr(deployerPrivateKey); + bytes32 salt = bytes32(keccak256("ParentCoFactory.deploy.v2")); + + vm.startBroadcast(deployerPrivateKey); + + BorgAuth auth = new BorgAuth{salt: salt}(deployerAddress); + + parentCoFactory = ParentCoFactory( + address( + new ERC1967Proxy{salt: salt}( + address(new ParentCoFactory{salt: salt}()), + abi.encodeWithSelector( + ParentCoFactory.initialize.selector, + address(auth), + deployment.cyberAgreementRegistry, + deployment.issuanceManagerFactory, + deployment.cyberCorpSingleFactory, + deployment.dealManagerFactory, + deployment.roundManagerFactory, + deployment.uriBuilder, + BASE_USDC + ) + ) + ) + ); + + parentCoFactory.setParentCoOfficerEOA(officerAddress); + parentCoFactory.setParentCoOfficerName(parentCoOfficerName); + parentCoFactory.setParentCoOfficerContact(parentCoOfficerContact); + parentCoFactory.setParentCoOfficerTitle(parentCoOfficerTitle); + + ( + address parentCorp, + address parentAuth, + address parentIssuance, + address parentDealMgr, + address parentRoundMgr + ) = parentCoFactory.createParentCorp( + bytes32(keccak256("Test2 ParentCo LLC")), + "Test ParentCo LLC", + "limited liability company", + "Delaware", + "test@parentco.example", + "binding arbitration", + corpPayable + ); + + // Escrow signature bytes for parent signing path (placeholder/test value). + bytes memory parentEscrowSig = hex"73f62ac9b08c813401a02a16a920a106e525ac65dff992dccfd2cb42e5423db6725bb1b4d6e0244a635665f4965514512253613e3b032491f7ec85c2f657154e1a"; + parentCoFactory.setParentCoSignatureHash(parentEscrowSig); + + CyberAgreementRegistry registry = CyberAgreementRegistry( + deployment.cyberAgreementRegistry + ); + + // Create (or no-op if already present) templates used by deployCorpContractFor. + bytes32 segCoTemplateId = keccak256("ParentCo.Test2.SegCo.v1"); + bytes32 boardConsentTemplateId = keccak256( + "ParentCo.Test2.BoardConsent.v1" + ); + + string[] memory globalFields = new string[](8); + globalFields[0] = "founderName"; + globalFields[1] = "enterpriseName"; + globalFields[2] = "companyName"; + globalFields[3] = "companyType"; + globalFields[4] = "companyJurisdiction"; + globalFields[5] = "companyContactDetails"; + globalFields[6] = "tokenSymbol"; + globalFields[7] = "tokenName"; + + string[] memory partyFields = new string[](2); + partyFields[0] = "name"; + partyFields[1] = "contactDetails"; + + try + registry.createTemplate( + segCoTemplateId, + "ParentCo Test SegCo Agreement", + "ipfs://parentco-test-segco-template", + globalFields, + partyFields + ) + {} catch {} + + try + registry.createTemplate( + boardConsentTemplateId, + "ParentCo Test Board Consent", + "ipfs://parentco-test-board-consent-template", + globalFields, + partyFields + ) + {} catch {} + + // Build SubCorp inputs matching ParentCoFactory's strict field checks. + uint256 subCorpSalt = uint256(keccak256("ParentCo.Test2.SubCorp.v1")); + string memory subCompanyName = "Test SubCo SPV 1"; + string memory subCompanyType = "series limited liability company"; + string memory subCompanyJurisdiction = "Delaware"; + string memory subCompanyContact = "subco@parentco.example"; + string memory subDisputeResolution = "binding arbitration"; + + string[] memory globalValues = new string[](8); + globalValues[0] = "Test Founder"; + globalValues[1] = "Test ParentCo Enterprise"; + globalValues[2] = subCompanyName; + globalValues[3] = subCompanyType; + globalValues[4] = subCompanyJurisdiction; + globalValues[5] = subCompanyContact; + globalValues[6] = "TSC1"; + globalValues[7] = "Test SubCo One"; + + string[] memory partyValues = new string[](2); + partyValues[0] = "Test Deployer Officer"; + partyValues[1] = "deployer@parentco.example"; + + CompanyOfficer memory subOfficer = CompanyOfficer({ + eoa: deployerAddress, + name: partyValues[0], + contact: partyValues[1], + title: "Founder" + }); + + // Pre-compute agreement id and signer signature expected by signContractFor. + address[] memory agreementParties = new address[](2); + agreementParties[0] = officerAddress; + agreementParties[1] = deployerAddress; + bytes32 agreementId = keccak256( + abi.encode(segCoTemplateId, subCorpSalt, globalValues, agreementParties) + ); + + ( + string memory legalContractUri, + , + string[] memory templateGlobalFields, + string[] memory templatePartyFields + ) = registry.getTemplateDetails(segCoTemplateId); + + bytes memory deployerSignature = CyberAgreementUtils.signAgreementTypedData( + vm, + registry.DOMAIN_SEPARATOR(), + registry.SIGNATUREDATA_TYPEHASH(), + agreementId, + legalContractUri, + templateGlobalFields, + templatePartyFields, + globalValues, + partyValues, + deployerPrivateKey + ); + + ( + address subCorp, + address subAuth, + address subIssuance, + address subDealMgr, + address subRoundMgr, + address[] memory subCertPrinters, + bytes32 subAgreementId, + uint256[] memory subCertIds + ) = parentCoFactory.deployCorpContractFor( + subCorpSalt, + subCompanyName, + subCompanyType, + subCompanyJurisdiction, + subCompanyContact, + subDisputeResolution, + corpPayable, + subOfficer, + segCoTemplateId, + boardConsentTemplateId, + globalValues, + partyValues, + deployerSignature, + deployerAddress + ); + + auth.updateRole(officerAddress, auth.OWNER_ROLE()); + auth.updateRole(corpPayable, auth.OWNER_ROLE()); + + console2.log("Auth:", address(auth)); + console2.log( + "CyberAgreementRegistry:", + deployment.cyberAgreementRegistry + ); + console2.log("IssuanceManagerFactory:", deployment.issuanceManagerFactory); + console2.log("CyberCorpSingleFactory:", deployment.cyberCorpSingleFactory); + console2.log("DealManagerFactory:", deployment.dealManagerFactory); + console2.log("RoundManagerFactory:", deployment.roundManagerFactory); + console2.log("CertificateUriBuilder:", deployment.uriBuilder); + console2.log("ParentCoFactory (proxy):", address(parentCoFactory)); + console2.log("ParentCorp:", parentCorp); + console2.log("ParentAuth:", parentAuth); + console2.log("ParentIssuance:", parentIssuance); + console2.log("ParentDealMgr:", parentDealMgr); + console2.log("ParentRoundMgr:", parentRoundMgr); + console2.log("SubCorp:", subCorp); + console2.log("SubAuth:", subAuth); + console2.log("SubIssuance:", subIssuance); + console2.log("SubDealMgr:", subDealMgr); + console2.log("SubRoundMgr:", subRoundMgr); + console2.log("SubAgreementId:"); + console2.logBytes32(subAgreementId); + console2.log("SubCertPrinters count:", subCertPrinters.length); + console2.log("SubCertIds count:", subCertIds.length); + + vm.stopBroadcast(); + } +} diff --git a/script/deploy-pump-factory-full-lifecycle.s.sol b/script/deploy-pump-factory-full-lifecycle.s.sol new file mode 100644 index 00000000..d80d423f --- /dev/null +++ b/script/deploy-pump-factory-full-lifecycle.s.sol @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {Vm} from "forge-std/Test.sol"; +import {DeployPumpCorpFactoryScript} from "./deploy-pump-factory.s.sol"; +import {PumpCorpFactory, PumpCorpFactoryLib} from "../src/PumpCorpFactory.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {EIP712Lib} from "../src/libs/EIP712Lib.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {DeploymentConstants} from "./libs/DeploymentConstants.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import {IssuanceManager} from "../src/IssuanceManager.sol"; +import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; +import {CyberScrip} from "../src/CyberScrip.sol"; +import {CyberAgreementUtils} from "../test/libs/CyberAgreementUtils.sol"; +import {CompanyOfficer, SecuritySeries, SecurityClass} from "../src/CyberCorpConstants.sol"; +import {RoundType} from "../src/libs/RoundLib.sol"; +import {CyberCertData, EOI, LexChexDetails, MintRequest} from "../src/storage/RoundManagerStorage.sol"; +import {MockERC20} from "../test/mock/MockERC20.sol"; + +contract DeployPumpCorpFactoryFullLifeCycleScript is Script { + + function run() public { + return + runWithArgs( + // Staging + DeploymentConstants.BASE, + "PumpCorp.V1.0.0.staging.dev2", + vm.envUint("PRIVATE_KEY_MAIN"), // deployerPrivateKey + vm.envUint("FOUNDER_KEY_MAIN"), // founderPrivateKey + vm.envUint("INVESTOR_KEY_MAIN"), // investorPrivateKey + 0x5ff4e90Efa2B88cf3cA92D63d244a78a88219Abf, // test corp payable + 0x5ff4e90Efa2B88cf3cA92D63d244a78a88219Abf // test officer EOA + ); + } + + function runWithArgs( + uint256 chainId, + string memory saltStr, + uint256 deployerPrivateKey, + uint256 founderPrivateKey, + uint256 investorPrivateKey, + address corpPayable, + address officerAddress + ) public { + // (1) Deploy factory contracts + + (PumpCorpFactory pumpCorpFactory, RoundManagerFactory rmFactory, , , ) = (new DeployPumpCorpFactoryScript()).runWithArgs( + chainId, + saltStr, + deployerPrivateKey + ); + + // Deploy the rests + + address deployerAddress = vm.addr(deployerPrivateKey); + address investorAddress = vm.addr(investorPrivateKey); + + bytes32 salt = bytes32(keccak256(bytes(saltStr))); + + DeploymentConstants.CoreDeployment memory deployment = DeploymentConstants + .coreV2(chainId); + + console2.log("==== Configs ===="); + console2.log("salt string: %s", saltStr); + console2.log("deployer: %s", deployerAddress); + console2.log("founder: %s", vm.addr(founderPrivateKey)); + console2.log("investor: %s", investorAddress); + console2.log("corpPayable: %s", corpPayable); + console2.log("officerAddress: %s", officerAddress); + console2.log( + "CyberAgreementRegistry:", + deployment.cyberAgreementRegistry + ); + console2.log("CyberCorpSingleFactory:", deployment.cyberCorpSingleFactory); + console2.log("DealManagerFactory:", deployment.dealManagerFactory); + console2.log("CertificateUriBuilder:", deployment.uriBuilder); + console2.log(""); + + CyberAgreementRegistry registry = CyberAgreementRegistry( + deployment.cyberAgreementRegistry + ); + + vm.startBroadcast(deployerPrivateKey); + + // (1) In staging, reassign the dev LeXcheX to pumpCorpFactory and grant it owner role + BorgAuth lexchexAuth = BorgAuth(0x7F55596De9D4224520EBCf5256d3d9d3708e0F71); + pumpCorpFactory.setLexchexAuth(address(lexchexAuth)); + lexchexAuth.updateRole(address(pumpCorpFactory), 99); + + // (2) Deploy test meme token + MockERC20 memeToken = new MockERC20("Test Token", "TEST", 9); + memeToken.mint(investorAddress, 10000e9); + + console2.log("==== Deployed ===="); + console2.log("Test MEME token:", address(memeToken)); + console2.log(""); + + // TODO WIP: remove before production + // (3) Create corp and round + + uint256 corpSaltUint = uint256(keccak256(bytes(string.concat(saltStr, "-corp")))); + bytes32 corpSalt = keccak256(abi.encodePacked(corpSaltUint)); + string memory companyName = "Test Corp"; + string memory companyType = "series limited liability company"; + string memory companyJurisdiction = "Delaware"; + string memory companyContact = "contact@test.com"; + string memory disputeResolution = "binding arbitration"; + + CompanyOfficer memory officer = CompanyOfficer({ + eoa: officerAddress, + name: "Test Founder", + contact: "test@company.com", + title: "Founder" + }); + + string[] memory roundLegalDetails = new string[](1); + roundLegalDetails[0] = "Legal Details"; + + bytes[] memory roundExtensionData = new bytes[](1); + roundExtensionData[0] = ""; + + CyberCertData[] memory roundCertData = new CyberCertData[](1); + string[] memory defaultLegend = new string[](1); + defaultLegend[0] = "Legend"; + roundCertData[0] = CyberCertData({ + name: "CyberCorp", + symbol: "CC", + uri: "ipfs://certificate", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesA, + extension: address(0), + defaultLegend: defaultLegend + }); + + string[] memory roundFirstPartyValues = new string[](5); + roundFirstPartyValues[0] = officer.name; // name + roundFirstPartyValues[1] = vm.toString(officer.eoa); // evmAddress (string form) + roundFirstPartyValues[2] = "email@founder.net"; // contactDetails + roundFirstPartyValues[3] = "Individual"; // investorType + roundFirstPartyValues[4] = "US"; // investorJurisdiction + + // Predict corp and round manager addresses to build a valid escrow signature + address predictedCorp = CyberCorpSingleFactory(deployment.cyberCorpSingleFactory).computeCyberCorpSingleAddress(corpSalt); + address predictedRM = RoundManagerFactory(rmFactory).computeRoundManagerAddress(corpSalt); + + // Define shared round parameters + SecuritySeries roundSeriesType = SecuritySeries.ACE; + uint256 roundRaiseCap = 100000000000; + uint256 roundMinTicket = 1; + uint256 roundMaxTicket = 10000000; + RoundType roundType = RoundType.FCFS; + uint256 roundStartTime = 1773785359; // Tue Mar 17 15:09:19 PDT 2026 + uint256 roundEndTime = type(uint256).max; // no round expiry + bytes32 roundTemplateId = bytes32(uint256(1)); // SAFE + address roundPaymentToken = address(memeToken); + uint256 roundPricePerUnit = 1000; + uint256 roundValuation = 1000000000000000; + + // TODO test: print SAFE template details +// { +// ( +// string memory legalContractUri, +// string memory title, +// string[] memory globalFields, +// string[] memory partyFields +// ) = registry.getTemplateDetails(roundTemplateId); +// console2.log("legalContractUri: %s", legalContractUri); +// console2.log("title: %s", title); +// for (uint256 i = 0; i < globalFields.length; i++) { +// console2.log("globalFields[%d]: %s", i, globalFields[i]); +// } +// for (uint256 i = 0; i < partyFields.length; i++) { +// console2.log("partyFields[%d]: %s", i, partyFields[i]); +// } +// } + + bytes memory metadataSig = _computeMetadataSignature( + address(pumpCorpFactory), + corpSaltUint, + corpPayable, + true, // publicRound + true, // allowTimedOffers + true, // restrictEndTimeReduction + officer, + companyName, + companyType, + companyJurisdiction, + companyContact, + disputeResolution, + roundExtensionData, + roundFirstPartyValues, + roundLegalDetails, + roundCertData, + new address[](0), // conditions + founderPrivateKey + ); + + bytes memory escrowedSig = _computeEscrowSignature( + predictedRM, + roundSeriesType, + roundRaiseCap, + roundMinTicket, + roundMaxTicket, + roundType, + roundStartTime, + roundEndTime, + roundTemplateId, + roundPaymentToken, + roundPricePerUnit, + roundValuation, + predictedCorp, + founderPrivateKey + ); + + console2.log("==== Signatures ===="); + console2.log("metadataSig:"); + console2.logBytes(metadataSig); + console2.log("escrowedSig:"); + console2.logBytes(escrowedSig); + console2.log(""); + + // Deploy another CyberCorp and create a public round using SAFE template id 1 + ( + address corp, + address corpAuth, + address issuance, + address dealManager, + address roundManager, + bytes32 roundId + ) = pumpCorpFactory.deployCyberCorpAndCreateRoundFor( + corpSaltUint, // salt + roundSeriesType, // seriesType + companyName, + companyType, + companyJurisdiction, + companyContact, + disputeResolution, + corpPayable, // _companyPayable + officer, // _officer + roundLegalDetails, // legalDetails + roundExtensionData, // extensionData + roundCertData, // certData + roundTemplateId, // roundTemplateId + roundPaymentToken, // paymentToken + roundPricePerUnit, // pricePerUnit + roundValuation, // valuation + roundFirstPartyValues, // roundPartyValues + escrowedSig, // escrowedSignature + metadataSig, // metadataSignature + roundType, // roundType + new address[](0), // conditions + roundRaiseCap, // raiseCap + roundMinTicket, // minTicket + roundMaxTicket, // maxTicket + roundStartTime, // startTime + roundEndTime, // endTime + true, // publicRound + true, // allowTimedOffers + true // restrictEndTimeReduction + ); + + vm.stopBroadcast(); + + console2.log("==== Corp & Round Created ===="); + console2.log("corp:", corp); + console2.log("roundManager:", roundManager); + console2.log("roundId:"); + console2.logBytes32(roundId); + console2.log(""); + + // (4) Submit EOI + + string memory investorName = "Investor 1"; + string memory investorContact = "email@investor.net"; + string memory investorType = "Individual"; + string memory investorJurisdiction = "US"; + + string[] memory roundGlobalValues = new string[](5); + roundGlobalValues[0] = "1.00"; // purchaseAmount + roundGlobalValues[1] = "10000000"; // postMoneyValuationCap + roundGlobalValues[2] = "1700000000"; // expirationTime (ts) + roundGlobalValues[3] = "DE"; // governingJurisdiction + roundGlobalValues[4] = "arbitration"; // disputeResolution + + string[] memory investorPartyValues = new string[](5); + investorPartyValues[0] = investorName; // name + investorPartyValues[1] = vm.toString(investorAddress); // evmAddress (string form) + investorPartyValues[2] = investorContact; // contactDetails + investorPartyValues[3] = investorType; // investorType + investorPartyValues[4] = investorJurisdiction; // investorJurisdiction + + vm.startBroadcast(investorPrivateKey); + + EOI memory eoi = EOI({ + name: investorName, + investorType: investorType, + jurisdiction: investorJurisdiction, + contact: investorContact, + minAmount: 1, + maxAmount: 1, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: LexChexDetails({ + request: MintRequest({ + uuid: 0, + owner: address(0), + investorName: "", + investorType: "", + investorJurisdiction: "", + investorContact: "", + mintPrice: 0, + expiry: 0, + paymentToken: address(0) + }), + templateId: bytes32(0), + salt: 0, + globalValues: new string[](0), + parties: new address[](0), + partyValues: new string[][](0), + agreementSignature: "" + }) + }); + + memeToken.approve(roundManager, type(uint256).max); + uint256 eoiSaltUint = uint256(keccak256(bytes(string.concat(saltStr, "-eoi")))); + (bytes32 agreementId, uint256 tokenId) = RoundManager(roundManager).submitEOI( + roundId, + eoi, + roundGlobalValues, + investorPartyValues, + _computeEOISignature( + vm, + CyberAgreementRegistry(registry), + roundTemplateId, + eoiSaltUint, + roundGlobalValues, + investorPartyValues, + officerAddress, + investorPrivateKey + ), + eoiSaltUint, + new address[](0), + bytes32(0) + ); + + vm.stopBroadcast(); + + console2.log("==== EOI Submitted ===="); + console2.log("tokenId:", tokenId); + console2.log("agreementId:"); + console2.logBytes32(agreementId); + console2.log(""); + } + + function _computeMetadataSignature( + address factory, + uint256 salt, + address companyPayable, + bool publicRound, + bool allowTimedOffers, + bool restrictEndTimeReduction, + CompanyOfficer memory officer, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + bytes[] memory extensionData, + string[] memory roundPartyValues, + string[] memory legalDetails, + CyberCertData[] memory certData, + address[] memory conditions, + uint256 signerPrivKey + ) internal view returns (bytes memory sig) { + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + bytes32 domainSep = keccak256(abi.encode( + PumpCorpFactoryLib.FACTORY_DOMAIN_TYPEHASH, + keccak256(bytes("PumpCorpFactory")), + keccak256(bytes("1")), + block.chainid, + factory + )); + bytes32 officerHash = keccak256(abi.encode( + PumpCorpFactoryLib.OFFICER_TYPEHASH, + officer.eoa, + keccak256(bytes(officer.name)), + keccak256(bytes(officer.contact)), + keccak256(bytes(officer.title)) + )); + bytes32 structHash = keccak256(abi.encode( + PumpCorpFactoryLib.ROUND_SUPPLEMENTAL_TYPEHASH, + corpSalt, + companyPayable, + publicRound, + allowTimedOffers, + restrictEndTimeReduction, + officerHash, + keccak256(bytes(companyName)), + keccak256(bytes(companyType)), + keccak256(bytes(companyJurisdiction)), + keccak256(bytes(companyContactDetails)), + keccak256(bytes(defaultDisputeResolution)), + PumpCorpFactoryLib.hashBytesArray(extensionData), + PumpCorpFactoryLib.hashStringArray(roundPartyValues), + PumpCorpFactoryLib.hashStringArray(legalDetails), + PumpCorpFactoryLib.hashCertDataArray(certData), + PumpCorpFactoryLib.hashAddresses(conditions) + )); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSep, structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivKey, digest); + sig = abi.encodePacked(r, s, v); + } + + function _computeEscrowSignature( + address roundManager, + SecuritySeries seriesType, + uint256 raiseCap, + uint256 minTicket, + uint256 maxTicket, + RoundType roundType, + uint256 startTime, + uint256 endTime, + bytes32 templateId, + address paymentToken, + uint256 pricePerUnit, + uint256 valuation, + address companyAddress, + uint256 signerPrivKey + ) internal view returns (bytes memory sig) { + bytes32 roundId = keccak256( + abi.encodePacked( + seriesType, + raiseCap, + minTicket, + maxTicket, + uint8(roundType), + startTime, + endTime, + templateId, + paymentToken, + pricePerUnit, + valuation, + companyAddress + ) + ); + + bytes32 domainSeparator = keccak256( + abi.encode( + EIP712Lib.EIP712_DOMAIN_TYPEHASH, + keccak256(bytes("RoundManager")), + keccak256(bytes("1")), + block.chainid, + roundManager + ) + ); + bytes32 structHash = keccak256( + abi.encode( + EIP712Lib.ESCROWEDSIGNATUREDATA_TYPEHASH, + roundId, + uint8(seriesType), + raiseCap, + minTicket, + maxTicket, + uint8(roundType), + startTime, + endTime, + templateId, + paymentToken, + pricePerUnit, + valuation, + companyAddress + ) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivKey, digest); + sig = abi.encodePacked(r, s, v); + } + + function _computeEOISignature( + Vm vm, + CyberAgreementRegistry registry, + bytes32 templateId, + uint256 salt, + string[] memory globalValues, + string[] memory partyValues, + address authorityOfficer, + uint256 signerPrivKey + ) internal view returns (bytes memory) { + ( + string memory legalUri, + , + string[] memory glFields, + string[] memory partyFields + ) = registry.getTemplateDetails(templateId); + address signer = vm.addr(signerPrivKey); + address[] memory parties = new address[](2); + parties[0] = authorityOfficer; + parties[1] = signer; + bytes32 contractId = keccak256( + abi.encode(templateId, salt, globalValues, parties) + ); + return CyberAgreementUtils.signAgreementTypedData( + vm, + registry.DOMAIN_SEPARATOR(), + registry.SIGNATUREDATA_TYPEHASH(), + contractId, + legalUri, + glFields, + partyFields, + globalValues, + partyValues, + signerPrivKey + ); + } +} diff --git a/script/deploy-pump-factory.s.sol b/script/deploy-pump-factory.s.sol new file mode 100644 index 00000000..5e6f8ae9 --- /dev/null +++ b/script/deploy-pump-factory.s.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {Vm} from "forge-std/Test.sol"; +import {PumpCorpFactory, PumpCorpFactoryLib} from "../src/PumpCorpFactory.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {EIP712Lib} from "../src/libs/EIP712Lib.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {DeploymentConstants} from "./libs/DeploymentConstants.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import {IssuanceManager} from "../src/IssuanceManager.sol"; +import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; +import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol"; +import {CertificateImageBuilderContract} from "../src/CertificateImageBuilderContract.sol"; +import {CyberScrip} from "../src/CyberScrip.sol"; +import {CyberAgreementUtils} from "../test/libs/CyberAgreementUtils.sol"; +import {CompanyOfficer, SecuritySeries, SecurityClass} from "../src/CyberCorpConstants.sol"; +import {RoundType} from "../src/libs/RoundLib.sol"; +import {CyberCertData, EOI, LexChexDetails, MintRequest} from "../src/storage/RoundManagerStorage.sol"; +import {MockERC20} from "../test/mock/MockERC20.sol"; + +contract DeployPumpCorpFactoryScript is Script { + + function run() public returns ( + PumpCorpFactory pumpCorpFactory, + RoundManagerFactory rmFactory, + IssuanceManagerFactory imFactory, + CertificateUriBuilder uriBuilder, + BorgAuth pumpAuth + ) { + return + runWithArgs( + // Production + DeploymentConstants.BASE, + "PumpCorp.V1.0.0", + vm.envUint("PRIVATE_KEY_MAIN") // deployerPrivateKey + +// // Staging +// DeploymentConstants.BASE, +// "PumpCorp.V1.0.0.staging", +// vm.envUint("PRIVATE_KEY_MAIN") // deployerPrivateKey + ); + } + + function runWithArgs( + uint256 chainId, + string memory saltStr, + uint256 deployerPrivateKey + ) public returns ( + PumpCorpFactory pumpCorpFactory, + RoundManagerFactory rmFactory, + IssuanceManagerFactory imFactory, + CertificateUriBuilder uriBuilder, + BorgAuth pumpAuth + ) { + address deployerAddress = vm.addr(deployerPrivateKey); + + bytes32 salt = bytes32(keccak256(bytes(saltStr))); + + DeploymentConstants.CoreDeployment memory deployment = DeploymentConstants + .coreV2(chainId); + + console2.log("==== Configs ===="); + console2.log("chainId: %d", chainId); + console2.log("salt string: %s", saltStr); + console2.log("deployer: %s", deployerAddress); + console2.log( + "CyberAgreementRegistry:", + deployment.cyberAgreementRegistry + ); + console2.log("CyberCorpSingleFactory:", deployment.cyberCorpSingleFactory); + console2.log("DealManagerFactory:", deployment.dealManagerFactory); + console2.log("RoundManagerFactory:", deployment.roundManagerFactory); + console2.log("CertificateUriBuilder:", deployment.uriBuilder); + console2.log(""); + + CyberAgreementRegistry registry = CyberAgreementRegistry( + deployment.cyberAgreementRegistry + ); + + vm.startBroadcast(deployerPrivateKey); + +// // (1) Deploy factory contracts +// +// pumpAuth = new BorgAuth{salt: salt}(deployerAddress); +// pumpAuth.updateRole(deployment.metalexSafe, 99); +// +// // TODO WIP: as of 2026/03/16 we haven't deployed the new RoundManagerFactory with restrictEndTimeReduction yet, +// // so we deploy a dev one here for now +// rmFactory = RoundManagerFactory(address( +// new ERC1967Proxy{salt: salt}( +// address(new RoundManagerFactory{salt: salt}()), +// abi.encodeWithSelector( +// RoundManagerFactory.initialize.selector, +// address(pumpAuth), +// address(new RoundManager()) +// ) +// ) +// )); +// +// // TODO WIP: as of 2026/03/16 the on-chain IssuanceManager and CyberCertPrinter lack addOfficerSignature/addIssuerSignature, +// // so we deploy a new factory pointing to locally compiled implementations +// imFactory = IssuanceManagerFactory(address( +// new ERC1967Proxy{salt: salt}( +// address(new IssuanceManagerFactory{salt: salt}()), +// abi.encodeWithSelector( +// IssuanceManagerFactory.initialize.selector, +// address(pumpAuth), +// address(new IssuanceManager()), +// address(new CyberCertPrinter()), +// address(new CyberScrip()) +// ) +// ) +// )); +// +// // TODO WIP: as of 2026/03/16 the on-chain CertificateUriBuilder lack ACE securitySeries +// // so we deploy a new one pointing to locally compiled implementations +// uriBuilder = CertificateUriBuilder(address( +// new ERC1967Proxy{salt: salt}( +// address(new CertificateUriBuilder{salt: salt}()), +// abi.encodeWithSelector( +// CertificateUriBuilder.initialize.selector, +// address(pumpAuth) +// ) +// ) +// )); +// uriBuilder.setImageBuilder(address(new CertificateImageBuilderContract())); +// +// console2.log("==== Deployed (for dev purposes) ===="); +// console2.log("PumpAuth:", address(pumpAuth)); +// console2.log("IssuanceManagerFactory:", address(imFactory)); +// console2.log("RoundManagerFactory:", address(rmFactory)); +// console2.log("CertificateUriBuilder:", address(uriBuilder)); +// console2.log(""); + + // In production, the IssuanceManagerFactory and RoundManagerFactory should be upgraded by now so we will just use it + rmFactory = RoundManagerFactory(deployment.roundManagerFactory); + imFactory = IssuanceManagerFactory(deployment.issuanceManagerFactory); + uriBuilder = CertificateUriBuilder(deployment.uriBuilder); + pumpAuth = BorgAuth(deployment.auth); + + pumpCorpFactory = PumpCorpFactory( + address( + new ERC1967Proxy{salt: salt}( + address(new PumpCorpFactory{salt: salt}()), + abi.encodeWithSelector( + PumpCorpFactory.initialize.selector, + pumpAuth, + deployment.cyberAgreementRegistry, + address(imFactory), + deployment.cyberCorpSingleFactory, + deployment.dealManagerFactory, + rmFactory, + uriBuilder + ) + ) + ) + ); + + vm.stopBroadcast(); + + console2.log("==== Deployed ===="); + console2.log("PumpCorpFactory (proxy):", address(pumpCorpFactory)); + console2.log(""); + } +} diff --git a/script/deploy-round-manager-factory-implementation.s.sol b/script/deploy-round-manager-factory-implementation.s.sol new file mode 100644 index 00000000..157eb086 --- /dev/null +++ b/script/deploy-round-manager-factory-implementation.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; + +contract BaseScript is Script { + string internal constant DEFAULT_SALT_STRING = + "MetaLexCyberCorp.RoundManagerFactory.Implementation.V2"; + + function run() public returns (RoundManagerFactory implementation) { + return + runWithArgs( + DEFAULT_SALT_STRING, + vm.envUint("PRIVATE_KEY_MAIN") + ); + } + + function runWithArgs( + string memory saltStr, + uint256 deployerPrivateKey + ) public returns (RoundManagerFactory implementation) { + address deployer = vm.addr(deployerPrivateKey); + bytes32 salt = keccak256(bytes(saltStr)); + + console2.log("==== Configs ===="); + console2.log("chainId:", block.chainid); + console2.log("deployer:", deployer); + console2.log("salt string:", saltStr); + console2.logBytes32(salt); + console2.log(""); + + vm.startBroadcast(deployerPrivateKey); + implementation = new RoundManagerFactory{salt: salt}(); + vm.stopBroadcast(); + + console2.log("==== Deployed ===="); + console2.log( + "RoundManagerFactory implementation:", + address(implementation) + ); + console2.log(""); + } +} diff --git a/script/deploy-umia.factory.sol b/script/deploy-umia.factory.sol new file mode 100644 index 00000000..2b243285 --- /dev/null +++ b/script/deploy-umia.factory.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; + +import {CyberAgreementUtils} from "../test/libs/CyberAgreementUtils.sol"; +import {MetaDAOFactory} from "../src/MetaDAOFactory.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import {IssuanceManager} from "../src/IssuanceManager.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {CyberCorp} from "../src/CyberCorp.sol"; +import {DealManagerFactory, DealManager} from "../src/DealManagerFactory.sol"; +import {RoundManagerFactory, RoundManager} from "../src/RoundManagerFactory.sol"; +import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol"; +import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; +import {CyberScrip} from "../src/CyberScrip.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {CompanyOfficer} from "../src/CyberCorpConstants.sol"; + +contract DeployScript is Script { + // Hard-coded since we don't have programmatic access to CyberAgreementRegistry's underlying types + string constant DOMAIN_SEPARATOR_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; + string constant ESCROW_SIGNATUREDATA_TYPE = "EscrowSignatureData(string legalContractUri,string[] partyFields,string[] partyValues)"; + + struct DomainSeparator { + string name; + string version; + uint256 chainId; + address verifyingContract; + } + + struct EscrowSignatureData { + string legalContractUri; + string[] partyFields; + string[] partyValues; + } + + function run() public returns ( + CyberAgreementRegistry registry, + MetaDAOFactory metaDAOFactory + ) { + return runWithArgs( + vm.envUint("PRIVATE_KEY_MAIN"), // deployerPrivateKey + 0x59026c9A3871505c8E5fb0B021e274a0B28547F6, // corpPayable + 0x76A6168B69f8f1b27E06dC77a30F2D1C92733e7A, // officerAddress + hex"73f62ac9b08c813401a02a16a920a106e525ac65dff992dccfd2cb42e5423db6725bb1b4d6e0244a635665f4965514512253613e3b032491f7ec85c2f657154e1a" // metadaoEscrowSig + ); + } + + function runWithArgs( + uint256 deployerPrivateKey, + address corpPayable, + address officerAddress, + bytes memory metadaoEscrowSig + ) public returns (CyberAgreementRegistry registry, MetaDAOFactory metaDAOFactory) { + // Other configs + string memory metaDAOOfficerName = "Test Umia Officer"; + string memory metaDAOOfficerContact = "Test Contact"; + string memory metaDAOOfficerTitle = "Director & Management Shareholder"; + + address deployerAddress = vm.addr(deployerPrivateKey); + vm.startBroadcast(deployerPrivateKey); + + bytes32 salt = bytes32(keccak256("UmiaFactory.deploy.v1")); + + address stable = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; // USDC @ Base + + BorgAuth auth = new BorgAuth{salt: salt}(deployerAddress); + + address registry = 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134; + // Create templates + + string[] memory globalFields = new string[](8); + globalFields[0] = "founderName"; + globalFields[1] = "enterpriseName"; + globalFields[2] = "companyName"; + globalFields[3] = "companyType"; + globalFields[4] = "companyJurisdiction"; + globalFields[5] = "companyContactDetails"; + globalFields[6] = "tokenSymbol"; + globalFields[7] = "tokenName"; + string[] memory partyFields = new string[](2); + partyFields[0] = "name"; + partyFields[1] = "contactDetails"; + + // Create template for SegCo + string memory segCoAgreementTitle = "Test Umia Futarchy Governance SPC - SegCo combined v 1.0"; + string memory segCoAgreementUri = "ipfs://bafybeifpvfwxfmobk7nhflsczqiynp3ca5urvyk3duh7s3rwptcnfzhuje"; + + // Create template for Board Consent + string memory boardConsentTitle = "Test Futarchy Governance SPC - Board Consent - Approval of SegCo v 1.0"; + string memory boardConsentUri = "ipfs://bafkreic7dscoigvwjc23vzvkmzophm34kpafu6nrctykq5bif63lqvpuoa"; + + + address uriBuilder = 0x5500c095ea7dE6F8a5E15949e24B80604cc670A3; + + address issuanceManagerFactory = 0xA32547aAdAA4975082D729c79e79dBaE4385EBCf; + + address cyberCorpSingleFactory = 0xc8e084D3f8B3b326FCc894C7afD28F4904196406; + + address dealManagerFactory = 0x975df8A99C895d04ae158F8C91Ba562Fce3ECDA3; + + // upgrade CyberAgreementRegistry + address newAgreementRegistryImplementation = address(new CyberAgreementRegistry{salt: salt}()); + CyberAgreementRegistry(registry).upgradeToAndCall(newAgreementRegistryImplementation, ""); + + MetaDAOFactory metaDAOFactory = MetaDAOFactory( + address( + new ERC1967Proxy{salt: salt}( + address(new MetaDAOFactory{salt: salt}()), + abi.encodeWithSelector( + MetaDAOFactory.initialize.selector, + address(auth), + address(registry), + address(issuanceManagerFactory), + address(cyberCorpSingleFactory), + address(dealManagerFactory), + address(0), + address(uriBuilder), + address(stable) + ) + ) + ) + ); + + // Configure MetaDAO officer and escrowed signature BEFORE revoking deployer ownership + metaDAOFactory.setMetaDAOOfficerEOA(officerAddress); + metaDAOFactory.setMetaDAOOfficerName(metaDAOOfficerName); + metaDAOFactory.setMetaDAOOfficerContact(metaDAOOfficerContact); + metaDAOFactory.setMetaDAOOfficerTitle(metaDAOOfficerTitle); + + // Create the parent corp (one-time). Reverts if called again. + (address parentCorp, + address parentAuth, + address parentIssuance, + address parentDealMgr, + address parentRoundMgr) = metaDAOFactory.createParentCorp( + bytes32(keccak256("Futarchy Governance SPC")), + "Futarchy Governance SPC", + "segregated portfolio company", + "Cayman Islands", + "market.governed.civilization@metadao.fi", + "binding arbitration", + corpPayable + ); + + // Assign roles and revoke EOA ownership (after setup) + auth.updateRole(address(officerAddress), auth.OWNER_ROLE()); + auth.updateRole(address(corpPayable), auth.OWNER_ROLE()); + + + console.log("Auth:", address(auth)); + console.log("CyberAgreementRegistry:", address(registry)); + console.log("CertificateUriBuilder:", address(uriBuilder)); + console.log("IssuanceManagerFactory:", address(issuanceManagerFactory)); + console.log("CyberCorpSingleFactory:", address(cyberCorpSingleFactory)); + console.log("DealManagerFactory:", address(dealManagerFactory)); + // console.log("RoundManagerFactory:", address(roundManagerFactory)); + //console.log("CyberCertPrinter Impl:", address(cyberCertPrinterImplementation)); + // console.log("CyberScrip Impl:", address(cyberCert20Implementation)); + console.log("MetaDAOFactory (proxy):", address(metaDAOFactory)); + console.log("ParentCorp:", parentCorp); + console.log("ParentAuth:", parentAuth); + console.log("ParentIssuance:", parentIssuance); + console.log("ParentDealMgr:", parentDealMgr); + console.log("NewAgreementRegistryImplementation:", address(newAgreementRegistryImplementation)); + //console.log("ParentRoundMgr:", parentRoundMgr); + + vm.stopBroadcast(); + + return (CyberAgreementRegistry(registry), metaDAOFactory); + } + + function _formatEscrowAgreementTypedDataJson( + CyberAgreementRegistry registry, + string memory contractUri, + string[] memory partyFields, + string[] memory partyValues + ) internal returns (string memory) { + string memory domainSeparatorJson = vm.serializeJsonType( + DOMAIN_SEPARATOR_TYPE, + abi.encode(DomainSeparator({ + name: registry.name(), + version: registry.version(), + chainId: block.chainid, + verifyingContract: address(registry) + })) + ); + + string memory escrowSignatureDataJson = vm.serializeJsonType( + ESCROW_SIGNATUREDATA_TYPE, + abi.encode(EscrowSignatureData({ + legalContractUri: contractUri, + partyFields: partyFields, + partyValues: partyValues + })) + ); + + // Build the json string with the temporary buffer at key "outputKey" + vm.serializeString("outputKey", "domain", domainSeparatorJson); + vm.serializeString("outputKey", "message", escrowSignatureDataJson); + vm.serializeString("outputKey", "primaryType", "EscrowSignatureData"); + return vm.serializeString("outputKey", "types", "{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"EscrowSignatureData\":[{\"name\":\"legalContractUri\",\"type\":\"string\"},{\"name\":\"partyFields\",\"type\":\"string[]\"},{\"name\":\"partyValues\",\"type\":\"string[]\"}]}"); + } +} \ No newline at end of file diff --git a/script/lex-chex-perms.s.sol b/script/lex-chex-perms.s.sol index 271a34c0..ed6640cd 100644 --- a/script/lex-chex-perms.s.sol +++ b/script/lex-chex-perms.s.sol @@ -34,10 +34,10 @@ contract BaseScript is Script { address multisig = 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C; uint256 currentChainId = block.chainid; address stable; - address minter = 0xb8fA82FE034DCa53de30Ba6BC9B571c406C2AFA4; + address minter = 0x5ff4e90Efa2B88cf3cA92D63d244a78a88219Abf; BorgAuth lexchexAuth = BorgAuth(0xeAdeaD5C4A6747D4959489742c143bCDb95a01c2); - lexchexAuth.updateRole(address(minter), lexchexAuth.ADMIN_ROLE()); + lexchexAuth.updateRole(address(minter), lexchexAuth.OWNER_ROLE()); //CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(25)), "MetaLeX cyberSAFT reg D v.1.0", "ipfs://bafybeif6fqgexescp4g2hbb6fjkk3ifrqpopc2lv2oue5tiq6h3t2pmgc4", globalFieldsSafT, partyFieldsSaft); diff --git a/script/libs/DeploymentConstants.sol b/script/libs/DeploymentConstants.sol new file mode 100644 index 00000000..2b510504 --- /dev/null +++ b/script/libs/DeploymentConstants.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +library DeploymentConstants { + error UnsupportedChain(uint256 chainId); + + uint256 internal constant BASE = 8453; + + uint256 internal constant ETH_SEPOLIA = 11155111; + uint256 internal constant BASE_SEPOLIA = 84532; + + struct CoreDeployment { + address metalexSafe; + address auth; + address cyberCorpFactory; + address issuanceManagerFactory; + address cyberCorpSingleFactory; + address dealManagerFactory; + address roundManagerFactory; + address cyberAgreementRegistry; + address uriBuilder; + address lexchexAuth; + address lexchex; + address lexchexMinter; + address lexchexCondition; + } + + /// @notice Latest CyberCorps V2 deployment constants. + /// @dev Source: script/res/deployment-addresses.md + function coreV2(uint256 chainId) + internal + pure + returns (CoreDeployment memory deployment) + { + if (chainId == BASE_SEPOLIA) { + return + CoreDeployment({ + metalexSafe: 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C, + auth: 0x033012a1eDA6e2E00D12CD37c5b63B9440ef5E01, + cyberCorpFactory: 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2, + issuanceManagerFactory: 0xbbD386D237f3b407E6511A52488850b1Da0cCad2, + cyberCorpSingleFactory: 0xBE0D3D13AA07501beAC9b72dE9e9292E66C7A5C4, + dealManagerFactory: 0x3982b078f2ac306219c9540Ebc908360a960C251, + roundManagerFactory: 0x9E2A3a07711Ce4b5A2F4D62a5c8f8B5307Af9C34, + cyberAgreementRegistry: 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134, + uriBuilder: 0x5500c095ea7dE6F8a5E15949e24B80604cc670A3, + lexchexAuth: 0xeAdeaD5C4A6747D4959489742c143bCDb95a01c2, + lexchex: 0xc8db0c3f47656aee725b0AD1835F9A3FbD0a0b62, + lexchexMinter: 0x0dD1a2a89eC172ac322B6a7a6c869180CBD0F960, + lexchexCondition: 0x4a08547d57C8d01e59bA8F884aB90CEe0d6d5b42 + }); + } + else { + return + CoreDeployment({ + metalexSafe: 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C, + auth: 0x033012a1eDA6e2E00D12CD37c5b63B9440ef5E01, + cyberCorpFactory: 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2, + issuanceManagerFactory: 0xD353972D7955F421d94d0eA8c42c88c417F7155A, + cyberCorpSingleFactory: 0xBE0D3D13AA07501beAC9b72dE9e9292E66C7A5C4, + dealManagerFactory: 0x3982b078f2ac306219c9540Ebc908360a960C251, + roundManagerFactory: 0xc9d5d0DeDD124f9351E5880469f25AB41869aeb9, + cyberAgreementRegistry: 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134, + uriBuilder: 0x5500c095ea7dE6F8a5E15949e24B80604cc670A3, + lexchexAuth: 0xeAdeaD5C4A6747D4959489742c143bCDb95a01c2, + lexchex: 0xc8db0c3f47656aee725b0AD1835F9A3FbD0a0b62, + lexchexMinter: 0x0dD1a2a89eC172ac322B6a7a6c869180CBD0F960, + lexchexCondition: 0x4a08547d57C8d01e59bA8F884aB90CEe0d6d5b42 + }); + } + } +} diff --git a/script/public-round-test-deploy.s.sol b/script/public-round-test-deploy.s.sol index 5aad936d..358653dc 100644 --- a/script/public-round-test-deploy.s.sol +++ b/script/public-round-test-deploy.s.sol @@ -294,7 +294,8 @@ contract PublicRoundTestDeploy is Script { block.timestamp - 1, block.timestamp + 21 days, true, - true + true, + false ); diff --git a/script/res/deployment-addresses.md b/script/res/deployment-addresses.md new file mode 100644 index 00000000..399682f5 --- /dev/null +++ b/script/res/deployment-addresses.md @@ -0,0 +1,72 @@ +== Logs == +auth: 0x033012a1eDA6e2E00D12CD37c5b63B9440ef5E01 +issuanceManagerFactory: 0xA32547aAdAA4975082D729c79e79dBaE4385EBCf +cyberCorpSingleFactory: 0xc8e084D3f8B3b326FCc894C7afD28F4904196406 +dealManagerFactory: 0x975df8A99C895d04ae158F8C91Ba562Fce3ECDA3 +uriBuilder: 0x5500c095ea7dE6F8a5E15949e24B80604cc670A3 +cyberCertPrinterImplementation: 0x016C3a68C3a82179B4e63871c7730aaA272c9638 +CyberAgreementRegistry: 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134 +CyberCorpFactory: 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2 +tokenWarrantExtension: 0xbad0b411C37cfF66e4C0B7764Db2d499eA757bb4 + +== Logs == + LexchexAuth: 0xeAdeaD5C4A6747D4959489742c143bCDb95a01c2 + Lexchex: 0xc8db0c3f47656aee725b0AD1835F9A3FbD0a0b62 + LexchexMinter: 0x0dD1a2a89eC172ac322B6a7a6c869180CBD0F960 + LexchexCondition: 0x4a08547d57C8d01e59bA8F884aB90CEe0d6d5b42 + +## CyberCorps V2 (Base Sepolia) + +== Logs == +Test Deployer: 0x1A762EfF397a3C519da3dF9FCDDdca7D1BD43B5e +Deployer: 0x341Da9fb8F9bD9a775f6bD641091b24Dd9aA459B +Upgrader role: 99 +RoundManagerFactory deployed: 0x9E2A3a07711Ce4b5A2F4D62a5c8f8B5307Af9C34 +New CyberCorpFactory implementation: 0x4c75E3E46C211f890BE248A8f20615579b79D2e1 +CyberCorpFactory upgraded (proxy via upgradeToAndCall): 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2 +CyberCorpFactory.roundManagerFactory set to: 0x9E2A3a07711Ce4b5A2F4D62a5c8f8B5307Af9C34 +New CyberCorp implementation: 0x2a7CcABAa3c14e7F666585f1c10ee47859cA2e75 +CyberCorpSingleFactory deployed: 0xBE0D3D13AA07501beAC9b72dE9e9292E66C7A5C4 +CyberCorpFactory.cyberCorpSingleFactory set to: 0xBE0D3D13AA07501beAC9b72dE9e9292E66C7A5C4 +New IssuanceManager implementation: 0x93ef1D261c3A7E22427Bb706cBEdB25eE76446cc +New CyberCertPrinter implementation: 0x4E41811f6B5765Fa3b2F0821665d80A98a4A17C0 +New CyberScrip implementation: 0x50A8E54717D34AC4Da4f84f34Fc892A6d190143e +IssuanceManagerFactory deployed: 0xbbD386D237f3b407E6511A52488850b1Da0cCad2 +CyberCorpFactory.issuanceManagerFactory set to: 0xbbD386D237f3b407E6511A52488850b1Da0cCad2 +New DealManager implementation: 0x33318de307E76DAF8e04338890164618d6CF70eb +DealManagerFactory deployed: 0x3982b078f2ac306219c9540Ebc908360a960C251 +CyberCorpFactory.dealManagerFactory set to: 0x3982b078f2ac306219c9540Ebc908360a960C251 +CyberCorp beacon implementation set to: 0x2a7CcABAa3c14e7F666585f1c10ee47859cA2e75 +New CyberAgreementRegistry implementation: 0xcD8d40F2f0713561DA8BEd46D34E6705283BDcA6 +CyberAgreementRegistry upgraded (proxy via upgradeToAndCall): 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134 +IssuanceManager beacon implementation set to: 0x93ef1D261c3A7E22427Bb706cBEdB25eE76446cc +CyberCorpFactory: 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2 +RoundManagerFactory: 0x9E2A3a07711Ce4b5A2F4D62a5c8f8B5307Af9C34 + +## CyberCorps V2 (Eth Sepolia) + +== Logs == +Test Deployer: 0x1A762EfF397a3C519da3dF9FCDDdca7D1BD43B5e +Deployer: 0x341Da9fb8F9bD9a775f6bD641091b24Dd9aA459B +Upgrader role: 99 +RoundManagerFactory deployed: 0xc9d5d0DeDD124f9351E5880469f25AB41869aeb9 +New CyberCorpFactory implementation: 0x424ab1B1DA8b7B2FE13cA0A7ABC346b22efa9191 +CyberCorpFactory upgraded (proxy via upgradeToAndCall): 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2 +CyberCorpFactory.roundManagerFactory set to: 0xc9d5d0DeDD124f9351E5880469f25AB41869aeb9 +New CyberCorp implementation: 0x2a7CcABAa3c14e7F666585f1c10ee47859cA2e75 +CyberCorpSingleFactory deployed: 0xBE0D3D13AA07501beAC9b72dE9e9292E66C7A5C4 +CyberCorpFactory.cyberCorpSingleFactory set to: 0xBE0D3D13AA07501beAC9b72dE9e9292E66C7A5C4 +New IssuanceManager implementation: 0xdF7994f7A12482c473c3BbF51D9a0fF4572F4F30 +New CyberCertPrinter implementation: 0x4E41811f6B5765Fa3b2F0821665d80A98a4A17C0 +New CyberScrip implementation: 0x50A8E54717D34AC4Da4f84f34Fc892A6d190143e +IssuanceManagerFactory deployed: 0xD353972D7955F421d94d0eA8c42c88c417F7155A +CyberCorpFactory.issuanceManagerFactory set to: 0xD353972D7955F421d94d0eA8c42c88c417F7155A +New DealManager implementation: 0x33318de307E76DAF8e04338890164618d6CF70eb +DealManagerFactory deployed: 0x3982b078f2ac306219c9540Ebc908360a960C251 +CyberCorpFactory.dealManagerFactory set to: 0x3982b078f2ac306219c9540Ebc908360a960C251 +CyberCorp beacon implementation set to: 0x2a7CcABAa3c14e7F666585f1c10ee47859cA2e75 +New CyberAgreementRegistry implementation: 0xcD8d40F2f0713561DA8BEd46D34E6705283BDcA6 +CyberAgreementRegistry upgraded (proxy via upgradeToAndCall): 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134 +IssuanceManager beacon implementation set to: 0xdF7994f7A12482c473c3BbF51D9a0fF4572F4F30 +CyberCorpFactory: 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2 +RoundManagerFactory: 0xc9d5d0DeDD124f9351E5880469f25AB41869aeb9 \ No newline at end of file diff --git a/script/res/gnosis-batch-add-blackhaven-safe-t-template.json b/script/res/gnosis-batch-add-blackhaven-safe-t-template.json new file mode 100644 index 00000000..13a67053 --- /dev/null +++ b/script/res/gnosis-batch-add-blackhaven-safe-t-template.json @@ -0,0 +1,58 @@ +{ + "version": "1.0", + "chainId": "8453", + "createdAt": 1763075700311, + "meta": { + "name": "Transactions Batch", + "description": "Add blackhaven_safe_t template", + "txBuilderVersion": "1.18.2", + "createdFromSafeAddress": "0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C", + "createdFromOwnerAddress": "", + "checksum": "" + }, + "transactions": [ + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x626c61636b686176656e5f736166655f74000000000000000000000000000000", + "title": "blackhaven_safe_t", + "legalContractUri": "IPFS://bafybeiamepjce7tn4oigr3q6zdph2glgali3jp2xe25pivifsx3rha55gi", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"exercisePriceMethod\", \"exercisePrice\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"latestExpirationTime\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + } + ] +} diff --git a/script/res/gnosis-batch-upgrade-spa-plus-templates-v1-3-eth.json b/script/res/gnosis-batch-upgrade-spa-plus-templates-v1-3-eth.json new file mode 100644 index 00000000..fc8c76bb --- /dev/null +++ b/script/res/gnosis-batch-upgrade-spa-plus-templates-v1-3-eth.json @@ -0,0 +1,359 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1763075700311, + "meta": { + "name": "Transactions Batch", + "description": "Upgrade SPA+ templates v1.3", + "txBuilderVersion": "1.18.2", + "createdFromSafeAddress": "0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C", + "createdFromOwnerAddress": "", + "checksum": "" + }, + "transactions": [ + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f7265675f645f76315f3300000000000000000000000000", + "title": "mlx_safe_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeih7l2kxncjuwrfgv5gnmpcik43dnn4pxpe4it4u7ti2hgfgrlot2a", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f7265675f735f76315f3300000000000000000000000000", + "title": "mlx_safe_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeieh7jn553jmrjmwee3dsvwf5hkedomey2vhubc3mumlewfpumvlae", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f74775f7265675f645f76315f3300000000000000000000", + "title": "mlx_safe_tw_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeiaw3pwov3ahg4bk2hte2hu4pwv34nndoguxyk3umq6f5su3kod6ay", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"exercisePriceMethod\", \"exercisePrice\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"latestExpirationTime\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f74775f7265675f735f76315f3300000000000000000000", + "title": "mlx_safe_tw_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeicto2raupsj5ad7snxvhmmll2plwyploqho4fg2cibnn2fuhlm2d4", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"exercisePriceMethod\", \"exercisePrice\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"latestExpirationTime\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f73616674655f7265675f645f76315f33000000000000000000000000", + "title": "mlx_safte_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeiag7xatsusb24evnpyj6ztf62kix36dgbsp3kbazfyvr273ph56ay", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"protocolUSDValuationAtTimeofInvestment\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f73616674655f7265675f735f76315f33000000000000000000000000", + "title": "mlx_safte_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeia43r7e566s2jlq4gtaasmtybutujy7fuizhw3fycxtwnstfbkeia", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"protocolUSDValuationAtTimeofInvestment\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166745f7265675f645f76315f3300000000000000000000000000", + "title": "mlx_saft_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeieoljri2rwuv35rymjd654sr3u46kbcao7mymseqobfo7x6lxgdcy", + "globalFields": "[\"purchaseAmount\", \"protocolValuationCap\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166745f7265675f735f76315f3300000000000000000000000000", + "title": "mlx_saft_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeibwrz3rttteguo5ccoh5x7ndwdu6hyhy7i3iraii5c5ml4pfv73t4", + "globalFields": "[\"purchaseAmount\", \"protocolValuationCap\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + } + ] +} diff --git a/script/res/gnosis-batch-upgrade-spa-plus-templates-v1-3.json b/script/res/gnosis-batch-upgrade-spa-plus-templates-v1-3.json new file mode 100644 index 00000000..3bd2b484 --- /dev/null +++ b/script/res/gnosis-batch-upgrade-spa-plus-templates-v1-3.json @@ -0,0 +1,359 @@ +{ + "version": "1.0", + "chainId": "8453", + "createdAt": 1763075700311, + "meta": { + "name": "Transactions Batch", + "description": "Upgrade SPA+ templates v1.3", + "txBuilderVersion": "1.18.2", + "createdFromSafeAddress": "0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C", + "createdFromOwnerAddress": "", + "checksum": "" + }, + "transactions": [ + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f7265675f645f76315f3300000000000000000000000000", + "title": "mlx_safe_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeih7l2kxncjuwrfgv5gnmpcik43dnn4pxpe4it4u7ti2hgfgrlot2a", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f7265675f735f76315f3300000000000000000000000000", + "title": "mlx_safe_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeieh7jn553jmrjmwee3dsvwf5hkedomey2vhubc3mumlewfpumvlae", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f74775f7265675f645f76315f3300000000000000000000", + "title": "mlx_safe_tw_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeiaw3pwov3ahg4bk2hte2hu4pwv34nndoguxyk3umq6f5su3kod6ay", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"exercisePriceMethod\", \"exercisePrice\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"latestExpirationTime\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166655f74775f7265675f735f76315f3300000000000000000000", + "title": "mlx_safe_tw_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeicto2raupsj5ad7snxvhmmll2plwyploqho4fg2cibnn2fuhlm2d4", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"exercisePriceMethod\", \"exercisePrice\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"latestExpirationTime\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f73616674655f7265675f645f76315f33000000000000000000000000", + "title": "mlx_safte_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeiag7xatsusb24evnpyj6ztf62kix36dgbsp3kbazfyvr273ph56ay", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"protocolUSDValuationAtTimeofInvestment\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f73616674655f7265675f735f76315f33000000000000000000000000", + "title": "mlx_safte_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeia43r7e566s2jlq4gtaasmtybutujy7fuizhw3fycxtwnstfbkeia", + "globalFields": "[\"purchaseAmount\", \"postMoneyValuationCap\", \"protocolUSDValuationAtTimeofInvestment\", \"expirationTime\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\", \"tokenCalculationMethod\", \"minCompanyReserve\", \"tokenPremiumMultiplier\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166745f7265675f645f76315f3300000000000000000000000000", + "title": "mlx_saft_reg_d_v1_3", + "legalContractUri": "IPFS://bafybeieoljri2rwuv35rymjd654sr3u46kbcao7mymseqobfo7x6lxgdcy", + "globalFields": "[\"purchaseAmount\", \"protocolValuationCap\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + }, + { + "to": "0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "internalType": "bytes32", + "name": "templateId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "title", + "type": "string" + }, + { + "internalType": "string", + "name": "legalContractUri", + "type": "string" + }, + { + "internalType": "string[]", + "name": "globalFields", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "partyFields", + "type": "string[]" + } + ], + "name": "createTemplate", + "payable": false + }, + "contractInputsValues": { + "templateId": "0x6d6c785f736166745f7265675f735f76315f3300000000000000000000000000", + "title": "mlx_saft_reg_s_v1_3", + "legalContractUri": "IPFS://bafybeibwrz3rttteguo5ccoh5x7ndwdu6hyhy7i3iraii5c5ml4pfv73t4", + "globalFields": "[\"purchaseAmount\", \"protocolValuationCap\", \"governingJurisdiction\", \"disputeResolution\", \"unlockStartTimeType\", \"unlockStartTime\", \"unlockingPeriod\", \"unlockingCliffPeriod\", \"unlockingCliffPercentage\", \"unlockingIntervalType\"]", + "partyFields": "[\"name\", \"evmAddress\", \"contactDetails\", \"investorType\", \"investorJurisdiction\"]" + } + } + ] +} diff --git a/script/templatev2.s.sol b/script/templatev2.s.sol index 30f963c7..e77ce08e 100644 --- a/script/templatev2.s.sol +++ b/script/templatev2.s.sol @@ -44,8 +44,8 @@ contract BaseScript is Script { partyFields[3] = "investorType"; partyFields[4] = "investorJurisdiction"; - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(50)), "cySPA + Reg D SAFE", "IPFS://bafybeics3btqftkfnzchtisazgvlvtq3xok6rrdvhjyhdvr7lhoa6snjxe", globalFieldsSafe, partyFields); - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(51)), "cySPA + REG S SAFE", "IPFS://bafybeidwqou5x4amvsidepwbuqpwarowv3vce473jpqcgejvbf4g2xxdee", globalFieldsSafe, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_safe_reg_d_v1_3")), "metalex cybersafe jx-neutral reg d raise v 1.3", "IPFS://bafybeih7l2kxncjuwrfgv5gnmpcik43dnn4pxpe4it4u7ti2hgfgrlot2a", globalFieldsSafe, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_safe_reg_s_v1_3")), "metalex cybersafe jx-neutral reg s raise v 1.3", "IPFS://bafybeieh7jn553jmrjmwee3dsvwf5hkedomey2vhubc3mumlewfpumvlae", globalFieldsSafe, partyFields); string[] memory globalFieldsSafeTokenWarrant = new string[](17); globalFieldsSafeTokenWarrant[0] = "purchaseAmount"; @@ -67,8 +67,8 @@ contract BaseScript is Script { globalFieldsSafeTokenWarrant[16] = "tokenPremiumMultiplier"; - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(52)), "cySPA + REG D SAFE + REG D TOKEN WARRANT", "IPFS://bafybeiapw7thrkzymtnhilmr5sjl7sm55yc42d2zxl66u6tdutfvm55t2y", globalFieldsSafeTokenWarrant, partyFields); - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(53)), "cySPA + REG S SAFE + REG S TOKEN WARRANT", "IPFS://bafybeianosjn74ldjexzmwcji6nl3l24ikwazd64uei625nszysqfwla2i", globalFieldsSafeTokenWarrant, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_safe_tw_reg_d_v1_3")), "metalex cybersafe + cybertokenwarrant jx-neutral reg d raise v 1.3", "IPFS://bafybeiaw3pwov3ahg4bk2hte2hu4pwv34nndoguxyk3umq6f5su3kod6ay", globalFieldsSafeTokenWarrant, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_safe_tw_reg_s_v1_3")), "metalex cybersafe + cybertokenwarrant jx-neutral reg s raise v 1.3", "IPFS://bafybeicto2raupsj5ad7snxvhmmll2plwyploqho4fg2cibnn2fuhlm2d4", globalFieldsSafeTokenWarrant, partyFields); //make an array for this: ["purchaseAmount", "postMoneyValuationCap", "protocolUSDValuationAtTimeofInvestment", "expirationTime", "governingJurisdiction", "disputeResolution", "unlockStartTimeType", "unlockStartTime", "unlockingPeriod", "unlockingCliffPeriod", "unlockingCliffPercentage", "unlockingIntervalType", "tokenCalculationMethod", "minCompanyReserve", "tokenPremiumMultiplier"]*/ string[] memory globalFieldsSafte = new string[](15); @@ -88,8 +88,8 @@ contract BaseScript is Script { globalFieldsSafte[13] = "minCompanyReserve"; globalFieldsSafte[14] = "tokenPremiumMultiplier"; - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(54)), "cySPA + REG D SAFTE ", "IPFS://bafybeidb2ebvu7uxt6m2ukrdnytzpwrb4ihcncbx2v4qohe5xao3xr3m7e", globalFieldsSafte, partyFields); - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(55)), "cySPA + REG S SAFTE ", "IPFS://bafybeideutuq3r3v66rdcvzar5heefyark24urc3ix44tvvjpo2ntvkc7i", globalFieldsSafte, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_safte_reg_d_v1_3")), "metalex cybersafte jx-neutral reg d raise v 1.3", "IPFS://bafybeiag7xatsusb24evnpyj6ztf62kix36dgbsp3kbazfyvr273ph56ay", globalFieldsSafte, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_safte_reg_s_v1_3")), "metalex cybersafte jx-neutral reg s raise v 1.3", "IPFS://bafybeia43r7e566s2jlq4gtaasmtybutujy7fuizhw3fycxtwnstfbkeia", globalFieldsSafte, partyFields); string[] memory globalFieldsSaft = new string[](10); globalFieldsSaft[0] = "purchaseAmount"; @@ -103,8 +103,8 @@ contract BaseScript is Script { globalFieldsSaft[8] = "unlockingCliffPercentage"; globalFieldsSaft[9] = "unlockingIntervalType"; - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(56)), "cySPA + REG D SAFT", "IPFS://bafybeidfbgwv35cu22ouwdpmho35gicfnkno2em7ngjn4mbhbiogvvaf7i", globalFieldsSaft, partyFields); - CyberAgreementRegistry(registry).createTemplate(bytes32(uint256(57)), "cySPA + REG S SAFT", "IPFS://bafybeibjm2mss4ctfsyajehtwnmje3aa2agif5n47i575pxdejrk7dee5m", globalFieldsSaft, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_saft_reg_d_v1_3")), "metalex cybersaft jx-neutral reg d raise v 1.3", "IPFS://bafybeieoljri2rwuv35rymjd654sr3u46kbcao7mymseqobfo7x6lxgdcy", globalFieldsSaft, partyFields); + CyberAgreementRegistry(registry).createTemplate(bytes32(bytes("mlx_saft_reg_s_v1_3")), "metalex cybersaft jx-neutral reg s raise v 1.3", "IPFS://bafybeibwrz3rttteguo5ccoh5x7ndwdu6hyhy7i3iraii5c5ml4pfv73t4", globalFieldsSaft, partyFields); } diff --git a/script/upgrade-certificate-uri-builder.s.sol b/script/upgrade-certificate-uri-builder.s.sol new file mode 100644 index 00000000..aab8e27b --- /dev/null +++ b/script/upgrade-certificate-uri-builder.s.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol"; +import {CertificateImageBuilderContract} from "../src/CertificateImageBuilderContract.sol"; + +/// @title UpgradeCertificateUriBuilder +/// @notice Main upgrade script - deploys image builder and upgrades CertificateUriBuilder +/// @dev Run with: forge script script/upgrade-certificate-uri-builder.s.sol:UpgradeCertificateUriBuilder --rpc-url $RPC_URL --broadcast --verify +contract UpgradeCertificateUriBuilder is Script { + function run() public { + // Use different salts for different contracts to avoid CREATE2 collision + bytes32 imageBuilderSalt = bytes32(keccak256("MetaLexCyberCorpImageBuilderV1.2")); + bytes32 implementationSalt = bytes32(keccak256("MetaLexCyberCorpUriBuilderV2.5")); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY_MAIN"); + address deployer = vm.addr(deployerPrivateKey); + + // Get the deployed CertificateUriBuilder proxy address from env + address deployedCertificateUriBuilderProxy = 0x5500c095ea7dE6F8a5E15949e24B80604cc670A3; + + console.log("=== CertificateUriBuilder Upgrade Script ==="); + console.log("Deployer address:", deployer); + console.log("Target proxy:", deployedCertificateUriBuilderProxy); + + + console.log(""); + console.log("=== Starting Deployment ==="); + + vm.startBroadcast(deployerPrivateKey); + + // Step 1: Deploy the new CertificateImageBuilderContract (standalone contract) + CertificateImageBuilderContract imageBuilderContract = new CertificateImageBuilderContract{salt: imageBuilderSalt}(); + address imageBuilderAddr = address(imageBuilderContract); + console.log("Step 1: CertificateImageBuilderContract deployed at:", imageBuilderAddr); + + // Step 2: Deploy new CertificateUriBuilder implementation + CertificateUriBuilder newImplementation = new CertificateUriBuilder{salt: implementationSalt}(); + address newImplAddr = address(newImplementation); + console.log("Step 2: New CertificateUriBuilder implementation deployed at:", newImplAddr); + + // Step 3: Upgrade the proxy to the new implementation + CertificateUriBuilder(deployedCertificateUriBuilderProxy).upgradeToAndCall(newImplAddr, ""); + console.log("Step 3: CertificateUriBuilder proxy upgraded successfully"); + + // Step 4: Set the image builder contract address on the upgraded CertificateUriBuilder + CertificateUriBuilder(deployedCertificateUriBuilderProxy).setImageBuilder(imageBuilderAddr); + console.log("Step 4: ImageBuilder set on CertificateUriBuilder"); + + vm.stopBroadcast(); + + // Post-deployment verification + console.log(""); + console.log("=== Post-Deployment Verification ==="); + address verifyImageBuilder = CertificateUriBuilder(deployedCertificateUriBuilderProxy).imageBuilder(); + console.log("Verified imageBuilder:", verifyImageBuilder); + require(verifyImageBuilder == imageBuilderAddr, "ImageBuilder not set correctly!"); + console.log("Upgrade completed successfully!"); + + console.log(""); + console.log("=== Summary ==="); + console.log("Proxy address:", deployedCertificateUriBuilderProxy); + console.log("New implementation:", newImplAddr); + console.log("Image builder:", imageBuilderAddr); + } +} diff --git a/script/upgrade-core-stack.s.sol b/script/upgrade-core-stack.s.sol new file mode 100644 index 00000000..369ab3cb --- /dev/null +++ b/script/upgrade-core-stack.s.sol @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {CyberCorpFactory} from "../src/CyberCorpFactory.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import {DealManagerFactory} from "../src/DealManagerFactory.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {CyberCorp} from "../src/CyberCorp.sol"; +import {IssuanceManager} from "../src/IssuanceManager.sol"; +import {DealManager} from "../src/DealManager.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; +import {CyberScrip} from "../src/CyberScrip.sol"; +import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol"; +import {CertificateImageBuilderContract} from "../src/CertificateImageBuilderContract.sol"; +import {IssuerApprovalRecertificationCondition} from "../src/libs/conditions/IssuerApprovalRecertificationCondition.sol"; +import {ERC1967ProxyLib} from "../test/libs/ERC1967ProxyLib.sol"; +import {DeploymentConstants} from "./libs/DeploymentConstants.sol"; + +interface IUUPS { + function upgradeToAndCall( + address newImplementation, + bytes calldata data + ) external payable; +} + +/// @notice Upgrade script for CyberCorp + IssuanceManager + CyberCertPrinter + CyberScrip. +/// @dev Default run updates factory reference implementations only. +/// To also upgrade a specific deployed stack, set CORP_ADDRESS env var +/// or call run(address[]) with explicit corp addresses. +contract BaseScript is Script { + using ERC1967ProxyLib for address; + bytes32 internal constant UPGRADE_SALT = + keccak256("MetaLexCyberCorp.CoreStack.UpgradeV2.0.8"); + + struct UpgradeImplementations { + address cyberCorpFactoryImpl; + address cyberCorpImpl; + address issuanceManagerImpl; + address dealManagerImpl; + address roundManagerImpl; + address certificateUriBuilderImpl; + address certificateImageBuilderImpl; + address cyberCertPrinterImpl; + address cyberScripImpl; + address issuerApprovalRecertificationCondition; + } + + function run() public { + address[] memory cyberCorps = new address[](0); + address maybeCorp = vm.envOr("CORP_ADDRESS", address(0)); + if (maybeCorp != address(0)) { + cyberCorps = new address[](1); + cyberCorps[0] = maybeCorp; + } + _run(cyberCorps); + } + + function run(address[] memory cyberCorps) public { + _run(cyberCorps); + } + + function _run(address[] memory cyberCorps) internal { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY_MAIN"); + address deployer = vm.addr(deployerPrivateKey); + DeploymentConstants.CoreDeployment memory deployment = DeploymentConstants + .coreV2(block.chainid); + address cyberCorpFactoryProxyAddr = vm.envOr( + "CYBERCORP_FACTORY", + deployment.cyberCorpFactory + ); + address uriBuilderProxyAddr = vm.envOr( + "URI_BUILDER", + deployment.uriBuilder + ); + + CyberCorpFactory cyberCorpFactory = CyberCorpFactory( + cyberCorpFactoryProxyAddr + ); + address auth = address(cyberCorpFactory.AUTH()); + + uint256 role = BorgAuth(auth).userRoles(deployer); + if (role < BorgAuth(auth).OWNER_ROLE()) { + revert( + "Deployer is not AUTH owner; use the AUTH owner key to run upgrades" + ); + } + + vm.startBroadcast(deployerPrivateKey); + + UpgradeImplementations memory impls = _deployNewImplementations(); + _upgradeCyberCorpFactory(cyberCorpFactoryProxyAddr, impls); + _upgradeUriBuilderStack(uriBuilderProxyAddr, impls); + _updateFactoryReferences(cyberCorpFactory, impls); + _upgradeCyberCorpStacks(cyberCorps, impls); + BorgAuth(auth).zeroOwner(); + //log that owner has been zeroed + console2.log("Deployer auth has been zeroed"); + vm.stopBroadcast(); + } + + function _deployNewImplementations() + internal + returns (UpgradeImplementations memory impls) + { + impls.cyberCorpFactoryImpl = address( + new CyberCorpFactory{salt: UPGRADE_SALT}() + ); + impls.cyberCorpImpl = address(new CyberCorp{salt: UPGRADE_SALT}()); + impls.issuanceManagerImpl = address( + new IssuanceManager{salt: UPGRADE_SALT}() + ); + impls.dealManagerImpl = address(new DealManager{salt: UPGRADE_SALT}()); + impls.roundManagerImpl = address( + new RoundManager{salt: UPGRADE_SALT}() + ); + impls.certificateUriBuilderImpl = address( + new CertificateUriBuilder{salt: UPGRADE_SALT}() + ); + impls.certificateImageBuilderImpl = address( + new CertificateImageBuilderContract{salt: UPGRADE_SALT}() + ); + impls.cyberCertPrinterImpl = address( + new CyberCertPrinter{salt: UPGRADE_SALT}() + ); + impls.cyberScripImpl = address(new CyberScrip{salt: UPGRADE_SALT}()); + impls.issuerApprovalRecertificationCondition = address( + new IssuerApprovalRecertificationCondition{salt: UPGRADE_SALT}() + ); + + console2.log( + "New CyberCorpFactory implementation:", + impls.cyberCorpFactoryImpl + ); + console2.log("New CyberCorp implementation:", impls.cyberCorpImpl); + console2.log( + "New IssuanceManager implementation:", + impls.issuanceManagerImpl + ); + console2.log("New DealManager implementation:", impls.dealManagerImpl); + console2.log("New RoundManager implementation:", impls.roundManagerImpl); + console2.log( + "New CertificateUriBuilder implementation:", + impls.certificateUriBuilderImpl + ); + console2.log( + "New CertificateImageBuilder implementation:", + impls.certificateImageBuilderImpl + ); + console2.log( + "New CyberCertPrinter implementation:", + impls.cyberCertPrinterImpl + ); + console2.log("New CyberScrip implementation:", impls.cyberScripImpl); + console2.log( + "New IssuerApprovalRecertificationCondition:", + impls.issuerApprovalRecertificationCondition + ); + } + + function _upgradeCyberCorpFactory( + address cyberCorpFactoryProxyAddr, + UpgradeImplementations memory impls + ) internal { + address oldCyberCorpFactoryImpl = cyberCorpFactoryProxyAddr + .getErc1967Implementation(); + + console2.log("Upgrading CyberCorpFactory:", cyberCorpFactoryProxyAddr); + console2.log( + " old CyberCorpFactory impl:", + oldCyberCorpFactoryImpl + ); + + IUUPS(cyberCorpFactoryProxyAddr).upgradeToAndCall( + impls.cyberCorpFactoryImpl, + "" + ); + + vm.assertEq( + cyberCorpFactoryProxyAddr.getErc1967Implementation(), + impls.cyberCorpFactoryImpl, + "CyberCorpFactory upgrade failed" + ); + + console2.log( + " new CyberCorpFactory impl:", + cyberCorpFactoryProxyAddr.getErc1967Implementation() + ); + } + + function _updateFactoryReferences( + CyberCorpFactory cyberCorpFactory, + UpgradeImplementations memory impls + ) internal { + // These refs must be set before proxy/beacon upgrades due to _authorizeUpgrade checks. + CyberCorpSingleFactory corpSingleFactory = CyberCorpSingleFactory( + cyberCorpFactory.cyberCorpSingleFactory() + ); + IssuanceManagerFactory issuanceManagerFactory = IssuanceManagerFactory( + cyberCorpFactory.issuanceManagerFactory() + ); + DealManagerFactory dealManagerFactory = DealManagerFactory( + cyberCorpFactory.dealManagerFactory() + ); + RoundManagerFactory roundManagerFactory = RoundManagerFactory( + cyberCorpFactory.roundManagerFactory() + ); + + corpSingleFactory.setRefImplementation(impls.cyberCorpImpl); + issuanceManagerFactory.setRefImplementation(impls.issuanceManagerImpl); + dealManagerFactory.setRefImplementation(impls.dealManagerImpl); + roundManagerFactory.setRefImplementation(impls.roundManagerImpl); + issuanceManagerFactory.setCyberCertPrinterRefImplementation( + impls.cyberCertPrinterImpl + ); + issuanceManagerFactory.setCyberScripRefImplementation( + impls.cyberScripImpl + ); + + vm.assertEq( + corpSingleFactory.getRefImplementation(), + impls.cyberCorpImpl, + "CyberCorpSingleFactory reference implementation mismatch" + ); + vm.assertEq( + issuanceManagerFactory.getRefImplementation(), + impls.issuanceManagerImpl, + "IssuanceManagerFactory reference implementation mismatch" + ); + vm.assertEq( + dealManagerFactory.getRefImplementation(), + impls.dealManagerImpl, + "DealManagerFactory reference implementation mismatch" + ); + vm.assertEq( + roundManagerFactory.getRefImplementation(), + impls.roundManagerImpl, + "RoundManagerFactory reference implementation mismatch" + ); + vm.assertEq( + issuanceManagerFactory.getCyberCertPrinterRefImplementation(), + impls.cyberCertPrinterImpl, + "IssuanceManagerFactory CyberCertPrinter reference implementation mismatch" + ); + vm.assertEq( + issuanceManagerFactory.getCyberScripRefImplementation(), + impls.cyberScripImpl, + "IssuanceManagerFactory CyberScrip reference implementation mismatch" + ); + + console2.log( + "Factory refs updated: CyberCorp, IssuanceManager, DealManager, RoundManager, CyberCertPrinter, CyberScrip" + ); + } + + function _upgradeUriBuilderStack( + address uriBuilderProxyAddr, + UpgradeImplementations memory impls + ) internal { + address oldUriBuilderImpl = uriBuilderProxyAddr.getErc1967Implementation(); + CertificateUriBuilder uriBuilder = CertificateUriBuilder(uriBuilderProxyAddr); + address oldImageBuilderImpl = uriBuilder.imageBuilder(); + + console2.log("Upgrading CertificateUriBuilder:", uriBuilderProxyAddr); + console2.log(" old CertificateUriBuilder impl:", oldUriBuilderImpl); + console2.log(" old CertificateImageBuilder impl:", oldImageBuilderImpl); + + IUUPS(uriBuilderProxyAddr).upgradeToAndCall( + impls.certificateUriBuilderImpl, + "" + ); + uriBuilder.setImageBuilder(impls.certificateImageBuilderImpl); + + vm.assertEq( + uriBuilderProxyAddr.getErc1967Implementation(), + impls.certificateUriBuilderImpl, + "CertificateUriBuilder upgrade failed" + ); + vm.assertEq( + uriBuilder.imageBuilder(), + impls.certificateImageBuilderImpl, + "CertificateImageBuilder update failed" + ); + + console2.log( + " new CertificateUriBuilder impl:", + uriBuilderProxyAddr.getErc1967Implementation() + ); + console2.log( + " new CertificateImageBuilder impl:", + uriBuilder.imageBuilder() + ); + } + + function _upgradeCyberCorpStacks( + address[] memory cyberCorps, + UpgradeImplementations memory impls + ) internal { + if (cyberCorps.length == 0) { + console2.log( + "No CyberCorp addresses provided; skipped proxy/beacon upgrades." + ); + return; + } + + for (uint256 i = 0; i < cyberCorps.length; i++) { + _upgradeSingleCyberCorpStack(cyberCorps[i], impls); + } + } + + function _upgradeSingleCyberCorpStack( + address cyberCorpAddr, + UpgradeImplementations memory impls + ) internal { + address oldCyberCorpImpl = cyberCorpAddr.getErc1967Implementation(); + + CyberCorp cyberCorp = CyberCorp(cyberCorpAddr); + address issuanceManagerAddr = cyberCorp.issuanceManager(); + address dealManagerAddr = cyberCorp.dealManager(); + address roundManagerAddr = cyberCorp.roundManager(); + if (issuanceManagerAddr == address(0)) { + revert("CyberCorp has no IssuanceManager"); + } + if (dealManagerAddr == address(0)) { + revert("CyberCorp has no DealManager"); + } + if (roundManagerAddr == address(0)) { + revert("CyberCorp has no RoundManager"); + } + address oldIssuanceManagerImpl = issuanceManagerAddr + .getErc1967Implementation(); + address oldDealManagerImpl = dealManagerAddr.getErc1967Implementation(); + address oldRoundManagerImpl = roundManagerAddr.getErc1967Implementation(); + + console2.log("Upgrading CyberCorp:", cyberCorpAddr); + console2.log(" old CyberCorp impl:", oldCyberCorpImpl); + console2.log(" old IssuanceManager impl:", oldIssuanceManagerImpl); + console2.log(" old DealManager impl:", oldDealManagerImpl); + console2.log(" old RoundManager impl:", oldRoundManagerImpl); + + IUUPS(cyberCorpAddr).upgradeToAndCall(impls.cyberCorpImpl, ""); + IUUPS(issuanceManagerAddr).upgradeToAndCall( + impls.issuanceManagerImpl, + "" + ); + IUUPS(dealManagerAddr).upgradeToAndCall(impls.dealManagerImpl, ""); + IUUPS(roundManagerAddr).upgradeToAndCall(impls.roundManagerImpl, ""); + + IssuanceManager issuanceManager = IssuanceManager(issuanceManagerAddr); + issuanceManager.upgradeCertPrinterBeaconImplementation( + impls.cyberCertPrinterImpl + ); + issuanceManager.upgradeScripBeaconImplementation(impls.cyberScripImpl); + + vm.assertEq( + cyberCorpAddr.getErc1967Implementation(), + impls.cyberCorpImpl, + "CyberCorp upgrade failed" + ); + vm.assertEq( + issuanceManagerAddr.getErc1967Implementation(), + impls.issuanceManagerImpl, + "IssuanceManager upgrade failed" + ); + vm.assertEq( + dealManagerAddr.getErc1967Implementation(), + impls.dealManagerImpl, + "DealManager upgrade failed" + ); + vm.assertEq( + roundManagerAddr.getErc1967Implementation(), + impls.roundManagerImpl, + "RoundManager upgrade failed" + ); + vm.assertEq( + issuanceManager.getCertPrinterBeaconImplementation(), + impls.cyberCertPrinterImpl, + "CyberCertPrinter beacon upgrade failed" + ); + vm.assertEq( + issuanceManager.getScripBeaconImplementation(), + impls.cyberScripImpl, + "CyberScrip beacon upgrade failed" + ); + + console2.log(" new CyberCorp impl:", cyberCorpAddr.getErc1967Implementation()); + console2.log( + " new IssuanceManager impl:", + issuanceManagerAddr.getErc1967Implementation() + ); + console2.log( + " new DealManager impl:", + dealManagerAddr.getErc1967Implementation() + ); + console2.log( + " new RoundManager impl:", + roundManagerAddr.getErc1967Implementation() + ); + console2.log( + " new CyberCertPrinter beacon impl:", + issuanceManager.getCertPrinterBeaconImplementation() + ); + console2.log( + " new CyberScrip beacon impl:", + issuanceManager.getScripBeaconImplementation() + ); + } +} diff --git a/script/upgrade-issuance-manager-and-beacons.s.sol b/script/upgrade-issuance-manager-and-beacons.s.sol new file mode 100644 index 00000000..70e47e54 --- /dev/null +++ b/script/upgrade-issuance-manager-and-beacons.s.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {CyberCorpFactory} from "../src/CyberCorpFactory.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import {IssuanceManager} from "../src/IssuanceManager.sol"; +import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; +import {CyberScrip} from "../src/CyberScrip.sol"; +import {CyberCorp} from "../src/CyberCorp.sol"; +import {KnownAddressesLoader} from "./libs/KnownAddressesLoader.sol"; + +interface IUUPS { + function upgradeToAndCall( + address newImplementation, + bytes calldata data + ) external payable; +} + +contract UpgradeIssuanceManagerAndBeaconsScript is Script { + function run() public { + runWithArgs(type(uint256).max); + } + + function runWithArgs(uint256 maxCount) public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY_MAIN"); + address cyberCorpFactoryProxyAddr = vm.envAddress("CYBERCORP_FACTORY"); + + CyberCorpFactory factoryProxy = CyberCorpFactory( + cyberCorpFactoryProxyAddr + ); + address auth = address(factoryProxy.AUTH()); + + uint256 role = BorgAuth(auth).userRoles(vm.addr(deployerPrivateKey)); + if (role < BorgAuth(auth).OWNER_ROLE()) { + revert("Deployer is not AUTH owner; use the AUTH owner key"); + } + + address[] memory knownCyberCorps = KnownAddressesLoader.load( + block.chainid, + "/script/res/known-cyber-corps.json", + maxCount + ); + + vm.startBroadcast(deployerPrivateKey); + + // Deploy new reference implementations + IssuanceManager newImImpl = new IssuanceManager(); + CyberCertPrinter newCertImpl = new CyberCertPrinter(); + CyberScrip newScripImpl = new CyberScrip(); + + console2.log("New IssuanceManager impl:", address(newImImpl)); + console2.log("New CyberCertPrinter impl:", address(newCertImpl)); + console2.log("New CyberScrip impl:", address(newScripImpl)); + + // Update factory reference implementations (required for UUPS upgrade checks) + IssuanceManagerFactory imFactory = IssuanceManagerFactory( + factoryProxy.issuanceManagerFactory() + ); + imFactory.setRefImplementation(address(newImImpl)); + imFactory.setCyberCertPrinterRefImplementation(address(newCertImpl)); + imFactory.setCyberScripRefImplementation(address(newScripImpl)); + + vm.stopBroadcast(); + } +} diff --git a/src/CertificateImageBuilder.sol b/src/CertificateImageBuilder.sol index 3de98ce9..d582a4ef 100644 --- a/src/CertificateImageBuilder.sol +++ b/src/CertificateImageBuilder.sol @@ -41,7 +41,9 @@ except with the express prior written permission of the copyright holder.*/ pragma solidity ^0.8.28; +import "./CyberCorpConstants.sol"; import "./creds/storage/lexchexStorage.sol"; +import "./CertificateImageContentBuilder.sol"; /// @title CertificateImageBuilder /// @notice Minimal SVG builder used by CertificateUriBuilder; mirrors LeXcheX styling @@ -57,31 +59,71 @@ library CertificateImageBuilder { } function buildCertificateSVG( - string memory corpName, - string memory securityType, - string memory officerName, - string memory officerTitle, - uint256 units, - uint256 valuation - ) internal pure returns (string memory) { + CertificateSVGParams memory params, + uint256 timestamp + ) internal view returns (string memory) { return string(abi.encodePacked( - '', - '', - _generateDefs(), - '', - '', corpName, '', - '', securityType, '', - 'Officer', - '', officerName, ' (', officerTitle, ')', - 'Units', - '', _uintToString(units), '', - 'Issuer Valuation (USD)', - '', _uintToString(valuation), '', - _generateMetaLeXLogo(), - '' + _getSVGHeader(), + _getSVGBackground(), + CertificateImageContentBuilder.buildSVGContent(params, timestamp), + _getSVGFooter() + )); + } + + function _getSVGHeader() private pure returns (string memory) { + return ''; + } + + function _getSVGBackground() private pure returns (string memory) { + return string(abi.encodePacked( + '', + '', + '', + _getSVGDecorativeElements() )); } + function _getSVGDecorativeElements() private pure returns (string memory) { + return string(abi.encodePacked( + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + _generateBottomDecorations(), + '' + )); + } + + function _getSVGFooter() private pure returns (string memory) { + return ''; + } + function _generateMetaLeXLogo() private pure returns (string memory) { return string( abi.encodePacked( @@ -92,6 +134,43 @@ 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( @@ -147,15 +226,6 @@ library CertificateImageBuilder { return string(abi.encodePacked(_uintToString(month), '/', _uintToString(day), '/', _uintToString(year))); } - function _bytes32ToHexString(bytes32 value) private pure returns (string memory) { - bytes memory str = new bytes(64); - for (uint256 i = 0; i < 32; i++) { - str[i * 2] = bytes1(uint8(uint256(uint8(value[i] >> 4)) + (uint256(uint8(value[i] >> 4)) < 10 ? 48 : 87))); - str[i * 2 + 1] = bytes1(uint8(uint256(uint8(value[i] & 0x0f)) + (uint256(uint8(value[i] & 0x0f)) < 10 ? 48 : 87))); - } - return string(abi.encodePacked("0x", string(str))); - } - function _uintToString(uint256 _i) private pure returns (string memory) { if (_i == 0) return "0"; uint256 j = _i; @@ -173,6 +243,13 @@ library CertificateImageBuilder { } return string(bstr); } -} - + function _bytes32ToHexString(bytes32 value) private pure returns (string memory) { + bytes memory str = new bytes(64); + for (uint256 i = 0; i < 32; i++) { + str[i * 2] = bytes1(uint8(uint256(uint8(value[i] >> 4)) + (uint256(uint8(value[i] >> 4)) < 10 ? 48 : 87))); + str[i * 2 + 1] = bytes1(uint8(uint256(uint8(value[i] & 0x0f)) + (uint256(uint8(value[i] & 0x0f)) < 10 ? 48 : 87))); + } + return string(abi.encodePacked("0x", string(str))); + } +} diff --git a/src/CertificateImageBuilderContract.sol b/src/CertificateImageBuilderContract.sol new file mode 100644 index 00000000..ba8ed0a8 --- /dev/null +++ b/src/CertificateImageBuilderContract.sol @@ -0,0 +1,508 @@ +/* .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 "./CyberCorpConstants.sol"; +import "./interfaces/ICertificateImageBuilder.sol"; + +/// @title CertificateImageBuilderContract +/// @notice Standalone contract for building certificate SVG images +/// @dev Deployed separately to reduce CertificateUriBuilder contract size +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); + } +} + diff --git a/src/CertificateImageContentBuilder.sol b/src/CertificateImageContentBuilder.sol new file mode 100644 index 00000000..81665cec --- /dev/null +++ b/src/CertificateImageContentBuilder.sol @@ -0,0 +1,349 @@ +/* .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 "./CyberCorpConstants.sol"; + +/// @title CertificateImageContentBuilder +/// @notice Helper library for generating SVG content +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); + + return string(abi.encodePacked( + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + _buildTextContent(params, securityType, unitType, day, month, year) + )); + } + + 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.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"; + } + + 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 _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 _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; + } + + 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 _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"; + } + + function _getDateComponents(uint256 timestamp) private pure returns (string memory day, string memory month, string memory year) { + // Convert timestamp to days since epoch + 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 + } + + // 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); + } + + 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 _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)); + } + + string memory middleSection; + if (_isConvertible(params.securityType)) { + middleSection = _buildMiddleConvertible(params, formattedUnits); + } else { + middleSection = _buildMiddleShares(params, unitType, formattedUnits); + } + + return string(abi.encodePacked( + _buildHeader(params, securityType, formattedUnits), + middleSection, + _buildFooter(params, day, month, year) + )); + } + + 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, '' + )); + } + + 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.' + )); + } + + 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, '' + )); + } + + 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); + } +} diff --git a/src/CertificateUriBuilder.sol b/src/CertificateUriBuilder.sol index 2c01db6d..2311dece 100644 --- a/src/CertificateUriBuilder.sol +++ b/src/CertificateUriBuilder.sol @@ -44,20 +44,35 @@ pragma solidity ^0.8.28; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "./CyberCorpConstants.sol"; import "./interfaces/ICyberAgreementRegistry.sol"; +import "./interfaces/ICertificateImageBuilder.sol"; import "./storage/extensions/ICertificateExtension.sol"; import "./libs/auth.sol"; -import "./CertificateImageBuilder.sol"; contract CertificateUriBuilder is UUPSUpgradeable, BorgAuthACL { - // Upgrade notes: Reduced gap to account for new variables (50 - 1 = 49) - uint256[49] private __gap; + /// @notice Address of the external image builder contract + address public imageBuilder; + + /// @notice Emitted when the image builder is updated + event ImageBuilderUpdated(address indexed oldBuilder, address indexed newBuilder); + + // Upgrade notes: Reduced gap to account for new variables (50 - 2 = 48) + uint256[48] private __gap; function initialize(address _auth) public initializer { __UUPSUpgradeable_init(); __BorgAuthACL_init(_auth); } + /// @notice Sets the image builder contract address + /// @param _imageBuilder The address of the CertificateImageBuilderContract + function setImageBuilder(address _imageBuilder) external onlyOwner { + require(_imageBuilder != address(0), "Invalid image builder address"); + address oldBuilder = imageBuilder; + imageBuilder = _imageBuilder; + emit ImageBuilderUpdated(oldBuilder, _imageBuilder); + } + // Helper function to convert SecurityClass enum to string function securityClassToString(SecurityClass _class) public pure returns (string memory) { if (_class == SecurityClass.SAFE) return "SAFE"; @@ -104,6 +119,7 @@ contract CertificateUriBuilder is UUPSUpgradeable, BorgAuthACL { if (_series == SecuritySeries.SeriesE) return "SeriesE"; if (_series == SecuritySeries.SeriesF) return "SeriesF"; if (_series == SecuritySeries.NA) return "NA"; + if (_series == SecuritySeries.ACE) return "ACE"; return "Unknown"; } @@ -153,6 +169,28 @@ contract CertificateUriBuilder is UUPSUpgradeable, BorgAuthACL { return string(bstr); } + /// @notice Converts 18-decimal value to string with 2 decimal places (cents) + /// @param value The 18-decimal value (e.g., 100000.50 * 1e18) + /// @return The formatted string with 2 decimals (e.g., "100000.50") + function from18DecimalsToString(uint256 value) public 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 = uint256ToString(wholePart); + + // Format cents with leading zero if needed + string memory centsStr; + if (centsPart < 10) { + centsStr = string(abi.encodePacked("0", uint256ToString(centsPart))); + } else { + centsStr = uint256ToString(centsPart); + } + + return string(abi.encodePacked(wholeStr, ".", centsStr)); + } + // Helper function to convert bytes32 to string function bytes32ToString(bytes32 _bytes32) public pure returns (string memory) { bytes memory bytesArray = new bytes(64); @@ -179,6 +217,21 @@ contract CertificateUriBuilder is UUPSUpgradeable, BorgAuthACL { return string(hexString); } + /// @notice Removes the first 7 characters from a string (e.g., strips "ipfs://") + /// @param str The original string + /// @return The string with first 7 characters removed + function stripIpfsPrefix(string memory str) public pure returns (string memory) { + bytes memory strBytes = bytes(str); + if (strBytes.length <= 7) { + return ""; + } + bytes memory result = new bytes(strBytes.length - 7); + for (uint i = 7; i < strBytes.length; i++) { + result[i - 7] = strBytes[i]; + } + return string(result); + } + struct CertificateDetails { string signingOfficerName; string signingOfficerTitle; @@ -204,15 +257,91 @@ struct CertificateDetails { address ownerAddress; } + + /// @notice Fetches the last signed timestamp from the registry for a given agreement + /// @param registry The registry contract address + /// @param agreementId The agreement ID + /// @return timestamp The last signed timestamp, or block.timestamp if unavailable + function _getAgreementTimestamp(address registry, bytes32 agreementId) internal view returns (uint256 timestamp) { + if (registry == address(0) || agreementId == bytes32(0)) { + return block.timestamp; + } + + try ICyberAgreementRegistry(registry).getContractDetails(agreementId) returns ( + bytes32, + string memory, + string[] memory, + string[] memory, + string[] memory, + address[] memory, + string[][] memory, + uint256[] memory signedAt, + uint256, + bool, + bytes32 + ) { + // Use the last signature timestamp + if (signedAt.length > 0) { + uint256 lastTimestamp = signedAt[signedAt.length - 1]; + if (lastTimestamp > 0) { + return lastTimestamp; + } + } + } catch { + // If the call fails, fall through to return block.timestamp + } + + return block.timestamp; + } + function buildAttributes( OwnerDetails memory owner, - CertificateDetails memory details + CertificateDetails memory details, + string memory cyberCORPName, + string memory cyberCORPType, + string memory cyberCORPJurisdiction, + string memory cyberCORPContactDetails, + SecurityClass securityType, + SecuritySeries securitySeries, + string memory certificateUri + ) internal pure returns (string memory) { + return string(abi.encodePacked( + _buildAttributesPart1(owner, details, cyberCORPName, cyberCORPType), + _buildAttributesPart2(cyberCORPJurisdiction, cyberCORPContactDetails, securityType, securitySeries, certificateUri) + )); + } + + function _buildAttributesPart1( + OwnerDetails memory owner, + CertificateDetails memory details, + string memory cyberCORPName, + string memory cyberCORPType ) internal pure returns (string memory) { return string(abi.encodePacked( '{"trait_type": "CurrentOwner", "value": "', addressToString(owner.ownerAddress), - '"}, {"trait_type": "investmentAmount", "value": "', uint256ToString(details.investmentAmountUSD), - '"}, {"trait_type": "unitsRepresented", "value": "', uint256ToString(details.unitsRepresented), - '"}, {"trait_type": "issuerUSDValuationAtTimeOfInvestment", "value": "', uint256ToString(details.issuerUSDValuationAtTimeOfInvestment), + '"}, {"trait_type": "CurrentOwnerName", "value": "', owner.name, + '"}, {"trait_type": "investmentAmount", "value": "', from18DecimalsToString(details.investmentAmountUSD), + '"}, {"trait_type": "unitsRepresented", "value": "', from18DecimalsToString(details.unitsRepresented), + '"}, {"trait_type": "issuerUSDValuationAtTimeOfInvestment", "value": "', from18DecimalsToString(details.issuerUSDValuationAtTimeOfInvestment), + '"}, {"trait_type": "cyberCORPName", "value": "', cyberCORPName, + '"}, {"trait_type": "cyberCORPType", "value": "', cyberCORPType, + '"}' + )); + } + + function _buildAttributesPart2( + string memory cyberCORPJurisdiction, + string memory cyberCORPContactDetails, + SecurityClass securityType, + SecuritySeries securitySeries, + string memory certificateUri + ) internal pure returns (string memory) { + return string(abi.encodePacked( + ', {"trait_type": "cyberCORPJurisdiction", "value": "', cyberCORPJurisdiction, + '"}, {"trait_type": "cyberCORPContactDetails", "value": "', cyberCORPContactDetails, + '"}, {"trait_type": "securityType", "value": "', securityClassToString(securityType), + '"}, {"trait_type": "securitySeries", "value": "', securitySeriesToString(securitySeries), + '"}, {"trait_type": "certificateUri", "value": "https://ipfs.io/ipfs/', stripIpfsPrefix(certificateUri), '"}' )); } @@ -316,21 +445,32 @@ 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 - string memory svg = CertificateImageBuilder.buildCertificateSVG( - cyberCORPName, - securityClassToString(securityType), - details.signingOfficerName, - details.signingOfficerTitle, - details.unitsRepresented, - details.issuerUSDValuationAtTimeOfInvestment - ); + + // 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), + '"attributes": [', buildAttributes(owner, details, cyberCORPName, cyberCORPType, cyberCORPJurisdiction, cyberCORPContactDetails, securityType, securitySeries, certificateUri), '],' )); @@ -351,9 +491,9 @@ struct CertificateDetails { json = string.concat(json, ', "signingOfficerName": "', details.signingOfficerName, '", "signingOfficerTitle": "', details.signingOfficerTitle, - '", "investmentAmountUSD": "', uint256ToString(details.investmentAmountUSD), - '", "issuerUSDValuationAtTimeOfInvestment": "', uint256ToString(details.issuerUSDValuationAtTimeOfInvestment), - '", "unitsRepresented": "', uint256ToString(details.unitsRepresented), + '", "investmentAmountUSD": "', from18DecimalsToString(details.investmentAmountUSD), + '", "issuerUSDValuationAtTimeOfInvestment": "', from18DecimalsToString(details.issuerUSDValuationAtTimeOfInvestment), + '", "unitsRepresented": "', from18DecimalsToString(details.unitsRepresented), '", "legalDetails": "', details.legalDetails, '"' ); @@ -384,7 +524,7 @@ struct CertificateDetails { return json; } - function buildCertificateUriNotEncoded( + function buildCertificateUriNotEncoded( string memory cyberCORPName, string memory cyberCORPType, string memory cyberCORPJurisdiction, @@ -404,21 +544,32 @@ 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 - string memory svg = CertificateImageBuilder.buildCertificateSVG( - cyberCORPName, - securityClassToString(securityType), - details.signingOfficerName, - details.signingOfficerTitle, - details.unitsRepresented, - details.issuerUSDValuationAtTimeOfInvestment - ); + + // 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), + '"attributes": [', buildAttributes(owner, details, cyberCORPName, cyberCORPType, cyberCORPJurisdiction, cyberCORPContactDetails, securityType, securitySeries, certificateUri), '],' )); @@ -439,9 +590,9 @@ struct CertificateDetails { json = string.concat(json, ', "signingOfficerName": "', details.signingOfficerName, '", "signingOfficerTitle": "', details.signingOfficerTitle, - '", "investmentAmountUSD": "', uint256ToString(details.investmentAmountUSD), - '", "issuerUSDValuationAtTimeOfInvestment": "', uint256ToString(details.issuerUSDValuationAtTimeOfInvestment), - '", "unitsRepresented": "', uint256ToString(details.unitsRepresented), + '", "investmentAmountUSD": "', from18DecimalsToString(details.investmentAmountUSD), + '", "issuerUSDValuationAtTimeOfInvestment": "', from18DecimalsToString(details.issuerUSDValuationAtTimeOfInvestment), + '", "unitsRepresented": "', from18DecimalsToString(details.unitsRepresented), '", "legalDetails": "', details.legalDetails, '"' ); diff --git a/src/CyberCertPrinter.sol b/src/CyberCertPrinter.sol index cb937684..fd6187cc 100644 --- a/src/CyberCertPrinter.sol +++ b/src/CyberCertPrinter.sol @@ -52,7 +52,7 @@ import "./interfaces/ICyberAgreementRegistry.sol"; contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { using CyberCertPrinterStorage for CyberCertPrinterStorage.CyberCertStorage; - string public constant DEPLOY_VERSION = "3"; // For version-tracking on all deployment and future upgrades + string public constant DEPLOY_VERSION = "4"; // For version-tracking on all deployment and future upgrades // Custom errors error NotIssuanceManager(); @@ -66,11 +66,12 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { error EndorsementNotSignedOrInvalid(); error InvalidEndorsement(); error InvalidLegendIndex(); + error SignatureRequired(); //events event CertificateCreated(uint256 indexed tokenId, address indexed investor, uint256 amount, uint256 cap); event Converted(uint256 indexed oldTokenId, uint256 indexed newTokenId); - event CertificateSigned(uint256 indexed tokenId, string signatureURI); + event CertificateSigned(uint256 indexed tokenId, bytes signature); event CertificateEndorsed( uint256 indexed tokenId, address indexed endorser, @@ -146,6 +147,10 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { _safeMint(to, tokenId); CyberCertPrinterStorage.cyberCertStorage().certLegend[tokenId] = CyberCertPrinterStorage.cyberCertStorage().defaultLegend; CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId] = details; + CyberCertPrinterStorage.cyberCertStorage().owners[tokenId] = OwnerDetails( + "", + to + ); emit CyberCertPrinter_CertificateCreated(tokenId); return tokenId; } @@ -154,13 +159,19 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { function safeMintAndAssign( address to, uint256 tokenId, - CertificateDetails memory details + CertificateDetails memory details, + string memory investorName ) external onlyIssuanceManager returns (uint256) { _safeMint(to, tokenId); CyberCertPrinterStorage.cyberCertStorage().certLegend[tokenId] = CyberCertPrinterStorage.cyberCertStorage().defaultLegend; // Store agreement details CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId] = details; + CyberCertPrinterStorage.cyberCertStorage().owners[tokenId] = OwnerDetails( + investorName, + to + ); string memory issuerName = IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).companyName(); + emit CertificateAssigned(tokenId, to, investorName, issuerName); emit CyberCertPrinter_CertificateCreated(tokenId); return tokenId; } @@ -173,16 +184,15 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { ) external onlyIssuanceManager returns (uint256) { if(ownerOf(tokenId) != from) revert InvalidTokenId(); CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId] = details; + CyberCertPrinterStorage.cyberCertStorage().owners[tokenId] = OwnerDetails( + "", + to + ); string memory issuerName = IIssuanceManager(CyberCertPrinterStorage.cyberCertStorage().issuanceManager).companyName(); - // _transfer(from, to, tokenId); + emit CertificateAssigned(tokenId, to, "", issuerName); return tokenId; } - // Simplified mint for backward compatibility - function safeMint(address to, uint256 tokenId) external onlyIssuanceManager { - _safeMint(to, tokenId); - } - // Add endorsement (for transfers in secondary market) function addEndorsement(uint256 tokenId, Endorsement memory newEndorsement) public { if(msg.sender != CyberCertPrinterStorage.cyberCertStorage().issuanceManager && msg.sender != ownerOf(tokenId)) revert InvalidEndorsement(); @@ -199,6 +209,16 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { ); } + function addIssuerSignature( + uint256 tokenId, + bytes calldata signature + ) external onlyIssuanceManager { + if (!_exists(tokenId)) revert TokenDoesNotExist(); + if (signature.length == 0) revert SignatureRequired(); + CyberCertPrinterStorage.cyberCertStorage().issuerSignatures[tokenId].push(signature); + emit CertificateSigned(tokenId, signature); + } + function endorseAndTransfer(uint256 tokenId, Endorsement memory newEndorsement, address from, address to) external { addEndorsement(tokenId, newEndorsement); _transfer(from, to, tokenId); @@ -215,8 +235,9 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { // Clear agreement details delete CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId]; + delete CyberCertPrinterStorage.cyberCertStorage().issuerSignatures[tokenId]; } - + /** * @dev Override _update to enforce transferability restrictions * This function is called for all token transfers, mints, and burns @@ -291,9 +312,16 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { // Get full agreement details function getCertificateDetails(uint256 tokenId) external view returns (CertificateDetails memory) { if (ownerOf(tokenId) == address(0)) revert TokenDoesNotExist(); - return CyberCertPrinterStorage.cyberCertStorage().certificateDetails[tokenId]; + return CyberCertPrinterStorage.getCertificateDetails(tokenId); } - + + function getActiveCertificateDetails( + uint256 tokenId + ) external view returns (CertificateDetails memory) { + if (ownerOf(tokenId) == address(0)) revert TokenDoesNotExist(); + return CyberCertPrinterStorage.getActiveCertificateDetails(tokenId); + } + // Get endorsement history function getEndorsementHistory(uint256 tokenId, uint256 index) external view returns ( Endorsement memory details @@ -302,10 +330,32 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { details = CyberCertPrinterStorage.cyberCertStorage().endorsements[tokenId][index]; } + function getIssuerSignatureCount(uint256 tokenId) external view returns (uint256) { + if (!_exists(tokenId)) revert TokenDoesNotExist(); + return CyberCertPrinterStorage.cyberCertStorage().issuerSignatures[tokenId].length; + } + + function getIssuerSignatureAt(uint256 tokenId, uint256 index) external view returns (bytes memory) { + if (!_exists(tokenId)) revert TokenDoesNotExist(); + return CyberCertPrinterStorage.cyberCertStorage().issuerSignatures[tokenId][index]; + } + function voidCert(uint256 tokenId) external onlyIssuanceManager { CyberCertPrinterStorage.setSecurityStatus(tokenId, SecurityStatus.Void); emit CertificateVoided(tokenId, block.timestamp); } + + function unvoidCert(uint256 tokenId) external onlyIssuanceManager { + if (!_exists(tokenId)) revert TokenDoesNotExist(); + CyberCertPrinterStorage.setSecurityStatus(tokenId, SecurityStatus.Assigned); + } + + function isVoided(uint256 tokenId) external view returns (bool) { + if (ownerOf(tokenId) == address(0)) revert TokenDoesNotExist(); + return + CyberCertPrinterStorage.getSecurityStatus(tokenId) == + SecurityStatus.Void; + } // URI storage functionality function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { @@ -461,4 +511,9 @@ contract CyberCertPrinter is Initializable, ERC721EnumerableUpgradeable { return CyberCertPrinterStorage.cyberCertStorage().tokenTransferable[tokenId]; } + function legalOwnerOf(uint256 tokenId) external view returns (address) { + if (!_exists(tokenId)) revert TokenDoesNotExist(); + return CyberCertPrinterStorage.cyberCertStorage().owners[tokenId].ownerAddress; + } + } diff --git a/src/CyberCorp.sol b/src/CyberCorp.sol index 72cd4a0c..31813fa4 100644 --- a/src/CyberCorp.sol +++ b/src/CyberCorp.sol @@ -50,7 +50,7 @@ import "./interfaces/ICyberCorpSingleFactory.sol"; /// @notice Main contract representing a corporation's on-chain presence and management /// @dev Implements UUPS upgradeable pattern and BorgAuth access control contract CyberCorp is Initializable, BorgAuthACL, UUPSUpgradeable { - string public constant DEPLOY_VERSION = "3"; // For version-tracking on all deployment and future upgrades + string public constant DEPLOY_VERSION = "4"; // For version-tracking on all deployment and future upgrades // cyberCORP details /// @notice Legal name of the entity, including designation (e.g., "Inc." or "LLC") @@ -78,13 +78,19 @@ contract CyberCorp is Initializable, BorgAuthACL, UUPSUpgradeable { /// @notice Address of the round manager contract address public roundManager; + /// @notice Escrowed officer signatures that can be applied to certificates + bytes[] public escrowedOfficerSignatures; event CyberCORPDetailsUpdated(string cyberCORPName, string cyberCORPType, string cyberCORPJurisdiction, string cyberCORPContactDetails, string defaultDisputeResolution); event OfficerAdded(address indexed officer, uint256 index); event OfficerRemoved(address indexed officer, uint256 index); event CompanyPayableUpdated(address indexed companyPayable, address indexed oldCompanyPayable); + event EscrowedOfficerSignatureAdded(uint256 indexed index, address indexed officer); + event EscrowedOfficerSignatureUpdated(uint256 indexed index, address indexed officer); error NotRefImplementation(); + error SignatureRequired(); + error InvalidEscrowSignatureIndex(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -221,6 +227,42 @@ contract CyberCorp is Initializable, BorgAuthACL, UUPSUpgradeable { emit CompanyPayableUpdated(companyPayable, oldCompanyPayable); } + /// @notice Adds a reusable escrowed officer signature + /// @dev Officer role (200+) required + function addEscrowedOfficerSignature(bytes calldata signature) external onlyRole(200) { + if (signature.length == 0) revert SignatureRequired(); + escrowedOfficerSignatures.push(signature); + emit EscrowedOfficerSignatureAdded( + escrowedOfficerSignatures.length - 1, + msg.sender + ); + } + + /// @notice Updates an existing reusable escrowed officer signature + /// @dev Officer role (200+) required + function setEscrowedOfficerSignature( + uint256 index, + bytes calldata signature + ) external onlyRole(200) { + if (index >= escrowedOfficerSignatures.length) revert InvalidEscrowSignatureIndex(); + if (signature.length == 0) revert SignatureRequired(); + escrowedOfficerSignatures[index] = signature; + emit EscrowedOfficerSignatureUpdated(index, msg.sender); + } + + /// @notice Reads an escrowed officer signature by index + function getEscrowedOfficerSignature( + uint256 index + ) external view returns (bytes memory) { + if (index >= escrowedOfficerSignatures.length) revert InvalidEscrowSignatureIndex(); + return escrowedOfficerSignatures[index]; + } + + /// @notice Gets total escrowed officer signature count + function getEscrowedOfficerSignatureCount() external view returns (uint256) { + return escrowedOfficerSignatures.length; + } + // ======================== // UUPSUpgradeable // ======================== diff --git a/src/CyberCorpConstants.sol b/src/CyberCorpConstants.sol index ae40540b..534ce81e 100644 --- a/src/CyberCorpConstants.sol +++ b/src/CyberCorpConstants.sol @@ -66,7 +66,8 @@ enum SecuritySeries { SeriesD, SeriesE, SeriesF, - NA + NA, + ACE } enum SecurityStatus { @@ -107,6 +108,20 @@ enum UnlockingIntervalType { monthly } +struct CertificateSVGParams { + string corpName; + SecurityClass securityType; + SecuritySeries securitySeries; + string officerName; + string officerTitle; + uint256 units; + uint256 valuation; + string jurisdiction; + string ownerName; + uint256 tokenId; + string certificateUri; +} + diff --git a/src/CyberCorpFactory.sol b/src/CyberCorpFactory.sol index f5d79942..d8843f5f 100644 --- a/src/CyberCorpFactory.sol +++ b/src/CyberCorpFactory.sol @@ -411,7 +411,8 @@ contract CyberCorpFactory is UUPSUpgradeable, BorgAuthACL { uint256 startTime, uint256 endTime, bool publicRound, - bool allowTimedOffers + bool allowTimedOffers, + bool restrictEndTimeReduction ) external returns ( @@ -455,6 +456,7 @@ contract CyberCorpFactory is UUPSUpgradeable, BorgAuthACL { roundType, publicRound, allowTimedOffers, + restrictEndTimeReduction, raiseCap, minTicket, maxTicket, diff --git a/src/CyberScrip.sol b/src/CyberScrip.sol index b338a958..f419c03b 100644 --- a/src/CyberScrip.sol +++ b/src/CyberScrip.sol @@ -11,15 +11,19 @@ import "./storage/CyberScripStorage.sol"; contract CyberScrip is Initializable, ERC20Upgradeable, BorgAuthACL { using CyberScripStorage for CyberScripStorage.StorageData; - string public constant DEPLOY_VERSION = "3"; // For version-tracking on all deployment and future upgrades + string public constant DEPLOY_VERSION = "4"; // For version-tracking on all deployment and future upgrades error RestrictedTransfer(string reason); error NotIssuanceManager(); error ComplianceFeatureDisabled(); error AccountFrozen(address account); + error HolderLimitExceeded(uint256 limit); event FreezeStatusUpdated(address indexed account, bool frozen); event ComplianceFeatureDisabledEvent(string feature); + event ForceTransfer(address indexed from, address indexed to, uint256 amount); + event ForceBurn(address indexed account, uint256 amount); + event MaxHolderCountUpdated(uint256 maxHolderCount); modifier onlyIssuanceManager() { if (msg.sender != CyberScripStorage.getStorageData().issuanceManager) revert NotIssuanceManager(); @@ -31,6 +35,7 @@ contract CyberScrip is Initializable, ERC20Upgradeable, BorgAuthACL { } function initialize( + address _auth, address _certPrinter, address _issuanceManager, string memory _name, @@ -41,6 +46,7 @@ contract CyberScrip is Initializable, ERC20Upgradeable, BorgAuthACL { bool _enableFreeze ) external initializer { __ERC20_init(_name, _symbol); + __BorgAuthACL_init(_auth); CyberScripStorage.StorageData storage s = CyberScripStorage.getStorageData(); s.certPrinter = _certPrinter; @@ -53,19 +59,76 @@ contract CyberScrip is Initializable, ERC20Upgradeable, BorgAuthACL { function _update(address from, address to, uint256 amount) internal virtual override { CyberScripStorage.StorageData storage s = CyberScripStorage.getStorageData(); + uint256 fromBalanceBefore = 0; + uint256 toBalanceBefore = 0; + + if (from != address(0)) { + fromBalanceBefore = balanceOf(from); + } + if (to != address(0)) { + toBalanceBefore = balanceOf(to); + } + // Enforce freeze checks for normal transfers (not mint/burn) - if (from != address(0) && to != address(0)) { + if (to != address(0)) { if (s.canFreeze) { if (s.frozen[from]) revert AccountFrozen(from); if (s.frozen[to]) revert AccountFrozen(to); } // Enforce transfer restriction hooks - for (uint256 i = 0; i < s.transferRestrictionHooks.length; i++) { + uint256 length = s.transferRestrictionHooks.length; + for (uint256 i = 0; i < length; i++) { (bool allowed, string memory reason) = s.transferRestrictionHooks[i].checkTransferRestriction(from, to, amount, ""); if (!allowed) revert RestrictedTransfer(reason); } } + + if (amount > 0 && from != to) { + uint256 holderDelta = s.holderCount; + bool decrementHolder = from != address(0) && fromBalanceBefore == amount; + bool incrementHolder = to != address(0) && toBalanceBefore == 0; + + if (decrementHolder) { + holderDelta -= 1; + } + if (incrementHolder) { + holderDelta += 1; + } + + if (s.maxHolderCount > 0 && holderDelta > s.maxHolderCount) { + revert HolderLimitExceeded(s.maxHolderCount); + } + } + super._update(from, to, amount); + + _updateHolderCount(from, to, amount, fromBalanceBefore, toBalanceBefore); + } + + /// @dev Updates holderCount after a token transfer/mint/burn has been executed. + /// Must be called with the balances captured BEFORE the transfer. + function _updateHolderCount( + address from, + address to, + uint256 amount, + uint256 fromBalanceBefore, + uint256 toBalanceBefore + ) private { + if (amount > 0 && from != to) { + CyberScripStorage.StorageData storage s = CyberScripStorage.getStorageData(); + bool decrementHolder = from != address(0) && fromBalanceBefore == amount; + bool incrementHolder = to != address(0) && toBalanceBefore == 0; + if (decrementHolder) { + s.holderCount -= 1; + } + if (incrementHolder) { + s.holderCount += 1; + } + } + } + + function mint(address to, uint256 amount) public virtual onlyIssuanceManager { + super._mint(to, amount); } function burnFrom(address account, uint256 amount) public virtual onlyIssuanceManager { @@ -106,6 +169,12 @@ contract CyberScrip is Initializable, ERC20Upgradeable, BorgAuthACL { } } + function setMaxHolderCount(uint256 maxHolders) external onlyIssuanceManager { + CyberScripStorage.StorageData storage s = CyberScripStorage.getStorageData(); + s.maxHolderCount = maxHolders; + emit MaxHolderCountUpdated(maxHolders); + } + // Freeze/unfreeze an account (only if freezing is enabled) function setFrozen(address account, bool isFrozen) external onlyIssuanceManager { CyberScripStorage.StorageData storage s = CyberScripStorage.getStorageData(); @@ -118,46 +187,134 @@ contract CyberScrip is Initializable, ERC20Upgradeable, BorgAuthACL { function forceTransfer(address from, address to, uint256 amount) external onlyIssuanceManager { if (!CyberScripStorage.getStorageData().canForceTransfer) revert ComplianceFeatureDisabled(); require(from != address(0) && to != address(0), "force: zero addr"); + uint256 fromBalanceBefore = balanceOf(from); + uint256 toBalanceBefore = balanceOf(to); // Bypass our override and hooks by calling the base ERC20 implementation directly ERC20Upgradeable._update(from, to, amount); + // Still maintain accurate holder count tracking + _updateHolderCount(from, to, amount, fromBalanceBefore, toBalanceBefore); + emit ForceTransfer(from, to, amount); } // Force burn ignoring hooks and freezes (only if feature enabled) function forceBurn(address account, uint256 amount) external onlyIssuanceManager { if (!CyberScripStorage.getStorageData().canForceBurn) revert ComplianceFeatureDisabled(); require(account != address(0), "forceBurn: zero addr"); + uint256 accountBalanceBefore = balanceOf(account); ERC20Upgradeable._update(account, address(0), amount); + // Still maintain accurate holder count tracking + _updateHolderCount(account, address(0), amount, accountBalanceBefore, 0); + emit ForceBurn(account, amount); } // ======================== // Getter / Setter // ======================== - function certPrinter() external returns (address) { + function certPrinter() external view returns (address) { return CyberScripStorage.getStorageData().certPrinter; } - function issuanceManager() external returns (address) { + function issuanceManager() external view returns (address) { return CyberScripStorage.getStorageData().issuanceManager; } - function transferRestrictionHooks(uint256 i) external returns (ITransferRestrictionHook) { + function transferRestrictionHooks(uint256 i) external view returns (ITransferRestrictionHook) { return CyberScripStorage.getStorageData().transferRestrictionHooks[i]; } - function canForceTransfer() external returns (bool) { + function transferRestrictionHooksLength() external view returns (uint256) { + return CyberScripStorage.getStorageData().transferRestrictionHooks.length; + } + + function canForceTransfer() external view returns (bool) { return CyberScripStorage.getStorageData().canForceTransfer; } - function canForceBurn() external returns (bool) { + function canForceBurn() external view returns (bool) { return CyberScripStorage.getStorageData().canForceBurn; } - function canFreeze() external returns (bool) { + function canFreeze() external view returns (bool) { return CyberScripStorage.getStorageData().canFreeze; } - function frozen(address account) external returns (bool) { + function frozen(address account) external view returns (bool) { return CyberScripStorage.getStorageData().frozen[account]; } + + function holderCount() external view returns (uint256) { + return CyberScripStorage.getStorageData().holderCount; + } + + function maxHolderCount() external view returns (uint256) { + return CyberScripStorage.getStorageData().maxHolderCount; + } + + function canTransfer( + address from, + address to, + uint256 amount + ) external view returns (bool) { + CyberScripStorage.StorageData storage s = CyberScripStorage + .getStorageData(); + if (amount == 0 || from == to) { + return true; + } + if (from == address(0) || to == address(0)) { + return true; + } + if (s.canFreeze) { + if (s.frozen[from]) return false; + if (s.frozen[to]) return false; + } + uint256 length = s.transferRestrictionHooks.length; + for (uint256 i = 0; i < length; i++) { + (bool allowed, ) = s.transferRestrictionHooks[i] + .checkTransferRestriction(from, to, amount, ""); + if (!allowed) return false; + } + + uint256 fromBalanceBefore = balanceOf(from); + uint256 toBalanceBefore = balanceOf(to); + uint256 holderDelta = s.holderCount; + bool decrementHolder = fromBalanceBefore == amount; + bool incrementHolder = toBalanceBefore == 0; + if (decrementHolder) { + holderDelta -= 1; + } + if (incrementHolder) { + holderDelta += 1; + } + if (s.maxHolderCount > 0 && holderDelta > s.maxHolderCount) { + return false; + } + return true; + } + + function willCreateNewHolder( + address to, + uint256 amount + ) external view returns (bool) { + if (amount == 0 || to == address(0)) { + return false; + } + return balanceOf(to) == 0; + } + + function currentHolderCount() external view returns (uint256) { + return CyberScripStorage.getStorageData().holderCount; + } + + function remainingSlots() external view returns (uint256) { + CyberScripStorage.StorageData storage s = CyberScripStorage + .getStorageData(); + if (s.maxHolderCount == 0) { + return type(uint256).max; + } + if (s.holderCount >= s.maxHolderCount) { + return 0; + } + return s.maxHolderCount - s.holderCount; + } } diff --git a/src/DealManager.sol b/src/DealManager.sol index 55ca5301..6af09b84 100644 --- a/src/DealManager.sol +++ b/src/DealManager.sol @@ -59,7 +59,7 @@ import "./interfaces/IDealManagerFactory.sol"; contract DealManager is Initializable, BorgAuthACL, LexScroWLite, UUPSUpgradeable, ReentrancyGuard { using DealManagerStorage for DealManagerStorage.DealManagerData; - string public constant DEPLOY_VERSION = "3"; // For version-tracking on all deployment and future upgrades + string public constant DEPLOY_VERSION = "4"; // For version-tracking on all deployment and future upgrades /// @notice Certificate data structure for creating new certificates struct CyberCertData { diff --git a/src/IssuanceManager.sol b/src/IssuanceManager.sol index 9cc5cf38..98382510 100644 --- a/src/IssuanceManager.sol +++ b/src/IssuanceManager.sol @@ -42,15 +42,10 @@ except with the express prior written permission of the copyright holder.*/ pragma solidity 0.8.28; import "./libs/auth.sol"; -import "openzeppelin-contracts/proxy/beacon/BeaconProxy.sol"; import "openzeppelin-contracts/proxy/beacon/UpgradeableBeacon.sol"; -import "openzeppelin-contracts/utils/Create2.sol"; -import "openzeppelin-contracts/utils/Address.sol"; import "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; import "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import "./interfaces/ICyberCertPrinter.sol"; import "./interfaces/ITransferRestrictionHook.sol"; -import "./interfaces/ICyberScrip.sol"; import "./interfaces/ICertificateConverter.sol"; import "./interfaces/IIssuanceManagerFactory.sol"; @@ -60,24 +55,28 @@ import "./storage/IssuanceManagerStorage.sol"; /// @notice Manages the issuance and lifecycle of digital certificates representing securities and more /// @dev Implements UUPS upgradeable pattern and BorgAuth access control contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { - using IssuanceManagerStorage for IssuanceManagerStorage.IssuanceManagerData; - - string public constant DEPLOY_VERSION = "3"; // For version-tracking on all deployment and future upgrades + string public constant DEPLOY_VERSION = "4"; // For version-tracking on all deployment and future upgrades // IssuanceManager errors error CompanyDetailsNotSet(); - error SignatureURIRequired(); + error SignatureRequired(); error TokenProxyNotFound(); error NotSAFEToken(); error NotUpgradeFactory(); error ScripifiedCertNotAllowed(); error ConditionCheckFailed(); error NotRefImplementation(); - + error InvalidScripRatio(); + error ScripToCertMinimumNotMet(); + error ScripifyNotWhitelisted(); + error RecertificationApprovalRequired(); + error InvalidInvestor(); + error InvalidInvestorName(); event ScripifiedCert( address indexed certAddress, uint256 indexed id, - address indexed scripifiedCert + address indexed scripifiedCert, + uint256 amount ); event CertPrinterCreated( @@ -101,6 +100,46 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { event CompanyDetailsUpdated(string companyName, string jurisdiction); event CertPrinterBeaconImplementationUpgraded(address implementation); event ScripBeaconImplementationUpgraded(address implementation); + event ScripToCertMinimumSet(address indexed certAddress, uint256 minimum); + event ScripifyWhitelistEnabledSet(address indexed certAddress, bool enabled); + event ScripifyWhitelistUpdated( + address indexed certAddress, + uint256 indexed id, + bool isWhitelisted + ); + event CyberScripDeployed( + address indexed certPrinterAddress, + address indexed cyberScripAddress, + uint256 scripRatioNumerator, + uint256 scripRatioDenominator, + bool enableForceTransfer, + bool enableForceBurn, + bool enableFreeze + ); + event RecertificationApprovalSet( + address indexed certAddress, + address indexed investor, + string investorName + ); + event RecertificationApprovalCleared( + address indexed certAddress, + address indexed investor + ); + event ScripRecertified( + address indexed certAddress, + address indexed user, + uint256 indexed certId, + uint256 scripAmount, + uint256 oldUnitsRepresented, + uint256 newUnitsRepresented + ); + event ScripAddedToExistingCert( + address indexed certAddress, + address indexed user, + uint256 indexed certId, + uint256 oldUnitsRepresented, + uint256 newUnitsRepresented + ); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -126,14 +165,17 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { // share the same implementation or upgraded to a new version all at the same time. // Maintenance-wise, since IssuanceManager itself is upgradeable, we don't need to worry about beacon ownership transfers - address cyberCertPrinterRefImpl = IIssuanceManagerFactory(_upgradeFactory).getCyberCertPrinterRefImplementation(); + address cyberCertPrinterRefImpl = IIssuanceManagerFactory( + _upgradeFactory + ).getCyberCertPrinterRefImplementation(); UpgradeableBeacon beaconCertPrinter = new UpgradeableBeacon( cyberCertPrinterRefImpl, address(this) ); emit CertPrinterBeaconImplementationUpgraded(cyberCertPrinterRefImpl); - address cyberScripRefImpl = IIssuanceManagerFactory(_upgradeFactory).getCyberScripRefImplementation(); + address cyberScripRefImpl = IIssuanceManagerFactory(_upgradeFactory) + .getCyberScripRefImplementation(); UpgradeableBeacon beaconScrip = new UpgradeableBeacon( cyberScripRefImpl, address(this) @@ -161,6 +203,14 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { _; } + /// @dev Restricts execution to contract itself or AUTH.OWNER_ROLE callers + modifier onlyOwnerOrSelf() { + if (msg.sender != address(this)) { + AUTH.onlyRole(AUTH.OWNER_ROLE(), msg.sender); + } + _; + } + /// @notice Creates a new certificate printer contract /// @dev Only callable by owner /// @param _ledger Array of default restrictive ledgers for a certificate @@ -179,35 +229,16 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { SecuritySeries _securitySeries, address _extension ) public onlyOwner returns (address) { - bytes32 salt = keccak256( - abi.encodePacked( - IssuanceManagerStorage.getPrinters().length, - address(this) - ) - ); - address newCert = Create2.deploy(0, salt, _getBytecodeCertPrinter()); - IssuanceManagerStorage.addPrinter(newCert); - ICyberCertPrinter(newCert).initialize( + return + IssuanceManagerStorage.executeCreateCertPrinter( _ledger, _name, _ticker, _certificateUri, - address(this), _securityType, _securitySeries, _extension ); - emit CertPrinterCreated( - newCert, - IssuanceManagerStorage.getCORP(), - _ledger, - _name, - _ticker, - _securityType, - _securitySeries, - _certificateUri - ); - return newCert; } /// @notice Creates a new certificate @@ -221,19 +252,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address to, CertificateDetails memory _details ) public onlyOwner returns (uint256) { - ICyberCertPrinter cert = ICyberCertPrinter(certAddress); - uint256 tokenId = cert.totalSupply(); - uint256 id = cert.safeMint(tokenId, to, _details); - string memory tokenURI = cert.tokenURI(tokenId); - emit CertificateCreated( - tokenId, - certAddress, - _details.investmentAmountUSD, - _details.issuerUSDValuationAtTimeOfInvestment, - _details, - tokenURI - ); - return id; + return IssuanceManagerStorage.executeCreateCert(certAddress, to, _details); } /// @notice Assigns an existing certificate to a new investor @@ -250,8 +269,13 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address investor, CertificateDetails memory _details ) public onlyOwner { - ICyberCertPrinter cert = ICyberCertPrinter(certAddress); - cert.assignCert(from, tokenId, investor, _details); + IssuanceManagerStorage.executeAssignCert( + certAddress, + from, + tokenId, + investor, + _details + ); } /// @notice Creates and assigns a new certificate in one transaction @@ -264,41 +288,100 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address certAddress, address investor, CertificateDetails memory _details - ) public onlyOwner returns (uint256 tokenId) { - if ( - bytes(ICyberCorp(IssuanceManagerStorage.getCORP()).cyberCORPName()) - .length == 0 - ) revert CompanyDetailsNotSet(); - ICyberCertPrinter cert = ICyberCertPrinter(certAddress); - tokenId = cert.totalSupply(); - - cert.safeMintAndAssign(investor, tokenId, _details); - string memory tokenURI = cert.tokenURI(tokenId); - emit CertificateCreated( - tokenId, - certAddress, - _details.investmentAmountUSD, - _details.issuerUSDValuationAtTimeOfInvestment, - _details, - tokenURI - ); - return tokenId; + ) public onlyOwnerOrSelf returns (uint256 tokenId) { + return + createCertAndAssignWithName( + certAddress, + investor, + _details, + "", + bytes(""), + block.timestamp + ); + } + + function createCertAndAssignWithName( + address certAddress, + address investor, + CertificateDetails memory _details, + string memory investorName, + bytes memory endorsementSignature, + uint256 timestamp + ) public onlyOwnerOrSelf returns (uint256 tokenId) { + return + IssuanceManagerStorage.executeCreateCertAndAssign( + certAddress, + investor, + _details, + investorName, + endorsementSignature, + timestamp + ); + } + + /// @notice Creates, assigns, signs, and endorses a new certificate in one transaction + /// @dev Only callable by owner/self, requires company details to be set + /// @param certAddress Address of the certificate printer contract + /// @param investor Recipient of the certificate + /// @param _details Certificate details + /// @param endorsementSignature Signature hash to store in endorsement and cert signatures + /// @param registry Optional source registry associated with endorsement + /// @param agreementId Optional agreement id associated with endorsement + /// @param investorName Human-readable investor name stored in endorsement + /// @return tokenId ID of the new certificate + function createCertSignAndAssign( + address certAddress, + address investor, + CertificateDetails memory _details, + bytes memory endorsementSignature, + address registry, + bytes32 agreementId, + string memory investorName + ) public onlyOwnerOrSelf returns (uint256 tokenId) { + return + IssuanceManagerStorage.executeCreateCertSignAndAssign( + certAddress, + investor, + _details, + endorsementSignature, + registry, + agreementId, + investorName + ); } /// @notice Adds an issuer's signature to a certificate - /// @dev Only callable by admin, requires valid signature URI + /// @dev Only callable by admin, requires non-empty signature bytes /// @param certAddress Address of the certificate printer contract /// @param tokenId ID of the certificate - /// @param signatureURI URI containing the signature data + /// @param signature Signed hash payload function signCertificate( address certAddress, uint256 tokenId, - string calldata signatureURI + bytes calldata signature ) external onlyAdmin { - if (bytes(signatureURI).length == 0) revert SignatureURIRequired(); + IssuanceManagerStorage.executeAddIssuerSignature( + certAddress, + tokenId, + signature + ); + } - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.addIssuerSignature(tokenId, signatureURI); + /// @notice Adds an officer signature to a certificate + /// @dev Alias maintained for IIssuanceManager compatibility + /// @param certAddress Address of the certificate printer contract + /// @param tokenId ID of the certificate + /// @param signature Signed hash payload + function addOfficerSignature( + address certAddress, + uint256 tokenId, + bytes calldata signature + ) external onlyAdmin { + IssuanceManagerStorage.executeAddIssuerSignature( + certAddress, + tokenId, + signature + ); } /// @notice Adds an endorsement for secondary market transfer @@ -315,20 +398,16 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { bytes memory signature, bytes32 agreementId ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - Endorsement memory newEndorsement = Endorsement( + IssuanceManagerStorage.executeEndorseCertificate( + certAddress, + tokenId, endorser, - block.timestamp, signature, - address(0), - agreementId, - address(0), - "" + agreementId ); - certificate.addEndorsement(tokenId, newEndorsement); } - /// @notice Updates the details of an existing certificate + /* /// @notice Updates the details of an existing certificate /// @dev Only callable by admin /// @param certAddress Address of the certificate printer contract /// @param tokenId ID of the certificate @@ -340,7 +419,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { ) external onlyAdmin { ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); certificate.updateCertificateDetails(tokenId, _details); - } + }*/ /// @notice Voids a certificate /// @dev Only callable by admin @@ -350,8 +429,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address certAddress, uint256 tokenId ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.voidCert(tokenId); + IssuanceManagerStorage.executeVoidCertificate(certAddress, tokenId); } /// @notice Sets the global transferability status for a certificate contract @@ -362,8 +440,10 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address certAddress, bool transferable ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.setGlobalTransferable(transferable); + IssuanceManagerStorage.executeSetGlobalTransferable( + certAddress, + transferable + ); } /// @notice Upgrades the implementation of the certificate printer @@ -372,19 +452,25 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { function upgradeCertPrinterBeaconImplementation( address _newImplementation ) external onlyOwner { - if( - IIssuanceManagerFactory( - IssuanceManagerStorage.getUpgradeFactory() - ).getCyberCertPrinterRefImplementation() != _newImplementation) { + if ( + IIssuanceManagerFactory(IssuanceManagerStorage.getUpgradeFactory()) + .getCyberCertPrinterRefImplementation() != _newImplementation + ) { revert NotRefImplementation(); } - IssuanceManagerStorage.upgradeCertPrinterBeaconImplementation(_newImplementation); + IssuanceManagerStorage.upgradeCertPrinterBeaconImplementation( + _newImplementation + ); emit CertPrinterBeaconImplementationUpgraded(_newImplementation); } /// @notice Gets the current implementation address of the certificate printer /// @return address Current implementation address - function getCertPrinterBeaconImplementation() external view returns (address) { + function getCertPrinterBeaconImplementation() + external + view + returns (address) + { return IssuanceManagerStorage.getCyberCertPrinterBeacon().implementation(); } @@ -395,38 +481,20 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { function upgradeScripBeaconImplementation( address _newImplementation ) external onlyOwner { - if( - IIssuanceManagerFactory( - IssuanceManagerStorage.getUpgradeFactory() - ).getCyberScripRefImplementation() != _newImplementation) { + if ( + IIssuanceManagerFactory(IssuanceManagerStorage.getUpgradeFactory()) + .getCyberScripRefImplementation() != _newImplementation + ) { revert NotRefImplementation(); } - IssuanceManagerStorage.updateScripBeaconImplementation(_newImplementation); + IssuanceManagerStorage.updateScripBeaconImplementation( + _newImplementation + ); emit ScripBeaconImplementationUpgraded(_newImplementation); } function getScripBeaconImplementation() external view returns (address) { - return - IssuanceManagerStorage.getCyberScripBeacon().implementation(); - } - - /// @notice Gets the bytecode for creating new certificate printer proxies - /// @dev Internal function used by createCertPrinter - /// @return bytecode The proxy contract creation bytecode - function _getBytecodeCertPrinter() private view returns (bytes memory bytecode) { - bytes memory sourceCodeBytes = type(BeaconProxy).creationCode; - bytecode = abi.encodePacked( - sourceCodeBytes, - abi.encode(IssuanceManagerStorage.getCyberCertPrinterBeacon(), "") - ); - } - - function _getBytecodeScrip() private view returns (bytes memory bytecode) { - bytes memory sourceCodeBytes = type(BeaconProxy).creationCode; - bytecode = abi.encodePacked( - sourceCodeBytes, - abi.encode(IssuanceManagerStorage.getCyberScripBeacon(), "") - ); + return IssuanceManagerStorage.getCyberScripBeacon().implementation(); } /// @notice Gets the company name from the CyberCorp contract @@ -467,11 +535,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { /// @notice Gets the scrip beacon contract /// @return UpgradeableBeacon The beacon contract - function cyberScripBeacon() - external - view - returns (UpgradeableBeacon) - { + function cyberScripBeacon() external view returns (UpgradeableBeacon) { return IssuanceManagerStorage.getCyberScripBeacon(); } @@ -489,6 +553,30 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { IssuanceManagerStorage.setUriBuilder(_uriBuilder); } + function setScripRatio( + address certAddress, + uint256 numerator, + uint256 denominator + ) external onlyOwner { + IssuanceManagerStorage.executeSetScripRatio( + certAddress, + numerator, + denominator + ); + } + + function getScripRatio( + address certAddress + ) external view returns (uint256 numerator, uint256 denominator) { + IssuanceManagerStorage.ScripRatio storage ratio = IssuanceManagerStorage + .getScripRatio(certAddress); + numerator = ratio.numerator; + denominator = ratio.denominator; + if (numerator == 0 || denominator == 0) { + return (1, 1); + } + } + /// @notice Sets a restriction hook for a specific certificate /// @dev Only callable by admin /// @param certAddress Address of the certificate printer contract @@ -499,8 +587,11 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { uint256 _id, address _hookAddress ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.setRestrictionHook(_id, _hookAddress); + IssuanceManagerStorage.executeSetRestrictionHook( + certAddress, + _id, + _hookAddress + ); } /// @notice Sets a global restriction hook for a certificate contract @@ -511,8 +602,10 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address certAddress, address hookAddress ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.setGlobalRestrictionHook(hookAddress); + IssuanceManagerStorage.executeSetGlobalRestrictionHook( + certAddress, + hookAddress + ); } function setTokenTransferable( @@ -520,8 +613,57 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { uint256 tokenId, bool value ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.setTokenTransferable(tokenId, value); + IssuanceManagerStorage.executeSetTokenTransferable( + certAddress, + tokenId, + value + ); + } + + /// @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 + /// @param minimum Minimum amount required for scrip-to-cert conversion + function setScripToCertMinimum( + address certAddress, + uint256 minimum + ) external onlyOwner { + IssuanceManagerStorage.executeSetScripToCertMinimum( + certAddress, + minimum + ); + } + + function setScripifyWhitelistEnabled( + address certAddress, + bool enabled + ) external onlyOwner { + IssuanceManagerStorage.executeSetScripifyWhitelistEnabled( + certAddress, + enabled + ); + } + + function addScripifyWhitelistIds( + address certAddress, + uint256[] memory ids + ) external onlyOwner { + IssuanceManagerStorage.executeSetScripifyWhitelistIds( + certAddress, + ids, + true + ); + } + + function removeScripifyWhitelistIds( + address certAddress, + uint256[] memory ids + ) external onlyOwner { + IssuanceManagerStorage.executeSetScripifyWhitelistIds( + certAddress, + ids, + false + ); } /// @notice Adds a default legend to a certificate contract @@ -532,8 +674,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address certAddress, string memory newLegend ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.addDefaultLegend(newLegend); + IssuanceManagerStorage.executeAddDefaultLegend(certAddress, newLegend); } /// @notice Removes a default legend from a certificate contract @@ -544,8 +685,7 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address certAddress, uint256 index ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.removeDefaultLegendAt(index); + IssuanceManagerStorage.executeRemoveDefaultLegendAt(certAddress, index); } /// @notice Adds a legend to a specific certificate @@ -558,8 +698,11 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { uint256 tokenId, string memory newLegend ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.addCertLegend(tokenId, newLegend); + IssuanceManagerStorage.executeAddCertLegend( + certAddress, + tokenId, + newLegend + ); } /// @notice Removes a legend from a specific certificate @@ -572,8 +715,11 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { uint256 tokenId, uint256 index ) external onlyAdmin { - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - certificate.removeCertLegendAt(tokenId, index); + IssuanceManagerStorage.executeRemoveCertLegendAt( + certAddress, + tokenId, + index + ); } //deploy matching erc20 contract for a cert @@ -581,145 +727,230 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { address certAddress, ITransferRestrictionHook[] memory typeRestrictionHooks, ICondition[] memory certToScripConditions, - ICondition[] memory scripToCertConditions - // TODO TBD: changed to external for now but final design may change -// ) internal returns (address) { - ) external returns (address) { - bytes32 salt = keccak256(abi.encodePacked(certAddress, address(this))); - address newScrip = Create2.deploy(0, salt, _getBytecodeScrip()); - ICyberScrip(newScrip).initialize( + ICondition[] memory scripToCertConditions, + uint256 scripToCertMinimum, + uint256 scripRatioNumerator, + uint256 scripRatioDenominator, + uint256[] memory scripifyWhitelistIds, + bool scripifyWhitelistEnabled, + bool enableForceTransfer, + bool enableForceBurn, + bool enableFreeze + ) onlyOwner external returns (address) { + return + IssuanceManagerStorage.executeDeployCyberScrip( + certAddress, + address(AUTH), + typeRestrictionHooks, + certToScripConditions, + scripToCertConditions, + scripToCertMinimum, + scripRatioNumerator, + scripRatioDenominator, + scripifyWhitelistIds, + scripifyWhitelistEnabled, + enableForceTransfer, + enableForceBurn, + enableFreeze + ); + } + + function setScripRestrictionHooks( + address certAddress, + ITransferRestrictionHook[] memory hooks + ) external onlyAdmin { + IssuanceManagerStorage.executeSetScripRestrictionHooks( certAddress, - address(this), - string( - abi.encodePacked("scrip", ICyberCertPrinter(certAddress).name()) - ), - string( - abi.encodePacked("scrip", ICyberCertPrinter(certAddress).symbol()) - ), - typeRestrictionHooks, - // TODO TBD: placeholder just to make tests work - true, - true, - true + hooks + ); + } + + function disableScripForceTransfer(address certAddress) external onlyOwner { + IssuanceManagerStorage.executeDisableScripForceTransfer(certAddress); + } + + function disableScripForceBurn(address certAddress) external onlyOwner { + IssuanceManagerStorage.executeDisableScripForceBurn(certAddress); + } + + function disableScripFreeze(address certAddress) external onlyOwner { + IssuanceManagerStorage.executeDisableScripFreeze(certAddress); + } + + function setScripFrozen( + address certAddress, + address account, + bool isFrozen + ) external onlyAdmin { + IssuanceManagerStorage.executeSetScripFrozen( + certAddress, + account, + isFrozen + ); + } + + function forceScripTransfer( + address certAddress, + address from, + address to, + uint256 amount + ) external onlyAdmin { + IssuanceManagerStorage.executeForceScripTransfer( + certAddress, + from, + to, + amount + ); + } + + function forceScripBurn( + address certAddress, + address account, + uint256 amount + ) external onlyAdmin { + IssuanceManagerStorage.executeForceScripBurn( + certAddress, + account, + amount ); - IssuanceManagerStorage.setScripifiedCert(certAddress, newScrip); - IssuanceManagerStorage.setCertToScripConditions(certAddress, certToScripConditions); - IssuanceManagerStorage.setScripToCertConditions(certAddress, scripToCertConditions); - return newScrip; } /// @notice Convert a certificate into scrip tokens, partially or fully /// @param certAddress Address of the certificate printer contract /// @param id ID of the certificate to convert /// @param amount Number of units to convert into scrip - function scripifyCert(address certAddress, uint256 id, uint256 amount) external { - if (amount == 0) revert ConditionCheckFailed(); + function scripifyCert( + address certAddress, + uint256 id, + uint256 amount, + address target + ) external { + IssuanceManagerStorage.executeScripifyCert( + certAddress, + id, + amount, + target, + msg.sender + ); + } - address scripifiedCert = IssuanceManagerStorage.getScripifiedCert(certAddress); - if (scripifiedCert == address(0)) revert ScripifiedCertNotAllowed(); + function getUpgradeFactory() public view returns (address) { + return IssuanceManagerStorage.getUpgradeFactory(); + } - // Check all cert-to-scrip conditions - ICondition[] storage conditions = IssuanceManagerStorage.getCertToScripConditions(certAddress); - bytes4 selector3 = bytes4(keccak256("scripifyCert(address,uint256,uint256)")); - for (uint i = 0; i < conditions.length; i++) { - if (!conditions[i].checkCondition( - certAddress, - selector3, - abi.encode(id, amount) - )) { - revert ConditionCheckFailed(); - } - } + function getScripToCertMinimum(address certAddress) external view returns (uint256) { + return IssuanceManagerStorage.getScripToCertMinimum(certAddress); + } - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - if (certificate.ownerOf(id) != msg.sender) revert ConditionCheckFailed(); + function setRecertificationApproval( + address certAddress, + address investor, + string calldata investorName, + CertificateDetails calldata details + ) external onlyAdmin { + IssuanceManagerStorage.executeSetRecertificationApproval( + certAddress, + investor, + investorName, + details + ); + } - CertificateDetails memory details = certificate.getCertificateDetails(id); - if (amount > details.unitsRepresented) revert ConditionCheckFailed(); + function clearRecertificationApproval( + address certAddress, + address investor + ) external onlyAdmin { + IssuanceManagerStorage.executeClearRecertificationApproval( + certAddress, + investor + ); + } - if (amount == details.unitsRepresented) { - // Full conversion: transfer cert, void it, and mint full amount - certificate.safeTransferFrom( - msg.sender, - address(this), - id - ); - certificate.voidCert(id); - ICyberScrip(scripifiedCert).mint( - msg.sender, - details.unitsRepresented + function getRecertificationApproval( + address certAddress, + address investor + ) + external + view + returns ( + bool approved, + string memory investorName, + CertificateDetails memory details + ) + { + return + IssuanceManagerStorage.getRecertificationApprovalData( + certAddress, + investor ); - emit ScripifiedCert(certAddress, id, scripifiedCert); - return; - } + } - // Partial conversion: reduce units on the certificate and mint scrip for `amount` - details.unitsRepresented = details.unitsRepresented - amount; - certificate.updateCertificateDetails(id, details); - ICyberScrip(scripifiedCert).mint(msg.sender, amount); - emit ScripifiedCert(certAddress, id, scripifiedCert); + function getScripifyWhitelistEnabled( + address certAddress + ) external view returns (bool) { + return IssuanceManagerStorage.getScripifyWhitelistEnabled(certAddress); } - function getUpgradeFactory() public view returns (address) { - return IssuanceManagerStorage.getUpgradeFactory(); + function getCertScripifiedStatus( + address certAddress, + uint256 id + ) + external + view + returns (bool isScripified, uint256 scripifiedUnits, uint256 maxUnitsRepresented) + { + return IssuanceManagerStorage.getCertScripifiedStatus(certAddress, id); } - function convertScripToCert(address certAddress, uint256 amount) external { - address scripifiedCert = IssuanceManagerStorage.getScripifiedCert(certAddress); - if (scripifiedCert == address(0)) - revert ScripifiedCertNotAllowed(); - - // Check all scrip-to-cert conditions - ICondition[] storage conditions = IssuanceManagerStorage.getScripToCertConditions(certAddress); - for (uint i = 0; i < conditions.length; i++) { - if (!conditions[i].checkCondition( - certAddress, - this.convertScripToCert.selector, - abi.encode(amount) - )) { - revert ConditionCheckFailed(); - } - } + /// @notice `totalTrackedScrip` is CyberScrip `totalSupply`. Second value is vault price per + /// nominal share: `totalAssetsWad * 1e27 / totalNominalShares` (ray), or 0 if empty. + function getScripPoolTotals( + address certAddress + ) + external + view + returns (uint256 totalTrackedScrip, uint256 pricePerShareRay) + { + return IssuanceManagerStorage.getScripPoolTotals(certAddress); + } - // Check for voided certificates owned by the sender - ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); - uint256 balance = certificate.balanceOf(msg.sender); - bool foundVoided = false; - uint256 voidedTokenId; - CertificateDetails memory voidedDetails; - - for (uint256 i = 0; i < balance; i++) { - uint256 tokenId = certificate.tokenOfOwnerByIndex(msg.sender, i); - voidedDetails = certificate.getCertificateDetails(tokenId); - if (voidedDetails.unitsRepresented == amount) { - // Found a matching voided certificate, unvoid it - foundVoided = true; - voidedTokenId = tokenId; - break; - } - } + /// @notice Underlying units (wad) and total nominal shares in the scripified-units vault. + function getCertScripUnitVault(address certAddress) + external + view + returns (uint256 totalAssetsWad, uint256 totalNominalShares) + { + return IssuanceManagerStorage.getCertScripUnitVault(certAddress); + } - // Burn the scrip tokens - ICyberScrip(scripifiedCert).burnFrom(msg.sender, amount); - - if (foundVoided) { - // Unvoid the existing certificate by updating its details - certificate.updateCertificateDetails(voidedTokenId, voidedDetails); - } else { - // Create a new certificate if no matching voided one was found - - CertificateDetails memory details = CertificateDetails({ - signingOfficerName: "", // Can be set by admin later if needed - signingOfficerTitle: "", // Can be set by admin later if needed - investmentAmountUSD: 0, // Maintaining original value from scrip - issuerUSDValuationAtTimeOfInvestment: 0, // Maintaining original value from scrip - unitsRepresented: amount, - legalDetails: "", // Can be set by admin later if needed - extensionData: "" // Can be set by admin later if needed - }); - - createCertAndAssign(certAddress, msg.sender, details); - } + function getScripPoolAmountById( + address certAddress, + uint256 id + ) external view returns (uint256) { + return IssuanceManagerStorage.getScripPoolAmountById(certAddress, id); + } + + function getScripPoolSharesById( + address certAddress, + uint256 id + ) external view returns (uint256) { + return IssuanceManagerStorage.getScripPoolSharesById(certAddress, id); + } + + function isScripifyWhitelisted( + address certAddress, + uint256 id + ) external view returns (bool) { + return IssuanceManagerStorage.isScripifyWhitelisted(certAddress, id); + } + + function convertScripToCert(address certAddress, uint256 amount) external { + IssuanceManagerStorage.executeConvertScripToCert( + certAddress, + amount, + msg.sender, + this.convertScripToCert.selector + ); } /// @notice UUPS upgrade authorization @@ -728,11 +959,12 @@ contract IssuanceManager is Initializable, BorgAuthACL, UUPSUpgradeable { function _authorizeUpgrade( address newImplementation ) internal override onlyOwner { - if( - IIssuanceManagerFactory( - IssuanceManagerStorage.getUpgradeFactory() - ).getRefImplementation() != newImplementation) { + if ( + IIssuanceManagerFactory(IssuanceManagerStorage.getUpgradeFactory()) + .getRefImplementation() != newImplementation + ) { revert NotRefImplementation(); } } + } diff --git a/src/MetaDAOFactory.sol b/src/MetaDAOFactory.sol index bea26efa..ccc8ba77 100644 --- a/src/MetaDAOFactory.sol +++ b/src/MetaDAOFactory.sol @@ -68,10 +68,15 @@ interface IRoundManagerInit { ) external; } +interface ICyberCorpLocal { + function issuanceManager() external view returns (address); +} + contract MetaDAOFactory is UUPSUpgradeable, BorgAuthACL, IERC721Receiver { using Strings for string; error InvalidSalt(); + error RoundManagerAlreadyExists(); address public registryAddress; address public issuanceManagerFactory; @@ -125,6 +130,29 @@ contract MetaDAOFactory is UUPSUpgradeable, BorgAuthACL, IERC721Receiver { address roundManager ); + event IssuanceManagerFactoryUpdated( + address indexed issuanceManagerFactory, + address oldIssuanceFactory + ); + + event CyberCorpSingleFactoryUpdated( + address indexed cyberCorpSingleFactory, + address oldCyberCorpFactory + ); + + event DealManagerFactoryUpdated( + address indexed dealManagerFactory, + address oldDealFactory + ); + + event RoundManagerFactoryUpdated( + address indexed roundManagerFactory, + address oldRoundManagerFactory + ); + + event UriBuilderUpdated(address indexed uriBuilder, address oldUriBuilder); + event RegistryAddressUpdated(address indexed registryAddress, address oldRegistryAddress); + error GlobalOrPartyValuesMismatch(); error OfficerValuesMismatch(); @@ -178,6 +206,42 @@ contract MetaDAOFactory is UUPSUpgradeable, BorgAuthACL, IERC721Receiver { metaDAOOfficer.title = _title; } + function setIssuanceManagerFactory(address _issuanceManagerFactory) external onlyOwner { + address old = issuanceManagerFactory; + issuanceManagerFactory = _issuanceManagerFactory; + emit IssuanceManagerFactoryUpdated(_issuanceManagerFactory, old); + } + + function setCyberCorpSingleFactory(address _cyberCorpSingleFactory) external onlyOwner { + address old = cyberCorpSingleFactory; + cyberCorpSingleFactory = _cyberCorpSingleFactory; + emit CyberCorpSingleFactoryUpdated(_cyberCorpSingleFactory, old); + } + + function setDealManagerFactory(address _dealManagerFactory) external onlyOwner { + address old = dealManagerFactory; + dealManagerFactory = _dealManagerFactory; + emit DealManagerFactoryUpdated(_dealManagerFactory, old); + } + + function setRoundManagerFactory(address _roundManagerFactory) external onlyOwner { + address old = roundManagerFactory; + roundManagerFactory = _roundManagerFactory; + emit RoundManagerFactoryUpdated(_roundManagerFactory, old); + } + + function setUriBuilder(address _uriBuilder) external onlyOwner { + address old = uriBuilder; + uriBuilder = _uriBuilder; + emit UriBuilderUpdated(_uriBuilder, old); + } + + function setRegistryAddress(address _registryAddress) external onlyOwner { + address old = registryAddress; + registryAddress = _registryAddress; + emit RegistryAddressUpdated(_registryAddress, old); + } + function deployMetaCorp( bytes32 salt, string memory companyName, @@ -238,7 +302,7 @@ contract MetaDAOFactory is UUPSUpgradeable, BorgAuthACL, IERC721Receiver { dealManagerAddress = IDealManagerFactory(dealManagerFactory) .deployDealManager(salt); ICyberCorp(cyberCorpAddress).setDealManager(dealManagerAddress); - // Initialize IssuanceManager + IIssuanceManager(issuanceManagerAddress).initialize( authAddress, cyberCorpAddress, @@ -255,25 +319,9 @@ contract MetaDAOFactory is UUPSUpgradeable, BorgAuthACL, IERC721Receiver { dealManagerFactory ); - roundManagerAddress = IRoundManagerFactory(roundManagerFactory).deployRoundManager(salt); - - // Initialize RoundManager - IRoundManagerInit(roundManagerAddress).initialize( - authAddress, - cyberCorpAddress, - registryAddress, - issuanceManagerAddress, - roundManagerFactory - ); - - // Set RoundManager on the corp - ICyberCorp(cyberCorpAddress).setRoundManager(roundManagerAddress); - BorgAuth(authAddress).updateRole(issuanceManagerAddress, 99); BorgAuth(authAddress).updateRole(dealManagerAddress, 99); - BorgAuth(authAddress).updateRole(roundManagerAddress, 99); -//fix event emit MetaCorpDeployed(cyberCorpAddress, authAddress, issuanceManagerAddress, dealManagerAddress, roundManagerAddress, address(0), 0, _officer.eoa); } @@ -460,6 +508,4 @@ contract MetaDAOFactory is UUPSUpgradeable, BorgAuthACL, IERC721Receiver { } function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {} -} - - +} \ No newline at end of file diff --git a/src/ParentCoFactory.sol b/src/ParentCoFactory.sol new file mode 100644 index 00000000..ec826e3c --- /dev/null +++ b/src/ParentCoFactory.sol @@ -0,0 +1,511 @@ +/* .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 "./interfaces/IIssuanceManagerFactory.sol"; +import "./interfaces/IIssuanceManager.sol"; +import "./interfaces/ICyberCorp.sol"; +import "./interfaces/ICyberCorpSingleFactory.sol"; +import "./interfaces/IDealManagerFactory.sol"; +import "./interfaces/IDealManager.sol"; +import "./interfaces/IRoundManagerFactory.sol"; +import "./interfaces/ICyberCertPrinter.sol"; +import "./interfaces/ICyberAgreementRegistry.sol"; +import "./CyberCorpConstants.sol"; +import "./storage/CyberCertPrinterStorage.sol"; +import "./libs/auth.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +interface IRoundManagerInit { + function initialize( + address _auth, + address _corp, + address _dealRegistry, + address _issuanceManager, + address _upgradeFactory + ) external; +} + +interface ICyberCorpLocal { + function issuanceManager() external view returns (address); +} + +contract ParentCoFactory is UUPSUpgradeable, BorgAuthACL, IERC721Receiver { + using Strings for string; + + error InvalidSalt(); + error RoundManagerAlreadyExists(); + + address public registryAddress; + address public issuanceManagerFactory; + address public cyberCorpSingleFactory; + address public dealManagerFactory; + address public roundManagerFactory; + address public uriBuilder; + //store an escrowed signature hash for parent corp + bytes public parentCoSignatureHash; + address public stable; + // stored ParentCo officer details used in agreements + CompanyOfficer public parentCoOfficer; + + // Parent corp (ParentCo) deployment record + address public parentCorp; + address public parentAuth; + address public parentIssuanceManager; + address public parentDealManager; + address public parentRoundManager; + bool public parentCorpCreated; + + //adjust storage gap based on new variable + uint256[38] private __gap; // keep storage gap similar to CyberCorpFactory + + struct CyberCertData { + string name; + string symbol; + string uri; + SecurityClass securityClass; + SecuritySeries securitySeries; + address extension; + string[] defaultLegend; + } + + event CorpDeployed( + address indexed cyberCorp, + address indexed auth, + address indexed issuanceManager, + address dealManager, + address roundManager, + address certPrinter, + uint256 certTokenId, + address ownerEOA + ); + + event ParentCorpCreated( + address indexed corp, + address indexed auth, + address indexed issuanceManager, + address dealManager, + address roundManager + ); + + event IssuanceManagerFactoryUpdated( + address indexed issuanceManagerFactory, + address oldIssuanceFactory + ); + + event CyberCorpSingleFactoryUpdated( + address indexed cyberCorpSingleFactory, + address oldCyberCorpFactory + ); + + event DealManagerFactoryUpdated( + address indexed dealManagerFactory, + address oldDealFactory + ); + + event RoundManagerFactoryUpdated( + address indexed roundManagerFactory, + address oldRoundManagerFactory + ); + + event UriBuilderUpdated(address indexed uriBuilder, address oldUriBuilder); + event RegistryAddressUpdated(address indexed registryAddress, address oldRegistryAddress); + + error GlobalOrPartyValuesMismatch(); + error OfficerValuesMismatch(); + + function initialize( + address _auth, + address _registryAddress, + address _issuanceManagerFactory, + address _cyberCorpSingleFactory, + address _dealManagerFactory, + address _roundManagerFactory, + address _uriBuilder, + address _stable + ) public initializer { + __UUPSUpgradeable_init(); + __BorgAuthACL_init(_auth); + + registryAddress = _registryAddress; + issuanceManagerFactory = _issuanceManagerFactory; + cyberCorpSingleFactory = _cyberCorpSingleFactory; + dealManagerFactory = _dealManagerFactory; + roundManagerFactory = _roundManagerFactory; + uriBuilder = _uriBuilder; + stable = _stable; + } + + function setParentCoSignatureHash(bytes memory _parentCoSignatureHash) public onlyOwner { + parentCoSignatureHash = _parentCoSignatureHash; + } + + function setStable(address _stable) public onlyOwner { + stable = _stable; + } + + function setParentCoOfficer(CompanyOfficer memory _officer) public onlyOwner { + parentCoOfficer = _officer; + } + + function setParentCoOfficerEOA(address _eoa) public onlyOwner { + parentCoOfficer.eoa = _eoa; + } + + function setParentCoOfficerName(string memory _name) public onlyOwner { + parentCoOfficer.name = _name; + } + + function setParentCoOfficerContact(string memory _contact) public onlyOwner { + parentCoOfficer.contact = _contact; + } + + function setParentCoOfficerTitle(string memory _title) public onlyOwner { + parentCoOfficer.title = _title; + } + + function setIssuanceManagerFactory(address _issuanceManagerFactory) external onlyOwner { + address old = issuanceManagerFactory; + issuanceManagerFactory = _issuanceManagerFactory; + emit IssuanceManagerFactoryUpdated(_issuanceManagerFactory, old); + } + + function setCyberCorpSingleFactory(address _cyberCorpSingleFactory) external onlyOwner { + address old = cyberCorpSingleFactory; + cyberCorpSingleFactory = _cyberCorpSingleFactory; + emit CyberCorpSingleFactoryUpdated(_cyberCorpSingleFactory, old); + } + + function setDealManagerFactory(address _dealManagerFactory) external onlyOwner { + address old = dealManagerFactory; + dealManagerFactory = _dealManagerFactory; + emit DealManagerFactoryUpdated(_dealManagerFactory, old); + } + + function setRoundManagerFactory(address _roundManagerFactory) external onlyOwner { + address old = roundManagerFactory; + roundManagerFactory = _roundManagerFactory; + emit RoundManagerFactoryUpdated(_roundManagerFactory, old); + } + + function setUriBuilder(address _uriBuilder) external onlyOwner { + address old = uriBuilder; + uriBuilder = _uriBuilder; + emit UriBuilderUpdated(_uriBuilder, old); + } + + function setRegistryAddress(address _registryAddress) external onlyOwner { + address old = registryAddress; + registryAddress = _registryAddress; + emit RegistryAddressUpdated(_registryAddress, old); + } + + function deployCorp( + bytes32 salt, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + address _companyPayable, + CompanyOfficer memory _officer + ) + public + returns ( + address cyberCorpAddress, + address authAddress, + address issuanceManagerAddress, + address dealManagerAddress, + address roundManagerAddress + ) + { + if (salt == bytes32(0)) revert InvalidSalt(); + + // Deploy BorgAuth with CREATE2 with new param address owner + bytes memory authBytecode = type(BorgAuth).creationCode; + bytes32 authSalt = keccak256(abi.encodePacked("auth", salt)); + authAddress = Create2.deploy( + 0, + authSalt, + abi.encodePacked(authBytecode, abi.encode(address(this))) + ); + + // Initialize BorgAuth + // BorgAuth(authAddress).initialize(); + BorgAuth(authAddress).updateRole(_officer.eoa, 200); + + issuanceManagerAddress = IIssuanceManagerFactory(issuanceManagerFactory) + .deployIssuanceManager(salt); + + cyberCorpAddress = ICyberCorpSingleFactory(cyberCorpSingleFactory) + .deployCyberCorpSingle(salt); + + // Initialize CyberCorp + ICyberCorp(cyberCorpAddress).initialize( + authAddress, + companyName, + companyType, + companyJurisdiction, + companyContactDetails, + defaultDisputeResolution, + issuanceManagerAddress, + _companyPayable, + _officer, + cyberCorpSingleFactory, + address(0) + ); + + BorgAuth(authAddress).updateRole(cyberCorpAddress, 200); + //deploy deal manager + dealManagerAddress = IDealManagerFactory(dealManagerFactory) + .deployDealManager(salt); + ICyberCorp(cyberCorpAddress).setDealManager(dealManagerAddress); + + IIssuanceManager(issuanceManagerAddress).initialize( + authAddress, + cyberCorpAddress, + uriBuilder, + issuanceManagerFactory + ); + + // Initialize DealManager + IDealManager(dealManagerAddress).initialize( + authAddress, + cyberCorpAddress, + registryAddress, + issuanceManagerAddress, + dealManagerFactory + ); + + BorgAuth(authAddress).updateRole(issuanceManagerAddress, 99); + BorgAuth(authAddress).updateRole(dealManagerAddress, 99); + + emit CorpDeployed(cyberCorpAddress, authAddress, issuanceManagerAddress, dealManagerAddress, roundManagerAddress, address(0), 0, _officer.eoa); + } + + // Admin-only, one-time creation of the parent corp + function createParentCorp( + bytes32 salt, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + address _companyPayable + ) external onlyOwner returns ( + address corp, + address auth, + address issuance, + address dealMgr, + address roundMgr + ) { + if (parentCorpCreated) revert("ParentCorpAlreadyCreated"); + CompanyOfficer memory officer = parentCoOfficer; + if (officer.eoa == address(0)) revert("ParentCoOfficerNotSet"); + + ( + corp, + auth, + issuance, + dealMgr, + roundMgr + ) = deployCorp( + salt, + companyName, + companyType, + companyJurisdiction, + companyContactDetails, + defaultDisputeResolution, + _companyPayable, + officer + ); + + parentCorp = corp; + parentAuth = auth; + parentIssuanceManager = issuance; + parentDealManager = dealMgr; + parentRoundManager = roundMgr; + parentCorpCreated = true; + + emit ParentCorpCreated(corp, auth, issuance, dealMgr, roundMgr); + } + + function deployCorpContractFor( + uint256 salt, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + address _companyPayable, + CompanyOfficer memory _officer, + bytes32 _segCoTemplateId, + bytes32 _boardConsentTempateId, + string[] memory _globalValues, + string[] memory _partyValues, + bytes memory signature, + address deployer + ) + external + returns ( + address cyberCorpAddress, + address authAddress, + address issuanceManagerAddress, + address dealManagerAddress, + address roundManagerAddress, + address[] memory certPrinterAddress, + bytes32 id, + uint256[] memory certIds + ) + { + // Check: validate key fields + + if (_partyValues.length < 2 + || !_partyValues[0].equal(_officer.name) + || !_partyValues[1].equal(_officer.contact) + || !_globalValues[2].equal(companyName) + || !_globalValues[3].equal(companyType) + || !_globalValues[4].equal(companyJurisdiction) + || !_globalValues[5].equal(companyContactDetails) + ) { + revert GlobalOrPartyValuesMismatch(); + } + + if (_officer.eoa != deployer) { + revert OfficerValuesMismatch(); + } + + // Effect: construct parties + address[] memory partiesOverride = new address[](2); + partiesOverride[0] = parentCoOfficer.eoa; + partiesOverride[1] = deployer; + + string[][] memory partyValuesOverride = new string[][](2); + partyValuesOverride[0] = new string[](2); + partyValuesOverride[0][0] = parentCoOfficer.name; + partyValuesOverride[0][1] = parentCoOfficer.contact; + partyValuesOverride[1] = _partyValues; + + //create bytes32 salt + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + + ( + cyberCorpAddress, + authAddress, + issuanceManagerAddress, + dealManagerAddress, + roundManagerAddress + ) = deployCorp( + corpSalt, + companyName, + companyType, + companyJurisdiction, + companyContactDetails, + defaultDisputeResolution, + _companyPayable, + _officer + ); + + //both parties sign one agreement + bytes32 agreementId = ICyberAgreementRegistry(registryAddress).createContract( + _segCoTemplateId, + salt, + _globalValues, + partiesOverride, + partyValuesOverride, + bytes32(0), + address(this), + block.timestamp + 7 days + ); + + ICyberAgreementRegistry(registryAddress).signContractFor(deployer, agreementId, partyValuesOverride[1], signature, false, ""); + + ICyberAgreementRegistry(registryAddress).signContractWithEscrow( + parentCoOfficer.eoa, + agreementId, + partyValuesOverride[0], + parentCoSignatureHash, + false, + "" + ); + + //parent company sign the meeting notes (single-party) + address[] memory meetingNotesParties = new address[](1); + meetingNotesParties[0] = partiesOverride[0]; + string[][] memory meetingNotesPartyValues = new string[][](1); + meetingNotesPartyValues[0] = partyValuesOverride[0]; + bytes32 meetingNotesId = ICyberAgreementRegistry(registryAddress).createContract( + _boardConsentTempateId, + salt, + _globalValues, + meetingNotesParties, + meetingNotesPartyValues, + bytes32(0), + address(this), + block.timestamp + 7 days + ); + + ICyberAgreementRegistry(registryAddress).signContractWithEscrow( + parentCoOfficer.eoa, + meetingNotesId, + meetingNotesPartyValues[0], + parentCoSignatureHash, + false, + "" + ); + } + + // Allow this factory to receive ERC721 tokens via safeTransferFrom/safeMint + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external pure override returns (bytes4) { + return this.onERC721Received.selector; + } + + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {} +} \ No newline at end of file diff --git a/src/PumpCorpFactory.sol b/src/PumpCorpFactory.sol new file mode 100644 index 00000000..585fe7a3 --- /dev/null +++ b/src/PumpCorpFactory.sol @@ -0,0 +1,736 @@ +/* .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 "./interfaces/IIssuanceManagerFactory.sol"; +import "./libs/auth.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "./interfaces/IIssuanceManager.sol"; +import "./interfaces/ICyberCorp.sol"; +import "./interfaces/ICyberCorpSingleFactory.sol"; +import "./interfaces/IDealManagerFactory.sol"; +import "./interfaces/IDealManager.sol"; +import "./interfaces/IRoundManagerFactory.sol"; +import {IRoundManager as IRoundManagerInterface} from "./interfaces/IRoundManager.sol"; +import "./interfaces/ICyberCertPrinter.sol"; +import "./CyberCorpConstants.sol"; +import "./storage/CyberCertPrinterStorage.sol"; +import {CyberCertData as RM_CyberCertData} from "./storage/RoundManagerStorage.sol"; +import {Round, RoundType, RoundLib} from "./libs/RoundLib.sol"; + +interface IRoundManagerInit { + function initialize( + address _auth, + address _corp, + address _dealRegistry, + address _issuanceManager, + address _upgradeFactory + ) external; +} + +interface ICyberCorpLocal { + function issuanceManager() external view returns (address); +} + +library PumpCorpFactoryLib { + bytes32 constant FACTORY_DOMAIN_TYPEHASH = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 constant OFFICER_TYPEHASH = keccak256( + "CompanyOfficer(address eoa,string name,string contact,string title)" + ); + bytes32 constant CERT_DATA_TYPEHASH = keccak256( + "CyberCertData(string name,string symbol,string uri,uint8 securityClass,uint8 securitySeries,address extension,string[] defaultLegend)" + ); + bytes32 constant ROUND_SUPPLEMENTAL_TYPEHASH = keccak256( + "RoundSupplementalData(bytes32 corpSalt,address companyPayable,bool publicRound,bool allowTimedOffers,bool restrictEndTimeReduction,CompanyOfficer officer,string companyName,string companyType,string companyJurisdiction,string companyContactDetails,string defaultDisputeResolution,bytes[] extensionData,string[] roundPartyValues,string[] legalDetails,CyberCertData[] certData,address[] conditionAddresses)CompanyOfficer(address eoa,string name,string contact,string title)CyberCertData(string name,string symbol,string uri,uint8 securityClass,uint8 securitySeries,address extension,string[] defaultLegend)" + ); + + /// @notice EIP-712 helper for encoding an array of addresses + /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodedata + function hashAddresses(address[] memory addrs) internal pure returns (bytes32) { + bytes32[] memory padded = new bytes32[](addrs.length); + for (uint256 i = 0; i < addrs.length; i++) { + padded[i] = bytes32(uint256(uint160(addrs[i]))); + } + return keccak256(abi.encodePacked(padded)); + } + + function hashStringArray(string[] memory data) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](data.length); + for (uint256 i = 0; i < data.length; i++) { + hashes[i] = keccak256(bytes(data[i])); + } + return keccak256(abi.encodePacked(hashes)); + } + + function hashBytesArray(bytes[] memory data) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](data.length); + for (uint256 i = 0; i < data.length; i++) { + hashes[i] = keccak256(data[i]); + } + return keccak256(abi.encodePacked(hashes)); + } + + function hashCertData(RM_CyberCertData memory cd) internal pure returns (bytes32) { + return keccak256(abi.encode( + CERT_DATA_TYPEHASH, + keccak256(bytes(cd.name)), + keccak256(bytes(cd.symbol)), + keccak256(bytes(cd.uri)), + cd.securityClass, + cd.securitySeries, + cd.extension, + hashStringArray(cd.defaultLegend) + )); + } + + function hashCertDataArray(RM_CyberCertData[] memory data) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](data.length); + for (uint256 i = 0; i < data.length; i++) { + hashes[i] = hashCertData(data[i]); + } + return keccak256(abi.encodePacked(hashes)); + } +} + +contract PumpCorpFactory is UUPSUpgradeable, BorgAuthACL { + using Strings for string; + using RoundLib for Round; + error InvalidSalt(); + error RoundManagerAlreadyExists(); + error GlobalOrPartyValuesMismatch(); + error InvalidMetadataSignature(); + + address public registryAddress; + address public issuanceManagerFactory; + address public cyberCorpSingleFactory; + address public dealManagerFactory; + address public roundManagerFactory; + address public uriBuilder; + address public lexchexAuth; + + // adjust storage gap based on new variable + uint256[42] private __gap; + + struct CyberCertData { + string name; + string symbol; + string uri; + SecurityClass securityClass; + SecuritySeries securitySeries; + address extension; + string[] defaultLegend; + } + + event CyberCorpDeployed( + address indexed cyberCorp, + address indexed auth, + address indexed issuanceManager, + address dealManager, + string cyberCORPName, + string cyberCORPType, + string cyberCORPContactDetails, + string cyberCORPJurisdiction, + string defaultDisputeResolution, + address _companyPayable + ); + + event DealManagerFactoryUpdated( + address indexed dealManagerFactory, + address oldDealFactory + ); + + event RoundManagerDeployed( + address indexed cyberCorp, + address indexed roundManager + ); + + //create an event when IssuanceManagerFactory is updated + event IssuanceManagerFactoryUpdated( + address indexed issuanceManagerFactory, + address oldIssuanceFactory + ); + + event CyberCorpSingleFactoryUpdated( + address indexed cyberCorpSingleFactory, + address oldCyberCorpFactory + ); + + event RoundManagerFactoryUpdated( + address indexed roundManagerFactory, + address oldRoundManagerFactory + ); + + event UriBuilderUpdated( + address indexed uriBuilder, + address oldUriBuilder + ); + + event RegistryAddressUpdated( + address indexed registryAddress, + address oldRegistryAddress + ); + + event LexchexAuthUpdated(address indexed lexchexAuth, address oldLexchexAuth); + + function initialize( + address _auth, + address _registryAddress, + address _issuanceManagerFactory, + address _cyberCorpSingleFactory, + address _dealManagerFactory, + address _roundManagerFactory, + address _uriBuilder + ) public initializer { + __UUPSUpgradeable_init(); + // Initialize BorgAuthACL + __BorgAuthACL_init(_auth); + + registryAddress = _registryAddress; + issuanceManagerFactory = _issuanceManagerFactory; + cyberCorpSingleFactory = _cyberCorpSingleFactory; + dealManagerFactory = _dealManagerFactory; + roundManagerFactory = _roundManagerFactory; + uriBuilder = _uriBuilder; + + // Set default LeXcheX AUTH if not already set + if (lexchexAuth == address(0)) { + lexchexAuth = 0xeAdeaD5C4A6747D4959489742c143bCDb95a01c2; + } + } + + function deployCyberCorp( + bytes32 salt, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + address _companyPayable, + CompanyOfficer memory _officer + ) + public + returns ( + address cyberCorpAddress, + address authAddress, + address issuanceManagerAddress, + address dealManagerAddress, + address roundManagerAddress + ) + { + if (salt == bytes32(0)) revert InvalidSalt(); + + // Deploy BorgAuth with CREATE2 with new param address owner + bytes memory authBytecode = type(BorgAuth).creationCode; + bytes32 authSalt = keccak256(abi.encodePacked("auth", salt)); + authAddress = Create2.deploy( + 0, + authSalt, + abi.encodePacked(authBytecode, abi.encode(address(this))) + ); + + // Initialize BorgAuth + // BorgAuth(authAddress).initialize(); + BorgAuth(authAddress).updateRole(_officer.eoa, 200); + + issuanceManagerAddress = IIssuanceManagerFactory(issuanceManagerFactory) + .deployIssuanceManager(salt); + + cyberCorpAddress = ICyberCorpSingleFactory(cyberCorpSingleFactory) + .deployCyberCorpSingle(salt); + + // Initialize CyberCorp + ICyberCorp(cyberCorpAddress).initialize( + authAddress, + companyName, + companyType, + companyJurisdiction, + companyContactDetails, + defaultDisputeResolution, + issuanceManagerAddress, + _companyPayable, + _officer, + cyberCorpSingleFactory, + address(0) + ); + + BorgAuth(authAddress).updateRole(cyberCorpAddress, 200); + //deploy deal manager + dealManagerAddress = IDealManagerFactory(dealManagerFactory) + .deployDealManager(salt); + ICyberCorp(cyberCorpAddress).setDealManager(dealManagerAddress); + // Initialize IssuanceManager + IIssuanceManager(issuanceManagerAddress).initialize( + authAddress, + cyberCorpAddress, + uriBuilder, + issuanceManagerFactory + ); + + // Initialize DealManager + IDealManager(dealManagerAddress).initialize( + authAddress, + cyberCorpAddress, + registryAddress, + issuanceManagerAddress, + dealManagerFactory + ); + + // Deploy and initialize RoundManager + roundManagerAddress = deployAndInitializeRoundManager(salt, cyberCorpAddress); + + // Authorize peripheral contracts for the cyber corp. It is ok to do it here on behalf of the corp + // because the corp has just been created by us. + // In contrast, if any of the peripheral contract is being retrofitted to an existing corp, + // they would have to authorize it themselves for security reasons. + + // Set RoundManager on the corp + ICyberCorp(cyberCorpAddress).setRoundManager(roundManagerAddress); + + BorgAuth(authAddress).updateRole(issuanceManagerAddress, 99); + BorgAuth(authAddress).updateRole(dealManagerAddress, 99); + BorgAuth(authAddress).updateRole(roundManagerAddress, 99); + + emit CyberCorpDeployed( + cyberCorpAddress, + authAddress, + issuanceManagerAddress, + dealManagerAddress, + companyName, + companyType, + companyContactDetails, + companyJurisdiction, + defaultDisputeResolution, + _companyPayable + ); + } + + function _verifySupplementalSignature( + bytes32 corpSalt, + address companyPayable, + bool publicRound, + bool allowTimedOffers, + bool restrictEndTimeReduction, + CompanyOfficer memory officer, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + bytes[] memory extensionData, + string[] memory roundPartyValues, + string[] memory legalDetails, + RM_CyberCertData[] memory certData, + address[] memory conditionAddresses, + bytes memory signature + ) internal view { + bytes32 domainSep = keccak256(abi.encode( + PumpCorpFactoryLib.FACTORY_DOMAIN_TYPEHASH, + keccak256(bytes("PumpCorpFactory")), + keccak256(bytes("1")), + block.chainid, + address(this) + )); + bytes32 officerHash = keccak256(abi.encode( + PumpCorpFactoryLib.OFFICER_TYPEHASH, + officer.eoa, + keccak256(bytes(officer.name)), + keccak256(bytes(officer.contact)), + keccak256(bytes(officer.title)) + )); + bytes32 structHash = keccak256(abi.encode( + PumpCorpFactoryLib.ROUND_SUPPLEMENTAL_TYPEHASH, + corpSalt, + companyPayable, + publicRound, + allowTimedOffers, + restrictEndTimeReduction, + officerHash, + keccak256(bytes(companyName)), + keccak256(bytes(companyType)), + keccak256(bytes(companyJurisdiction)), + keccak256(bytes(companyContactDetails)), + keccak256(bytes(defaultDisputeResolution)), + PumpCorpFactoryLib.hashBytesArray(extensionData), + PumpCorpFactoryLib.hashStringArray(roundPartyValues), + PumpCorpFactoryLib.hashStringArray(legalDetails), + PumpCorpFactoryLib.hashCertDataArray(certData), + PumpCorpFactoryLib.hashAddresses(conditionAddresses) + )); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSep, structHash)); + if (ECDSA.recover(digest, signature) != officer.eoa) revert InvalidMetadataSignature(); + } + + // TODO WIP: currently not in use. Will need metasig support + function deployCyberCorpAndCreateOffer( + uint256 salt, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + address _companyPayable, + CompanyOfficer memory _officer, + CyberCertData[] memory _certData, + bytes32 _templateId, + address paymentToken, + string[] memory _globalValues, + address[] memory _parties, + uint256 _paymentAmount, + string[][] memory _partyValues, + bytes memory signature, + CertificateDetails[] memory _details, + address[] memory conditions, + bytes32 secretHash, + uint256 expiry + ) + external + returns ( + address cyberCorpAddress, + address authAddress, + address issuanceManagerAddress, + address dealManagerAddress, + address roundManagerAddress, + address[] memory certPrinterAddress, + bytes32 id, + uint256[] memory certIds + ) + { + //create bytes32 salt + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + + //set this officer's eoa to the sender + _officer.eoa = msg.sender; + + ( + cyberCorpAddress, + authAddress, + issuanceManagerAddress, + dealManagerAddress, + roundManagerAddress + ) = deployCyberCorp( + corpSalt, + companyName, + companyType, + companyJurisdiction, + companyContactDetails, + defaultDisputeResolution, + _companyPayable, + _officer + ); + + certPrinterAddress = new address[](_certData.length); + + for (uint256 i = 0; i < _certData.length; i++) { + ICyberCertPrinter certPrinter = ICyberCertPrinter( + IIssuanceManager(issuanceManagerAddress).createCertPrinter( + _certData[i].defaultLegend, + string.concat(companyName, " ", _certData[i].name), + _certData[i].symbol, + _certData[i].uri, + _certData[i].securityClass, + _certData[i].securitySeries, + _certData[i].extension + ) + ); + certPrinterAddress[i] = address(certPrinter); + } + + // Create and sign deal + certIds = new uint256[](_certData.length); + (id, certIds) = IDealManager(dealManagerAddress).proposeAndSignDeal( + certPrinterAddress, + paymentToken, + _paymentAmount, + _templateId, + salt, + _globalValues, + _parties, + _details, + msg.sender, + signature, + _partyValues, + conditions, + secretHash, + expiry + ); + } + + function deployCyberCorpAndCreateRoundFor( + uint256 salt, + SecuritySeries seriesType, + string memory companyName, + string memory companyType, + string memory companyJurisdiction, + string memory companyContactDetails, + string memory defaultDisputeResolution, + address _companyPayable, + CompanyOfficer memory _officer, + string[] memory legalDetails, + bytes[] memory extensionData, + RM_CyberCertData[] memory certData, + bytes32 templateId, + address paymentToken, + uint256 pricePerUnit, + uint256 valuation, + string[] memory roundPartyValues, + bytes memory escrowedSignature, + bytes memory metadataSignature, + RoundType roundType, + address[] memory conditions, + uint256 raiseCap, + uint256 minTicket, + uint256 maxTicket, + uint256 startTime, + uint256 endTime, + bool publicRound, + bool allowTimedOffers, + bool restrictEndTimeReduction + ) + external + returns ( + address cyberCorpAddress, + address authAddress, + address issuanceManagerAddress, + address dealManagerAddress, + address roundManagerAddress, + bytes32 roundId + ) + { + // Validation + + // share the same partyFields structures as following: + if (roundPartyValues.length < 2 + || !roundPartyValues[0].equal(_officer.name) + || roundPartyValues[1].parseAddress() != _officer.eoa + ) { + revert GlobalOrPartyValuesMismatch(); + } + + // Create corp + + //create bytes32 salt + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + + _verifySupplementalSignature( + corpSalt, + _companyPayable, + publicRound, + allowTimedOffers, + restrictEndTimeReduction, + _officer, + companyName, + companyType, + companyJurisdiction, + companyContactDetails, + defaultDisputeResolution, + extensionData, + roundPartyValues, + legalDetails, + certData, + conditions, + metadataSignature + ); + + ( + cyberCorpAddress, + authAddress, + issuanceManagerAddress, + dealManagerAddress, + roundManagerAddress + ) = deployCyberCorp( + corpSalt, + companyName, + companyType, + companyJurisdiction, + companyContactDetails, + defaultDisputeResolution, + _companyPayable, + _officer + ); + + // Create round with provided round type using RoundLib + { + Round memory draft = RoundLib + .draft() + .setTickets( + seriesType, + roundType, + publicRound, + allowTimedOffers, + restrictEndTimeReduction, + raiseCap, + minTicket, + maxTicket, + paymentToken, + pricePerUnit, + valuation, + startTime, + endTime + ) + .setAgreement( + templateId, + _officer.eoa, // ensure only who signs the escrowedsignature can be the owner of the cybercorp + _officer.name, + _officer.title, + legalDetails, + roundPartyValues, + extensionData, + conditions, + escrowedSignature + ); + roundId = IRoundManagerInterface(roundManagerAddress).createRound( + draft, + certData + ); + } + } + + /// @notice Deploy, initialize and grant LeXCheX access to a new RoundManager for the given cyber corp + /// @dev For security, the cyber corp is expected to authorize the created RoundManager itself + function deployAndInitializeRoundManager(bytes32 salt, address cyberCorpAddress) internal returns (address) { + if (ICyberCorp(cyberCorpAddress).roundManager() != address(0)) { + revert RoundManagerAlreadyExists(); + } + + address roundManagerAddress = IRoundManagerFactory(roundManagerFactory).deployRoundManager(salt); + + // Zero-out fees for this instance + IRoundManagerFactory(roundManagerFactory).setInstanceFeeOverride(roundManagerAddress, true, 0); + + // Initialize RoundManager + IRoundManagerInit(roundManagerAddress).initialize( + address(BorgAuthACL(cyberCorpAddress).AUTH()), + cyberCorpAddress, + registryAddress, + ICyberCorpLocal(cyberCorpAddress).issuanceManager(), + roundManagerFactory + ); + + // Add newly created RoundManager as OWNER in LeXcheX AUTH + if (lexchexAuth != address(0)) { + BorgAuth(lexchexAuth).updateRole( + roundManagerAddress, + BorgAuth(lexchexAuth).OWNER_ROLE() + ); + } + + emit RoundManagerDeployed( + cyberCorpAddress, + roundManagerAddress + ); + + return roundManagerAddress; + } + + function setIssuanceManagerFactory( + address _issuanceManagerFactory + ) external onlyOwner { + address oldIssuanceFactory = issuanceManagerFactory; + issuanceManagerFactory = _issuanceManagerFactory; + emit IssuanceManagerFactoryUpdated( + issuanceManagerFactory, + oldIssuanceFactory + ); + } + + function setCyberCorpSingleFactory( + address _cyberCorpSingleFactory + ) external onlyOwner { + address oldCyberCorpFactory = cyberCorpSingleFactory; + cyberCorpSingleFactory = _cyberCorpSingleFactory; + emit CyberCorpSingleFactoryUpdated( + cyberCorpSingleFactory, + oldCyberCorpFactory + ); + } + + function setRegistryAddress( + address _registryAddress + ) external onlyOwner { + address old = registryAddress; + registryAddress = _registryAddress; + emit RegistryAddressUpdated( + registryAddress, + old + ); + } + + function setDealManagerFactory( + address _dealManagerFactory + ) external onlyOwner { + address oldDealFactory = dealManagerFactory; + dealManagerFactory = _dealManagerFactory; + emit DealManagerFactoryUpdated(dealManagerFactory, oldDealFactory); + } + + function setRoundManagerFactory( + address _roundManagerFactory + ) external onlyOwner { + address oldRoundManagerFactory = roundManagerFactory; + roundManagerFactory = _roundManagerFactory; + emit RoundManagerFactoryUpdated( + roundManagerFactory, + oldRoundManagerFactory + ); + } + + function setUriBuilder(address _uriBuilder) external onlyOwner { + address old = uriBuilder; + uriBuilder = _uriBuilder; + emit UriBuilderUpdated(_uriBuilder, old); + } + + function setLexchexAuth(address _lexchexAuth) external onlyOwner { + address old = lexchexAuth; + lexchexAuth = _lexchexAuth; + emit LexchexAuthUpdated(lexchexAuth, old); + } + + function _authorizeUpgrade( + address newImplementation + ) internal virtual override onlyOwner {} +} diff --git a/src/RoundManager.sol b/src/RoundManager.sol index e5dc1c76..02ea6546 100644 --- a/src/RoundManager.sol +++ b/src/RoundManager.sol @@ -72,7 +72,7 @@ contract RoundManager is using LexScrowStorage for LexScrowStorage.LexScrowData; using SafeERC20 for IERC20; - string public constant DEPLOY_VERSION = "3"; // For version-tracking on all deployment and future upgrades + string public constant DEPLOY_VERSION = "4"; // For version-tracking on all deployment and future upgrades error InvalidRound(); error RoundNotOpen(); @@ -90,6 +90,8 @@ contract RoundManager is error EOIExpired(); error NotEOISubmitter(); error NotRefImplementation(); + error EndTimeReductionRestricted(); + error RoundAlreadyExists(); event RoundCreated(bytes32 indexed roundId, address indexed corp, Round round, bool publicRound); event RoundSnapshotSet( @@ -223,6 +225,8 @@ contract RoundManager is ) ); + if (RoundManagerStorage.getRound(roundDraft.id).id != bytes32(0)) revert RoundAlreadyExists(); + if(!EIP712Lib.verifyEscrowedSignature( address(this), roundDraft.authorityOfficer, @@ -292,6 +296,7 @@ contract RoundManager is function setRoundEndTime(bytes32 roundId, uint256 newEndTime) external onlyOwner { Round storage round = RoundManagerStorage.getRound(roundId); if (round.id == bytes32(0)) revert InvalidRound(); + if (round.restrictEndTimeReduction && newEndTime < round.endTime) revert EndTimeReductionRestricted(); uint256 oldEndTime = round.endTime; round.endTime = newEndTime; emit RoundEndTimeUpdated(roundId, oldEndTime, newEndTime); @@ -302,6 +307,7 @@ contract RoundManager is function closeRoundNow(bytes32 roundId) external onlyOwner { Round storage round = RoundManagerStorage.getRound(roundId); if (round.id == bytes32(0)) revert InvalidRound(); + if (round.restrictEndTimeReduction) revert EndTimeReductionRestricted(); uint256 oldEndTime = round.endTime; round.endTime = block.timestamp; emit RoundEndTimeUpdated(roundId, oldEndTime, round.endTime); diff --git a/src/RoundManagerFactory.sol b/src/RoundManagerFactory.sol index b8b8fb8a..1693f895 100644 --- a/src/RoundManagerFactory.sol +++ b/src/RoundManagerFactory.sol @@ -42,6 +42,7 @@ except with the express prior written permission of the copyright holder.*/ pragma solidity 0.8.28; import "./RoundManager.sol"; +import {FeeOverride} from "./interfaces/IRoundManagerFactory.sol"; import "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "openzeppelin-contracts/utils/Create2.sol"; import "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -60,6 +61,7 @@ contract RoundManagerFactory is UUPSUpgradeable, BorgAuthACL { event RoundManagerDeployed(address roundManager, string version); event RefImplementationSet(address refImplementation, string version); event TokenWhitelistUpdated(address token, bool isWhitelisted); + event InstanceFeeOverrideSet(address indexed roundManager, bool enabled, uint256 ratio); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -138,10 +140,36 @@ contract RoundManagerFactory is UUPSUpgradeable, BorgAuthACL { RoundManagerFactoryStorage.setPlatformPayable(platformPayable); } - /// @notice Get the fee ratio - /// @return Fee ratio (same unit as BASIS_POINTS + /// @notice Get the effective fee ratio for the calling RoundManager instance. + /// @dev Returns the instance-specific override if set, otherwise the global default. + /// Intended to be called by RoundManager instances (msg.sender = the RoundManager address). + /// We intentionally name it `getDefaultFeeRatio` for backward compatibility so that older RoundManagers can + /// still utilize the fee overrides. + /// @return Fee ratio (same unit as BASIS_POINTS) function getDefaultFeeRatio() external view returns (uint256) { - return RoundManagerFactoryStorage.getDefaultFeeRatio(); + RoundManagerFactoryStorage.RoundManagerFactoryData storage s = RoundManagerFactoryStorage.roundManagerFactoryStorage(); + FeeOverride storage fo = s.instanceFeeOverrides[msg.sender]; + return fo.enabled ? fo.ratio : s.defaultFeeRatio; + } + + /// @notice Get the underlying default fee ratio without overrides + /// @return Fee ratio (same unit as BASIS_POINTS + function getUnderlyingDefaultFeeRatio() external view returns (uint256) { + return RoundManagerFactoryStorage.roundManagerFactoryStorage().defaultFeeRatio; + } + + /// @notice Get the per-instance fee override for a specific RoundManager + /// @return Fee override configs + function getInstanceFeeOverride(address roundManager) external view returns (FeeOverride memory) { + return RoundManagerFactoryStorage.roundManagerFactoryStorage().instanceFeeOverrides[roundManager]; + } + + /// @notice Set a per-instance fee override for a specific RoundManager + /// @dev Only callable by the factory owner. Pass enabled=false to remove the override. + function setInstanceFeeOverride(address roundManager, bool enabled, uint256 ratio) external onlyOwner { + if (ratio > RoundManagerFactoryStorage.BASIS_POINTS) revert InvalidFeeRatio(); + RoundManagerFactoryStorage.roundManagerFactoryStorage().instanceFeeOverrides[roundManager] = FeeOverride(enabled, ratio); + emit InstanceFeeOverrideSet(roundManager, enabled, ratio); } /// @notice Set the fee ratio diff --git a/src/creds/lexchexMinter.sol b/src/creds/lexchexMinter.sol index 15208750..18bd56b0 100644 --- a/src/creds/lexchexMinter.sol +++ b/src/creds/lexchexMinter.sol @@ -67,6 +67,7 @@ contract LeXcheXMinter is Initializable, UUPSUpgradeable, BorgAuthACL { error MintFailed(); error AccreditationDoesNotExist(); error AccreditationVoided(); + error RequestOwnerMismatch(); // Events event MintRequested(address indexed requester, uint256 mintPrice, bytes32 agreementId); @@ -215,7 +216,7 @@ contract LeXcheXMinter is Initializable, UUPSUpgradeable, BorgAuthACL { /// @notice Admin-only path to mint for autominting with certain conditions in investing /// @dev Mirrors requestMint but gated by onlyAdmin and skips _verifyAuthoritySignature function requestMintFor( - MintRequest calldata request, + MintRequest memory request, bytes32 _templateId, uint256 _salt, string[] memory _globalValues, @@ -331,6 +332,10 @@ contract LeXcheXMinter is Initializable, UUPSUpgradeable, BorgAuthACL { if(bytes(acc.voided).length > 0) { revert AccreditationVoided(); } + // Bind signed subject to the actual token owner to prevent cross-account renewals. + if (LeXcheX(lexchex).ownerOf(tokenId) != request.owner) { + revert RequestOwnerMismatch(); + } // Note an expired token could still be renewed @@ -373,6 +378,10 @@ contract LeXcheXMinter is Initializable, UUPSUpgradeable, BorgAuthACL { if(bytes(acc.voided).length > 0) { revert AccreditationVoided(); } + // Keep admin path consistent and avoid mutating a token for a different subject. + if (LeXcheX(lexchex).ownerOf(tokenId) != request.owner) { + revert RequestOwnerMismatch(); + } // Handle payment using safeTransferFrom if (request.mintPrice > 0) { diff --git a/src/hooks/uniswap/MetalexIssuerFeeHook.sol b/src/hooks/uniswap/MetalexIssuerFeeHook.sol new file mode 100644 index 00000000..1de92464 --- /dev/null +++ b/src/hooks/uniswap/MetalexIssuerFeeHook.sol @@ -0,0 +1,356 @@ +pragma solidity 0.8.28; + +import "../../libs/auth.sol"; + +interface IPoolManager { + struct SwapParams { + bool zeroForOne; + int256 amountSpecified; + uint160 sqrtPriceLimitX96; + } + + struct ModifyLiquidityParams { + int24 tickLower; + int24 tickUpper; + int256 liquidityDelta; + bytes32 salt; + } + + function take(address currency, address to, uint256 amount) external; +} + +struct PoolKey { + address currency0; + address currency1; + uint24 fee; + int24 tickSpacing; + address hooks; +} + +type BalanceDelta is int256; + +type BeforeSwapDelta is int256; + +struct HookPermissions { + bool beforeInitialize; + bool afterInitialize; + bool beforeAddLiquidity; + bool afterAddLiquidity; + bool beforeRemoveLiquidity; + bool afterRemoveLiquidity; + bool beforeSwap; + bool afterSwap; + bool beforeDonate; + bool afterDonate; +} + +interface IHooks { + function getHookPermissions() external pure returns (HookPermissions memory); + + function beforeInitialize( + address sender, + PoolKey calldata key, + uint160 sqrtPriceX96, + bytes calldata hookData + ) external returns (bytes4); + + function afterInitialize( + address sender, + PoolKey calldata key, + uint160 sqrtPriceX96, + int24 tick, + bytes calldata hookData + ) external returns (bytes4); + + function beforeAddLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external returns (bytes4); + + function afterAddLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external returns (bytes4); + + function beforeRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external returns (bytes4); + + function afterRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external returns (bytes4); + + function beforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata hookData + ) external returns (bytes4, BeforeSwapDelta, uint24); + + function afterSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external returns (bytes4); + + function beforeDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external returns (bytes4); + + function afterDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external returns (bytes4); +} + +contract MetalexIssuerFeeHook is IHooks, BorgAuthACL { + uint24 public constant BPS_DENOMINATOR = 10_000; + + IPoolManager public poolManager; + + struct PoolFeeConfig { + address metalexRecipient; + address issuerRecipient; + uint24 metalexFeeBps; + uint24 issuerFeeBps; + bool enabled; + } + + mapping(bytes32 => PoolFeeConfig) public poolFeeConfig; + + error ZeroAddress(); + error FeeTooHigh(); + error UnauthorizedPoolManager(); + + event PoolConfigUpdated( + bytes32 indexed poolId, + address metalexRecipient, + address issuerRecipient, + uint24 metalexFeeBps, + uint24 issuerFeeBps, + bool enabled + ); + + function initialize( + address _auth, + address _poolManager + ) external initializer { + __BorgAuthACL_init(_auth); + _setPoolManager(_poolManager); + } + + function setPoolConfig( + PoolKey calldata key, + address _metalexRecipient, + address _issuerRecipient, + uint24 _metalexFeeBps, + uint24 _issuerFeeBps, + bool _enabled + ) external onlyAdmin { + bytes32 poolId = _poolId(key); + if (_enabled && (_metalexRecipient == address(0) || _issuerRecipient == address(0))) { + revert ZeroAddress(); + } + if (uint256(_metalexFeeBps) + uint256(_issuerFeeBps) > BPS_DENOMINATOR) { + revert FeeTooHigh(); + } + + poolFeeConfig[poolId] = PoolFeeConfig({ + metalexRecipient: _metalexRecipient, + issuerRecipient: _issuerRecipient, + metalexFeeBps: _metalexFeeBps, + issuerFeeBps: _issuerFeeBps, + enabled: _enabled + }); + + emit PoolConfigUpdated( + poolId, + _metalexRecipient, + _issuerRecipient, + _metalexFeeBps, + _issuerFeeBps, + _enabled + ); + } + + function _setPoolManager(address _poolManager) internal { + if (_poolManager == address(0)) { + revert ZeroAddress(); + } + poolManager = IPoolManager(_poolManager); + } + + function getHookPermissions() external pure returns (HookPermissions memory permissions) { + permissions.afterSwap = true; + } + + function beforeInitialize( + address, + PoolKey calldata, + uint160, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.beforeInitialize.selector; + } + + function afterInitialize( + address, + PoolKey calldata, + uint160, + int24, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.afterInitialize.selector; + } + + function beforeAddLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.beforeAddLiquidity.selector; + } + + function afterAddLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.afterAddLiquidity.selector; + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.beforeRemoveLiquidity.selector; + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.afterRemoveLiquidity.selector; + } + + function beforeSwap( + address, + PoolKey calldata, + IPoolManager.SwapParams calldata, + bytes calldata + ) external pure returns (bytes4, BeforeSwapDelta, uint24) { + return (MetalexIssuerFeeHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0); + } + + function afterSwap( + address, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata + ) external returns (bytes4) { + if (msg.sender != address(poolManager)) { + revert UnauthorizedPoolManager(); + } + + PoolFeeConfig memory config = poolFeeConfig[_poolId(key)]; + if (!config.enabled) { + return MetalexIssuerFeeHook.afterSwap.selector; + } + + uint256 inputAmount = _inputAmount(delta, params.zeroForOne); + if (inputAmount == 0) { + return MetalexIssuerFeeHook.afterSwap.selector; + } + + uint256 metalexFee = (inputAmount * config.metalexFeeBps) / BPS_DENOMINATOR; + uint256 issuerFee = (inputAmount * config.issuerFeeBps) / BPS_DENOMINATOR; + address currencyIn = params.zeroForOne ? key.currency0 : key.currency1; + + if (metalexFee > 0) { + poolManager.take(currencyIn, config.metalexRecipient, metalexFee); + } + if (issuerFee > 0) { + poolManager.take(currencyIn, config.issuerRecipient, issuerFee); + } + + return MetalexIssuerFeeHook.afterSwap.selector; + } + + function beforeDonate( + address, + PoolKey calldata, + uint256, + uint256, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.beforeDonate.selector; + } + + function afterDonate( + address, + PoolKey calldata, + uint256, + uint256, + bytes calldata + ) external pure returns (bytes4) { + return MetalexIssuerFeeHook.afterDonate.selector; + } + + function _inputAmount(BalanceDelta delta, bool zeroForOne) internal pure returns (uint256) { + if (zeroForOne) { + int128 amount0 = _amount0(delta); + if (amount0 <= 0) { + return 0; + } + return uint256(uint128(amount0)); + } + + int128 amount1 = _amount1(delta); + if (amount1 <= 0) { + return 0; + } + return uint256(uint128(amount1)); + } + + function _poolId(PoolKey calldata key) internal pure returns (bytes32) { + return keccak256(abi.encode(key)); + } + + function _amount0(BalanceDelta delta) internal pure returns (int128) { + return int128(int256(BalanceDelta.unwrap(delta) >> 128)); + } + + function _amount1(BalanceDelta delta) internal pure returns (int128) { + return int128(int256(BalanceDelta.unwrap(delta))); + } +} diff --git a/src/interfaces/ICertificateImageBuilder.sol b/src/interfaces/ICertificateImageBuilder.sol new file mode 100644 index 00000000..4b4eba5b --- /dev/null +++ b/src/interfaces/ICertificateImageBuilder.sol @@ -0,0 +1,58 @@ +/* .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 "../CyberCorpConstants.sol"; + +/// @title ICertificateImageBuilder +/// @notice Interface for the certificate image builder contract +interface ICertificateImageBuilder { + /// @notice Builds a certificate SVG image + /// @param params The certificate parameters + /// @param timestamp The timestamp for the certificate date + /// @return The complete SVG string + function buildCertificateSVG( + CertificateSVGParams calldata params, + uint256 timestamp + ) external pure returns (string memory); +} + diff --git a/src/interfaces/ICyberCertPrinter.sol b/src/interfaces/ICyberCertPrinter.sol index 00c1d06f..b7962848 100644 --- a/src/interfaces/ICyberCertPrinter.sol +++ b/src/interfaces/ICyberCertPrinter.sol @@ -72,7 +72,8 @@ interface ICyberCertPrinter is IERC721 { function safeMintAndAssign( address to, uint256 tokenId, - CertificateDetails memory details + CertificateDetails memory details, + string memory investorName ) external returns (uint256); function assignCert( address from, @@ -82,7 +83,7 @@ interface ICyberCertPrinter is IERC721 { ) external returns (uint256); function addIssuerSignature( uint256 tokenId, - string calldata signatureURI + bytes calldata signature ) external; function addEndorsement( uint256 tokenId, @@ -100,9 +101,16 @@ interface ICyberCertPrinter is IERC721 { ) external; function burn(uint256 tokenId) external; function voidCert(uint256 tokenId) external; + function unvoidCert(uint256 tokenId) external; + function isVoided(uint256 tokenId) external view returns (bool); function getCertificateDetails( uint256 tokenId ) external view returns (CertificateDetails memory); + function getActiveCertificateDetails( + uint256 tokenId + ) external view returns (CertificateDetails memory); + function getIssuerSignatureCount(uint256 tokenId) external view returns (uint256); + function getIssuerSignatureAt(uint256 tokenId, uint256 index) external view returns (bytes memory); function addCertLegend(uint256 tokenId, string memory newLegend) external; function removeCertLegendAt(uint256 tokenId, uint256 index) external; function addDefaultLegend(string memory newLegend) external; @@ -123,7 +131,10 @@ interface ICyberCertPrinter is IERC721 { address endorsee ); function tokenURI(uint256 tokenId) external view returns (string memory); + function certificateUri() external view returns (string memory); function totalSupply() external view returns (uint256); + function tokenByIndex(uint256 index) external view returns (uint256); 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; } diff --git a/src/interfaces/ICyberCorp.sol b/src/interfaces/ICyberCorp.sol index 458c6151..8f747a5e 100644 --- a/src/interfaces/ICyberCorp.sol +++ b/src/interfaces/ICyberCorp.sol @@ -68,6 +68,10 @@ interface ICyberCorp { function setDealManager(address _dealManager) external; function setRoundManager(address _roundManager) external; function roundManager() external view returns (address); + function addEscrowedOfficerSignature(bytes calldata signature) external; + function setEscrowedOfficerSignature(uint256 index, bytes calldata signature) external; + function getEscrowedOfficerSignature(uint256 index) external view returns (bytes memory); + function getEscrowedOfficerSignatureCount() external view returns (uint256); } diff --git a/src/interfaces/ICyberScrip.sol b/src/interfaces/ICyberScrip.sol index f1fec50e..c7d67bf8 100644 --- a/src/interfaces/ICyberScrip.sol +++ b/src/interfaces/ICyberScrip.sol @@ -6,8 +6,10 @@ import "./ITransferRestrictionHook.sol"; interface ICyberScrip is IERC20 { error NotTransferable(); error RestrictedTransfer(string reason); + error HolderLimitExceeded(uint256 limit); function initialize( + address _auth, address _certPrinter, address _issuanceManager, string calldata _name, @@ -21,8 +23,18 @@ interface ICyberScrip is IERC20 { function setRestrictionHook(ITransferRestrictionHook[] calldata _typeRestrictionHook) external; function certPrinter() external view returns (address); - function IssuanceManager() external view returns (address); + function issuanceManager() external view returns (address); function transferRestrictionHooks(uint256 index) external view returns (ITransferRestrictionHook); + function transferRestrictionHooksLength() external view returns (uint256); function mint(address to, uint256 amount) external; function burnFrom(address account, uint256 amount) external; + function disableForceTransfer() external; + function disableForceBurn() external; + function disableFreeze() external; + function setFrozen(address account, bool isFrozen) external; + function forceTransfer(address from, address to, uint256 amount) external; + function forceBurn(address account, uint256 amount) external; + function holderCount() external view returns (uint256); + function maxHolderCount() external view returns (uint256); + function setMaxHolderCount(uint256 maxHolders) external; } \ No newline at end of file diff --git a/src/interfaces/IIssuanceManager.sol b/src/interfaces/IIssuanceManager.sol index 961a5a81..66884058 100644 --- a/src/interfaces/IIssuanceManager.sol +++ b/src/interfaces/IIssuanceManager.sol @@ -41,24 +41,57 @@ except with the express prior written permission of the copyright holder.*/ pragma solidity 0.8.28; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import "./ICyberCorp.sol"; +import "openzeppelin-contracts/proxy/beacon/UpgradeableBeacon.sol"; import "./ITransferRestrictionHook.sol"; +import "./ICondition.sol"; import "../CyberCorpConstants.sol"; import "../storage/CyberCertPrinterStorage.sol"; -//Adapter interface for custom auth roles. Allows extensibility for different auth protocols i.e. hats. -interface IIssuanceManager is IERC721, IERC721Enumerable, IERC721Metadata { - +interface IIssuanceManager { // Events - event CertificateCreated(uint256 indexed tokenId, address indexed investor, uint256 amount, uint256 cap); - event Converted(uint256 indexed oldTokenId, uint256 indexed newTokenId); - event CertificateSigned(uint256 indexed tokenId, string signatureURI); - event CertificateEndorsed(uint256 indexed tokenId, address indexed endorser, string signatureURI); - event HookStatusChanged(bool enabled); - event WhitelistUpdated(address indexed account, bool whitelisted); + event ScripifiedCert( + address indexed certAddress, + uint256 indexed id, + address indexed scripifiedCert + ); + event CertPrinterCreated( + address indexed certificate, + address indexed corp, + string[] ledger, + string name, + string ticker, + SecurityClass securityType, + SecuritySeries securitySeries, + string certificateUri + ); + event CertificateCreated( + uint256 indexed tokenId, + address indexed certificate, + uint256 amount, + uint256 cap, + CertificateDetails details, + string tokenURI + ); + event CompanyDetailsUpdated(string companyName, string jurisdiction); + event CertPrinterBeaconImplementationUpgraded(address implementation); + event ScripBeaconImplementationUpgraded(address implementation); + event ScripToCertMinimumSet(address indexed certAddress, uint256 minimum); + event ScripRecertified( + address indexed certAddress, + address indexed user, + uint256 indexed certId, + uint256 scripAmount, + uint256 oldUnitsRepresented, + uint256 newUnitsRepresented + ); + event ScripAddedToExistingCert( + address indexed certAddress, + address indexed user, + uint256 indexed certId, + uint256 oldUnitsRepresented, + uint256 newUnitsRepresented + ); // Issuance Manager Functions function initialize( @@ -73,7 +106,7 @@ interface IIssuanceManager is IERC721, IERC721Enumerable, IERC721Metadata { string memory _name, string memory _ticker, string memory _certificateUri, - SecurityClass _securityClass, + SecurityClass _securityType, SecuritySeries _securitySeries, address _extension ) external returns (address); @@ -98,23 +131,43 @@ interface IIssuanceManager is IERC721, IERC721Enumerable, IERC721Metadata { CertificateDetails memory _details ) external returns (uint256 tokenId); + function createCertAndAssignWithName( + address certAddress, + address investor, + CertificateDetails memory _details, + string calldata investorName, + bytes calldata endorsementSignature, + uint256 timestamp + ) external returns (uint256 tokenId); + + function createCertSignAndAssign( + address certAddress, + address investor, + CertificateDetails memory _details, + bytes calldata endorsementSignature, + address registry, + bytes32 agreementId, + string calldata investorName + ) external returns (uint256 tokenId); + function signCertificate( address certAddress, uint256 tokenId, - string calldata signatureURI + bytes calldata signature ) external; - function endorseCertificate( + function addOfficerSignature( address certAddress, uint256 tokenId, - address endorser, - string calldata signatureURI + bytes calldata signature ) external; - function updateCertificateDetails( + function endorseCertificate( address certAddress, uint256 tokenId, - CertificateDetails memory _details + address endorser, + bytes calldata signature, + bytes32 agreementId ) external; function voidCertificate( @@ -122,11 +175,9 @@ interface IIssuanceManager is IERC721, IERC721Enumerable, IERC721Metadata { uint256 tokenId ) external; - function convert( + function setGlobalTransferable( address certAddress, - uint256 tokenId, - address convertTo, - uint256 stockAmount + bool transferable ) external; function getUpgradeFactory() external view returns (address); @@ -143,27 +194,15 @@ interface IIssuanceManager is IERC721, IERC721Enumerable, IERC721Metadata { function getScripBeaconImplementation() external view returns (address); - // Certificate Details Functions - function getCertificateDetails( - uint256 tokenId - ) external view returns (CertificateDetails memory); - - function getEndorsementHistory( - uint256 tokenId, - uint256 index - ) external view returns ( - address endorser, - string memory signatureURI, - uint256 timestamp - ); - // Transfer Hook Functions function setRestrictionHook( + address certAddress, uint256 _id, address _hookAddress ) external; function setGlobalRestrictionHook( + address certAddress, address hookAddress ) external; @@ -173,21 +212,149 @@ interface IIssuanceManager is IERC721, IERC721Enumerable, IERC721Metadata { bool value ) external; - function restrictionHooksById( - uint256 tokenId - ) external view returns (ITransferRestrictionHook); + function addDefaultLegend( + address certAddress, + string memory newLegend + ) external; + + function removeDefaultLegendAt( + address certAddress, + uint256 index + ) external; + + function addCertLegend( + address certAddress, + uint256 tokenId, + string memory newLegend + ) external; + + function removeCertLegendAt( + address certAddress, + uint256 tokenId, + uint256 index + ) external; + + function deployCyberScrip( + address certAddress, + ITransferRestrictionHook[] memory typeRestrictionHooks, + ICondition[] memory certToScripConditions, + ICondition[] memory scripToCertConditions, + uint256 scripToCertMinimum, + uint256 scripRatioNumerator, + uint256 scripRatioDenominator, + uint256[] memory scripifyWhitelistIds, + bool scripifyWhitelistEnabled, + bool enableForceTransfer, + bool enableForceBurn, + bool enableFreeze + ) external returns (address); + + function scripifyCert( + address certAddress, + uint256 id, + uint256 amount, + address recipient + ) external; + + function setScripRatio( + address certAddress, + uint256 numerator, + uint256 denominator + ) external; + + function getScripRatio( + address certAddress + ) external view returns (uint256 numerator, uint256 denominator); + + function setScripToCertMinimum( + address certAddress, + uint256 minimum + ) external; + + function getScripToCertMinimum( + address certAddress + ) external view returns (uint256); + + function setRecertificationApproval( + address certAddress, + address investor, + string calldata investorName, + CertificateDetails calldata details + ) external; + + function clearRecertificationApproval( + address certAddress, + address investor + ) external; - function globalRestrictionHook() external view returns (ITransferRestrictionHook); + function getRecertificationApproval( + address certAddress, + address investor + ) + external + view + returns ( + bool approved, + string memory investorName, + CertificateDetails memory details + ); + + function setScripifyWhitelistEnabled( + address certAddress, + bool enabled + ) external; + + function addScripifyWhitelistIds( + address certAddress, + uint256[] memory ids + ) external; + + function removeScripifyWhitelistIds( + address certAddress, + uint256[] memory ids + ) external; + + function getScripifyWhitelistEnabled( + address certAddress + ) external view returns (bool); + + function isScripifyWhitelisted( + address certAddress, + uint256 id + ) external view returns (bool); + + function getCertScripifiedStatus( + address certAddress, + uint256 id + ) + external + view + returns (bool isScripified, uint256 scripifiedUnits, uint256 maxUnitsRepresented); + + function getScripPoolAmountById( + address certAddress, + uint256 id + ) external view returns (uint256); + + function getScripPoolSharesById( + address certAddress, + uint256 id + ) external view returns (uint256); + + function convertScripToCert( + address certAddress, + uint256 amount + ) external; - // Beacon Functions - function CyberCertPrinterBeacon() external view returns (address); + // Beacon / Config Functions function CORP() external view returns (address); function uriBuilder() external view returns (address); - function certifications(uint256) external view returns (address); function companyName() external view returns (string memory); function companyJurisdiction() external view returns (string memory); function AUTH() external view returns (address); function DEPLOY_VERSION() external view returns (string memory); - function cyberCertPrinterBeacon() external view returns (address); - function cyberScripBeacon() external view returns (address); + function cyberCertPrinterBeacon() external view returns (UpgradeableBeacon); + function cyberScripBeacon() external view returns (UpgradeableBeacon); + function printers(uint256 index) external view returns (address); + function setUriBuilder(address _uriBuilder) external; } \ No newline at end of file diff --git a/src/interfaces/IRoundManagerFactory.sol b/src/interfaces/IRoundManagerFactory.sol index accd8b29..c95cf30d 100644 --- a/src/interfaces/IRoundManagerFactory.sol +++ b/src/interfaces/IRoundManagerFactory.sol @@ -41,12 +41,20 @@ except with the express prior written permission of the copyright holder.*/ pragma solidity 0.8.28; +struct FeeOverride { + bool enabled; + uint256 ratio; +} + interface IRoundManagerFactory { function deployRoundManager(bytes32 _salt) external returns (address); function getRefImplementation() external view returns (address); function getDefaultFeeRatio() external view returns (uint256); + function getUnderlyingDefaultFeeRatio() external view returns (uint256); + function getInstanceFeeOverride(address roundManager) external view returns (FeeOverride memory); + function setInstanceFeeOverride(address roundManager, bool enabled, uint256 ratio) external; function getPlatformPayable() external view returns (address); function isWhitelistedToken(address token) external view returns (bool); function setWhitelistedToken(address token, bool isWhitelisted) external; diff --git a/src/interfaces/IZKPassportVerifier.sol b/src/interfaces/IZKPassportVerifier.sol new file mode 100644 index 00000000..16cede7d --- /dev/null +++ b/src/interfaces/IZKPassportVerifier.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.28; + +struct BoundData { + address senderAddress; + uint256 chainId; + string customData; +} + +struct DisclosedData { + string name; + string issuingCountry; + string nationality; + string gender; + string birthDate; + string expiryDate; + string documentNumber; + string documentType; +} + +struct ProofVerificationParams { + bytes32 version; + ProofVerificationData proofVerificationData; + bytes committedInputs; + ServiceConfig serviceConfig; +} + +struct ProofVerificationData { + bytes32 vkeyHash; + bytes proof; + bytes32[] publicInputs; +} + +struct ServiceConfig { + uint256 validityPeriodInSeconds; + string domain; + string scope; + bool devMode; +} + +interface IZKPassportVerifier { + function verify(ProofVerificationParams calldata params) + external + returns (bool verified, bytes32 uniqueIdentifier, IZKPassportHelper helper); +} + +interface IZKPassportHelper { + function verifyScopes( + bytes32[] calldata publicInputs, + string calldata domain, + string calldata scope + ) external pure returns (bool); + + function getBoundData( + bytes calldata committedInputs + ) external pure returns (BoundData memory); + + function getProofTimestamp( + bytes32[] calldata publicInputs + ) external pure returns (uint256); + + function isNationalityOut( + string[] memory countryList, + bytes calldata committedInputs + ) external view returns (bool); + + function enforceSanctionsRoot( + uint256 currentTimestamp, + bool isStrict, + bytes calldata committedInputs + ) external view; +} diff --git a/src/libs/LexScroWLite.sol b/src/libs/LexScroWLite.sol index e9c7bec5..1fa5dc54 100644 --- a/src/libs/LexScroWLite.sol +++ b/src/libs/LexScroWLite.sol @@ -124,6 +124,24 @@ abstract contract LexScroWLite is Initializable { for(uint256 i = 0; i < escrow.corpAssets.length; i++) { if(escrow.corpAssets[i].tokenType == TokenType.ERC721) { ICyberCertPrinter(escrow.corpAssets[i].tokenAddress).addEndorsement(escrow.corpAssets[i].tokenId, newEndorsement); + // check if there is an escrowed officer signature in cybercorp + bytes memory officerSignature = ""; + address corp = LexScrowStorage.getCorp(); + try ICyberCorp(corp).getEscrowedOfficerSignatureCount() returns ( + uint256 count + ) { + if (count > 0) { + try ICyberCorp(corp).getEscrowedOfficerSignature(0) returns (bytes memory sig) { + officerSignature = sig; + } catch {} + } + } catch {} + if (officerSignature.length > 0) { + ICyberCertPrinter(escrow.corpAssets[i].tokenAddress).addIssuerSignature( + escrow.corpAssets[i].tokenId, + officerSignature + ); + } } } } diff --git a/src/libs/RoundLib.sol b/src/libs/RoundLib.sol index 5fc75c4a..3040def6 100644 --- a/src/libs/RoundLib.sol +++ b/src/libs/RoundLib.sol @@ -78,6 +78,7 @@ struct Round { bytes escrowedSignature; bool publicRound; bool allowTimedOffers; // if false, ignore EOI expiries and use round end + bool restrictEndTimeReduction; // if true, owner cannot reduce endTime after creation } library RoundLib { @@ -106,6 +107,7 @@ library RoundLib { RoundType roundType, bool publicRound, bool allowTimedOffers, + bool restrictEndTimeReduction, uint256 raiseCap, uint256 minTicket, uint256 maxTicket, @@ -119,6 +121,7 @@ library RoundLib { round.roundType = roundType; round.publicRound = publicRound; round.allowTimedOffers = allowTimedOffers; + round.restrictEndTimeReduction = restrictEndTimeReduction; round.raiseCap = raiseCap; round.minTicket = minTicket; round.maxTicket = maxTicket; diff --git a/src/libs/conditions/IssuerApprovalRecertificationCondition.sol b/src/libs/conditions/IssuerApprovalRecertificationCondition.sol new file mode 100644 index 00000000..bb3af1b2 --- /dev/null +++ b/src/libs/conditions/IssuerApprovalRecertificationCondition.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.28; + +import "../../interfaces/IIssuanceManager.sol"; +import "../../interfaces/ICyberScrip.sol"; +import "../../interfaces/ICondition.sol"; +import "../auth.sol"; +import "./baseCondition.sol"; + +interface ICertWithIssuanceManager { + function issuanceManager() external view returns (address); +} + +/// @title IssuerApprovalRecertificationCondition +/// @notice Requires issuer admin approval before scrip-to-cert conversion. +/// @dev This contract is intended to be deployed once and reused as a singleton condition. +contract IssuerApprovalRecertificationCondition is BaseCondition { + error InvalidContractAddress(); + error InvalidInvestor(); + + event InvestorApprovalUpdated( + address indexed certAddress, + address indexed investor, + bool approved, + address indexed approver + ); + + bytes4 private constant _CONVERT_SCRIP_TO_CERT_SELECTOR = + bytes4(keccak256("convertScripToCert(address,uint256)")); + + mapping(address => mapping(address => bool)) private _investorApprovals; + + function setInvestorApproval( + address certOrScrip, + address investor, + bool approved + ) external { + if (certOrScrip == address(0)) revert InvalidContractAddress(); + if (investor == address(0)) revert InvalidInvestor(); + + address certAddress = _resolveCertAddress(certOrScrip); + address issuanceManager = ICertWithIssuanceManager(certAddress) + .issuanceManager(); + _requireAdmin(issuanceManager); + + _investorApprovals[certAddress][investor] = approved; + emit InvestorApprovalUpdated(certAddress, investor, approved, msg.sender); + } + + function isInvestorApproved( + address certOrScrip, + address investor + ) external view returns (bool) { + address certAddress = _resolveCertAddress(certOrScrip); + return _investorApprovals[certAddress][investor]; + } + + function checkCondition( + address _contract, + bytes4 _functionSignature, + bytes memory data + ) public view override returns (bool) { + if (_functionSignature != _CONVERT_SCRIP_TO_CERT_SELECTOR) { + return false; + } + + address certAddress = _resolveCertAddress(_contract); + address investor = _resolveInvestor(data); + + if (investor == address(0)) return false; + return _investorApprovals[certAddress][investor]; + } + + function _resolveInvestor(bytes memory data) internal view returns (address) { + (, address investor) = abi.decode(data, (uint256, address)); + return investor; + } + + function _resolveCertAddress( + address certOrScrip + ) internal view returns (address certAddress) { + certAddress = certOrScrip; + try ICyberScrip(certOrScrip).certPrinter() returns (address fromScrip) { + if (fromScrip != address(0)) { + certAddress = fromScrip; + } + } catch {} + } + + function _requireAdmin(address issuanceManager) internal view { + if (issuanceManager == address(0)) revert InvalidContractAddress(); + BorgAuth auth = BorgAuth(IIssuanceManager(issuanceManager).AUTH()); + auth.onlyRole(auth.ADMIN_ROLE(), msg.sender); + } +} diff --git a/src/libs/conditions/NonUSNationalityCondition.sol b/src/libs/conditions/NonUSNationalityCondition.sol new file mode 100644 index 00000000..330faa28 --- /dev/null +++ b/src/libs/conditions/NonUSNationalityCondition.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.28; + +import "@openzeppelin/contracts/interfaces/IERC165.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./baseCondition.sol"; +import "../LexScroWLite.sol"; +import "../auth.sol"; +import "../../interfaces/IZKPassportVerifier.sol"; + +/// @title NonUSNationalityCondition +/// @notice Round condition requiring a valid, non-US ZKPassport proof for the participant +contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { + error InvalidVerifier(); + error InvalidProof(); + error InvalidScope(); + error InvalidBoundSender(); + error InvalidBoundChainId(); + error InvalidMaxValidityPeriod(); + error USAOrSanctionedCountriesNotAllowed(); + error ProofExpired(); + error ProofAlreadyUsed(); + error MaxValidityPeriodExceeded(); + + event ProofSubmitted( + address indexed account, + uint256 expiresAt + ); + + event MaxValidityPeriodUpdated(uint256 maxValidityPeriod); + event ExcludedCountriesUpdated(string[] countries); + + // Deterministic verifier address from ZKPassport docs. + address public constant DEFAULT_ZKPASSPORT_VERIFIER = + 0x1D000001000EFD9a6371f4d90bB8920D5431c0D8; + + IZKPassportVerifier public verifier; + string public expectedDomain; + string public expectedScope; + uint256 public maxValidityPeriod; + + mapping(address => uint256) public proofExpiry; + mapping(bytes32 => bool) public usedProofIdentifiers; + + string[] public excludedCountries; + + /// @notice initialize atomically since this is not an upgradeable contract + constructor( + address _auth, + string memory _expectedDomain, + string memory _expectedScope, + address _verifier, + uint256 _maxValidityPeriod, + string[] memory _excludedCountries + ) { + initialize( + _auth, + _expectedDomain, + _expectedScope, + _verifier, + _maxValidityPeriod, + _excludedCountries + ); + } + + function initialize( + address _auth, + string memory _expectedDomain, + string memory _expectedScope, + address _verifier, + uint256 _maxValidityPeriod, + string[] memory _excludedCountries + ) public initializer { + __BorgAuthACL_init(_auth); + + expectedDomain = _expectedDomain; + expectedScope = _expectedScope; + + address resolvedVerifier = _verifier == address(0) + ? DEFAULT_ZKPASSPORT_VERIFIER + : _verifier; + if (resolvedVerifier == address(0)) revert InvalidVerifier(); + verifier = IZKPassportVerifier(resolvedVerifier); + + if(_maxValidityPeriod == 0) revert InvalidMaxValidityPeriod(); + maxValidityPeriod = _maxValidityPeriod; + emit MaxValidityPeriodUpdated(_maxValidityPeriod); + + excludedCountries = _excludedCountries; + emit ExcludedCountriesUpdated(_excludedCountries); + } + + function updateMaxValidityPeriod(uint256 _maxValidityPeriod) external onlyAdmin { + if(_maxValidityPeriod == 0) revert InvalidMaxValidityPeriod(); + maxValidityPeriod = _maxValidityPeriod; + emit MaxValidityPeriodUpdated(_maxValidityPeriod); + } + + function updateExcludedCountries(string[] calldata _excludedCountries) external onlyAdmin { + excludedCountries = _excludedCountries; + emit ExcludedCountriesUpdated(_excludedCountries); + } + + /// @notice Submit and verify ZKPassport proof, then cache non-US eligibility for the caller + function submitProof( + ProofVerificationParams calldata params, + bool isIDCard + ) external { + (bool verified, bytes32 uniqueIdentifier, IZKPassportHelper helper) = verifier.verify(params); + if (!verified || address(helper) == address(0)) revert InvalidProof(); + + if (usedProofIdentifiers[uniqueIdentifier]) revert ProofAlreadyUsed(); + usedProofIdentifiers[uniqueIdentifier] = true; + + if ( + !helper.verifyScopes( + params.proofVerificationData.publicInputs, + expectedDomain, + expectedScope + ) + ) { + revert InvalidScope(); + } + + BoundData memory boundData = helper.getBoundData(params.committedInputs); + if (boundData.senderAddress != msg.sender) revert InvalidBoundSender(); + if (boundData.chainId != block.chainid) revert InvalidBoundChainId(); + + if(!helper.isNationalityOut(excludedCountries, params.committedInputs)) revert USAOrSanctionedCountriesNotAllowed(); + + uint256 proofTimestamp = helper.getProofTimestamp( + params.proofVerificationData.publicInputs + ); + + // Check against the sanctioned watchlist at the time of the proof + helper.enforceSanctionsRoot( + proofTimestamp, + false, + params.committedInputs + ); + + uint256 validityPeriod = params.serviceConfig.validityPeriodInSeconds; + if (validityPeriod > maxValidityPeriod) revert MaxValidityPeriodExceeded(); + + uint256 expiresAt = proofTimestamp + validityPeriod; + if (expiresAt < block.timestamp) revert ProofExpired(); + + proofExpiry[msg.sender] = expiresAt; + emit ProofSubmitted(msg.sender, expiresAt); + } + + /// @notice Condition check used by LexScroWLite.conditionCheck + function checkCondition( + address _contract, + bytes4, + bytes memory data + ) public view override returns (bool) { + LexScroWLite lexScrow = LexScroWLite(_contract); + bytes32 agreementId = abi.decode(data, (bytes32)); + address counterparty = lexScrow.getEscrowDetails(agreementId).counterParty; + return proofExpiry[counterparty] >= block.timestamp; + } + +} diff --git a/src/libs/conditions/OrCondition.sol b/src/libs/conditions/OrCondition.sol new file mode 100644 index 00000000..01aa1ff2 --- /dev/null +++ b/src/libs/conditions/OrCondition.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {BaseCondition} from "./baseCondition.sol"; +import {ICondition} from "../../interfaces/ICondition.sol"; + +contract OrCondition is BaseCondition { + error NeedMoreConditions(); + + ICondition[] public conditions; + + constructor(address[] memory _conditions) { + if (_conditions.length < 2) revert NeedMoreConditions(); + for (uint256 i = 0; i < _conditions.length; i++) { + conditions.push(ICondition(_conditions[i])); + } + } + + function checkCondition( + address _contract, + bytes4 _functionSignature, + bytes memory data + ) public view override returns (bool) { + for (uint256 i = 0; i < conditions.length; i++) { + if (conditions[i].checkCondition(_contract, _functionSignature, data)) + return true; + } + return false; + } +} diff --git a/src/storage/CyberCertPrinterStorage.sol b/src/storage/CyberCertPrinterStorage.sol index f3168253..99e35817 100644 --- a/src/storage/CyberCertPrinterStorage.sol +++ b/src/storage/CyberCertPrinterStorage.sol @@ -99,6 +99,7 @@ library CyberCertPrinterStorage { bool endorsementRequired; // New variables must be appended below to preserve storage layout for upgrades mapping(uint256 => bool) tokenTransferable; + mapping(uint256 => bytes[]) issuerSignatures; } @@ -115,6 +116,9 @@ library CyberCertPrinterStorage { 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); @@ -134,7 +138,7 @@ library CyberCertPrinterStorage { s.securitySeries, s.certificateUri, certLegend, - s.certificateDetails[tokenId], + effectiveDetails, s.endorsements[tokenId], s.owners[tokenId], registry, @@ -146,10 +150,36 @@ library CyberCertPrinterStorage { } // Internal getters for complex types - function getCertificateDetails(uint256 tokenId) internal view returns (CertificateDetails storage) { + function getStoredCertificateDetails(uint256 tokenId) internal view returns (CertificateDetails storage) { return cyberCertStorage().certificateDetails[tokenId]; } + function getActiveCertificateDetails( + uint256 tokenId + ) internal view returns (CertificateDetails memory details) { + details = cyberCertStorage().certificateDetails[tokenId]; + } + + function getCertificateDetails( + uint256 tokenId + ) internal view returns (CertificateDetails memory details) { + CyberCertStorage storage s = cyberCertStorage(); + details = getActiveCertificateDetails(tokenId); + + ( + bool isScripified, + uint256 scripifiedUnits, + uint256 _maxUnitsRepresented + ) = IIssuanceManager(s.issuanceManager).getCertScripifiedStatus( + address(this), + tokenId + ); + + if (isScripified) { + details.unitsRepresented = details.unitsRepresented + scripifiedUnits; + } + } + function getEndorsements(uint256 tokenId) internal view returns (Endorsement[] storage) { return cyberCertStorage().endorsements[tokenId]; } diff --git a/src/storage/CyberScripStorage.sol b/src/storage/CyberScripStorage.sol index f0aa2b8f..2f5027f1 100644 --- a/src/storage/CyberScripStorage.sol +++ b/src/storage/CyberScripStorage.sol @@ -60,6 +60,10 @@ library CyberScripStorage { // Per-account freeze registry mapping(address => bool) frozen; + + // Holder count tracking + uint256 holderCount; + uint256 maxHolderCount; } // Returns the storage layout diff --git a/src/storage/IssuanceManagerStorage.sol b/src/storage/IssuanceManagerStorage.sol index 71e45af9..c11f209d 100644 --- a/src/storage/IssuanceManagerStorage.sol +++ b/src/storage/IssuanceManagerStorage.sol @@ -41,10 +41,106 @@ except with the express prior written permission of the copyright holder.*/ pragma solidity 0.8.28; +import "openzeppelin-contracts/proxy/beacon/BeaconProxy.sol"; import "openzeppelin-contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "openzeppelin-contracts/utils/Create2.sol"; import "../interfaces/ICondition.sol"; +import "../interfaces/ICyberCertPrinter.sol"; +import "../interfaces/ICyberCorp.sol"; +import "../interfaces/ICyberScrip.sol"; +import "../interfaces/IIssuanceManager.sol"; +import "../interfaces/ITransferRestrictionHook.sol"; +import "./CyberCertPrinterStorage.sol"; library IssuanceManagerStorage { + error ConditionCheckFailed(); + error ScripifiedCertNotAllowed(); + error ScripToCertMinimumNotMet(); + error ScripifyNotWhitelisted(); + error ScripifyOverMax(); + error RecertificationApprovalRequired(); + error CompanyDetailsNotSet(); + error InvalidScripRatio(); + error SignatureRequired(); + error InvalidInvestor(); + error InvalidInvestorName(); + error InvalidAmount(); + error CertificateVoided(); + error NotLegalOwner(); + error AmountExceedsAvailableUnits(); + error ZeroSharesMinted(); + error EmptyVault(); + error VaultRedemptionExceedsClaim(); + error VaultWithdrawalExceedsAssets(); + + /// @dev Ray precision for vault price-per-share (assets per 1 nominal share, 1e27 = 1.0). + uint256 internal constant VAULT_RAY = 1e27; + + event ScripifiedCert( + address indexed certAddress, + uint256 indexed id, + address indexed scripifiedCert, + uint256 amount + ); + event CertPrinterCreated( + address indexed certificate, + address indexed corp, + string[] ledger, + string name, + string ticker, + SecurityClass securityType, + SecuritySeries securitySeries, + string certificateUri + ); + event CertificateCreated( + uint256 indexed tokenId, + address indexed certificate, + uint256 amount, + uint256 cap, + CertificateDetails details, + string tokenURI + ); + event ScripToCertMinimumSet(address indexed certAddress, uint256 minimum); + event ScripifyWhitelistEnabledSet(address indexed certAddress, bool enabled); + event ScripifyWhitelistUpdated( + address indexed certAddress, + uint256 indexed id, + bool isWhitelisted + ); + event CyberScripDeployed( + address indexed certPrinterAddress, + address indexed cyberScripAddress, + uint256 scripRatioNumerator, + uint256 scripRatioDenominator, + bool enableForceTransfer, + bool enableForceBurn, + bool enableFreeze + ); + event RecertificationApprovalSet( + address indexed certAddress, + address indexed investor, + string investorName + ); + event RecertificationApprovalCleared( + address indexed certAddress, + address indexed investor + ); + event ScripRecertified( + address indexed certAddress, + address indexed user, + uint256 indexed certId, + uint256 scripAmount, + uint256 oldUnitsRepresented, + uint256 newUnitsRepresented + ); + event ScripAddedToExistingCert( + address indexed certAddress, + address indexed user, + uint256 indexed certId, + uint256 oldUnitsRepresented, + uint256 newUnitsRepresented + ); + // Storage slot for our struct bytes32 constant STORAGE_POSITION = keccak256("cybercorp.issuancemanager.storage.v1"); @@ -59,6 +155,46 @@ library IssuanceManagerStorage { mapping(address => address) scripifiedCert; mapping(address => ICondition[]) certToScripConditions; mapping(address => ICondition[]) scripToCertConditions; + mapping(address => uint256) scripToCertMinimums; + mapping(address => ScripRatio) scripRatios; + mapping(address => bool) scripifyWhitelistEnabled; + mapping(address => mapping(uint256 => bool)) scripifyWhitelist; + mapping(address => mapping(uint256 => CertScripState)) certScripStates; + mapping(address => CertScripUnitPool) certScripUnitPools; + mapping(address => mapping(address => RecertificationApproval)) + recertificationApprovals; + } + + struct ScripRatio { + uint256 numerator; + uint256 denominator; + } + + struct RecertSelection { + bool foundActive; + uint256 activeTokenId; + } + + /// @notice Per-certificate vault position: nominal shares in the scripified-units vault. + /// Claim on underlying (wad) = vaultNominalShares * totalAssetsWad / totalNominalShares. + /// @dev Three slots preserve layout vs legacy (amount, reductionDebt, maxUnitsRepresented). + struct CertScripState { + uint256 vaultNominalShares; + /// @dev Legacy `reductionDebt` slot — unused after ERC4626 vault migration. + uint256 deprecatedMasterChefDebtSlot; + uint256 maxUnitsRepresented; + } + + /// @notice ERC4626-style pool for scripified certificate units (underlying in 18-dec wad). + struct CertScripUnitPool { + uint256 totalAssetsWad; + uint256 totalNominalShares; + } + + struct RecertificationApproval { + bool approved; + string investorName; + CertificateDetails details; } // Returns the storage layout @@ -86,14 +222,6 @@ library IssuanceManagerStorage { return issuanceManagerStorage().printers; } - function getPrinterAt(uint256 index) internal view returns (address) { - return issuanceManagerStorage().printers[index]; - } - - function getPrintersCount() internal view returns (uint256) { - return issuanceManagerStorage().printers.length; - } - // Setters function setCORP(address _corp) internal { issuanceManagerStorage().CORP = _corp; @@ -178,10 +306,1032 @@ library IssuanceManagerStorage { } } + function getScripToCertMinimum(address certAddress) internal view returns (uint256) { + return issuanceManagerStorage().scripToCertMinimums[certAddress]; + } + + function setScripToCertMinimum(address certAddress, uint256 minimum) internal { + issuanceManagerStorage().scripToCertMinimums[certAddress] = minimum; + } + + function isScripifyWhitelisted( + address certAddress, + uint256 id + ) internal view returns (bool) { + return issuanceManagerStorage().scripifyWhitelist[certAddress][id]; + } + + function setScripifyWhitelistEnabled( + address certAddress, + bool enabled + ) internal { + issuanceManagerStorage().scripifyWhitelistEnabled[certAddress] = enabled; + } + + function getScripifyWhitelistEnabled( + address certAddress + ) internal view returns (bool) { + return issuanceManagerStorage().scripifyWhitelistEnabled[certAddress]; + } + + function setScripifyWhitelisted( + address certAddress, + uint256 id, + bool isWhitelisted + ) internal { + issuanceManagerStorage().scripifyWhitelist[certAddress][id] = isWhitelisted; + } + function setCertToScripConditions(address certAddress, ICondition[] memory conditions) internal { delete issuanceManagerStorage().certToScripConditions[certAddress]; for (uint i = 0; i < conditions.length; i++) { issuanceManagerStorage().certToScripConditions[certAddress].push(conditions[i]); } } + + function getScripRatio(address certAddress) internal view returns (ScripRatio storage) { + return issuanceManagerStorage().scripRatios[certAddress]; + } + + function setScripRatio(address certAddress, uint256 numerator, uint256 denominator) internal { + issuanceManagerStorage().scripRatios[certAddress] = ScripRatio({ + numerator: numerator, + denominator: denominator + }); + } + + function getCertScripState( + address certAddress, + uint256 id + ) internal view returns (CertScripState storage) { + return issuanceManagerStorage().certScripStates[certAddress][id]; + } + + /// @notice Underlying wad claim for one certificate’s vault position (pro-rata on total pool). + function _assetsOfVaultPosition( + address certAddress, + uint256 tokenId + ) internal view returns (uint256 assetsWad) { + CertScripUnitPool storage pool = issuanceManagerStorage().certScripUnitPools[ + certAddress + ]; + if (pool.totalNominalShares == 0) { + return 0; + } + CertScripState storage certState = getCertScripState(certAddress, tokenId); + return + certState.vaultNominalShares * pool.totalAssetsWad / pool.totalNominalShares; + } + + /// @dev Scrip-token-equivalent claim for a single certificate's vault position. + function getScripPoolAmountById( + address certAddress, + uint256 tokenId + ) internal view returns (uint256 scripEquivalent) { + (uint256 num, uint256 den) = _getScripRatioOrDefault(certAddress); + uint256 assetsWad = _assetsOfVaultPosition(certAddress, tokenId); + if (assetsWad == 0) return 0; + return assetsWad * num / den; + } + + /// @dev Nominal vault shares held by a single certificate. + function getScripPoolSharesById( + address certAddress, + uint256 tokenId + ) internal view returns (uint256 shares) { + return getCertScripState(certAddress, tokenId).vaultNominalShares; + } + + function getRecertificationApproval( + address certAddress, + address investor + ) internal view returns (RecertificationApproval storage) { + return issuanceManagerStorage().recertificationApprovals[certAddress][ + investor + ]; + } + + function getRecertificationApprovalData( + address certAddress, + address investor + ) + internal + view + returns ( + bool approved, + string memory investorName, + CertificateDetails memory details + ) + { + RecertificationApproval storage approval = getRecertificationApproval( + certAddress, + investor + ); + approved = approval.approved; + investorName = approval.investorName; + details = approval.details; + } + + function setRecertificationApproval( + address certAddress, + address investor, + string memory investorName, + CertificateDetails memory details + ) internal { + issuanceManagerStorage().recertificationApprovals[certAddress][ + investor + ] = RecertificationApproval({ + approved: true, + investorName: investorName, + details: details + }); + } + + function clearRecertificationApproval( + address certAddress, + address investor + ) internal { + delete issuanceManagerStorage().recertificationApprovals[certAddress][ + investor + ]; + } + + /// @return totalTrackedScrip ERC20 scrip total supply (canonical circulating scrip). + /// @return pricePerShareRay underlying wad per nominal vault share, ray precision (0 if empty vault). + function getScripPoolTotals( + address certAddress + ) + internal + view + returns (uint256 totalTrackedScrip, uint256 pricePerShareRay) + { + address scrip = getScripifiedCert(certAddress); + totalTrackedScrip = scrip == address(0) ? 0 : ICyberScrip(scrip).totalSupply(); + CertScripUnitPool storage pool = issuanceManagerStorage().certScripUnitPools[ + certAddress + ]; + pricePerShareRay = pool.totalNominalShares == 0 + ? 0 + : pool.totalAssetsWad * VAULT_RAY / pool.totalNominalShares; + } + + function getCertScripUnitVault( + address certAddress + ) internal view returns (uint256 totalAssetsWad, uint256 totalNominalShares) { + CertScripUnitPool storage pool = issuanceManagerStorage().certScripUnitPools[ + certAddress + ]; + totalAssetsWad = pool.totalAssetsWad; + totalNominalShares = pool.totalNominalShares; + } + + function getCertScripifiedStatus( + address certAddress, + uint256 id + ) + internal + view + returns (bool isScripified, uint256 scripifiedUnits, uint256 maxUnitsRepresented) + { + CertScripState storage certState = getCertScripState(certAddress, id); + scripifiedUnits = getCurrentCertScripifiedUnits(certAddress, id); + isScripified = scripifiedUnits > 0; + maxUnitsRepresented = certState.maxUnitsRepresented; + } + + function getCurrentCertScripifiedUnits( + address certAddress, + uint256 id + ) internal view returns (uint256) { + return _assetsOfVaultPosition(certAddress, id); + } + + function executeCreateCertPrinter( + string[] memory ledger, + string memory name, + string memory ticker, + string memory certificateUri, + SecurityClass securityType, + SecuritySeries securitySeries, + address extension + ) external returns (address newCert) { + bytes32 salt = keccak256(abi.encodePacked(getPrinters().length, address(this))); + newCert = Create2.deploy(0, salt, _getBytecodeCertPrinter()); + addPrinter(newCert); + ICyberCertPrinter(newCert).initialize( + ledger, + name, + ticker, + certificateUri, + address(this), + securityType, + securitySeries, + extension + ); + emit CertPrinterCreated( + newCert, + getCORP(), + ledger, + name, + ticker, + securityType, + securitySeries, + certificateUri + ); + } + + function executeCreateCert( + address certAddress, + address to, + CertificateDetails memory details + ) external returns (uint256 id) { + ICyberCertPrinter cert = ICyberCertPrinter(certAddress); + uint256 tokenId = cert.totalSupply(); + id = cert.safeMint(tokenId, to, details); + _emitCertificateCreated(tokenId, certAddress, details, cert.tokenURI(tokenId)); + } + + function executeAssignCert( + address certAddress, + address from, + uint256 tokenId, + address investor, + CertificateDetails memory details + ) external { + ICyberCertPrinter(certAddress).assignCert(from, tokenId, investor, details); + } + + function executeCreateCertAndAssign( + address certAddress, + address investor, + CertificateDetails memory details, + string memory investorName, + bytes memory endorsementSignature, + uint256 timestamp + ) external returns (uint256 tokenId) { + ICyberCertPrinter cert; + string memory tokenURI; + (cert, tokenId, tokenURI) = _mintAssignedCert( + certAddress, + investor, + details, + investorName + ); + + Endorsement memory newEndorsement = Endorsement({ + endorser: address(this), + timestamp: timestamp, + signatureHash: endorsementSignature, + registry: address(0), + agreementId: 0, + endorsee: investor, + endorseeName: investorName + }); + cert.addEndorsement(tokenId, newEndorsement); + + bytes memory escrowedOfficerSignature = _getEscrowedOfficerSignature(); + + if (endorsementSignature.length > 0) { + cert.addIssuerSignature(tokenId, endorsementSignature); + } + if (escrowedOfficerSignature.length > 0) { + cert.addIssuerSignature(tokenId, escrowedOfficerSignature); + } + + _emitCertificateCreated(tokenId, certAddress, details, tokenURI); + } + + function executeCreateCertSignAndAssign( + address certAddress, + address investor, + CertificateDetails memory details, + bytes memory endorsementSignature, + address registry, + bytes32 agreementId, + string memory investorName + ) external returns (uint256 tokenId) { + ICyberCertPrinter cert; + string memory tokenURI; + (cert, tokenId, tokenURI) = _mintAssignedCert( + certAddress, + investor, + details, + investorName + ); + + Endorsement memory newEndorsement = Endorsement({ + endorser: address(this), + timestamp: block.timestamp, + signatureHash: endorsementSignature, + registry: registry, + agreementId: agreementId, + endorsee: investor, + endorseeName: investorName + }); + cert.addEndorsement(tokenId, newEndorsement); + + bytes memory escrowedOfficerSignature = _getEscrowedOfficerSignature(); + + if (endorsementSignature.length > 0) { + cert.addIssuerSignature(tokenId, endorsementSignature); + } + if (escrowedOfficerSignature.length > 0) { + cert.addIssuerSignature(tokenId, escrowedOfficerSignature); + } + + _emitCertificateCreated(tokenId, certAddress, details, tokenURI); + } + + function executeAddIssuerSignature( + address certAddress, + uint256 tokenId, + bytes memory signature + ) external { + if (signature.length == 0) revert SignatureRequired(); + ICyberCertPrinter(certAddress).addIssuerSignature(tokenId, signature); + } + + function executeEndorseCertificate( + address certAddress, + uint256 tokenId, + address endorser, + bytes memory signature, + bytes32 agreementId + ) external { + Endorsement memory newEndorsement = Endorsement({ + endorser: endorser, + timestamp: block.timestamp, + signatureHash: signature, + registry: address(0), + agreementId: agreementId, + endorsee: address(0), + endorseeName: "" + }); + ICyberCertPrinter(certAddress).addEndorsement(tokenId, newEndorsement); + } + + function executeVoidCertificate(address certAddress, uint256 tokenId) external { + ICyberCertPrinter(certAddress).voidCert(tokenId); + } + + function executeSetGlobalTransferable( + address certAddress, + bool transferable + ) external { + ICyberCertPrinter(certAddress).setGlobalTransferable(transferable); + } + + function executeSetRestrictionHook( + address certAddress, + uint256 id, + address hookAddress + ) external { + ICyberCertPrinter(certAddress).setRestrictionHook(id, hookAddress); + } + + function executeSetGlobalRestrictionHook( + address certAddress, + address hookAddress + ) external { + ICyberCertPrinter(certAddress).setGlobalRestrictionHook(hookAddress); + } + + function executeSetTokenTransferable( + address certAddress, + uint256 tokenId, + bool value + ) external { + ICyberCertPrinter(certAddress).setTokenTransferable(tokenId, value); + } + + function executeSetScripRatio( + address certAddress, + uint256 numerator, + uint256 denominator + ) external { + if (numerator == 0 || denominator == 0) revert InvalidScripRatio(); + setScripRatio(certAddress, numerator, denominator); + } + + function executeSetScripToCertMinimum( + address certAddress, + uint256 minimum + ) external { + setScripToCertMinimum(certAddress, minimum); + emit ScripToCertMinimumSet(certAddress, minimum); + } + + function executeSetScripifyWhitelistEnabled( + address certAddress, + bool enabled + ) external { + setScripifyWhitelistEnabled(certAddress, enabled); + emit ScripifyWhitelistEnabledSet(certAddress, enabled); + } + + function executeSetScripifyWhitelistIds( + address certAddress, + uint256[] memory ids, + bool isWhitelisted + ) external { + for (uint256 i = 0; i < ids.length; i++) { + setScripifyWhitelisted(certAddress, ids[i], isWhitelisted); + emit ScripifyWhitelistUpdated(certAddress, ids[i], isWhitelisted); + } + } + + function executeAddDefaultLegend( + address certAddress, + string memory newLegend + ) external { + ICyberCertPrinter(certAddress).addDefaultLegend(newLegend); + } + + function executeRemoveDefaultLegendAt( + address certAddress, + uint256 index + ) external { + ICyberCertPrinter(certAddress).removeDefaultLegendAt(index); + } + + function executeAddCertLegend( + address certAddress, + uint256 tokenId, + string memory newLegend + ) external { + ICyberCertPrinter(certAddress).addCertLegend(tokenId, newLegend); + } + + function executeRemoveCertLegendAt( + address certAddress, + uint256 tokenId, + uint256 index + ) external { + ICyberCertPrinter(certAddress).removeCertLegendAt(tokenId, index); + } + + function executeDeployCyberScrip( + address certAddress, + address auth, + ITransferRestrictionHook[] memory typeRestrictionHooks, + ICondition[] memory certToScripConditions, + ICondition[] memory scripToCertConditions, + uint256 scripToCertMinimum, + uint256 scripRatioNumerator, + uint256 scripRatioDenominator, + uint256[] memory scripifyWhitelistIds, + bool scripifyWhitelistEnabled, + bool enableForceTransfer, + bool enableForceBurn, + bool enableFreeze + ) external returns (address newScrip) { + if (scripRatioNumerator == 0 || scripRatioDenominator == 0) { + revert InvalidScripRatio(); + } + + bytes32 salt = keccak256(abi.encodePacked(certAddress, address(this))); + newScrip = Create2.deploy(0, salt, _getBytecodeScrip()); + emit CyberScripDeployed( + certAddress, + newScrip, + scripRatioNumerator, + scripRatioDenominator, + enableForceTransfer, + enableForceBurn, + enableFreeze + ); + + ICyberScrip(newScrip).initialize( + auth, + certAddress, + address(this), + string( + abi.encodePacked("scrip", ICyberCertPrinter(certAddress).name()) + ), + string( + abi.encodePacked("scrip", ICyberCertPrinter(certAddress).symbol()) + ), + typeRestrictionHooks, + enableForceTransfer, + enableForceBurn, + enableFreeze + ); + + setScripifiedCert(certAddress, newScrip); + setCertToScripConditions(certAddress, certToScripConditions); + setScripToCertConditions(certAddress, scripToCertConditions); + setScripToCertMinimum(certAddress, scripToCertMinimum); + setScripRatio(certAddress, scripRatioNumerator, scripRatioDenominator); + emit ScripToCertMinimumSet(certAddress, scripToCertMinimum); + + setScripifyWhitelistEnabled(certAddress, scripifyWhitelistEnabled); + emit ScripifyWhitelistEnabledSet(certAddress, scripifyWhitelistEnabled); + + for (uint256 i = 0; i < scripifyWhitelistIds.length; i++) { + setScripifyWhitelisted(certAddress, scripifyWhitelistIds[i], true); + emit ScripifyWhitelistUpdated(certAddress, scripifyWhitelistIds[i], true); + } + } + + function executeScripifyCert( + address certAddress, + uint256 id, + uint256 amount, + address target, + address account + ) external { + if (amount == 0) revert InvalidAmount(); + + address scripifiedCert = getScripifiedCert(certAddress); + if (scripifiedCert == address(0)) revert ScripifiedCertNotAllowed(); + + if (getScripifyWhitelistEnabled(certAddress)) { + if (!isScripifyWhitelisted(certAddress, id)) { + revert ScripifyNotWhitelisted(); + } + } + + ICondition[] storage conditions = getCertToScripConditions(certAddress); + bytes4 selector = bytes4( + keccak256("scripifyCert(address,uint256,uint256,address)") + ); + for (uint256 i = 0; i < conditions.length; i++) { + if ( + !conditions[i].checkCondition( + certAddress, + selector, + abi.encode(id, amount, target) + ) + ) { + revert ConditionCheckFailed(); + } + } + + ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); + if (certificate.isVoided(id)) revert CertificateVoided(); + if (certificate.legalOwnerOf(id) != account) revert NotLegalOwner(); + + address toSend = target; + if (toSend == address(0)) toSend = account; + + CertificateDetails memory details = certificate + .getActiveCertificateDetails(id); + + if (amount > details.unitsRepresented) { + revert AmountExceedsAvailableUnits(); + } + + (uint256 numerator, uint256 denominator) = _getScripRatioOrDefault( + certAddress + ); + uint256 scripAmount = amount * numerator; + scripAmount = scripAmount / denominator; + CertScripState storage certState = getCertScripState(certAddress, id); + uint256 currentScripifiedUnits = getCurrentCertScripifiedUnits( + certAddress, + id + ); + uint256 totalUnits = details.unitsRepresented + currentScripifiedUnits; + if (totalUnits > certState.maxUnitsRepresented) { + certState.maxUnitsRepresented = totalUnits; + } + if (currentScripifiedUnits + amount > certState.maxUnitsRepresented) { + revert ScripifyOverMax(); + } + + _depositCertScripUnits(certAddress, id, amount); + details.unitsRepresented = details.unitsRepresented - amount; + certificate.updateCertificateDetails(id, details); + ICyberScrip(scripifiedCert).mint(toSend, scripAmount); + emit ScripifiedCert(certAddress, id, scripifiedCert, amount); + } + + function executeConvertScripToCert( + address certAddress, + uint256 amount, + address account, + bytes4 convertSelector + ) external { + address scripifiedCert = getScripifiedCert(certAddress); + if (scripifiedCert == address(0)) revert ScripifiedCertNotAllowed(); + uint256 minimum = getScripToCertMinimum(certAddress); + if (minimum > 0 && amount < minimum) revert ScripToCertMinimumNotMet(); + + (uint256 numerator, uint256 denominator) = _getScripRatioOrDefault( + certAddress + ); + uint256 units = amount * denominator; + units = units / numerator; + + + ICondition[] storage conditions = getScripToCertConditions(certAddress); + for (uint256 i = 0; i < conditions.length; i++) { + if ( + !conditions[i].checkCondition( + certAddress, + convertSelector, + abi.encode(amount, account) + ) + ) { + revert ConditionCheckFailed(); + } + } + + ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); + RecertSelection memory selection = _selectRecertToken( + certAddress, + account + ); + bool requiresApproval = !selection.foundActive; + RecertificationApproval memory approval; + if (requiresApproval) { + ( + bool approved, + string memory investorName, + CertificateDetails memory approvedDetails + ) = getRecertificationApprovalData(certAddress, account); + if (!approved) revert RecertificationApprovalRequired(); + approval.approved = approved; + approval.investorName = investorName; + approval.details = approvedDetails; + } + + if (selection.foundActive) { + // Redeem vault shares for this certificate up to pool claim; burn nominals. Conversion + // above claim is still backed by burned scrip, so dilute remaining pool pro-rata via + // _withdrawVaultAssets (same economics as excess scrip burning without a cert position). + uint256 claimWad = _assetsOfVaultPosition( + certAddress, + selection.activeTokenId + ); + uint256 fromVaultWad = units < claimWad ? units : claimWad; + uint256 redeemedWad; + if (fromVaultWad > 0) { + redeemedWad = _redeemVaultForCert( + certAddress, + selection.activeTokenId, + fromVaultWad + ); + } + if (units > redeemedWad) { + _withdrawVaultAssets(certAddress, units - redeemedWad); + } + } else { + // No active cert: socialized withdrawal from the shared vault (nominals unchanged). + _withdrawVaultAssets(certAddress, units); + } + ICyberScrip(scripifiedCert).burnFrom(account, amount); + + if (selection.foundActive) { + CertificateDetails memory activeDetails = certificate + .getActiveCertificateDetails(selection.activeTokenId); + uint256 oldUnitsRepresented = activeDetails.unitsRepresented; + activeDetails.unitsRepresented = + activeDetails.unitsRepresented + + units; + certificate.updateCertificateDetails( + selection.activeTokenId, + activeDetails + ); + _setCertMaxFromCurrent( + certAddress, + selection.activeTokenId, + activeDetails.unitsRepresented + ); + emit ScripAddedToExistingCert( + certAddress, + account, + selection.activeTokenId, + oldUnitsRepresented, + activeDetails.unitsRepresented + ); + emit ScripRecertified( + certAddress, + account, + selection.activeTokenId, + amount, + oldUnitsRepresented, + activeDetails.unitsRepresented + ); + } else { + CertificateDetails memory details = approval.details; + details.unitsRepresented = units; + uint256 createdTokenId = IIssuanceManager(address(this)) + .createCertAndAssignWithName( + certAddress, + account, + details, + approval.investorName, + bytes(""), + block.timestamp + ); + clearRecertificationApproval(certAddress, account); + _setCertMaxFromCurrent( + certAddress, + createdTokenId, + details.unitsRepresented + ); + emit ScripRecertified( + certAddress, + account, + createdTokenId, + amount, + 0, + details.unitsRepresented + ); + } + } + + function executeForceScripBurn( + address certAddress, + address account, + uint256 amount + ) external { + if (amount == 0) revert InvalidAmount(); + + address scripifiedCert = getScripifiedCert(certAddress); + if (scripifiedCert == address(0)) revert ScripifiedCertNotAllowed(); + + (uint256 numerator, uint256 denominator) = _getScripRatioOrDefault( + certAddress + ); + uint256 units = amount * denominator; + units = units / numerator; + + _withdrawVaultAssets(certAddress, units); + ICyberScrip(scripifiedCert).forceBurn(account, amount); + } + + function executeSetScripRestrictionHooks( + address certAddress, + ITransferRestrictionHook[] memory hooks + ) external { + ICyberScrip(_getScripifiedCertOrRevert(certAddress)).setRestrictionHook(hooks); + } + + function executeDisableScripForceTransfer(address certAddress) external { + ICyberScrip(_getScripifiedCertOrRevert(certAddress)).disableForceTransfer(); + } + + function executeDisableScripForceBurn(address certAddress) external { + ICyberScrip(_getScripifiedCertOrRevert(certAddress)).disableForceBurn(); + } + + function executeDisableScripFreeze(address certAddress) external { + ICyberScrip(_getScripifiedCertOrRevert(certAddress)).disableFreeze(); + } + + function executeSetScripFrozen( + address certAddress, + address account, + bool isFrozen + ) external { + ICyberScrip(_getScripifiedCertOrRevert(certAddress)).setFrozen( + account, + isFrozen + ); + } + + function executeForceScripTransfer( + address certAddress, + address from, + address to, + uint256 amount + ) external { + ICyberScrip(_getScripifiedCertOrRevert(certAddress)).forceTransfer( + from, + to, + amount + ); + } + + function executeSetRecertificationApproval( + address certAddress, + address investor, + string memory investorName, + CertificateDetails memory details + ) external { + if (investor == address(0)) revert InvalidInvestor(); + if (bytes(investorName).length == 0) revert InvalidInvestorName(); + setRecertificationApproval(certAddress, investor, investorName, details); + emit RecertificationApprovalSet(certAddress, investor, investorName); + } + + function executeClearRecertificationApproval( + address certAddress, + address investor + ) external { + clearRecertificationApproval(certAddress, investor); + emit RecertificationApprovalCleared(certAddress, investor); + } + + function _selectRecertToken( + address certAddress, + address account + ) internal view returns (RecertSelection memory selection) { + ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); + uint256 ownedBalance = certificate.balanceOf(account); + + for (uint256 i = 0; i < ownedBalance; i++) { + uint256 tokenId = certificate.tokenOfOwnerByIndex(account, i); + if (certificate.legalOwnerOf(tokenId) != account) continue; + if (certificate.isVoided(tokenId)) continue; + selection.foundActive = true; + selection.activeTokenId = tokenId; + return selection; + } + } + + function _mintAssignedCert( + address certAddress, + address investor, + CertificateDetails memory details, + string memory investorName + ) + internal + returns (ICyberCertPrinter cert, uint256 tokenId, string memory tokenURI) + { + _requireCompanyDetailsSet(); + cert = ICyberCertPrinter(certAddress); + tokenId = cert.totalSupply(); + cert.safeMintAndAssign(investor, tokenId, details, investorName); + tokenURI = cert.tokenURI(tokenId); + _emitCertificateCreated(tokenId, certAddress, details, tokenURI); + } + + function _requireCompanyDetailsSet() internal view { + if (bytes(ICyberCorp(getCORP()).cyberCORPName()).length == 0) { + revert CompanyDetailsNotSet(); + } + } + + function _emitCertificateCreated( + uint256 tokenId, + address certAddress, + CertificateDetails memory details, + string memory tokenURI + ) internal { + emit CertificateCreated( + tokenId, + certAddress, + details.investmentAmountUSD, + details.issuerUSDValuationAtTimeOfInvestment, + details, + tokenURI + ); + } + + function _getEscrowedOfficerSignature() + internal + view + returns (bytes memory escrowedOfficerSignature) + { + address corp = getCORP(); + try ICyberCorp(corp).getEscrowedOfficerSignatureCount() returns ( + uint256 count + ) { + if (count > 0) { + try ICyberCorp(corp).getEscrowedOfficerSignature(0) returns ( + bytes memory sig + ) { + escrowedOfficerSignature = sig; + } catch {} + } + } catch {} + } + + function _getBytecodeCertPrinter() internal view returns (bytes memory bytecode) { + bytes memory sourceCodeBytes = type(BeaconProxy).creationCode; + bytecode = abi.encodePacked( + sourceCodeBytes, + abi.encode(getCyberCertPrinterBeacon(), "") + ); + } + + function _getBytecodeScrip() internal view returns (bytes memory bytecode) { + bytes memory sourceCodeBytes = type(BeaconProxy).creationCode; + bytecode = abi.encodePacked( + sourceCodeBytes, + abi.encode(getCyberScripBeacon(), "") + ); + } + + function _getScripRatioOrDefault( + address certAddress + ) internal view returns (uint256 numerator, uint256 denominator) { + ScripRatio storage ratio = getScripRatio(certAddress); + numerator = ratio.numerator; + denominator = ratio.denominator; + if (numerator == 0 || denominator == 0) { + return (1, 1); + } + } + + function _setCertMaxFromCurrent( + address certAddress, + uint256 tokenId, + uint256 currentUnits + ) internal { + CertScripState storage certState = getCertScripState(certAddress, tokenId); + uint256 currentTotal = currentUnits + + getCurrentCertScripifiedUnits(certAddress, tokenId); + if (currentTotal > certState.maxUnitsRepresented) { + certState.maxUnitsRepresented = currentTotal; + } + } + + /// @notice Deposit units (wad) into the shared vault; mint nominal shares to this certificate. + function _depositCertScripUnits( + address certAddress, + uint256 tokenId, + uint256 assetsWad + ) internal { + IssuanceManagerData storage ds = issuanceManagerStorage(); + CertScripUnitPool storage pool = ds.certScripUnitPools[certAddress]; + CertScripState storage certState = ds.certScripStates[certAddress][tokenId]; + + uint256 sharesMinted = pool.totalNominalShares == 0 + ? assetsWad + : assetsWad * pool.totalNominalShares / pool.totalAssetsWad; + if (sharesMinted == 0) revert ZeroSharesMinted(); + + certState.vaultNominalShares += sharesMinted; + pool.totalNominalShares += sharesMinted; + pool.totalAssetsWad += assetsWad; + } + + /// @notice ERC4626-style withdraw: burn this certificate's nominal shares and pull `assetsWad` + /// from the vault (exact asset burn; shares burnt round up). + function _redeemVaultForCert( + address certAddress, + uint256 tokenId, + uint256 assetsWad + ) internal returns (uint256 assetsRemovedWad) { + if (assetsWad == 0) return 0; + CertScripUnitPool storage pool = issuanceManagerStorage().certScripUnitPools[ + certAddress + ]; + CertScripState storage certState = getCertScripState(certAddress, tokenId); + uint256 S = pool.totalNominalShares; + uint256 T = pool.totalAssetsWad; + if (S == 0 || T == 0) revert EmptyVault(); + + uint256 claimWad = certState.vaultNominalShares * T / S; + if (assetsWad > claimWad) revert VaultRedemptionExceedsClaim(); + + uint256 sharesBurned = (assetsWad * S + T - 1) / T; + if (sharesBurned > certState.vaultNominalShares) { + sharesBurned = certState.vaultNominalShares; + assetsWad = sharesBurned * T / S; + } + + certState.vaultNominalShares -= sharesBurned; + pool.totalNominalShares -= sharesBurned; + pool.totalAssetsWad -= assetsWad; + + if (pool.totalAssetsWad == 0) { + _zeroAllVaultNominals(certAddress); + } + return assetsWad; + } + + /// @notice Remove underlying from vault; all certificate positions diluted pro-rata + /// (nominal shares unchanged, price per share in underlying drops). + function _withdrawVaultAssets( + address certAddress, + uint256 assetsOutWad + ) internal { + CertScripUnitPool storage pool = issuanceManagerStorage().certScripUnitPools[ + certAddress + ]; + if (pool.totalAssetsWad == 0) revert EmptyVault(); + if (assetsOutWad > pool.totalAssetsWad) { + revert VaultWithdrawalExceedsAssets(); + } + + pool.totalAssetsWad -= assetsOutWad; + if (pool.totalAssetsWad == 0) { + _zeroAllVaultNominals(certAddress); + } + } + + function _getScripifiedCertOrRevert( + address certAddress + ) internal view returns (address scripifiedCert) { + scripifiedCert = getScripifiedCert(certAddress); + if (scripifiedCert == address(0)) revert ScripifiedCertNotAllowed(); + } + + function _zeroAllVaultNominals(address certAddress) internal { + ICyberCertPrinter certificate = ICyberCertPrinter(certAddress); + uint256 supply = certificate.totalSupply(); + for (uint256 i = 0; i < supply; i++) { + uint256 tokenId = certificate.tokenByIndex(i); + getCertScripState(certAddress, tokenId).vaultNominalShares = 0; + } + CertScripUnitPool storage pool = issuanceManagerStorage().certScripUnitPools[ + certAddress + ]; + pool.totalNominalShares = 0; + } } diff --git a/src/storage/RoundManagerFactoryStorage.sol b/src/storage/RoundManagerFactoryStorage.sol index 75ee9e9d..2387979e 100644 --- a/src/storage/RoundManagerFactoryStorage.sol +++ b/src/storage/RoundManagerFactoryStorage.sol @@ -42,6 +42,8 @@ pragma solidity 0.8.28; +import {FeeOverride} from "../interfaces/IRoundManagerFactory.sol"; + /// @title RoundManagerFactoryStorage /// @notice Storage library for the RoundManagerFactory contract that handles persistent data storage /// @dev Uses the unstructured storage pattern to manage factory-related data @@ -59,6 +61,7 @@ library RoundManagerFactoryStorage { address platformPayable; // Recipient of platform fees uint256 defaultFeeRatio; // total fee as % of ticket size (BASIS_POINTS = 100%) mapping(address => bool) whitelistedTokens; + mapping(address => FeeOverride) instanceFeeOverrides; // RoundManager -> FeeOverride } /// @notice Retrieves the storage reference for the RoundManagerFactoryData struct diff --git a/src/storage/RoundManagerStorage.sol b/src/storage/RoundManagerStorage.sol index 8b57a3f5..64d917f7 100644 --- a/src/storage/RoundManagerStorage.sol +++ b/src/storage/RoundManagerStorage.sol @@ -421,6 +421,35 @@ library RoundManagerStorage { address(this), details ); + + //add officer signature from round escrowed signature + issuanceManager.addOfficerSignature( + round.certPrinter[i], + certIds[i], + round.escrowedSignature + ); + } + + if (_isStockSecurityClass(round.primarySecurityClass)) { + bytes memory secondEscrowedSignature = ""; + address corp = LexScrowStorage.getCorp(); + try ICyberCorp(corp).getEscrowedOfficerSignatureCount() returns (uint256 count) { + if (count > 1) { + try ICyberCorp(corp).getEscrowedOfficerSignature(1) returns (bytes memory sig) { + secondEscrowedSignature = sig; + } catch {} + } + } catch {} + + if (secondEscrowedSignature.length > 0) { + for (uint256 i = 0; i < round.certPrinter.length; i++) { + issuanceManager.addOfficerSignature( + round.certPrinter[i], + certIds[i], + secondEscrowedSignature + ); + } + } } escrow.signature = round.escrowedSignature; @@ -563,4 +592,12 @@ library RoundManagerStorage { function getLexChexMinter() internal view returns (address) { return roundManagerStorage().lexChexMinter; } + + function _isStockSecurityClass(SecurityClass cls) private pure returns (bool) { + return + cls == SecurityClass.CommonStock || + cls == SecurityClass.PreferredStock || + cls == SecurityClass.RestrictedStockPurchaseAgreement || + cls == SecurityClass.RestrictedStockUnit; + } } diff --git a/src/storage/extensions/SAFEExtension.sol b/src/storage/extensions/SAFEExtension.sol new file mode 100644 index 00000000..203ded65 --- /dev/null +++ b/src/storage/extensions/SAFEExtension.sol @@ -0,0 +1,90 @@ +/* .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 "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "./ICertificateExtension.sol"; +import "../../libs/auth.sol"; + +struct SAFEData { + string customProvisions; +} + +contract SAFEExtension is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { + bytes32 public constant EXTENSION_TYPE = keccak256("SAFE"); + + //ofset to leave for future upgrades + uint256[30] private __gap; + + function initialize(address _auth) external initializer { + __UUPSUpgradeable_init(); + __BorgAuthACL_init(_auth); + } + + function decodeExtensionData(bytes memory data) external view returns (SAFEData memory) { + return abi.decode(data, (SAFEData)); + } + + function encodeExtensionData(SAFEData 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 view override returns (string memory) { + SAFEData memory decoded = abi.decode(data, (SAFEData)); + + string memory json = string(abi.encodePacked( + ', "SAFEDetails": {', + '"customProvisions": "', decoded.customProvisions, + '"}' + )); + + return json; + } + + function _authorizeUpgrade( + address newImplementation + ) internal virtual override onlyOwner {} +} diff --git a/src/storage/extensions/SAFTEExtensionV2.sol b/src/storage/extensions/SAFTEExtensionV2.sol new file mode 100644 index 00000000..57bb18bb --- /dev/null +++ b/src/storage/extensions/SAFTEExtensionV2.sol @@ -0,0 +1,158 @@ +/* .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 "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "./ICertificateExtension.sol"; +import "../../CyberCorpConstants.sol"; +import "../../libs/auth.sol"; + +struct SAFTEDataV2 { + UnlockStartTimeType unlockStartTimeType; + uint256 unlockStartTime; + uint256 unlockingPeriod; + uint256 unlockingCliffPeriod; + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; + TokenCalculationMethod tokenCalculationMethod; + uint256 minCompanyReserve; + uint256 tokenPremiumMultiplier; + uint256 protocolUSDValuationAtTimeofInvestment; + string customProvisions; + } + +contract SAFTEExtensionV2 is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { + bytes32 public constant EXTENSION_TYPE = keccak256("SAFTE_V2"); + uint256 public constant PERCENTAGE_PRECISION = 10 ** 4; + + //ofset to leave for future upgrades + uint256[30] private __gap; + + function initialize(address _auth) external initializer { + __UUPSUpgradeable_init(); + __BorgAuthACL_init(_auth); + } + + function decodeExtensionData(bytes memory data) external view returns (SAFTEDataV2 memory) { + return abi.decode(data, (SAFTEDataV2)); + } + + function encodeExtensionData(SAFTEDataV2 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 view override returns (string memory) { + SAFTEDataV2 memory decoded = abi.decode(data, (SAFTEDataV2)); + + string memory json = string(abi.encodePacked( + ', "SAFTEDetails": {', + '"protocolUSDValuationAtTimeofInvestment": "', uint256ToString(decoded.protocolUSDValuationAtTimeofInvestment), + '", "unlockStartTimeType": "', UnlockStartTimeTypeToString(decoded.unlockStartTimeType), + '", "unlockStartTime": "', uint256ToString(decoded.unlockStartTime), + '", "unlockingPeriod": "', uint256ToString(decoded.unlockingPeriod), + '", "unlockingCliffPeriod": "', uint256ToString(decoded.unlockingCliffPeriod), + '", "unlockingCliffPercentage": "', uint256ToString(decoded.unlockingCliffPercentage), + '", "unlockingIntervalType": "', UnlockingIntervalTypeToString(decoded.unlockingIntervalType), + '", "tokenCalculationMethod": "', conversionTypeToString(decoded.tokenCalculationMethod), + '", "minCompanyReserve": "', uint256ToString(decoded.minCompanyReserve), + '", "tokenPremiumMultiplier": "', uint256ToString(decoded.tokenPremiumMultiplier), + '", "customProvisions": "', decoded.customProvisions, + '"}' + )); + + 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"; + if (_type == TokenCalculationMethod.dollarProRataToProtocolVal) return "dollarProRataToProtocolVal"; + return "Unknown"; + } + + function UnlockStartTimeTypeToString(UnlockStartTimeType _type) internal pure returns (string memory) { + if (_type == UnlockStartTimeType.tokenWarrantTime) return "agreementDateTime"; + if (_type == UnlockStartTimeType.tgeTime) return "tgeTime"; + if (_type == UnlockStartTimeType.setTime) return "setTime"; + return "Unknown"; + } + + function UnlockingIntervalTypeToString(UnlockingIntervalType _type) internal pure returns (string memory) { + if (_type == UnlockingIntervalType.blockly) return "blockly"; + if (_type == UnlockingIntervalType.secondly) return "secondly"; + if (_type == UnlockingIntervalType.hourly) return "hourly"; + if (_type == UnlockingIntervalType.daily) return "daily"; + if (_type == UnlockingIntervalType.monthly) return "monthly"; + return "Unknown"; + } + + function _authorizeUpgrade( + address newImplementation + ) internal virtual override onlyOwner {} +} diff --git a/src/storage/extensions/SAFTExtensionV2.sol b/src/storage/extensions/SAFTExtensionV2.sol new file mode 100644 index 00000000..33ba19b2 --- /dev/null +++ b/src/storage/extensions/SAFTExtensionV2.sol @@ -0,0 +1,143 @@ +/* .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 "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "./ICertificateExtension.sol"; +import "../../CyberCorpConstants.sol"; +import "../../libs/auth.sol"; + +struct SAFTDataV2 { + UnlockStartTimeType unlockStartTimeType; // enum of different types, can be agreementExecutionTime, tgeTime, or setTime + uint256 unlockStartTime; + uint256 unlockingPeriod; //in interval units + uint256 unlockingCliffPeriod; // seconds + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; + string customProvisions; +} + +contract SAFTExtensionV2 is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { + bytes32 public constant EXTENSION_TYPE = keccak256("SAFT_V2"); + uint256 public constant PERCENTAGE_PRECISION = 10 ** 4; + + //ofset to leave for future upgrades + uint256[30] private __gap; + + function initialize(address _auth) external initializer { + __UUPSUpgradeable_init(); + __BorgAuthACL_init(_auth); + } + + function decodeExtensionData(bytes memory data) external view returns (SAFTDataV2 memory) { + return abi.decode(data, (SAFTDataV2)); + } + + function encodeExtensionData(SAFTDataV2 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 view override returns (string memory) { + SAFTDataV2 memory decoded = abi.decode(data, (SAFTDataV2)); + + string memory json = string(abi.encodePacked( + ', "SAFTDetails": {', + '"unlockStartTimeType": "', UnlockStartTimeTypeToString(decoded.unlockStartTimeType), + '", "unlockStartTime": "', uint256ToString(decoded.unlockStartTime), + '", "unlockingPeriod": "', uint256ToString(decoded.unlockingPeriod), + '", "unlockingCliffPeriod": "', uint256ToString(decoded.unlockingCliffPeriod), + '", "unlockingCliffPercentage": "', uint256ToString(decoded.unlockingCliffPercentage), + '", "unlockingIntervalType": "', UnlockingIntervalTypeToString(decoded.unlockingIntervalType), + '", "customProvisions": "', decoded.customProvisions, + '"}' + )); + + 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 UnlockStartTimeTypeToString(UnlockStartTimeType _type) internal pure returns (string memory) { + if (_type == UnlockStartTimeType.tokenWarrantTime) return "agreementExecutionTime"; + if (_type == UnlockStartTimeType.tgeTime) return "tgeTime"; + if (_type == UnlockStartTimeType.setTime) return "setTime"; + return "Unknown"; + } + + function UnlockingIntervalTypeToString(UnlockingIntervalType _type) internal pure returns (string memory) { + if (_type == UnlockingIntervalType.blockly) return "blockly"; + if (_type == UnlockingIntervalType.secondly) return "secondly"; + if (_type == UnlockingIntervalType.hourly) return "hourly"; + if (_type == UnlockingIntervalType.daily) return "daily"; + if (_type == UnlockingIntervalType.monthly) return "monthly"; + return "Unknown"; + } + + function _authorizeUpgrade( + address newImplementation + ) internal virtual override onlyOwner {} +} diff --git a/src/storage/extensions/TokenWarrantExtensionV2.sol b/src/storage/extensions/TokenWarrantExtensionV2.sol new file mode 100644 index 00000000..0c2ef2e6 --- /dev/null +++ b/src/storage/extensions/TokenWarrantExtensionV2.sol @@ -0,0 +1,168 @@ +/* .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 "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "./ICertificateExtension.sol"; +import "../../CyberCorpConstants.sol"; +import "../../libs/auth.sol"; + +struct TokenWarrantDataV2 { + ExercisePriceMethod exercisePriceMethod; // perToken or perWarrant + uint256 exercisePrice; // 18 decimals + UnlockStartTimeType unlockStartTimeType; // enum of different types, can be tokenWarrantTime, tgeTime, or setTime + uint256 unlockStartTime; + uint256 unlockingPeriod; //in interval units + uint256 latestExpirationTime; //latest time at which the Warrant can expire (cease to be exercisable)--denominated in seconds + uint256 unlockingCliffPeriod; // seconds + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; + TokenCalculationMethod tokenCalculationMethod; //equityProRataToTokenSupply or equityProRataToCompanyReserve + uint256 minCompanyReserve; //minimum company reserve within an equityProRataToCompanyReserve method--set to 0 if there is no minimum + uint256 tokenPremiumMultiplier; //multiplier of network valuation over company equity valuation, to be used within equityProRataToTokenSupply method (set to 0 if no premium) + string customProvisions; +} + +contract TokenWarrantExtensionV2 is UUPSUpgradeable, ICertificateExtension, BorgAuthACL { + bytes32 public constant EXTENSION_TYPE = keccak256("TOKEN_WARRANT_V2"); + uint256 public constant PERCENTAGE_PRECISION = 10 ** 4; + + //ofset to leave for future upgrades + uint256[30] private __gap; + + function initialize(address _auth) external initializer { + __UUPSUpgradeable_init(); + __BorgAuthACL_init(_auth); + } + + function decodeExtensionData(bytes memory data) external view returns (TokenWarrantDataV2 memory) { + return abi.decode(data, (TokenWarrantDataV2)); + } + + function encodeExtensionData(TokenWarrantDataV2 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 view override returns (string memory) { + TokenWarrantDataV2 memory decoded = abi.decode(data, (TokenWarrantDataV2)); + + string memory json = string(abi.encodePacked( + ', "warrantDetails": {', + '"exercisePriceMethod": "', ExercisePriceMethodToString(decoded.exercisePriceMethod), + '", "exercisePrice": "', uint256ToString(decoded.exercisePrice), + '", "unlockStartTimeType": "', UnlockStartTimeTypeToString(decoded.unlockStartTimeType), + '", "unlockStartTime": "', uint256ToString(decoded.unlockStartTime), + '", "unlockingPeriod": "', uint256ToString(decoded.unlockingPeriod), + '", "latestExpirationTime": "', uint256ToString(decoded.latestExpirationTime), + '", "unlockingCliffPeriod": "', uint256ToString(decoded.unlockingCliffPeriod), + '", "unlockingCliffPercentage": "', uint256ToString(decoded.unlockingCliffPercentage), + '", "unlockingIntervalType": "', UnlockingIntervalTypeToString(decoded.unlockingIntervalType), + '", "tokenCalculationMethod": "', conversionTypeToString(decoded.tokenCalculationMethod), + '", "minCompanyReserve": "', uint256ToString(decoded.minCompanyReserve), + '", "tokenPremiumMultiplier": "', uint256ToString(decoded.tokenPremiumMultiplier), + '", "customProvisions": "', decoded.customProvisions, + '"}' + )); + + 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); + } + + // Helper functions to convert enums to strings + function ExercisePriceMethodToString(ExercisePriceMethod _type) internal pure returns (string memory) { + if (_type == ExercisePriceMethod.perWarrant) return "perWarrant"; + if (_type == ExercisePriceMethod.perToken) return "perToken"; + return "Unknown"; + } + + function conversionTypeToString(TokenCalculationMethod _type) internal pure returns (string memory) { + if (_type == TokenCalculationMethod.equityProRataToCompanyReserve) return "equityProRataToCompanyReserve"; + if (_type == TokenCalculationMethod.equityProRataToTokenSupply) return "equityProRataToTokenSupply"; + return "Unknown"; + } + + function UnlockStartTimeTypeToString(UnlockStartTimeType _type) internal pure returns (string memory) { + if (_type == UnlockStartTimeType.tokenWarrantTime) return "tokenWarrantTime"; + if (_type == UnlockStartTimeType.tgeTime) return "tgeTime"; + if (_type == UnlockStartTimeType.setTime) return "setTime"; + return "Unknown"; + } + + function UnlockingIntervalTypeToString(UnlockingIntervalType _type) internal pure returns (string memory) { + if (_type == UnlockingIntervalType.blockly) return "blockly"; + if (_type == UnlockingIntervalType.secondly) return "secondly"; + if (_type == UnlockingIntervalType.hourly) return "hourly"; + if (_type == UnlockingIntervalType.daily) return "daily"; + if (_type == UnlockingIntervalType.monthly) return "monthly"; + return "Unknown"; + } + + function _authorizeUpgrade( + address newImplementation + ) internal virtual override onlyOwner {} +} diff --git a/templates/Three Prime Custom cyberSAFE and cyberTokenWarrant.md b/templates/Three Prime Custom cyberSAFE and cyberTokenWarrant.md new file mode 100644 index 00000000..73ddeb2f --- /dev/null +++ b/templates/Three Prime Custom cyberSAFE and cyberTokenWarrant.md @@ -0,0 +1,151 @@ +# Data Overview + +id: bytes32(bytes("three_prime_safe_t")) + +legalURI: +safeURI: IFPS://bafybeibojsh6f4wxj3gvwjbv7uvvurony7jumyqqi5i6rqsv7wcywdsi44 + +combined doc: IPFS://bafybeidbzt4y3uvxxouzcli4k6p3zgozh5bp6dqphzhkn5o5adcgs5emja + +SAFE alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeifmwe5gzjl4j57qs4hdmytcocp55uaiqgdut5gvibuvejmd7avqja + +Warrant alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeibhtpwtgsdbwvqfg2dynotwiorbmbqmluj7ci6vc5wn6mrlh2eawy + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:-----------------------------------| +| purchaseAmount | e.g. "1000.00" | +| postMoneyValuationCap | | +| expirationTime | | +| governingJurisdiction | | +| disputeResolution | | +| exercisePriceMethod | "perToken" or "perWarrant" | +| exercisePrice | price, e.g. "1000.00" | +| unlockStartTimeType |"tokenWarrantTime" \|"tgeTime" \| "setTime" | +| unlockStartTime | only set if using `setTime` for `unlockStartTimeType` | +| unlockingPeriod | Duration in `unlockingInvervalType` units | +| latestExpirationTime | Unix timestamp. Will not be prompted for in UI, will be 10 yrs from deal date | +| unlockingCliffPeriod | Duration in `unlockingIntervalType`, first tokens unlocked at `unlockingStartTime` + `unlockingCliffPeriod` | +| unlockingCliffPercentage | e.g. "10.5%" | +| unlockingIntervalType | "secondly", "hourly", "daily", "monthly", "blockly". Note that this affects both `unlockingPeriod` and `unlockingCliffPeriod` | +| tokenCalculationMethod | `equityProRataToTokenSupply` or `equityProRataToCompanyReserve` | +| minCompanyReserve | This is a number of tokens | +| tokenPremiumMultiplier | A number. If SAFE is worth 30% of company fully diluted equity, and premium multiplier is 2, the investor can buy 15% of total supply. | + + + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------------------------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + + + +## Certificate Extension + +name: TokenWarrantExtension +```solidity +struct TokenWarrantData { + ExercisePriceMethod exercisePriceMethod; // perToken or perWarrant + uint256 exercisePrice; // 18 decimals + UnlockStartTimeType unlockStartTimeType; // enum of different types, can be tokenWarrantTime, tgeTime, or setTime + uint256 unlockStartTime; + uint256 unlockingPeriod; + uint256 latestExpirationTime; + uint256 unlockingCliffPeriod; + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; // blockly, secondly, daily, weekly, monthly + TokenCalculationMethod tokenCalculationMethod; //equityProRataToTokenSupply or equityProRataToCompanyReserve + uint256 minCompanyReserve; //minimum company reserve within an equityProRataToCompanyReserve method--set to 0 if there is no minimum + uint256 tokenPremiumMultiplier; //multiplier of network valuation over company equity valuation, to be used within equityProRataToTokenSupply method (set to 0 if no premium) +} +``` + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` + +``` +enum ExercisePriceMethod { + perToken, + perWarrant +} + +enum TokenCalculationMethod { + equityProRataToCompanyReserve, + equityProRataToTokenSupply +} + +enum UnlockStartTimeType { + tokenWarrantTime, + tgeTime, + setTime +} + +enum UnlockingIntervalType { + blockly, + secondly, + hourly, + daily, + monthly +} + +``` + +Restrictive Legends: + +safe + +[1] investment advisor certificate custody legend + +THE CERTIFICATE TOKEN MAY NOT BE USED TO EFFECT A TRANSFER OR TO OTHERWISE FACILITATE A CHANGE IN BENEFICIAL OWNERSHIP OF THIS SAFE WITHOUT THE PRIOR CONSENT OF THE COMPANY. + +[2] restricted security legend + +THIS SAFE, THE CERTIFICATE TOKEN, AND ANY SECURITIES ISSUABLE PURSUANT HERETO OR THERETO ARE “RESTRICTED SECURITIES” AS DEFINED IN SEC RULE 144. + +[3] unregistered security legend + +THIS SAFE, THE CERTIFICATE TOKEN AND ANY SECURITIES ISSUABLE PURSUANT HERETO OR THERETO HAVE NOT BEEN REGISTERED UNDER THE SECURITIES ACT OF 1933, AS AMENDED (THE “SECURITIES ACT”), OR UNDER THE SECURITIES LAWS OF CERTAIN STATES. THESE SECURITIES MAY NOT BE OFFERED, SOLD OR OTHERWISE TRANSFERRED, PLEDGED OR HYPOTHECATED EXCEPT AS PERMITTED IN THIS SAFE AND UNDER THE SECURITIES ACT AND APPLICABLE STATE SECURITIES LAWS PURSUANT TO AN EFFECTIVE REGISTRATION STATEMENT OR AN EXEMPTION THEREFROM. + +[4] hardfork legend + +IN THE EVENT THAT THE BLOCKCHAIN SYSTEM ON WHICH THE CERTIFICATE TOKEN WAS ORIGINALLY ISSUED UNDERGOES A PERSISTENT “CONTENTIOUS HARDFORK” (AS COMMONLY UNDERSTOOD IN THE BLOCKCHAIN INDUSTRY, RESULTING IN TWO INDEPENDENT BLOCKCHAIN SYSTEMS THAT ARE BOTH REASONABLY EXPECTED TO HAVE INDEPENDENT PERSISTENT COMMERCIAL VALUE), NO COPY OF THE CERTIFICATE TOKEN MAY BE OFFERED, SOLD, OR OTHERWISE TRANSFERRED, PLEDGED, OR HYPOTHECATED UNTIL THE COMPANY HAS DETERMINED, IN ITS SOLE AND ABSOLUTE DISCRETION, WHICH BLOCKCHAIN SYSTEM (AND WHICH CERTIFICATE TOKENS) TO TREAT AS CANONICAL, AND THEN ONLY THE CERTIFICATE TOKEN THUS DETERMINED BY THE COMPANY TO BE CANONICAL MAY BE OFFERED, SOLD, OR OTHERWISE TRANSFERRED, PLEDGED, OR HYPOTHECATED (TO THE EXTENT OTHERWISE PERMITTED). IN THE EVENT THAT THE BLOCKCHAIN SYSTEM DETERMINED BY THE COMPANY TO BE CANONICAL FOLLOWING A CONTENTIOUS HARDFORK ITSELF SUBSEQUENTLY UNDERGOES ANOTHER CONTENTIOUS HARDFORK, THIS RESTRICTIVE LEGEND SHALL LIKEWISE APPLY TO SUCH OTHER CONTENTIOUS HARDFORK, MUTATIS MUTANDIS. + +token warrant + +[1] investment advisor certificate custody legend + +THE CERTIFICATE TOKEN MAY NOT BE USED TO EFFECT A TRANSFER OR TO OTHERWISE FACILITATE A CHANGE IN BENEFICIAL OWNERSHIP OF THIS TOKEN WARRANT WITHOUT THE PRIOR CONSENT OF THE COMPANY. + +[2] restricted security legend + +THIS TOKEN WARRANT, THE CERTIFICATE TOKEN, AND ANY SECURITIES ISSUABLE PURSUANT HERETO OR THERETO ARE “RESTRICTED SECURITIES” AS DEFINED IN SEC RULE 144. + +[3] unregistered security legend + +THIS TOKEN WARRANT, THE CERTIFICATE TOKEN AND ANY SECURITIES ISSUABLE PURSUANT HERETO OR THERETO HAVE NOT BEEN REGISTERED UNDER THE SECURITIES ACT OF 1933, AS AMENDED (THE “SECURITIES ACT”), OR UNDER THE SECURITIES LAWS OF CERTAIN STATES. THESE SECURITIES MAY NOT BE OFFERED, SOLD OR OTHERWISE TRANSFERRED, PLEDGED OR HYPOTHECATED EXCEPT AS PERMITTED IN THIS TOKEN WARRANT AND UNDER THE SECURITIES ACT AND APPLICABLE STATE SECURITIES LAWS PURSUANT TO AN EFFECTIVE REGISTRATION STATEMENT OR AN EXEMPTION THEREFROM. + +[4] hardfork legend + +IN THE EVENT THAT THE BLOCKCHAIN SYSTEM ON WHICH THE CERTIFICATE TOKEN WAS ORIGINALLY ISSUED UNDERGOES A PERSISTENT “CONTENTIOUS HARDFORK” (AS COMMONLY UNDERSTOOD IN THE BLOCKCHAIN INDUSTRY, RESULTING IN TWO INDEPENDENT BLOCKCHAIN SYSTEMS THAT ARE BOTH REASONABLY EXPECTED TO HAVE INDEPENDENT PERSISTENT COMMERCIAL VALUE), NO COPY OF THE CERTIFICATE TOKEN MAY BE OFFERED, SOLD, OR OTHERWISE TRANSFERRED, PLEDGED, OR HYPOTHECATED UNTIL THE COMPANY HAS DETERMINED, IN ITS SOLE AND ABSOLUTE DISCRETION, WHICH BLOCKCHAIN SYSTEM (AND WHICH CERTIFICATE TOKENS) TO TREAT AS CANONICAL, AND THEN ONLY THE CERTIFICATE TOKEN THUS DETERMINED BY THE COMPANY TO BE CANONICAL MAY BE OFFERED, SOLD, OR OTHERWISE TRANSFERRED, PLEDGED, OR HYPOTHECATED (TO THE EXTENT OTHERWISE PERMITTED). IN THE EVENT THAT THE BLOCKCHAIN SYSTEM DETERMINED BY THE COMPANY TO BE CANONICAL FOLLOWING A CONTENTIOUS HARDFORK ITSELF SUBSEQUENTLY UNDERGOES ANOTHER CONTENTIOUS HARDFORK, THIS RESTRICTIVE LEGEND SHALL LIKEWISE APPLY TO SUCH OTHER CONTENTIOUS HARDFORK, MUTATIS MUTANDIS. + + + + diff --git a/templates/mlx_safe_reg_d_v1_3.md b/templates/mlx_safe_reg_d_v1_3.md new file mode 100644 index 00000000..a3823c5b --- /dev/null +++ b/templates/mlx_safe_reg_d_v1_3.md @@ -0,0 +1,49 @@ +# Data Overview + +id: bytes32(bytes("mlx_safe_reg_d_v1_3")) +idHex: 0x6d6c785f736166655f7265675f645f76315f3300000000000000000000000000 +title: mlx_safe_reg_d_v1_3 + +legalURI: +safeURI: IPFS://bafybeih7l2kxncjuwrfgv5gnmpcik43dnn4pxpe4it4u7ti2hgfgrlot2a + +combined doc: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeih7l2kxncjuwrfgv5gnmpcik43dnn4pxpe4it4u7ti2hgfgrlot2a +SAFE alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeidq7z4sgbh5tqxfehs5rz3r3il76ony3t7psetwge5ctld6lubi5e + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:----------------| +| purchaseAmount | e.g. "1000.00" | +| postMoneyValuationCap | | +| expirationTime | | +| governingJurisdiction | | +| disputeResolution | | + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + +## Certificate Extension + +none. + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` diff --git a/templates/mlx_safe_reg_s_v1_3.md b/templates/mlx_safe_reg_s_v1_3.md new file mode 100644 index 00000000..9d869f52 --- /dev/null +++ b/templates/mlx_safe_reg_s_v1_3.md @@ -0,0 +1,49 @@ +# Data Overview + +id: bytes32(bytes("mlx_safe_reg_s_v1_3")) +idHex: 0x6d6c785f736166655f7265675f735f76315f3300000000000000000000000000 +title: mlx_safe_reg_s_v1_3 + +legalURI: +safeURI: IPFS://bafybeieh7jn553jmrjmwee3dsvwf5hkedomey2vhubc3mumlewfpumvlae + +combined doc: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeieh7jn553jmrjmwee3dsvwf5hkedomey2vhubc3mumlewfpumvlae +SAFE alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeigkcbocfq3p7rscojjej24jajyrhqk6mukgetmlsmyo4f6cp6iqry + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:----------------| +| purchaseAmount | e.g. "1000.00" | +| postMoneyValuationCap | | +| expirationTime | | +| governingJurisdiction | | +| disputeResolution | | + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + +## Certificate Extension + +none. + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` diff --git a/templates/mlx_safe_tw_reg_d_v1_3.md b/templates/mlx_safe_tw_reg_d_v1_3.md new file mode 100644 index 00000000..f12ac8cd --- /dev/null +++ b/templates/mlx_safe_tw_reg_d_v1_3.md @@ -0,0 +1,79 @@ +# Data Overview + +id: bytes32(bytes("mlx_safe_tw_reg_d_v1_3")) +idHex: 0x6d6c785f736166655f74775f7265675f645f76315f3300000000000000000000 +title: mlx_safe_tw_reg_d_v1_3 + +legalURI: +safeURI: IPFS://bafybeiaw3pwov3ahg4bk2hte2hu4pwv34nndoguxyk3umq6f5su3kod6ay + +combined doc: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeiaw3pwov3ahg4bk2hte2hu4pwv34nndoguxyk3umq6f5su3kod6ay +SAFE alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeidq7z4sgbh5tqxfehs5rz3r3il76ony3t7psetwge5ctld6lubi5e +Warrant alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeidmtxc6hveimc43uxdkohbi7ubmtkd57irpday2hmhomuyguxtg7a + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:----------------| +| purchaseAmount | e.g. "1000.00" | +| postMoneyValuationCap | | +| expirationTime | | +| governingJurisdiction | | +| disputeResolution | | +| exercisePriceMethod | "perToken" or "perWarrant" | +| exercisePrice | price, e.g. "1000.00" | +| unlockStartTimeType | "tokenWarrantTime" \| "tgeTime" \| "setTime" | +| unlockStartTime | only set if using `setTime` for `unlockStartTimeType` | +| unlockingPeriod | Duration in `unlockingIntervalType` units | +| latestExpirationTime | Unix timestamp | +| unlockingCliffPeriod | Duration in `unlockingIntervalType` | +| unlockingCliffPercentage | e.g. "10.5%" | +| unlockingIntervalType | "secondly", "hourly", "daily", "monthly", "blockly" | +| tokenCalculationMethod | `equityProRataToTokenSupply` or `equityProRataToCompanyReserve` | +| minCompanyReserve | Number of tokens | +| tokenPremiumMultiplier | Premium multiplier | + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + +## Certificate Extension + +name: TokenWarrantExtension + +```solidity +struct TokenWarrantData { + ExercisePriceMethod exercisePriceMethod; + uint256 exercisePrice; + UnlockStartTimeType unlockStartTimeType; + uint256 unlockStartTime; + uint256 unlockingPeriod; + uint256 latestExpirationTime; + uint256 unlockingCliffPeriod; + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; + TokenCalculationMethod tokenCalculationMethod; + uint256 minCompanyReserve; + uint256 tokenPremiumMultiplier; +} +``` + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` diff --git a/templates/mlx_safe_tw_reg_s_v1_3.md b/templates/mlx_safe_tw_reg_s_v1_3.md new file mode 100644 index 00000000..f396a9fd --- /dev/null +++ b/templates/mlx_safe_tw_reg_s_v1_3.md @@ -0,0 +1,79 @@ +# Data Overview + +id: bytes32(bytes("mlx_safe_tw_reg_s_v1_3")) +idHex: 0x6d6c785f736166655f74775f7265675f735f76315f3300000000000000000000 +title: mlx_safe_tw_reg_s_v1_3 + +legalURI: +safeURI: IPFS://bafybeicto2raupsj5ad7snxvhmmll2plwyploqho4fg2cibnn2fuhlm2d4 + +combined doc: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeicto2raupsj5ad7snxvhmmll2plwyploqho4fg2cibnn2fuhlm2d4 +SAFE alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeigkcbocfq3p7rscojjej24jajyrhqk6mukgetmlsmyo4f6cp6iqry +Warrant alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeid2ldcznpjva3hf6km75zwsfe6tgaw23qabbapz63cgtmipnwy4kq + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:----------------| +| purchaseAmount | e.g. "1000.00" | +| postMoneyValuationCap | | +| expirationTime | | +| governingJurisdiction | | +| disputeResolution | | +| exercisePriceMethod | "perToken" or "perWarrant" | +| exercisePrice | price, e.g. "1000.00" | +| unlockStartTimeType | "tokenWarrantTime" \| "tgeTime" \| "setTime" | +| unlockStartTime | only set if using `setTime` for `unlockStartTimeType` | +| unlockingPeriod | Duration in `unlockingIntervalType` units | +| latestExpirationTime | Unix timestamp | +| unlockingCliffPeriod | Duration in `unlockingIntervalType` | +| unlockingCliffPercentage | e.g. "10.5%" | +| unlockingIntervalType | "secondly", "hourly", "daily", "monthly", "blockly" | +| tokenCalculationMethod | `equityProRataToTokenSupply` or `equityProRataToCompanyReserve` | +| minCompanyReserve | Number of tokens | +| tokenPremiumMultiplier | Premium multiplier | + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + +## Certificate Extension + +name: TokenWarrantExtension + +```solidity +struct TokenWarrantData { + ExercisePriceMethod exercisePriceMethod; + uint256 exercisePrice; + UnlockStartTimeType unlockStartTimeType; + uint256 unlockStartTime; + uint256 unlockingPeriod; + uint256 latestExpirationTime; + uint256 unlockingCliffPeriod; + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; + TokenCalculationMethod tokenCalculationMethod; + uint256 minCompanyReserve; + uint256 tokenPremiumMultiplier; +} +``` + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` diff --git a/templates/mlx_saft_reg_d_v1_3.md b/templates/mlx_saft_reg_d_v1_3.md new file mode 100644 index 00000000..b2488bec --- /dev/null +++ b/templates/mlx_saft_reg_d_v1_3.md @@ -0,0 +1,62 @@ +# Data Overview + +id: bytes32(bytes("mlx_saft_reg_d_v1_3")) +idHex: 0x6d6c785f736166745f7265675f645f76315f3300000000000000000000000000 +title: mlx_saft_reg_d_v1_3 + +combined doc: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeieoljri2rwuv35rymjd654sr3u46kbcao7mymseqobfo7x6lxgdcy +SAFT alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeid4xqmpm75krujhpzrzh4kvb6r2fjawdo37tbiq7o5kxwix64w5wa + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:----------------| +| purchaseAmount | e.g. "1000.00" | +| protocolValuationCap | | +| governingJurisdiction | | +| disputeResolution | | +| unlockStartTimeType | "agreementExecutionTime" \| "tgeTime" \| "setTime" | +| unlockStartTime | only set if using `setTime` for `unlockStartTimeType` | +| unlockingPeriod | Duration in `unlockingIntervalType` units | +| unlockingCliffPeriod | Duration in `unlockingIntervalType` | +| unlockingCliffPercentage | e.g. "10.5%" | +| unlockingIntervalType | "secondly", "hourly", "daily", "monthly", "blockly" | + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + +## Certificate Extension + +name: SAFTEExtension + +```solidity +struct SAFTData { + UnlockStartTimeType unlockStartTimeType; + uint256 agreementExecutionTime; + uint256 unlockingPeriod; + uint256 unlockingCliffPeriod; + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; +} +``` + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` diff --git a/templates/mlx_saft_reg_s_v1_3.md b/templates/mlx_saft_reg_s_v1_3.md new file mode 100644 index 00000000..73764978 --- /dev/null +++ b/templates/mlx_saft_reg_s_v1_3.md @@ -0,0 +1,62 @@ +# Data Overview + +id: bytes32(bytes("mlx_saft_reg_s_v1_3")) +idHex: 0x6d6c785f736166745f7265675f735f76315f3300000000000000000000000000 +title: mlx_saft_reg_s_v1_3 + +combined doc: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeibwrz3rttteguo5ccoh5x7ndwdu6hyhy7i3iraii5c5ml4pfv73t4 +SAFT alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeighy3fgweoeivmxnp62ryrckn7xgtjerrsefrkxrruea6tq2q6gyi + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:----------------| +| purchaseAmount | e.g. "1000.00" | +| protocolValuationCap | | +| governingJurisdiction | | +| disputeResolution | | +| unlockStartTimeType | "agreementExecutionTime" \| "tgeTime" \| "setTime" | +| unlockStartTime | only set if using `setTime` for `unlockStartTimeType` | +| unlockingPeriod | Duration in `unlockingIntervalType` units | +| unlockingCliffPeriod | Duration in `unlockingIntervalType` | +| unlockingCliffPercentage | e.g. "10.5%" | +| unlockingIntervalType | "secondly", "hourly", "daily", "monthly", "blockly" | + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + +## Certificate Extension + +name: SAFTEExtension + +```solidity +struct SAFTData { + UnlockStartTimeType unlockStartTimeType; + uint256 agreementExecutionTime; + uint256 unlockingPeriod; + uint256 unlockingCliffPeriod; + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; +} +``` + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` diff --git a/templates/mlx_safte_reg_d_v1_3.md b/templates/mlx_safte_reg_d_v1_3.md new file mode 100644 index 00000000..a8f6490b --- /dev/null +++ b/templates/mlx_safte_reg_d_v1_3.md @@ -0,0 +1,74 @@ +# Data Overview + +id: bytes32(bytes("mlx_safte_reg_d_v1_3")) +idHex: 0x6d6c785f73616674655f7265675f645f76315f33000000000000000000000000 +title: mlx_safte_reg_d_v1_3 + +legalURI: +safeURI: IPFS://bafybeiag7xatsusb24evnpyj6ztf62kix36dgbsp3kbazfyvr273ph56ay + +combined doc: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeiag7xatsusb24evnpyj6ztf62kix36dgbsp3kbazfyvr273ph56ay +SAFTE alone: https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeid4xxgesjbxpdwx3dmcxlupzscurpnh6c3k7lukhzt2fsfkbxjr34 + +## Global Fields + +| **globalFieldName** | **description** | +|:--------------------|:----------------| +| purchaseAmount | e.g. "1000.00" | +| postMoneyValuationCap | | +| protocolUSDValuationAtTimeofInvestment | | +| expirationTime | | +| governingJurisdiction | | +| disputeResolution | | +| unlockStartTimeType | "agreementExecutionTime" \| "tgeTime" \| "setTime" | +| unlockStartTime | only set if using `setTime` for `unlockStartTimeType` | +| unlockingPeriod | Duration in `unlockingIntervalType` units | +| unlockingCliffPeriod | Duration in `unlockingIntervalType` | +| unlockingCliffPercentage | e.g. "10.5%" | +| unlockingIntervalType | "secondly", "hourly", "daily", "monthly", "blockly" | +| tokenCalculationMethod | `equityProRataToTokenSupply` or `equityProRataToCompanyReserve` | +| minCompanyReserve | Number of tokens | +| tokenPremiumMultiplier | Premium multiplier | + +## Party Fields + +| **partyFieldName** | **description** | +|:-------------------|:----------------| +| name | Name of the individual or organization | +| evmAddress | | +| contactDetails | | +| investorType | | +| investorJurisdiction | | + +## Certificate Extension + +name: SAFTEExtension + +```solidity +struct SAFTEData { + uint256 protocolUSDValuationAtTimeofInvestment; + UnlockStartTimeType unlockStartTimeType; + uint256 unlockStartTime; + uint256 unlockingPeriod; + uint256 unlockingCliffPeriod; + uint256 unlockingCliffPercentage; + UnlockingIntervalType unlockingIntervalType; + TokenCalculationMethod tokenCalculationMethod; + uint256 minCompanyReserve; + uint256 tokenPremiumMultiplier; +} +``` + +## CertificateDetails Struct (for reference) + +```solidity +struct CertificateDetails { + string signingOfficerName; + string signingOfficerTitle; + uint256 investmentAmountUSD; + uint256 issuerUSDValuationAtTimeOfInvestment; + uint256 unitsRepresented; + string legalDetails; + bytes extensionData; +} +``` diff --git a/test/BaseSepoliaFactoryFcfsForkTest.t.sol b/test/BaseSepoliaFactoryFcfsForkTest.t.sol new file mode 100644 index 00000000..ecdd8a81 --- /dev/null +++ b/test/BaseSepoliaFactoryFcfsForkTest.t.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import {CyberCorpHelper} from "./RoundManagerTest.t.sol"; +import {KnownAddressesLoaded} from "./libs/KnownAddressesLoaded.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {CyberCorpFactory} from "../src/CyberCorpFactory.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import { + CompanyOfficer, + SecurityClass, + SecuritySeries +} from "../src/CyberCorpConstants.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {Round, RoundType} from "../src/libs/RoundLib.sol"; +import {Escrow, EscrowStatus} from "../src/storage/LexScrowStorage.sol"; +import {CyberCertData, EOI} from "../src/storage/RoundManagerStorage.sol"; + +contract BaseSepoliaFactoryFcfsForkTest is Test, KnownAddressesLoaded { + uint256 internal constant FOUNDER_PK = 0xA11CE; + uint256 internal constant OFFICER_PK = 0xB0B; + uint256 internal constant INVESTOR_PK = 0xC0DE; + + uint256 internal constant RAISE_CAP = 1_000_000e6; + uint256 internal constant TICKET = 25_000e6; + uint256 internal constant PRICE_PER_UNIT = 10e18; + uint256 internal constant VALUATION = 20_000_000e18; + + CyberAgreementRegistry internal registry; + CyberCorpFactory internal cyberCorpFactory; + CyberCorpSingleFactory internal cyberCorpSingleFactory; + RoundManagerFactory internal roundManagerFactory; + ERC20 internal stable; + + address internal founder; + address internal officer; + address internal investor; + + function setUp() public { + //assertEq(block.chainid, BASE_SEPOLIA_CHAIN_ID, "Fork test: Base Sepolia only"); + //vm.rollFork(BASE_SEPOLIA_FORK_BLOCK); // TODO: Uncomment this when the fork is ready + + registry = CyberAgreementRegistry(CYBER_AGREEMENT_REGISTRY); + cyberCorpFactory = CyberCorpFactory(CYBERCORP_FACTORY); + cyberCorpSingleFactory = CyberCorpSingleFactory(cyberCorpFactory.cyberCorpSingleFactory()); + roundManagerFactory = RoundManagerFactory(cyberCorpFactory.roundManagerFactory()); + stable = ERC20(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913); + + founder = vm.addr(FOUNDER_PK); + officer = vm.addr(OFFICER_PK); + investor = vm.addr(INVESTOR_PK); + } + + function test_BaseSepoliaFactory_CreatesFcfsRound_AndInvestorFundsIt() public { + bytes32 templateId = bytes32(uint256(5535)); + _createTemplate(templateId); + + uint256 salt = uint256(keccak256("BaseSepoliaFactoryFcfsForkTest.corp")); + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + address predictedCorp = cyberCorpSingleFactory.computeCyberCorpSingleAddress(corpSalt); + address predictedRoundManager = roundManagerFactory.computeRoundManagerAddress(corpSalt); + + CompanyOfficer memory companyOfficer = CompanyOfficer({ + eoa: officer, + name: "Fork Officer", + contact: "officer@cybercorp.test", + title: "CEO" + }); + + string[] memory legalDetails = new string[](1); + legalDetails[0] = "Base Sepolia FCFS SAFE"; + + bytes[] memory extensionData = new bytes[](1); + extensionData[0] = ""; + + string[] memory defaultLegend = new string[](1); + defaultLegend[0] = "Legend"; + + CyberCertData[] memory certData = new CyberCertData[](1); + certData[0] = CyberCertData({ + name: "SEED SAFE", + symbol: "SEEDSAFE", + uri: "ipfs://base-sepolia-fcfs-safe", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesSeed, + extension: address(0), + defaultLegend: defaultLegend + }); + + string[] memory roundPartyValues = new string[](2); + roundPartyValues[0] = companyOfficer.name; + roundPartyValues[1] = companyOfficer.title; + + uint256 startTime = block.timestamp - 1; + uint256 endTime = block.timestamp + 30 days; + + (bytes memory escrowedSig, ) = CyberCorpHelper.computeEscrowSignature( + predictedRoundManager, + SecuritySeries.SeriesSeed, + RAISE_CAP, + TICKET, + TICKET, + RoundType.FCFS, + startTime, + endTime, + templateId, + address(stable), + PRICE_PER_UNIT, + VALUATION, + OFFICER_PK, + predictedCorp + ); + + ( + address corp, + , + , + , + address roundManagerAddr, + bytes32 roundId + ) = cyberCorpFactory.deployCyberCorpAndCreateRound( + salt, + SecuritySeries.SeriesSeed, + "Base Sepolia FCFS Corp", + "Delaware C-Corp", + "DE", + "founder@cybercorp.test", + "Arbitration", + founder, + companyOfficer, + legalDetails, + extensionData, + certData, + templateId, + address(stable), + PRICE_PER_UNIT, + VALUATION, + roundPartyValues, + escrowedSig, + RoundType.FCFS, + new address[](0), + RAISE_CAP, + TICKET, + TICKET, + startTime, + endTime, + true, + true, + false + ); + + assertEq(corp, predictedCorp, "unexpected corp address"); + assertEq(roundManagerAddr, predictedRoundManager, "unexpected round manager address"); + + RoundManager roundManager = RoundManager(roundManagerAddr); + Round memory createdRound = roundManager.getRound(roundId); + assertEq(createdRound.paymentToken, address(stable), "wrong payment token"); + assertEq(uint256(createdRound.roundType), uint256(RoundType.FCFS), "wrong round type"); + assertEq(createdRound.raiseCap, RAISE_CAP, "wrong raise cap"); + assertEq(createdRound.raised, 0, "new round should start empty"); + + deal(address(stable), investor, TICKET * 4); + + string[] memory globalValues = new string[](1); + globalValues[0] = "Base Sepolia"; + + string[] memory eoiPartyValues = new string[](2); + eoiPartyValues[0] = "Fork Investor"; + eoiPartyValues[1] = "Individual"; + + uint256 eoiSalt = 777; + bytes memory eoiSignature = CyberCorpHelper.computeEOISignature( + registry, + templateId, + eoiSalt, + globalValues, + eoiPartyValues, + companyOfficer.eoa, + INVESTOR_PK + ); + + EOI memory eoi = EOI({ + name: "Fork Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@cybercorp.test", + minAmount: TICKET, + maxAmount: TICKET, + expiry: block.timestamp + 7 days, + naturalPerson: true, + lexchexDetails: CyberCorpHelper.emptyLex() + }); + + uint256 investorBalanceBefore = stable.balanceOf(investor); + + vm.startPrank(investor); + stable.approve(roundManagerAddr, TICKET); + (bytes32 agreementId, ) = roundManager.submitEOI( + roundId, + eoi, + globalValues, + eoiPartyValues, + eoiSignature, + eoiSalt, + new address[](0), + bytes32(0) + ); + vm.stopPrank(); + + Round memory fundedRound = roundManager.getRound(roundId); + Escrow memory escrow = roundManager.getEscrowDetails(agreementId); + + assertEq(fundedRound.raised, TICKET, "fcfs submission should raise funds immediately"); + assertEq(investorBalanceBefore - stable.balanceOf(investor), TICKET, "investor should spend the ticket"); + assertEq(uint256(escrow.status), uint256(EscrowStatus.FINALIZED), "escrow should finalize"); + assertGt(escrow.corpAssets.length, 0, "allocation should mint corp-side assets"); + } + + function _createTemplate(bytes32 templateId) internal { + string[] memory globalFields = new string[](1); + globalFields[0] = "Jurisdiction"; + + string[] memory partyFields = new string[](2); + partyFields[0] = "Officer Name"; + partyFields[1] = "Officer Title"; + + vm.prank(METALEX_SAFE); + registry.createTemplate( + templateId, + "Base Sepolia FCFS Template", + "ipfs://base-sepolia-fcfs-template", + globalFields, + partyFields + ); + } +} diff --git a/test/CyberCertPrinterReadForkTest.t.sol b/test/CyberCertPrinterReadForkTest.t.sol new file mode 100644 index 00000000..99f5bb0a --- /dev/null +++ b/test/CyberCertPrinterReadForkTest.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test, console2} from "forge-std/Test.sol"; +import {ICyberCertPrinter} from "../src/interfaces/ICyberCertPrinter.sol"; +import {CertificateDetails} from "../src/storage/CyberCertPrinterStorage.sol"; + +contract CyberCertPrinterReadForkTest is Test { + address internal constant PRINTER = 0xf77f10816D376E2D1f4a3FAF548E0E9142aB11D9; + + function test_ReadPrinterUriAndCertificateDetails() public { + ICyberCertPrinter printer = ICyberCertPrinter(PRINTER); + + console2.log("printer", PRINTER); + console2.log("name", printer.name()); + console2.log("symbol", printer.symbol()); + console2.log("certificateUri", printer.certificateUri()); + + uint256 supply = printer.totalSupply(); + console2.log("totalSupply", supply); + + if (supply == 0) { + console2.log("No certificates minted on this printer yet."); + return; + } + + uint256 tokenId = printer.tokenByIndex(0); + console2.log("tokenId", tokenId); + + try printer.tokenURI(tokenId) returns (string memory uri) { + console2.log("tokenURI", uri); + } catch { + console2.log("tokenURI read reverted"); + } + + try printer.getCertificateDetails(tokenId) returns (CertificateDetails memory details) { + console2.log("signingOfficerName", details.signingOfficerName); + console2.log("signingOfficerTitle", details.signingOfficerTitle); + console2.log("investmentAmountUSD", details.investmentAmountUSD); + console2.log("issuerUSDValuationAtTimeOfInvestment", details.issuerUSDValuationAtTimeOfInvestment); + console2.log("unitsRepresented", details.unitsRepresented); + console2.log("legalDetails", details.legalDetails); + console2.logBytes(details.extensionData); + } catch { + console2.log("certificate details read reverted"); + } + } +} diff --git a/test/CyberCorpTest.t.sol b/test/CyberCorpTest.t.sol index 81d38e54..5cff601b 100644 --- a/test/CyberCorpTest.t.sol +++ b/test/CyberCorpTest.t.sol @@ -61,6 +61,7 @@ import {CertificateDetails} from "../src/storage/CyberCertPrinterStorage.sol"; import {CompanyOfficer} from "../src/storage/CyberCertPrinterStorage.sol"; import {ToggleTransferHook} from "../src/hooks/transfer/ToggleTransferHook.sol"; import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol"; +import {CertificateImageBuilderContract} from "../src/CertificateImageBuilderContract.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {DealManager} from "../src/DealManager.sol"; @@ -213,6 +214,10 @@ contract CyberCorpTest is Test { ) ))); + // Deploy the CertificateImageBuilderContract (standalone contract for SVG generation) + address imageBuilder = address(new CertificateImageBuilderContract{salt: salt}()); + + // Deploy CertificateUriBuilder proxy address uriBuilder = address(new ERC1967Proxy{salt: salt}( address(new CertificateUriBuilder{salt: salt}()), abi.encodeWithSelector( @@ -220,6 +225,9 @@ contract CyberCorpTest is Test { address(auth)) )); + // Set the image builder on the CertificateUriBuilder + CertificateUriBuilder(uriBuilder).setImageBuilder(imageBuilder); + // RoundManager via factory and initialize address rmFactory = address( new ERC1967Proxy{salt: salt}( @@ -2590,6 +2598,7 @@ contract CyberCorpTest is Test { //create test to print certificateuri function testPrintCertificateUri() public { + // vm.warp(block.timestamp - 3000000); vm.startPrank(testAddress); CyberCorpFactory cyberCorpFactoryLive = CyberCorpFactory( 0x2aDA6E66a92CbF283B9F2f4f095Fe705faD357B8 @@ -2597,14 +2606,15 @@ contract CyberCorpTest is Test { CertificateDetails[] memory _details = new CertificateDetails[](1); CertificateDetails memory _detailsA = CertificateDetails({ - signingOfficerName: "Gabe", + signingOfficerName: "Gabriel Shapiro", signingOfficerTitle: "CEO", - investmentAmountUSD: 100000, - issuerUSDValuationAtTimeOfInvestment: 100000000, - unitsRepresented: 100000, + investmentAmountUSD: 10000000000000000, + issuerUSDValuationAtTimeOfInvestment: 10000000000000000000, + unitsRepresented: 10000000000000000, legalDetails: "Legal Details", extensionData: "" }); + _details[0] = _detailsA; CompanyOfficer memory officer = CompanyOfficer({ eoa: testAddress, @@ -2636,16 +2646,16 @@ contract CyberCorpTest is Test { globalValues[0] = "100000"; globalValues[1] = "100000000"; globalValues[2] = "12/1/2025"; - globalValues[3] = "Deleware"; + globalValues[3] = "Delaware"; globalValues[4] = "Binding Arbitration"; string[][] memory partyValues = new string[][](1); partyValues[0] = new string[](5); - partyValues[0][0] = "Gabe"; + partyValues[0][0] = "Gabriel Shapiro"; partyValues[0][1] = "0xDEADBABE12345678909876543210866666666666"; - partyValues[0][2] = "@Gabe"; + partyValues[0][2] = "@gabe"; partyValues[0][3] = "Limited Liability Company"; - partyValues[0][4] = "Deleware"; + partyValues[0][4] = "Delaware"; bytes32 contractId = keccak256( abi.encode( @@ -2669,6 +2679,19 @@ contract CyberCorpTest is Test { testPrivateKey ); + CyberCorpFactory.CyberCertData[] memory _certData = new CyberCorpFactory.CyberCertData[](1); + string[] memory defaultLegend = new string[](1); + defaultLegend[0] = "Legend 1"; + _certData[0] = CyberCorpFactory.CyberCertData({ + name: "Cert Name 1", + symbol: "Cert Symbol 1", + uri: "https://beige-just-flyingfish-108.mypinata.cloud/ipfs/bafybeiafzkynirjta4pd3g365qv6ttlz3pkeqcquhbald7nqqfmm5vpfua", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesPreSeed, + extension: address(0), + defaultLegend: defaultLegend + }); + ( address cyberCorp, address auth, @@ -2680,14 +2703,14 @@ contract CyberCorpTest is Test { uint256[] memory certIds ) = cyberCorpFactory.deployCyberCorpAndCreateOffer( block.timestamp, - "CyberCorp", + "Test CyberCorp, LLC", "Limited Liability Company", - "Juris", + "Delaware", "Contact Details", "Dispute Res", testAddress, officer, - certData, + _certData, bytes32(uint256(2)), globalValues, parties, @@ -2708,7 +2731,7 @@ contract CyberCorpTest is Test { partyValuesB[1] = "0xC0FFEEBABE12345678909876543210866666666666"; partyValuesB[2] = "@0xPrepop"; partyValuesB[3] = "Limited Liability Company"; - partyValuesB[4] = "Deleware"; + partyValuesB[4] = "Delaware"; vm.startPrank(newPartyAddr); bytes memory newPartySignature = CyberAgreementUtils.signAgreementTypedData( @@ -2739,11 +2762,18 @@ contract CyberCorpTest is Test { partyValuesB, newPartySignature, true, - "John Doe", + "Mr. Prepop", "" ); vm.stopPrank(); - console.log("printer addr length:", cyberCertPrinterAddr.length); + vm.warp(block.timestamp + 3000000); + string memory endorseeName = CyberCertPrinter(cyberCertPrinterAddr[0]).getEndorsementHistory(0, 0).endorseeName; + console.log("endorsee name:", endorseeName); + // console.log("printer addr length:", cyberCertPrinterAddr.length); + + //print endorsee name + + string memory certificateUri = CyberCertPrinter(cyberCertPrinterAddr[0]) .tokenURI(0); console.log(certificateUri); @@ -2870,7 +2900,7 @@ contract CyberCorpTest is Test { globalValues[0] = "100000"; globalValues[1] = "100000000"; globalValues[2] = "12/1/2025"; - globalValues[3] = "Deleware"; + globalValues[3] = "Delaware"; globalValues[4] = "Binding Arbitration"; string[][] memory partyValues = new string[][](1); @@ -2879,7 +2909,7 @@ contract CyberCorpTest is Test { partyValues[0][1] = "0xDEADBABE12345678909876543210866666666666"; partyValues[0][2] = "@Gabe"; partyValues[0][3] = "Limited Liability Company"; - partyValues[0][4] = "Deleware"; + partyValues[0][4] = "Delaware"; bytes32 contractId = keccak256( abi.encode( @@ -2946,7 +2976,7 @@ contract CyberCorpTest is Test { partyValuesB[1] = "0xC0FFEEBABE12345678909876543210866666666666"; partyValuesB[2] = "@0xPrepop"; partyValuesB[3] = "Limited Liability Company"; - partyValuesB[4] = "Deleware"; + partyValuesB[4] = "Delaware"; vm.startPrank(newPartyAddr); bytes memory newPartySignature = CyberAgreementUtils.signAgreementTypedData( @@ -3201,6 +3231,9 @@ contract CyberCorpTest is Test { // Deploy new implementation address newImplementation = address(new CertificateUriBuilder()); + // Deploy new image builder contract + address newImageBuilder = address(new CertificateImageBuilderContract()); + // Upgrade to new implementation without initialization data // Non-owner should not be able to upgrade it @@ -3212,6 +3245,11 @@ contract CyberCorpTest is Test { uriBuilder.upgradeToAndCall(newImplementation, ""); assertEq(address(uriBuilder).getErc1967Implementation(), newImplementation); + // Set the new image builder (required for the new architecture) + vm.prank(multisig); + uriBuilder.setImageBuilder(newImageBuilder); + assertEq(uriBuilder.imageBuilder(), newImageBuilder); + // Verify the URI builder still works assertEq(uriBuilder.securityClassToString(SecurityClass.SAFT), "SAFT"); } @@ -3868,16 +3906,24 @@ contract CyberCorpTest is Test { function testPrintCertificateSAFTEUri() public { vm.startPrank(testAddress); - //bytes32 check = bytes32(bytes("nuvolari_safet")); bytes32 check = bytes32(bytes("ABV_safe_t")); console.logBytes32(check); - check = bytes32(uint256(30)); + console.log("blackhaven_safe_t"); + check = bytes32(bytes("blackhaven_safe_t")); console.logBytes32(check); - - check = bytes32(uint256(31)); + check = bytes32(bytes("mlx_safe_reg_s_v1_3")); console.logBytes32(check); - - check = bytes32(uint256(32)); + check = bytes32(bytes("mlx_safe_tw_reg_d_v1_3")); + console.logBytes32(check); + check = bytes32(bytes("mlx_safe_tw_reg_s_v1_3")); + console.logBytes32(check); + check = bytes32(bytes("mlx_safte_reg_d_v1_3")); + console.logBytes32(check); + check = bytes32(bytes("mlx_safte_reg_s_v1_3")); + console.logBytes32(check); + check = bytes32(bytes("mlx_saft_reg_d_v1_3")); + console.logBytes32(check); + check = bytes32(bytes("mlx_saft_reg_s_v1_3")); console.logBytes32(check); bytes32 salt = bytes32(keccak256("TestSAFTE")); @@ -3943,7 +3989,7 @@ contract CyberCorpTest is Test { globalValues[0] = "100000"; globalValues[1] = "100000000"; globalValues[2] = "12/1/2025"; - globalValues[3] = "Deleware"; + globalValues[3] = "Delaware"; globalValues[4] = "Binding Arbitration"; string[][] memory partyValues = new string[][](1); @@ -3952,7 +3998,7 @@ contract CyberCorpTest is Test { partyValues[0][1] = "0xDEADBABE12345678909876543210866666666666"; partyValues[0][2] = "@Gabe"; partyValues[0][3] = "Limited Liability Company"; - partyValues[0][4] = "Deleware"; + partyValues[0][4] = "Delaware"; bytes32 contractId = keccak256( abi.encode( @@ -4016,7 +4062,7 @@ contract CyberCorpTest is Test { partyValuesB[1] = "0xC0FFEEBABE12345678909876543210866666666666"; partyValuesB[2] = "@0xPrepop"; partyValuesB[3] = "Limited Liability Company"; - partyValuesB[4] = "Deleware"; + partyValuesB[4] = "Delaware"; vm.startPrank(newPartyAddr); bytes memory newPartySignature = CyberAgreementUtils.signAgreementTypedData( @@ -5010,6 +5056,7 @@ contract CyberCorpTest is Test { function testLexChexMinterIntegration() public { uint256 investorPk = 12345; address investorAddr = vm.addr(investorPk); + vm.etch(investorAddr, hex""); vm.startPrank(multisig); auth.updateRole(address(testAddress), 98); // Give testAddress ADMIN_ROLE for testing @@ -5225,6 +5272,7 @@ contract CyberCorpTest is Test { address(dealManager), _paymentAmount ); + vm.stopPrank(); bytes memory investorDealSignature = CyberAgreementUtils.signAgreementTypedData( vm, @@ -5240,6 +5288,7 @@ contract CyberCorpTest is Test { ); // This should succeed because the investor has a valid LexChex token + vm.startPrank(testAddress); dealManager.signAndFinalizeDeal( investorAddr, dealContractId, @@ -5668,7 +5717,11 @@ contract CyberCorpTest is Test { hook.setTokenTransferable(1, true); vm.stopPrank(); - // Mint 3 certificates to the owner (testAddress) + // Mint 3 certificates to an EOA owner (avoid receiver-hook issues if testAddress has code) + address certOwner = vm.addr(0xA11CE); + assertEq(certOwner.code.length, 0, "certOwner must be EOA"); + + // Mint 3 certificates to the owner CertificateDetails memory cd = CertificateDetails({ signingOfficerName: "", signingOfficerTitle: "", @@ -5680,25 +5733,25 @@ contract CyberCorpTest is Test { }); vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // tokenId 0 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // tokenId 0 vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // tokenId 1 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // tokenId 1 vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // tokenId 2 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // tokenId 2 // Prepare recipient address recipient = vm.addr(0xBEEF); // Token 0 should be blocked by hook - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(abi.encodeWithSelector(CyberCertPrinter.TransferRestricted.selector, "Transfer disabled by global hook")); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient, 0); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient, 0); vm.stopPrank(); // Token 1 should be allowed by hook, but endorsement is required by printer - vm.startPrank(testAddress); + vm.startPrank(certOwner); Endorsement memory e = Endorsement({ - endorser: testAddress, + endorser: certOwner, timestamp: block.timestamp, signatureHash: bytes("hook-test"), registry: address(0), @@ -5708,14 +5761,14 @@ contract CyberCorpTest is Test { }); CyberCertPrinter(certPrinter).addEndorsement(1, e); vm.stopPrank(); - vm.prank(testAddress); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient, 1); + vm.prank(certOwner); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient, 1); assertEq(CyberCertPrinter(certPrinter).ownerOf(1), recipient); // Token 2 should be blocked - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(abi.encodeWithSelector(CyberCertPrinter.TransferRestricted.selector, "Transfer disabled by global hook")); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient, 2); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient, 2); vm.stopPrank(); } @@ -5759,8 +5812,10 @@ contract CyberCorpTest is Test { ); vm.stopPrank(); - // Create a certificate printer and mint two certs to testAddress + // Create a certificate printer and mint two certs to a code-less EOA recipient string[] memory ledger = new string[](0); + address certOwner = vm.addr(0xA11CE); + vm.etch(certOwner, hex""); vm.prank(testAddress); address certPrinter = IssuanceManager(issuanceManager).createCertPrinter( ledger, @@ -5782,16 +5837,16 @@ contract CyberCorpTest is Test { extensionData: "" }); vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // tokenId 0 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // tokenId 0 vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // tokenId 1 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // tokenId 1 address recipient = vm.addr(0xCAFE); // Global off; token 0 off => revert - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(abi.encodeWithSignature("TokenNotTransferable()")); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient, 0); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient, 0); vm.stopPrank(); // Enable token 0 only @@ -5799,14 +5854,14 @@ contract CyberCorpTest is Test { CyberCertPrinter(certPrinter).setTokenTransferable(0, true); // Without endorsement should still revert - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(CyberCertPrinter.EndorsementNotSignedOrInvalid.selector); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient, 0); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient, 0); vm.stopPrank(); // Add endorsement and transfer succeeds for token 0 Endorsement memory e = Endorsement({ - endorser: testAddress, + endorser: certOwner, timestamp: block.timestamp, signatureHash: hex"01", registry: address(0), @@ -5814,16 +5869,16 @@ contract CyberCorpTest is Test { endorsee: recipient, endorseeName: "Recipient" }); - vm.prank(testAddress); + vm.prank(certOwner); CyberCertPrinter(certPrinter).addEndorsement(0, e); - vm.prank(testAddress); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient, 0); + vm.prank(certOwner); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient, 0); assertEq(CyberCertPrinter(certPrinter).ownerOf(0), recipient); // Token 1 should remain blocked - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(abi.encodeWithSignature("TokenNotTransferable()")); - CyberCertPrinter(certPrinter).transferFrom(testAddress, vm.addr(0xBEEF), 1); + CyberCertPrinter(certPrinter).transferFrom(certOwner, vm.addr(0xBEEF), 1); vm.stopPrank(); } @@ -5894,8 +5949,10 @@ contract CyberCorpTest is Test { ); vm.stopPrank(); - // Create printer and mint two certs to testAddress + // Create printer and mint two certs to a code-less EOA recipient string[] memory ledger = new string[](0); + address certOwner = vm.addr(0xA11CE); + vm.etch(certOwner, hex""); vm.prank(testAddress); address certPrinter = IssuanceManager(issuanceManager).createCertPrinter( ledger, @@ -5916,16 +5973,16 @@ contract CyberCorpTest is Test { extensionData: "" }); vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // token 0 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // token 0 vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // token 1 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // token 1 address recipient1 = vm.addr(0x1001); address recipient2 = vm.addr(0x1002); // Add endorsement for token 0 -> recipient1 Endorsement memory e0 = Endorsement({ - endorser: testAddress, + endorser: certOwner, timestamp: block.timestamp, signatureHash: hex"01", registry: address(0), @@ -5933,20 +5990,20 @@ contract CyberCorpTest is Test { endorsee: recipient1, endorseeName: "R1" }); - vm.prank(testAddress); + vm.prank(certOwner); CyberCertPrinter(certPrinter).addEndorsement(0, e0); // Before enabling global: expect TokenNotTransferable - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(abi.encodeWithSignature("TokenNotTransferable()")); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient1, 0); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient1, 0); vm.stopPrank(); // Turn global on, transfer succeeds vm.prank(issuanceManager); CyberCertPrinter(certPrinter).setGlobalTransferable(true); - vm.prank(testAddress); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient1, 0); + vm.prank(certOwner); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient1, 0); assertEq(CyberCertPrinter(certPrinter).ownerOf(0), recipient1); // Global off again @@ -5959,7 +6016,7 @@ contract CyberCorpTest is Test { // Add endorsement for token 1 -> recipient2 and ensure it still reverts due to global off and no per-token flag Endorsement memory e1 = Endorsement({ - endorser: testAddress, + endorser: certOwner, timestamp: block.timestamp, signatureHash: hex"02", registry: address(0), @@ -5967,11 +6024,11 @@ contract CyberCorpTest is Test { endorsee: recipient2, endorseeName: "R2" }); - vm.prank(testAddress); + vm.prank(certOwner); CyberCertPrinter(certPrinter).addEndorsement(1, e1); - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(abi.encodeWithSignature("TokenNotTransferable()")); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient2, 1); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient2, 1); vm.stopPrank(); } @@ -6001,8 +6058,10 @@ contract CyberCorpTest is Test { ); vm.stopPrank(); - // Create printer and mint token 0 + // Create printer and mint token 0 to a code-less EOA recipient string[] memory ledger = new string[](0); + address certOwner = vm.addr(0xA11CE); + vm.etch(certOwner, hex""); vm.prank(testAddress); address certPrinter = IssuanceManager(issuanceManager).createCertPrinter( ledger, @@ -6023,7 +6082,7 @@ contract CyberCorpTest is Test { extensionData: "" }); vm.prank(testAddress); - IssuanceManager(issuanceManager).createCert(certPrinter, testAddress, cd); // token 0 + IssuanceManager(issuanceManager).createCert(certPrinter, certOwner, cd); // token 0 // Enable per-token transferability for token 0 vm.prank(issuanceManager); @@ -6041,7 +6100,7 @@ contract CyberCorpTest is Test { // With endorsement, transfer should still be blocked by hook address recipient = vm.addr(0x2222); Endorsement memory e = Endorsement({ - endorser: testAddress, + endorser: certOwner, timestamp: block.timestamp, signatureHash: hex"01", registry: address(0), @@ -6049,11 +6108,11 @@ contract CyberCorpTest is Test { endorsee: recipient, endorseeName: "R" }); - vm.prank(testAddress); + vm.prank(certOwner); CyberCertPrinter(certPrinter).addEndorsement(0, e); - vm.startPrank(testAddress); + vm.startPrank(certOwner); vm.expectRevert(abi.encodeWithSelector(CyberCertPrinter.TransferRestricted.selector, "Transfer disabled by global hook")); - CyberCertPrinter(certPrinter).transferFrom(testAddress, recipient, 0); + CyberCertPrinter(certPrinter).transferFrom(certOwner, recipient, 0); vm.stopPrank(); } diff --git a/test/CyberCorpUpgradeabilityTest.t.sol b/test/CyberCorpUpgradeabilityTest.t.sol index 46e5ca77..f2f87984 100644 --- a/test/CyberCorpUpgradeabilityTest.t.sol +++ b/test/CyberCorpUpgradeabilityTest.t.sol @@ -440,7 +440,15 @@ contract CyberCorpUpgradeabilityTest is Test { cyberCertPrinterAddrs[0], // certAddress new ITransferRestrictionHook[](0), // typeRestrictionHooks new ICondition[](0), // certToScripConditions - new ICondition[](0) // scripToCertConditions + new ICondition[](0), // scripToCertConditions + 0, // scripToCertMinimum + 1, // scripRatioNumerator + 1, // scripRatioDenominator + new uint256[](0), // scripifyWhitelistIds + false, // scripifyWhitelistEnabled + true, // enableForceTransfer + true, // enableForceBurn + true // enableFreeze ) ); diff --git a/test/CyberScripTest.t.sol b/test/CyberScripTest.t.sol index 8708b428..1b5332bf 100644 --- a/test/CyberScripTest.t.sol +++ b/test/CyberScripTest.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../test/mock/TestableCyberScrip.sol"; import "../test/mock/MockTransferHook.sol"; +import "../src/libs/auth.sol"; contract CyberScripTest is Test { TestableCyberScrip public cyberScrip; @@ -15,6 +16,10 @@ contract CyberScripTest is Test { address public user2; MockTransferHook public mockHook; + event FreezeStatusUpdated(address indexed account, bool frozen); + event ComplianceFeatureDisabledEvent(string feature); + event MaxHolderCountUpdated(uint256 maxHolderCount); + function setUp() public { owner = address(this); issuanceManager = makeAddr("issuanceManager"); @@ -28,6 +33,9 @@ contract CyberScripTest is Test { // Setup transfer restriction hooks ITransferRestrictionHook[] memory hooks = new ITransferRestrictionHook[](1); hooks[0] = ITransferRestrictionHook(address(mockHook)); + bytes32 salt = keccak256("CyberScripTest"); + //deploy auth + address auth = address(new BorgAuth{salt: salt}(owner)); // Deploy CyberScrip (testable) cyberScrip = TestableCyberScrip(address( @@ -35,6 +43,7 @@ contract CyberScripTest is Test { address(new TestableCyberScrip()), abi.encodeWithSelector( CyberScrip.initialize.selector, + auth, certPrinter, issuanceManager, "Test CyberScrip", @@ -48,7 +57,7 @@ contract CyberScripTest is Test { )); // Mint initial balance for user1 - cyberScrip.mint(user1, 1000 ether); + cyberScrip.unrestrictedMint(user1, 1000 ether); } function test_Initialization() public { @@ -124,6 +133,7 @@ contract CyberScripTest is Test { address(new TestableCyberScrip()), abi.encodeWithSelector( CyberScrip.initialize.selector, + makeAddr("auth"), certPrinter, issuanceManager, "Disabled", @@ -135,7 +145,7 @@ contract CyberScripTest is Test { ) ) )); - disabled.mint(user1, 1000 ether); + disabled.unrestrictedMint(user1, 1000 ether); vm.startPrank(issuanceManager); vm.expectRevert(abi.encodeWithSignature("ComplianceFeatureDisabled()")); @@ -258,15 +268,170 @@ contract CyberScripTest is Test { // Additional coverage // ------------------------ + function test_HolderCount_InitAndTransferAllUpdates() public { + assertEq(cyberScrip.holderCount(), 1); + + vm.startPrank(user1); + cyberScrip.transfer(user2, 100 ether); + vm.stopPrank(); + assertEq(cyberScrip.holderCount(), 2); + + vm.startPrank(user1); + cyberScrip.transfer(user2, 900 ether); + vm.stopPrank(); + assertEq(cyberScrip.holderCount(), 1); + } + + function test_HolderCount_BurnToZero() public { + vm.prank(issuanceManager); + cyberScrip.burnFrom(user1, 1000 ether); + assertEq(cyberScrip.holderCount(), 0); + } + + function test_MaxHolderCountBlocksTransfer() public { + vm.prank(issuanceManager); + cyberScrip.setMaxHolderCount(1); + + vm.startPrank(user1); + vm.expectRevert( + abi.encodeWithSignature("HolderLimitExceeded(uint256)", 1) + ); + cyberScrip.transfer(user2, 1 ether); + vm.stopPrank(); + } + + function test_MaxHolderCountBlocksMint() public { + vm.prank(issuanceManager); + cyberScrip.setMaxHolderCount(1); + + vm.prank(issuanceManager); + vm.expectRevert( + abi.encodeWithSignature("HolderLimitExceeded(uint256)", 1) + ); + cyberScrip.mint(user2, 1 ether); + } + + function test_SetMaxHolderCount_EmitsEvent() public { + vm.expectEmit(false, false, false, true); + emit MaxHolderCountUpdated(2); + vm.prank(issuanceManager); + cyberScrip.setMaxHolderCount(2); + assertEq(cyberScrip.maxHolderCount(), 2); + } + + function test_FreezeStatusUpdatedEventAndGetter() public { + vm.expectEmit(true, false, false, true); + emit FreezeStatusUpdated(user1, true); + vm.prank(issuanceManager); + cyberScrip.setFrozen(user1, true); + assertTrue(cyberScrip.frozen(user1)); + } + + function test_DisableForceTransfer_EmitsEvent() public { + vm.expectEmit(false, false, false, true); + emit ComplianceFeatureDisabledEvent("forceTransfer"); + vm.prank(issuanceManager); + cyberScrip.disableForceTransfer(); + assertFalse(cyberScrip.canForceTransfer()); + } + + function test_DisableForceBurn_EmitsEvent() public { + vm.expectEmit(false, false, false, true); + emit ComplianceFeatureDisabledEvent("forceBurn"); + vm.prank(issuanceManager); + cyberScrip.disableForceBurn(); + assertFalse(cyberScrip.canForceBurn()); + } + + function test_DisableFreeze_EmitsEvent() public { + vm.expectEmit(false, false, false, true); + emit ComplianceFeatureDisabledEvent("freeze"); + vm.prank(issuanceManager); + cyberScrip.disableFreeze(); + assertFalse(cyberScrip.canFreeze()); + } + + function test_TransferRestrictionHooksLengthAndAccessor() public { + assertEq(cyberScrip.transferRestrictionHooksLength(), 1); + assertEq( + address(cyberScrip.transferRestrictionHooks(0)), + address(mockHook) + ); + } + + function test_CanTransfer_TrueForSelfOrZeroAmount() public { + assertTrue(cyberScrip.canTransfer(user1, user1, 1 ether)); + assertTrue(cyberScrip.canTransfer(user1, user2, 0)); + } + + function test_CanTransfer_TrueForMintOrBurn() public { + assertTrue(cyberScrip.canTransfer(address(0), user1, 1 ether)); + assertTrue(cyberScrip.canTransfer(user1, address(0), 1 ether)); + } + + function test_CanTransfer_FalseWhenFrozen() public { + vm.prank(issuanceManager); + cyberScrip.setFrozen(user1, true); + assertFalse(cyberScrip.canTransfer(user1, user2, 1 ether)); + } + + function test_CanTransfer_FalseWhenHookDenies() public { + mockHook.setAllowTransfers(false); + assertFalse(cyberScrip.canTransfer(user1, user2, 1 ether)); + } + + function test_CanTransfer_RespectsHolderLimit() public { + vm.prank(issuanceManager); + cyberScrip.setMaxHolderCount(1); + assertFalse(cyberScrip.canTransfer(user1, user2, 1 ether)); + } + + function test_CanTransfer_AllowsTransferToExistingHolderAtLimit() public { + vm.startPrank(user1); + cyberScrip.transfer(user2, 1 ether); + vm.stopPrank(); + + vm.prank(issuanceManager); + cyberScrip.setMaxHolderCount(2); + + assertTrue(cyberScrip.canTransfer(user1, user2, 1 ether)); + } + + function test_WillCreateNewHolder_Behavior() public { + assertTrue(cyberScrip.willCreateNewHolder(user2, 1 ether)); + assertFalse(cyberScrip.willCreateNewHolder(user1, 1 ether)); + assertFalse(cyberScrip.willCreateNewHolder(user2, 0)); + assertFalse(cyberScrip.willCreateNewHolder(address(0), 1 ether)); + } + + function test_CurrentHolderCount_MatchesStorage() public { + assertEq(cyberScrip.currentHolderCount(), cyberScrip.holderCount()); + } + + function test_RemainingSlots_Unlimited() public { + assertEq(cyberScrip.remainingSlots(), type(uint256).max); + } + + function test_RemainingSlots_WithLimit() public { + vm.prank(issuanceManager); + cyberScrip.setMaxHolderCount(2); + assertEq(cyberScrip.remainingSlots(), 1); + + vm.startPrank(user1); + cyberScrip.transfer(user2, 1 ether); + vm.stopPrank(); + assertEq(cyberScrip.remainingSlots(), 0); + } + function test_MintBypassesHooksAndFreeze() public { // Disable transfers in hook and freeze recipient mockHook.setAllowTransfers(false); vm.startPrank(issuanceManager); cyberScrip.setFrozen(user2, true); - vm.stopPrank(); // Mint to frozen recipient should still work (from == address(0)) cyberScrip.mint(user2, 123 ether); + vm.stopPrank(); assertEq(cyberScrip.balanceOf(user2), 123 ether); } @@ -404,4 +569,58 @@ contract CyberScripTest is Test { cyberScrip.setRestrictionHook(newHooks); vm.stopPrank(); } + + // ======================== + // Bridge Simulation Tests (IssuanceManager scripify/convert) + // ======================== + + function test_Bridge_FullScripifySimulation() public { + // In a real scripifyCert (full), the IssuanceManager would: + // 1. Receive the Cert, 2. Void it, 3. Mint scrip + uint256 amount = 5000 ether; + + vm.prank(issuanceManager); + cyberScrip.mint(user2, amount); + + assertEq(cyberScrip.balanceOf(user2), amount); + assertEq(cyberScrip.totalSupply(), 1000 ether + amount); // 1000 from setUp + } + + function test_Bridge_PartialScripifySimulation() public { + // In a real scripifyCert (partial), the IssuanceManager would: + // 1. Reduce units on Cert, 2. Mint scrip + uint256 amount = 250 ether; + + vm.prank(issuanceManager); + cyberScrip.mint(user1, amount); + + assertEq(cyberScrip.balanceOf(user1), 1000 ether + amount); + } + + function test_Bridge_ConvertScripToCertSimulation() public { + // In a real convertScripToCert, the IssuanceManager would: + // 1. Burn scrip, 2. Unvoid or Mint new Cert + uint256 amountToConvertBack = 400 ether; + + // User1 has 1000 ether from setUp + vm.prank(issuanceManager); + cyberScrip.burnFrom(user1, amountToConvertBack); + + assertEq(cyberScrip.balanceOf(user1), 600 ether); + assertEq(cyberScrip.totalSupply(), 1000 ether - amountToConvertBack); + } + + function test_RevertWhen_UnauthorizedBridgeMint() public { + // Ensure only the designated issuanceManager can mint (crucial for bridge security) + vm.prank(user1); + vm.expectRevert(abi.encodeWithSignature("NotIssuanceManager()")); + cyberScrip.mint(user1, 100 ether); + } + + function test_RevertWhen_UnauthorizedBridgeBurn() public { + // Ensure only the designated issuanceManager can burn (crucial for bridge security) + vm.prank(user1); + vm.expectRevert(abi.encodeWithSignature("NotIssuanceManager()")); + cyberScrip.burnFrom(user1, 100 ether); + } } diff --git a/test/CyberScripUpgradeExistingV4.t.sol b/test/CyberScripUpgradeExistingV4.t.sol new file mode 100644 index 00000000..8754331d --- /dev/null +++ b/test/CyberScripUpgradeExistingV4.t.sol @@ -0,0 +1,939 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; + +import {CyberCorpFactory} from "../src/CyberCorpFactory.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {DealManagerFactory} from "../src/DealManagerFactory.sol"; +import {DealManager} from "../src/DealManager.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import {IssuanceManager} from "../src/IssuanceManager.sol"; +import {CyberCorp} from "../src/CyberCorp.sol"; +import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; +import {CyberScrip} from "../src/CyberScrip.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {ICyberCertPrinter} from "../src/interfaces/ICyberCertPrinter.sol"; +import {ICyberScrip} from "../src/interfaces/ICyberScrip.sol"; +import {IssuerApprovalRecertificationCondition} from "../src/libs/conditions/IssuerApprovalRecertificationCondition.sol"; +import { + CertificateDetails, + Endorsement +} from "../src/storage/CyberCertPrinterStorage.sol"; +import {CompanyOfficer, SecurityClass, SecuritySeries} from "../src/CyberCorpConstants.sol"; +import {ITransferRestrictionHook} from "../src/interfaces/ITransferRestrictionHook.sol"; +import {ICondition} from "../src/interfaces/ICondition.sol"; +import {ERC1967ProxyLib} from "./libs/ERC1967ProxyLib.sol"; + +interface ILegacyStackUUPS { + function upgradeToAndCall( + address newImplementation, + bytes calldata data + ) external payable; +} + +contract LegacySelectorCondition is ICondition { + address public expectedContract; + bytes4 public expectedSelector; + bytes32 public expectedDataHash; + + constructor( + address _contract, + bytes4 _selector, + bytes memory data + ) { + expectedContract = _contract; + expectedSelector = _selector; + expectedDataHash = keccak256(data); + } + + function checkCondition( + address _contract, + bytes4 _functionSignature, + bytes memory data + ) external view returns (bool) { + return + _contract == expectedContract && + _functionSignature == expectedSelector && + keccak256(data) == expectedDataHash; + } +} + +/// @notice Fork-based CyberScrip upgrade tests against a legacy Base Sepolia v3 deployment. +/// @dev The corp stack is deployed at a pre-upgrade block using the old live factories, +/// then upgraded locally to the latest in-repo implementations before each test runs. +contract CyberScripUpgradeExistingV3Test is Test { + using ERC1967ProxyLib for address; + + struct UpgradeImpls { + address cyberCorp; + address issuanceManager; + address dealManager; + address roundManager; + address certPrinter; + address scrip; + } + + struct MultiHolderFixture { + IssuanceManager issuanceManager; + ICyberCertPrinter certPrinter; + address scrip; + uint256 certIdA; + uint256 certIdB; + uint256 certIdC; + address thirdHolder; + address newInvestor; + } + + uint256 internal constant BASE_SEPOLIA = 84532; + uint256 internal constant PRE_UPGRADE_BLOCK = 38956871; + + address internal constant METALEX_SAFE = + 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C; + address internal constant LEXCHEX_OWNER = + 0x341Da9fb8F9bD9a775f6bD641091b24Dd9aA459B; + address internal constant CYBERCORP_FACTORY_PROXY = + 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2; + + CyberCorpFactory internal corpFactory; + CyberCorpSingleFactory internal corpSingleFactory; + IssuanceManagerFactory internal imFactory; + DealManagerFactory internal dmFactory; + RoundManagerFactory internal rmFactory; + + uint256 internal companyOwnerPk; + uint256 internal investorPk; + uint256 internal otherInvestorPk; + address internal companyOwner; + address internal investor; + address internal otherInvestor; + + function setUp() public { + assertEq(block.chainid, BASE_SEPOLIA, "Fork test: Base Sepolia only"); + vm.rollFork(PRE_UPGRADE_BLOCK); + + corpFactory = CyberCorpFactory(CYBERCORP_FACTORY_PROXY); + corpSingleFactory = CyberCorpSingleFactory( + corpFactory.cyberCorpSingleFactory() + ); + imFactory = IssuanceManagerFactory(corpFactory.issuanceManagerFactory()); + dmFactory = DealManagerFactory(corpFactory.dealManagerFactory()); + rmFactory = RoundManagerFactory(corpFactory.roundManagerFactory()); + + companyOwnerPk = uint256(keccak256("cyberscrip-upgrade-company-owner")); + investorPk = uint256(keccak256("cyberscrip-upgrade-investor")); + otherInvestorPk = uint256(keccak256("cyberscrip-upgrade-other-investor")); + companyOwner = vm.addr(companyOwnerPk); + investor = vm.addr(investorPk); + otherInvestor = vm.addr(otherInvestorPk); + } + + function test_PostUpgrade_ConversionLifecycleAndRuntimeUpdates() public { + IssuanceManager issuanceManager = _setupLegacyUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Lifecycle Cert", + "LCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 75 + ); + + uint256[] memory whitelistIds = new uint256[](1); + whitelistIds[0] = certId; + address scrip = _deployWhitelistedScrip( + issuanceManager, + certPrinter, + 50, + 3, + 2, + whitelistIds + ); + + (uint256 initialNum, uint256 initialDen) = issuanceManager.getScripRatio( + address(certPrinter) + ); + assertEq(initialNum, 3); + assertEq(initialDen, 2); + assertEq(issuanceManager.getScripToCertMinimum(address(certPrinter)), 50); + assertTrue(issuanceManager.getScripifyWhitelistEnabled(address(certPrinter))); + assertTrue(issuanceManager.isScripifyWhitelisted(address(certPrinter), certId)); + + vm.prank(companyOwner); + issuanceManager.setScripRatio(address(certPrinter), 4, 1); + vm.prank(companyOwner); + issuanceManager.setScripToCertMinimum(address(certPrinter), 40); + vm.prank(companyOwner); + issuanceManager.setScripifyWhitelistEnabled(address(certPrinter), false); + + uint256[] memory removeIds = new uint256[](1); + removeIds[0] = certId; + vm.prank(companyOwner); + issuanceManager.removeScripifyWhitelistIds(address(certPrinter), removeIds); + + uint256[] memory addIds = new uint256[](2); + addIds[0] = certId; + addIds[1] = certId + 1; + vm.prank(companyOwner); + issuanceManager.addScripifyWhitelistIds(address(certPrinter), addIds); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 40); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripToCertMinimumNotMet.selector); + issuanceManager.convertScripToCert(address(certPrinter), 39); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 40); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function test_PostUpgrade_ScripifyUsesLegalOwner() public { + IssuanceManager issuanceManager = _setupLegacyUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Legal Owner Cert", + "LOCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 25 + ); + + vm.prank(companyOwner); + issuanceManager.setGlobalTransferable(address(certPrinter), true); + + address scrip = _deployDefaultScrip( + issuanceManager, + certPrinter, + 0, + 1, + 1 + ); + + vm.prank(investor); + certPrinter.safeTransferFrom(investor, otherInvestor, certId); + + assertEq(certPrinter.ownerOf(certId), otherInvestor); + assertEq(certPrinter.legalOwnerOf(certId), investor); + + vm.prank(otherInvestor); + vm.expectRevert(bytes4(keccak256("NotLegalOwner()"))); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 10); + assertEq(certPrinter.getActiveCertificateDetails(certId).unitsRepresented, 15e18); + assertEq(certPrinter.getCertificateDetails(certId).unitsRepresented, 25e18); + } + + function test_PostUpgrade_ConversionGatesAndConditions() public { + IssuanceManager issuanceManager = _setupLegacyUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Guard Cert", + "GCERT" + ); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripifiedCertNotAllowed.selector); + issuanceManager.convertScripToCert(address(certPrinter), 1); + + ICondition[] memory scripToCert = new ICondition[](1); + scripToCert[0] = ICondition( + new LegacySelectorCondition( + address(certPrinter), + IssuanceManager.convertScripToCert.selector, + abi.encode(uint256(150), investor) + ) + ); + + address scrip = _deployScripWithScripToCertConditions( + issuanceManager, + certPrinter, + scripToCert, + 90, + 3, + 2 + ); + + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 100 + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 100, address(0)); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 150); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripToCertMinimumNotMet.selector); + issuanceManager.convertScripToCert(address(certPrinter), 80); + + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ConditionCheckFailed.selector); + issuanceManager.convertScripToCert(address(certPrinter), 120); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 150); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function test_PostUpgrade_ReformsVoidedPath() public { + IssuanceManager issuanceManager = _setupLegacyUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Voided Cert", + "VCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 500 + ); + + address scrip = _deployDefaultScrip( + issuanceManager, + certPrinter, + 0, + 2, + 1 + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 20); + + vm.prank(companyOwner); + issuanceManager.voidCertificate(address(certPrinter), certId); + assertTrue(certPrinter.isVoided(certId)); + + _approveRecertification( + issuanceManager, + address(certPrinter), + investor, + "Investor" + ); + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 20); + + assertEq(certPrinter.totalSupply(), 1); + assertEq(certPrinter.ownerOf(certId), investor); + assertFalse(certPrinter.isVoided(certId)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function test_PostUpgrade_ForceBurnReducesPoolTotals() public { + IssuanceManager issuanceManager = _setupLegacyUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Force Burn Cert", + "FBCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 100 + ); + + address scrip = _deployDefaultScrip( + issuanceManager, + certPrinter, + 0, + 2, + 1 + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + + (uint256 totalTrackedBefore,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + (bool isScripifiedBefore, uint256 scripifiedUnitsBefore,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certId); + assertEq(ICyberScrip(scrip).balanceOf(investor), 20); + assertEq(totalTrackedBefore, 20); + assertTrue(isScripifiedBefore); + assertEq(scripifiedUnitsBefore, 10e18); + + vm.prank(companyOwner); + issuanceManager.forceScripBurn(address(certPrinter), investor, 8); + + (uint256 totalTrackedAfter,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + (bool isScripifiedAfter, uint256 scripifiedUnitsAfter,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certId); + assertEq(ICyberScrip(scrip).balanceOf(investor), 12); + assertEq(totalTrackedAfter, 12); + assertTrue(isScripifiedAfter); + assertEq(scripifiedUnitsAfter, 6e18); + } + + function test_PostUpgrade_MultiHolderTransferAndRecertificationPoolAccounting() + public + { + MultiHolderFixture memory fixture = _setupMultiHolderFixture(); + + _assertActiveUnitsZero(fixture); + _assertStoredUnits(fixture, 10e18, 20e18, 50e18); + _assertBalances(fixture, 10, 20, 50, 0); + _assertPoolAmountsById(fixture, 10, 20, 50); + + _transferToNewInvestor(fixture); + + _assertBalances(fixture, 8, 16, 40, 16); + _assertPoolAmountsById(fixture, 10, 20, 50); + + (uint256 totalTrackedBefore,) = fixture.issuanceManager.getScripPoolTotals( + address(fixture.certPrinter) + ); + assertEq(totalTrackedBefore, 80); + + _approveRecertification( + fixture.issuanceManager, + address(fixture.certPrinter), + fixture.newInvestor, + "New Investor" + ); + vm.prank(fixture.newInvestor); + fixture.issuanceManager.convertScripToCert(address(fixture.certPrinter), 16); + + _assertBalances(fixture, 8, 16, 40, 0); + + (uint256 totalTrackedAfter,) = fixture.issuanceManager.getScripPoolTotals( + address(fixture.certPrinter) + ); + assertEq(totalTrackedAfter, 64); + _assertPoolAmountsById(fixture, 8, 16, 40); + _assertStoredUnits(fixture, 8e18, 16e18, 40e18); + + uint256 newCertId = 3; + assertEq(fixture.certPrinter.totalSupply(), 4); + assertEq(fixture.certPrinter.ownerOf(newCertId), fixture.newInvestor); + assertEq( + fixture.issuanceManager.getScripPoolAmountById( + address(fixture.certPrinter), + newCertId + ), + 0 + ); + assertEq( + fixture.certPrinter.getCertificateDetails(newCertId).unitsRepresented, + 16e18 + ); + assertEq( + fixture.certPrinter.getActiveCertificateDetails(newCertId).unitsRepresented, + 16e18 + ); + } + + function test_PostUpgrade_RequiresIssuerApprovalCondition() public { + IssuanceManager issuanceManager = _setupLegacyUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Approval Cert", + "APPR" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 10 + ); + + IssuerApprovalRecertificationCondition condition = new IssuerApprovalRecertificationCondition(); + ICondition[] memory scripToCert = new ICondition[](1); + scripToCert[0] = ICondition(address(condition)); + + address scrip = _deployScripWithScripToCertConditions( + issuanceManager, + certPrinter, + scripToCert, + 0, + 1, + 1 + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 5, address(0)); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ConditionCheckFailed.selector); + issuanceManager.convertScripToCert(address(certPrinter), 5); + + vm.prank(otherInvestor); + vm.expectRevert(); + condition.setInvestorApproval(address(certPrinter), investor, true); + + BorgAuth auth = BorgAuth(issuanceManager.AUTH()); + uint256 adminRole = auth.ADMIN_ROLE(); + vm.prank(companyOwner); + auth.updateRole(address(this), adminRole); + + condition.setInvestorApproval(address(scrip), investor, true); + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 5); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function _setupLegacyUpgradedIssuanceManager() + internal + returns (IssuanceManager issuanceManager) + { + bytes32 corpSalt = keccak256("cyberscrip-upgrade-existing-v3-corp"); + CompanyOfficer memory officer = CompanyOfficer({ + eoa: companyOwner, + name: "Officer One", + contact: "officer@test.local", + title: "CEO" + }); + + vm.prank(LEXCHEX_OWNER); + (address corp, address auth, address issuanceManagerAddr, , ) = corpFactory + .deployCyberCorp( + corpSalt, + "CyberCorp Upgrade Test", + "Limited Liability Company", + "Delaware", + "contact@test.local", + "Arbitration", + companyOwner, + officer + ); + + + uint256 ownerRole = BorgAuth(auth).OWNER_ROLE(); + vm.prank(address(corpFactory)); + BorgAuth(auth).updateRole(companyOwner, ownerRole); + + issuanceManager = IssuanceManager(issuanceManagerAddr); + _upgradeCoreStackForCorp(corp, issuanceManager); + } + + function _upgradeCoreStackForCorp( + address corp, + IssuanceManager issuanceManager + ) internal { + UpgradeImpls memory impls = _deployUpgradeImplementations(); + _setFactoryUpgradeReferences(impls); + _upgradeExistingCorpStack(corp, issuanceManager, impls); + _assertUpgradedImplementations(corp, issuanceManager, impls); + } + + function _deployUpgradeImplementations() + internal + returns (UpgradeImpls memory impls) + { + impls.cyberCorp = address(new CyberCorp()); + impls.issuanceManager = address(new IssuanceManager()); + impls.dealManager = address(new DealManager()); + impls.roundManager = address(new RoundManager()); + impls.certPrinter = address(new CyberCertPrinter()); + impls.scrip = address(new CyberScrip()); + } + + function _setFactoryUpgradeReferences(UpgradeImpls memory impls) internal { + vm.startPrank(LEXCHEX_OWNER); + corpSingleFactory.setRefImplementation(impls.cyberCorp); + imFactory.setRefImplementation(impls.issuanceManager); + dmFactory.setRefImplementation(impls.dealManager); + rmFactory.setRefImplementation(impls.roundManager); + imFactory.setCyberCertPrinterRefImplementation(impls.certPrinter); + imFactory.setCyberScripRefImplementation(impls.scrip); + vm.stopPrank(); + } + + function _upgradeExistingCorpStack( + address corp, + IssuanceManager issuanceManager, + UpgradeImpls memory impls + ) internal { + address issuanceManagerAddr = address(issuanceManager); + address dealManagerAddr = CyberCorp(corp).dealManager(); + address roundManagerAddr = CyberCorp(corp).roundManager(); + + vm.prank(companyOwner); + ILegacyStackUUPS(corp).upgradeToAndCall(impls.cyberCorp, ""); + vm.prank(companyOwner); + ILegacyStackUUPS(issuanceManagerAddr).upgradeToAndCall( + impls.issuanceManager, + "" + ); + vm.prank(companyOwner); + ILegacyStackUUPS(dealManagerAddr).upgradeToAndCall( + impls.dealManager, + "" + ); + vm.prank(companyOwner); + ILegacyStackUUPS(roundManagerAddr).upgradeToAndCall( + impls.roundManager, + "" + ); + vm.prank(companyOwner); + issuanceManager.upgradeCertPrinterBeaconImplementation(impls.certPrinter); + vm.prank(companyOwner); + issuanceManager.upgradeScripBeaconImplementation(impls.scrip); + } + + function _assertUpgradedImplementations( + address corp, + IssuanceManager issuanceManager, + UpgradeImpls memory impls + ) internal view { + address issuanceManagerAddr = address(issuanceManager); + address dealManagerAddr = CyberCorp(corp).dealManager(); + address roundManagerAddr = CyberCorp(corp).roundManager(); + + assertEq(corp.getErc1967Implementation(), impls.cyberCorp); + assertEq( + issuanceManagerAddr.getErc1967Implementation(), + impls.issuanceManager + ); + assertEq(dealManagerAddr.getErc1967Implementation(), impls.dealManager); + assertEq(roundManagerAddr.getErc1967Implementation(), impls.roundManager); + assertEq( + issuanceManager.getCertPrinterBeaconImplementation(), + impls.certPrinter + ); + assertEq(issuanceManager.getScripBeaconImplementation(), impls.scrip); + } + + function _deployPrinterAfterUpgrade( + IssuanceManager issuanceManager, + string memory name, + string memory symbol + ) internal returns (ICyberCertPrinter certPrinter) { + vm.prank(companyOwner); + certPrinter = ICyberCertPrinter( + issuanceManager.createCertPrinter( + new string[](0), + name, + symbol, + "uri://cert", + SecurityClass.CommonStock, + SecuritySeries.SeriesA, + address(0) + ) + ); + } + + function _deployDefaultScrip( + IssuanceManager issuanceManager, + ICyberCertPrinter certPrinter, + uint256 minimum, + uint256 numerator, + uint256 denominator + ) internal returns (address scrip) { + vm.prank(companyOwner); + scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + minimum, + numerator, + denominator, + new uint256[](0), + false, + true, + true, + true + ); + } + + function _deployWhitelistedScrip( + IssuanceManager issuanceManager, + ICyberCertPrinter certPrinter, + uint256 minimum, + uint256 numerator, + uint256 denominator, + uint256[] memory whitelistIds + ) internal returns (address scrip) { + vm.prank(companyOwner); + scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + minimum, + numerator, + denominator, + whitelistIds, + true, + true, + true, + true + ); + } + + function _deployScripWithScripToCertConditions( + IssuanceManager issuanceManager, + ICyberCertPrinter certPrinter, + ICondition[] memory scripToCertConditions, + uint256 minimum, + uint256 numerator, + uint256 denominator + ) internal returns (address scrip) { + vm.prank(companyOwner); + scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + scripToCertConditions, + minimum, + numerator, + denominator, + new uint256[](0), + false, + true, + true, + true + ); + } + + function _mintCertAfterUpgrade( + IssuanceManager issuanceManager, + ICyberCertPrinter certPrinter, + address to, + uint256 units + ) internal returns (uint256 tokenId) { + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: units * 1e18, + legalDetails: "", + extensionData: "" + }); + vm.prank(companyOwner); + tokenId = issuanceManager.createCert(address(certPrinter), to, details); + + Endorsement memory selfEndorsement = Endorsement({ + endorser: to, + timestamp: block.timestamp, + signatureHash: "", + registry: address(0), + agreementId: bytes32(0), + endorsee: to, + endorseeName: "" + }); + vm.prank(to); + certPrinter.addEndorsement(tokenId, selfEndorsement); + vm.prank(companyOwner); + issuanceManager.setTokenTransferable(address(certPrinter), tokenId, true); + vm.prank(to); + certPrinter.safeTransferFrom(to, to, tokenId); + vm.prank(companyOwner); + issuanceManager.setTokenTransferable(address(certPrinter), tokenId, false); + } + + function _approveRecertification( + IssuanceManager issuanceManager, + address certAddress, + address investorAddr, + string memory investorName + ) internal { + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 0, + legalDetails: "", + extensionData: "" + }); + + vm.prank(companyOwner); + issuanceManager.setRecertificationApproval( + certAddress, + investorAddr, + investorName, + details + ); + } + + function _setupMultiHolderFixture() + internal + returns (MultiHolderFixture memory fixture) + { + fixture.issuanceManager = _setupLegacyUpgradedIssuanceManager(); + fixture.certPrinter = _deployPrinterAfterUpgrade( + fixture.issuanceManager, + "Pool Cert", + "PCERT" + ); + fixture.thirdHolder = vm.addr( + uint256(keccak256("cyberscrip-upgrade-third-holder")) + ); + fixture.newInvestor = vm.addr( + uint256(keccak256("cyberscrip-upgrade-new-investor")) + ); + + fixture.certIdA = _mintCertAfterUpgrade( + fixture.issuanceManager, + fixture.certPrinter, + investor, + 10 + ); + fixture.certIdB = _mintCertAfterUpgrade( + fixture.issuanceManager, + fixture.certPrinter, + otherInvestor, + 20 + ); + fixture.certIdC = _mintCertAfterUpgrade( + fixture.issuanceManager, + fixture.certPrinter, + fixture.thirdHolder, + 50 + ); + + fixture.scrip = _deployDefaultScrip( + fixture.issuanceManager, + fixture.certPrinter, + 0, + 1, + 1 + ); + + vm.prank(investor); + fixture.issuanceManager.scripifyCert( + address(fixture.certPrinter), + fixture.certIdA, + 10, + address(0) + ); + vm.prank(otherInvestor); + fixture.issuanceManager.scripifyCert( + address(fixture.certPrinter), + fixture.certIdB, + 20, + address(0) + ); + vm.prank(fixture.thirdHolder); + fixture.issuanceManager.scripifyCert( + address(fixture.certPrinter), + fixture.certIdC, + 50, + address(0) + ); + } + + function _transferToNewInvestor(MultiHolderFixture memory fixture) internal { + vm.prank(investor); + ICyberScrip(fixture.scrip).transfer(fixture.newInvestor, 2); + vm.prank(otherInvestor); + ICyberScrip(fixture.scrip).transfer(fixture.newInvestor, 4); + vm.prank(fixture.thirdHolder); + ICyberScrip(fixture.scrip).transfer(fixture.newInvestor, 10); + } + + function _assertActiveUnitsZero(MultiHolderFixture memory fixture) internal view { + assertEq( + fixture.certPrinter.getActiveCertificateDetails(fixture.certIdA) + .unitsRepresented, + 0 + ); + assertEq( + fixture.certPrinter.getActiveCertificateDetails(fixture.certIdB) + .unitsRepresented, + 0 + ); + assertEq( + fixture.certPrinter.getActiveCertificateDetails(fixture.certIdC) + .unitsRepresented, + 0 + ); + } + + function _assertStoredUnits( + MultiHolderFixture memory fixture, + uint256 unitsA, + uint256 unitsB, + uint256 unitsC + ) internal view { + assertEq( + fixture.certPrinter.getCertificateDetails(fixture.certIdA).unitsRepresented, + unitsA + ); + assertEq( + fixture.certPrinter.getCertificateDetails(fixture.certIdB).unitsRepresented, + unitsB + ); + assertEq( + fixture.certPrinter.getCertificateDetails(fixture.certIdC).unitsRepresented, + unitsC + ); + } + + function _assertBalances( + MultiHolderFixture memory fixture, + uint256 investorBal, + uint256 otherInvestorBal, + uint256 thirdHolderBal, + uint256 newInvestorBal + ) internal view { + assertEq(ICyberScrip(fixture.scrip).balanceOf(investor), investorBal); + assertEq( + ICyberScrip(fixture.scrip).balanceOf(otherInvestor), + otherInvestorBal + ); + assertEq( + ICyberScrip(fixture.scrip).balanceOf(fixture.thirdHolder), + thirdHolderBal + ); + assertEq( + ICyberScrip(fixture.scrip).balanceOf(fixture.newInvestor), + newInvestorBal + ); + } + + function _assertPoolAmountsById( + MultiHolderFixture memory fixture, + uint256 certAmtA, + uint256 certAmtB, + uint256 certAmtC + ) internal view { + assertEq( + fixture.issuanceManager.getScripPoolAmountById( + address(fixture.certPrinter), + fixture.certIdA + ), + certAmtA + ); + assertEq( + fixture.issuanceManager.getScripPoolAmountById( + address(fixture.certPrinter), + fixture.certIdB + ), + certAmtB + ); + assertEq( + fixture.issuanceManager.getScripPoolAmountById( + address(fixture.certPrinter), + fixture.certIdC + ), + certAmtC + ); + } +} diff --git a/test/CyberScripUpgradeTest.t.sol b/test/CyberScripUpgradeTest.t.sol new file mode 100644 index 00000000..ba9cf40b --- /dev/null +++ b/test/CyberScripUpgradeTest.t.sol @@ -0,0 +1,1552 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {IERC721} from "openzeppelin-contracts/token/ERC721/IERC721.sol"; + +import {CyberAgreementUtils} from "./libs/CyberAgreementUtils.sol"; +import {DeploymentConstants} from "../script/libs/DeploymentConstants.sol"; + +import {CyberCorpFactory} from "../src/CyberCorpFactory.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {DealManagerFactory} from "../src/DealManagerFactory.sol"; +import {DealManager} from "../src/DealManager.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import {IssuanceManager} from "../src/IssuanceManager.sol"; +import {CyberCorp} from "../src/CyberCorp.sol"; +import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; +import {CyberScrip} from "../src/CyberScrip.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {Round, RoundLib} from "../src/libs/RoundLib.sol"; +import {ERC1967ProxyLib} from "./libs/ERC1967ProxyLib.sol"; +import {ICyberCertPrinter} from "../src/interfaces/ICyberCertPrinter.sol"; +import {ICyberScrip} from "../src/interfaces/ICyberScrip.sol"; +import {IssuerApprovalRecertificationCondition} from "../src/libs/conditions/IssuerApprovalRecertificationCondition.sol"; +import { + CertificateDetails, + Endorsement +} from "../src/storage/CyberCertPrinterStorage.sol"; + +import {CompanyOfficer, SecurityClass, SecuritySeries} from "../src/CyberCorpConstants.sol"; +import {ITransferRestrictionHook} from "../src/interfaces/ITransferRestrictionHook.sol"; +import {ICondition} from "../src/interfaces/ICondition.sol"; +import {CyberCertData, RoundType} from "../src/interfaces/IRoundManager.sol"; +import {EOI, LexChexDetails, MintRequest} from "../src/storage/RoundManagerStorage.sol"; + +interface IUUPS { + function upgradeToAndCall( + address newImplementation, + bytes calldata data + ) external payable; +} + +contract SelectorCondition is ICondition { + address public expectedContract; + bytes4 public expectedSelector; + bytes32 public expectedDataHash; + + constructor( + address _contract, + bytes4 _selector, + bytes memory data + ) { + expectedContract = _contract; + expectedSelector = _selector; + expectedDataHash = keccak256(data); + } + + function checkCondition( + address _contract, + bytes4 _functionSignature, + bytes memory data + ) external view returns (bool) { + return + _contract == expectedContract && + _functionSignature == expectedSelector && + keccak256(data) == expectedDataHash; + } +} + +contract CyberScripUpgradeTest is Test { + using ERC1967ProxyLib for address; + using RoundLib for Round; + + string internal constant RPC_ENV_VAR = "FORK_RPC_URL"; + address internal constant METALEX_SAFE = + 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C; + address internal constant LEXCHEX_OWNER = + 0x341Da9fb8F9bD9a775f6bD641091b24Dd9aA459B; + + bytes32 internal constant EIP712_DOMAIN_TYPEHASH = + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 internal constant ESCROWEDSIGNATUREDATA_TYPEHASH = + keccak256( + "EscrowedSignatureData(bytes32 roundId,uint8 seriesType,uint256 raiseCap,uint256 minTicket,uint256 maxTicket,uint8 roundType,uint256 startTime,uint256 endTime,bytes32 templateId,address paymentToken,uint256 pricePerUnit,uint256 valuation,address companyAddress)" + ); + + DeploymentConstants.CoreDeployment internal deployment; + + uint256 internal companyOwnerPk; + uint256 internal investorPk; + uint256 internal otherInvestorPk; + address internal companyOwner; + address internal investor; + address internal otherInvestor; + + function setUp() public { + + deployment = DeploymentConstants.coreV2(block.chainid); + + // deterministic test users + companyOwnerPk = uint256(keccak256("cyberscrip-upgrade-company-owner")); + investorPk = uint256(keccak256("cyberscrip-upgrade-investor")); + otherInvestorPk = uint256(keccak256("cyberscrip-upgrade-other-investor")); + companyOwner = vm.addr(companyOwnerPk); + investor = vm.addr(investorPk); + otherInvestor = vm.addr(otherInvestorPk); + } + + function test_UpgradeCyberScrip_And_InvestorRoundFlow() public { + CyberCorpFactory corpFactory = CyberCorpFactory( + deployment.cyberCorpFactory + ); + CyberAgreementRegistry registry = CyberAgreementRegistry( + deployment.cyberAgreementRegistry + ); + RoundManagerFactory rmFactory = RoundManagerFactory( + deployment.roundManagerFactory + ); + DealManagerFactory dmFactory = DealManagerFactory( + deployment.dealManagerFactory + ); + CyberCorpSingleFactory corpSingleFactory = CyberCorpSingleFactory( + deployment.cyberCorpSingleFactory + ); + IssuanceManagerFactory imFactory = IssuanceManagerFactory( + deployment.issuanceManagerFactory + ); + + address stable = corpFactory.stable(); + assertTrue(stable != address(0), "stable token not configured"); + + bytes32 templateId = bytes32( + uint256(keccak256("cyberscrip-upgrade-test-template")) + ); + + vm.prank(METALEX_SAFE); + registry.createTemplate( + templateId, + "CyberScrip upgrade test template", + "ipfs://cyberscrip-upgrade-template", + _strings("purchaseAmount", "valuation"), + _strings("name", "jurisdiction") + ); + + address issuerA = companyOwner; + uint256 issuerAPk = companyOwnerPk; + + CompanyOfficer memory officer = CompanyOfficer({ + eoa: issuerA, + name: "Officer One", + contact: "officer@test.local", + title: "CEO" + }); + + uint256 userSalt = uint256(keccak256("cyberscrip-upgrade-corp-salt")); + uint256 raiseCap = 100_000e6; + uint256 minTicket = 100e6; + uint256 maxTicket = 2_000e6; + uint256 startTime = block.timestamp - 1; + uint256 endTime = block.timestamp + 7 days; + uint256 pricePerUnit = 1e18; // USD (18 decimals) + uint256 valuation = 5_000_000e18; + + // 1) Pre-upgrade: issuerA deploys corp stack and creates a deal via factory. + CyberCorpFactory.CyberCertData[] memory offerCertData = new CyberCorpFactory.CyberCertData[](1); + offerCertData[0] = CyberCorpFactory.CyberCertData({ + name: "SAFE", + symbol: "SAFE", + uri: "ipfs://safe-cert", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesA, + extension: address(0), + defaultLegend: new string[](0) + }); + + string[] memory offerGlobalValues = _strings("100", "5000000"); + address[] memory offerParties = new address[](2); + offerParties[0] = issuerA; + offerParties[1] = investor; + string[][] memory offerPartyValues = new string[][](2); + offerPartyValues[0] = _strings("Officer One", "US"); + offerPartyValues[1] = _strings("Investor A", "US"); + bytes memory offerSignature = _computeAgreementSignature( + registry, + templateId, + userSalt, + offerGlobalValues, + offerPartyValues[0], + offerParties, + issuerAPk + ); + + CertificateDetails[] memory offerDetails = new CertificateDetails[](1); + offerDetails[0] = CertificateDetails({ + signingOfficerName: "Officer One", + signingOfficerTitle: "CEO", + investmentAmountUSD: minTicket * 1e12, + issuerUSDValuationAtTimeOfInvestment: valuation, + unitsRepresented: minTicket * 1e12, + legalDetails: "pre-upgrade-offer", + extensionData: "" + }); + + address corp; + address auth; + address issuanceManagerAddr; + address roundManagerAddr; + uint256[] memory preUpgradeCertIds; + vm.prank(issuerA); + ( + corp, + auth, + issuanceManagerAddr, + , + roundManagerAddr, + , + , + preUpgradeCertIds + ) = corpFactory.deployCyberCorpAndCreateOffer( + userSalt, + "Issuer A Corp", + "Limited Liability Company", + "Delaware", + "issuera@test.local", + "Arbitration", + issuerA, + officer, + offerCertData, + templateId, + offerGlobalValues, + offerParties, + minTicket, + offerPartyValues, + offerSignature, + offerDetails, + new address[](0), + bytes32(0), + block.timestamp + 7 days + ); + assertEq(preUpgradeCertIds.length, 1, "expected one pre-upgrade cert"); + assertTrue(corp != address(0), "corp should be deployed"); + + CyberCertData[] memory certData = new CyberCertData[](1); + certData[0] = CyberCertData({ + name: "SAFE", + symbol: "SAFE", + uri: "ipfs://safe-cert", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesA, + extension: address(0), + defaultLegend: new string[](0) + }); + + string[] memory legalDetails = new string[](1); + legalDetails[0] = "legal-details"; + bytes[] memory extensionData = new bytes[](1); + extensionData[0] = ""; + string[] memory roundPartyValues = _strings("Officer One", "US"); + (bytes memory escrowedSignature, ) = _computeEscrowSignature( + roundManagerAddr, + SecuritySeries.SeriesA, + raiseCap, + minTicket, + maxTicket, + RoundType.FCFS, + startTime, + endTime, + templateId, + stable, + pricePerUnit, + valuation, + issuerAPk, + corp + ); + + // Newly deployed corp AUTH owner is the factory by default. Grant owner to issuerA for owner-gated upgrades. + vm.prank(deployment.cyberCorpFactory); + BorgAuth(auth).updateRole(issuerA, 99); + + IssuanceManager issuanceManager = IssuanceManager(issuanceManagerAddr); + _upgradeCoreStackForCorp( + corp, + issuanceManager, + corpSingleFactory, + imFactory, + dmFactory, + rmFactory + ); + bytes32 roundId; + roundId = _recreateRoundAfterUpgrade( + roundManagerAddr, + SecuritySeries.SeriesA, + RoundType.FCFS, + templateId, + stable, + pricePerUnit, + valuation, + raiseCap, + minTicket, + maxTicket, + startTime, + endTime, + officer, + legalDetails, + extensionData, + roundPartyValues, + escrowedSignature, + certData + ); + + // Investor flow: submit EOI and auto-allocate (FCFS). + deal(stable, investor, maxTicket); + vm.prank(investor); + IERC20(stable).approve(roundManagerAddr, maxTicket); + + EOI memory eoi = EOI({ + name: "Investor A", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.local", + minAmount: minTicket, + maxAmount: minTicket, + expiry: block.timestamp + 2 days, + naturalPerson: true, + lexchexDetails: _emptyLex() + }); + + string[] memory globalValues = _strings("100", "5000000"); + string[] memory investorPartyValues = _strings("Investor A", "US"); + uint256 eoiSalt = uint256( + keccak256(abi.encodePacked("cyberscrip-upgrade-eoi-salt", userSalt)) + ); + bytes memory investorSignature = _computeEOISignature( + registry, + templateId, + eoiSalt, + globalValues, + investorPartyValues, + issuerA, + investorPk + ); + + vm.prank(investor); + RoundManager(roundManagerAddr).submitEOI( + roundId, + eoi, + globalValues, + investorPartyValues, + investorSignature, + eoiSalt, + new address[](0), + bytes32(0) + ); + + assertGt( + RoundManager(roundManagerAddr).getRound(roundId).raised, + 0, + "round should have raised capital" + ); + + // After allocation, issuer enables scrip for this certificate class. + address certPrinter = RoundManager(roundManagerAddr).getRound(roundId) + .certPrinter[0]; + assertEq(IERC721(certPrinter).ownerOf(0), investor, "investor should own cert 0"); + + ITransferRestrictionHook[] memory noHooks = new ITransferRestrictionHook[]( + 0 + ); + ICondition[] memory noConditions = new ICondition[](0); + uint256[] memory noWhitelist = new uint256[](0); + + vm.prank(issuerA); + address scrip = issuanceManager.deployCyberScrip( + certPrinter, + noHooks, + noConditions, + noConditions, + 0, // no minimum to re-certify + 1, // ratio numerator + 1, // ratio denominator + noWhitelist, + false, + false, + false, + false + ); + + uint256 fullCertUnits = ICyberCertPrinter(certPrinter) + .getCertificateDetails(0) + .unitsRepresented; + assertGt(fullCertUnits, 0, "certificate should have units"); + assertEq(IERC20(scrip).balanceOf(investor), 0, "initial scrip balance"); + + // Investor scripifies the full certificate amount. + vm.prank(investor); + issuanceManager.scripifyCert(certPrinter, 0, fullCertUnits, address(0)); + assertEq( + IERC20(scrip).balanceOf(investor), + fullCertUnits, + "scrip balance after full scripify" + ); + assertEq( + IERC721(certPrinter).balanceOf(investor), + 1, + "full scripify should not consume investor cert" + ); + + // Investor converts full scrip amount back to a certificate. + vm.prank(investor); + issuanceManager.convertScripToCert(certPrinter, fullCertUnits); + + assertEq( + IERC721(certPrinter).balanceOf(investor), + 1, + "investor should hold recertified cert" + ); + uint256 recertifiedTokenId = ICyberCertPrinter(certPrinter).tokenOfOwnerByIndex( + investor, + 0 + ); + string memory certUri = _getCertificateTokenURI( + certPrinter, + recertifiedTokenId + ); + assertGt(bytes(certUri).length, 0, "tokenURI should not be empty"); + + assertTrue(corp != address(0), "corp should be deployed"); + } + + function test_PostUpgrade_ConversionLifecycleAndRuntimeUpdates() public { + IssuanceManager issuanceManager = _setupUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Lifecycle Cert", + "LCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 75 + ); + + uint256[] memory whitelistIds = new uint256[](1); + whitelistIds[0] = certId; + vm.prank(companyOwner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 50, + 3, + 2, + whitelistIds, + true, + true, + true, + true + ); + + (uint256 initialNum, uint256 initialDen) = issuanceManager.getScripRatio( + address(certPrinter) + ); + assertEq(initialNum, 3); + assertEq(initialDen, 2); + assertEq(issuanceManager.getScripToCertMinimum(address(certPrinter)), 50); + assertTrue(issuanceManager.getScripifyWhitelistEnabled(address(certPrinter))); + assertTrue(issuanceManager.isScripifyWhitelisted(address(certPrinter), certId)); + + vm.prank(companyOwner); + issuanceManager.setScripRatio(address(certPrinter), 4, 1); + vm.prank(companyOwner); + issuanceManager.setScripToCertMinimum(address(certPrinter), 40); + vm.prank(companyOwner); + issuanceManager.setScripifyWhitelistEnabled(address(certPrinter), false); + + uint256[] memory removeIds = new uint256[](1); + removeIds[0] = certId; + vm.prank(companyOwner); + issuanceManager.removeScripifyWhitelistIds(address(certPrinter), removeIds); + + uint256[] memory addIds = new uint256[](2); + addIds[0] = certId; + addIds[1] = certId + 1; + vm.prank(companyOwner); + issuanceManager.addScripifyWhitelistIds(address(certPrinter), addIds); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 40); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripToCertMinimumNotMet.selector); + issuanceManager.convertScripToCert(address(certPrinter), 39); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 40); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function test_PostUpgrade_ScripifyUsesLegalOwner() public { + IssuanceManager issuanceManager = _setupUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Legal Owner Cert", + "LOCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 25 + ); + + vm.prank(companyOwner); + issuanceManager.setGlobalTransferable(address(certPrinter), true); + + vm.prank(companyOwner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(investor); + certPrinter.safeTransferFrom(investor, otherInvestor, certId); + + assertEq(certPrinter.ownerOf(certId), otherInvestor); + assertEq(certPrinter.legalOwnerOf(certId), investor); + + vm.prank(otherInvestor); + vm.expectRevert(IssuanceManager.ConditionCheckFailed.selector); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 10); + assertEq(certPrinter.getActiveCertificateDetails(certId).unitsRepresented, 15); + assertEq(certPrinter.getCertificateDetails(certId).unitsRepresented, 25); + } + + function test_PostUpgrade_ConversionGatesAndConditions() public { + IssuanceManager issuanceManager = _setupUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Guard Cert", + "GCERT" + ); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripifiedCertNotAllowed.selector); + issuanceManager.convertScripToCert(address(certPrinter), 1); + + ICondition[] memory scripToCert = new ICondition[](1); + scripToCert[0] = ICondition( + new SelectorCondition( + address(certPrinter), + IssuanceManager.convertScripToCert.selector, + abi.encode(uint256(150), investor) + ) + ); + + vm.prank(companyOwner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + scripToCert, + 90, + 3, + 2, + new uint256[](0), + false, + true, + true, + true + ); + + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 100 + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 100, address(0)); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 150); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripToCertMinimumNotMet.selector); + issuanceManager.convertScripToCert(address(certPrinter), 80); + + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ConditionCheckFailed.selector); + issuanceManager.convertScripToCert(address(certPrinter), 120); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 150); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function test_PostUpgrade_ReformsVoidedPath() public { + IssuanceManager issuanceManager = _setupUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Voided Cert", + "VCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 500 + ); + + vm.prank(companyOwner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 2, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 20); + + vm.prank(companyOwner); + issuanceManager.voidCertificate(address(certPrinter), certId); + assertTrue(certPrinter.isVoided(certId)); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 20); + + assertEq(certPrinter.totalSupply(), 1); + assertEq(certPrinter.ownerOf(certId), investor); + assertFalse(certPrinter.isVoided(certId)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function test_PostUpgrade_ForceBurnReducesPoolTotals() public { + IssuanceManager issuanceManager = _setupUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Force Burn Cert", + "FBCERT" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 100 + ); + + vm.prank(companyOwner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 2, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + + (uint256 totalTrackedBefore,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + (bool isScripifiedBefore, uint256 scripifiedUnitsBefore,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certId); + assertEq(ICyberScrip(scrip).balanceOf(investor), 20); + assertEq(totalTrackedBefore, 20); + assertTrue(isScripifiedBefore); + assertEq(scripifiedUnitsBefore, 10); + + vm.prank(companyOwner); + issuanceManager.forceScripBurn(address(certPrinter), investor, 8); + + (uint256 totalTrackedAfter,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + (bool isScripifiedAfter, uint256 scripifiedUnitsAfter,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certId); + assertEq(ICyberScrip(scrip).balanceOf(investor), 12); + assertEq(totalTrackedAfter, 12); + assertTrue(isScripifiedAfter); + assertEq(scripifiedUnitsAfter, 6); + } + + function test_PostUpgrade_MultiHolderTransferAndRecertificationPoolAccounting() + public + { + IssuanceManager issuanceManager = _setupUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Pool Cert", + "PCERT" + ); + address thirdHolder = vm.addr(uint256(keccak256("cyberscrip-upgrade-third-holder"))); + address newInvestor = vm.addr(uint256(keccak256("cyberscrip-upgrade-new-investor"))); + + uint256 certIdA = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 10 + ); + uint256 certIdB = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + otherInvestor, + 20 + ); + uint256 certIdC = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + thirdHolder, + 50 + ); + + vm.prank(companyOwner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certIdA, 10, address(0)); + vm.prank(otherInvestor); + issuanceManager.scripifyCert(address(certPrinter), certIdB, 20, address(0)); + vm.prank(thirdHolder); + issuanceManager.scripifyCert(address(certPrinter), certIdC, 50, address(0)); + + assertEq(certPrinter.getActiveCertificateDetails(certIdA).unitsRepresented, 0); + assertEq(certPrinter.getActiveCertificateDetails(certIdB).unitsRepresented, 0); + assertEq(certPrinter.getActiveCertificateDetails(certIdC).unitsRepresented, 0); + assertEq( + certPrinter.getCertificateDetails(certIdA).unitsRepresented, + 10 + ); + assertEq( + certPrinter.getCertificateDetails(certIdB).unitsRepresented, + 20 + ); + assertEq( + certPrinter.getCertificateDetails(certIdC).unitsRepresented, + 50 + ); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 10); + assertEq(ICyberScrip(scrip).balanceOf(otherInvestor), 20); + assertEq(ICyberScrip(scrip).balanceOf(thirdHolder), 50); + assertEq(issuanceManager.getScripPoolAmountById(address(certPrinter), certIdA), 10); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), certIdB), + 20 + ); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), certIdC), + 50 + ); + + vm.prank(investor); + ICyberScrip(scrip).transfer(newInvestor, 2); + vm.prank(otherInvestor); + ICyberScrip(scrip).transfer(newInvestor, 4); + vm.prank(thirdHolder); + ICyberScrip(scrip).transfer(newInvestor, 10); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 8); + assertEq(ICyberScrip(scrip).balanceOf(otherInvestor), 16); + assertEq(ICyberScrip(scrip).balanceOf(thirdHolder), 40); + assertEq(ICyberScrip(scrip).balanceOf(newInvestor), 16); + + // ERC20 transfers do not move pool ownership. + assertEq(issuanceManager.getScripPoolAmountById(address(certPrinter), certIdA), 10); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), certIdB), + 20 + ); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), certIdC), + 50 + ); + + (uint256 totalTrackedBefore,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + assertEq(totalTrackedBefore, 80); + + vm.prank(newInvestor); + issuanceManager.convertScripToCert(address(certPrinter), 16); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 8); + assertEq(ICyberScrip(scrip).balanceOf(otherInvestor), 16); + assertEq(ICyberScrip(scrip).balanceOf(thirdHolder), 40); + assertEq(ICyberScrip(scrip).balanceOf(newInvestor), 0); + + (uint256 totalTrackedAfter,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + assertEq(totalTrackedAfter, 64); + assertEq(issuanceManager.getScripPoolAmountById(address(certPrinter), certIdA), 8); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), certIdB), + 16 + ); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), certIdC), + 40 + ); + + assertEq( + certPrinter.getCertificateDetails(certIdA).unitsRepresented, + 8 + ); + assertEq( + certPrinter.getCertificateDetails(certIdB).unitsRepresented, + 16 + ); + assertEq( + certPrinter.getCertificateDetails(certIdC).unitsRepresented, + 40 + ); + + uint256 newCertId = 3; + assertEq(certPrinter.totalSupply(), 4); + assertEq(certPrinter.ownerOf(newCertId), newInvestor); + assertEq(certPrinter.getCertificateDetails(newCertId).unitsRepresented, 16); + assertEq(issuanceManager.getScripPoolAmountById(address(certPrinter), newCertId), 0); + assertEq( + certPrinter.getActiveCertificateDetails(newCertId).unitsRepresented, + 16 + ); + } + + function test_PostUpgrade_RequiresIssuerApprovalCondition() public { + IssuanceManager issuanceManager = _setupUpgradedIssuanceManager(); + ICyberCertPrinter certPrinter = _deployPrinterAfterUpgrade( + issuanceManager, + "Approval Cert", + "APPR" + ); + uint256 certId = _mintCertAfterUpgrade( + issuanceManager, + certPrinter, + investor, + 10 + ); + + IssuerApprovalRecertificationCondition condition = new IssuerApprovalRecertificationCondition(); + ICondition[] memory scripToCert = new ICondition[](1); + scripToCert[0] = ICondition(address(condition)); + + vm.prank(companyOwner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + scripToCert, + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 5, address(0)); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ConditionCheckFailed.selector); + issuanceManager.convertScripToCert(address(certPrinter), 5); + + vm.prank(otherInvestor); + vm.expectRevert(); + condition.setInvestorApproval(address(certPrinter), investor, true); + + // Condition approvals require AUTH.ADMIN_ROLE on the cert's issuance manager. + BorgAuth auth = BorgAuth(issuanceManager.AUTH()); + uint256 adminRole = auth.ADMIN_ROLE(); + vm.prank(companyOwner); + auth.updateRole(address(this), adminRole); + + condition.setInvestorApproval(address(scrip), investor, true); + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 5); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function _recreateRoundAfterUpgrade( + address roundManagerAddr, + SecuritySeries seriesType, + RoundType roundType, + bytes32 templateId, + address paymentToken, + uint256 pricePerUnit, + uint256 valuation, + uint256 raiseCap, + uint256 minTicket, + uint256 maxTicket, + uint256 startTime, + uint256 endTime, + CompanyOfficer memory officer, + string[] memory legalDetails, + bytes[] memory extensionData, + string[] memory roundPartyValues, + bytes memory escrowedSignature, + CyberCertData[] memory certData + ) internal returns (bytes32 roundId) { + Round memory draft = RoundLib + .draft() + .setTickets( + seriesType, + roundType, + true, + true, + false, + raiseCap, + minTicket, + maxTicket, + paymentToken, + pricePerUnit, + valuation, + startTime, + endTime + ) + .setAgreement( + templateId, + officer.eoa, + officer.name, + officer.title, + legalDetails, + roundPartyValues, + extensionData, + new address[](0), + escrowedSignature + ); + + vm.prank(companyOwner); + roundId = RoundManager(roundManagerAddr).createRound(draft, certData); + } + + function _upgradeCoreStackForCorp( + address corp, + IssuanceManager issuanceManager, + CyberCorpSingleFactory corpSingleFactory, + IssuanceManagerFactory imFactory, + DealManagerFactory dmFactory, + RoundManagerFactory rmFactory + ) internal { + address newCyberCorpImpl = address(new CyberCorp()); + address newIssuanceManagerImpl = address(new IssuanceManager()); + address newDealManagerImpl = address(new DealManager()); + address newRoundManagerImpl = address(new RoundManager()); + address newCertPrinterImpl = address(new CyberCertPrinter()); + address newScripImpl = address(new CyberScrip()); + + vm.startPrank(METALEX_SAFE); + corpSingleFactory.setRefImplementation(newCyberCorpImpl); + imFactory.setRefImplementation(newIssuanceManagerImpl); + dmFactory.setRefImplementation(newDealManagerImpl); + rmFactory.setRefImplementation(newRoundManagerImpl); + imFactory.setCyberCertPrinterRefImplementation(newCertPrinterImpl); + imFactory.setCyberScripRefImplementation(newScripImpl); + vm.stopPrank(); + + assertEq( + corpSingleFactory.getRefImplementation(), + newCyberCorpImpl, + "CyberCorp factory ref implementation mismatch" + ); + assertEq( + imFactory.getRefImplementation(), + newIssuanceManagerImpl, + "IssuanceManager factory ref implementation mismatch" + ); + assertEq( + dmFactory.getRefImplementation(), + newDealManagerImpl, + "DealManager factory ref implementation mismatch" + ); + assertEq( + rmFactory.getRefImplementation(), + newRoundManagerImpl, + "RoundManager factory ref implementation mismatch" + ); + assertEq( + imFactory.getCyberCertPrinterRefImplementation(), + newCertPrinterImpl, + "CyberCertPrinter factory ref implementation mismatch" + ); + assertEq( + imFactory.getCyberScripRefImplementation(), + newScripImpl, + "CyberScrip factory ref implementation mismatch" + ); + + address issuanceManagerAddr = address(issuanceManager); + address dealManagerAddr = CyberCorp(corp).dealManager(); + address roundManagerAddr = CyberCorp(corp).roundManager(); + address oldCyberCorpImpl = corp.getErc1967Implementation(); + address oldIssuanceManagerImpl = issuanceManagerAddr + .getErc1967Implementation(); + address oldDealManagerImpl = dealManagerAddr.getErc1967Implementation(); + address oldRoundManagerImpl = roundManagerAddr.getErc1967Implementation(); + address oldCertPrinterImpl = issuanceManager + .getCertPrinterBeaconImplementation(); + address oldScripImpl = issuanceManager.getScripBeaconImplementation(); + + vm.prank(companyOwner); + IUUPS(corp).upgradeToAndCall(newCyberCorpImpl, ""); + vm.prank(companyOwner); + IUUPS(issuanceManagerAddr).upgradeToAndCall(newIssuanceManagerImpl, ""); + vm.prank(companyOwner); + IUUPS(dealManagerAddr).upgradeToAndCall(newDealManagerImpl, ""); + vm.prank(companyOwner); + IUUPS(roundManagerAddr).upgradeToAndCall(newRoundManagerImpl, ""); + vm.prank(companyOwner); + issuanceManager.upgradeCertPrinterBeaconImplementation(newCertPrinterImpl); + vm.prank(companyOwner); + issuanceManager.upgradeScripBeaconImplementation(newScripImpl); + + assertEq( + corp.getErc1967Implementation(), + newCyberCorpImpl, + "CyberCorp implementation not upgraded" + ); + assertEq( + issuanceManagerAddr.getErc1967Implementation(), + newIssuanceManagerImpl, + "IssuanceManager implementation not upgraded" + ); + assertEq( + dealManagerAddr.getErc1967Implementation(), + newDealManagerImpl, + "DealManager implementation not upgraded" + ); + assertEq( + roundManagerAddr.getErc1967Implementation(), + newRoundManagerImpl, + "RoundManager implementation not upgraded" + ); + assertEq( + issuanceManager.getCertPrinterBeaconImplementation(), + newCertPrinterImpl, + "CyberCertPrinter beacon implementation not upgraded" + ); + assertEq( + issuanceManager.getScripBeaconImplementation(), + newScripImpl, + "CyberScrip beacon implementation not upgraded" + ); + + assertTrue(oldCyberCorpImpl != newCyberCorpImpl, "expected new corp impl"); + assertTrue( + oldIssuanceManagerImpl != newIssuanceManagerImpl, + "expected new issuance manager impl" + ); + assertTrue( + oldDealManagerImpl != newDealManagerImpl, + "expected new deal manager impl" + ); + assertTrue( + oldRoundManagerImpl != newRoundManagerImpl, + "expected new round manager impl" + ); + assertTrue( + oldCertPrinterImpl != newCertPrinterImpl, + "expected new cert printer impl" + ); + assertTrue(oldScripImpl != newScripImpl, "expected new scrip impl"); + } + + function _deployCorpAndRound( + CyberCorpFactory corpFactory, + uint256 userSalt, + CompanyOfficer memory officer, + string[] memory legalDetails, + bytes[] memory extensionData, + CyberCertData[] memory certData, + bytes32 templateId, + address stable, + uint256 pricePerUnit, + uint256 valuation, + string[] memory roundPartyValues, + bytes memory escrowedSignature, + uint256 raiseCap, + uint256 minTicket, + uint256 maxTicket, + uint256 startTime, + uint256 endTime + ) + internal + returns ( + address corp, + address auth, + address issuanceManagerAddr, + address dealManager, + address roundManagerAddr, + bytes32 roundId + ) + { + vm.prank(companyOwner); + return + corpFactory.deployCyberCorpAndCreateRound( + userSalt, + SecuritySeries.SeriesA, + "CyberCorp Upgrade Test", + "Limited Liability Company", + "Delaware", + "contact@test.local", + "Arbitration", + companyOwner, + officer, + legalDetails, + extensionData, + certData, + templateId, + stable, + pricePerUnit, + valuation, + roundPartyValues, + escrowedSignature, + RoundType.FCFS, + new address[](0), + raiseCap, + minTicket, + maxTicket, + startTime, + endTime, + true, + true, + false + ); + } + + function _computeEscrowSignature( + address roundManager, + SecuritySeries seriesType, + uint256 raiseCap, + uint256 minTicket, + uint256 maxTicket, + RoundType roundType, + uint256 startTime, + uint256 endTime, + bytes32 templateId_, + address paymentToken, + uint256 pricePerUnit, + uint256 valuation, + uint256 signerPrivKey, + address companyAddress + ) internal view returns (bytes memory sig, bytes32 roundId) { + roundId = keccak256( + abi.encodePacked( + seriesType, + raiseCap, + minTicket, + maxTicket, + uint8(roundType), + startTime, + endTime, + templateId_, + paymentToken, + pricePerUnit, + valuation, + companyAddress + ) + ); + + bytes32 domainSeparator = keccak256( + abi.encode( + EIP712_DOMAIN_TYPEHASH, + keccak256(bytes("RoundManager")), + keccak256(bytes("1")), + block.chainid, + roundManager + ) + ); + bytes32 structHash = keccak256( + abi.encode( + ESCROWEDSIGNATUREDATA_TYPEHASH, + roundId, + uint8(seriesType), + raiseCap, + minTicket, + maxTicket, + uint8(roundType), + startTime, + endTime, + templateId_, + paymentToken, + pricePerUnit, + valuation, + companyAddress + ) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", domainSeparator, structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivKey, digest); + sig = abi.encodePacked(r, s, v); + } + + function _computeEOISignature( + CyberAgreementRegistry registry, + bytes32 templateId, + uint256 salt, + string[] memory globalValues, + string[] memory partyValues, + address authorityOfficer, + uint256 signerPrivKey + ) internal view returns (bytes memory) { + ( + string memory legalUri, + , + string[] memory glFields, + string[] memory partyFields + ) = registry.getTemplateDetails(templateId); + address signer = vm.addr(signerPrivKey); + address[] memory parties = new address[](2); + parties[0] = authorityOfficer; + parties[1] = signer; + bytes32 contractId = keccak256( + abi.encode(templateId, salt, globalValues, parties) + ); + return + CyberAgreementUtils.signAgreementTypedData( + vm, + registry.DOMAIN_SEPARATOR(), + registry.SIGNATUREDATA_TYPEHASH(), + contractId, + legalUri, + glFields, + partyFields, + globalValues, + partyValues, + signerPrivKey + ); + } + + function _computeAgreementSignature( + CyberAgreementRegistry registry, + bytes32 templateId, + uint256 salt, + string[] memory globalValues, + string[] memory partyValues, + address[] memory parties, + uint256 signerPrivKey + ) internal view returns (bytes memory) { + ( + string memory legalUri, + , + string[] memory glFields, + string[] memory partyFields + ) = registry.getTemplateDetails(templateId); + bytes32 contractId = keccak256( + abi.encode(templateId, salt, globalValues, parties) + ); + return + CyberAgreementUtils.signAgreementTypedData( + vm, + registry.DOMAIN_SEPARATOR(), + registry.SIGNATUREDATA_TYPEHASH(), + contractId, + legalUri, + glFields, + partyFields, + globalValues, + partyValues, + signerPrivKey + ); + } + + function _getCertificateTokenURI( + address certPrinter, + uint256 tokenId + ) internal view returns (string memory) { + return ICyberCertPrinter(certPrinter).tokenURI(tokenId); + } + + function _emptyLex() internal pure returns (LexChexDetails memory) { + return + LexChexDetails({ + request: MintRequest({ + uuid: 0, + owner: address(0), + investorName: "", + investorType: "", + investorJurisdiction: "", + investorContact: "", + mintPrice: 0, + expiry: 0, + paymentToken: address(0) + }), + templateId: bytes32(0), + salt: 0, + globalValues: new string[](0), + parties: new address[](0), + partyValues: new string[][](0), + agreementSignature: "" + }); + } + + function _strings( + string memory a, + string memory b + ) internal pure returns (string[] memory arr) { + arr = new string[](2); + arr[0] = a; + arr[1] = b; + } + + function _setupUpgradedIssuanceManager() + internal + returns (IssuanceManager issuanceManager) + { + CyberCorpFactory corpFactory = CyberCorpFactory( + deployment.cyberCorpFactory + ); + CyberAgreementRegistry registry = CyberAgreementRegistry( + deployment.cyberAgreementRegistry + ); + RoundManagerFactory rmFactory = RoundManagerFactory( + deployment.roundManagerFactory + ); + DealManagerFactory dmFactory = DealManagerFactory( + deployment.dealManagerFactory + ); + CyberCorpSingleFactory corpSingleFactory = CyberCorpSingleFactory( + deployment.cyberCorpSingleFactory + ); + IssuanceManagerFactory imFactory = IssuanceManagerFactory( + deployment.issuanceManagerFactory + ); + + address lxAuth = corpFactory.lexchexAuth(); + vm.startPrank(LEXCHEX_OWNER); + BorgAuth(lxAuth).updateRole( + address(corpFactory), + BorgAuth(lxAuth).OWNER_ROLE() + ); + vm.stopPrank(); + + address stable = corpFactory.stable(); + bytes32 templateId = bytes32( + uint256(keccak256(abi.encodePacked("cyberscrip-upgrade-conversion-template", address(this), block.timestamp))) + ); + + vm.prank(METALEX_SAFE); + registry.createTemplate( + templateId, + "CyberScrip conversion template", + "ipfs://cyberscrip-conversion-template", + _strings("purchaseAmount", "valuation"), + _strings("name", "jurisdiction") + ); + + CompanyOfficer memory officer = CompanyOfficer({ + eoa: companyOwner, + name: "Officer One", + contact: "officer@test.local", + title: "CEO" + }); + + uint256 userSalt = uint256( + keccak256(abi.encodePacked("cyberscrip-upgrade-conversion-salt", address(this), block.timestamp)) + ); + bytes32 corpSalt = keccak256(abi.encodePacked(userSalt)); + + uint256 raiseCap = 100_000e6; + uint256 minTicket = 100e6; + uint256 maxTicket = 2_000e6; + uint256 startTime = block.timestamp - 1; + uint256 endTime = block.timestamp + 7 days; + uint256 pricePerUnit = 1e18; + uint256 valuation = 5_000_000e18; + + (bytes memory escrowedSignature, ) = _computeEscrowSignature( + rmFactory.computeRoundManagerAddress(corpSalt), + SecuritySeries.SeriesA, + raiseCap, + minTicket, + maxTicket, + RoundType.FCFS, + startTime, + endTime, + templateId, + stable, + pricePerUnit, + valuation, + companyOwnerPk, + corpSingleFactory.computeCyberCorpSingleAddress(corpSalt) + ); + + CyberCertData[] memory certData = new CyberCertData[](1); + certData[0] = CyberCertData({ + name: "SAFE", + symbol: "SAFE", + uri: "ipfs://safe-cert", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesA, + extension: address(0), + defaultLegend: new string[](0) + }); + + string[] memory legalDetails = new string[](1); + legalDetails[0] = "legal-details"; + bytes[] memory extensionData = new bytes[](1); + extensionData[0] = ""; + string[] memory roundPartyValues = _strings("Officer One", "US"); + + (address corp, address auth, address issuanceManagerAddr, , , ) = _deployCorpAndRound( + corpFactory, + userSalt, + officer, + legalDetails, + extensionData, + certData, + templateId, + stable, + pricePerUnit, + valuation, + roundPartyValues, + escrowedSignature, + raiseCap, + minTicket, + maxTicket, + startTime, + endTime + ); + + vm.prank(deployment.cyberCorpFactory); + BorgAuth(auth).updateRole(companyOwner, 99); + + issuanceManager = IssuanceManager(issuanceManagerAddr); + _upgradeCoreStackForCorp( + corp, + issuanceManager, + corpSingleFactory, + imFactory, + dmFactory, + rmFactory + ); + } + + function _deployPrinterAfterUpgrade( + IssuanceManager issuanceManager, + string memory name, + string memory symbol + ) internal returns (ICyberCertPrinter certPrinter) { + vm.prank(companyOwner); + certPrinter = ICyberCertPrinter( + issuanceManager.createCertPrinter( + new string[](0), + name, + symbol, + "uri://cert", + SecurityClass.CommonStock, + SecuritySeries.SeriesA, + address(0) + ) + ); + } + + function _mintCertAfterUpgrade( + IssuanceManager issuanceManager, + ICyberCertPrinter certPrinter, + address to, + uint256 units + ) internal returns (uint256 tokenId) { + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: units, + legalDetails: "", + extensionData: "" + }); + vm.prank(companyOwner); + tokenId = issuanceManager.createCert(address(certPrinter), to, details); + + // Seed legal owner in tests: endorsement + self-transfer triggers owner details update. + Endorsement memory selfEndorsement = Endorsement({ + endorser: to, + timestamp: block.timestamp, + signatureHash: "", + registry: address(0), + agreementId: bytes32(0), + endorsee: to, + endorseeName: "" + }); + vm.prank(to); + certPrinter.addEndorsement(tokenId, selfEndorsement); + vm.prank(companyOwner); + issuanceManager.setTokenTransferable(address(certPrinter), tokenId, true); + vm.prank(to); + certPrinter.safeTransferFrom(to, to, tokenId); + vm.prank(companyOwner); + issuanceManager.setTokenTransferable(address(certPrinter), tokenId, false); + } +} diff --git a/test/DealManagerTest.t.sol b/test/DealManagerTest.t.sol index a17b2e64..965b99e8 100644 --- a/test/DealManagerTest.t.sol +++ b/test/DealManagerTest.t.sol @@ -320,6 +320,41 @@ contract DealManagerTest is Test { ); } + function testPOC_SignAndFinalizeDeal_BypassesSignatureWhenRegistryAlreadySigned() public { + (bytes32 agreementId, ) = _proposeSignedDeal(); + + // Alice signs directly in the registry first (out-of-band from DealManager). + registry.signContractFor( + alice, + agreementId, + new string[](0), + GOOD_SIGNATURE, + false, + "" + ); + + uint256 companyPaymentTokenBalancesBefore = paymentToken.balanceOf(companyPayable); + + // Bob can now finalize on behalf of Alice with a bad signature because + // DealManager skips signature verification when registry.hasSigned == true. + vm.prank(bob); + dm.signAndFinalizeDeal( + alice, // signer + agreementId, + new string[](0), // partyValues + BAD_SIGNATURE, // ignored on hasSigned branch + false, + "Bob as Alice", + "" + ); + + assertEq( + paymentToken.balanceOf(companyPayable), + companyPaymentTokenBalancesBefore + 10 ether, + "Bob triggers Alice payment/finalization with invalid signature" + ); + } + function test_PaymentFlow_ProposeDeal() public { // proposeDeal() is one of the two methods that'll pull certificates from the issuing company (first party) // Unlike the more generic LexScroWLite, DealManager assumes the company's assets are certificates-only diff --git a/test/FactoryArbitraryErc20RoundPOC.t.sol b/test/FactoryArbitraryErc20RoundPOC.t.sol new file mode 100644 index 00000000..0017c3e5 --- /dev/null +++ b/test/FactoryArbitraryErc20RoundPOC.t.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {CyberCorpHelper} from "./RoundManagerTest.t.sol"; +import {MockERC20} from "./mock/MockERC20.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {CyberCorpFactory} from "../src/CyberCorpFactory.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol"; +import {CertificateImageBuilderContract} from "../src/CertificateImageBuilderContract.sol"; +import { + CompanyOfficer, + SecurityClass, + SecuritySeries +} from "../src/CyberCorpConstants.sol"; +import {CyberCertData, RoundType} from "../src/interfaces/IRoundManager.sol"; +import {EOI} from "../src/storage/RoundManagerStorage.sol"; +import {Round} from "../src/libs/RoundLib.sol"; + +contract FactoryArbitraryErc20RoundPOCTest is Test { + uint256 internal ownerPk = 0xA11CE; + uint256 internal officerPk = 0xB0B; + uint256 internal investorPk = 0xC0DE; + + address internal owner = vm.addr(ownerPk); + address internal officer = vm.addr(officerPk); + address internal investor = vm.addr(investorPk); + + CyberAgreementRegistry internal registry; + CyberCorpFactory internal corpFactory; + address internal cyberCorpSingleFactory; + address internal rmFactory; + MockERC20 internal paymentToken; + + uint8 internal constant PAYMENT_DECIMALS = 18; + uint256 internal constant TOKEN_SCALE = 10 ** PAYMENT_DECIMALS; + uint256 internal constant VALUATION = 20_000_000 * TOKEN_SCALE; // 20M in mock ERC-20 units + + function setUp() public { + address uriBuilder; + ( + registry, + corpFactory, + , + cyberCorpSingleFactory, + , + rmFactory, + uriBuilder, + ) = CyberCorpHelper.deployRegistryAndFactories(owner); + + vm.prank(owner); + CyberCorpHelper.createTemplate(registry); + + // FCFS allocation mints certs and resolves tokenURI, which requires a configured image builder. + address imageBuilder = address(new CertificateImageBuilderContract()); + vm.prank(owner); + CertificateUriBuilder(uriBuilder).setImageBuilder(imageBuilder); + + paymentToken = new MockERC20("Mock USD", "mUSD", PAYMENT_DECIMALS); + paymentToken.mint(investor, 2_000_000 * TOKEN_SCALE); + } + + function test_POC_ArbitraryErc20DrivesRoundDenominationAndRatio() public { + uint256 salt = 424242; + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + address predictedCorp = CyberCorpSingleFactory(cyberCorpSingleFactory) + .computeCyberCorpSingleAddress(corpSalt); + address predictedRM = RoundManagerFactory(rmFactory) + .computeRoundManagerAddress(corpSalt); + + CompanyOfficer memory companyOfficer = CompanyOfficer({ + eoa: officer, + name: "Officer A", + contact: "officer@corp.com", + title: "CEO" + }); + + uint256 raiseCap = 1_000_000 * TOKEN_SCALE; // 5% of valuation + uint256 ticket = 1_000_000 * TOKEN_SCALE; + uint256 pricePerUnit = 1 * TOKEN_SCALE; // 1 "unit" priced in mock ERC-20 + uint256 startTime = block.timestamp - 1; + uint256 endTime = block.timestamp + 30 days; + + string[] memory legalDetails = new string[](1); + legalDetails[0] = "SEED SAFE legal details"; + bytes[] memory extensionData = new bytes[](1); + extensionData[0] = ""; + + string[] memory defaultLegend = new string[](1); + defaultLegend[0] = "SEED SAFE"; + CyberCertData[] memory certData = new CyberCertData[](1); + certData[0] = CyberCertData({ + name: "SEED SAFE", + symbol: "SEEDSAFE", + uri: "ipfs://seed-safe", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesSeed, + extension: address(0), + defaultLegend: defaultLegend + }); + + string[] memory roundPartyValues = new string[](2); + roundPartyValues[0] = companyOfficer.name; + roundPartyValues[1] = companyOfficer.title; + + (bytes memory escrowedSig, ) = CyberCorpHelper.computeEscrowSignature( + predictedRM, + SecuritySeries.SeriesSeed, + raiseCap, + ticket, + ticket, + RoundType.FCFS, + startTime, + endTime, + CyberCorpHelper.TEMPLATE_ID, + address(paymentToken), + pricePerUnit, + VALUATION, + officerPk, + predictedCorp + ); + + ( + address corp, + , + , + , + address roundManagerAddr, + bytes32 roundId + ) = corpFactory.deployCyberCorpAndCreateRound( + salt, + SecuritySeries.SeriesSeed, + "Seed Corp", + "C-Corp", + "DE", + "contact@seedcorp.com", + "Arbitration", + owner, + companyOfficer, + legalDetails, + extensionData, + certData, + CyberCorpHelper.TEMPLATE_ID, + address(paymentToken), + pricePerUnit, + VALUATION, + roundPartyValues, + escrowedSig, + RoundType.FCFS, + new address[](0), + raiseCap, + ticket, + ticket, + startTime, + endTime, + true, + true, + false + ); + + assertEq(corp, predictedCorp, "unexpected corp address"); + assertEq(roundManagerAddr, predictedRM, "unexpected round manager address"); + + Round memory createdRound = RoundManager(roundManagerAddr).getRound(roundId); + assertEq(createdRound.paymentToken, address(paymentToken), "round payment token must be mock erc20"); + assertEq(createdRound.raiseCap, raiseCap, "raise cap should be token-denominated"); + assertEq(createdRound.minTicket, ticket, "min ticket should be token-denominated"); + assertEq(createdRound.maxTicket, ticket, "max ticket should be token-denominated"); + assertEq(createdRound.pricePerUnit, pricePerUnit, "price per unit should be token-denominated"); + assertEq(createdRound.valuation, VALUATION, "valuation should be token-denominated"); + assertEq(uint256(createdRound.seriesType), uint256(SecuritySeries.SeriesSeed), "series should be seed"); + assertEq( + uint256(createdRound.primarySecurityClass), + uint256(SecurityClass.SAFE), + "primary security should be SAFE" + ); + assertEq( + uint256(createdRound.primarySecuritySeries), + uint256(SecuritySeries.SeriesSeed), + "primary security series should be seed" + ); + + string[] memory globalValues = new string[](1); + globalValues[0] = "global"; + string[] memory partyValues = new string[](2); + partyValues[0] = "Investor"; + partyValues[1] = "Individual"; + uint256 eoiSalt = 777; + + bytes memory eoiSignature = CyberCorpHelper.computeEOISignature( + registry, + CyberCorpHelper.TEMPLATE_ID, + eoiSalt, + globalValues, + partyValues, + companyOfficer.eoa, + investorPk + ); + + EOI memory eoi = EOI({ + name: "Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@example.com", + minAmount: ticket, + maxAmount: ticket, + expiry: block.timestamp + 7 days, + naturalPerson: true, + lexchexDetails: CyberCorpHelper.emptyLex() + }); + + uint256 investorBalanceBefore = paymentToken.balanceOf(investor); + vm.startPrank(investor); + paymentToken.approve(roundManagerAddr, ticket); + RoundManager(roundManagerAddr).submitEOI( + roundId, + eoi, + globalValues, + partyValues, + eoiSignature, + eoiSalt, + new address[](0), + bytes32(0) + ); + vm.stopPrank(); + + Round memory roundAfterPayment = RoundManager(roundManagerAddr).getRound(roundId); + uint256 investorSpent = investorBalanceBefore - paymentToken.balanceOf(investor); + + assertEq(roundAfterPayment.raised, ticket, "raised should track payment token amount"); + assertEq(investorSpent, ticket, "investor payment should be in mock erc20 units"); + + // 1,000,000 / 20,000,000 = 5% == 0.05e18 + uint256 expectedRatio1e18 = 50_000_000_000_000_000; + uint256 paidToValuationRatio1e18 = (roundAfterPayment.raised * 1e18) / VALUATION; + assertEq( + paidToValuationRatio1e18, + expectedRatio1e18, + "payment-to-valuation ratio should remain consistent in token units" + ); + } +} diff --git a/test/IssuanceManagerConversionTest.t.sol b/test/IssuanceManagerConversionTest.t.sol index 88377314..a6bc1e8a 100644 --- a/test/IssuanceManagerConversionTest.t.sol +++ b/test/IssuanceManagerConversionTest.t.sol @@ -3,7 +3,13 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../src/IssuanceManager.sol"; +import "../src/CyberCertPrinter.sol"; import "../src/CyberScrip.sol"; +import "../src/interfaces/ICyberScrip.sol"; +import "../src/interfaces/ICyberCertPrinter.sol"; +import "../src/interfaces/ICondition.sol"; +import "../src/interfaces/ITransferRestrictionHook.sol"; +import "../src/interfaces/IUriBuilder.sol"; import "../src/libs/auth.sol"; import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; @@ -53,6 +59,33 @@ contract MockRoundManagerForConversion { } } +contract SelectorCondition is ICondition { + address public expectedContract; + bytes4 public expectedSelector; + bytes32 public expectedDataHash; + + constructor( + address _contract, + bytes4 _selector, + bytes memory data + ) { + expectedContract = _contract; + expectedSelector = _selector; + expectedDataHash = keccak256(data); + } + + function checkCondition( + address _contract, + bytes4 _functionSignature, + bytes memory data + ) external view returns (bool) { + return + _contract == expectedContract && + _functionSignature == expectedSelector && + keccak256(data) == expectedDataHash; + } +} + contract MockCertPrinter { using IssuanceManagerStorage for IssuanceManagerStorage.IssuanceManagerData; @@ -60,36 +93,71 @@ contract MockCertPrinter { mapping(uint256 => CertificateDetails) internal _details; mapping(uint256 => address) internal _owners; + mapping(address => uint256) internal _balances; + mapping(address => uint256[]) internal _ownedTokens; + mapping(uint256 => bool) internal _voided; uint256 internal _total; string internal _name = "Mock"; string internal _symbol = "MOCK"; + address internal _issuanceManager; function initialize( string[] memory, string memory name_, string memory symbol_, string memory, - address, + address issuanceManager_, SecurityClass, SecuritySeries, address ) external { _name = name_; _symbol = symbol_; + _issuanceManager = issuanceManager_; } function name() external view returns (string memory) { return _name; } function symbol() external view returns (string memory) { return _symbol; } + function issuanceManager() external view returns (address) { return _issuanceManager; } function totalSupply() external view returns (uint256) { return _total; } function safeMint(uint256 tokenId, address to, CertificateDetails memory details) external returns (uint256) { + _mint(tokenId, to, details); + return tokenId; + } + + function safeMintAndAssign(address to, uint256 tokenId, CertificateDetails memory details) external returns (uint256) { + _mint(tokenId, to, details); + return tokenId; + } + + function assignCert(address from, uint256 tokenId, address to, CertificateDetails memory details) external returns (uint256) { + if (_owners[tokenId] == from) { + _owners[tokenId] = to; + } + _details[tokenId] = details; + return tokenId; + } + + function updateCertificateDetails(uint256 tokenId, CertificateDetails calldata details) external { + _details[tokenId] = details; + } + + function balanceOf(address owner) external view returns (uint256) { return _balances[owner]; } + + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) { + return _ownedTokens[owner][index]; + } + + function _mint(uint256 tokenId, address to, CertificateDetails memory details) internal { _details[tokenId] = details; _owners[tokenId] = to; + _balances[to] += 1; + _ownedTokens[to].push(tokenId); if (tokenId == _total) { _total = tokenId + 1; } - return tokenId; } function tokenURI(uint256) external pure returns (string memory) { return ""; } @@ -97,25 +165,102 @@ contract MockCertPrinter { function ownerOf(uint256 tokenId) external view returns (address) { return _owners[tokenId]; } function getCertificateDetails(uint256 tokenId) external view returns (CertificateDetails memory) { return _details[tokenId]; } + function getActiveCertificateDetails(uint256 tokenId) external view returns (CertificateDetails memory) { return _details[tokenId]; } - function voidCert(uint256 /*tokenId*/) external {} + function safeTransferFrom(address from, address to, uint256 tokenId) external { + if (_owners[tokenId] != from) { + revert("NOT_OWNER"); + } + _owners[tokenId] = to; + _balances[from] -= 1; + _balances[to] += 1; + _ownedTokens[to].push(tokenId); + } + + function voidCert(uint256 tokenId) external { + _voided[tokenId] = true; + } + + function isVoided(uint256 tokenId) external view returns (bool) { + return _voided[tokenId]; + } +} + +contract MockCyberCorp { + address public dealManagerAddress = address(0xD34D); + address public roundManagerAddress = address(0xB0B0); + + function cyberCORPName() external pure returns (string memory) { return "MockCorp"; } + function cyberCORPType() external pure returns (string memory) { return "C-Corp"; } + function cyberCORPJurisdiction() external pure returns (string memory) { return "DE"; } + function cyberCORPContactDetails() external pure returns (string memory) { return "mock@corp.test"; } + function dealManager() external view returns (address) { return dealManagerAddress; } + function roundManager() external view returns (address) { return roundManagerAddress; } +} + +contract MockUriBuilder is IUriBuilder { + function buildCertificateUri( + string memory, + string memory, + string memory, + string memory, + SecurityClass, + SecuritySeries, + string memory, + string[] memory, + CertificateDetails memory, + Endorsement[] memory, + OwnerDetails memory, + address, + bytes32, + uint256, + address, + address + ) external pure returns (string memory) { + return "uri://mock"; + } + + function buildCertificateUriNotEncoded( + string memory, + string memory, + string memory, + string memory, + SecurityClass, + SecuritySeries, + string memory, + string[] memory, + CertificateDetails memory, + Endorsement[] memory, + OwnerDetails memory, + address, + bytes32, + uint256, + address, + address + ) external pure returns (string memory) { + return "uri://mock"; + } } contract IssuanceManagerConversionTest is Test { bytes32 salt = bytes32(keccak256("IssuanceManagerConversionTest")); IssuanceManager public issuanceManager; - MockCertPrinter public safePrinter; - MockCertPrinter public equityPrinter; + ICyberCertPrinter public safePrinter; + ICyberCertPrinter public equityPrinter; BorgAuth public auth; MockRoundManagerForConversion public mockRM; + MockCyberCorp public mockCorp; + MockUriBuilder public mockUriBuilder; address public owner; address public investor; + address public otherInvestor; function setUp() public { owner = address(this); investor = makeAddr("investor"); + otherInvestor = makeAddr("otherInvestor"); // Auth auth = new BorgAuth(owner); @@ -127,7 +272,7 @@ contract IssuanceManagerConversionTest is Test { IssuanceManagerFactory.initialize.selector, address(auth), new IssuanceManager(), - new MockCertPrinter(), + new CyberCertPrinter(), new CyberScrip() ) ) @@ -135,24 +280,115 @@ contract IssuanceManagerConversionTest is Test { // IssuanceManager via proxy (implementation disables initializers in constructor) issuanceManager = IssuanceManager(imFactory.deployIssuanceManager(salt)); + mockCorp = new MockCyberCorp(); + mockUriBuilder = new MockUriBuilder(); issuanceManager.initialize( address(auth), - address(0xC0DE), - address(0xBEEF), + address(mockCorp), + address(mockUriBuilder), address(imFactory) ); - // Deploy printers and initialize with issuanceManager as controller - safePrinter = new MockCertPrinter(); - safePrinter.initialize(new string[](0), "SAFE Cert", "SAFE", "uri://safe", address(issuanceManager), SecurityClass.SAFT, SecuritySeries.NA, address(0)); - - equityPrinter = new MockCertPrinter(); - equityPrinter.initialize(new string[](0), "Equity Cert", "EQTY", "uri://eq", address(issuanceManager), SecurityClass.PreferredStock, SecuritySeries.SeriesA, address(0)); + safePrinter = ICyberCertPrinter( + issuanceManager.createCertPrinter( + new string[](0), + "SAFE Cert", + "SAFE", + "uri://safe", + SecurityClass.SAFT, + SecuritySeries.NA, + address(0) + ) + ); + equityPrinter = ICyberCertPrinter( + issuanceManager.createCertPrinter( + new string[](0), + "Equity Cert", + "EQTY", + "uri://eq", + SecurityClass.PreferredStock, + SecuritySeries.SeriesA, + address(0) + ) + ); // Mock round manager mockRM = new MockRoundManagerForConversion(); } + function test_createCertAndAssignWithName_storesEndorsementSignatureAndTimestamp() + public + { + ICyberCertPrinter certPrinter = _deployPrinter("Signed Cert", "SCERT"); + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 25 * 1e18, + legalDetails: "Signed legal details", + extensionData: "Signed extension" + }); + bytes memory endorsementSignature = hex"1234abcd"; + uint256 endorsementTimestamp = 1_717_171_717; + + vm.prank(owner); + uint256 certId = issuanceManager.createCertAndAssignWithName( + address(certPrinter), + investor, + details, + "Signed Investor", + endorsementSignature, + endorsementTimestamp + ); + + Endorsement memory endorsement = CyberCertPrinter(address(certPrinter)) + .getEndorsementHistory(certId, 0); + + assertEq(endorsement.endorser, address(issuanceManager)); + assertEq(endorsement.endorseeName, "Signed Investor"); + assertEq(endorsement.registry, address(0)); + assertEq(endorsement.agreementId, bytes32(0)); + assertEq(endorsement.timestamp, endorsementTimestamp); + assertEq(endorsement.signatureHash, endorsementSignature); + assertEq(endorsement.endorsee, investor); + + assertEq(certPrinter.getIssuerSignatureCount(certId), 1); + assertEq(certPrinter.getIssuerSignatureAt(certId, 0), endorsementSignature); + } + + function test_createCertAndAssignWithName_withoutSignature_skipsIssuerSignatureStorage() + public + { + ICyberCertPrinter certPrinter = _deployPrinter("Unsigned Cert", "UCERT"); + CertificateDetails memory details = _buildCertificateDetails( + 25, + "Unsigned legal details", + bytes("Unsigned extension") + ); + uint256 endorsementTimestamp = 1_717_171_718; + + vm.prank(owner); + uint256 certId = issuanceManager.createCertAndAssignWithName( + address(certPrinter), + investor, + details, + "Unsigned Investor", + bytes(""), + endorsementTimestamp + ); + + Endorsement memory endorsement = CyberCertPrinter(address(certPrinter)) + .getEndorsementHistory(certId, 0); + + assertEq(certPrinter.ownerOf(certId), investor); + assertEq(endorsement.endorser, address(issuanceManager)); + assertEq(endorsement.endorseeName, "Unsigned Investor"); + assertEq(endorsement.timestamp, endorsementTimestamp); + assertEq(endorsement.signatureHash, bytes("")); + assertEq(certPrinter.getIssuerSignatureCount(certId), 0); + } + function test_convertSAFE_floor_minPicksLower() public { // Configure round // price decimals = 2, share decimals = 0, mode = floor @@ -192,6 +428,1557 @@ contract IssuanceManagerConversionTest is Test { // SAFE should be voided (tokenURI would revert or ownerOf may still show owner but status void stored internally) // We can assert that further transfers are restricted due to void status only if exposed; check that updateCertificateDetails or owner unchanged is fine. } + + function test_convertScripToCert_AllowsNonOwnerMintPath() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + // Integer unit count; _mintCert stores unitsRepresented as units * 1e18 + uint256 amount = 100; + uint256 sourceCertId = _mintCert(certPrinter, otherInvestor, amount); + + ITransferRestrictionHook[] memory hooks = new ITransferRestrictionHook[](0); + ICondition[] memory certToScrip = new ICondition[](0); + ICondition[] memory scripToCert = new ICondition[](0); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + hooks, + certToScrip, + scripToCert, + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(otherInvestor); + issuanceManager.scripifyCert( + address(certPrinter), + sourceCertId, + amount * 1e18, + investor + ); + assertEq(ICyberScrip(scrip).balanceOf(investor), amount * 1e18); + + CertificateDetails memory approvalDetails = _stageRecertificationApproval( + certPrinter, + investor, + "Investor Name", + 777, + "Approved legal details", + bytes("approved extension") + ); + + // Non-owner should be able to convert and mint a cert via IssuanceManager + vm.recordLogs(); + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), amount * 1e18); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 recertifiedTopic = keccak256( + "ScripRecertified(address,address,uint256,uint256,uint256,uint256)" + ); + bool sawRecertified; + for (uint256 i = 0; i < logs.length; i++) { + if ( + logs[i].emitter == address(issuanceManager) && + logs[i].topics.length == 4 && + logs[i].topics[0] == recertifiedTopic && + address(uint160(uint256(logs[i].topics[1]))) == address(certPrinter) && + address(uint160(uint256(logs[i].topics[2]))) == investor && + uint256(logs[i].topics[3]) == 1 + ) { + ( + uint256 scripAmount, + uint256 oldUnitsRepresented, + uint256 newUnitsRepresented + ) = abi.decode(logs[i].data, (uint256, uint256, uint256)); + assertEq(scripAmount, amount * 1e18); + assertEq(oldUnitsRepresented, 0); + assertEq(newUnitsRepresented, amount * 1e18); + sawRecertified = true; + break; + } + } + assertTrue(sawRecertified); + + assertEq(certPrinter.totalSupply(), 2); + assertEq(certPrinter.ownerOf(1), investor); + CertificateDetails memory details = certPrinter.getCertificateDetails(1); + assertEq(details.unitsRepresented, amount * 1e18); + assertEq(details.legalDetails, approvalDetails.legalDetails); + assertEq(details.extensionData, approvalDetails.extensionData); + } + + function test_ScripifyAndUnscripify_WithConditions() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 1000 * 1e18, + legalDetails: "", + extensionData: "" + }); + + vm.prank(owner); + uint256 certId = issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + details + ); + + uint256 scripAmount = 250; + bytes4 scripifySelector = bytes4( + keccak256("scripifyCert(address,uint256,uint256,address)") + ); + ICondition[] memory certToScrip = new ICondition[](2); + certToScrip[0] = ICondition( + new SelectorCondition( + address(certPrinter), + scripifySelector, + abi.encode(certId, scripAmount, address(0)) + ) + ); + certToScrip[1] = ICondition( + new SelectorCondition( + address(certPrinter), + scripifySelector, + abi.encode(certId, scripAmount, address(0)) + ) + ); + + ICondition[] memory scripToCert = new ICondition[](1); + scripToCert[0] = ICondition( + new SelectorCondition( + address(certPrinter), + IssuanceManager.convertScripToCert.selector, + abi.encode(scripAmount, investor) + ) + ); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + certToScrip, + scripToCert, + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, scripAmount, address(0)); + assertEq(ICyberScrip(scrip).balanceOf(investor), scripAmount); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), scripAmount); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + + assertEq(certPrinter.totalSupply(), 1); + assertEq(certPrinter.ownerOf(0), investor); + CertificateDetails memory restored = certPrinter.getCertificateDetails(0); + assertEq(restored.unitsRepresented, 1000 * 1e18); + } + + function test_ScripRatio_AppliesOnScripifyAndConvert() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 10 * 1e18, + legalDetails: "", + extensionData: "" + }); + + vm.prank(owner); + issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + details + ); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(owner); + issuanceManager.setScripRatio(address(certPrinter), 2, 1); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), 0, 4, address(0)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 8); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 8); + + assertEq(certPrinter.totalSupply(), 1); + CertificateDetails memory newDetails = certPrinter.getCertificateDetails( + 0 + ); + assertEq(newDetails.unitsRepresented, 10 * 1e18); + } + + function test_ScripifyWhitelist_EnabledBlocksNonWhitelisted() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 10 * 1e18, + legalDetails: "", + extensionData: "" + }); + + vm.prank(owner); + uint256 certId = issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + details + ); + + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + true, + true, + true, + true + ); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripifyNotWhitelisted.selector); + issuanceManager.scripifyCert(address(certPrinter), certId, 1, address(0)); + } + + function test_ScripifyWhitelist_EnabledAllowsWhitelisted() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 10 * 1e18, + legalDetails: "", + extensionData: "" + }); + + vm.prank(owner); + uint256 certId = issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + details + ); + + uint256[] memory whitelistIds = new uint256[](1); + whitelistIds[0] = certId; + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + whitelistIds, + true, + true, + true, + true + ); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 1, address(0)); + } + + function test_ScripifyWhitelist_ToggleAndUpdate() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 10 * 1e18, + legalDetails: "", + extensionData: "" + }); + + vm.prank(owner); + uint256 certId = issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + details + ); + + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + // Enable whitelist and add ID + issuanceManager.setScripifyWhitelistEnabled(address(certPrinter), true); + uint256[] memory addIds = new uint256[](1); + addIds[0] = certId; + issuanceManager.addScripifyWhitelistIds(address(certPrinter), addIds); + + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 1, address(0)); + + // Remove ID and ensure blocked + uint256[] memory removeIds = new uint256[](1); + removeIds[0] = certId; + issuanceManager.removeScripifyWhitelistIds(address(certPrinter), removeIds); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripifyNotWhitelisted.selector); + issuanceManager.scripifyCert(address(certPrinter), certId, 1, address(0)); + + // Disable whitelist and allow again + issuanceManager.setScripifyWhitelistEnabled(address(certPrinter), false); + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 1, address(0)); + } + + function test_GetScripRatio_DefaultsToOneWhenUnset() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + (uint256 numerator, uint256 denominator) = issuanceManager.getScripRatio( + address(certPrinter) + ); + assertEq(numerator, 1); + assertEq(denominator, 1); + } + + function test_DeployCyberScrip_SetsDefaultRatio() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + (uint256 numerator, uint256 denominator) = issuanceManager.getScripRatio( + address(certPrinter) + ); + assertEq(numerator, 1); + assertEq(denominator, 1); + } + + function test_RevertWhen_SetScripRatioZeroNumeratorOrDenominator() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + vm.expectRevert(IssuanceManager.InvalidScripRatio.selector); + issuanceManager.setScripRatio(address(certPrinter), 0, 1); + + vm.expectRevert(IssuanceManager.InvalidScripRatio.selector); + issuanceManager.setScripRatio(address(certPrinter), 1, 0); + } + + function test_RevertWhen_ScripifyRatioRemainder() public { + ICyberCertPrinter certPrinter = _deployPrinter("Cert", "CERT"); + + CertificateDetails memory details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: 10 * 1e18, + legalDetails: "", + extensionData: "" + }); + + issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + details + ); + + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + issuanceManager.setScripRatio(address(certPrinter), 2, 3); + + } + + + + function test_convertScripToCert_parameterLifecycleAndRuntimeUpdates() public { + ICyberCertPrinter certPrinter = _deployPrinter("Lifecycle Cert", "LCERT"); + uint256 certId = _mintCert(certPrinter, investor, 75); + + uint256[] memory whitelistIds = new uint256[](1); + whitelistIds[0] = certId; + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 50, // initial minimum + 3, // initial ratio numerator + 2, // initial ratio denominator + whitelistIds, + true, // whitelist enabled + true, + true, + true + ); + + // Validate deploy-time parameters + (uint256 initialNum, uint256 initialDen) = issuanceManager.getScripRatio( + address(certPrinter) + ); + assertEq(initialNum, 3); + assertEq(initialDen, 2); + assertEq(issuanceManager.getScripToCertMinimum(address(certPrinter)), 50); + assertTrue(issuanceManager.getScripifyWhitelistEnabled(address(certPrinter))); + assertTrue(issuanceManager.isScripifyWhitelisted(address(certPrinter), certId)); + assertFalse(issuanceManager.isScripifyWhitelisted(address(certPrinter), 99999)); + + // Update all runtime parameters and verify + issuanceManager.setScripRatio(address(certPrinter), 4, 1); + issuanceManager.setScripToCertMinimum(address(certPrinter), 40); + issuanceManager.setScripifyWhitelistEnabled(address(certPrinter), false); + + uint256[] memory removeIds = new uint256[](1); + removeIds[0] = certId; + issuanceManager.removeScripifyWhitelistIds(address(certPrinter), removeIds); + assertFalse(issuanceManager.isScripifyWhitelisted(address(certPrinter), certId)); + + uint256[] memory addIds = new uint256[](2); + addIds[0] = certId; + addIds[1] = certId + 1; + issuanceManager.addScripifyWhitelistIds(address(certPrinter), addIds); + + (uint256 updatedNum, uint256 updatedDen) = issuanceManager.getScripRatio( + address(certPrinter) + ); + assertEq(updatedNum, 4); + assertEq(updatedDen, 1); + assertEq(issuanceManager.getScripToCertMinimum(address(certPrinter)), 40); + assertFalse(issuanceManager.getScripifyWhitelistEnabled(address(certPrinter))); + assertTrue(issuanceManager.isScripifyWhitelisted(address(certPrinter), certId)); + assertTrue(issuanceManager.isScripifyWhitelisted(address(certPrinter), certId + 1)); + + // With ratio 4:1, scripifying 10 units mints 40 scrip + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), certId, 10, address(0)); + assertEq(ICyberScrip(scrip).balanceOf(investor), 40); + + // Minimum is now 40; lower amounts should revert + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripToCertMinimumNotMet.selector); + issuanceManager.convertScripToCert(address(certPrinter), 39); + + // Convert exactly at minimum, ensuring conversion uses updated ratio + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 40); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + assertEq(certPrinter.totalSupply(), 1); + assertEq(certPrinter.ownerOf(0), investor); + CertificateDetails memory converted = certPrinter.getCertificateDetails(0); + assertEq(converted.unitsRepresented, 75 * 1e18); + } + + function test_convertScripToCert_revertGatesAndConditionValidation() public { + ICyberCertPrinter certPrinter = _deployPrinter("Guard Cert", "GCERT"); + + // Unconfigured cert should always fail conversion. + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripifiedCertNotAllowed.selector); + issuanceManager.convertScripToCert(address(certPrinter), 1); + + // Condition requires exact amount = 150 + ICondition[] memory scripToCert = new ICondition[](1); + scripToCert[0] = ICondition( + new SelectorCondition( + address(certPrinter), + IssuanceManager.convertScripToCert.selector, + abi.encode(uint256(150), investor) + ) + ); + + uint256 sourceCertId = _mintCert(certPrinter, otherInvestor, 134); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 90*1e18, // minimum + 3, // ratio numerator + 2, // ratio denominator + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(otherInvestor); + issuanceManager.scripifyCert( + address(certPrinter), + sourceCertId, + 134*1e18, + investor + ); + assertEq(ICyberScrip(scrip).balanceOf(investor), 201*1e18); + + // Fails minimum + vm.prank(investor); + vm.expectRevert(IssuanceManager.ScripToCertMinimumNotMet.selector); + issuanceManager.convertScripToCert(address(certPrinter), 80*1e18); + + + _stageRecertificationApproval( + certPrinter, + investor, + "Guard Investor", + 555*1e18, + "Guard legal details", + bytes("guard extension") + ); + + // Successful conversion with expected amount + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 150*1e18); + + assertEq(ICyberScrip(scrip).balanceOf(investor), 51*1e18); + assertEq(certPrinter.totalSupply(), 2); + assertEq(certPrinter.ownerOf(1), investor); + CertificateDetails memory newCert = certPrinter.getCertificateDetails(1); + assertEq(newCert.unitsRepresented, 100 * 1e18); // 150 * 2 / 3 + } + + function test_convertScripToCert_ignoresVoidedCertAndMintsNewCertificate() + public + { + ICyberCertPrinter certPrinter = _deployPrinter("Voided Cert", "VCERT"); + + CertificateDetails memory original = CertificateDetails({ + signingOfficerName: "Alice Officer", + signingOfficerTitle: "General Counsel", + investmentAmountUSD: 3_000_000, + issuerUSDValuationAtTimeOfInvestment: 33_000_000, + unitsRepresented: 500 * 1e18, + legalDetails: "Original legal details", + extensionData: "Original extension" + }); + vm.prank(owner); + issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + original + ); + + // Mark existing cert as voided while investor still owns it. + issuanceManager.voidCertificate(address(certPrinter), 0); + assertTrue(certPrinter.isVoided(0)); + assertEq(certPrinter.ownerOf(0), investor); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 2, // ratio numerator + 1, // ratio denominator + new uint256[](0), + false, + true, + true, + true + ); + + uint256 sourceCertId = _mintCert(certPrinter, otherInvestor, 10); + vm.prank(otherInvestor); + issuanceManager.scripifyCert( + address(certPrinter), + sourceCertId, + 10 * 1e18, + investor + ); + vm.prank(investor); + vm.expectRevert(IssuanceManager.RecertificationApprovalRequired.selector); + issuanceManager.convertScripToCert(address(certPrinter), 20 * 1e18); + + CertificateDetails memory approvalDetails = _stageRecertificationApproval( + certPrinter, + investor, + "Reformed Investor", + 999, + "Fresh legal details", + bytes("Fresh extension") + ); + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 20 * 1e18); + + assertEq(certPrinter.totalSupply(), 3); + assertEq(certPrinter.ownerOf(0), investor); + assertEq(certPrinter.ownerOf(1), otherInvestor); + assertEq(certPrinter.ownerOf(2), investor); + assertTrue(certPrinter.isVoided(0)); + + CertificateDetails memory reformed = certPrinter.getCertificateDetails(2); + assertEq(reformed.unitsRepresented, 10 * 1e18); + assertEq(reformed.legalDetails, approvalDetails.legalDetails); + assertEq(reformed.extensionData, approvalDetails.extensionData); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + } + + function test_convertScripToCert_RequiresNativeRecertificationApproval() public { + ICyberCertPrinter certPrinter = _deployPrinter("Approval Cert", "APPR"); + uint256 certId = _mintCert(certPrinter, otherInvestor, 10); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(otherInvestor); + issuanceManager.scripifyCert(address(certPrinter), certId, 5 * 1e18, investor); + assertEq(ICyberScrip(scrip).balanceOf(investor), 5 * 1e18); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.RecertificationApprovalRequired.selector); + issuanceManager.convertScripToCert(address(certPrinter), 5 * 1e18); + + vm.prank(otherInvestor); + vm.expectRevert(); + issuanceManager.setRecertificationApproval( + address(certPrinter), + investor, + "Approved Investor", + _buildCertificateDetails(999, "Approved legal details", bytes("approved extension")) + ); + + CertificateDetails memory approvedDetails = _buildCertificateDetails( + 999, + "Approved legal details", + bytes("approved extension") + ); + vm.prank(owner); + issuanceManager.setRecertificationApproval( + address(certPrinter), + investor, + "Approved Investor", + approvedDetails + ); + ( + bool approved, + string memory investorName, + CertificateDetails memory stagedDetails + ) = issuanceManager.getRecertificationApproval( + address(certPrinter), + investor + ); + assertTrue(approved); + assertEq(investorName, "Approved Investor"); + assertEq(stagedDetails.legalDetails, approvedDetails.legalDetails); + assertEq(stagedDetails.extensionData, approvedDetails.extensionData); + + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 5 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(investor), 0); + assertEq(certPrinter.totalSupply(), 2); + assertEq(certPrinter.ownerOf(1), investor); + CertificateDetails memory restored = certPrinter.getCertificateDetails(1); + assertEq(restored.unitsRepresented, 5 * 1e18); + assertEq(restored.legalDetails, approvedDetails.legalDetails); + assertEq(restored.extensionData, approvedDetails.extensionData); + (approved,,) = issuanceManager.getRecertificationApproval( + address(certPrinter), + investor + ); + assertFalse(approved); + } + + function test_TwoHolders_ScripTransferThenRecertify_UpdatesUnitsAsExpected() + public + { + ICyberCertPrinter certPrinter = _deployPrinter("Shared Cert", "SHARE"); + uint256 investorCertId = _mintCert(certPrinter, investor, 100); + uint256 otherInvestorCertId = _mintCert(certPrinter, otherInvestor, 100); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(investor); + issuanceManager.scripifyCert( + address(certPrinter), + investorCertId, + 100 * 1e18, + address(0) + ); + vm.prank(otherInvestor); + issuanceManager.scripifyCert( + address(certPrinter), + otherInvestorCertId, + 100 * 1e18, + address(0) + ); + + CertificateDetails memory investorAfterScripify = certPrinter + .getActiveCertificateDetails(investorCertId); + CertificateDetails memory otherAfterScripify = certPrinter + .getActiveCertificateDetails(otherInvestorCertId); + assertEq(investorAfterScripify.unitsRepresented, 0); + assertEq(otherAfterScripify.unitsRepresented, 0); + assertEq(ICyberScrip(scrip).balanceOf(investor), 100 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(otherInvestor), 100 * 1e18); + + vm.prank(investor); + ICyberScrip(scrip).transfer(otherInvestor, 50 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(investor), 50 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(otherInvestor), 150 * 1e18); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), investorCertId), + 100 * 1e18 + ); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), otherInvestorCertId), + 100 * 1e18 + ); + assertEq( + issuanceManager.getScripPoolSharesById(address(certPrinter), investorCertId), + 100 * 1e18 + ); + assertEq( + issuanceManager.getScripPoolSharesById(address(certPrinter), otherInvestorCertId), + 100 * 1e18 + ); + + vm.expectEmit(true, true, true, true); + emit IssuanceManager.ScripAddedToExistingCert( + address(certPrinter), + otherInvestor, + otherInvestorCertId, + 0, + 150 * 1e18 + ); + vm.expectEmit(true, true, true, true); + emit IssuanceManager.ScripRecertified( + address(certPrinter), + otherInvestor, + otherInvestorCertId, + 150 * 1e18, + 0, + 150 * 1e18 + ); + vm.prank(otherInvestor); + issuanceManager.convertScripToCert(address(certPrinter), 150 * 1e18); + + CertificateDetails memory investorActiveFinal = certPrinter + .getActiveCertificateDetails(investorCertId); + CertificateDetails memory otherActiveFinal = certPrinter + .getActiveCertificateDetails(otherInvestorCertId); + CertificateDetails memory investorFinal = certPrinter.getCertificateDetails( + investorCertId + ); + CertificateDetails memory otherFinal = certPrinter.getCertificateDetails( + otherInvestorCertId + ); + (bool investorIsScripified, uint256 investorScripified,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), investorCertId); + (bool otherIsScripified, uint256 otherScripified,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), otherInvestorCertId); + + assertEq(investorActiveFinal.unitsRepresented, 0); + assertEq(otherActiveFinal.unitsRepresented, 150 * 1e18); + assertTrue(investorIsScripified); + assertEq(investorScripified, 50 * 1e18); + assertFalse(otherIsScripified); + assertEq(otherScripified, 0); + assertEq(investorFinal.unitsRepresented, 50 * 1e18); + assertEq(otherFinal.unitsRepresented, 150 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(investor), 50 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(otherInvestor), 0); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), investorCertId), + 50 * 1e18 + ); + assertEq( + issuanceManager.getScripPoolAmountById(address(certPrinter), otherInvestorCertId), + 0 + ); + assertEq( + issuanceManager.getScripPoolSharesById(address(certPrinter), investorCertId), + 100 * 1e18 + ); + assertEq( + issuanceManager.getScripPoolSharesById(address(certPrinter), otherInvestorCertId), + 0 + ); + } + + function test_ComplexScripPoolAccounting_FourHolders_MixedRecertificationsAndNewInvestors() + public + { + ICyberCertPrinter certPrinter = _deployPrinter("Four Holder Cert", "4CERT"); + address holderA = investor; + address holderB = otherInvestor; + address holderC = makeAddr("fourHolderC"); + address holderD = makeAddr("fourHolderD"); + address newInvestorOne = makeAddr("fourNewInvestorOne"); + address newInvestorTwo = makeAddr("fourNewInvestorTwo"); + + uint256 certIdA = _mintCert(certPrinter, holderA, 100); + uint256 certIdB = _mintCert(certPrinter, holderB, 100); + uint256 certIdC = _mintCert(certPrinter, holderC, 100); + uint256 certIdD = _mintCert(certPrinter, holderD, 100); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(holderA); + issuanceManager.scripifyCert(address(certPrinter), certIdA, 100 * 1e18, address(0)); + vm.prank(holderB); + issuanceManager.scripifyCert(address(certPrinter), certIdB, 100 * 1e18, address(0)); + vm.prank(holderC); + issuanceManager.scripifyCert(address(certPrinter), certIdC, 100 * 1e18, address(0)); + vm.prank(holderD); + issuanceManager.scripifyCert(address(certPrinter), certIdD, 100 * 1e18, address(0)); + + (uint256 totalTrackedScrip,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + assertEq(totalTrackedScrip, 400 * 1e18); + assertEq(ICyberScrip(scrip).totalSupply(), 400 * 1e18); + + vm.prank(holderA); + ICyberScrip(scrip).transfer(holderB, 40 * 1e18); + vm.prank(holderC); + ICyberScrip(scrip).transfer(newInvestorOne, 50 * 1e18); + vm.prank(holderD); + ICyberScrip(scrip).transfer(newInvestorTwo, 20 * 1e18); + + assertEq(ICyberScrip(scrip).balanceOf(holderA), 60 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderB), 140 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderC), 50 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderD), 80 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorOne), 50 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorTwo), 20 * 1e18); + + vm.prank(holderB); + issuanceManager.convertScripToCert(address(certPrinter), 120 * 1e18); + + //get and print all certificateDetails + CertificateDetails memory activeApre = certPrinter.getCertificateDetails( + certIdA + ); + CertificateDetails memory activeBpre = certPrinter.getCertificateDetails( + certIdB + ); + CertificateDetails memory activeCpre = certPrinter.getCertificateDetails( + certIdC + ); + CertificateDetails memory activeDpre = certPrinter.getCertificateDetails( + certIdD + ); + console.log("activeApre", activeApre.unitsRepresented); + console.log("activeBpre", activeBpre.unitsRepresented); + console.log("activeCpre", activeCpre.unitsRepresented); + console.log("activeDpre", activeDpre.unitsRepresented); + (totalTrackedScrip,) = issuanceManager.getScripPoolTotals(address(certPrinter)); + console.log("totalTrackedScrip", totalTrackedScrip); + (,uint scripA,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdA); + (, uint256 scripB,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdB); + (, uint256 scripC,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdC); + (, uint256 scripD,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdD); + console.log("scripA", scripA); + console.log("scripB", scripB); + console.log("scripC", scripC); + console.log("scripD", scripD); + (totalTrackedScrip,) = issuanceManager.getScripPoolTotals(address(certPrinter)); + console.log("totalTrackedScrip", totalTrackedScrip); + + vm.prank(holderC); + issuanceManager.convertScripToCert(address(certPrinter), 50 * 1e18); + + + //price active cert units: + (, scripA,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdA); + (, scripB,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdB); + (, scripC,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdC); + (, scripD,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdD); + console.log("scripA", scripA); + console.log("scripB", scripB); + console.log("scripC", scripC); + console.log("scripD", scripD); + (totalTrackedScrip,) = issuanceManager.getScripPoolTotals(address(certPrinter)); + console.log("totalTrackedScrip", totalTrackedScrip); + + //print cert units again + + activeApre = certPrinter.getCertificateDetails(certIdA); + activeBpre = certPrinter.getCertificateDetails(certIdB); + activeCpre = certPrinter.getCertificateDetails(certIdC); + activeDpre = certPrinter.getCertificateDetails(certIdD); + console.log("activeApost", activeApre.unitsRepresented); + console.log("activeBpost", activeBpre.unitsRepresented); + console.log("activeCpost", activeCpre.unitsRepresented); + console.log("activeDpost", activeDpre.unitsRepresented); + + + CertificateDetails memory approvalOne = _stageRecertificationApproval( + certPrinter, + newInvestorOne, + "Four New Investor One", + 50, + "Four new investor one legal details", + bytes("four-new-investor-one-extension") + ); + CertificateDetails memory approvalTwo = _stageRecertificationApproval( + certPrinter, + newInvestorTwo, + "Four New Investor Two", + 20, + "Four new investor two legal details", + bytes("four-new-investor-two-extension") + ); + + vm.prank(newInvestorOne); + issuanceManager.convertScripToCert(address(certPrinter), 50 * 1e18); + + activeApre = certPrinter.getCertificateDetails(certIdA); + activeBpre = certPrinter.getCertificateDetails(certIdB); + activeCpre = certPrinter.getCertificateDetails(certIdC); + activeDpre = certPrinter.getCertificateDetails(certIdD); + //add the new cert e + CertificateDetails memory newCertOnea = certPrinter.getCertificateDetails(4); + console.log("activeApost", activeApre.unitsRepresented); + console.log("activeBpost", activeBpre.unitsRepresented); + console.log("activeCpost", activeCpre.unitsRepresented); + console.log("activeDpost", activeDpre.unitsRepresented); + console.log("newCertOne", newCertOnea.unitsRepresented); + vm.prank(newInvestorTwo); + issuanceManager.convertScripToCert(address(certPrinter), 20 * 1e18); + //add the new cert f + CertificateDetails memory newCertTwoa = certPrinter.getCertificateDetails(5); + activeApre = certPrinter.getCertificateDetails(certIdA); + activeBpre = certPrinter.getCertificateDetails(certIdB); + activeCpre = certPrinter.getCertificateDetails(certIdC); + activeDpre = certPrinter.getCertificateDetails(certIdD); + newCertOnea = certPrinter.getCertificateDetails(4); + newCertTwoa = certPrinter.getCertificateDetails(5); + console.log("activeAfin", activeApre.unitsRepresented); + console.log("activeBpost", activeBpre.unitsRepresented); + console.log("activeCpost", activeCpre.unitsRepresented); + console.log("activeDpost", activeDpre.unitsRepresented); + console.log("newCertOne", newCertOnea.unitsRepresented); + console.log("newCertTwo", newCertTwoa.unitsRepresented); + + (totalTrackedScrip,) = issuanceManager.getScripPoolTotals(address(certPrinter)); + assertEq(totalTrackedScrip, 160 * 1e18); + assertEq(ICyberScrip(scrip).totalSupply(), 160 * 1e18); + + CertificateDetails memory activeA = certPrinter.getActiveCertificateDetails( + certIdA + ); + CertificateDetails memory activeB = certPrinter.getActiveCertificateDetails( + certIdB + ); + CertificateDetails memory activeC = certPrinter.getActiveCertificateDetails( + certIdC + ); + CertificateDetails memory activeD = certPrinter.getActiveCertificateDetails( + certIdD + ); + CertificateDetails memory activeNewOne = certPrinter + .getActiveCertificateDetails(4); + CertificateDetails memory activeNewTwo = certPrinter + .getActiveCertificateDetails(5); + + assertEq(activeA.unitsRepresented, 0); + assertEq(activeB.unitsRepresented, 120 * 1e18); + assertEq(activeC.unitsRepresented, 50 * 1e18); + assertEq(activeD.unitsRepresented, 0); + assertEq(activeNewOne.unitsRepresented, 50 * 1e18); + assertEq(activeNewTwo.unitsRepresented, 20 * 1e18); + + (bool isScripifiedA, uint256 scripifiedA,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdA); + (bool isScripifiedB, uint256 scripifiedB,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdB); + (bool isScripifiedC, uint256 scripifiedC,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdC); + (bool isScripifiedD, uint256 scripifiedD,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdD); + (bool isScripifiedNewOne, uint256 scripifiedNewOne,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), 4); + (bool isScripifiedNewTwo, uint256 scripifiedNewTwo,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), 5); + + assertTrue(isScripifiedA); + assertFalse(isScripifiedB); + assertTrue(isScripifiedC); + assertTrue(isScripifiedD); + assertFalse(isScripifiedNewOne); + assertFalse(isScripifiedNewTwo); + + CertificateDetails memory effectiveA = certPrinter.getCertificateDetails( + certIdA + ); + CertificateDetails memory effectiveB = certPrinter.getCertificateDetails( + certIdB + ); + CertificateDetails memory effectiveC = certPrinter.getCertificateDetails( + certIdC + ); + CertificateDetails memory effectiveD = certPrinter.getCertificateDetails( + certIdD + ); + CertificateDetails memory newCertOne = certPrinter.getCertificateDetails(4); + CertificateDetails memory newCertTwo = certPrinter.getCertificateDetails(5); + + assertApproxEqAbs(effectiveA.unitsRepresented, scripifiedA, 1); + assertApproxEqAbs( + effectiveB.unitsRepresented, + (120 * 1e18) + scripifiedB, + 1 + ); + assertApproxEqAbs( + effectiveC.unitsRepresented, + (50 * 1e18) + scripifiedC, + 1 + ); + assertApproxEqAbs(effectiveD.unitsRepresented, scripifiedD, 1); + assertEq(newCertOne.unitsRepresented, 50 * 1e18); + assertEq(newCertTwo.unitsRepresented, 20 * 1e18); + assertEq(newCertOne.legalDetails, approvalOne.legalDetails); + assertEq(newCertOne.extensionData, approvalOne.extensionData); + assertEq(newCertTwo.legalDetails, approvalTwo.legalDetails); + assertEq(newCertTwo.extensionData, approvalTwo.extensionData); + assertEq(certPrinter.ownerOf(4), newInvestorOne); + assertEq(certPrinter.ownerOf(5), newInvestorTwo); + assertEq(certPrinter.legalOwnerOf(4), newInvestorOne); + assertEq(certPrinter.legalOwnerOf(5), newInvestorTwo); + + assertEq(ICyberScrip(scrip).balanceOf(holderA), 60 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderB), 20 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderC), 0); + assertEq(ICyberScrip(scrip).balanceOf(holderD), 80 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorOne), 0); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorTwo), 0); + + uint256 totalActiveWad = activeA.unitsRepresented + + activeB.unitsRepresented + + activeC.unitsRepresented + + activeD.unitsRepresented + + activeNewOne.unitsRepresented + + activeNewTwo.unitsRepresented; + uint256 totalScripifiedWad = scripifiedA + + scripifiedB + + scripifiedC + + scripifiedD + + scripifiedNewOne + + scripifiedNewTwo; + // Per-cert claims use integer division (floor); summing (scrip/wad)/1e18 per cert truncates + // again and can under-count vs vault. Sum wads first — multi-step fixed-point can differ by ≤1 wei. + (uint256 vaultAssetsWad,) = issuanceManager.getCertScripUnitVault( + address(certPrinter) + ); + assertApproxEqAbs( + totalScripifiedWad, + vaultAssetsWad, + 1, + "scripified wad sum vs vault totalAssetsWad" + ); + assertApproxEqAbs( + totalActiveWad + totalScripifiedWad, + 400e18, + 1, + "active + scripified units vs pool cap" + ); + + (bool approvalStillSetOne,,) = issuanceManager.getRecertificationApproval( + address(certPrinter), + newInvestorOne + ); + (bool approvalStillSetTwo,,) = issuanceManager.getRecertificationApproval( + address(certPrinter), + newInvestorTwo + ); + assertFalse(approvalStillSetOne); + assertFalse(approvalStillSetTwo); + } + + function test_ComplexScripPoolAccounting_FiveHolders_MixedRecertifications() + public + { + ICyberCertPrinter certPrinter = _deployPrinter("Complex Cert", "CCERT"); + address holderA = investor; + address holderB = otherInvestor; + address holderC = makeAddr("holderC"); + address holderD = makeAddr("holderD"); + address holderE = makeAddr("holderE"); + address newInvestorOne = makeAddr("newInvestorOne"); + address newInvestorTwo = makeAddr("newInvestorTwo"); + + uint256 certIdA = _mintCert(certPrinter, holderA, 100); + uint256 certIdB = _mintCert(certPrinter, holderB, 100); + uint256 certIdC = _mintCert(certPrinter, holderC, 100); + uint256 certIdD = _mintCert(certPrinter, holderD, 100); + uint256 certIdE = _mintCert(certPrinter, holderE, 100); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + vm.prank(holderA); + issuanceManager.scripifyCert(address(certPrinter), certIdA, 100 * 1e18, address(0)); + vm.prank(holderB); + issuanceManager.scripifyCert(address(certPrinter), certIdB, 100 * 1e18, address(0)); + vm.prank(holderC); + issuanceManager.scripifyCert(address(certPrinter), certIdC, 100 * 1e18, address(0)); + vm.prank(holderD); + issuanceManager.scripifyCert(address(certPrinter), certIdD, 100 * 1e18, address(0)); + vm.prank(holderE); + issuanceManager.scripifyCert(address(certPrinter), certIdE, 100 * 1e18, address(0)); + + (uint256 totalTrackedScrip,) = issuanceManager.getScripPoolTotals( + address(certPrinter) + ); + assertEq(totalTrackedScrip, 500 * 1e18); + assertEq(ICyberScrip(scrip).totalSupply(), 500 * 1e18); + + vm.prank(holderA); + ICyberScrip(scrip).transfer(newInvestorOne, 40 * 1e18); + vm.prank(holderD); + ICyberScrip(scrip).transfer(holderB, 20 * 1e18); + vm.prank(holderE); + ICyberScrip(scrip).transfer(holderB, 20 * 1e18); + vm.prank(holderE); + ICyberScrip(scrip).transfer(newInvestorOne, 20 * 1e18); + vm.prank(holderC); + ICyberScrip(scrip).transfer(newInvestorTwo, 20 * 1e18); + + assertEq(ICyberScrip(scrip).balanceOf(holderA), 60 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderB), 140 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderC), 80 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderD), 80 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderE), 60 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorOne), 60 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorTwo), 20 * 1e18); + + vm.prank(holderB); + issuanceManager.convertScripToCert(address(certPrinter), 140 * 1e18); + + vm.prank(holderC); + issuanceManager.convertScripToCert(address(certPrinter), 80 * 1e18); + + vm.prank(holderD); + issuanceManager.convertScripToCert(address(certPrinter), 80 * 1e18); + + CertificateDetails memory approvalOne = _stageRecertificationApproval( + certPrinter, + newInvestorOne, + "New Investor One", + 60, + "New investor one legal details", + bytes("new-investor-one-extension") + ); + CertificateDetails memory approvalTwo = _stageRecertificationApproval( + certPrinter, + newInvestorTwo, + "New Investor Two", + 20, + "New investor two legal details", + bytes("new-investor-two-extension") + ); + + vm.prank(newInvestorOne); + issuanceManager.convertScripToCert(address(certPrinter), 60 * 1e18); + vm.prank(newInvestorTwo); + issuanceManager.convertScripToCert(address(certPrinter), 20 * 1e18); + + (totalTrackedScrip,) = issuanceManager.getScripPoolTotals(address(certPrinter)); + assertEq(totalTrackedScrip, 120 * 1e18); + assertEq(ICyberScrip(scrip).totalSupply(), 120 * 1e18); + assertEq(certPrinter.totalSupply(), 7); + + CertificateDetails memory activeA = certPrinter.getActiveCertificateDetails( + certIdA + ); + CertificateDetails memory activeB = certPrinter.getActiveCertificateDetails( + certIdB + ); + CertificateDetails memory activeC = certPrinter.getActiveCertificateDetails( + certIdC + ); + CertificateDetails memory activeD = certPrinter.getActiveCertificateDetails( + certIdD + ); + CertificateDetails memory activeE = certPrinter.getActiveCertificateDetails( + certIdE + ); + CertificateDetails memory activeNewOne = certPrinter + .getActiveCertificateDetails(5); + CertificateDetails memory activeNewTwo = certPrinter + .getActiveCertificateDetails(6); + + assertEq(activeA.unitsRepresented, 0); + assertEq(activeB.unitsRepresented, 140 * 1e18); + assertEq(activeC.unitsRepresented, 80 * 1e18); + assertEq(activeD.unitsRepresented, 80 * 1e18); + assertEq(activeE.unitsRepresented, 0); + assertEq(activeNewOne.unitsRepresented, 60 * 1e18); + assertEq(activeNewTwo.unitsRepresented, 20 * 1e18); + + (bool isScripifiedA, uint256 scripifiedA,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdA); + (bool isScripifiedB, uint256 scripifiedB,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdB); + (bool isScripifiedC, uint256 scripifiedC,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdC); + (bool isScripifiedD, uint256 scripifiedD,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdD); + (bool isScripifiedE, uint256 scripifiedE,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), certIdE); + (bool isScripifiedNewOne, uint256 scripifiedNewOne,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), 5); + (bool isScripifiedNewTwo, uint256 scripifiedNewTwo,) = issuanceManager + .getCertScripifiedStatus(address(certPrinter), 6); + + assertTrue(isScripifiedA); + assertEq(scripifiedA, 54 * 1e18); + assertFalse(isScripifiedB); + assertEq(scripifiedB, 0); + assertTrue(isScripifiedC); + // Vault share→asset conversion can floor nominal claims by ≤1 unit vs naive expectations + assertApproxEqAbs( + scripifiedC, + 6 * 1e18, + 1 * 1e18, + "holder C scripified wad (rounding)" + ); + assertTrue(isScripifiedD); + assertApproxEqAbs( + scripifiedD, + 6 * 1e18, + 1 * 1e18, + "holder D scripified wad (rounding)" + ); + assertTrue(isScripifiedE); + assertEq(scripifiedE, 54 * 1e18); + assertFalse(isScripifiedNewOne); + assertEq(scripifiedNewOne, 0); + assertFalse(isScripifiedNewTwo); + assertEq(scripifiedNewTwo, 0); + + CertificateDetails memory effectiveA = certPrinter.getCertificateDetails( + certIdA + ); + CertificateDetails memory effectiveB = certPrinter.getCertificateDetails( + certIdB + ); + CertificateDetails memory effectiveC = certPrinter.getCertificateDetails( + certIdC + ); + CertificateDetails memory effectiveD = certPrinter.getCertificateDetails( + certIdD + ); + CertificateDetails memory effectiveE = certPrinter.getCertificateDetails( + certIdE + ); + CertificateDetails memory newCertOne = certPrinter.getCertificateDetails(5); + CertificateDetails memory newCertTwo = certPrinter.getCertificateDetails(6); + + // Effective details are active + scripified vault claim (full wad). Compare in wad + // space — do not divide by 1e18 first (that floors whole units and caused 85 vs 86). + uint256 wadRoundingTol = 100 * 1e9; // 100 gwei + assertApproxEqAbs( + effectiveA.unitsRepresented, + activeA.unitsRepresented + scripifiedA, + wadRoundingTol, + "effective A == active + scripified (wad)" + ); + assertApproxEqAbs( + effectiveB.unitsRepresented, + activeB.unitsRepresented + scripifiedB, + wadRoundingTol, + "effective B == active + scripified (wad)" + ); + assertApproxEqAbs( + effectiveC.unitsRepresented, + activeC.unitsRepresented + scripifiedC, + wadRoundingTol, + "effective C == active + scripified (wad)" + ); + assertApproxEqAbs( + effectiveD.unitsRepresented, + activeD.unitsRepresented + scripifiedD, + wadRoundingTol, + "effective D == active + scripified (wad)" + ); + assertApproxEqAbs( + effectiveE.unitsRepresented, + activeE.unitsRepresented + scripifiedE, + wadRoundingTol, + "effective E == active + scripified (wad)" + ); + assertApproxEqAbs( + newCertOne.unitsRepresented, + activeNewOne.unitsRepresented + scripifiedNewOne, + wadRoundingTol, + "new cert one effective (wad)" + ); + assertApproxEqAbs( + newCertTwo.unitsRepresented, + activeNewTwo.unitsRepresented + scripifiedNewTwo, + wadRoundingTol, + "new cert two effective (wad)" + ); + assertEq(newCertOne.legalDetails, approvalOne.legalDetails); + assertEq(newCertOne.extensionData, approvalOne.extensionData); + assertEq(newCertTwo.legalDetails, approvalTwo.legalDetails); + assertEq(newCertTwo.extensionData, approvalTwo.extensionData); + assertEq(certPrinter.ownerOf(5), newInvestorOne); + assertEq(certPrinter.ownerOf(6), newInvestorTwo); + assertEq(certPrinter.legalOwnerOf(5), newInvestorOne); + assertEq(certPrinter.legalOwnerOf(6), newInvestorTwo); + + assertEq(ICyberScrip(scrip).balanceOf(holderA), 60 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(holderB), 0); + assertEq(ICyberScrip(scrip).balanceOf(holderC), 0); + assertEq(ICyberScrip(scrip).balanceOf(holderD), 0); + assertEq(ICyberScrip(scrip).balanceOf(holderE), 60 * 1e18); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorOne), 0); + assertEq(ICyberScrip(scrip).balanceOf(newInvestorTwo), 0); + + uint256 totalActiveWadFive = activeA.unitsRepresented + + activeB.unitsRepresented + + activeC.unitsRepresented + + activeD.unitsRepresented + + activeE.unitsRepresented + + activeNewOne.unitsRepresented + + activeNewTwo.unitsRepresented; + uint256 totalScripifiedWadFive = scripifiedA + + scripifiedB + + scripifiedC + + scripifiedD + + scripifiedE + + scripifiedNewOne + + scripifiedNewTwo; + (uint256 vaultAssetsWadFive,) = issuanceManager.getCertScripUnitVault( + address(certPrinter) + ); + // More holders / conversions → slightly larger aggregated rounding vs vault assets + uint256 fiveHolderWadTol = 3; + assertApproxEqAbs( + totalScripifiedWadFive, + vaultAssetsWadFive, + fiveHolderWadTol, + "scripified wad sum vs vault totalAssetsWad (five holders)" + ); + assertApproxEqAbs( + totalActiveWadFive + totalScripifiedWadFive, + 500e18, + fiveHolderWadTol, + "active + scripified units vs pool cap (five holders)" + ); + + (bool approvalStillSetOne,,) = issuanceManager.getRecertificationApproval( + address(certPrinter), + newInvestorOne + ); + (bool approvalStillSetTwo,,) = issuanceManager.getRecertificationApproval( + address(certPrinter), + newInvestorTwo + ); + assertFalse(approvalStillSetOne); + assertFalse(approvalStillSetTwo); + } + + function _deployPrinter( + string memory name, + string memory symbol + ) internal returns (ICyberCertPrinter certPrinter) { + certPrinter = ICyberCertPrinter( + issuanceManager.createCertPrinter( + new string[](0), + name, + symbol, + "uri://cert", + SecurityClass.CommonStock, + SecuritySeries.SeriesA, + address(0) + ) + ); + } + + function _mintCert( + ICyberCertPrinter certPrinter, + address to, + uint256 units + ) internal returns (uint256 tokenId) { + CertificateDetails memory details = _buildCertificateDetails( + units, + "", + bytes("") + ); + vm.prank(owner); + tokenId = issuanceManager.createCertAndAssign( + address(certPrinter), + to, + details + ); + } + + function _stageRecertificationApproval( + ICyberCertPrinter certPrinter, + address investorAddress, + string memory investorName, + uint256 units, + string memory legalDetails, + bytes memory extensionData + ) internal returns (CertificateDetails memory details) { + details = _buildCertificateDetails(units, legalDetails, extensionData); + vm.prank(owner); + issuanceManager.setRecertificationApproval( + address(certPrinter), + investorAddress, + investorName, + details + ); + } + + function _buildCertificateDetails( + uint256 units, + string memory legalDetails, + bytes memory extensionData + ) internal pure returns (CertificateDetails memory details) { + details = CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + // Store units in 18-decimal precision internally + unitsRepresented: units * 1e18, + legalDetails: legalDetails, + extensionData: extensionData + }); + } } diff --git a/test/LegalOwnerBugPOC.t.sol b/test/LegalOwnerBugPOC.t.sol new file mode 100644 index 00000000..de4c7c41 --- /dev/null +++ b/test/LegalOwnerBugPOC.t.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import "forge-std/Test.sol"; +import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import "../src/IssuanceManager.sol"; +import "../src/IssuanceManagerFactory.sol"; +import "../src/CyberCertPrinter.sol"; +import "../src/CyberScrip.sol"; +import "../src/interfaces/ICyberCertPrinter.sol"; +import "../src/interfaces/ITransferRestrictionHook.sol"; +import "../src/interfaces/ICondition.sol"; +import "../src/interfaces/IUriBuilder.sol"; +import "../src/libs/auth.sol"; + +contract LegalOwnerMockCyberCorp { + function cyberCORPName() external pure returns (string memory) { + return "MockCorp"; + } + + function cyberCORPType() external pure returns (string memory) { + return "C-Corp"; + } + + function cyberCORPJurisdiction() external pure returns (string memory) { + return "DE"; + } + + function cyberCORPContactDetails() external pure returns (string memory) { + return "mock@corp.test"; + } + + function dealManager() external pure returns (address) { + return address(0xD34D); + } + + function roundManager() external pure returns (address) { + return address(0xB0B0); + } +} + +contract LegalOwnerMockUriBuilder is IUriBuilder { + function buildCertificateUri( + string memory, + string memory, + string memory, + string memory, + SecurityClass, + SecuritySeries, + string memory, + string[] memory, + CertificateDetails memory, + Endorsement[] memory, + OwnerDetails memory, + address, + bytes32, + uint256, + address, + address + ) external pure returns (string memory) { + return "uri://mock"; + } + + function buildCertificateUriNotEncoded( + string memory, + string memory, + string memory, + string memory, + SecurityClass, + SecuritySeries, + string memory, + string[] memory, + CertificateDetails memory, + Endorsement[] memory, + OwnerDetails memory, + address, + bytes32, + uint256, + address, + address + ) external pure returns (string memory) { + return "uri://mock"; + } +} + +contract LegalOwnerBugPOCTest is Test { + bytes32 internal constant SALT = bytes32(keccak256("LegalOwnerBugPOC")); + + IssuanceManager internal issuanceManager; + ICyberCertPrinter internal certPrinter; + BorgAuth internal auth; + + address internal owner; + address internal investor; + + function setUp() public { + owner = address(this); + investor = makeAddr("investor"); + + auth = new BorgAuth(owner); + + IssuanceManagerFactory factory = IssuanceManagerFactory( + address( + new ERC1967Proxy( + address(new IssuanceManagerFactory()), + abi.encodeWithSelector( + IssuanceManagerFactory.initialize.selector, + address(auth), + new IssuanceManager(), + new CyberCertPrinter(), + new CyberScrip() + ) + ) + ) + ); + + issuanceManager = IssuanceManager(factory.deployIssuanceManager(SALT)); + issuanceManager.initialize( + address(auth), + address(new LegalOwnerMockCyberCorp()), + address(new LegalOwnerMockUriBuilder()), + address(factory) + ); + + certPrinter = ICyberCertPrinter( + issuanceManager.createCertPrinter( + new string[](0), + "Legal Owner Cert", + "LOC", + "uri://cert", + SecurityClass.CommonStock, + SecuritySeries.SeriesA, + address(0) + ) + ); + } + + function test_POC_CreateCert_MintsNftButLeavesLegalOwnerUnset() public { + uint256 certId = issuanceManager.createCert( + address(certPrinter), + investor, + _details(10) + ); + + assertEq(certPrinter.ownerOf(certId), investor, "ERC721 owner should be investor"); + assertEq( + certPrinter.legalOwnerOf(certId), + address(0), + "legal owner mapping is never initialized on mint" + ); + } + + function test_POC_ScripifyRevertsBecauseLegalOwnerIsZeroAddress() public { + uint256 certId = issuanceManager.createCert( + address(certPrinter), + investor, + _details(10) + ); + + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, + 1, + new uint256[](0), + false, + true, + true, + true + ); + + assertEq(certPrinter.ownerOf(certId), investor, "ERC721 owner should be investor"); + assertEq(certPrinter.legalOwnerOf(certId), address(0), "legal owner should still be unset"); + + vm.prank(investor); + vm.expectRevert(IssuanceManager.ConditionCheckFailed.selector); + issuanceManager.scripifyCert(address(certPrinter), certId, 1, address(0)); + } + + function test_POC_CreateCertAndAssign_AlsoLeavesLegalOwnerUnset() public { + uint256 certId = issuanceManager.createCertAndAssign( + address(certPrinter), + investor, + _details(5) + ); + + assertEq(certPrinter.ownerOf(certId), investor, "ERC721 owner should be investor"); + assertEq( + certPrinter.legalOwnerOf(certId), + address(0), + "recert mint path also leaves legal owner unset" + ); + } + + function _details(uint256 units) internal pure returns (CertificateDetails memory) { + return CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: units, + legalDetails: "", + extensionData: "" + }); + } +} diff --git a/test/LexChexMinterTest.t.sol b/test/LexChexMinterTest.t.sol index 39933925..1b0181f9 100644 --- a/test/LexChexMinterTest.t.sol +++ b/test/LexChexMinterTest.t.sol @@ -572,6 +572,52 @@ contract LexChexMinterTest is Test { assertEq(paymentToken.balanceOf(treasury) - treasuryPaymentBalanceBefore, 10e6, "treasury received amount is not correct"); } + function testPOC_RequestRenewal_SignatureSubjectMismatchReverts() public { + // Mint tokenId=0 for user1 first. + testRequestMint(); + + uint256 originalExpiry = lexchex.accreditations(0).expiryDate; + + // Authority signs renewal for user2, not the token owner (user1). + request = LeXcheXMinter.MintRequest({ + uuid: 999, + owner: user2, + investorName: "Different Subject", + investorType: "DAO", + investorJurisdiction: "Cayman", + investorContact: "user2@test.com", + mintPrice: 0, + expiry: block.timestamp + 30 days, + paymentToken: address(paymentToken) + }); + + authoritySignature = LeXcheXUtils.signAuthorizationTypedData( + vm, + lexchexMinter.DOMAIN_SEPARATOR(), + lexchexMinter.AUTHORITY_TYPEHASH(), + LeXcheXMinter.AuthorityData({ + uuid: request.uuid, + owner: request.owner, + investorName: request.investorName, + investorType: request.investorType, + investorJurisdiction: request.investorJurisdiction, + investorContact: request.investorContact, + mintPrice: request.mintPrice, + expiry: request.expiry, + paymentToken: request.paymentToken + }), + authorityPrivateKey + ); + + // Renewal must now fail because request.owner does not own tokenId=0. + vm.expectRevert(abi.encodeWithSelector(LeXcheXMinter.RequestOwnerMismatch.selector)); + vm.prank(agent); + lexchexMinter.requestRenewal(request, 0, authoritySignature); + + assertEq(lexchex.ownerOf(0), user1, "token owner is still user1"); + assertEq(lexchex.accreditations(0).expiryDate, originalExpiry, "expiry should remain unchanged"); + } + function test_RevertIf_RequestRenewalInvalidAuthoritySignature() public { // Mint a token first testRequestMint(); diff --git a/test/MetaDAOTest.t.sol b/test/MetaDAOTest.t.sol index d21e0331..e69de29b 100644 --- a/test/MetaDAOTest.t.sol +++ b/test/MetaDAOTest.t.sol @@ -1,513 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import {DeployMetaDAOFactoryScript} from "../script/deploy-metadao-factory.s.sol"; -import {MetaDAOFactory} from "../src/MetaDAOFactory.sol"; -import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; -import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; -import {IssuanceManager} from "../src/IssuanceManager.sol"; -import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; -import {CyberCorp} from "../src/CyberCorp.sol"; -import {DealManagerFactory, DealManager} from "../src/DealManagerFactory.sol"; -import {RoundManagerFactory, RoundManager} from "../src/RoundManagerFactory.sol"; -import {CertificateUriBuilder} from "../src/CertificateUriBuilder.sol"; -import {CyberCertPrinter} from "../src/CyberCertPrinter.sol"; -import {CyberScrip} from "../src/CyberScrip.sol"; -import {BorgAuth} from "../src/libs/auth.sol"; -import {ERC1967Proxy} from "../dependencies/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {ERC20} from "../dependencies/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {CyberAgreementUtils} from "./libs/CyberAgreementUtils.sol"; -import {ICyberAgreementRegistry} from "../src/interfaces/ICyberAgreementRegistry.sol"; -import "../src/storage/CyberCertPrinterStorage.sol"; // CertificateDetails -import {CompanyOfficer, SecurityClass, SecuritySeries} from "../src/CyberCorpConstants.sol"; - -contract MockPaymentToken is ERC20 { - constructor() ERC20("Mock USDC", "USDC") { - _mint(msg.sender, 1_000_000 * 10 ** 6); - } - - function decimals() public pure override returns (uint8) { - return 6; - } -} - -contract MetaDAOTest is Test { - uint256 private deployerPrivKey; - address private deployer; - uint256 private metaDAOPrivKey; - address private metaDAO; - uint256 private founderPrivKey; - address private founder; - uint256 private alicePrivKey; - address private alice; - - CyberAgreementRegistry public registry; - MetaDAOFactory public factory; - - string public segCoTemplateTitle = "MetaDAO Futarchy Governance SPC - SegCo combined v 1.0"; - bytes32 public segCoTemplateId = keccak256(bytes(segCoTemplateTitle)); - string public segCoTemplateUri = "ipfs://bafybeifpvfwxfmobk7nhflsczqiynp3ca5urvyk3duh7s3rwptcnfzhuje"; - - string public boardConsentTemplateTitle = "MetaDAO Futarchy Governance SPC - Board Consent - Approval of SegCo v 1.0"; - bytes32 public boardConsentTemplateId = keccak256(bytes(boardConsentTemplateTitle)); - string public boardConsentTemplateUri = "ipfs://bafkreic7dscoigvwjc23vzvkmzophm34kpafu6nrctykq5bif63lqvpuoa"; - - string[] public globalFields; - string[] public partyFields; - - function setUp() public { - // Keys - (deployer, deployerPrivKey) = makeAddrAndKey("deployer"); - (metaDAO, metaDAOPrivKey) = makeAddrAndKey("metaDAO"); - (founder, founderPrivKey) = makeAddrAndKey("founder"); - (alice, alicePrivKey) = makeAddrAndKey("alice"); - - // Deploy MetaDAO factories with production scripts - (registry, factory) = (new DeployMetaDAOFactoryScript()).run( - deployerPrivKey, // deployerPrivateKey - metaDAO, // multisig - hex"63f62ac9b08c813401a02a16a820a106e525ac65dff992dccfd2cb42e5423db6725bb1b4d6e0244a635665f4965514512253613e3b032491f7ec85c2f657154e1b" // metadaoEscrowSig - ); - - globalFields = new string[](8); - globalFields[0] = "founderName"; - globalFields[1] = "enterpriseName"; - globalFields[2] = "companyName"; - globalFields[3] = "companyType"; - globalFields[4] = "companyJurisdiction"; - globalFields[5] = "companyContactDetails"; - globalFields[6] = "tokenSymbol"; - globalFields[7] = "tokenName"; - partyFields = new string[](2); - partyFields[0] = "name"; - partyFields[1] = "contactDetails"; - } - - function test_metadata() public { - // Verify template for SegCo combined agreement - { - ( - string memory legalContractUri, - string memory title, - string[] memory _globalFields, - string[] memory _partyFields - ) = registry.getTemplateDetails(segCoTemplateId); - assertEq(legalContractUri, segCoTemplateUri); - assertEq(title, segCoTemplateTitle); - assertEq(_globalFields, globalFields); - assertEq(_partyFields, partyFields); - } - - // Verify template for Board Consent - { - ( - string memory legalContractUri, - string memory title, - string[] memory _globalFields, - string[] memory _partyFields - ) = registry.getTemplateDetails(boardConsentTemplateId); - assertEq(legalContractUri, boardConsentTemplateUri); - assertEq(title, boardConsentTemplateTitle); - assertEq(_globalFields, globalFields); - assertEq(_partyFields, partyFields); - } - } - - function test_deployMetaDAOContractFor() public { - // Parties and values - string[] memory globalValues = _getDefaultGlobalValues(); - string[][] memory partyValues = _getDefaultPartyValues(); - - // Compute agreementId and signer signature for deployer (who will sign) - uint256 saltUint = 1; - // Parties must match what factory will set: [metaDAOOfficer.eoa, deployer] - address[] memory parties = new address[](2); - parties[0] = metaDAO; - parties[1] = founder; - bytes32 contractId = keccak256(abi.encode(segCoTemplateId, saltUint, globalValues, parties)); - bytes memory signature = CyberAgreementUtils.signAgreementTypedData( - vm, - registry.DOMAIN_SEPARATOR(), - registry.SIGNATUREDATA_TYPEHASH(), - contractId, - segCoTemplateUri, - globalFields, - partyFields, - globalValues, - partyValues[1], - founderPrivKey - ); - - vm.startPrank(founder); - factory.deployMetaDAOContractFor( - saltUint, - "testcorp S.P., a segregated portfolio of Futarchy Governance SPC", - "Segregated Portfolio of Segregated Portfolio Company", - "Cayman Islands", - "email@testcorp.com", - "arbitration", - founder, - _getDefaultCorpOfficer(), - segCoTemplateId, - boardConsentTemplateId, - globalValues, - partyValues[1], - signature, - founder - ); - vm.stopPrank(); - - address[] memory meetingNotesParties = new address[](1); - meetingNotesParties[0] = metaDAO; - _verifyContractsDefault( - contractId, - keccak256(abi.encode(boardConsentTemplateId, saltUint, globalValues, meetingNotesParties)) - ); - } - - function test_deployMetaDAOContractForOnBehalf() public { - // Parties and values - string[] memory globalValues = _getDefaultGlobalValues(); - string[][] memory partyValues = _getDefaultPartyValues(); - - // Compute agreementId and signer signature for deployer (who will sign) - uint256 saltUint = 1; - // Parties must match what factory will set: [metaDAOOfficer.eoa, deployer] - address[] memory parties = new address[](2); - parties[0] = metaDAO; - parties[1] = founder; - bytes32 contractId = keccak256(abi.encode(segCoTemplateId, saltUint, globalValues, parties)); - bytes memory signature = CyberAgreementUtils.signAgreementTypedData( - vm, - registry.DOMAIN_SEPARATOR(), - registry.SIGNATUREDATA_TYPEHASH(), - contractId, - segCoTemplateUri, - globalFields, - partyFields, - globalValues, - partyValues[1], - founderPrivKey - ); - - // deploy on behalf of founder should also work as long as the signature is correct - vm.startPrank(alice); - factory.deployMetaDAOContractFor( - saltUint, - "testcorp S.P., a segregated portfolio of Futarchy Governance SPC", - "Segregated Portfolio of Segregated Portfolio Company", - "Cayman Islands", - "email@testcorp.com", - "arbitration", - founder, - _getDefaultCorpOfficer(), - segCoTemplateId, - boardConsentTemplateId, - globalValues, - partyValues[1], - signature, - founder - ); - vm.stopPrank(); - - address[] memory meetingNotesParties = new address[](1); - meetingNotesParties[0] = metaDAO; - _verifyContractsDefault( - contractId, - keccak256(abi.encode(boardConsentTemplateId, saltUint, globalValues, meetingNotesParties)) - ); - } - - function test_RevertIf_deployMetaDAOContractForWrongSignature() public { - // Parties and values - string[] memory globalValues = _getDefaultGlobalValues(); - string[][] memory partyValues = _getDefaultPartyValues(); - - // Compute agreementId and signer signature for deployer (who will sign) - uint256 saltUint = 1; - // Parties must match what factory will set: [metaDAOOfficer.eoa, deployer] - address[] memory parties = new address[](2); - parties[0] = metaDAO; - parties[1] = founder; - bytes32 contractId = keccak256(abi.encode(segCoTemplateId, saltUint, globalValues, parties)); - bytes memory signature = CyberAgreementUtils.signAgreementTypedData( - vm, - registry.DOMAIN_SEPARATOR(), - registry.SIGNATUREDATA_TYPEHASH(), - contractId, - segCoTemplateUri, - globalFields, - partyFields, - globalValues, - partyValues[1], - alicePrivKey // wrong signature - ); - - // alice should not be able to deploy with her own signature on behalf of founder - vm.startPrank(alice); - vm.expectRevert(CyberAgreementRegistry.SignatureVerificationFailed.selector); - factory.deployMetaDAOContractFor( - saltUint, - "testcorp S.P., a segregated portfolio of Futarchy Governance SPC", - "Segregated Portfolio of Segregated Portfolio Company", - "Cayman Islands", - "email@testcorp.com", - "arbitration", - founder, - _getDefaultCorpOfficer(), - segCoTemplateId, - boardConsentTemplateId, - globalValues, - partyValues[1], - signature, - founder - ); - vm.stopPrank(); - } - - function test_RevertIf_deployMetaDAOContractForMismatchGlobalValues() public { - // Parties and values - string[] memory globalValues = _getDefaultGlobalValues(); - string[][] memory partyValues = _getDefaultPartyValues(); - - // Compute agreementId and signer signature for deployer (who will sign) - uint256 saltUint = 1; - // Parties must match what factory will set: [metaDAOOfficer.eoa, corpOfficer.eoa] - address[] memory parties = new address[](2); - parties[0] = metaDAO; - parties[1] = founder; - bytes32 contractId = keccak256(abi.encode(segCoTemplateId, saltUint, globalValues, parties)); - bytes memory signature = CyberAgreementUtils.signAgreementTypedData( - vm, - registry.DOMAIN_SEPARATOR(), - registry.SIGNATUREDATA_TYPEHASH(), - contractId, - segCoTemplateUri, - globalFields, - partyFields, - globalValues, - partyValues[1], - founderPrivKey - ); - - // alice should not be able temper with the field values - vm.startPrank(alice); - vm.expectRevert(MetaDAOFactory.GlobalOrPartyValuesMismatch.selector); - factory.deployMetaDAOContractFor( - saltUint, - "Alice's Company", // intentionally wrong company name - "Segregated Portfolio of Segregated Portfolio Company", - "Cayman Islands", - "email@testcorp.com", - "arbitration", - founder, - _getDefaultCorpOfficer(), - segCoTemplateId, - boardConsentTemplateId, - globalValues, - partyValues[1], - signature, - founder - ); - vm.stopPrank(); - } - - function test_RevertIf_deployMetaDAOContractForMismatchOfficerValues() public { - // Parties and values - string[] memory globalValues = _getDefaultGlobalValues(); - string[][] memory partyValues = _getDefaultPartyValues(); - - // Compute agreementId and signer signature for deployer (who will sign) - uint256 saltUint = 1; - // Parties must match what factory will set: [metaDAOOfficer.eoa, deployer] - address[] memory parties = new address[](2); - parties[0] = metaDAO; - parties[1] = founder; - bytes32 contractId = keccak256(abi.encode(segCoTemplateId, saltUint, globalValues, parties)); - bytes memory signature = CyberAgreementUtils.signAgreementTypedData( - vm, - registry.DOMAIN_SEPARATOR(), - registry.SIGNATUREDATA_TYPEHASH(), - contractId, - segCoTemplateUri, - globalFields, - partyFields, - globalValues, - partyValues[1], - founderPrivKey - ); - - CompanyOfficer memory officer = _getDefaultCorpOfficer(); - // Overwrite with incorrect officer EOA - officer.eoa = alice; - - // alice should not be able temper with the officer values - vm.startPrank(alice); - vm.expectRevert(MetaDAOFactory.OfficerValuesMismatch.selector); - factory.deployMetaDAOContractFor( - saltUint, - "testcorp S.P., a segregated portfolio of Futarchy Governance SPC", - "Segregated Portfolio of Segregated Portfolio Company", - "Cayman Islands", - "email@testcorp.com", - "arbitration", - founder, - officer, - segCoTemplateId, - boardConsentTemplateId, - globalValues, - partyValues[1], - signature, - founder - ); - vm.stopPrank(); - } - - function _getDefaultGlobalValues() internal returns (string[] memory) { - string[] memory globalValues = new string[](8); - globalValues[0] = "Founder"; // founderName - globalValues[1] = "testcorp"; // enterpriseNAme - globalValues[2] = "testcorp S.P., a segregated portfolio of Futarchy Governance SPC"; // companyName - globalValues[3] = "Segregated Portfolio of Segregated Portfolio Company"; // companyType - globalValues[4] = "Cayman Islands"; // companyJurisdiction - globalValues[5] = "email@testcorp.com"; // companyContactDetails - globalValues[6] = "TESTCORP"; // tokenSymbol - globalValues[7] = "Test Corp"; // tokenName - return globalValues; - } - - function _getDefaultPartyValues() internal returns (string[][] memory) { - string[][] memory partyValues = new string[][](2); - partyValues[0] = new string[](2); - partyValues[0][0] = "MetaDAO Officer"; // name - partyValues[0][1] = "metadao@example.com"; // contactDetails - partyValues[1] = new string[](2); - partyValues[1][0] = "Founder"; // name - partyValues[1][1] = "founder@example.com"; // contactDetails - return partyValues; - } - - function _getDefaultCorpOfficer() internal returns (CompanyOfficer memory) { - return CompanyOfficer({ - eoa: founder, - name: "Founder", - contact: "founder@example.com", - title: "CEO" - }); - } - - /// @notice This is to make sure contracts fields are created and signed with the expected values because - /// they are not strong typed and are error-prone to typos or out of sync across many iterations - /// @dev The test assumes all default values - function _verifyContractsDefault(bytes32 agreementId, bytes32 meetingNotesId) internal { - string[] memory expectedGlobalFields = new string[](8); - expectedGlobalFields[0] = "founderName"; - expectedGlobalFields[1] = "enterpriseName"; - expectedGlobalFields[2] = "companyName"; - expectedGlobalFields[3] = "companyType"; - expectedGlobalFields[4] = "companyJurisdiction"; - expectedGlobalFields[5] = "companyContactDetails"; - expectedGlobalFields[6] = "tokenSymbol"; - expectedGlobalFields[7] = "tokenName"; - - string[] memory expectedGlobalValues = new string[](8); - expectedGlobalValues[0] = "Founder"; // founderName - expectedGlobalValues[1] = "testcorp"; // enterpriseNAme - expectedGlobalValues[2] = "testcorp S.P., a segregated portfolio of Futarchy Governance SPC"; // companyName - expectedGlobalValues[3] = "Segregated Portfolio of Segregated Portfolio Company"; // companyType - expectedGlobalValues[4] = "Cayman Islands"; // companyJurisdiction - expectedGlobalValues[5] = "email@testcorp.com"; // companyContactDetails - expectedGlobalValues[6] = "TESTCORP"; // tokenSymbol - expectedGlobalValues[7] = "Test Corp"; // tokenName - - string[] memory expectedPartyFields = new string[](2); - expectedPartyFields[0] = "name"; - expectedPartyFields[1] = "contactDetails"; - - { - address[] memory expectedParties = new address[](2); - expectedParties[0] = metaDAO; - expectedParties[1] = founder; - - string[][] memory expectedPartyValues = new string[][](2); - expectedPartyValues[0] = new string[](2); - expectedPartyValues[0][0] = "MetaDAO Officer"; // name - expectedPartyValues[0][1] = "metadao@example.com"; // contactDetails - expectedPartyValues[1] = new string[](2); - expectedPartyValues[1][0] = "Founder"; // name - expectedPartyValues[1][1] = "founder@example.com"; // contactDetails - - _verifyContractDetails( - "SegCo agreement", - agreementId, - segCoTemplateUri, - expectedGlobalFields, - expectedPartyFields, - expectedGlobalValues, - expectedParties, - expectedPartyValues, - 2 - ); - } - - { - address[] memory expectedParties = new address[](1); - expectedParties[0] = metaDAO; - - string[][] memory expectedPartyValues = new string[][](1); - expectedPartyValues[0] = new string[](2); - expectedPartyValues[0][0] = "MetaDAO Officer"; // name - expectedPartyValues[0][1] = "metadao@example.com"; // contactDetails - - _verifyContractDetails( - "Board consent", - meetingNotesId, - boardConsentTemplateUri, - expectedGlobalFields, - expectedPartyFields, - expectedGlobalValues, - expectedParties, - expectedPartyValues, - 1 - ); - } - } - - /// @dev Made a separate function to avoid stack-too-deep errors - function _verifyContractDetails( - string memory contractName, - bytes32 agreementId, - string memory expectedLegalContractUri, - string[] memory expectedGlobalFields, - string[] memory expectedPartyFields, - string[] memory expectedGlobalValues, - address[] memory expectedParties, - string[][] memory expectedPartyValues, - uint256 expectedNumSignatures - ) internal { - ( - , - string memory legalContractUri, - string[] memory globalFields, - string[] memory partyFields, - string[] memory globalValues, - address[] memory parties, - string[][] memory partyValues, - , - uint256 numSignatures, - bool isComplete - ) = CyberAgreementRegistry(registry).getContractDetails(agreementId); - - assertEq(legalContractUri, expectedLegalContractUri, string(abi.encodePacked(contractName, ": unexpected legal contract URI"))); - assertEq(globalFields, expectedGlobalFields, string(abi.encodePacked(contractName, ": unexpected globalFields"))); - assertEq(partyFields, expectedPartyFields, string(abi.encodePacked(contractName, ": unexpected partyFields"))); - assertEq(globalValues, expectedGlobalValues, string(abi.encodePacked(contractName, ": unexpected globalValues"))); - for (uint256 i = 0; i < partyValues.length; i++) { - assertEq(partyValues[i], expectedPartyValues[i], string(abi.encodePacked(contractName, ": unexpected partyValues"))); - } - assertEq(parties, expectedParties, string(abi.encodePacked(contractName, ": unexpected parties"))); - assertEq(numSignatures, expectedNumSignatures, string(abi.encodePacked(contractName, ": unexpected number of signatures"))); - assertTrue(isComplete, string(abi.encodePacked(contractName, ": agreement should be complete"))); - } -} diff --git a/test/NonUSNationalityConditionForkTest.t.sol b/test/NonUSNationalityConditionForkTest.t.sol new file mode 100644 index 00000000..5c364ece --- /dev/null +++ b/test/NonUSNationalityConditionForkTest.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test, Vm, console2} from "forge-std/Test.sol"; +import {Escrow, EscrowStatus, Token} from "../src/storage/LexScrowStorage.sol"; +import { + BoundData, + DisclosedData, + IZKPassportHelper, + IZKPassportVerifier, + ProofVerificationData, + ProofVerificationParams, + ServiceConfig +} from "../src/interfaces/IZKPassportVerifier.sol"; +import {NonUSNationalityCondition} from "../src/libs/conditions/NonUSNationalityCondition.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +library NonUSNationalityConditionHelper { + using stdJson for string; + + Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function parseProofFromJson(string memory path) internal returns (ProofVerificationParams memory params, address account) { + string memory json = vm.readFile(path); + + bytes32 vkeyHash = json.readBytes32(".args[0].proofVerificationData.vkeyHash"); + bytes memory proof = json.readBytes(".args[0].proofVerificationData.proof"); + bytes32 version = json.readBytes32(".args[0].version"); + bytes memory committedInputs = json.readBytes(".args[0].committedInputs"); // = abi.encode(boundData, disclosedData) + bytes32[] memory publicInputs = json.readBytes32Array(".args[0].proofVerificationData.publicInputs"); + + ProofVerificationData memory proofData = ProofVerificationData({ + vkeyHash: vkeyHash, + proof: proof, + publicInputs: publicInputs + }); + + ServiceConfig memory serviceConfig = ServiceConfig({ + validityPeriodInSeconds: json.readUint(".args[0].serviceConfig.validityPeriodInSeconds"), + domain: json.readString(".args[0].serviceConfig.domain"), + scope: json.readString(".args[0].serviceConfig.scope"), + devMode: json.readBool(".args[0].serviceConfig.devMode") + }); + + params = ProofVerificationParams({ + version: version, + proofVerificationData: proofData, + committedInputs: committedInputs, + serviceConfig: serviceConfig + }); + + account = json.readAddress(".account"); + + return (params, account); + } +} + +/// @notice Assume Sepolia testnet +contract NonUSNationalityConditionForkTest is Test { + BorgAuth internal zkpassportAuth; + + NonUSNationalityCondition internal condition; + + uint256 internal constant MAX_VALIDITY_PERIOD = 365 days; + address public constant REAL_VERIFIER = 0x1D000001000EFD9a6371f4d90bB8920D5431c0D8; + + // These must match the proof or what the condition expects + string domain = "localhost"; + string scope = "hello-world"; + + string[] excludedCountries; + + function setUp() public { + excludedCountries = new string[](9); + excludedCountries[0] = "IRN"; + excludedCountries[1] = "IRQ"; + excludedCountries[2] = "LBY"; + excludedCountries[3] = "PRK"; + excludedCountries[4] = "SDN"; + excludedCountries[5] = "SOM"; + excludedCountries[6] = "SYR"; + excludedCountries[7] = "USA"; + excludedCountries[8] = "YEM"; + + zkpassportAuth = new BorgAuth(address(this)); + + condition = new NonUSNationalityCondition( + address(zkpassportAuth), + domain, + scope, + REAL_VERIFIER, + MAX_VALIDITY_PERIOD, + excludedCountries + ); + } + + function test_SubmitRealProofValid() public { + // Assume the sample data is signed for Sepolia (included in committedInputs) + // at timestamp: 1772783327 (included in publicInputs) + uint256 signedTimestamp = 1772783327; + (ProofVerificationParams memory params, address account) = NonUSNationalityConditionHelper.parseProofFromJson("test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call.json"); + + vm.warp(signedTimestamp); + vm.prank(account); + condition.submitProof(params, false); + assertEq(condition.proofExpiry(account), signedTimestamp + params.serviceConfig.validityPeriodInSeconds, "unexpected proof expiry"); + } + + /// @notice Real proof of non-FRA nationality should not pass since we want non-US + non-sanctioned proof + function test_RevertIf_RealProofInvalid() public { + // Assume the sample data is signed for Sepolia (included in committedInputs) + // at timestamp: 1772768315 (included in publicInputs) + uint256 signedTimestamp = 1772768315; + (ProofVerificationParams memory params, address account) = NonUSNationalityConditionHelper.parseProofFromJson("test/res/sample-non-fr-proof-call.json"); + + vm.warp(signedTimestamp); + vm.prank(account); + vm.expectRevert(NonUSNationalityCondition.USAOrSanctionedCountriesNotAllowed.selector); + condition.submitProof(params, false); + } +} diff --git a/test/NonUSNationalityConditionTest.t.sol b/test/NonUSNationalityConditionTest.t.sol new file mode 100644 index 00000000..c3202fdc --- /dev/null +++ b/test/NonUSNationalityConditionTest.t.sol @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {Escrow, EscrowStatus, Token} from "../src/storage/LexScrowStorage.sol"; +import {NonUSNationalityCondition} from "../src/libs/conditions/NonUSNationalityCondition.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {ICondition} from "../src/interfaces/ICondition.sol"; +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { + BoundData, + DisclosedData, + IZKPassportHelper, + IZKPassportVerifier, + ProofVerificationData, + ProofVerificationParams, + ServiceConfig +} from "../src/interfaces/IZKPassportVerifier.sol"; + +contract MockEscrowSource { + mapping(bytes32 => address) public counterpartyByAgreementId; + + function setCounterparty(bytes32 agreementId, address counterparty) external { + counterpartyByAgreementId[agreementId] = counterparty; + } + + function getEscrowDetails(bytes32 agreementId) external view returns (Escrow memory esc) { + Token[] memory corpAssets = new Token[](0); + Token[] memory buyerAssets = new Token[](0); + esc = Escrow({ + agreementId: agreementId, + counterParty: counterpartyByAgreementId[agreementId], + corpAssets: corpAssets, + buyerAssets: buyerAssets, + signature: "", + expiry: block.timestamp + 1 days, + status: EscrowStatus.PAID + }); + } +} + +contract MockZKPassportHelper is IZKPassportHelper { + bool public shouldRevertSanctions; + bool public shouldRejectNationality; + + error SanctionsCheckFailed(); + + function setShouldRevertSanctions(bool _should) external { + shouldRevertSanctions = _should; + } + + function setShouldRejectNationality(bool _should) external { + shouldRejectNationality = _should; + } + + function verifyScopes( + bytes32[] calldata publicInputs, + string calldata domain, + string calldata scope + ) external pure returns (bool) { + return publicInputs[0] == keccak256(bytes(domain)) && publicInputs[1] == keccak256(bytes(scope)); + } + + function getBoundData(bytes calldata committedInputs) external pure returns (BoundData memory) { + return abi.decode(committedInputs, (BoundData)); + } + + function getProofTimestamp(bytes32[] calldata publicInputs) external pure returns (uint256) { + return uint256(publicInputs[2]); + } + + function isNationalityOut(string[] memory, bytes calldata) external view returns (bool) { + return !shouldRejectNationality; + } + + function enforceSanctionsRoot(uint256, bool, bytes calldata) external view { + if (shouldRevertSanctions) revert SanctionsCheckFailed(); + } +} + +contract MockZKPassportVerifier is IZKPassportVerifier { + bool public shouldVerify = true; + bool public shouldReturnZeroHelper; + IZKPassportHelper public helperContract; + bytes32 public uniqueId = keccak256("default-proof-id"); + + function setHelper(address _helper) external { + helperContract = IZKPassportHelper(_helper); + } + + function setShouldVerify(bool _should) external { + shouldVerify = _should; + } + + function setShouldReturnZeroHelper(bool _should) external { + shouldReturnZeroHelper = _should; + } + + function setUniqueId(bytes32 _id) external { + uniqueId = _id; + } + + function verify(ProofVerificationParams calldata) + external + returns (bool, bytes32, IZKPassportHelper) + { + if (!shouldVerify) return (false, bytes32(0), IZKPassportHelper(address(0))); + if (shouldReturnZeroHelper) return (true, uniqueId, IZKPassportHelper(address(0))); + return (true, uniqueId, helperContract); + } +} + +contract NonUSNationalityConditionTest is Test { + string internal constant EXPECTED_DOMAIN = "app.example"; + string internal constant EXPECTED_SCOPE = "non-us-round"; + + NonUSNationalityCondition internal condition; + MockEscrowSource internal escrowSource; + BorgAuth internal zkpassportAuth; + MockZKPassportHelper internal mockHelper; + MockZKPassportVerifier internal mockVerifier; + + uint256 internal constant MAX_VALIDITY_PERIOD = 30 days; + + function setUp() public { + zkpassportAuth = new BorgAuth(address(this)); + mockHelper = new MockZKPassportHelper(); + mockVerifier = new MockZKPassportVerifier(); + mockVerifier.setHelper(address(mockHelper)); + + string[] memory excludedCountries = new string[](1); + excludedCountries[0] = "USA"; + + condition = new NonUSNationalityCondition( + address(zkpassportAuth), + EXPECTED_DOMAIN, + EXPECTED_SCOPE, + address(mockVerifier), + MAX_VALIDITY_PERIOD, + excludedCountries + ); + escrowSource = new MockEscrowSource(); + } + + // --- existing tests --- + + function test_ConditionCheck_RevertsWithoutSubmittedProof() public { + bytes32 agreementId = keccak256("agreement-no-proof"); + address investor = address(0xA11CE); + escrowSource.setCounterparty(agreementId, investor); + + bool allowed = condition.checkCondition( + address(escrowSource), + bytes4(0), + abi.encode(agreementId) + ); + assertFalse(allowed); + } + + function test_UpdateMaxValidityPeriod() public { + condition.updateMaxValidityPeriod(1 days); + assertEq(condition.maxValidityPeriod(), 1 days); + } + + function test_UpdateSanctionedCountries() public { + string[] memory sanctioned = new string[](2); + sanctioned[0] = "FRA"; + sanctioned[1] = "NK"; + condition.updateExcludedCountries(sanctioned); + + assertEq(condition.excludedCountries(0), "FRA"); + assertEq(condition.excludedCountries(1), "NK"); + } + + // --- submitProof tests --- + + function test_RevertWhen_InvalidProof_VerificationFailed() public { + mockVerifier.setShouldVerify(false); + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.InvalidProof.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_InvalidProof_ZeroHelper() public { + mockVerifier.setShouldReturnZeroHelper(true); + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.InvalidProof.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_ProofAlreadyUsed() public { + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + condition.submitProof(params, false); + + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.ProofAlreadyUsed.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_InvalidScope() public { + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + params.proofVerificationData.publicInputs[1] = keccak256(bytes("wrong-scope")); + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.InvalidScope.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_BoundSenderMismatch() public { + address investor = address(0xA11CE); + address attacker = address(0xDEAD); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(attacker); + vm.expectRevert(NonUSNationalityCondition.InvalidBoundSender.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_BoundChainIdMismatch() public { + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + params.committedInputs = _buildCommittedInputs(investor, block.chainid + 1); + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.InvalidBoundChainId.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_NationalityRejected() public { + mockHelper.setShouldRejectNationality(true); + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.USAOrSanctionedCountriesNotAllowed.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_SanctionsFail() public { + mockHelper.setShouldRevertSanctions(true); + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + vm.expectRevert(MockZKPassportHelper.SanctionsCheckFailed.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_MaxValidityPeriodExceeded() public { + address investor = address(0xA11CE); + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 100 days); + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.MaxValidityPeriodExceeded.selector); + condition.submitProof(params, false); + } + + function test_RevertWhen_ProofExpired() public { + vm.warp(3 days); + address investor = address(0xA11CE); + uint256 proofTimestamp = block.timestamp - 2 days; + ProofVerificationParams memory params = _buildParams(investor, proofTimestamp, 1 days); + vm.prank(investor); + vm.expectRevert(NonUSNationalityCondition.ProofExpired.selector); + condition.submitProof(params, false); + } + + function test_SubmitProof_HappyPath() public { + address investor = address(0xA11CE); + uint256 proofTimestamp = block.timestamp; + uint256 validityPeriod = 1 days; + ProofVerificationParams memory params = _buildParams(investor, proofTimestamp, validityPeriod); + vm.expectEmit(true, false, false, true); + emit NonUSNationalityCondition.ProofSubmitted(investor, proofTimestamp + validityPeriod); + vm.prank(investor); + condition.submitProof(params, false); + assertEq(condition.proofExpiry(investor), proofTimestamp + validityPeriod); + } + + // --- checkCondition tests --- + + function test_CheckCondition_ReturnsTrueWithValidProof() public { + bytes32 agreementId = keccak256("agreement-1"); + address investor = address(0xA11CE); + escrowSource.setCounterparty(agreementId, investor); + + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + condition.submitProof(params, false); + + bool result = condition.checkCondition(address(escrowSource), bytes4(0), abi.encode(agreementId)); + assertTrue(result); + } + + function test_CheckCondition_ReturnsFalseAfterExpiry() public { + bytes32 agreementId = keccak256("agreement-2"); + address investor = address(0xA11CE); + escrowSource.setCounterparty(agreementId, investor); + + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + condition.submitProof(params, false); + + vm.warp(block.timestamp + 2 days); + bool result = condition.checkCondition(address(escrowSource), bytes4(0), abi.encode(agreementId)); + assertFalse(result); + } + + // --- access control tests --- + + function test_RevertWhen_UpdateMaxValidityPeriod_Unauthorized() public { + vm.prank(address(0xBEEF)); + vm.expectRevert( + abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, uint256(98), address(0xBEEF)) + ); + condition.updateMaxValidityPeriod(1 days); + } + + function test_RevertWhen_UpdateExcludedCountries_Unauthorized() public { + string[] memory countries = new string[](1); + countries[0] = "FRA"; + vm.prank(address(0xBEEF)); + vm.expectRevert( + abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, uint256(98), address(0xBEEF)) + ); + condition.updateExcludedCountries(countries); + } + + // --- initialize validation tests --- + + function test_RevertWhen_Initialize_ZeroMaxValidityPeriod() public { + string[] memory excludedCountries = new string[](1); + excludedCountries[0] = "USA"; + + vm.expectRevert(NonUSNationalityCondition.InvalidMaxValidityPeriod.selector); + NonUSNationalityCondition fresh = new NonUSNationalityCondition( + address(zkpassportAuth), + EXPECTED_DOMAIN, + EXPECTED_SCOPE, + address(mockVerifier), + 0, + excludedCountries + ); + } + + function test_RevertWhen_UpdateMaxValidityPeriod_Zero() public { + vm.expectRevert(NonUSNationalityCondition.InvalidMaxValidityPeriod.selector); + condition.updateMaxValidityPeriod(0); + } + + // --- supportsInterface tests --- + + function test_SupportsInterface_ICondition() public view { + assertTrue(condition.supportsInterface(type(ICondition).interfaceId)); + } + + function test_SupportsInterface_IERC165() public view { + assertTrue(condition.supportsInterface(type(IERC165).interfaceId)); + } + + function test_SupportsInterface_Unknown() public view { + assertFalse(condition.supportsInterface(bytes4(0xDEADBEEF))); + } + + // --- internal helpers --- + // + // NOTE: The encoding conventions used here (committedInputs, publicInputs, BoundData) + // are simplified mock conventions chosen for testability. They do NOT necessarily + // reflect the actual production encoding used by the ZKPassport protocol. Specifically: + // - committedInputs: abi.encode(BoundData) — mock only; nationality is controlled via flag + // - publicInputs[0]: keccak256(domain), [1]: keccak256(scope), [2]: timestamp — mock only + // - BoundData fields are populated with synthetic test values + + function _buildCommittedInputs(address sender, uint256 chainId) internal pure returns (bytes memory) { + return abi.encode(BoundData({senderAddress: sender, chainId: chainId, customData: ""})); + } + + function _buildPublicInputs(uint256 proofTimestamp) internal pure returns (bytes32[] memory) { + bytes32[] memory pubs = new bytes32[](3); + pubs[0] = keccak256(bytes(EXPECTED_DOMAIN)); + pubs[1] = keccak256(bytes(EXPECTED_SCOPE)); + pubs[2] = bytes32(proofTimestamp); + return pubs; + } + + function _buildParams( + address sender, + uint256 proofTimestamp, + uint256 validityPeriod + ) internal view returns (ProofVerificationParams memory) { + return ProofVerificationParams({ + version: bytes32(0), + proofVerificationData: ProofVerificationData({ + vkeyHash: bytes32(0), + proof: "", + publicInputs: _buildPublicInputs(proofTimestamp) + }), + committedInputs: _buildCommittedInputs(sender, block.chainid), + serviceConfig: ServiceConfig({ + validityPeriodInSeconds: validityPeriod, + domain: EXPECTED_DOMAIN, + scope: EXPECTED_SCOPE, + devMode: false + }) + }); + } +} diff --git a/test/OrConditionTest.t.sol b/test/OrConditionTest.t.sol new file mode 100644 index 00000000..1dab6b52 --- /dev/null +++ b/test/OrConditionTest.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {ICondition} from "../src/interfaces/ICondition.sol"; +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import {OrCondition} from "../src/libs/conditions/OrCondition.sol"; + +contract AlwaysTrueCondition is ICondition { + function checkCondition(address, bytes4, bytes memory) external pure returns (bool) { + return true; + } +} + +contract AlwaysFalseCondition is ICondition { + function checkCondition(address, bytes4, bytes memory) external pure returns (bool) { + return false; + } +} + +contract OrConditionTest is Test { + AlwaysTrueCondition internal trueCondition; + AlwaysFalseCondition internal falseCondition; + + function setUp() public { + trueCondition = new AlwaysTrueCondition(); + falseCondition = new AlwaysFalseCondition(); + } + + function _makeOr(address a, address b) internal returns (OrCondition) { + address[] memory addrs = new address[](2); + addrs[0] = a; + addrs[1] = b; + return new OrCondition(addrs); + } + + function test_AllFalse_ReturnsFalse() public { + OrCondition orCond = _makeOr(address(falseCondition), address(falseCondition)); + assertFalse(orCond.checkCondition(address(0), bytes4(0), "")); + } + + function test_FirstTrue_ReturnsTrue() public { + OrCondition orCond = _makeOr(address(trueCondition), address(falseCondition)); + assertTrue(orCond.checkCondition(address(0), bytes4(0), "")); + } + + function test_LastTrue_ReturnsTrue() public { + OrCondition orCond = _makeOr(address(falseCondition), address(trueCondition)); + assertTrue(orCond.checkCondition(address(0), bytes4(0), "")); + } + + function test_AllTrue_ReturnsTrue() public { + OrCondition orCond = _makeOr(address(trueCondition), address(trueCondition)); + assertTrue(orCond.checkCondition(address(0), bytes4(0), "")); + } + + function test_ThreeConditions_MiddleTrue_ReturnsTrue() public { + address[] memory addrs = new address[](3); + addrs[0] = address(falseCondition); + addrs[1] = address(trueCondition); + addrs[2] = address(falseCondition); + OrCondition orCond = new OrCondition(addrs); + assertTrue(orCond.checkCondition(address(0), bytes4(0), "")); + } + + function test_RevertIf_ConstructorWithFewerThanTwoConditions() public { + address[] memory addrs = new address[](1); + addrs[0] = address(trueCondition); + vm.expectRevert(OrCondition.NeedMoreConditions.selector); + new OrCondition(addrs); + } + + function test_RevertIf_ConstructorWithZeroConditions() public { + address[] memory addrs = new address[](0); + vm.expectRevert(OrCondition.NeedMoreConditions.selector); + new OrCondition(addrs); + } + + function test_SupportsInterface_ICondition() public { + OrCondition orCond = _makeOr(address(trueCondition), address(falseCondition)); + assertTrue(orCond.supportsInterface(type(ICondition).interfaceId)); + } + + function test_SupportsInterface_IERC165() public { + OrCondition orCond = _makeOr(address(trueCondition), address(falseCondition)); + assertTrue(orCond.supportsInterface(type(IERC165).interfaceId)); + } + + function test_SupportsInterface_Unknown_ReturnsFalse() public { + OrCondition orCond = _makeOr(address(trueCondition), address(falseCondition)); + assertFalse(orCond.supportsInterface(bytes4(0xDEADBEEF))); + } + + function test_ConditionsArray_StoredCorrectly() public { + OrCondition orCond = _makeOr(address(trueCondition), address(falseCondition)); + assertEq(address(orCond.conditions(0)), address(trueCondition)); + assertEq(address(orCond.conditions(1)), address(falseCondition)); + } + + // Integration: OrCondition composed with two real sub-conditions, one of which passes + function test_Integration_OrCondition_PassesWhenOneSubConditionPasses() public { + // Simulate an OrCondition used as a round condition: + // falseCondition || trueCondition → should pass + OrCondition orCond = _makeOr(address(falseCondition), address(trueCondition)); + + // checkCondition args don't matter for always-true/false mocks + bool result = orCond.checkCondition(address(this), bytes4(keccak256("allocate(bytes32,uint256)")), abi.encode(bytes32(0))); + assertTrue(result); + } + + function test_Integration_OrCondition_FailsWhenAllSubConditionsFail() public { + OrCondition orCond = _makeOr(address(falseCondition), address(falseCondition)); + + bool result = orCond.checkCondition(address(this), bytes4(keccak256("allocate(bytes32,uint256)")), abi.encode(bytes32(0))); + assertFalse(result); + } +} diff --git a/test/PumpCorpFactory.t.sol b/test/PumpCorpFactory.t.sol new file mode 100644 index 00000000..41145ccc --- /dev/null +++ b/test/PumpCorpFactory.t.sol @@ -0,0 +1,1928 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Test, console2} from "forge-std/Test.sol"; +import {Strings} from "openzeppelin-contracts/utils/Strings.sol"; +import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {NonUSNationalityConditionHelper} from "./NonUSNationalityConditionForkTest.t.sol"; +import {DeployNonUsZkPassportConditionScript} from "../script/deploy-non-us-zkpassport-condition.s.sol"; +import {DeployPumpCorpFactoryScript} from "../script/deploy-pump-factory.s.sol"; +import {PumpCorpFactory, PumpCorpFactoryLib} from "../src/PumpCorpFactory.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {FeeOverride} from "../src/interfaces/IRoundManagerFactory.sol"; +import {CyberCorpSingleFactory} from "../src/CyberCorpSingleFactory.sol"; +import {EIP712Lib} from "../src/libs/EIP712Lib.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {Round, RoundType} from "../src/libs/RoundLib.sol"; +import {CompanyOfficer, SecurityClass, SecuritySeries} from "../src/CyberCorpConstants.sol"; +import {CyberCertData} from "../src/interfaces/IRoundManager.sol"; +import {DeploymentConstants} from "../script/libs/DeploymentConstants.sol"; +import {NonUSNationalityCondition} from "../src/libs/conditions/NonUSNationalityCondition.sol"; +import {LexChexCondition} from "../src/libs/conditions/lexchexCondition.sol"; +import {OrCondition} from "../src/libs/conditions/OrCondition.sol"; +import {LeXcheX} from "../src/creds/lexchex.sol"; +import {Accreditation} from "../src/creds/storage/lexchexStorage.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {CyberAgreementUtils} from "./libs/CyberAgreementUtils.sol"; +import {EOI, LexChexDetails, MintRequest} from "../src/storage/RoundManagerStorage.sol"; +import {ProofVerificationParams} from "../src/interfaces/IZKPassportVerifier.sol"; +import {MockERC20} from "./mock/MockERC20.sol"; +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; + +/// @dev Always-failing condition: any allocation attempt on its escrow is blocked. +contract AlwaysFalseCondition { + function checkCondition(address, bytes4, bytes memory) external pure returns (bool) { + return false; + } +} + +/// @title PumpCorpFactory Escrow Signature Security Tests +/// @notice Focused on whether an attacker who hijacks an escrowed signature can +/// swap in a different officer or alter key corp/round parameters. +/// +/// Run with (timeout = 5m): +/// forge test --use solc:0.8.28 --via-ir --fork-url $END_PT_BASE -vvv --mp PumpCorpFactory.t.sol +contract PumpCorpFactoryTest is Test { + using Strings for address; + + string saltStrZkpassport = "PumpCorpFactoryTest.zkpassport"; + string saltStrPump = "PumpCorpFactoryTest.pump"; + + // ── Actors ──────────────────────────────────────────────────────────────── + uint256 internal deployerPk; + uint256 internal officerPk; + uint256 internal attackerPk; + uint256 internal investorPk; + + address internal deployer; + address internal officer; + address internal attacker; + address internal investor; + + // ── Live deployments (DeploymentConstants.coreV2) ──────────── + DeploymentConstants.CoreDeployment internal net = + DeploymentConstants.coreV2(block.chainid); + address internal metalexSafe = net.metalexSafe; + + // Convenience aliases + address internal REGISTRY = net.cyberAgreementRegistry; + address internal CYBERCORP_SINGLE_FACTORY = net.cyberCorpSingleFactory; + address internal DEAL_MANAGER_FACTORY = net.dealManagerFactory; + address internal URI_BUILDER = net.uriBuilder; + + // ── Live LexChex addresses on Base ─────────────────────────────────────── + address internal LEXCHEX = net.lexchex; + address internal LEXCHEX_CONDITION = net.lexchexCondition; + address internal LEXCHEX_AUTH = net.lexchexAuth; + + // ── Fresh zkPassport condition deployed in setUp ─────────────────────────────── + string internal constant expectedDomain = "localhost"; + string internal constant expectedScope = "non-us-non-sanctioned"; + NonUSNationalityCondition internal zkpassportCondition; + + // ── OrCondition: zkpassportCondition || LEXCHEX_CONDITION ──────────────── + OrCondition internal orCondition; + + // ── Fresh PumpCorpFactory deployed in setUp ─────────────────────────────── + PumpCorpFactory internal pumpFactory; + RoundManagerFactory internal rmFactory; + + // ── Shared round constants ──────────────────────────────────────────────── + uint256 internal constant RAISE_CAP = 1_000_000e6; + uint256 internal constant MIN_TICKET = 10_000e6; + uint256 internal constant MAX_TICKET = 100_000e6; + uint256 internal constant PRICE_PER_UNIT = 1e6; + uint256 internal constant VALUATION = 20_000_000e6; + + // Any bytes32 template ID works for createRound (registry not consulted there) + bytes32 internal constant TEMPLATE_ID = bytes32(uint256(1)); + + // ── Lifecycle test additions ────────────────────────────────────────────── + MockERC20 internal payToken; // deployed in setUp + + // ── Shared cert / legal arrays (1 cert → legalDetails & extensionData length 1) ─ + CyberCertData[] internal certDataArr; + string[] internal legalDetails; + bytes[] internal extensionData; + string[] internal officerPartyValues; + + /// *** WARNING ***: As of 2026/03/18, we haven't deployed the dependent `RoundManager` with `restrictEndTimeReduction` + /// to Base mainnet yet, so we need to make sure the upgrade is simulated here + function setUp() public { + assertEq(block.chainid, DeploymentConstants.BASE, "Fork test: Base only @ block 43552581"); + vm.rollFork(43552581); +// assertEq(block.chainid, DeploymentConstants.BASE_SEPOLIA, "For test: Base Sepolia only"); + + (deployer, deployerPk) = makeAddrAndKey("deployer"); + (officer, officerPk) = makeAddrAndKey("officer"); + (attacker, attackerPk) = makeAddrAndKey("attacker"); + (investor, investorPk) = makeAddrAndKey("investor"); + + // Deploy zkPassport condition + (, zkpassportCondition) = (new DeployNonUsZkPassportConditionScript()).runWithArgs( + saltStrZkpassport, + deployerPk, + expectedDomain, + expectedScope, + 2592000, // maxValidityPeriod, + block.chainid + ); + + // Deploy OrCondition (zkPassport || LexChex) + address[] memory orAddrs = new address[](2); + orAddrs[0] = address(zkpassportCondition); + orAddrs[1] = LEXCHEX_CONDITION; + orCondition = new OrCondition(orAddrs); + + // Deploy PumpCorpFactory + BorgAuth pumpAuth; + (pumpFactory, rmFactory, , , pumpAuth) = (new DeployPumpCorpFactoryScript()).runWithArgs( + block.chainid, + saltStrPump, + deployerPk + ); + + // Simulate granting PumpCorpFactory owner access to LeXcheX and to RoundManagerFactory auth + vm.startPrank(metalexSafe); + BorgAuth(pumpFactory.lexchexAuth()).updateRole(address(pumpFactory), 99); + BorgAuth(pumpAuth).updateRole(address(pumpFactory), 99); + vm.stopPrank(); + + string[] memory legend = new string[](1); + legend[0] = "SEED SAFE"; + certDataArr.push(CyberCertData({ + name: "SEED SAFE", + symbol: "SEEDSAFE", + uri: "ipfs://seed-safe", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesSeed, + extension: address(0), + defaultLegend: legend + })); + + legalDetails = new string[](1); + legalDetails[0] = "SEED SAFE legal details"; + extensionData = new bytes[](1); + extensionData[0] = ""; + + // ── Lifecycle setup ────────────────────────────────────────────────── + payToken = new MockERC20("Test Token", "TT", 9); + officerPartyValues = _lifecyclePartyValues("Alice Officer", officer); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + /// Predict the CREATE2 addresses that PumpCorpFactory will deploy to for a given salt. + function _predict(uint256 salt) + internal + view + returns (address corp, address rm) + { + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + corp = CyberCorpSingleFactory(CYBERCORP_SINGLE_FACTORY) + .computeCyberCorpSingleAddress(corpSalt); + rm = rmFactory + .computeRoundManagerAddress(corpSalt); + } + + /// Build a CompanyOfficer for the given EOA. + function _officer(address eoa, string memory name) + internal + pure + returns (CompanyOfficer memory) + { + return CompanyOfficer({eoa: eoa, name: name, contact: "officer@corp.com", title: "CEO"}); + } + + /// Build party-values sized to TEMPLATE_ID's partyFields. + /// Slot 0 = name, slot 1 = eoa hex string, remaining slots = "". + function _lifecyclePartyValues(string memory name_, address eoa_) + internal view returns (string[] memory pv) + { + (, , , string[] memory pFields) = CyberAgreementRegistry(REGISTRY) + .getTemplateDetails(TEMPLATE_ID); + pv = new string[](pFields.length); + if (pFields.length > 0) pv[0] = name_; + if (pFields.length > 1) pv[1] = eoa_.toHexString(); + } + + /// Build global-values sized to TEMPLATE_ID's globalFields. + /// Slot 0 = "Seed Round", remaining slots = "". + function _lifecycleGlobalValues() + internal view returns (string[] memory gv) + { + (, , string[] memory gFields, ) = CyberAgreementRegistry(REGISTRY) + .getTemplateDetails(TEMPLATE_ID); + gv = new string[](gFields.length); + if (gFields.length > 0) gv[0] = "Seed Round"; + } + + /// Compute the EIP-712 supplemental metadata signature for PumpCorpFactory. + function _metaSig( + uint256 salt, + address companyPayable, + bool publicRound, + bool allowTimedOffers, + bool restrictEndTimeReduction, + CompanyOfficer memory off, + string memory companyName_, + string memory companyType_, + string memory companyJurisdiction_, + string memory companyContactDetails_, + string memory defaultDisputeResolution_, + bytes[] memory extensionData_, + string[] memory roundPartyValues_, + string[] memory legal, + CyberCertData[] memory certs, + address[] memory conditions, + uint256 signerPk + ) internal view returns (bytes memory) { + bytes32 corpSalt = keccak256(abi.encodePacked(salt)); + bytes32 domainSep = keccak256(abi.encode( + PumpCorpFactoryLib.FACTORY_DOMAIN_TYPEHASH, + keccak256(bytes("PumpCorpFactory")), + keccak256(bytes("1")), + block.chainid, + address(pumpFactory) + )); + bytes32 officerHash = keccak256(abi.encode( + PumpCorpFactoryLib.OFFICER_TYPEHASH, + off.eoa, + keccak256(bytes(off.name)), + keccak256(bytes(off.contact)), + keccak256(bytes(off.title)) + )); + bytes32 structHash = keccak256(abi.encode( + PumpCorpFactoryLib.ROUND_SUPPLEMENTAL_TYPEHASH, + corpSalt, + companyPayable, + publicRound, + allowTimedOffers, + restrictEndTimeReduction, + officerHash, + keccak256(bytes(companyName_)), + keccak256(bytes(companyType_)), + keccak256(bytes(companyJurisdiction_)), + keccak256(bytes(companyContactDetails_)), + keccak256(bytes(defaultDisputeResolution_)), + PumpCorpFactoryLib.hashBytesArray(extensionData_), + PumpCorpFactoryLib.hashStringArray(roundPartyValues_), + PumpCorpFactoryLib.hashStringArray(legal), + PumpCorpFactoryLib.hashCertDataArray(certs), + PumpCorpFactoryLib.hashAddresses(conditions) + )); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSep, structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest); + return abi.encodePacked(r, s, v); + } + + /// Convenience wrapper using standard happy-path values. + function _metaSigDefault(uint256 salt, uint256 signerPk) internal view returns (bytes memory) { + return _metaSig( + salt, + address(this), + true, + true, + true, + _officer(officer, "Alice Officer"), + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + extensionData, + officerPartyValues, + legalDetails, + certDataArr, + new address[](0), + signerPk + ); + } + + /// Escrow signature parameterised for a specific paymentToken, templateId, and roundType. + function _escrowSigFull( + address rm_, + address corp_, + uint256 signerPk_, + uint256 startTime_, + uint256 endTime_, + address paymentToken_, + bytes32 templateId_, + RoundType roundType_ + ) internal view returns (bytes memory sig) { + bytes32 roundId_ = keccak256(abi.encodePacked( + SecuritySeries.SeriesSeed, + RAISE_CAP, MIN_TICKET, MAX_TICKET, + uint8(roundType_), + startTime_, endTime_, + templateId_, + paymentToken_, + PRICE_PER_UNIT, VALUATION, corp_ + )); + bytes32 domainSep = keccak256(abi.encode( + EIP712Lib.EIP712_DOMAIN_TYPEHASH, + keccak256(bytes("RoundManager")), + keccak256(bytes("1")), + block.chainid, rm_ + )); + bytes32 structHash = keccak256(abi.encode( + EIP712Lib.ESCROWEDSIGNATUREDATA_TYPEHASH, + roundId_, + uint8(SecuritySeries.SeriesSeed), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + uint8(roundType_), + startTime_, endTime_, + templateId_, + paymentToken_, + PRICE_PER_UNIT, VALUATION, corp_ + )); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSep, structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk_, digest); + sig = abi.encodePacked(r, s, v); + } + + /// Compute the investor's EIP-712 EOI signature for TEMPLATE_ID. + function _eoiSig( + uint256 eoiSalt_, + string[] memory globalValues_, + string[] memory investorPv_ + ) internal view returns (bytes memory) { + CyberAgreementRegistry reg = CyberAgreementRegistry(REGISTRY); + ( + string memory legalUri, + , + string[] memory gFields, + string[] memory pFields + ) = reg.getTemplateDetails(TEMPLATE_ID); + address[] memory parties = new address[](2); + parties[0] = officer; + parties[1] = investor; + bytes32 contractId = keccak256( + abi.encode(TEMPLATE_ID, eoiSalt_, globalValues_, parties) + ); + return CyberAgreementUtils.signAgreementTypedData( + vm, + reg.DOMAIN_SEPARATOR(), + reg.SIGNATUREDATA_TYPEHASH(), + contractId, + legalUri, + gFields, + pFields, + globalValues_, + investorPv_, + investorPk + ); + } + + function _emptyLex() internal pure returns (LexChexDetails memory) { + return LexChexDetails({ + request: MintRequest({ + uuid: 0, owner: address(0), + investorName: "", investorType: "", + investorJurisdiction: "", investorContact: "", + mintPrice: 0, expiry: 0, paymentToken: address(0) + }), + templateId: bytes32(0), + salt: 0, + globalValues: new string[](0), + parties: new address[](0), + partyValues: new string[][](0), + agreementSignature: "" + }); + } + + /// Deploy a corp + round using payToken and TEMPLATE_ID. + function _deployLifecycle( + uint256 salt_, + RoundType roundType_, + address[] memory conditions + ) internal returns (address corp_, address rm_, bytes32 roundId_) + { + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory escrowSig; + { + (address predCorp, address predRM) = _predict(salt_); + escrowSig = _escrowSigFull( + predRM, predCorp, officerPk, + start, end, + address(payToken), TEMPLATE_ID, roundType_ + ); + } + + (corp_, , , , rm_, roundId_) = pumpFactory.deployCyberCorpAndCreateRoundFor( + salt_, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(payToken), PRICE_PER_UNIT, VALUATION, + officerPartyValues, + escrowSig, + _metaSig( + salt_, address(this), true, true, true, + _officer(officer, "Alice Officer"), + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + extensionData, + officerPartyValues, + legalDetails, certDataArr, conditions, + officerPk + ), + roundType_, + conditions, + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // FEE OVERRIDE + // ═══════════════════════════════════════════════════════════════════════════ + + function test_FeeOverride_ZeroOnDeployedRoundManager() public { + (, address rm, ) = _deployLifecycle(299999, RoundType.FCFS, new address[](0)); + + FeeOverride memory fo = rmFactory.getInstanceFeeOverride(rm); + assertTrue(fo.enabled, "fee override must be enabled"); + assertEq(fo.ratio, 0, "fee ratio must be zero"); + assertEq(RoundManager(rm).computeFee(1 ether), 0, "computeFee must return 0"); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // HAPPY PATH + // ═══════════════════════════════════════════════════════════════════════════ + + /// FCFS round: submitEOI auto-triggers allocate — full life cycle in one tx. + function test_HappyPath_SubmitEOI_FCFS() public { + (, address rm, bytes32 roundId) = _deployLifecycle(300001, RoundType.FCFS, new address[](0)); + + string[] memory globalValues = _lifecycleGlobalValues(); + string[] memory investorPv = _lifecyclePartyValues("Test Investor", investor); + uint256 eoiSalt = 1; + uint256 investAmount = MIN_TICKET; + + payToken.mint(investor, investAmount); + vm.startPrank(investor); + payToken.approve(rm, investAmount); + + EOI memory eoi = EOI({ + name: "Test Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.com", + minAmount: investAmount, + maxAmount: investAmount, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: _emptyLex() + }); + + (bytes32 agreementId, ) = RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + _eoiSig(eoiSalt, globalValues, investorPv), + eoiSalt, new address[](0), bytes32(0) + ); + vm.stopPrank(); + + assertTrue( + CyberAgreementRegistry(REGISTRY).isFinalized(agreementId), + "agreement must be finalized after FCFS submitEOI" + ); + assertEq( + RoundManager(rm).getRound(roundId).raised, + investAmount, + "raised must equal investAmount" + ); + // No fees: companyPayable receives full investAmount, fee recipient receives nothing + assertEq( + payToken.balanceOf(address(this)), + investAmount, + "companyPayable must receive full investAmount (no fee deducted)" + ); + assertEq( + payToken.balanceOf(rmFactory.getPlatformPayable()), + 0, + "fee recipient must receive zero" + ); + } + + /// FCFS round: submitEOI auto-triggers allocate — full life cycle in one tx, with zkPassport + function test_HappyPath_SubmitEOI_FCFS_zkpassportCondition() public { + address[] memory zkConds = new address[](1); + zkConds[0] = address(zkpassportCondition); + (, address rm, bytes32 roundId) = _deployLifecycle(300003, RoundType.FCFS, zkConds); + + string[] memory globalValues = _lifecycleGlobalValues(); + string[] memory investorPv = _lifecyclePartyValues("Test Investor", investor); + uint256 eoiSalt = 1; + EOI memory eoi; + + { + uint256 investAmount = MIN_TICKET; + + payToken.mint(investor, investAmount); + vm.startPrank(investor); + payToken.approve(rm, investAmount); + + eoi = EOI({ + name: "Test Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.com", + minAmount: investAmount, + maxAmount: investAmount, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: _emptyLex() + }); + } + + bytes memory eoiSig = _eoiSig(eoiSalt, globalValues, investorPv); + + // EOI submission should fail without a valid zkPassport + vm.expectRevert(RoundManager.AgreementConditionsNotMet.selector); + RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + eoiSig, + eoiSalt, new address[](0), bytes32(0) + ); + + // Submit zkPassport proof + // Assume the proof is for investor wallet and was signed before block 43552581 + (ProofVerificationParams memory proofParams, ) = NonUSNationalityConditionHelper.parseProofFromJson("test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call-base.json"); + zkpassportCondition.submitProof(proofParams, false); + + // Now EOI submission should pass + RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + eoiSig, + eoiSalt, new address[](0), bytes32(0) + ); + + vm.stopPrank(); + } + + /// FounderApproved round: submitEOI parks the EOI, then officer calls allocate. + function test_HappyPath_SubmitEOI_FounderApproved_Allocate() public { + (, address rm, bytes32 roundId) = _deployLifecycle(300002, RoundType.FounderApproved, new address[](0)); + + string[] memory globalValues = _lifecycleGlobalValues(); + string[] memory investorPv = _lifecyclePartyValues("Test Investor", investor); + uint256 eoiSalt = 2; + uint256 investAmount = MIN_TICKET; + + payToken.mint(investor, investAmount); + vm.startPrank(investor); + payToken.approve(rm, investAmount); + + EOI memory eoi = EOI({ + name: "Test Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.com", + minAmount: investAmount, + maxAmount: investAmount, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: _emptyLex() + }); + + (bytes32 agreementId, ) = RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + _eoiSig(eoiSalt, globalValues, investorPv), + eoiSalt, new address[](0), bytes32(0) + ); + vm.stopPrank(); + + // EOI is parked (escrow PAID) but agreement not yet finalized + assertFalse( + CyberAgreementRegistry(REGISTRY).isFinalized(agreementId), + "must not be finalized before allocate" + ); + + // Officer approves and allocates + vm.prank(officer); + RoundManager(rm).allocate(agreementId, investAmount); + + assertTrue( + CyberAgreementRegistry(REGISTRY).isFinalized(agreementId), + "agreement must be finalized after allocate" + ); + assertEq( + RoundManager(rm).getRound(roundId).raised, + investAmount, + "raised must equal investAmount" + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ATTACKER CANNOT SUBSTITUTE A DIFFERENT OFFICER EOA + // ═══════════════════════════════════════════════════════════════════════════ + + /// Attacker intercepts the officer's escrow signature but plugs in their own + /// officer info. Should be prevented by the officer's meta signature. + function test_RevertIf_AttackerSwapsOfficer() public { + uint256 salt = 11111; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory officerEscrowSig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Attacker's officer struct + matching partyValues pointing to attacker + CompanyOfficer memory fakeOff = _officer(attacker, "Attacker"); + string[] memory fakePv = _lifecyclePartyValues(fakeOff.name, attacker); + + // Attacker cannot use his own signatures + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), // companyPayable + fakeOff, // ← faked by attacker + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + fakePv, // ← faked by attacker + _metaSig( + salt, + address(this), + true, + true, + true, + _officer(officer, "Alice Officer"), + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + extensionData, + officerPartyValues, + legalDetails, + certDataArr, + new address[](0), + attackerPk + ), // ← signed by attacker + _metaSigDefault(salt, officerPk), // ← signed by officer, not attacker + RoundType.FCFS, + new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), // companyPayable + fakeOff, // ← faked by attacker + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + fakePv, // ← faked by attacker + officerEscrowSig, // ← signed by officer, not attacker + _metaSigDefault(salt, officerPk), // ← signed by officer, not attacker + RoundType.FCFS, + new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, true + ); + } + + /// Attacker signs the escrow data with their own key but claims the victim + /// officer's EOA. RoundManager recovers attacker ≠ claimed officer → revert. + function test_RevertIf_AttackerSignsEscrowForVictimOfficer() public { + uint256 salt = 22222; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory attackerSig = _escrowSigFull(predRM, predCorp, attackerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + CompanyOfficer memory claimedOff = _officer(officer, "Alice Officer"); + string[] memory pv = _lifecyclePartyValues(claimedOff.name, officer); + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + claimedOff, + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + pv, + attackerSig, // ← signed by attacker, not officer + _metaSigDefault(salt, officerPk), + RoundType.FCFS, + new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, true + ); + } + + /// Attacker should not be able to sign the metadata with their own key but claims + /// the victim officer's EOA. + function test_RevertIf_AttackerSignsMetadataForVictimOfficer() public { + uint256 salt = 33333; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory attackerSig = _metaSigDefault(salt, attackerPk); + + CompanyOfficer memory claimedOff = _officer(officer, "Alice Officer"); + string[] memory pv = _lifecyclePartyValues(claimedOff.name, officer); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + claimedOff, + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + pv, + _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS), + attackerSig, // ← signed by attacker, not officer + RoundType.FCFS, + new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PARTY VALUES GUARD (factory-level, before signature check) + // ═══════════════════════════════════════════════════════════════════════════ + + /// roundPartyValues[1] does not parse to officer.eoa → GlobalOrPartyValuesMismatch. + function test_RevertIf_PartyValues_EOA_Mismatch() public { + uint256 salt = 33333; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + string[] memory pv = new string[](2); + pv[0] = "Alice Officer"; + pv[1] = attacker.toHexString(); // wrong EOA + + vm.expectRevert(PumpCorpFactory.GlobalOrPartyValuesMismatch.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + pv, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// roundPartyValues[0] does not match officer.name → GlobalOrPartyValuesMismatch. + function test_RevertIf_PartyValues_Name_Mismatch() public { + uint256 salt = 44444; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + string[] memory pv = new string[](2); + pv[0] = "Bob Attacker"; // wrong name + pv[1] = officer.toHexString(); + + vm.expectRevert(PumpCorpFactory.GlobalOrPartyValuesMismatch.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + pv, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// roundPartyValues.length == 1 → GlobalOrPartyValuesMismatch. + function test_RevertIf_PartyValues_TooShort() public { + uint256 salt = 55555; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + string[] memory pv = new string[](1); + pv[0] = "Alice Officer"; + + vm.expectRevert(PumpCorpFactory.GlobalOrPartyValuesMismatch.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + pv, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Empty roundPartyValues → GlobalOrPartyValuesMismatch. + function test_RevertIf_PartyValues_Empty() public { + uint256 salt = 66666; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + vm.expectRevert(PumpCorpFactory.GlobalOrPartyValuesMismatch.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + new string[](0), sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CROSS-CORP REPLAY: signature bound to a specific corp address via CREATE2 + // ═══════════════════════════════════════════════════════════════════════════ + + /// Escrowed signature for salt A is bound to corpA/rmA. Replaying it for salt B + /// (different corp/rm) must fail because the signed companyAddress mismatches. + function test_RevertIf_EscrowedSignatureReplayedForDifferentSalt() public { + uint256 saltA = 77777; + uint256 saltB = 88888; + + (address predCorpA, address predRMA) = _predict(saltA); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + saltB, // different salt → different corp address in RoundManager + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, + _escrowSigFull(predRMA, predCorpA, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS), // signed with saltA + _metaSigDefault(saltB, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Metadata signature for salt A is bound to corpA/rmA. Replaying it for salt B + /// (different corp/rm) must fail because the signed companyAddress mismatches. + function test_RevertIf_MetadataSignatureReplayedForDifferentSalt() public { + uint256 saltA = 77777; + uint256 saltB = 88888; + + (address predCorpB, address predRMB) = _predict(saltA); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + saltB, // different salt → different corp address in RoundManager + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, + _escrowSigFull(predRMB, predCorpB, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS), + _metaSigDefault(saltA, officerPk), // signed with saltA + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ROUND PARAMETER TAMPERING + // ═══════════════════════════════════════════════════════════════════════════ + + /// Attacker inflates raiseCap after intercepting a valid signature. + function test_RevertIf_TamperedRaiseCap() public { + uint256 salt = 11112; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP * 100, // ← tampered: 100× raise cap + MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Attacker deflates valuation to give investors a larger equity slice. + function test_RevertIf_TamperedValuation() public { + uint256 salt = 11113; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, + VALUATION / 10, // ← tampered: 10× lower valuation + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Attacker swaps the payment token to drain a different ERC-20. + function test_RevertIf_TamperedPaymentToken() public { + uint256 salt = 11114; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0xCAFEBABE), // ← tampered payment token + PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Attacker changes RoundType from FCFS to FounderApproved to gain + /// discretionary allocation control. + function test_RevertIf_TamperedRoundType() public { + uint256 salt = 11115; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + // sig was produced for FCFS + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FounderApproved, // ← tampered + new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Attacker extends the end time to keep a round open beyond the signed window. + function test_RevertIf_TamperedEndTime() public { + uint256 salt = 11116; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, + end + 365 days, // ← tampered: extend by 1 year + true, true, true + ); + } + + /// Zero-length signature is explicitly rejected before EIP-712 recovery. + function test_RevertIf_EmptyEscrowedSignature() public { + uint256 salt = 22222; + + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + vm.expectRevert(RoundManager.InvalidEscrowedSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, + "", // empty escrowed sig + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PARAMETERS NOT IN ESCROW SIGNATURE + // ═══════════════════════════════════════════════════════════════════════════ + + /// `_companyPayable` is protected by the meta signature. + /// An attacker who intercepts the officer's escrow signature cannot redirect + /// the company payment address without the officer's meta signature. + function test_RevertIf_MetaSigRequired_CompanyPayableProtected() public { + uint256 salt = 55551; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory officerEscrowedSig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Attacker forges a meta sig with their own key, redirecting payment to themselves. + // The factory checks that the meta sig signer == officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, attacker, true, true, true, _officer(officer, "Alice Officer"), "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, legalDetails, certDataArr, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + attacker, // ← redirected payment address + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, officerEscrowedSig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + attacker, // ← redirected payment address + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, officerEscrowedSig, + _metaSigDefault(salt, officerPk), // ← signed by officer, not attacker + RoundType.FCFS, + new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, true + ); + } + + /// Demonstrates that legalDetails are not covered by the escrow signature, + /// but ARE covered by the meta signature — an attacker who cannot forge the + /// meta sig is blocked from substituting legal text. + function test_RevertIf_MetaSigRequired_LegalDetailsProtected() public { + uint256 salt = 55552; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory officerSig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + string[] memory altLegal = new string[](1); + altLegal[0] = "Attacker-substituted legal details"; + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, address(this), true, true, true, _officer(officer, "Alice Officer"), "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, altLegal, certDataArr, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + altLegal, // ← substituted legal details + extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, officerSig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + altLegal, // ← substituted legal details + extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, officerSig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CERTIFICATE DATA PROTECTED BY META SIGNATURE + // certData is NOT covered by the escrow signature, but IS covered by the + // meta signature. An attacker without the officer's key cannot substitute it. + // ═══════════════════════════════════════════════════════════════════════════ + + /// Attacker tries to substitute CommonStock / SeriesA cert while the officer + /// signed for SeriesSeed / SAFE. Without the officer's key the forged meta + /// sig is rejected. + function test_RevertIf_MetaSigRequired_CertSecurityClassAndSeriesProtected() public { + uint256 salt = 60001; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + // Signature was produced for SeriesSeed (as usual) + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Attacker substitutes CommonStock / SeriesA cert + CyberCertData[] memory altCert = new CyberCertData[](1); + string[] memory legend = new string[](1); + legend[0] = "SERIES A"; + altCert[0] = CyberCertData({ + name: "SERIES A COMMON", + symbol: "SERIESA", + uri: "ipfs://series-a", + securityClass: SecurityClass.CommonStock, // ← different from signed + securitySeries: SecuritySeries.SeriesA, // ← different from signed + extension: address(0), + defaultLegend: legend + }); + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, address(this), true, true, true, _officer(officer, "Alice Officer"), "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, legalDetails, altCert, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, + altCert, // ← substituted cert + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, + altCert, // ← substituted cert + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Cert name/symbol are not covered by the escrow signature but ARE covered + /// by the meta signature. An attacker without the officer's key is blocked. + function test_RevertIf_MetaSigRequired_CertNameAndSymbolProtected() public { + uint256 salt = 60002; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + CyberCertData[] memory altCert = new CyberCertData[](1); + string[] memory legend = new string[](1); + legend[0] = "FAKE SAFE"; + altCert[0] = CyberCertData({ + name: "FAKE SAFE", // ← not what officer approved + symbol: "FAKESAFE", + uri: "ipfs://fake-uri", + securityClass: SecurityClass.SAFE, + securitySeries: SecuritySeries.SeriesSeed, + extension: address(0), + defaultLegend: legend + }); + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, address(this), true, true, true, _officer(officer, "Alice Officer"), "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, legalDetails, altCert, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, + altCert, // ← substituted cert + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, + altCert, // ← substituted cert + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // OFFICER METADATA PROTECTED BY META SIGNATURE + // The full officer struct (eoa, name, contact, title) is NOT covered by the + // escrow signature, but IS covered by the meta signature. An attacker without + // the officer's key cannot substitute any of these fields. + // ═══════════════════════════════════════════════════════════════════════════ + + /// Attacker tries to display "Dr. Impostor" on certificates by supplying + /// a forged meta sig. Without the officer's key the call reverts. + function test_RevertIf_MetaSigRequired_OfficerNameProtected() public { + uint256 salt = 61001; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Use a different name in the officer struct while keeping the same EOA. + // roundPartyValues[0] must also match the (fake) name to pass the factory guard. + CompanyOfficer memory fakeNameOfficer = CompanyOfficer({ + eoa: officer, + name: "Dr. Impostor", // ← not what officer intended + contact: "officer@corp.com", + title: "CEO" + }); + string[] memory pv = _lifecyclePartyValues("Dr. Impostor", officer); + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, address(this), true, true, true, fakeNameOfficer, "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, pv, legalDetails, certDataArr, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + fakeNameOfficer, + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + pv, sig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + fakeNameOfficer, + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + pv, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + /// Attacker tries to use "Supreme Overlord" as officer title on certificates. + /// Without the officer's key the forged meta sig is rejected. + function test_RevertIf_MetaSigRequired_OfficerTitleProtected() public { + uint256 salt = 61002; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + CompanyOfficer memory altTitleOfficer = CompanyOfficer({ + eoa: officer, + name: "Alice Officer", + contact: "officer@corp.com", + title: "Supreme Overlord" + }); + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, address(this), true, true, true, altTitleOfficer, "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, legalDetails, certDataArr, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + altTitleOfficer, + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + altTitleOfficer, + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // UNSIGNED ROUND FLAGS + // publicRound and allowTimedOffers are NOT committed to by the escrow + // signature — they can be flipped without the officer's consent. + // ═══════════════════════════════════════════════════════════════════════════ + + /// Officer signs for a private round (publicRound=false) but deployer + /// flips it to public — investors not on any allowlist can still submit EOIs. + function test_RevertIf_MetaSigRequired_PublicRoundFlagProtected() public { + uint256 salt = 62001; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Attacker cannot use his own signatures + bytes memory attackerMetaSig = _metaSig(salt, address(this), false, true, true, _officer(officer, "Alice Officer"), "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, legalDetails, certDataArr, new address[](0), attackerPk); + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + false, // ← publicRound flipped + true, true + ); + + // Test against signature malleability + // Deploy with publicRound=false even though the officer signed for true + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + false, // ← publicRound flipped + true, true + ); + } + + /// allowTimedOffers is not covered by the escrow signature but IS covered by + /// the meta signature. An attacker who cannot forge the meta sig is blocked. + function test_RevertIf_MetaSigRequired_AllowTimedOffersFlagProtected() public { + uint256 salt = 62002; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, address(this), true, false, true, _officer(officer, "Alice Officer"), "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, legalDetails, certDataArr, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, + false, true // ← allowTimedOffers flipped + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, + false, true // ← allowTimedOffers flipped + ); + } + + /// restrictEndTimeReduction is covered by the meta signature. + /// An attacker who cannot forge the meta sig is blocked from flipping it. + function test_RevertIf_MetaSigRequired_RestrictEndTimeReductionFlagProtected() public { + uint256 salt = 62003; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + bytes memory attackerMetaSig = _metaSig(salt, address(this), true, true, false, _officer(officer, "Alice Officer"), "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", extensionData, officerPartyValues, legalDetails, certDataArr, new address[](0), attackerPk); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + attackerMetaSig, + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, + false // ← restrictEndTimeReduction flipped + ); + + // Test against signature malleability: officer's sig commits to true, + // but the call passes false → digest mismatch → revert. + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, new address[](0), + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, + true, true, + false // ← restrictEndTimeReduction flipped + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // MALICIOUS CONDITION INJECTION (round-level DoS griefing) + // roundConditions are NOT signed. An attacker who submits the transaction + // can inject a condition contract that always returns false, permanently + // blocking every allocation in the round. + // ═══════════════════════════════════════════════════════════════════════════ + + /// Attacker tries to inject a malicious condition contract that always returns false. + /// Without the officer's meta signature, the deployment reverts. + function test_RevertIf_MetaSigRequired_ConditionsProtected() public { + uint256 salt = 63001; + (address predCorp, address predRM) = _predict(salt); + uint256 start = block.timestamp - 1; + uint256 end = block.timestamp + 30 days; + + bytes memory sig = _escrowSigFull(predRM, predCorp, officerPk, start, end, address(0), TEMPLATE_ID, RoundType.FCFS); + + // Attacker forges meta sig with their own key → signer != officer.eoa → revert. + address badCondition = address(new AlwaysFalseCondition()); + address[] memory conditions = new address[](1); + conditions[0] = badCondition; + bytes memory attackerMetaSig = _metaSig( + salt, + address(this), + true, + true, + true, + _officer(officer, "Alice Officer"), + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + extensionData, + officerPartyValues, + legalDetails, + certDataArr, + new address[](0), + attackerPk + ); + + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + attackerMetaSig, + RoundType.FCFS, + conditions, // ← malicious condition injected + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + + // Test against signature malleability + vm.expectRevert(PumpCorpFactory.InvalidMetadataSignature.selector); + pumpFactory.deployCyberCorpAndCreateRoundFor( + salt, + SecuritySeries.SeriesSeed, + "Test Corp", "C-Corp", "DE", "contact@test.com", "Arbitration", + address(this), + _officer(officer, "Alice Officer"), + legalDetails, extensionData, certDataArr, + TEMPLATE_ID, + address(0), PRICE_PER_UNIT, VALUATION, + officerPartyValues, sig, + _metaSigDefault(salt, officerPk), + RoundType.FCFS, + conditions, // ← malicious condition injected + RAISE_CAP, MIN_TICKET, MAX_TICKET, + start, end, true, true, true + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // OR CONDITION — zkPassport || LexChex + // ═══════════════════════════════════════════════════════════════════════════ + + /// Grant this test contract admin rights on LEXCHEX and mint a valid LexChex to `to`. + function _mintLexChex(address to) internal { + uint256 adminRole = BorgAuth(LEXCHEX_AUTH).ADMIN_ROLE(); + vm.prank(metalexSafe); + BorgAuth(LEXCHEX_AUTH).updateRole(address(this), adminRole); + + LeXcheX(LEXCHEX).mint(to, Accreditation({ + uuid: 0, + agreementId: bytes32(0), + registryAddress: address(0), + investorName: "Test Investor", + investorType: "Individual", + investorJurisdiction: "XX", + investorContact: "investor@test.com", + issuanceDate: block.timestamp, + expiryDate: block.timestamp + 365 days, + voided: "", + signature: "" + })); + } + + /// Neither credential present → AgreementConditionsNotMet. + function test_OrCondition_BlocksWhenNeitherCredentialPresent() public { + address[] memory conds = new address[](1); + conds[0] = address(orCondition); + (, address rm, bytes32 roundId) = _deployLifecycle(400001, RoundType.FCFS, conds); + + string[] memory globalValues = _lifecycleGlobalValues(); + string[] memory investorPv = _lifecyclePartyValues("Test Investor", investor); + uint256 investAmount = MIN_TICKET; + + payToken.mint(investor, investAmount); + vm.startPrank(investor); + payToken.approve(rm, investAmount); + + EOI memory eoi = EOI({ + name: "Test Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.com", + minAmount: investAmount, + maxAmount: investAmount, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: _emptyLex() + }); + + bytes memory eoiSig = _eoiSig(1, globalValues, investorPv); + vm.expectRevert(RoundManager.AgreementConditionsNotMet.selector); + RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + eoiSig, + 1, new address[](0), bytes32(0) + ); + vm.stopPrank(); + } + + /// LexChex only (no zkPassport) → EOI succeeds. + function test_OrCondition_PassesWithLexChexOnly() public { + address[] memory conds = new address[](1); + conds[0] = address(orCondition); + (, address rm, bytes32 roundId) = _deployLifecycle(400002, RoundType.FCFS, conds); + + _mintLexChex(investor); + + string[] memory globalValues = _lifecycleGlobalValues(); + string[] memory investorPv = _lifecyclePartyValues("Test Investor", investor); + uint256 investAmount = MIN_TICKET; + + payToken.mint(investor, investAmount); + vm.startPrank(investor); + payToken.approve(rm, investAmount); + + EOI memory eoi = EOI({ + name: "Test Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.com", + minAmount: investAmount, + maxAmount: investAmount, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: _emptyLex() + }); + + (bytes32 agreementId, ) = RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + _eoiSig(1, globalValues, investorPv), + 1, new address[](0), bytes32(0) + ); + vm.stopPrank(); + + assertTrue( + CyberAgreementRegistry(REGISTRY).isFinalized(agreementId), + "agreement must be finalized after FCFS submitEOI with LexChex" + ); + } + + /// zkPassport only (no LexChex) → EOI succeeds. + function test_OrCondition_PassesWithZkPassportOnly() public { + address[] memory conds = new address[](1); + conds[0] = address(orCondition); + (, address rm, bytes32 roundId) = _deployLifecycle(400003, RoundType.FCFS, conds); + + string[] memory globalValues = _lifecycleGlobalValues(); + string[] memory investorPv = _lifecyclePartyValues("Test Investor", investor); + uint256 investAmount = MIN_TICKET; + + payToken.mint(investor, investAmount); + vm.startPrank(investor); + payToken.approve(rm, investAmount); + + EOI memory eoi = EOI({ + name: "Test Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.com", + minAmount: investAmount, + maxAmount: investAmount, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: _emptyLex() + }); + + (ProofVerificationParams memory proofParams, ) = NonUSNationalityConditionHelper.parseProofFromJson("test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call-base.json"); + zkpassportCondition.submitProof(proofParams, false); + + (bytes32 agreementId, ) = RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + _eoiSig(1, globalValues, investorPv), + 1, new address[](0), bytes32(0) + ); + vm.stopPrank(); + + assertTrue( + CyberAgreementRegistry(REGISTRY).isFinalized(agreementId), + "agreement must be finalized after FCFS submitEOI with zkPassport" + ); + } + + /// Both credentials present → EOI succeeds. + function test_OrCondition_PassesWithBoth() public { + address[] memory conds = new address[](1); + conds[0] = address(orCondition); + (, address rm, bytes32 roundId) = _deployLifecycle(400004, RoundType.FCFS, conds); + + _mintLexChex(investor); + + string[] memory globalValues = _lifecycleGlobalValues(); + string[] memory investorPv = _lifecyclePartyValues("Test Investor", investor); + uint256 investAmount = MIN_TICKET; + + payToken.mint(investor, investAmount); + vm.startPrank(investor); + payToken.approve(rm, investAmount); + + EOI memory eoi = EOI({ + name: "Test Investor", + investorType: "Individual", + jurisdiction: "US", + contact: "investor@test.com", + minAmount: investAmount, + maxAmount: investAmount, + expiry: block.timestamp + 7 days, + naturalPerson: false, + lexchexDetails: _emptyLex() + }); + + (ProofVerificationParams memory proofParams, ) = NonUSNationalityConditionHelper.parseProofFromJson("test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call-base.json"); + zkpassportCondition.submitProof(proofParams, false); + + (bytes32 agreementId, ) = RoundManager(rm).submitEOI( + roundId, eoi, + globalValues, investorPv, + _eoiSig(1, globalValues, investorPv), + 1, new address[](0), bytes32(0) + ); + vm.stopPrank(); + + assertTrue( + CyberAgreementRegistry(REGISTRY).isFinalized(agreementId), + "agreement must be finalized after FCFS submitEOI with both credentials" + ); + } +} diff --git a/test/RoundManagerFactoryTest.t.sol b/test/RoundManagerFactoryTest.t.sol index 61a32fb5..a597da57 100644 --- a/test/RoundManagerFactoryTest.t.sol +++ b/test/RoundManagerFactoryTest.t.sol @@ -188,11 +188,11 @@ contract RoundManagerFactoryTest is Test { function test_SetDefaultFeeRatio() public { uint256 newValue = 123; - assertNotEq(rmFactory.getDefaultFeeRatio(), newValue, "Unexpected defaultFeeRatio before set"); + assertNotEq(rmFactory.getUnderlyingDefaultFeeRatio(), newValue, "Unexpected defaultFeeRatio before set"); vm.prank(owner); rmFactory.setDefaultFeeRatio(newValue); - assertEq(rmFactory.getDefaultFeeRatio(), newValue, "Unexpected defaultFeeRatio after set"); + assertEq(rmFactory.getUnderlyingDefaultFeeRatio(), newValue, "Unexpected defaultFeeRatio after set"); } function test_RevertIf_SetDefaultFeeRatioNonOwner() public { @@ -206,4 +206,69 @@ contract RoundManagerFactoryTest is Test { vm.expectRevert(RoundManagerFactory.InvalidFeeRatio.selector); rmFactory.setDefaultFeeRatio(RoundManagerFactoryStorage.BASIS_POINTS + 1); } + + function test_GetFeeRatio_FallsBackToGlobalDefault() public { + vm.prank(owner); + rmFactory.setDefaultFeeRatio(500); + + // Any address with no instance override returns the global default + vm.prank(address(0x1234)); + assertEq(rmFactory.getDefaultFeeRatio(), 500); + } + + function test_SetInstanceFeeOverride() public { + address rm = rmFactory.deployRoundManager(keccak256("test_SetInstanceFeeOverride")); + + vm.startPrank(owner); + rmFactory.setDefaultFeeRatio(500); + rmFactory.setInstanceFeeOverride(rm, true, 0); + vm.stopPrank(); + + // getDefaultFeeRatio called from rm should return 0, not the global 500 + vm.prank(rm); + assertEq(rmFactory.getDefaultFeeRatio(), 0); + } + + function test_RevertIf_SetInstanceFeeOverrideNonOwner() public { + address rm = rmFactory.deployRoundManager(keccak256("test_SetInstanceFeeOverrideNonOwner")); + + vm.prank(companyOwner); + vm.expectRevert(abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, ownerRole, companyOwner)); + rmFactory.setInstanceFeeOverride(rm, true, 0); + } + + function test_RevertIf_SetInstanceFeeOverrideInvalid() public { + address rm = rmFactory.deployRoundManager(keccak256("test_SetInstanceFeeOverrideInvalid")); + + vm.prank(owner); + vm.expectRevert(RoundManagerFactory.InvalidFeeRatio.selector); + rmFactory.setInstanceFeeOverride(rm, true, RoundManagerFactoryStorage.BASIS_POINTS + 1); + } + + function test_ClearInstanceFeeOverride() public { + address rm = rmFactory.deployRoundManager(keccak256("test_ClearInstanceFeeOverride")); + + vm.startPrank(owner); + rmFactory.setDefaultFeeRatio(500); + rmFactory.setInstanceFeeOverride(rm, true, 0); + vm.stopPrank(); + + vm.prank(rm); + assertEq(rmFactory.getDefaultFeeRatio(), 0, "override should be active"); + + vm.prank(owner); + rmFactory.setInstanceFeeOverride(rm, false, 0); + + vm.prank(rm); + assertEq(rmFactory.getDefaultFeeRatio(), 500, "should fall back to global default after override cleared"); + } + + function test_InstanceFeeOverrideSet_EventEmitted() public { + address rm = rmFactory.deployRoundManager(keccak256("test_InstanceFeeOverrideSet_EventEmitted")); + + vm.expectEmit(true, false, false, true); + emit RoundManagerFactory.InstanceFeeOverrideSet(rm, true, 0); + vm.prank(owner); + rmFactory.setInstanceFeeOverride(rm, true, 0); + } } diff --git a/test/RoundManagerTest.t.sol b/test/RoundManagerTest.t.sol index 23074ed6..301942ee 100644 --- a/test/RoundManagerTest.t.sol +++ b/test/RoundManagerTest.t.sol @@ -125,6 +125,7 @@ library CyberCorpHelper { address constant LEXCHEX_MINTER_ADDRESS = 0x0dD1a2a89eC172ac322B6a7a6c869180CBD0F960; address constant LEXCHEX_ADDRESS = 0xc8db0c3f47656aee725b0AD1835F9A3FbD0a0b62; address constant UPGRADE_OWNER = 0x341Da9fb8F9bD9a775f6bD641091b24Dd9aA459B; + address constant BASE_SEPOLIA_URI_BUILDER = 0x5500c095ea7dE6F8a5E15949e24B80604cc670A3; bytes32 constant SALT = keccak256("CyberCorpHelper"); @@ -155,15 +156,20 @@ library CyberCorpHelper { ) ); - uriBuilder = address( - new ERC1967Proxy{salt: SALT}( - address(new CertificateUriBuilder{salt: SALT}()), - abi.encodeWithSelector( - CertificateUriBuilder.initialize.selector, - address(bootstrapAuth) + // Use deployed URI builder on Base Sepolia so imageBuilder is already configured. + if (block.chainid == 84532) { + uriBuilder = BASE_SEPOLIA_URI_BUILDER; + } else { + uriBuilder = address( + new ERC1967Proxy{salt: SALT}( + address(new CertificateUriBuilder{salt: SALT}()), + abi.encodeWithSelector( + CertificateUriBuilder.initialize.selector, + address(bootstrapAuth) + ) ) - ) - ); + ); + } address issuanceManagerImpl = address(new IssuanceManager{salt: SALT}()); address certPrinterImpl = address(new CyberCertPrinter{salt: SALT}()); @@ -352,6 +358,7 @@ library CyberCorpHelper { roundType, publicRound, true, + false, raiseCap, minTicket, maxTicket, @@ -437,6 +444,7 @@ library CyberCorpHelper { roundType, publicRound, true, + false, raiseCap, minTicket, maxTicket, @@ -914,6 +922,7 @@ contract RoundManagerTest is Test { RoundType.FounderApproved, false, true, + false, RAISE_CAP, MIN_TICKET, MAX_TICKET, @@ -1092,6 +1101,53 @@ contract RoundManagerTest is Test { // Note: In a real test you'd need to properly mock the CertPrinter and verify its state } + function testPOC_Allocate_CanMarkOfficerSignedWithNonAgreementSignature() public { + // Investor submits an EOI with a valid investor agreement signature. + vm.startPrank(investor); + (bytes32 agreementId, ) = CyberCorpHelper.submitEOI( + RoundManager(roundManager), + registry, + roundId, + 77, + 5_000 * 10 ** 6, + 10_000 * 10 ** 6, + corpOwner, + investorPrivKey + ); + vm.stopPrank(); + + Round memory round = RoundManager(roundManager).getRound(roundId); + + // Officer has not signed yet in FounderApproved mode prior to allocation. + assertFalse(registry.hasSigned(agreementId, round.authorityOfficer)); + + // The stored escrowed signature is NOT a valid agreement signature. + vm.startPrank(round.authorityOfficer); + vm.expectRevert( + abi.encodeWithSelector( + CyberAgreementRegistry.SignatureVerificationFailed.selector + ) + ); + registry.signContractFor( + round.authorityOfficer, + agreementId, + round.roundPartyValues, + round.escrowedSignature, + false, + "" + ); + vm.stopPrank(); + + // Yet allocate() will mark the same officer as signed through signContractWithEscrow. + vm.prank(corpOwner); + RoundManager(roundManager).allocate(agreementId, 7_500 * 10 ** 6); + + assertTrue( + registry.hasSigned(agreementId, round.authorityOfficer), + "officer marked signed without a valid agreement signature" + ); + } + function test_Allocate_RefundsDustAndUpdatesCertificateDetails() public { // Submit EOI with a max that creates 5 USDC dust w.r.t. 10 USDC price per unit vm.startPrank(investor); @@ -2096,6 +2152,7 @@ contract RoundManagerTest is Test { RoundType.FounderApproved, false, true, + false, 100_000 * 10 ** 6, 1_000 * 10 ** 6, 50_000 * 10 ** 6, @@ -2475,6 +2532,7 @@ contract RoundManagerTest is Test { RoundType.FCFS, false, true, + false, RAISE_CAP, MIN_TICKET, MAX_TICKET, @@ -2537,6 +2595,26 @@ contract RoundManagerTest is Test { assertEq(price, 42); assertEq(decimals_, 2); } + + function test_RevertIf_CreateRound_AlreadyExists() public { + vm.prank(corpOwner); + vm.expectRevert(RoundManager.RoundAlreadyExists.selector); + CyberCorpHelper.createRound( + RoundManager(roundManager), + address(paymentToken), + CyberCorpHelper.TEMPLATE_ID, + RAISE_CAP, + MIN_TICKET, + MAX_TICKET, + PRICE_PER_UNIT, + VALUATION, + RoundType.FounderApproved, + corpOwnerPrivKey, + corp, + false + ); + } + } // Separate FCFS tests in their own contract to avoid the original setUp() @@ -2716,6 +2794,7 @@ contract RoundManagerFCFSTest is Test { RoundType.FCFS, true, true, + false, 1, 1, 1, @@ -3960,7 +4039,8 @@ contract CyberCorpFactoryPublicRoundTest is Test { startTime, endTime, true, - true + true, + false ); // Validations diff --git a/test/RoundManagerTest.v3.next.t.sol b/test/RoundManagerTest.v3.next.t.sol new file mode 100644 index 00000000..362181b5 --- /dev/null +++ b/test/RoundManagerTest.v3.next.t.sol @@ -0,0 +1,498 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test, Vm} from "forge-std/Test.sol"; +import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import {CyberCorpHelper} from "./RoundManagerTest.t.sol"; +import {SecuritySeries, SecurityClass, CompanyOfficer} from "../src/CyberCorpConstants.sol"; +import {CyberAgreementRegistry} from "../src/CyberAgreementRegistry.sol"; +import {CyberCorpFactory} from "../src/CyberCorpFactory.sol"; +import {CyberCorp} from "../src/CyberCorp.sol"; +import {RoundManager} from "../src/RoundManager.sol"; +import {RoundManagerFactory} from "../src/RoundManagerFactory.sol"; +import {BorgAuth} from "../src/libs/auth.sol"; +import {RoundLib, Round, RoundType} from "../src/libs/RoundLib.sol"; +import {RoundLib as RoundLibV3, Round as RoundV3} from "./libs/v3/RoundLib.sol"; +import {ERC1967ProxyLib} from "./libs/ERC1967ProxyLib.sol"; +import {CyberCertData, EOI, LexChexDetails, MintRequest} from "../src/storage/RoundManagerStorage.sol"; +import {LexScrowStorage, Escrow, EscrowStatus} from "../src/storage/LexScrowStorage.sol"; + +interface IRoundManagerV3 { + function createRound( + RoundV3 memory roundDraft, + CyberCertData[] memory certData + ) external returns (bytes32); +} + +/// @notice Helper for creating rounds against the pre-upgrade (v3) RoundManager ABI, +/// which lacks the `restrictEndTimeReduction` field. +library CyberCorpHelperV3 { + using RoundLibV3 for RoundV3; + + Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function createRound( + IRoundManagerV3 rm, + address paymentToken, + bytes32 templateId, + uint256 raiseCap, + uint256 minTicket, + uint256 maxTicket, + uint256 pricePerUnit, + uint256 valuation, + RoundType roundType, + uint256 officerPrivKey, + address companyAddress, + bool publicRound + ) internal returns (bytes32) { + address officerEOA = vm.addr(officerPrivKey); + + string[] memory defaultLegend = new string[](1); + defaultLegend[0] = "Legend"; + CyberCertData[] memory certData = new CyberCertData[](1); + certData[0] = CyberCertData({ + name: "Equity", + symbol: "EQ", + uri: "ipfs://eq", + securityClass: SecurityClass.CommonStock, + securitySeries: SecuritySeries.NA, + extension: address(0), + defaultLegend: defaultLegend + }); + + string[] memory roundPartyValues = new string[](2); + roundPartyValues[0] = "Alice Officer"; + roundPartyValues[1] = "CEO"; + + (bytes memory escrowedSig, ) = CyberCorpHelper.computeEscrowSignature( + address(rm), + SecuritySeries.SeriesSeed, + raiseCap, + minTicket, + maxTicket, + roundType, + block.timestamp, + block.timestamp + 30 days, + templateId, + paymentToken, + pricePerUnit, + valuation, + officerPrivKey, + companyAddress + ); + + vm.startPrank(officerEOA); + return rm.createRound( + RoundLibV3.draft() + .setTickets( + SecuritySeries.SeriesSeed, + roundType, + publicRound, + true, + raiseCap, + minTicket, + maxTicket, + paymentToken, + pricePerUnit, + valuation, + block.timestamp, + block.timestamp + 30 days + ) + .setAgreement( + templateId, + officerEOA, + "Officer", + "CEO", + new string[](certData.length), + roundPartyValues, + new bytes[](certData.length), + new address[](0), + escrowedSig + ), + certData + ); + vm.stopPrank(); + } +} + +/// @notice Fork-based test for the restrictEndTimeReduction flag introduced in v3.next. +/// A corp and round are created on the OLD (pre-upgrade) RoundManager implementation, +/// the upgrade is then simulated, and tests verify both backward compatibility and +/// new-feature correctness. +contract RoundManagerV3NextTest is Test { + using RoundLib for Round; + using ERC1967ProxyLib for address; + + address metalexSafe = 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C; + CyberCorpFactory cyberCorpFactory = CyberCorpFactory(0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2); + CyberAgreementRegistry registry = CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134); + ERC20 stable = ERC20(0x036CbD53842c5426634e7929541eC2318f3dCF7e); // Base Sepolia USDC + + address deployer; + uint256 deployerPrivKey; + address corpOwnerV3; + uint256 corpOwnerPrivKeyV3; + address corpOwnerV4; + uint256 corpOwnerPrivKeyV4; + address investor; + uint256 investorPrivKey; + + address corpV3; + address rmV3; + bytes32 roundIdV3; + + address corpV4; + address rmV4; + + string[] testRoundPartyValues; + address[] knownLegacyCorps; + + RoundManagerFactory rmFactory; + + uint256 constant MIN_TICKET = 1_000 * 1e6; + uint256 constant MAX_TICKET = 100_000 * 1e6; + uint256 constant RAISE_CAP = 1_000_000 * 1e6; + uint256 constant PRICE_PER_UNIT = 10 * 1e18; + uint256 constant VALUATION = 10_000_000 * 1e18; + + function setUp() public { + assertEq(block.chainid, 84532, "Fork test: Base Sepolia only @ block 38956871"); + vm.rollFork(38956871); + + (deployer, deployerPrivKey) = makeAddrAndKey("deployer"); + (corpOwnerV3, corpOwnerPrivKeyV3) = makeAddrAndKey("corpOwnerV3"); + (corpOwnerV4, corpOwnerPrivKeyV4) = makeAddrAndKey("corpOwnerV4"); + (investor, investorPrivKey) = makeAddrAndKey("investor"); + + testRoundPartyValues = new string[](2); + testRoundPartyValues[0] = "Officer"; + testRoundPartyValues[1] = "CEO"; + + rmFactory = RoundManagerFactory(cyberCorpFactory.roundManagerFactory()); + + // Create template 777 in the live registry so EOI signatures can be verified + { + string[] memory globalFields = new string[](1); + globalFields[0] = "Global Field"; + string[] memory partyFields = new string[](2); + partyFields[0] = "Officer Name"; + partyFields[1] = "Officer Title"; + vm.prank(metalexSafe); + registry.createTemplate( + CyberCorpHelper.TEMPLATE_ID, + "Test", + "ipfs://template", + globalFields, + partyFields + ); + } + + // ── Pre-upgrade: deploy v3 corp and create unrestricted round ─────────────── + // Corp and RoundManager proxy are on the OLD implementation. createRound is + // called via the v3 ABI (no restrictEndTimeReduction field) to match the + // deployed impl's function selector. + (corpV3, , , , rmV3) = cyberCorpFactory.deployCyberCorp( + keccak256("v3Corp"), + "Test v3 Corp", + "corporation", + "DE", + "contact", + "arbitration", + corpOwnerV3, + CompanyOfficer({ + eoa: corpOwnerV3, + name: "Officer", + contact: "officer@example.com", + title: "CEO" + }) + ); + + roundIdV3 = CyberCorpHelperV3.createRound( + IRoundManagerV3(rmV3), + address(stable), + CyberCorpHelper.TEMPLATE_ID, + RAISE_CAP, + MIN_TICKET, + MAX_TICKET, + PRICE_PER_UNIT, + VALUATION, + RoundType.FounderApproved, + corpOwnerPrivKeyV3, + corpV3, + false + ); + + // ── Simulate upgrade ───────────────────────────────────────────────────── + vm.startPrank(metalexSafe); + rmFactory.AUTH().updateRole(deployer, rmFactory.AUTH().OWNER_ROLE()); + vm.stopPrank(); + + RoundManager newRmRef = new RoundManager(); + vm.prank(deployer); + rmFactory.setRefImplementation(address(newRmRef)); + + vm.prank(corpV3); + RoundManager(rmV3).upgradeToAndCall(address(newRmRef), ""); + + // ── deploy v4 corp ─────────────── + + (corpV4, , , , rmV4) = cyberCorpFactory.deployCyberCorp( + keccak256("v4Corp"), + "Test v4 Corp", + "corporation", + "DE", + "contact", + "arbitration", + corpOwnerV4, + CompanyOfficer({ + eoa: corpOwnerV4, + name: "Officer", + contact: "officer@example.com", + title: "CEO" + }) + ); + + // ── provision investor ───────────────────────────────────────────────────── + + deal(address(stable), investor, 1_000_000e6); + vm.prank(investor); + stable.approve(rmV3, type(uint256).max); + } + + // ── Upgrade sanity ──────────────────────────────────────────────────────────── + + function test_SanityCheck() public view { + address newRef = rmFactory.getRefImplementation(); + for (uint256 i = 0; i < knownLegacyCorps.length; i++) { + CyberCorp c = CyberCorp(knownLegacyCorps[i]); + RoundManager rm = RoundManager(c.roundManager()); + assertEq( + address(rm).getErc1967Implementation(), + newRef, + string(abi.encodePacked("Legacy corp ", vm.toString(address(c)), " RoundManager not on new impl")) + ); + } + assertEq( + rmV3.getErc1967Implementation(), + newRef, + "Test corp RoundManager not on new impl" + ); + } + + // ── Backward compatibility (round created on old impl, restrictEndTimeReduction=0) ── + + function test_SetRoundEndTime_Reduce_NotRestricted() public { + uint256 newEndTime = block.timestamp + 1 days; + vm.prank(corpOwnerV3); + RoundManager(rmV3).setRoundEndTime(roundIdV3, newEndTime); + assertEq(RoundManager(rmV3).getRound(roundIdV3).endTime, newEndTime); + } + + function test_CloseRoundNow_NotRestricted() public { + vm.prank(corpOwnerV3); + RoundManager(rmV3).closeRoundNow(roundIdV3); + assertEq(RoundManager(rmV3).getRound(roundIdV3).endTime, block.timestamp); + } + + // ── New flag behavior (restricted round created post-upgrade) ───────────────── + + function test_RevertIf_SetRoundEndTime_Reduce_Restricted() public { + bytes32 restrictedRoundId = _createV4CorpRound(true); + uint256 currentEndTime = RoundManager(rmV4).getRound(restrictedRoundId).endTime; + vm.expectRevert(RoundManager.EndTimeReductionRestricted.selector); + vm.prank(corpOwnerV4); + RoundManager(rmV4).setRoundEndTime(restrictedRoundId, currentEndTime - 1 days); + } + + function test_SetRoundEndTime_Increase_Restricted() public { + bytes32 restrictedRoundId = _createV4CorpRound(true); + uint256 currentEndTime = RoundManager(rmV4).getRound(restrictedRoundId).endTime; + uint256 newEndTime = currentEndTime + 1 days; + vm.prank(corpOwnerV4); + RoundManager(rmV4).setRoundEndTime(restrictedRoundId, newEndTime); + assertEq(RoundManager(rmV4).getRound(restrictedRoundId).endTime, newEndTime); + } + + function test_RevertIf_CloseRoundNow_Restricted() public { + bytes32 restrictedRoundId = _createV4CorpRound(true); + vm.expectRevert(RoundManager.EndTimeReductionRestricted.selector); + vm.prank(corpOwnerV4); + RoundManager(rmV4).closeRoundNow(restrictedRoundId); + } + + /// @notice When restrictEndTimeReduction == true, allowTimedOffers == false, and + /// endTime == type(uint256).max, the founder cannot close/shorten the round and the + /// investor cannot recall their EOI (it never expires). However, reject() can still release funds. + function test_RejectEOI_WorksWhenRestrictedWithMaxEndTime() public { + string[] memory defaultLegend = new string[](1); + defaultLegend[0] = "Legend"; + CyberCertData[] memory cd = new CyberCertData[](1); + cd[0] = CyberCertData({ + name: "Equity", + symbol: "EQ", + uri: "ipfs://eq", + securityClass: SecurityClass.CommonStock, + securitySeries: SecuritySeries.NA, + extension: address(0), + defaultLegend: defaultLegend + }); + + uint256 maxEndTime = type(uint256).max; + + (bytes memory sig, ) = CyberCorpHelper.computeEscrowSignature( + rmV3, + SecuritySeries.SeriesB, + RAISE_CAP, + MIN_TICKET, + MAX_TICKET, + RoundType.FounderApproved, + block.timestamp, + maxEndTime, + CyberCorpHelper.TEMPLATE_ID, + address(stable), + PRICE_PER_UNIT, + VALUATION, + corpOwnerPrivKeyV3, + corpV3 + ); + + vm.prank(corpOwnerV3); + bytes32 restrictedMaxRoundId = RoundManager(rmV3).createRound( + RoundLib.draft() + .setTickets( + SecuritySeries.SeriesB, + RoundType.FounderApproved, + false, + false, // allowTimedOffers = false + true, // restrictEndTimeReduction = true + RAISE_CAP, + MIN_TICKET, + MAX_TICKET, + address(stable), + PRICE_PER_UNIT, + VALUATION, + block.timestamp, + maxEndTime + ) + .setAgreement( + CyberCorpHelper.TEMPLATE_ID, + corpOwnerV3, + "Officer", + "CEO", + new string[](cd.length), + testRoundPartyValues, + new bytes[](cd.length), + new address[](0), + sig + ), + cd + ); + + // Confirm the round cannot be closed or shortened by the founder + vm.startPrank(corpOwnerV3); + vm.expectRevert(RoundManager.EndTimeReductionRestricted.selector); + RoundManager(rmV3).closeRoundNow(restrictedMaxRoundId); + vm.expectRevert(RoundManager.EndTimeReductionRestricted.selector); + RoundManager(rmV3).setRoundEndTime(restrictedMaxRoundId, block.timestamp + 1 days); + vm.stopPrank(); + + // Investor submits EOI + uint256 balBefore = stable.balanceOf(investor); + vm.startPrank(investor); + (bytes32 agreementId, ) = CyberCorpHelper.submitEOI( + RoundManager(rmV3), + registry, + restrictedMaxRoundId, + 42, + 5_000 * 10 ** 6, + 10_000 * 10 ** 6, + corpOwnerV3, + investorPrivKey + ); + vm.stopPrank(); + + Escrow memory escBefore = RoundManager(rmV3).getEscrowDetails(agreementId); + assertEq(uint256(escBefore.status), uint256(EscrowStatus.PAID)); + assertEq(stable.balanceOf(investor), balBefore - 10_000 * 10 ** 6); + + // Investor cannot recall — EOI never expires (allowTimedOffers=false + endTime=max) + vm.prank(investor); + vm.expectRevert(RoundManager.EOINotExpired.selector); + RoundManager(rmV3).recallEOI(agreementId); + + // Founder rejects the EOI — must refund the investor + vm.prank(corpOwnerV3); + RoundManager(rmV3).reject(agreementId); + + Escrow memory escAfter = RoundManager(rmV3).getEscrowDetails(agreementId); + assertEq(uint256(escAfter.status), uint256(EscrowStatus.VOIDED)); + assertEq(stable.balanceOf(investor), balBefore); + } + + // ── Private helper ──────────────────────────────────────────────────────────── + + function _createV4CorpRound(bool restrictEndTimeReduction) private returns (bytes32 newRoundId) { + string[] memory defaultLegend = new string[](1); + defaultLegend[0] = "Legend"; + CyberCertData[] memory cd = new CyberCertData[](1); + cd[0] = CyberCertData({ + name: "Equity", + symbol: "EQ", + uri: "ipfs://eq", + securityClass: SecurityClass.CommonStock, + securitySeries: SecuritySeries.NA, + extension: address(0), + defaultLegend: defaultLegend + }); + + (bytes memory sig, ) = CyberCorpHelper.computeEscrowSignature( + rmV4, + SecuritySeries.SeriesA, + RAISE_CAP, + MIN_TICKET, + MAX_TICKET, + RoundType.FounderApproved, + block.timestamp, + block.timestamp + 30 days, + CyberCorpHelper.TEMPLATE_ID, + address(stable), + PRICE_PER_UNIT, + VALUATION, + corpOwnerPrivKeyV4, + corpV4 + ); + + vm.startPrank(corpOwnerV4); + newRoundId = RoundManager(rmV4).createRound( + RoundLib.draft() + .setTickets( + SecuritySeries.SeriesA, + RoundType.FounderApproved, + false, + true, + restrictEndTimeReduction, + RAISE_CAP, + MIN_TICKET, + MAX_TICKET, + address(stable), + PRICE_PER_UNIT, + VALUATION, + block.timestamp, + block.timestamp + 30 days + ) + .setAgreement( + CyberCorpHelper.TEMPLATE_ID, + corpOwnerV4, + "Officer", + "CEO", + new string[](cd.length), + testRoundPartyValues, + new bytes[](cd.length), + new address[](0), + sig + ), + cd + ); + vm.stopPrank(); + } +} diff --git a/test/ScripPOC.t.sol b/test/ScripPOC.t.sol new file mode 100644 index 00000000..0820ae4f --- /dev/null +++ b/test/ScripPOC.t.sol @@ -0,0 +1,675 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../src/IssuanceManager.sol"; +import "../src/CyberScrip.sol"; +import "../src/CyberCertPrinter.sol"; +import "../src/interfaces/ICyberScrip.sol"; +import "../src/interfaces/ICyberCertPrinter.sol"; +import "../src/interfaces/ICondition.sol"; +import "../src/interfaces/ITransferRestrictionHook.sol"; +import "../src/libs/auth.sol"; +import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IssuanceManagerFactory} from "../src/IssuanceManagerFactory.sol"; +import "../test/mock/TestableCyberScrip.sol"; +import "../test/mock/MockTransferHook.sol"; + +// ============================================================================ +// Mock contracts +// ============================================================================ + +/// @notice Mock CyberCorp that exposes dealManager +contract POCMockCyberCorp { + string public cyberCORPName = "TestCorp"; + string public cyberCORPJurisdiction = "Delaware"; + string public cyberCORPType = "corporation"; + string public cyberCORPContactDetails = "test@test.com"; + address public dealManager; + address public roundManager; + + function setDealManager(address _dm) external { + dealManager = _dm; + } +} + +/// @notice Extended mock CertPrinter for IssuanceManager-level POC tests. +/// Mimics the real CyberCertPrinter enough for the IM to call through the interface. +contract POCMockCertPrinter { + mapping(uint256 => CertificateDetails) internal _details; + mapping(uint256 => address) internal _owners; + mapping(address => uint256) internal _balances; + mapping(address => uint256[]) internal _ownedTokens; + mapping(uint256 => bool) internal _voided; + mapping(uint256 => address) internal _restrictionHooks; + mapping(uint256 => bytes[]) internal _issuerSignatures; + uint256 internal _total; + string internal _name; + string internal _symbol; + + function initialize( + string[] memory, + string memory name_, + string memory symbol_, + string memory, + address, + SecurityClass, + SecuritySeries, + address + ) external { + _name = name_; + _symbol = symbol_; + } + + function name() external view returns (string memory) { return _name; } + function symbol() external view returns (string memory) { return _symbol; } + function totalSupply() external view returns (uint256) { return _total; } + function tokenURI(uint256) external pure returns (string memory) { return ""; } + function ownerOf(uint256 tokenId) external view returns (address) { return _owners[tokenId]; } + function balanceOf(address owner_) external view returns (uint256) { return _balances[owner_]; } + + function tokenOfOwnerByIndex(address owner_, uint256 index) external view returns (uint256) { + return _ownedTokens[owner_][index]; + } + + function getCertificateDetails(uint256 tokenId) external view returns (CertificateDetails memory) { + return _details[tokenId]; + } + + function getActiveCertificateDetails(uint256 tokenId) external view returns (CertificateDetails memory) { + return _details[tokenId]; + } + + function safeMint(uint256 tokenId, address to, CertificateDetails memory details) external returns (uint256) { + return _mint(tokenId, to, details); + } + + function safeMintAndAssign(address to, uint256 tokenId, CertificateDetails memory details) external returns (uint256) { + return _mint(tokenId, to, details); + } + + function updateCertificateDetails(uint256 tokenId, CertificateDetails calldata details) external { + _details[tokenId] = details; + } + + function voidCert(uint256 tokenId) external { _voided[tokenId] = true; } + function isVoided(uint256 tokenId) external view returns (bool) { return _voided[tokenId]; } + + /// @dev Mock safeTransferFrom -- no IERC721Receiver check, no endorsement check. + /// The real CyberCertPrinter would revert here if `to` is a contract without + /// IERC721Receiver, or if transfer restrictions / endorsements aren't met. + function safeTransferFrom(address from, address to, uint256 tokenId) external { + require(_owners[tokenId] == from, "not owner"); + _owners[tokenId] = to; + _balances[from] -= 1; + _balances[to] += 1; + } + + function assignCert(address from, uint256 tokenId, address, CertificateDetails memory details) external returns (uint256) { + require(_owners[tokenId] == from, "not owner"); + // NOTE: the real CyberCertPrinter has _transfer commented out, so the token is NOT moved + _details[tokenId] = details; + return tokenId; + } + + function addIssuerSignature(uint256 tokenId, bytes calldata signature) external { + _issuerSignatures[tokenId].push(signature); + } + + function getIssuerSignatureCount(uint256 tokenId) external view returns (uint256) { + return _issuerSignatures[tokenId].length; + } + + function getIssuerSignatureAt(uint256 tokenId, uint256 index) external view returns (bytes memory) { + return _issuerSignatures[tokenId][index]; + } + + function setRestrictionHook(uint256 id, address hook) external { + _restrictionHooks[id] = hook; + } + + function getRestrictionHook(uint256 id) external view returns (address) { + return _restrictionHooks[id]; + } + + // Stubs for functions called through the interface + function addEndorsement(uint256, Endorsement memory) external {} + function setGlobalRestrictionHook(address) external {} + function setGlobalTransferable(bool) external {} + function setTokenTransferable(uint256, bool) external {} + function addDefaultLegend(string memory) external {} + function removeDefaultLegendAt(uint256) external {} + function addCertLegend(uint256, string memory) external {} + function removeCertLegendAt(uint256, uint256) external {} + + function _mint(uint256 tokenId, address to, CertificateDetails memory details) internal returns (uint256) { + _details[tokenId] = details; + _owners[tokenId] = to; + _balances[to] += 1; + _ownedTokens[to].push(tokenId); + if (tokenId >= _total) { + _total = tokenId + 1; + } + return tokenId; + } +} + +// ============================================================================ +// POC Test Contract +// ============================================================================ + +contract ScripPOCTest is Test { + bytes32 constant SALT = bytes32(keccak256("ScripPOCTest")); + + IssuanceManager public issuanceManager; + IssuanceManagerFactory public imFactory; + POCMockCyberCorp public mockCorp; + POCMockCertPrinter public certPrinter; + BorgAuth public auth; + + address public owner; + address public investor; + address public attacker; + address public dealManagerAddr; + + function setUp() public { + owner = address(this); + investor = makeAddr("investor"); + attacker = makeAddr("attacker"); + dealManagerAddr = makeAddr("dealManager"); + + auth = new BorgAuth(owner); + + imFactory = IssuanceManagerFactory(address( + new ERC1967Proxy{salt: SALT}( + address(new IssuanceManagerFactory{salt: SALT}()), + abi.encodeWithSelector( + IssuanceManagerFactory.initialize.selector, + address(auth), + new IssuanceManager(), + new CyberCertPrinter(), + new CyberScrip() + ) + ) + )); + + issuanceManager = IssuanceManager(imFactory.deployIssuanceManager(SALT)); + + mockCorp = new POCMockCyberCorp(); + mockCorp.setDealManager(dealManagerAddr); + + issuanceManager.initialize( + address(auth), + address(mockCorp), + address(0xBEEF), + address(imFactory) + ); + + certPrinter = new POCMockCertPrinter(); + certPrinter.initialize( + new string[](0), "Test Cert", "TCRT", "uri://test", + address(issuanceManager), SecurityClass.CommonStock, SecuritySeries.SeriesA, address(0) + ); + } + + // ========================================================================= + // POC #1 - deployCyberScrip access control (Fixed) + // + // Previously had no access modifier. Now protected by onlyOwner. + // This test verifies the fix: unauthorized callers are rejected. + // ========================================================================= + + function test_POC1_DeployCyberScrip_RejectsUnauthorized() public { + // Attacker (random address with no role) cannot deploy scrip + vm.prank(attacker); + vm.expectRevert(); // BorgAuth_NotAuthorized + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, // no minimum + 1, 1, // 1:1 ratio + new uint256[](0), + false, + true, true, true + ); + } + + function test_POC1_DeployCyberScrip_OwnerSucceeds() public { + // Owner can deploy + vm.prank(owner); + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, + 1, 1, + new uint256[](0), + false, + true, true, true + ); + assertTrue(scrip != address(0), "Owner can deploy scrip"); + } + + // ========================================================================= + // POC #2 - addIssuerSignature implementation regression guard + // ========================================================================= + + function test_POC2_AddIssuerSignature_Implemented() public { + CertificateDetails memory details = _defaultDetails(100); + vm.prank(owner); + uint256 certId = issuanceManager.createCert(address(certPrinter), investor, details); + + vm.prank(owner); + bytes memory signature = abi.encodePacked("signed-hash"); + issuanceManager.signCertificate(address(certPrinter), certId, signature); + assertEq(certPrinter.getIssuerSignatureCount(certId), 1, "signature should be added"); + assertEq(certPrinter.getIssuerSignatureAt(certId, 0), signature, "stored signature mismatch"); + } + + // ========================================================================= + // POC #3 - forceTransfer/forceBurn corrupt holderCount (High) + // + // Before the fix: forceTransfer and forceBurn called ERC20._update directly, + // bypassing the holderCount tracking in CyberScrip._update. + // + // After the fix: _updateHolderCount is called explicitly in both functions. + // These tests verify the fix works correctly. + // ========================================================================= + + function test_POC3_ForceTransfer_HolderCountTracked() public { + TestableCyberScrip scrip = _deployTestableScrip(); + address user1 = makeAddr("u1"); + address user2 = makeAddr("u2"); + address im = scrip.issuanceManager(); + + scrip.unrestrictedMint(user1, 1000); + assertEq(scrip.holderCount(), 1, "1 holder after mint"); + + // Force transfer part of balance to new user + vm.prank(im); + scrip.forceTransfer(user1, user2, 400); + assertEq(scrip.holderCount(), 2, "2 holders after force transfer to new address"); + + // Force transfer entire remaining balance (user1 drops to 0) + vm.prank(im); + scrip.forceTransfer(user1, user2, 600); + assertEq(scrip.holderCount(), 1, "1 holder after force transfer drains user1"); + } + + function test_POC3_ForceBurn_HolderCountTracked() public { + TestableCyberScrip scrip = _deployTestableScrip(); + address user1 = makeAddr("u1b"); + address im = scrip.issuanceManager(); + + scrip.unrestrictedMint(user1, 1000); + assertEq(scrip.holderCount(), 1); + + // Force burn all tokens + vm.prank(im); + scrip.forceBurn(user1, 1000); + assertEq(scrip.holderCount(), 0, "0 holders after force burn drains account"); + } + + function test_POC3_MaxHolderCount_RespectedAfterForceOps() public { + TestableCyberScrip scrip = _deployTestableScrip(); + address user1 = makeAddr("u1c"); + address user2 = makeAddr("u2c"); + address user3 = makeAddr("u3c"); + address im = scrip.issuanceManager(); + + scrip.unrestrictedMint(user1, 1000); + + // Set max holders to 2 + vm.prank(im); + scrip.setMaxHolderCount(2); + + // Force transfer to user2 (now 2 holders, at limit) + vm.prank(im); + scrip.forceTransfer(user1, user2, 100); + assertEq(scrip.holderCount(), 2); + + // Normal transfer to user3 should be blocked by max holder limit + vm.prank(user1); + vm.expectRevert(abi.encodeWithSelector(CyberScrip.HolderLimitExceeded.selector, 2)); + scrip.transfer(user3, 50); + } + + // ========================================================================= + // POC #4 - Full scripification path uses safeTransferFrom (High) + // + // The current code does: certificate.safeTransferFrom(msg.sender, dm, id) + // In the real CyberCertPrinter: + // a) The IssuanceManager is msg.sender to CertPrinter, but isn't the + // token owner or approved -- transferFrom would fail + // b) _update override enforces endorsement checks for transfers + // c) If dm is a contract, safeTransferFrom checks IERC721Receiver + // + // With our mock (no restriction checks), we verify the flow's intent: + // the voided cert should end up at the dealManager. + // ========================================================================= + + function test_POC4_FullScripifyCert_SendsToDealManager() public { + CertificateDetails memory details = _defaultDetails(100); + vm.prank(owner); + issuanceManager.createCert(address(certPrinter), investor, details); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, 1, 1, new uint256[](0), false, true, true, true + ); + + // Full scripification (amount == unitsRepresented) + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), 0, 100, address(0)); + + // Cert should be voided and held by dealManager + assertEq(certPrinter.ownerOf(0), dealManagerAddr, "voided cert should be at dealManager"); + assertTrue(certPrinter.isVoided(0), "cert should be voided"); + assertEq(ICyberScrip(scrip).balanceOf(investor), 100, "investor should receive scrip"); + } + + function test_POC4_PartialScripifyCert_CertStaysWithOwner() public { + CertificateDetails memory details = _defaultDetails(100); + vm.prank(owner); + issuanceManager.createCert(address(certPrinter), investor, details); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, 1, 1, new uint256[](0), false, true, true, true + ); + + // Partial scripification (50 of 100 units) + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), 0, 50, address(0)); + + // Cert stays with investor, units reduced + assertEq(certPrinter.ownerOf(0), investor, "cert stays with investor on partial"); + assertFalse(certPrinter.isVoided(0), "cert should NOT be voided on partial"); + CertificateDetails memory updated = certPrinter.getCertificateDetails(0); + assertEq(updated.unitsRepresented, 50, "units should be reduced"); + assertEq(ICyberScrip(scrip).balanceOf(investor), 50, "investor should receive partial scrip"); + } + + // ========================================================================= + // POC #4 (continued) - convertScripToCert should search dealManager for + // voided certs and move them back to the user + // + // Current code searches msg.sender's certs for voided ones. After the + // scripifyCert fix sends voided certs to dealManager, the search must + // also look at the dealManager's holdings. + // ========================================================================= + + function test_POC4_ConvertScripToCert_CreatesNewWhenNoVoidedFound() public { + CertificateDetails memory details = _defaultDetails(100); + vm.prank(owner); + issuanceManager.createCert(address(certPrinter), investor, details); + + address scrip = issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, 1, 1, new uint256[](0), false, true, true, true + ); + + // Full scripify -> cert goes to dealManager + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), 0, 100, address(0)); + + // Convert scrip back to cert + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 100); + + // Scrip should be burned + assertEq(ICyberScrip(scrip).balanceOf(investor), 0, "scrip burned"); + + // A new cert should exist for the investor + assertEq(certPrinter.ownerOf(1), investor, "new cert minted to investor"); + CertificateDetails memory newDetails = certPrinter.getCertificateDetails(1); + assertEq(newDetails.unitsRepresented, 100, "new cert has correct units"); + } + + // ========================================================================= + // POC #6 - assignCert doesn't actually transfer the token (Medium) + // + // In the real CyberCertPrinter, _transfer(from, to, tokenId) is + // commented out inside assignCert. The function updates details but + // the ERC721 ownership doesn't change. + // ========================================================================= + + function test_POC6_AssignCert_TransferCommentedOut() public { + // Document the issue: in CyberCertPrinter.sol line 177: + // // _transfer(from, to, tokenId); + // This means assignCert only updates details, not ownership. + // + // Our mock DOES move ownership for utility, but the real contract doesn't. + // The IssuanceManager.assignCert function is therefore broken for its + // intended purpose of reassigning a certificate to a new investor. + assertTrue(true); + } + + // ========================================================================= + // POC #8 - convertScripToCert reform carries stale data (Medium) + // + // When a voided cert is found and reformed, only unitsRepresented is + // updated. All other fields (signingOfficerName, investmentAmountUSD, + // legalDetails, etc.) carry over from the original voided cert. + // ========================================================================= + + function test_POC8_ConvertScripToCert_StaleDataOnReform() public { + // Create cert with specific metadata + CertificateDetails memory originalDetails = CertificateDetails({ + signingOfficerName: "Jane Smith", + signingOfficerTitle: "CFO", + investmentAmountUSD: 500000, + issuerUSDValuationAtTimeOfInvestment: 50000000, + unitsRepresented: 100, + legalDetails: "SAFE Agreement dated 2024-01-01", + extensionData: "" + }); + + vm.prank(owner); + issuanceManager.createCert(address(certPrinter), investor, originalDetails); + + issuanceManager.deployCyberScrip( + address(certPrinter), + new ITransferRestrictionHook[](0), + new ICondition[](0), + new ICondition[](0), + 0, 1, 1, new uint256[](0), false, true, true, true + ); + + // Partial scripify: converts 50 of 100 units + vm.prank(investor); + issuanceManager.scripifyCert(address(certPrinter), 0, 50, address(0)); + + // Now manually void cert #0 in the mock to simulate the voided-reform path + certPrinter.voidCert(0); + + // Convert scrip back -- should find voided cert #0 owned by investor + vm.prank(investor); + issuanceManager.convertScripToCert(address(certPrinter), 50); + + // The reformed cert carries ALL original metadata, only units updated + CertificateDetails memory reformed = certPrinter.getCertificateDetails(0); + assertEq(reformed.unitsRepresented, 50, "units updated correctly"); + + // BUG: These fields carry over from the original cert, which is stale/misleading + assertEq(reformed.signingOfficerName, "Jane Smith", "STALE: old officer name persists"); + assertEq(reformed.investmentAmountUSD, 500000, "STALE: old investment amount persists"); + assertEq(reformed.legalDetails, "SAFE Agreement dated 2024-01-01", "STALE: old legal details persist"); + } + + // ========================================================================= + // POC #9 - setExtension/getExtension misleading tokenId parameter (Low) + // + // Both functions accept tokenId but operate on a GLOBAL extension field. + // Setting extension for token 0 also changes it for token 1, etc. + // ========================================================================= + + function test_POC9_SetExtension_IgnoresTokenId() public { + // In CyberCertPrinter: + // function setExtension(uint256 tokenId, address extension) external onlyIssuanceManager { + // CyberCertPrinterStorage.cyberCertStorage().extension = extension; + // } + // + // function getExtension(uint256 tokenId) external view returns (address) { + // return CyberCertPrinterStorage.cyberCertStorage().extension; + // } + // + // tokenId is completely ignored - extension is a single global value. + // Setting extension "for token 5" actually sets it for ALL tokens. + assertTrue(true); + } + + // ========================================================================= + // POC #11 - Per-token restriction hooks settable but never enforced (Low) + // + // setRestrictionHook stores a per-token hook, but the check in _update + // is commented out. Admins may believe hooks are active when they're not. + // ========================================================================= + + function test_POC11_PerTokenHooks_StoredButNotEnforced() public { + CertificateDetails memory details = _defaultDetails(100); + vm.prank(owner); + issuanceManager.createCert(address(certPrinter), investor, details); + + // Admin sets a deny-all hook for token 0 + MockTransferHook denyHook = new MockTransferHook(); + denyHook.setAllowTransfers(false); + + vm.prank(owner); + issuanceManager.setRestrictionHook(address(certPrinter), 0, address(denyHook)); + + // Hook is stored + assertEq(certPrinter.getRestrictionHook(0), address(denyHook), "hook stored"); + + // But in the real CyberCertPrinter._update, the per-token hook check + // is commented out (lines 235-242), so transfers would SUCCEED despite + // the hook being configured to deny them. + } + + // ========================================================================= + // POC #14 - Manual selector hash instead of .selector (Low) + // + // scripifyCert computes the selector via keccak256 of the signature string + // instead of using this.scripifyCert.selector. This is fragile. + // ========================================================================= + + function test_POC14_ManualSelectorHash_MatchesButFragile() public { + // scripifyCert uses: + // bytes4 selector3 = bytes4(keccak256("scripifyCert(address,uint256,uint256,address)")); + // + // vs convertScripToCert which correctly uses: + // this.convertScripToCert.selector + + bytes4 manualSelector = bytes4(keccak256("scripifyCert(address,uint256,uint256,address)")); + bytes4 compilerSelector = IssuanceManager.scripifyCert.selector; + + // They match today, but manual approach won't auto-update if params change + assertEq(manualSelector, compilerSelector, "match now, but manual is fragile if signature changes"); + } + + // ========================================================================= + // POC #5 - getEndorsementHistory interface mismatch (Medium) + // + // ICyberCertPrinter declares getEndorsementHistory returning individual + // fields, but CyberCertPrinter returns Endorsement memory. These are + // ABI-incompatible for external callers using the interface. + // ========================================================================= + + function test_POC5_GetEndorsementHistory_InterfaceMismatch() public { + // Deploy a real printer (not the local mock), mint, then add an endorsement. + address realPrinter = issuanceManager.createCertPrinter( + new string[](0), + "Real Cert", + "RCERT", + "uri://real", + SecurityClass.CommonStock, + SecuritySeries.SeriesA, + address(0) + ); + + // Avoid IssuanceManager.createCert in this PoC: it fetches tokenURI, which + // depends on uriBuilder being configured in setUp. + vm.prank(address(issuanceManager)); + ICyberCertPrinter(realPrinter).safeMint(0, investor, _defaultDetails(100)); + + bytes memory signature = abi.encodePacked(uint256(12345)); + bytes32 agreementId = keccak256("POC5"); + Endorsement memory endorsement = Endorsement( + owner, + block.timestamp, + signature, + address(0), + agreementId, + address(0), + "" + ); + vm.prank(address(issuanceManager)); + ICyberCertPrinter(realPrinter).addEndorsement(0, endorsement); + + // Do a raw call first (this succeeds and returns bytes). + (bool ok, bytes memory returndata) = realPrinter.staticcall( + abi.encodeWithSelector(ICyberCertPrinter.getEndorsementHistory.selector, 0, 0) + ); + assertTrue(ok, "raw getEndorsementHistory call failed"); + + // Then decode as the INTERFACE tuple shape. + // If issue #5 exists, this decode reverts due ABI layout mismatch. + vm.expectRevert(); + abi.decode( + returndata, + (address, string, address, bytes32, uint256, bytes, address) + ); + } + + // ========================================================================= + // Helpers + // ========================================================================= + + function _defaultDetails(uint256 units) internal pure returns (CertificateDetails memory) { + return CertificateDetails({ + signingOfficerName: "Officer", + signingOfficerTitle: "Title", + investmentAmountUSD: 1000, + issuerUSDValuationAtTimeOfInvestment: 10000, + unitsRepresented: units, + legalDetails: "", + extensionData: "" + }); + } + + function _deployTestableScrip() internal returns (TestableCyberScrip) { + address im = makeAddr("scripIM"); + BorgAuth testAuth = new BorgAuth(address(this)); + + TestableCyberScrip scrip = TestableCyberScrip(address( + new ERC1967Proxy( + address(new TestableCyberScrip()), + abi.encodeWithSelector( + CyberScrip.initialize.selector, + address(testAuth), + makeAddr("certPrinterForScrip"), + im, + "POC Scrip", + "POCS", + new ITransferRestrictionHook[](0), + true, // enableForceTransfer + true, // enableForceBurn + true // enableFreeze + ) + ) + )); + return scrip; + } +} diff --git a/test/UniswapV4CyberScripPoolTest.t.sol b/test/UniswapV4CyberScripPoolTest.t.sol new file mode 100644 index 00000000..a6099094 --- /dev/null +++ b/test/UniswapV4CyberScripPoolTest.t.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import "forge-std/Test.sol"; +import "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "openzeppelin-contracts/token/ERC20/IERC20.sol"; + +import "../src/CyberScrip.sol"; +import "../src/interfaces/ITransferRestrictionHook.sol"; +import "../src/libs/auth.sol"; +import "../src/hooks/uniswap/MetalexIssuerFeeHook.sol"; +import "../test/mock/TestableCyberScrip.sol"; + +interface IPoolManagerV4 { + function unlock(bytes calldata data) external returns (bytes memory); + function initialize(PoolKey calldata key, uint160 sqrtPriceX96) external returns (int24); + function modifyLiquidity( + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external returns (BalanceDelta callerDelta, BalanceDelta feesAccrued); + function swap( + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata hookData + ) external returns (BalanceDelta swapDelta); + function sync(address currency) external; + function settle() external payable returns (uint256); + function take(address currency, address to, uint256 amount) external; +} + +interface IUnlockCallback { + function unlockCallback(bytes calldata data) external returns (bytes memory); +} + +contract HookCreate2Deployer { + function deploy(bytes32 salt, bytes memory bytecode) external returns (address addr) { + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + } + require(addr != address(0), "create2 failed"); + } +} + +contract UniswapV4CyberScripPoolTest is Test, IUnlockCallback { + string internal constant RPC_ENV_VAR = "FORK_RPC_URL"; + + address internal constant BASE_POOL_MANAGER = 0x498581fF718922c3f8e6A244956aF099B2652b2b; + address internal constant BASE_USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + + uint16 internal constant AFTER_SWAP_FLAG = 1 << 7; + uint24 internal constant POOL_FEE = 3000; // 0.30% + int24 internal constant TICK_SPACING = 60; + + uint160 internal constant MIN_SQRT_RATIO = 4295128739; + uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; + + IPoolManagerV4 internal poolManager; + TestableCyberScrip internal cyberScrip; + MetalexIssuerFeeHook internal hook; + PoolKey internal poolKey; + + address internal metalexRecipient; + address internal issuerRecipient; + + function setUp() public { + + poolManager = IPoolManagerV4(BASE_POOL_MANAGER); + + metalexRecipient = makeAddr("metalexRecipient"); + issuerRecipient = makeAddr("issuerRecipient"); + + BorgAuth auth = new BorgAuth(address(this)); + auth.updateRole(address(this), auth.ADMIN_ROLE()); + + hook = _deployHookWithAfterSwapFlag(); + hook.initialize(address(auth), BASE_POOL_MANAGER); + + cyberScrip = _deployCyberScrip(address(auth)); + + (address currency0, address currency1) = _sortCurrencies(address(cyberScrip), BASE_USDC); + poolKey = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: POOL_FEE, + tickSpacing: TICK_SPACING, + hooks: address(hook) + }); + + hook.setPoolConfig(poolKey, metalexRecipient, issuerRecipient, 100, 100, true); + + cyberScrip.unrestrictedMint(address(this), 5_000_000 ether); + deal(BASE_USDC, address(this), 5_000_000 * 1e6); + } + + function test_CreatePool_AddHook_AndSwap() public { + _unlock(UnlockAction.InitializeAndAddLiquidity); + _unlock(UnlockAction.SwapExactInput); + + address inputCurrency = _inputCurrency(); + assertGt(IERC20(inputCurrency).balanceOf(metalexRecipient), 0, "metalex fee not collected"); + assertGt(IERC20(inputCurrency).balanceOf(issuerRecipient), 0, "issuer fee not collected"); + } + + function unlockCallback(bytes calldata data) external override returns (bytes memory) { + require(msg.sender == address(poolManager), "only pool manager"); + + UnlockAction action = abi.decode(data, (UnlockAction)); + if (action == UnlockAction.InitializeAndAddLiquidity) { + _initializeAndAddLiquidity(); + return ""; + } + if (action == UnlockAction.SwapExactInput) { + _swapExactInput(); + return ""; + } + + revert("unknown action"); + } + + enum UnlockAction { + InitializeAndAddLiquidity, + SwapExactInput + } + + function _unlock(UnlockAction action) internal { + poolManager.unlock(abi.encode(action)); + } + + function _initializeAndAddLiquidity() internal { + poolManager.initialize(poolKey, uint160(1 << 96)); + + IPoolManager.ModifyLiquidityParams memory params = IPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: int256(1_000_000), + salt: bytes32(0) + }); + + (BalanceDelta callerDelta,) = poolManager.modifyLiquidity(poolKey, params, ""); + _settleDelta(callerDelta); + } + + function _swapExactInput() internal { + bool zeroForOne = poolKey.currency0 == address(cyberScrip); + uint256 inputAmount = 10 ether; + + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: -int256(inputAmount), + sqrtPriceLimitX96: zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1 + }); + + BalanceDelta swapDelta = poolManager.swap(poolKey, params, ""); + _settleDelta(swapDelta); + } + + function _settleDelta(BalanceDelta delta) internal { + int128 amount0 = _amount0(delta); + if (amount0 < 0) { + _pay(poolKey.currency0, uint256(uint128(-amount0))); + } else if (amount0 > 0) { + poolManager.take(poolKey.currency0, address(this), uint256(uint128(amount0))); + } + + int128 amount1 = _amount1(delta); + if (amount1 < 0) { + _pay(poolKey.currency1, uint256(uint128(-amount1))); + } else if (amount1 > 0) { + poolManager.take(poolKey.currency1, address(this), uint256(uint128(amount1))); + } + } + + function _pay(address currency, uint256 amount) internal { + if (amount == 0) { + return; + } + poolManager.sync(currency); + IERC20(currency).transfer(address(poolManager), amount); + poolManager.settle(); + } + + function _inputCurrency() internal view returns (address) { + return poolKey.currency0 == address(cyberScrip) ? poolKey.currency0 : poolKey.currency1; + } + + function _sortCurrencies(address tokenA, address tokenB) internal pure returns (address, address) { + return tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + } + + function _deployCyberScrip(address auth) internal returns (TestableCyberScrip) { + ITransferRestrictionHook[] memory hooks = new ITransferRestrictionHook[](0); + return TestableCyberScrip(address( + new ERC1967Proxy( + address(new TestableCyberScrip()), + abi.encodeWithSelector( + CyberScrip.initialize.selector, + auth, + makeAddr("certPrinter"), + makeAddr("issuanceManager"), + "CyberScrip", + "CS", + hooks, + true, + true, + true + ) + ) + )); + } + + function _deployHookWithAfterSwapFlag() internal returns (MetalexIssuerFeeHook) { + HookCreate2Deployer deployer = new HookCreate2Deployer(); + bytes memory creationCode = type(MetalexIssuerFeeHook).creationCode; + bytes32 initCodeHash = keccak256(creationCode); + + for (uint256 i = 0; i < 200_000; i++) { + bytes32 salt = bytes32(i); + address predicted = _computeCreate2Address(address(deployer), salt, initCodeHash); + if (uint16(uint160(predicted)) == AFTER_SWAP_FLAG) { + address hookAddr = deployer.deploy(salt, creationCode); + require(hookAddr == predicted, "hook addr mismatch"); + return MetalexIssuerFeeHook(hookAddr); + } + } + + revert("hook address not found"); + } + + function _computeCreate2Address( + address deployer, + bytes32 salt, + bytes32 initCodeHash + ) internal pure returns (address) { + return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initCodeHash))))); + } + + function _amount0(BalanceDelta delta) internal pure returns (int128) { + return int128(int256(BalanceDelta.unwrap(delta) >> 128)); + } + + function _amount1(BalanceDelta delta) internal pure returns (int128) { + return int128(int256(BalanceDelta.unwrap(delta))); + } +} diff --git a/test/UpgradeLegacyCyberCorpsTest.t.sol b/test/UpgradeLegacyCyberCorpsTest.t.sol index 23b06ebc..30ebb8f2 100644 --- a/test/UpgradeLegacyCyberCorpsTest.t.sol +++ b/test/UpgradeLegacyCyberCorpsTest.t.sol @@ -496,7 +496,7 @@ contract UpgradeLegacyCyberCorpsTest is Test { assertEq(issuanceManagerAddress.getErc1967Beacon(), address(0), "new IssuanceManager should not be a BeaconProxy"); assertEq(dealManagerAddress.getErc1967Beacon(), address(0), "new DealManager should not be a BeaconProxy"); assertEq(roundManagerAddress.getErc1967Beacon(), address(0), "new RoundManager should not be a BeaconProxy"); - assertEq(certPrinterAddress[0].getErc1967Beacon(), IIssuanceManager(issuanceManagerAddress).cyberCertPrinterBeacon(), "new CyberCertPrinter should still be a BeaconProxy"); + assertEq(certPrinterAddress[0].getErc1967Beacon(), address(IIssuanceManager(issuanceManagerAddress).cyberCertPrinterBeacon()), "new CyberCertPrinter should still be a BeaconProxy"); } function test_EnableFeesNewCorp() public { diff --git a/test/UpgradePublicRoundsTest.t.sol b/test/UpgradePublicRoundsTest.t.sol index f3034390..7ecde080 100644 --- a/test/UpgradePublicRoundsTest.t.sol +++ b/test/UpgradePublicRoundsTest.t.sol @@ -228,7 +228,8 @@ contract UpgradePublicRoundsTest is Test { block.timestamp - 1, block.timestamp + 14 days, true, - true + true, + false ); } @@ -289,10 +290,11 @@ contract UpgradePublicRoundsTest is Test { block.timestamp - 1, block.timestamp + 21 days, true, - true + true, + false ); } - + vm.stopPrank(); // Prepare bob for submission @@ -591,7 +593,8 @@ contract UpgradePublicRoundsTest is Test { startTime, endTime, true, // publicRound - true // allowTimedOffers + true, // allowTimedOffers + false // restrictEndTimeReduction ); vm.stopPrank(); diff --git a/test/UpgradeRoundManagerTokenWhitelistTest.t.sol b/test/UpgradeRoundManagerTokenWhitelistTest.t.sol index 6fd5a54b..48a06587 100644 --- a/test/UpgradeRoundManagerTokenWhitelistTest.t.sol +++ b/test/UpgradeRoundManagerTokenWhitelistTest.t.sol @@ -197,6 +197,7 @@ contract UpgradeRoundManagerTokenWhitelistTest is Test { roundType, true, // publicRound true, // allowTimedOffers + false, // restrictEndTimeReduction raiseCap, minTicket, maxTicket, diff --git a/test/libs/KnownAddressesLoaded.sol b/test/libs/KnownAddressesLoaded.sol new file mode 100644 index 00000000..e3c56b34 --- /dev/null +++ b/test/libs/KnownAddressesLoaded.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +abstract contract KnownAddressesLoaded { + uint256 internal constant BASE_SEPOLIA_CHAIN_ID = 84532; + uint256 internal constant BASE_SEPOLIA_FORK_BLOCK = 38956871; + + address internal constant METALEX_SAFE = 0x68Ab3F79622cBe74C9683aA54D7E1BBdCAE8003C; + address internal constant CYBER_AGREEMENT_REGISTRY = 0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134; + address internal constant CYBERCORP_FACTORY = 0x51413048f3Dfc4516e95BC8e249341B1D53B6cB2; + address internal constant BASE_SEPOLIA_USDC = 0x036CbD53842c5426634e7929541eC2318f3dCF7e; +} diff --git a/test/libs/v3/RoundLib.sol b/test/libs/v3/RoundLib.sol new file mode 100644 index 00000000..b0e8ab29 --- /dev/null +++ b/test/libs/v3/RoundLib.sol @@ -0,0 +1,160 @@ +/* .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 {SecurityClass, SecuritySeries} from "../../../src/CyberCorpConstants.sol"; +import {RoundType} from "../../../src/libs/RoundLib.sol"; + +struct Round { + bytes32 id; + SecuritySeries seriesType; + uint256 raiseCap; + uint256 minTicket; + uint256 maxTicket; + RoundType roundType; + uint256 startTime; + uint256 endTime; + bytes32 templateId; + address[] certPrinter; + address paymentToken; + uint256 pricePerUnit; + uint256 valuation; + uint256 raised; + address[] roundConditions; + // Normalized round price and primary security sold to new money + uint256 roundPricePerShare; // normalized to priceDecimals + uint8 roundPriceDecimals; + SecurityClass primarySecurityClass; + SecuritySeries primarySecuritySeries; + address authorityOfficer; + string officerName; + string officerTitle; + string[] legalDetails; + bytes[] extensionData; + string[] roundPartyValues; + bytes escrowedSignature; + bool publicRound; + bool allowTimedOffers; // if false, ignore EOI expiries and use round end +} + +library RoundLib { + function draft() internal pure returns (Round memory) { + Round memory round; // all default values + return round; + } + + /// @notice Partially fill the given Round struct (ticket-related parameters) + /// @dev Beware of which fields are not filled and using default values + /// @param seriesType The series type (e.g., Series A) + /// @param roundType FCFS or FounderApproved + /// @param publicRound Indicate public round + /// @param raiseCap The maximum amount to raise + /// @param minTicket Minimum investment per EOI + /// @param maxTicket Maximum investment per EOI + /// @param paymentToken Payment token address + /// @param pricePerUnit Price per unit in USD (decimals = 18) + /// @param valuation Valuation in USD (decimals = 18) + /// @param startTime Start timestamp + /// @param endTime End timestamp + /// @return Partially filled Round struct + function setTickets( + Round memory round, + SecuritySeries seriesType, + RoundType roundType, + bool publicRound, + bool allowTimedOffers, + uint256 raiseCap, + uint256 minTicket, + uint256 maxTicket, + address paymentToken, + uint256 pricePerUnit, + uint256 valuation, + uint256 startTime, + uint256 endTime + ) internal pure returns (Round memory) { + round.seriesType = seriesType; + round.roundType = roundType; + round.publicRound = publicRound; + round.allowTimedOffers = allowTimedOffers; + round.raiseCap = raiseCap; + round.minTicket = minTicket; + round.maxTicket = maxTicket; + round.paymentToken = paymentToken; + round.pricePerUnit = pricePerUnit; + round.roundPricePerShare = pricePerUnit; // default value + round.roundPriceDecimals = 18; // default value + round.valuation = valuation; + round.startTime = startTime; + round.endTime = endTime; + return round; + } + + /// @notice Partially fill the given Round struct (agreement-related parameters) + /// @dev Beware of which fields are not filled and using default values + /// @param templateId Agreement template ID + /// @param roundPartyValues Round party values + /// @param escrowedSignature Escrowed signature + /// @return Partially filled Round struct + function setAgreement( + Round memory round, + bytes32 templateId, + address authorityOfficer, + string memory officerName, + string memory officerTitle, + string[] memory legalDetails, + string[] memory roundPartyValues, + bytes[] memory extensionData, + address[] memory roundConditions, + bytes memory escrowedSignature + ) internal pure returns (Round memory) { + round.templateId = templateId; + round.authorityOfficer = authorityOfficer; + round.officerName = officerName; + round.officerTitle = officerTitle; + round.legalDetails = legalDetails; + round.roundPartyValues = roundPartyValues; + round.extensionData = extensionData; + round.roundConditions = roundConditions; + round.escrowedSignature = escrowedSignature; + return round; + } +} diff --git a/test/mock/MockERC20.sol b/test/mock/MockERC20.sol index 5aa2d2cd..a98926f0 100644 --- a/test/mock/MockERC20.sol +++ b/test/mock/MockERC20.sol @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; contract MockERC20 is ERC20 { diff --git a/test/mock/TestableCyberScrip.sol b/test/mock/TestableCyberScrip.sol index 58bc836e..3417c097 100644 --- a/test/mock/TestableCyberScrip.sol +++ b/test/mock/TestableCyberScrip.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.28; import "../../src/CyberScrip.sol"; contract TestableCyberScrip is CyberScrip { - function mint(address to, uint256 amount) external { + function unrestrictedMint(address to, uint256 amount) public { _mint(to, amount); } } diff --git a/test/res/sample-non-fr-proof-call.json b/test/res/sample-non-fr-proof-call.json new file mode 100644 index 00000000..21692665 --- /dev/null +++ b/test/res/sample-non-fr-proof-call.json @@ -0,0 +1,720 @@ +{ + "address": "0x1D000001000EFD9a6371f4d90bB8920D5431c0D8", + "abi": [ + { + "type": "constructor", + "inputs": [ + { + "name": "_admin", + "type": "address", + "internalType": "address" + }, + { + "name": "_guardian", + "type": "address", + "internalType": "address" + }, + { + "name": "_rootRegistry", + "type": "address", + "internalType": "contract IRootRegistry" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newHelper", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "internalType": "contract ZKPassportSubVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "admin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "config", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "value", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "guardian", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "helperCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "helpers", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ZKPassportHelper" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "rootRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IRootRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setGuardian", + "inputs": [ + { + "name": "newGuardian", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "subverifierCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "subverifiers", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ZKPassportSubVerifier" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferAdmin", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unpause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateConfig", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "value", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newHelper", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newSubVerifier", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "verify", + "inputs": [ + { + "name": "params", + "type": "tuple", + "internalType": "struct ProofVerificationParams", + "components": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "proofVerificationData", + "type": "tuple", + "internalType": "struct ProofVerificationData", + "components": [ + { + "name": "vkeyHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "publicInputs", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ] + }, + { + "name": "committedInputs", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "serviceConfig", + "type": "tuple", + "internalType": "struct ServiceConfig", + "components": [ + { + "name": "validityPeriodInSeconds", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "domain", + "type": "string", + "internalType": "string" + }, + { + "name": "scope", + "type": "string", + "internalType": "string" + }, + { + "name": "devMode", + "type": "bool", + "internalType": "bool" + } + ] + } + ] + } + ], + "outputs": [ + { + "name": "valid", + "type": "bool", + "internalType": "bool" + }, + { + "name": "uniqueIdentifier", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "internalType": "contract ZKPassportHelper" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AdminUpdated", + "inputs": [ + { + "name": "oldAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ConfigUpdated", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldValue", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + }, + { + "name": "newValue", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "GuardianUpdated", + "inputs": [ + { + "name": "oldGuardian", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newGuardian", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperAdded", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperRemoved", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperUpdated", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldHelper", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newHelper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PausedStatusChanged", + "inputs": [ + { + "name": "paused", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RootRegistryUpdated", + "inputs": [ + { + "name": "oldRootRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newRootRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RootVerifierDeployed", + "inputs": [ + { + "name": "admin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "guardian", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "rootRegistry", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierAdded", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierRemoved", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierUpdated", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldSubVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newSubVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + } + ], + "functionName": "verify", + "args": [ + { + "version": "0x0000001000000000000000000000000000000000000000000000000000000000", + "proofVerificationData": { + "vkeyHash": "0x20a5dfe7875cf4cb9fe6b4e13397434bc7b33ebb42431c09f9d2eb20f0f09a4d", + "proof": "0x00000000000000000000000000000000000000000000000c2c112c8932497632000000000000000000000000000000000000000000000005c8a3fe28763fb35100000000000000000000000000000000000000000000000aff7c91a579480acf000000000000000000000000000000000000000000000000000085095f3b497c0000000000000000000000000000000000000000000000020fa16603014171a9000000000000000000000000000000000000000000000000cc5ee700f251fc2f00000000000000000000000000000000000000000000000f4f0a4019bd4873a80000000000000000000000000000000000000000000000000001c2f0e85cdd69000000000000000000000000000000000000000000000000a749cc2d0427a5f50000000000000000000000000000000000000000000000088584622436f1000400000000000000000000000000000000000000000000000c1eaa6d311865b00c0000000000000000000000000000000000000000000000000002f96cc3d1841c000000000000000000000000000000000000000000000000ed37fee519e7b02400000000000000000000000000000000000000000000000236e4fc449a99ef6f000000000000000000000000000000000000000000000002fd1b1028350246880000000000000000000000000000000000000000000000000002c163e020a9cb0d8af69da0bd7bbda3d48fcbf89e5c9a244a9be8046e87f0fd0d9ede708b4e5b2d14a37b0449844ae96707faa9a66c98df0d1b8f2c791c460bfcf9f87b7d6fdd0683a72f699bcd95773deb94155207b75892eeb67080ddf4ceec222f21f643252bc0199ace07a0fe625a1c9421a31130c3e294ac4f9ba2fb59b66f48ff6aff2e2db74879a6e133087039c162c3a60f7ecc86824a459dd0aa95eebf3a14d291541bf4b5d48714e39e3b39a289ae4bdd45efd86dccf963ac846feb493afbe4241d146ded07c81bdbdb09ce09df77c60d2d035c6197a5e8c93ad1ed9f18484c5dce01230d00293c3665277f9602cfb990abae626066cf2744694c753da2d27bbf07146ded07c81bdbdb09ce09df77c60d2d035c6197a5e8c93ad1ed9f18484c5dce01230d00293c3665277f9602cfb990abae626066cf2744694c753da2d27bbf07157c954935760886eb09a7100d7a4546a3eb3080d1b2327b39951754101abb14282a5fbf96fda4808b271a5ad6c32b9649df4c3d1e6b5095a660769179e0800507597655388f0a5e33835317a4c1357fd1910ba1382ca30ae1e68ff3a923c0f1167493e9fb1a71d45da6a062f28b7249e4246c475bd3a70a59b219e6fdbb4c462d5c4f4d1b48c46df3ad29e11d5ff76c17802646ad04ed77ee587206d2c1eb602721883089d06c644fcafca5f91de661ae6803cff4d4c571151289230054a8f41c5c9b5964f3c56d021ad9bddb508005747bbc1eac9fa68270558705f120400c1407b3197c3ddabcb6356bf8a630d857b3b82c29cd19ca0ed38c6e8dfedfbff51f4423cb8818d01ab4af24d3c4bfa464945e0f0b4ecd98897beaae9bd23cdf5b11365408679750b6e9cdb82c07d99b6a65ec1dd9d2f35666c801294e18101eb402c21f53e072d25a6a383e9ab45811ceb9f7c818b1e8e57bf364eb7f7fcd5da82b96e9e5d39bf991e8848a0ad973ddb2b40a8f2545f09132b4275c6b300d9fce049d404e6da3c48b789c9220e9ed80d8de30e759a4f36503439ef0f69e775ca7049db9b3a04948a5c8294a04b55d0b2ee73686cbe998220a3d32576bf5be3ef82c903a585e4abe5572a581237e0bab93f55d285d9cfe48c08a3e51673f52536a0351a570bb4e5a2c6d9fd9f4b90ad468e42201dcb25c7558bfb4fc9879d6670318eba355513b3d52f1cfb8c72d7605e7146bcc1cf529b31f3feda5eed456522a00fc6cecf905af6ad62a9fc20a4137bd6d5a3914edcdfe0af2e54eb63f719c1a2496c898f6a97513f8e67c27835ba92d914e5f620391bee41e7b283b124b20f01db107d196123843c19cf6a18a1ac1a7a0b9c3a1a1d238b63c74d4cebd01e19e0a9a18634eafc90754f770634cb2827f4f5f2a107a69b0e410bbcd31b0904888213b60ed61b67843656c7d6c8b22c25be9cd7635dea33ce814ae764dcdeb469b29f33635af048351683b2e1aee5e098ea44438b3b4f1e5fa7096c5df6b47b36e03a7fbd3ba7fdbb9a31750d10a8ae5d70c4214c24eefd82ee9fca709090d9ed8037e63b3bf31fc621f73caec339f18cef30cb87dbb3dfa134becd9b7dd23668e0a32da20b469880a932503fe763e92985eddc892e02f395c9591a92addc506040d9fdee21da38ba203e334b8a9d1d28bbb5f9b5cb3d9887a617223e1d489bbe506b1b520435036c38543c201b7094171a236c92f25985dd263dd25932eb8b9be0ce6661a118821354bc1927210a6d7692a8fd36235ca767d1b9542323e4ef7b2081142aeacb003fc324bebad58acd3f6a0690800249bf24d7adbbf71e0321875143b512f146013bf369bbdb29c7cb8e3c86d3a76cdd7bcde892f4276e968aa541ca1e72e1c7eac26c1bfc152e601fa9a90177030252cd27b537bd149309da018261292501bfb720c92c49e1b8a54aa0712b1cd23b1a2080ec3308bf9c7a08e6a21c48e0bae8bb6f5a2b141b3c4172c12371e24d6f1e452f3bcbfee8c347d35571e9b2f2d31b15f68a667f49c367cfbbcb2a8418b836d1b677754f2ef0ef93e5028a2fd9a68d8b9179bcb65c890405b7067839d32febdc5c649d84a19ee6358890a771960963ac61363097b46c712c5b16e9cd773027d4533ab2eeaaa6795d7c51d871c233ac1cd38b8e71f5a478b769bdc4b1efdd71083c9b1a4c8ac2890eaed2a784ffe3cd853b45ba3649dac719e597adce24a99a8d5aa1bd99503c91146d826df2f501ed172b3534f37d2ba9ffb7971d4742bface44cf5ff4613bc26804c7101f03450a712a800f7f7af3d7f3efc25d446d2aed0adc1cf0bacc139f5d3c07184330363c718d354684da912184187f5075eb0cfd3c1e743e5d508ff50285f41b3359a39eb74eec2e88ee13c293cb8934b61f22af897bf05c2edb8af98805e9203d246dd01b2f003de945ea2cd3a70458a7b32aaa40f20c62e69c3eab6220fa134c0132a7642c6c89d76ef0cb7e025ebc1c987392f916defccb39bd80c6b52417f4326166d2a00f10e94c99ed1cd9f9fd37f9fd18a2aeee6842cdf0ac82544b2727cda9c0a8badb5a3d4990570f8427a83f763bb901ca1b8c218941307e401f0e94c0ee2fdd7a247a03005846f9ceef81b652207bc68b9d49348bb40621be0d0bde72bce2f0d1a020862eddc1e2db3762ab679a2d50df5cbfa2731863878d3403653253e97dc98b126f7b0cf82920f69ebc26a2adda7830da03b7361fcc435f12c3d1c2ffe5ef191ca6c81add2c7140a106e8feeed02579f02b76327dc5f48d0038534340927c2be66111d77d40fa4171a2107c5ebe1f672348e3697f89b3f80053a393da1eecdcf39d204a9d5bd55a1067feb67ded215138b0526802520fb728d827336ec9050d33a53cf61219d2410acb5a79c000c7bb16cdbc42ffaf38a72fb3320228469f0bad0ff38791f4548f6d67e7e73b93ec8c4f7e2fd9b7a2ec982886baea6ddfcb46eefeaffbd1369faf1d8b7212d00837527850e72a6814f3f222532eb1ea9ee4eb2e7b438c2d7c07294541d0ff606d7bf13530c85f6219e1562cb03e6dd95307dd4d8709adea619957690d846019584332d8ff868b594fbf1b1917ff4ca39cc115a43cb3db5913d0d05c28ac9d7934385ab8ea32e56184cd91116b4a6ff7c4a8f668f2843e88c944d0ce9c2700b3103f9b0bceed41545b7c2514882cd370dd88d922ca9d606aa22579595394e3d4f52b75d39c16aa2df197b90f82e84e4181fb990785e51c098182af449cc5f807ffe280b42798868e2810f90f0a2c13c35f13f7da3dc33064f92c41960ed3dc31b575c4f68613cd6e5471fb2b33fba7cb9fcf511d042a17c73f3f0134b5a200d32e692449e627ea8ff4eb8a14cd5f6145ee3282a5278f4fc30c663840edc65365636c20a401b10c3feb18430c76e296255c68a9b9b05ffa74db0acfca1f55adecd0995036f54b7add5e0ae7238c30474e5466264f542347254fc4e71d27308a77d84d8e59b76886e7ff52872e069fafc267541e9b1c456558c1ca1863a7cafb6837ed24c2e813a45d51451d12b71d719ff41ee85bc9922a7d3b5c0ae84584acb67cb463adab647bc59fbeae156e0730293191cb16e0c2baf7ba89a32b2b78b12b25ef6da5e1f58d9fb7a9852dc154845a69cbd19e2babd62d5fcf2cc0c40011f3dbd6096c70b19a3fce3a232375e3cf63f8914155bb318e19a09196e5f8528ad98eab298cfbb9a5ea70b56010d6ad04e78d4b9fed5f003636652d74db1b512d8b1480d9688532adbd7ef78b01f2aa2670f1c1a428bedf9c0d4549f4b2c94b0c66c2fe902babd231b1dcc40c28bead09f3d1944b6aa42da2beb3eddd52259e481377eec8e0f67272a01582c22797b22cacac4c0c6bba4c913b06c66375b7c2d7a3b057cb5d2e911872320ca30127ef33a01d1f491d3e6354a3b63266792d83eb93a8ebc32ba7d7604119a68f00f26b7747f4d9f80a3f9325f21d7bda73401b1ec5103d8c05257efdd7bb671922567865e2f6373b28943d1657bd85642a9b37d77b03d5e8fb592009f2c5172725800d2edf72f3e0db09b5230d5cf98001f67a0db69e75d1fa2cb3af2b00bbeb0370602b53004301ac359a704bc5b2543e406714e135cf9328267ac696227ba02d6c066dcb694e07e72f3be34d7b32ad9be7d9c25ab8ee4b6ee929952c07c97713e4f666f1c4b33d7a66b04b96da7929eaa88ff30af984ab83eeb8d75d5fdf870cf3d58a56aa3564bbf71c96e52dfcd1cd40416269ba76ffad50e8e0414c0bcd12c78e5ad753af6b0cf12f3258f666bdc788cf14eeae42d0ac1b223c8f205c981f1ffad3cc86be3ec94f622263891cd384602a7a34fcb4e5694ad1f606e8a9c01313dcb2ba24612cd8a56567b1387fe487764fadc4b73741a11f76123358a87e0d1b8b2c1b8815c9a35dd9feff4b36a285b9987bdaae8f299b22f45f6d2e0ed42130fb94290a03b8ac26f5af5a36063f4ca438d707c3a1738227ee4f9a3da45e27438954e48562cc440df7e2b3303207f26ad579ca0374de1d9a856a88e020d21accf70e995b3f582ee52bad0918d79b8d81cef5a567890b323d138a46ad768600aeddee9ccc3f823f06a94214964a0596e7501fba76617f1dacf88e69272f2b268d1789dca1828d9e98f37de5bc507b03892a0568da54c957f08706f74042c91337432072ce8fbf62cf0f7b78728de5f051cb36437b1743f13e4dee0979259a09a3fa059b3736de8bc2fe0988600e165b2d93e660392541c3b5af13f644b4de204f9c1d4c45ec6941e67f7228afc816e6e359893d74a37c354adf45dfaf340b23cd2d187046145108ed2a73c6f7d46eaf00f7b22fbb6b05c560bcac47f577fa2fd80cc05e699f7feb1fbbed2587e397829aabcc45246a9e961da91e5890ea822ecbd4755e662dd838361dc11595075c981e6468aa777231c97cbd21dfd9fee61fd0b882db525420e4042fbe3cdc477004da0c28077517ac0f5f4966f6d7b8fb1ca88b4f3fa40884f89f22abe52bb2fc7af3e1c0e7ba2a2046f200ff4caeffde02875aa3f2ae383a7a580e2add57a41655d99524b845f2cb574d393ab8bd83450224cdd2d91bfcafb08f8c35ea8f2e8b2169539f8ae78316c3fd3a82bce4865928a88137bd7db6dbd4a791837c7649813f0f051d1618a036af9aa510f0ee04f8254a1172ddc3446053ab00b157619bc194371b5419b059feb1d69138278023d7127f0135973a719911ed946b4ab102f68ed1354ad99915a75b3d4460aa58b5542ecd2e59aff30e03e930e6922d3ca4c1a38d3c8a3f31dd14694b3a40c63c191225ca68bd07689fec1674f24f2ee7ac066c73e26634e4748a785b9e6ad43cb4551f12960a27e76dde8f918bb89e3320e264cc111b6358cc0951089d31b6f1bc6319ec1be2c80ee1d053d1ef6836240abfe55a34078c83c80ec544e492f296b2a42ca8057bd526b2f9985c59425d729dd71455916737a932dab26776fc63c09855198d350a76d6ac3ff1eec794daa607b41569a77b90df79bdc62c10252d2b85a61a6d53e30793e1a73c11af48d72175f4ee07a9073dfe78ce6cb08998af3a2f86061b972e4955a6d3304867bf00d2d5f4b91b2c8b4d16acd47734983048d3a44e2872ed7bf1802c7109da56075956fd366baf5725528cad88baecb989ab8c09771477f74b976b08654f8527d4f0ad6f72fe2335ec5e73ced479039fc5ebd8f3532195ece91a453ee30782bfd2ffa59d37181256958699cf075db3487ae163f3b52920e6e2245091739e275effddf04175a48eb2363bb592a37d756469a129148a018fcec65d805fa6137d7739905e321fc8a7888d2d18573d81b3fefa59b79bd5051d36a5fac269fb1f87961e51c4ac1b185645af1874b956bfad73ab83880faa07818a03c2600c87467b27fb98507301a07f61378a1e2d2f47d2af55399d876c29c1e9eb40f403536252684dc5de618d5ade84f7cf613a383f8bd9edca5aeae32f45beeb766e61b7cca59b24eb9190e04c9439f20fb85671b845f529046402d52ed6c104a2fc1407396bac17c2d3b96903366f5a953146b02e0634060f0fa71b159b7d08e79c4dbf00d4310ee84e7f15da660ba0d82516166a60918b732b198711b322d6b34e8a636edb3c698438a3e8be3d853c41c639214ff98b383ba3e481301c7745558541ad7e54d46d0c65f0714a5581ffb3ddaaab7a1fb8840147cf0003ce812db23cb15d173392d2e490d2fe65af7361e1feca6c6f2c29ecd35b188020c250cdcf6340323d5fc269987991c6d23fb9999b23a2351d41fcaea5c23c8d04eac535057aa9c3c13b91cb688ede7fb3348038e78c940e05522bcf574f32ad237b644139b01874804e9e9a3cf1cdf36933b8c2cd880e36c8b8a133c9465d66100fc8813523b40a2e32988a7fe3971ffe2d08fabbf62c860dddb8c2172377d81d9c6d29142f5e4cd781c5fd536db7abc687205311be4a74cbc2f27fab3863ab2cc0bdb5f34855e190e5a1d78d95d40d9bcf18ae7bc45caf83bda9d2e89ad7a8055a0c8113850e39bb5c34f9a37f0d9aacd65dfbbbe7e29a0d0d14f87eef00712bad99f6625be1b331afa587265643be701accbb2aaa0c389df003e6fa98834f2ad59e0290c6a3094c6e45dc62701e3b02417f8af47e577cf806aadc101ba4860e116218081ef7c3d51d46e683b424200047f31433fca1164cb9002b9909afbf2e47b62303c90c23f0f0aeb7a4f200215ff6c6a9b7da453f5a0178d62b1f5f381b155e6c2dad1d81b37a11470f8adebd711523b8240de32f4c94831464c44d04248fb80cd4669a1548f37dab3ca82fc45c8cfbe3b0a2def5d9f1724488c10daa0ddb9ddba27726baeea96cdef81e6e968cdc65cd7d1e6616a69a251b8779702818b233ec5316e2f47053ad3793027379e66195844c5e66dfc3e1a9e06d87e3171db2ed4b28ef7484183f4fc2c71cef41ddf4bd54da01ea43cb3d8d28f35260a61bdbdd6a832c3c998bae74c25a5eef4116d943637d532876e90cbfe2e5a9cf5e0cd3766025a9a525cbeb264bca7eb8020035676b48338d65ccfc72fa8cfceb801678849ac5cdbc926bf79b652b4be55375930eb7d75f1015a666317c44c30aca04153b3cfe8c8f30ce7e5afbe0881df70d8906580a9812873ef1d495765333b02b8f26fbc2ae5c10a6dcd96f0d03a815b27f42c058cffeaf5255af52a511239e2d4748197ef2f61778445986daa8f18638b4f9c88137c518089e4a7c55159b201c828e2fff92849cf852ca1f314c73c944eccde64185e171c2e57d69605908f308ba21b9f1388a5e48c9853e33e6af3f95826a4a55af0331d491ff5b96ed07aa3059883ee0c2166b1e992a867fa6d26d3be4da679f2f08695490e75e5e9c0c7e00f61b699e3f8718042cf325a3524f39b6ee50c64f8dd6e75077be9d2406be8b22202c0035573dc6158c2cdb576d4398afcb44879035db2909005cf89b018f1b21ae112cd79e315bb3d851e4edfe43203b5b36f33cc8cb7db2e294ced262e08829ffbdc9e9f517c2628ebb08976e77c89fcfc7e32d3ad731b7e209f34f6abd5904924e507a55bb874f3e82fe92b9eaf63be17da18e2b5bf00b38c3a5f91ac4690ba6b1be27457743d99c0e20b2174194aeaba5485c28bde3d4249f6d1586760700969cfdddead533642c50ea647f8022cf4a10843bf37bb34cfa9e8843af3ac123bb4058179ad0c26538475c454cde2f39f21d8caba80149756ef91d7b5602ee2a643f6b6324c2a933e3cdbadf6f9a7aa5af105c09fa21cd732c8e9ab1ed49120d4c4ecd36c55040005f129b301db11facd0f4da4172ffd5f207e811edc65a760d68e772aad3c8ebdf542853631b5cd52b7c777ce9642dc46e1acaabf013d2da10a850af85176970e2faeb72f9f94c6479cb2332f6e4853f5945fc25ab3d59070892055417a21b7024f0b04eb734bc6f0460f177109428124e551045088af31d159af2f3c77c2fce463d9422cf2c2d7883cc4bb6068d68896ff03de34db7b6c124cc904474e38288a470a2c307b6cc64881057af588090dd24abc220bff6d1e71e4a0b9c5d1ac7dfa12161d18f000a3b24e14c90424689d51adf2083a1b7ceb5023e3d4ebd8d5fb3b4c0a1bf3a816248717cadb4c7c7d2535aedd76b901645ae2c362a4626d63dc87706faa5d25b0d16fed8d508c31a4040459137930f3f64f520b7e9aa7c3e1db6f7d8091167574eeab12f06293061256e9047bc67798273e6065fffc0bd8beaa301b4735dfb8a4617f87cdea98a5661009fd734df4302c02404e87f2ab0b1272b433d7703a44b38263c635a41f98e8dd6b3b50056e2ff7e7f0f78d4b3e447008cbfa6643034ef1caa316b4ec58d16db8a9a6dac2bc29bdf182cbfd334ee871899bb916738e8453989c59f1a2afb1aad13ed294947ca59551c0ab6acacfb39ad830f3c299dc6d7809b63efb799d26cf420707d8e71b5208ecb0393a948f67e0d944fd9d94ef0fc579e0d02310b70801625fb293f35a130066e27e99f4bcb7999489c398d945c83e95016aeff5501f1fa2f829030594abb53651800270501feb41ed150c9918d7ad37863210ba0bde4bb40b8d373212d79a3131e47c42e0e0beb0966b07c27f536f90e25f82e584ffba67508e89c57c5c0ff421d07870d73e47ed1b27b3fe04835982c40989d3b5c5779ca196541fdc673eeee0979798cb53627db4dfab130319611f6636c438a4b037631836c52683d51e69901fbcb4e904dca76725676743b801af5f10714b07acf23e86307226cb0fb33400f6ad767136aaf940f75cf920b29333dbe226f32c6efbc0edffa3cc084ae6db108d5fcaf6ab2724ed58daf0927f7a2ba0ee5e176defaf0668e794e8bc0e3d98a209daa1839f69847f395bef1cac90b17009454762aad81b99d91f91d3302a0761e6f78606933c36ff02e68c1d7958f481d9dba486a9d71eb30adca3b9e641ab51486cbe62be06a3a2bb76f9cad8f61147670c4c25752971bb52a394fda56bea32cdf3abb7c1440730f7def93a45b5fab0f68605034eb2cdbdc42ed84cee2d4f42d75abbf1bb8fa0304ee1e2075a69183915e589c95fd0c99f1094a46d90313a021a7524bb9c222537cccbeb78231e8e653f8d7cc17fc2c56c1f0c204dfcf525310f4ad81acb6f49ed14d3559c280d25950b1fbc16806a983d202786b8a214a23017dad2afa3b21336fa79385bad87fe71aa7ecb25a2b77d54e338d2bc3a1db06115d043569cec3cd4864a496d287779bad10df80a1fb3e0aa9064cd7097c69ef2dabee6b699f3cc82ed682d7362248bb8b22042e08691248007d7095f064987e201eab6d1c4b41a523c695b997755c99df29f368ae458fb1cf7bc6b36d44484f1f0b2fe5539b7b4e2b23a52e391a2da8c6668aa87cbd062323a14bfd4d4f7c563009c9f68db08b05ca710cf92e7ae5acfd999862efe156e8ff574e0e121cf6d525c8ac2ff690257f5587201b760fb0de70e293aadadd2a5f6138c12866dd3ae51b5a865dff29248114f38d02891aca958d613e3aa437a757829c8a899988d4dc1055be032d25805cf9e8af43168e13c19aff9d33d5090cfdce00292eead116d42ea6f9f04cd268bb8e5a2050dc0b4b04fdbce8158a120a1ef3549ff11e3b882b0eacdf1b59e01a09f9c0e3bdc54bfbce85c4aade8ad1231039d63eacf157d96a1f693231360849d6c32c86d33cd30fcb77dbfbfa1ae11a65b35a45f705030285280c31d8a43c90b31b58e444c96b4ce12ccc3027c0a29e3d6e898bc9646367b42b93d471e6690266f24d72a270d5b4a1866559ca9934231caf498256b534a46f04e7ea8f4ba47bdc1d58e52017de1542258f5a4d7b7656bccfdbb55f0e01da39283674ea8347ec949a42232f1a4cca665080eabfab998f1fef315f9c52c64d6d1d37e5a0fe3cb4c047660a461a47827fa82dc5bce5c8f4a0adbb66f02cc7d0ee26bc4fd421350fab873ec6dc7e8fa35ba9b3003952503c308579b7d7456ce74f239cf5774ebab157ea532c13a514c1284abea4e9ee1a44439b1121303ce0c5ba09de0dcb5df1f694800e4db0be00b43b9f19fb07ea00a104e302cfe295805bd8127aaa7249b732a0b2218fed65fcabd99315184a81f398fc7e7e57d55ae5a4ef2f8726b391563102babe5692c1a55c4041c80fdb63258cbda167b29a9ee14afe2651c9de21bf29cfa89f311014db53c1ad872788350324bc7389a7fcd90315312dd8b4e0c2f49d2f6eb75e9d25ad536fcf267d5f5198cc33c586d673b01d7b0b2dd8b4e0c2f49d2f6eb75e9d25ad536fcf267d5f5198cc33c586d673b01d7b0b152a06957dbe416ad081c12cb1247e124a94c9142dd4af8269db7960ba1f8fb521de5d8b7708e0564cd40bd69d6b03d5909c1b4162dfca64cf52e02b932ae7d61130a1f764f7d356641445ac23a402bcb51d83bee7c93420410736c612924cb71f19507dcf832b4483bd0bcaf5dd7d43d6b7457f26cdf51b8fbc87146a4bcce42b4b8a9d3d6d3b481d2159ca841f26a03192c00a6573d2a1d67bb9ee3393ea9903a656b92a8bc4d173ce611d3f1c45d78df33967ffcbb5a4621f97b9c0c89fa32aded4d2c1fa785e8706e04ffe04b8a80e0412fb02e11cafc1da919a13565a3d1b6464fc2f2f871eb6c4124fb204f65343d3abd0ad8c8234c0d878d68229045028eb7da4b87f17e5e1b0d291f582cf597ce832805c9fe756e900efb5b40a71dd1c08d1b26644d1f9bd4bd3c598323ee097288e20eeae3ea66e6841d3fe3d194b2caaf4f94729d465fa0f57c83701f4cbf3e413275cab2764ef3f5aaa37062562170be3f80e7ecde32104f9afb524216a872fd23028f0e3d6fc2ebc979a1f1b9113963d9dac06d8d77f17f1d5e8ef1db4c8e6afb0f679efaa969da46f3b89521215edf1c6b7a2cf01b6bf07201bf4e27ad70b674a499350a2f28a1950d875ee790215afd5add48eef6433b28eb858a5074da22b60ba4e7ea8a5b3d99657db32931531862a0b2e85dd2b052926ae6c4f49f281a2c36a9035a95a778d8b4ef327a319897729a46ee73a206dc7f99052d4c1d1327028bf2d128a718e850235951fa91aadd00af11a68961cf407a978d782edfbd090afb07d47e581c53b7744c6adf525ec0f22f90ef1d5c84dc2a96b2b512943e0ef1b1629c4a8a824b9c7a95a8d2b08e4460e27ac9359f88402cdd4d59ca37dc6fc75a5caabe1595ffec32ad65ad01cff552290b0b6a2cda7d61ae61f7460cc125e78d66c2f0746d1c1054ad31c8a02f56a5cbac7621a9fd5f2bd88b11d3d740becef249bfb7775965d8b3280ba1715a18b18dd13b56418202e073b3d766d2771b2772d822b707b951e782691c45d10a8fa1669397c34560d18e70596725caab079eaf435c6f8b4d2bc8c6740280019b459d0390212302a3eb1de5f7cc7baba647715cb835a4c7f9711dcb4693fa60adb9fccd255d42013ee360bd434d7130925f1687d4205ff9f3d63870aa7821e0fae899fd706fb8b1072e49e8174cce37a22976dfa4121b1b079730290c3cf821c2c7bb70f8dc9ebaefdbc11ed634f9d31136947d3599bbf352f6e0aec055ef207f8c2ceada573d3d86a830d5a70da830b55d4f948406ade98203f7f6e83d69222b252a98eceb9d3984ae309f56f4e25cbc76a91d305a44bb36e7ecca9617eb023ce5cd5fd17ad339f99f8434818445bf154f938b1fe48067e1a596edf0794222b2c6db37da3eec860d9276ea1cdd3e4a4e84fabdda2f3587b61a9f28b97667a1940b6fcb7c0cc4cfb6d3c1a48cb6561131d8e59407947fab6e819e14a34d14607360370c78e621c7efa84a69bb521709e9d367d86d379137102a9ab172a69840bf0be63e3c321b621ddfb976c90ff68e9558ddd99c91d23c3b627063b7c89180c18056753ad11a9ed35c6b01a622b92366ab211a4825187ac1d03c32974ba332132a43dcc4ca14ad6569dcb37281c321ae165d2c2a7d871d62d746a343642fc207942cf7c0e0a10801d5acb46a8a6a9a566e8efe7647a9443cb7c4d60fdbe8f134efe0d6fcf5839059ef7f4a49f80842c9193bff4537d3936c9db9072ba2e2816f8990bb69d5c29f442811e4a2f5166ca1e9a785110cea6d76e727d133f25d70132b3d3dec88bb7703645f6445c0dfba611accd2f52070f858de161712e032216ca9fdb5da863c5c4f6b12e34611ead849486e70e085a617633b47473edcd4b077c886dae89655d2c057c845bb23b2596dbb3cbbe0bd943e8ae0e6ddca0a9020eff11cd8b4d4d4b9405d4a34b179bd2721d53d5e5903fc0290103f64e5c16f5130eafdabf54806f555e52c403f62d85a6d69d379749e88a229fd04959bb94591c2d27473106414cbbe0441b222ccbefa4cd65c0e725723be44d800cd3fdfe17221c155c824340d4946690e35d574b7ba9a9f808d8d786a1dda7385bb4b8bfeb1e1e35792428204693144c960a261b6134d734ab64942d198c44bc392639793600d20ff327cb08a35c0ff244f887d2fdf433ef690df5509eda46185c941072940d013285c549bc5c13ac1c7a27aebe7d19193dfaf3e34a954e664b9ee0412cd90247015e62ef6f26e591b91868ef0f274143b92b121164606ef60c32c55285c316fd730dd153e40d1c902d0f62435a61a101b9caadfa963924853dd0ebf26787215d27cda4fae6e34b22f7f2bae185a70e8badb7005883c1c33778c907a4c30e27ebf070bd6e4428d465deca4ac54ed8082c8af0521ef8a1e2b4ec6bab2cd8fa2172989d718d1172509a216f6d33152210fccd2f7d4b1833b051444cd2ada2be0fb8c4240c89f9e2d8858fac94f0debcd1a75992067bfcbe2ed4c85aa80c0301149cd54eb6af1bb562526f67dcdea9fef4dbe6bfbeef6f574f94a55c7ff72db125f8c011d47d9b1fbb5ef8dc017e1e6b4b2c188544c18a68fc6335e9085f08e30baac5ce2008dbc0c9774606a53c1cd516cb68579aa6119c80ddcae70aaecbb92d115c35b286cf3ee729562731975476b89aff1d6366fd963a4070a295af895b0503438c4ee1750dd7e422189cb0f8dc71142366743f316baacefdd1340703920582eceb04427e8e8332dc341d7555365a0a29f0896d28e1941a15942d24399f1a0c8d04d133f685ad4bdf7f7ca409d99bb3eedecfea065d0ab8c23c65ece6e5160fdb1e936d2fdb3187059cc8642fcb49b301732d5aad3954e0580a07c00c2c27d2b2764947f66879bc2ce81586c8f9082895b6d2a868c849f3516369a351fc044827d9b1de79399f1f7c6bb44e91e384c9089676e8d3684adbedfe539f42d214b0b27fb0ef88de44cc2fbb97e8aab68d484f69dca863f2745ba19d14fbc3e41769d663855682dd211c4d13067c2f34555d517a34757f98d1b8bf64e35622921ecf122f3f8f2e0cb019230ac4049fe990812dff42d432d7663c417a442f3c4710a6805e857b05187b0097cd55076b0ef3cbbbab7ef70b8041b6472fa4aa628f29c811e6459482881b7c99687c300a5105f91e2704193e7a32eb64303ddaca9e2d50608cfbb42a7ba25dd175700f913ad8f379957c9e323a502229060c18270e113acd33c852268b23dc9c5f5d36dfab016ecda1b50ed790e6a9086727b4bc2b", + "publicInputs": [ + "0x2f696abafd61692fe9c82281fd461431f5ff1d3ec31c10b2258b3151d89b9c6d", + "0x2c0ba69927ad2b3737a57195469c8185f0bcf42ea920cb0ac4963981f23f9e87", + "0x0000000000000000000000000000000000000000000000000000000069aa4c3b", + "0x0049960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97", + "0x00afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce28", + "0x00b4a2b0ae4ee1b273470e31316cd2202df52e51e4eb19117768886cc15b8f4a", + "0x007e54c9c86ca576f0db71833eb6c815d41ac3372f465221f9bc6a5bea553758", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x12304d7feba021ee21ddd4ab53402116177980f3fc3183c2dcb5f731df55eb20" + ] + }, + "committedInputs": "0x0502584652410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000801fd0100145ff4e90efa2b88cf3ca92d63d244a78a88219abf020003aa36a7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "serviceConfig": { + "validityPeriodInSeconds": 604800, + "domain": "localhost", + "scope": "hello-world", + "devMode": false + } + } + ], + "account": "0x5ff4e90Efa2B88cf3cA92D63d244a78a88219Abf" +} \ No newline at end of file diff --git a/test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call-base.json b/test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call-base.json new file mode 100644 index 00000000..a23d576d --- /dev/null +++ b/test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call-base.json @@ -0,0 +1,721 @@ +{ + "address": "0x1D000001000EFD9a6371f4d90bB8920D5431c0D8", + "abi": [ + { + "type": "constructor", + "inputs": [ + { + "name": "_admin", + "type": "address", + "internalType": "address" + }, + { + "name": "_guardian", + "type": "address", + "internalType": "address" + }, + { + "name": "_rootRegistry", + "type": "address", + "internalType": "contract IRootRegistry" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newHelper", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "internalType": "contract ZKPassportSubVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "admin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "config", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "value", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "guardian", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "helperCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "helpers", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ZKPassportHelper" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "rootRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IRootRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setGuardian", + "inputs": [ + { + "name": "newGuardian", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "subverifierCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "subverifiers", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ZKPassportSubVerifier" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferAdmin", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unpause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateConfig", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "value", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newHelper", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newSubVerifier", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "verify", + "inputs": [ + { + "name": "params", + "type": "tuple", + "internalType": "struct ProofVerificationParams", + "components": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "proofVerificationData", + "type": "tuple", + "internalType": "struct ProofVerificationData", + "components": [ + { + "name": "vkeyHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "publicInputs", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ] + }, + { + "name": "committedInputs", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "serviceConfig", + "type": "tuple", + "internalType": "struct ServiceConfig", + "components": [ + { + "name": "validityPeriodInSeconds", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "domain", + "type": "string", + "internalType": "string" + }, + { + "name": "scope", + "type": "string", + "internalType": "string" + }, + { + "name": "devMode", + "type": "bool", + "internalType": "bool" + } + ] + } + ] + } + ], + "outputs": [ + { + "name": "valid", + "type": "bool", + "internalType": "bool" + }, + { + "name": "uniqueIdentifier", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "internalType": "contract ZKPassportHelper" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AdminUpdated", + "inputs": [ + { + "name": "oldAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ConfigUpdated", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldValue", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + }, + { + "name": "newValue", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "GuardianUpdated", + "inputs": [ + { + "name": "oldGuardian", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newGuardian", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperAdded", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperRemoved", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperUpdated", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldHelper", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newHelper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PausedStatusChanged", + "inputs": [ + { + "name": "paused", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RootRegistryUpdated", + "inputs": [ + { + "name": "oldRootRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newRootRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RootVerifierDeployed", + "inputs": [ + { + "name": "admin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "guardian", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "rootRegistry", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierAdded", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierRemoved", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierUpdated", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldSubVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newSubVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + } + ], + "functionName": "verify", + "args": [ + { + "version": "0x0000001000000000000000000000000000000000000000000000000000000000", + "proofVerificationData": { + "vkeyHash": "0x2fe35634ea36d09761105fc3b6dfb4883411171afddc6c2e146ae350a72b53d6", + "proof": "0x000000000000000000000000000000000000000000000002272332b13d85227f000000000000000000000000000000000000000000000002f4c2933eb3bbb59e000000000000000000000000000000000000000000000008e2ad21164986f1fc000000000000000000000000000000000000000000000000000014987dc48c6900000000000000000000000000000000000000000000000baea0929f10e47ed0000000000000000000000000000000000000000000000004c01348750109103a0000000000000000000000000000000000000000000000000cd0e7b7b90104ce000000000000000000000000000000000000000000000000000065b439bee66700000000000000000000000000000000000000000000000b9a271fa467961dab000000000000000000000000000000000000000000000000886feacfcfd0857600000000000000000000000000000000000000000000000dee451fea468391fe0000000000000000000000000000000000000000000000000001dfbfc42ae40b0000000000000000000000000000000000000000000000023f2236c5cd7a2310000000000000000000000000000000000000000000000001f2c35212a55cb04d0000000000000000000000000000000000000000000000092dbf28056a7a66f30000000000000000000000000000000000000000000000000000a31cada7e2b11506a26ad14fea8ca54d9ec31e476a2f5cc8695945dc2a83949474baa8bdacd53041133d6763bf4ac2da4d8ec3608ab6b7981f999347807edd078168683d3517208623505018ea2426eef3821b1bdb2b3dae76ddb3428a1e17d0409d2243f7ef254827b2d2f4698aee4602d54ff4a1a136d0b55000992dff4ea01feacf813c501b451c64c7b6f8f5cd49c2f337e1bbec29d2cbff1133d781205e9e8b0c9d04422d268958b1a3921dd2286b7be9be9c3d75bddfc6c4fa5d4cd63cd2855a5876021ef6cef632a74763ac78bc5e3e8e6030aaae36cd01475f46135cf7bc065b9eda2f19774c97404c26b82ed4fa7b54580c19e856ad54a56a7536d6b744389ba8751ef6cef632a74763ac78bc5e3e8e6030aaae36cd01475f46135cf7bc065b9eda2f19774c97404c26b82ed4fa7b54580c19e856ad54a56a7536d6b744389ba8752376475a45ad7ff8165ac21b6f0fabda7cfc1ca3dd1db1b05bebbacd8ce608e114eca2650cb963ca8a179f853b7478356bb6f40f5f5fb2e57bd5aa92c2bae4b50b3817ba3475a8d735a5246fcd83eb1c3555294359f2023f9f3dc8254e66fa8e0b59f1e4e9775a7c22f10f49f94f3db52ff0cb49281bd4bcebf842ca1f4c83fd1918b9fa43b6df6444338283ff98267516b7171ab6bd5f8891c7485a52c2a0b30a2e9d78c4f20ecd55671e974d7d62eeb80862b939a1a47836103aa0e6eab5ee11558662d75762f4d2b151903279f32678b79aec4754e0c264addb8bd9b9588e1f0ec81009da3d34e59ef4264f076536af7c4d5c32648fcedf341a081646a773303cceddc5f14c7a9895336e642d1e4f672440ae04f36807e200419bad0bb71929ac2a4c201e5881710d81858347935a0e8ace4605fa52e7523aed19ff31bd5c012d36786739f26c062886d8ab16a3f7b659d4e204f6e5d877da85b7e4aac67312ea2329821de100b568eb3d23a55cbfe590a7773d019b22544bb19108c4050a07448bde81218bf20e1f26ab6bf2ecb48a80d32e87cf0633d6ff0fbe77de2a2b204e7ff8f816e0e4e91998f113e4b52232dc6e8b5467f15dff68da7c0fed260b01f4cd57019914ee718d0f89e085b6fcdaf1f6c34768dcdb2da2217defe7ccf1041a3d3ade2a0b5a3cdca79f5759a3ee11571efd8e22c465439f65be3a01a46a1c2cef3a6f572f788cd5eff030a43c013da5b4d711873988f1909569be48745a0e092636716c7e422e02483657eb0b69c9bc397061c7867e38d7b05ba0276a690cef2750d31f189c9cced01253db34bd104636b39eb5a8d78ee1f7710ae5ee7f05cf7ea84d862b6d5281f8db477db980805dd2367ee42908d705741187ee4fc926bac7c77e76762b0a5cbca04c893b0a10d796ba2d46b35748edcbc84dbd861805f283a48ae7d86bd2ebe55eb52ab9d27fc910fbb398b2d58d0369b4b394e6942815f118c6c93d555e5f43652f13685b57f63b00298e9a54fc5238c6cb4f7aa71e68bbdd232fec3d84bc2d574bc3401c0339ccdb375019f116d31b24eb3a839607e29678e6fef164868616e078467e35657bb0446c0338a72f9e9d98438eac0617c23e80fc4f292978d36bcda33f183f9edc99beea18ef60ededf14662f2040010dd0a5469bfaf12a906089877ad095ff8aee8ef69cc69057114ec6d545c8f0b2286ba0386a4a0f200a2b9af9edd74ace68a2117aea893aa81b97175c68648471ef682d8858267f7afbbf4440e731a58023ce4901cf05bb8be2f6248264f8bd12a385d2ff252b4a31c4063b1bf3e5dbaff226156675b6bc3f8f414b835daf6e52c5a1ac5324d8c651a765cd80f7bb88c74ad2ae1b0797a96789629d4e03aedc618c02d0a0fc3019fe1fa1ea793af46d3dc2d9489cb9a40ec26ec9e61ec3a9bd6228caffa00f665481974b33872560057ba27e4e4809904d3983001d34a9eba4e16a531f99a1567b3e262efc9de202f2b9df8d070fea33cbddb990f865b9e05332fee991da13b6aaa7d0ad1eeba6a3dabce64a1019298887cbfb29fb1129a14631113cc118e134c53377c6641e3e65c5eddfadea9f65125d673be7a7785f7f099148ad8d670480199e66a80e37723066b2196ddfc00b9933fb76f669013a181e92ef11f75dc0e03022ef8b40ca0fa1e8c364b56a29860d0606d4d96f5232078fa22e92f4db124e15c4e9a8c326d7598b53c00e278ca105f0a2ce4126853baead01407f8e171bd4cb7421a8531deafbf661d9413ca89eb14f32a4eeeaa96ec92c0011dc3e27fd83186dc18d1fe453d6685ebc0f4dfabe34c215830dbe6606bde5b26cad404d7b3dd15bef4cdeebf2fb76ac1547fb44adfdee6a298d743979b00ef2acd509bc7a380c46c6c5b11b2414a63df1c70f7f69546c823dfdc366e3ed3670868cd85db8078a3bb5cd622e25f7c63c23b389a6cb27e07fdf95c4f918feec715298f1173aa63d19795c35ac740ffbe740316ff98a8b96c03cc563cc147c64f2c93b6cbb8266f5639a7f15b3957aa1890a8070b96bc51938faa9b8307ebf358243c972bcfc084ceeefdfea4a12673fd8a4878eb3578c0d366e6ecdb03c750cb1652f8765a2cb6f541bc1e819e64001d84bcaa424e992023b5ff8da5a48537fc1a53ef84123a74c28143bf93795ff4c26be8d515ebe7ec533a30f3e98b027bf5235eed5445fe5e2b0dc7838399ddde4c6527cd86c19ff99ef7df4ec903acc390118f60f110754e9627fcd889bc7c625c4b93b0ade2f856d6dc783a7265dd2d210ef0783446a62849603cded51db03ebbd88fb99df7db4bc04e29dffea0fa84ae033fed09efe51d6dc26e4bd4e9d8a0c1b6ecf36391ed11d7957ff1c8385d26c90c0886238b392683b352ca29b7b4ca73523a1a47452ba377d18eaff15474e5d1182e852231dfa99236678a4195fbac93af58500c66b7f61c781f8acddfc04bde280b471972755666369ebafc52ddd03b1bdd74497febb0eda7f1a5c00d100c6a00b1b28fa2a92a2253bff732bdb5b86178b3baaa6153d7e2c9405f970671cef51607eaf6df5a47dfd2924172ae6e2a671b0585df8c918d49ae5cf170ad77694e1a7bb6edcb88a9193f2da5e221a89e5f307d45898fce056f8247371b8cd5c246299c6343e98baf7e80e722aab6471141867f47aaf031a75c19030db0bfe86d621a8bd19fd64d4c0b7db261fd16e3c71973c7e2770e1d2be122d9adcc5c5472bf0bb1828492114b6b3cccaace6cfed329b2b9a7d7de2a578b0ab3b5794c6e90e52e2bb62e8bba39bead709d53062a0a578225e207c08fa5e78ec3a55e20766fbc1fbbae6356cb00e4cede931608b8da3534efef3004d612f0a8169cb38773924c2c11c6e638d7954497bbdac371a3c5a556859b12a2469faa1d6fe045f43a78a62496be7e62e81013426a7ef9c015ba8598482edaa5cc28781e93da6c8829d87f1abd3df47f1c4d6ad6ada688ed2ca094fbd6fb75b0553a2a3b3ccec11ed510f917b91d206b2dfc849de508fb48d56e07af54a6b5ce60fc401667fc99537bc7a918029ecdacf57636fe69d02b90bc4f46eafa6d087901b90ab4ba30effa6a38f80f2ddc62314059b9c8fdf67d9d37e31ba03df83b017c9077e60ee06176af1afc305d02fc6515904f75edc7545663fcae6c3dd28d82cd952e2e128d880959b46309ba3ae782a9b7d0616d01c3f9f35d86c4ecb8c389e5f23ed60609dbe4afec41080ed726ed86e3168d4856c955611727072bc236adfb553b7afa2f02669157f721391976ff3e1319c085d18446f3e2db7d990719b90cddbd2f9a877ccdd6b55d0d160c6b4691e086450c8b0f1c0405a76b6c7db20d70e1f5a402534445817b772d1e6b35c6df0e4c3c60bd8a89fd94132ee373c7cef5fc448c5d2b4d4cc7d0552431397f827f78379ee99ab1dc5f51a96c71c6e3d6b0ca6cf168d43e41f48f0f0fd3c4fd34c4e0893447f4c09f57d5151776703416b0152336662a905956d75d2b2d5928ab153aedf17ff34a05dc6ab844d59abcf84da17000f6718ae468c603076e3e451d1a19e96a87a4c010f62b1c029e4b20d47d05878444c45c2fd7908f04048d1086b048070e4bfc8a5c5824dc0860ad8ed8c57a045d63a8934705f6ad218ecfe0164ecd7fc15d3ff97e1c0f02802900db1cedee877332829e29a748c02ce7f94d45ac254784b0894681675a3dd14a08a99f0d076aaa43b81fe61f29c01a58799161d6cf2c28417e6959d0f353a6266faef380608b01bac961b8318059006e8b4ae0a47731aa177bbf11def237f8ab4aff6411177846390d275cb4757009de45faedf8f7f12197ef586c681d2ee7dacec7fccf8b45658f161877baeeb504c879c13edd0e1bd7e1de328bad95f0f2b7387f9f225ce4713d2025292644682b7428d0329f877b59a7a781cce999bb9201dbcd3d94ed174dd6c43bc0cb53b91cebeab53b72a2eea8683b7928785c6b09daba68e55445c3fc7ddcc421d64b2d1f18f5c7aa21a11d7cdd26a2747997307a50a02805b8e020e16f904663df90372e1f4142625c885d16dbc7cbf8cd949d94770ae7b3c2ab971f1cf44c6e842b810b5f07aa84512b0aac9f369cd59f8122fe4aefa2ffc671614051ba3092be8b202f46f2c0911c98c2f9f33a00788a05f8ee47892b5ff63a4c10748497e5b681790f0f9d69017cde4d2ac9a5fff557c459e41626bb7bb687be5c18fff18d845aee0f2f79587d54c11275f70672c0f1186135ab64e22251460408880acda531852e1d1702f89b3f74981105e8f970c3764f54765c6a5a6b8aa00c393239ba6c319210f348ffb2b06e55aaba587e903489a77399b22196d7940382e13e82b5d4c4061588880dd2853da6ea33eb5bb9f69fccf1eec6a241b0c001dde8fe5f0588664914dc152676e88f93fc055f53f4fccc8de9dd86e12275ab729221b106af38b99011da2df68af7145a83ad12b37b999ddfa546aed8e1c973558f58718ca98162e413f473730f318a8b0238926f67ed932184c09bdd5c5c8193eea6da623b4d68e7192cc46505e1380d941474b80df6fee0a0ae9184fd16ae66b8057785a14dc2612f2871051337d4435ca0762f438ba5059f603800699700688c721d3d2c79571e22019400bc3f4bf797f60edfe0ee1d4becf2bddfa2eebfd4aa8d3fe863c884612afa6a9ba80daae1a08ce4b92ecbb9a45378ba0fbdad6b67f9a3e2fa9f6b77d717d81c95f5b492f3ec2a9dcaf024a8f4725a1a53ac9761252825378aef9cd6a414065e0059522c0d426c5cd347f3413f73f18388d2490775ac18b90210c6de81198912ef05bf6b2e3e17fa3e9ee5d1c25c20ca094a27bbc1f13421f95aad88d71965127068f1c329630880093f5e218b605c56cde2395dda9511ec90b44c79f900b6284494f095919da42ac50a2e562b2303d9888c7349436536a737e5db41c30ec941214a3e87d48b0f5070531674c2863e0090306bba8e020d9b0b46c70dc221be24f115f9735f62b652be209b1bebeab3f29cd1e8407535b082dc0a9ebd5f23429ea7850d95597b861cd6ae4adacbe291a92ebdebcf902ab5ee8c00ad057e1e5bdffc5cdc69a83a870d115e25b842145e08fbb7ce8049a7b55087476f40101c8404b2c25aba466b719b1b279327bc8574a1518eee3aa00365924042a416d513c54e4c06c67179c3f02a49f43fa30065b3fc238e9b4a39f2d1a8cf3472abfd21e35fd6b7a08a106677fec3f9178e6c6ccceca0e5063b315bdf1014da06a3710ee3413ba20f6c8152b782dff0580f112b5cc22e8894d91ac1a461171114dcb90a6bb4a06dca33a04c8be57514aaf716c9febb8d519b5865a1a3d303557823f10857e1e65474ad75bcda136a99e44b797841873fc263bede261e97c40af1b16014deacc2497ec82c097803f378599926cc735311dafd5d7b801a688f1697cf591b05f071767ea8d640e30fa58a7b3462e57836101692a9fbe5792b4ac40b8bd00752069821d363ac9dfc6209aae15f848113837644e87bb201879e446b9f0adb22aeb60a8a62375970a056e6ec9e12eaa904b033f746535154b9ef0722ef4f5730233bb09785f8956e99d09e5d33793cb517057c4adf97d61975307cda8354270974b97edb8305faf174bbe646a6d6d9f3f08c9a9b6d93367bef65c747568e9d2646da5790a2415844920a5ecc5dfc8221ca4486760199c342d942649eafd6552192e6fb87a85a92d42982fd6869b8efc30b9bacd5aef5622077dba9e175761b0fb536c80567370e4ce472d53ca5c1a9321c082cd49f166ae172855e404b7e612ff9beda7344a1a33f911b729d79d25ff338bdafc285d528baccbb16d0efa28400bbabda3579cac56ba4049dd5d6787f874f30a644b3983bdabb0d933c90312621d7d66ca3f3d0c372ebfb908361fc9cd9cb6b992d18096e203c7913fa77de7a01ee10a6e513006be8065d6616f83d588ab7b431f403f462afd90c3b37105a430d547aa7648b8ec86148afe7401e76b3739f6ee68ef8580341e1692beb4b7cae0f8fd9411215dc298f2e25b76068d64ec2b026d5679637f1f5e61624da9ee6d11c6dcef512239b62b7e7fd5e7794504931b9178fbfb10aae81b2311cd5ef6e191c8419ba695144e10aaea8035e8465886c09f012140247cdd2aececc9d61df5718c4fd96740fa9d633fbe2175268b50a1f6d745901e678e95a738c6d00b64d9d19e835aa2460745fadd71116f1ad159d0b148218d8d06a512bf966a4034f95ee0a84fe925f9a745de59e3009db8c0027add826ce3d27769c5765644c1fb070f20d973e465de36b45639d4dde2a481afad210ecf360b1de00d46203ed92fac3aa0dec729269cb62c8ad4fa0490025b24dafb1eb0589bae39ec293ec02e46f51b02f584f0fa40b305c338f9a4bce340bc3efc7d5be9b92c68f00776bec1c251b3d13ed6aed0caa047c035e1965c085c14390884f90fe77120b223d18063871a5621a6b551820eb83dd26bf578e849eb3db55d9fc92ed78ec3a61f9fbc9afd07bb7106047105118421253628d5444c3457b4329cd3ad8d9869aa79140fb313669fd095dc63d78aaf882ff504fc4e0f76023df29749d6eee392e8bb4d5bf1eea5c6409a7ca2ac1b320378ade417f954ab8144c978df4aa0f5916fcc6283b7c4285df138bc0a7c0c5df89e4480bcacc41b4760c8bd1c0034192c8c087d68d1def92c805a2a7aefa98d7a40952fdfd8c9f10b97189ca31ddd1d164465cbed4101dae171e054c9e070c4934874cc8f5da02a804437e890a6f3e2c56bf78f8b0867bec0208ee2a53add1523a9ea5a6aa54dadf1214cb8ac8e77477bad306f6b44348990c13cd8a5e5ce83ea5039d2fdabbe4076e7201ad451c04352a52ed05e234a4d2d80f334a0c3e50811e2bcbf00cc9ee911261f8a88d757f8626b38a083708b818d40b024f6e9f61d59cd821036d712028f2b2607e8036ae6f6abc13ccf5ff303f4114db33ba91cf841198de8d500058ed0387e32796a5c0c6b417584016326e46900bdd72fbeb5e4f1a2acc3be02b51a5cd6394daef4e57cba881664aa2531108c725c43a84f69d7a781195596d3c3bd8929ef17790f86d7b6905329ab6215efb722bd459bad1562196e0ca84dfedf381b7d11cac82a49b520b851ab14d14ffa68e232e19005b0e7f21b5fd20b807085254e2e7d9476228893f916f784691312a440917aefaa1a268512efd418c8a02323acff20f22da641e0c03b2118ef742f6e724c7a58e2cfa9d3207dc1bfa67a63464a25a5d5f665a17e85f05a5526778270d067ba6f10cdfc3c4a751b3cea11d65e49035fd71b5089da7c784def63a8c26672f6ffd7056ac33029a5b54e9d8cece142127ea0d01a75b81108d086c3718861d17adc909d1e67fee3b29ebed66c6faf2c89aa4f3dcaad811a1b352fc2c22616629e25f3e63e9e2df4a2a92c561cbffee5e3105f5afd79ca65d05e455f3127cf329830f71f894a0b7e1d0e860ac744ec6a2f8bd620b55d6f11208b508fd5e980c2b869c107c9dee903ae6a4ece927d3e95b2229924c1a182d8ad6f03113fabbab2ee7facba45d5b2459cc24fd61141a05937f264a727fa0cc711b813010bb619620f083dd64ba1dce18ff5ef42a39e3243ea0d928c620653a9a8cb3da0e0404391300b6c3fe9f2543acb84caa05e7190d1da751ec7135e87d2d85b479b71897740a78d4d5be5027e82e1d04ba350161ddd717d394cd5f20bf6162afab43becb7128949776c9235fda9d2cf2701b5bdcebe7e405bdccae9d6ee972ee2618244c63233eba2cec22829595fe62bd9b0f05a91da38f20d943d5d487f993f62e4ba0ae00500dbbb3f06de3b6cbcf73d1476398fb4c9555674e932b3f39c567585220da29f2d9c3f817817f2256e9e636fabac8d528906e498a019550d6a84cddacd9550d3346953abf21d74a79cd72b8104a0417bc1c2447405ace451e2488b487a9bc10f9961ee11917eb30443b37ca7c4dea0ea23f9f9faa31bcb7b46de3177a540327c55e89725ad9fdf926b1a29ad652dd46255a382bb4cbf46e1ddf46daea127f10dcbdf820df9b722564da3483502411733fb40b8abebabb9b002739e6e8614a22f483f4205825570793d5660e4f710ac5ea8e79a45e70a2f21b28fd9de8cda90011225bac6079ac280e542705ad95c1bbd895b4e5a291b7b679462e3920fc2d24b3c685e8b23f77a4b85af1b57724ba8ef3a19fed057e21ef3d231ae9669eff089b29787920c94c9caf26c5255a858991ea577c29a76f736d27864962370e0a1fb598e90049f13894e59d979c4020e002466f40a045e7a9c9426e7d77d0bc47305dc1b401374a1e39c1bb4787849f4d162ed92dbb2e9bc303fa5544475c7d551c459cb11efc28844bd44db1aaf421ae1b9fe54baa5837321e94b6a35a9310c50d83ab255bcad0ec33c35d49dece65941bf5b11d10896030eab8c16fc221d4eb0eb5253855a4e7c5eda9dbba37bceb7db91fc429a51c55290822c58c159a84252abb184733c21184187aef0a5bf01feabac84b751e6c0adaf967664f5f4e5de30cc595c059a5438f36c972e13566681dae60662ee7f819aedf0ff04af9da39d925c076eeeaca126b604a4a81d96c7f1c2062dd4ca91ebc82f03451c5b6467be92b846b5674bd0eb090c23ad56442087c96690a9b54e524f79e4ba0941a9aa6b022be95887225b09f970094db69974a5b540353fa7f4dd0f486e97e6c2e5624521fbd90783e2e13556e13b50b3bdd386f3d853e380c1354e2d0ef811b1fbe60720110d42bb3a13aa2fc172e7372f9c858046f4e19d1b3513da35f85ac228e9722087b6569935c16f4f411b36cd8b2a4e2a0d724990de8c86a68480e0f96417b13013060e0f83eb750f40c75d1be728a5d933575c8f62493f50aab67baa1d5240f2de84e274f7e897e2e4220dcef0c09f484abeb42bd5b997fa1c63af88d6862aa2bc77e413b342f5d9cb560b9ee9e60801f9458ab913ec7dc00b5175ce7c81e90218880c9b5477e8a828e0ac961488bdc8b4bf21f8095a70eed7357bd3b3f1e452ad6e15fcefefba194d363cb3ddea13c2a15438c56bd6cba537574333ac74f16082c25389a8077d3e27d3a1fc8990569e9b2c8ea290bc3de890bda0ed8a1a92c0a5f5294d6e8bd5a6010bb26090dd1cf6af71965a207d7af4952b900ac01f93f18e7e4e28296b6b293f2cbf077c88aeee24184addbb7ea7a1213ed310a6e67ee2246e1d648e0d056237b8a5448138ebdfccdb34f5c8e5616eb9b1bad06dbe8d81093b6e28d842cd13c939965377fcf00a6b4fe1c9dc9bb3a1d34905016da89092b5f7232ba5ac0b825348bcad2edb943db625964a432ff044cf9b88eaf7622c60c72c071f37b6ef5063a8225bb8967374ec80b977833ef4f37ec7face1bbea7a2ad2e92633c60ec287c0c56fb2a3a91caafff60a4e1b47c97df66da3838e6a172131acd6c19b58e056c9687bbcd4141552ae18ddc26b658861ad430f20f6a4751331819cd46d9b0fa42a890891e13d4ea61c14b5af1e69c6620afe17be7536920001c7e38ce85c282f05d446a95437557e7f8b7cbdcf5f967f2ca956e0983a650c9d903933dfabd433bb24e6307855db9327c6e44286c6ec9d9f3ff2ca674f5b00186e401f9e63728c12ec9b123e1deaaa5df9f8fb70e010d58714e857b7f2f40891dfd9257a8f7d42609776ccd077771e11733e5826483b938d776d0081bd3a1e5cad1c24f0e5017cea07a236aa0558db431c29d2f79958754efd2a80889ab108094dacdac6d47a9559f7324d0961182857e27d5d4f5c4208e27482e06f8b510a7c6c3ae5eb3766289251061d86d3c1247f521cf75cd7602f9550692f64e0d520e1b52341b43ecd816b11f6e0ad6fc3ee76b4f875850e58f15c60b361cf16db0de6f129d06fef4c9630ae327dcd460c3b45a04065f9a5398147e6ae4f9806711d47328291a301b9d3f1ffd49bd93377ada479ab9fbb534d86d59b5660f4f37725d64834a1ec10d795c34c3f01bb53b5da5c92423c50f8b310488bf92e482f2c0f88e1c3ca7231fceaaf8c23df0ea22235aba58bf3d3b8bd6b049bf265c87885016588fedf27e290f658d807c54bae9821ca36b93ab408cf0b3862d31d010dbf016588fedf27e290f658d807c54bae9821ca36b93ab408cf0b3862d31d010dbf2ea92c72a6d7a4726590936cace15bfc8dfef4cd4ff572016085ab40ed11de9a1f480116af893a4a65ae26b733eca07d2ac018d7b0e22c440a4129ec93b2e8ac06c76c4ac0aed5d8a35c7a9747bf75641f4f4ba43db698a4ff29f8a140f11a271c3db9b50147811438573a983b4751bfc84f20fc5721d897d379cde947df1d380211cf05214392be565ba29ee13b680efc53daf199a611f6f60bea49d930e32801c6a867b49f8c18772f264861ab26cee07f0104d3a17e32c4bfd6ddd0bbed18117cdc648ebee640f59bb3537c49f0f9ecb819ed2e699562478935bf2dbec5fe0b6c9909e69d37f3141cd39abb6fe8ce3e22c457866c5061f1f0f0dcc93822e42678c38cc20b90b684cda1d557ed518d25d17b751e604e803427cc00efb51d451bb1495ba9c237a3a78e757971b7c1424b52e16b31ad1e09e307529982261c0303e9c7411050372128d2b76d7d64589cd1a274c3ecf63fcec3ba76ef395a8def21f0ded434a2c785a2d31880eec1403907f1bc7de1f69d58b33aaf792d4356e50fb5b2e6a720b836dd8b870d26b9ecb8ca7050cd70136ff44cc68db6d769e2901aea39c6f8e9a2fe568c0ee14ad1701a70238c5cb36361d95ce3675ade89df0a201c9c9a55a965cabe913c8a88340ca768ae57eadd62fe9f92ff630c3198e48f1a1d76571583c51e061cb7956aa3d528e1760d85f4df85fb7381064949fa43190f2bd7805b478d0ce39124e89990de24eb9bd1f892ac4e471f0662f2c1fbc78828409c103218fda30045c7a17ffb5ae0b939dba6c5fc7c2bd0dbb02e84014afb077004224d606f00c9c607fccedd80a57e03385752ea6237317cd413efb393ef1ccc235ce2a175c4d7e1dd3e590ae8b0a3341e52e26e00aebfb971065d508f2302d904041fca68286570418d144ca7399387abf7e7b8720863fa14b8aee0e6b625ca7ce6e76f3aab0105489a89a23f40f577dd9dabcfa1b2cccaff81c6b68aa40224268e81cce91f46b970c07f88432a53a13968b1eebcbba5269b87a7a76b662e22eac2688fd8814afb4922c490470d381d67593a86a93f2db185e631ead07e02b9e0be893f8a869f2c38a3ea66874ab312e557eb416424989ea8488eca85c80d108f8ebcc88634d4fa17fc65b08bce4377ccc668ef7cceabc97437008c82161030e913e2a7164a99dd120f7f796160f8c8bced2445139fdc448aca138638e10b69fad840707f90aa1c775067a1c55bd4cb9a7b605008db6874c353c3c56c092a81b15c8665621b851058ad5c2c9bce5fa0bd989ddb007f5ba03d23110b989b1ff2c642cd85b1e586faf9d9e2598933c83a6eabd11561edfad962ddf3a384e301f8a16bcdbc7ea054a884eebeeffc6954bbdc86d29e7e4cdeac2e3cb8b8be8f2e93ec99ca635aa633bb9c176ad5e91f750a51c5593f8872262bfe74364a34991e155b39cc0bd7cb99d8b98f2ffec83cde151f5cd8428d5aafb3b1f9a10b00ae13c6bfa75d0c3d9b62950eb993fe7b064de3fe5a98bea64edf1897cedf681528021912cc91b6d594bbbea36e098c6a16fef5fde97036b4f4e3f0505828eee79503f5ceb86187ea53781ec2c11339ae963e3cb9219f187fb80d10c5e0e492485807c35175fb2ce42089acfbba0c594050768e3b8a873e7ba6ad70c02b6a03ddab182f97da6415f6fe499cf8e338177b922e64b6b1fb42ee0a271d2575dd88dcab0d3ce9546bd743475d19c39280e877895a7c4bcb09f3d69deae9d9fc01462dff06438056c692ff9e83b175eab9ad697c458f6aa15991dc4bc3d050f3232c488b077e4d911b08310f3c1a0f7e40da0475f2f3529fa7903e3c0acd0c034a96cfbd2faeab3ad2a66f05fdddaf7759e05f5d2a4d2037ffc52e0640acc0764a4c0de10fbca4341380b050c58d1f2a877e2cef8f866eabf4bc855e111311e2c737910c0957002f479405cf78840f3ffe9698faf3af00ed3bc8383ca27f7c4fb2d898ee2854790ee05478edc9c0f988a80dea713d43fc04f0986b6c76f89729cd964095174608e2ce551274a6e1b492d3bc02ea34618cbb1794fb1d6cd79b2edfa6675a0e63ed00f9b4a8a4c9999b40d3901b350cc00bd37657870cc2aa6c403979faf12502d51b151e26ce97f6390e4848c4b6453b55abbf9db4a376e0f032c28f9d340d15fa0673a0c69a22f4727a58ce54e45391daeb2774dcf6eca524ea4400a1b408cfc72002fc7e3c660c043343eb5aee8b32523bbd9711b9c05ee971810c98af176be2de8944a36fa5e5d0045a31152050ef6acaeb353004bb2022a42cca8a5e04376da40045bab63ea7e14417e0f4ec29d183bc9d30b430af3184aec930bce62f66959cd634ed70bf2451420c3b2f7bd27d2b8544b4a793fc3991a52ae70f641b222af8bce1a46d2054a7bc6a4277d2f74930150f2b66208e7b04e56ba0b64703be74f8082ae6d1426195db43856282ee6924a51e365cb1317a687781ade74725ebefd45b1dd43ced210c4db199010982a82d1e3e0a203cb27430fba65d0f3f1f6a5ce4002951079996d91da2436e3d4a5f664bdb05f699f1d058708092ac162f15f8715565d59840d03a62a159151e3d75bcdd284eca249c0594e7a8a68a7d0f9ae05ab38c51df2044db7f9e45f6bf42202bb20d1ceb604f13f49916897a5604078ef9a930b0e8fbc65c123aec75210b2f08f6bcefd512b92a0077b44a46440608b0a1c5c7602c9389f616b182d2b24d356bd8061b2ce604395bdc889a4b7b0f2bffb76346b8d30bcbfaf221e75c9e3359596d732d2253b60438627da7de78163d85c188b2564b1e1e33aa71b0a8aa5e5d5ec814e7db371bc30fc606d18c96086e4fd988955f8e45468f148e21bb3357ffb925dfdc6dbf2e83fc632f58d647186202c73a217e3bb825d10adf5b2b38ff9637bac4f1f76d2f69a68713bb7f5a2f069d86def49403ae06a2ac3a88423561c92862545caaadc70835594bfb42172bc9f6beeaeeecf948fe5b69e2056ebf9b9b34e10b415953cc561c9575b6938824b0317c80d0f7581ed6d966acf4120f6949b0c8d62307a74e0c31a5b15f275e1777aecbbe568e9c5509b44bc1ada6ca8b1ee7069f20b62edbe17c75060c16de045e5ac2ed1fffc0be7503ecf2ec6584969ee382f7eec93ebfdc17b529dcf43f14936eb1094370004a3a587d714d0462f26438bf0023479b19794d1a45103a06145fc152b7599da24983a42e786d736ba0282ee810ae0dcee93ebcf82595add7027e95f9cbba7d84b95d60ef3f98240a5b20e2e1cc66b275d79ba54e0963d6920526eae9f646c35fd6661cbb687cfeb02570eeb253771488b8902e7fab2afe5b1974ca8790f8df8e22ae88f39f7adf42d04016a38d12c57a84e1036024c6ef692790555a2a6c23acd89f41cd6f02e4a4f30c8375766c7daa0a536d5a6e9f62fe", + "publicInputs": [ + "0x2f696abafd61692fe9c82281fd461431f5ff1d3ec31c10b2258b3151d89b9c6d", + "0x2c0ba69927ad2b3737a57195469c8185f0bcf42ea920cb0ac4963981f23f9e87", + "0x0000000000000000000000000000000000000000000000000000000069bb7b07", + "0x0049960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97", + "0x0079cb6f108c22a4cc9b54613929ee42a8e5fc4047aca68da0a10437830c936b", + "0x0001d056a1fd5ece845559d52d6b09ad23b63488b95f5c74e708c6615c124417", + "0x00faddd397e54f3b68d873568139f5967d6d03c0d22c97e33e864599bd1e0f01", + "0x00ed19a8809304eda854b241e3841938f2011b9ce7a9c392fdbaa02382539b8d", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x19d050382dd3ac7e75d6cace046c3f52adc1a79274efc0c4ac2ad9911d956852" + ] + }, + "committedInputs": "0x05025849524e4952514c425950524b53444e534f4d53595255534159454d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000801fd010014958a7e75e1a51269bd267d873357909d176239710200022105000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900212dfcc0ca426d9d8e751bb00fc9ab502bfb081ba8d2ce3f5f94a8f1712b3afca800", + "serviceConfig": { + "validityPeriodInSeconds": 604800, + "domain": "localhost", + "scope": "non-us-non-sanctioned", + "devMode": false + } + } + ], + "account": "0x958A7E75e1a51269bd267D873357909d17623971" +} \ No newline at end of file diff --git a/test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call.json b/test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call.json new file mode 100644 index 00000000..ae060db2 --- /dev/null +++ b/test/res/sample-non-us-sanctioned-countries-sanctioned-list-proof-call.json @@ -0,0 +1,721 @@ +{ + "address": "0x1D000001000EFD9a6371f4d90bB8920D5431c0D8", + "abi": [ + { + "type": "constructor", + "inputs": [ + { + "name": "_admin", + "type": "address", + "internalType": "address" + }, + { + "name": "_guardian", + "type": "address", + "internalType": "address" + }, + { + "name": "_rootRegistry", + "type": "address", + "internalType": "contract IRootRegistry" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newHelper", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "internalType": "contract ZKPassportSubVerifier" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "admin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "config", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "value", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "guardian", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "helperCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "helpers", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ZKPassportHelper" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "rootRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IRootRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setGuardian", + "inputs": [ + { + "name": "newGuardian", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "subverifierCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "subverifiers", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ZKPassportSubVerifier" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferAdmin", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unpause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateConfig", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "value", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateHelper", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newHelper", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateSubVerifier", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "newSubVerifier", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "verify", + "inputs": [ + { + "name": "params", + "type": "tuple", + "internalType": "struct ProofVerificationParams", + "components": [ + { + "name": "version", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "proofVerificationData", + "type": "tuple", + "internalType": "struct ProofVerificationData", + "components": [ + { + "name": "vkeyHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "publicInputs", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ] + }, + { + "name": "committedInputs", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "serviceConfig", + "type": "tuple", + "internalType": "struct ServiceConfig", + "components": [ + { + "name": "validityPeriodInSeconds", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "domain", + "type": "string", + "internalType": "string" + }, + { + "name": "scope", + "type": "string", + "internalType": "string" + }, + { + "name": "devMode", + "type": "bool", + "internalType": "bool" + } + ] + } + ] + } + ], + "outputs": [ + { + "name": "valid", + "type": "bool", + "internalType": "bool" + }, + { + "name": "uniqueIdentifier", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "internalType": "contract ZKPassportHelper" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AdminUpdated", + "inputs": [ + { + "name": "oldAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ConfigUpdated", + "inputs": [ + { + "name": "key", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldValue", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + }, + { + "name": "newValue", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "GuardianUpdated", + "inputs": [ + { + "name": "oldGuardian", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newGuardian", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperAdded", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperRemoved", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "helper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HelperUpdated", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldHelper", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newHelper", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PausedStatusChanged", + "inputs": [ + { + "name": "paused", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RootRegistryUpdated", + "inputs": [ + { + "name": "oldRootRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newRootRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RootVerifierDeployed", + "inputs": [ + { + "name": "admin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "guardian", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "rootRegistry", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierAdded", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierRemoved", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "subVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SubVerifierUpdated", + "inputs": [ + { + "name": "version", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "oldSubVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newSubVerifier", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + } + ], + "functionName": "verify", + "args": [ + { + "version": "0x0000001000000000000000000000000000000000000000000000000000000000", + "proofVerificationData": { + "vkeyHash": "0x2fe35634ea36d09761105fc3b6dfb4883411171afddc6c2e146ae350a72b53d6", + "proof": "0x000000000000000000000000000000000000000000000003b0bc4c636077df7f00000000000000000000000000000000000000000000000c117aa586632a7d70000000000000000000000000000000000000000000000009e0b19c00f4eaf50900000000000000000000000000000000000000000000000000005926f5e83484000000000000000000000000000000000000000000000003c826884218b7b614000000000000000000000000000000000000000000000004fa335c086f3dd738000000000000000000000000000000000000000000000005ebc8c216d2fd541800000000000000000000000000000000000000000000000000019c95aa585ab50000000000000000000000000000000000000000000000055872f29a4fa9f97d00000000000000000000000000000000000000000000000beca749f31bc86fd00000000000000000000000000000000000000000000000043f0151398ff08a0700000000000000000000000000000000000000000000000000029673e0864a0100000000000000000000000000000000000000000000000b4f52e86ab7896ed500000000000000000000000000000000000000000000000c9ab8c0e8c2055ee200000000000000000000000000000000000000000000000e9090f30e2b516e130000000000000000000000000000000000000000000000000001d2de7e1be2792ed32c870cccdcfbb983d0caae9d627cd97f77af98ad9f971dbb46ed3b5957eb0468b63d2b3a645e321fe7ffdb31f9d6934d48dd0181777443951d11716df03f1bffc8e1cf0e70dea4050b6a82b9cf09e57d6af48cfd0eab0ddc4b3654fcb90d003c0b81ed333746a34d0f7d76a16b0bd5da301fef04fdc17d9133e2fc7e6e8b09958442206f7fa77f8632780b922aa6aa65881d4b8c8bf7d65e8d83b2d522750dfb7651f968e17195849314bdaad43a35d536aa4433f698b29ad8d073d6b90f1ef6cef632a74763ac78bc5e3e8e6030aaae36cd01475f46135cf7bc065b9eda2f19774c97404c26b82ed4fa7b54580c19e856ad54a56a7536d6b744389ba8751ef6cef632a74763ac78bc5e3e8e6030aaae36cd01475f46135cf7bc065b9eda2f19774c97404c26b82ed4fa7b54580c19e856ad54a56a7536d6b744389ba87529211a82c07e5330dbcc2f098969da18a0e542bec3d620023c3b47ace5f759cd12bc35bfeca56fefb8cb4b8dae13f7fc5ecb9eaf06c8513f62d31dc84934f4d61a7138fa6c9091ebc96c69f0c073bf16391cabcd21cc31f310bc5d448822f82b01f244c746d7a776e5a76e79114e8c54eb201f408204a11bdb241b6153cb61f11b876bcb0aa64c435a2239229d5e4013e7898890400caf72867eff4f94f5fccf0f79993f21ded74540587aa9aa224330bdf8ff5d84ba037de75d20dfdb8d68d5038e723b713b5b17f1d2ca8a273e1d411e9ced5cd2e04951d67431eadb2af2b92cd5dc376ff64511c67d7b2c5a433b1c0996faeba6d9273f6d6dc3a914d50d481cb0e58165c2cc38877e3d2f0f0afa2546ac9dde8dd5f608b3db892f8b6d2a3820f11398e3b1759ce085b2ace07ed62eaeae605242a0ef38f6d93e0f18d2bc240bd21fae9f9ef3733539e135004c844aae17bee7e6bb8592f17c0615fd74f0031cbb3d4cd093b2ff093658b322cbd71264612709923140b7502d629229ea97a22c1bf8812aa86e4719c4ddb7c46a560675504272d7ba278ab899cb52a13e6dd820c72043bf1b5afc0b365cac16300ec36ed3c98c36d8f90753e0dcca2c0ea1e11dee3c135410d6a53d2f81dac6b613b1ebf72eb72a326c5e828561708598028e0358759e8e2b554e5a9cf9beecf4d054fdf7c311984da17a8673e8d7614302b722927991ddd19caf526a7376e3107742b5d34a88e38b17aad609d8f7b1700cea18fdce75f17f646d49fb07863e381e24468e407c61263ac4f817c08838a4a0ff2241c10367b40f0b368b5c365f80020ac43d8f68f345e92d02138c9134e9847a24bd27495c5655e3a120931ca45a0a0262d3497907498b100ea4375081a7a9400d7020b0ffcb67f378773b4064fc3baf94a13c3c451690d68e78ac3c5a25b37d01deafeb9f97111d5488c210ea91aaf880ddb9dbfb2533106177023058acce640dc30e0232e5a862875d449009be0f02fa96f3a5b9fcbce17548a8e00b39743426d549ceeed341b57576a64f9df6d767074027f8fac83b5c3a7e6e8bffaba8bc0eff056c00e2ddf477af26953da00593f048e809cfb1a946db091cf300b5b8de21bf47011153c7b9b92ac5aefef2efd28a722684ae915f16360fad96534af2f0045d7df0770dbeba70ae56a019e97c67d3312510737de5988116056afaed0fa41735dfe89344c17ef042378bec9038540d6b1e495c161edcf3b62a54bec1a5380f940a46bf7a0779aa227b0974078c619158095992abcaa41dbecff564918786245bc696726c661f9132d47c13bf3dd1f4711b31b062a85f4cb6974407f3c040073da360d9de4973ffe9c9aa5e1e3cea07827cf7bd110fe9603b59eb20164d6c26fd195ee5c3ec2f45897d1023f6ae1d01a86d03a5bacbc9bbb4c77480db9cc815b0ae61886241ba852274e3dc5d3d26c9f7fca48d2147c052ba95ff6dc70dba29d12c5e7fee32d3e57d43a1274e27cd54180b2dcbc061060a927d3af33d498e1d682e87ff28c5edf1bfa493402b1e866f243357d6730cc0cea1bbfb2a76085f182c156d6ff236d551beb955863f00f169fd34dd4baf64d3848e82f75597608323b70c21f968ab1faf3e74e629146d8db83899effd265b48ae4514282cdaa64205665c35ae49d36438508b6231f41a748f64494b14ccf3aef1819ed7d51db22e19f122189c18a952050d20584c6689f11f95c707cf304e6812d3e1b77b6681f52c3ce10c40f663719571ce710ea4f9d52a65ee84aa0e0ad72fc8919d378a0d60013506aacf63bb0eb1dbf7ec92f82bb6a3e73a491242f5834ef56c49499678eb2a24895cde3d33971093c528c5423257d8b3437068adaebfb28e74b50f5f6fb62a276b6333d791f85fae37d722d9d45e3986d99f47eaeef4ef72c456c37a30fd1182dac8074a53fdc282940a64c1bda6936823ed5fd29aaa26b10bd26319b55016fad99caa0c85957d89a675ac74a96520d29d2d35b2e3a345a093d4345f12bd0e59b791cf60ed9ca50ab7c1acda4d97115c431b6904f2e61eccfccd73437713203f955e322f899b4a9876471c6dcf74de188a73c22c2d88e97b905e2912a55e133768ad7ae40d0bb979311023fd6c38beac17ae2f4ee1383286727cd8e3ef642a68c7ee76e95f15d5628205cc7d58d48170a1ffb2dd17e93fa15bf2886902c017901beb89d8bb737bcf99c68eee3d443e3b11d42bf2592259b40e2ffa78a516282afd56f472b4d56de8bdab0f65caa5487158c8e4ee1e81b7e9e121e0627f6b1bf9c9069cfe844381bdcc404795658b2168aedf9394375a441994d61690eb5c21d44eab05943a399c71233b799aea06225e2b90be5e1c0a8c5e5981fe9ba02b1a56bddc761706ac79f485a9b11d9f50b6b07ec5f839b940e57c64d164f030e412a896c1a13bb3d43b883a403278c4705da9fd1b0afd49881c6f40b8682f96151939f042c840d282e7bf5875387a5afce32998612617441a2e6d9615caa443d92528d704f0141b83e0cd1ec03e013deb6928d27bd3508b690964b6c52dc6df2d1e44e9de7ba5daaa607c61ded2c776d0c560a39e6cc44cd3bf2124ba3fd287402c2523fffdcebbba4191a758109091d22913e3228be102ec0b6f85d53574eca71b71d4d4b9578b53e9e60de1cdc1f4e7e1620e248331ed014471bdfd48b307dd2fea81d1b891cd151021ae6256c0388bb8aeda8af28480c57e9f20604955e3f42169a94678335b12b4a9ad3c8f986293ec317d594c1fd6a7993d779de0d27e2214730d629c11e9cb14013f26dbab0e6faf7828ec6e6e35ca2d5936a47b8799d21556fe1b2ee71cde324bac75c1ac635365c99af509b6de400e4730e85b5aed6c0c1244dd7931e73f59cec61cca7c4b43bdb838c742309bba6631585096885f3521301c868d4dda12cc80f6918ca95bc96953fc41180d986c2191296a56503fcf237da65d05ed697841373c8723d6c5b4416ded97ed14279136024c4f93c7281401f87ca0a4f791bcc01ffffa95c6ef2556b5b7b98fe3502a70a24bd273e7eb8b0937256bb41ce0017cb1eb8f5b3001bc6e5ffbfdc60d0ceb9eea2605d75d02c02e7937847671ab8351ae5e1f215919d5bd2620bb117db21bcaa987d696cf1d1d21770f544f8505d021dd20c8801b6252222ac480d8ccbfc18c1472d9cf764de206bef2ac05a7a29871ef326bdb919f4269a5386ffd1f876cc490959cbfb15f691cefea1be856f06ba2a522ed0210ae2a253dadec978082d5e6c4d92f99fe5ba42b2ee9cbd3b9e23f7d11e5055daf4bd7395e621a06da7925a725835b038240ba151d047196f53c9a4d13110fc6ca893d3985b110edf477784dba632559c7cf5b11fbe3b95fb893e72cd9035c24563f90c30d5ee6341cc66e82ed47a226614e8d26700a86424d14bcfa4bb2f9c318e93587308dc2f6249efd348895668bbcb42715f6b900c0d38c0035e4e06ad186515e35610e6184138f612d1506545d7198011e5c3482f938731d9d65809b9704572efbd0ba069cc7f15867d04cd081cdfdbd26debfcb4661b4729a2ab77758eb51e6e983df19140c09487e8a3d396796bd360e287834a7080c8f2ee24ab3d31560fdbbbdb69f102f7105e2a6572e525747f115c09326abf7afa2b4adb049258c053656a7b3b16d226057c20f4dcf20f3fce522bf9a6157dc4165ad5af1e2183dee2ccf199854b280fb8e6aa550a2e593038625286ecce1f773bdfa9fb4fc0abd26f268cd28936d23a6c4b03f2734fb25832d274bd361f408aa32c61737eb0f6d1ac8f95be2b8a980c81accb4a200f48dad36031a7f6e75cb53454b2b2e319b9140bd7dd32970617a3b54e0a06119b005aa3d16853efbb3a5bf7dad414a6697bf8605b0d245eb4125467f22d5fc738a31742e1ba1e881076c409cf7d7900b2aad785b90de441eda41a16d2d725c348edb08dc15a1ac327dc28cd8b3c89716ae6685f94efcb44b6855376b4f23b5ce8c17e7752cbf3e90c4d264ee9155c60c0db2e9bd60189ab9f6448c1172eacb8a9665d16c2569f253eadbead6ef6d5ed2fec3696da00ee2cd0e51e2e5d1ed92fc9a26901703994bae21b70f320261dfcb466f47b99e289bc29e7570e752287c7bb034f4a50b096f5a37ac6a60d2b4109b3b443ae314dfaf49b80561f94cb25a069f08dcf001fb346cbeaf3c0c64ba15ab15ff0e2b8f18f46db6ee3b999fa12329c1d7794e21f963c261d4432f72f792a2c575bd26ad82be859fdc4c5fa7a2f0a36d2f937424e2a24d920ff9b2f9f3fbdf596c09ccada005568df46fd497f92b5e57dc21340fe88c47303ca4739ad7891aadd82d2926ff567385cd17f750235c73512ca5942b9c81a602cf055685f2e52afff5aaf55730d5cc7394b7647182be5e456e246e2b89fb27f87ee664d10a4703eaed5e84f52b9c54a2d03ccd175fcb7adf969ca40e11c91103a6b03a97d2d0dfa8c87ee284ba50c2045018b3a6f005488d9340c40edf40f9af7659df81cdaa71cd80b7ca6f8b9d500f7f825eedc15b23d46ffbf5199929ea64058fdc3a501880d3fa2d0d3d295d00dc094ab2e9d67991163449c42e2cb5e7c6e86456d302fa2aad830fd646905c8c898388d7f1505459f5a5d711137cad4d3b5d25698d96a47f338da5b19ba69408f8522f7f0f6368360d01a19b15dd7d44aabbccf1433c87fb0d9ce2c2dc08a092b5cf937a4fb6b4186e33fe8401a2bb267eb740ba03e6685e9823d1c0ec5d82cd64ed9fdfbce7bed1541bf1ec1a50ffafd68ac3338fde7ba63cd16f2854d672ff835c6d44ce835ea22924319d1d48928420b4fee75a909ff90b4403aa0d38e6f2d2e0d1a8613eeb3005a06112004d4b42c630f042178d2681ad7bf9b3daa98a56c8f5e0228f4ccc45b0175e841072ddab177a238ef05310b7e7ffd8f1bff7a63cb8084a969b77524ae2887fc82fc82e4e859261eee20dc67d49eb0237bf4b8386b88bdd106faaf8e6b9b8a6af1a5672f9ead7ecf7b54b3a00999007c56cce5aeb197e5107f98470c992443a472c578ede9b5f9fa22300a217b2f542b4fe8b36b00843501519ac5c3d4cc9407c061bbf5836af4e9fbd5807fda1caf95637f139c719790b6a5922ef789fb8064f22272f96e3df111c4cc882ef254ffecc295e982d7030619b95086cac1cb95a121702fe1a777c276e753dc3ef1ea4a6561a26e9287ba78a3cae3c8472363332452ba7766f7ca5e78008d61d9d5239f1d6c9f8212a41068cca1ed6e94c1ae60c6a0d95e0ad53857eca744bc2c9cce5964961d5655d57628c1061eb97c494bb920c2c48b794fd6f7b4f68590e61086e01b8f5fad14761a6f0a74ce7da6e3517fdb72528973fa0ea41b6881f2ff39489ca773f1e51be5f3f50efe6625a36f75f9b3c01bafe4b0f10b797e01a52080588c68307aeb9d46afcb2cf27fc1e6d70a62738243828bc266dc0f0fe84404d44448a5423d688a961721a9fbf32fb616bf98bcd26a9b7d63684b9ce527631903ff402792fe0860496ad48a584bf530e0216bde20231259c85c5e054db656364f72e194ca5417ef4765527c24841e97da6496c190ce3fa164065228a407390caa7ced1575432307ba36e7478f0bddd4a74dc7f4110afc9bf5c087fe57d4228d6095ce6b077a14239fe671d3c3cf7c8dc299fb94a0bf41428a7c2939dcc9c2476eb9ab345a530c52338744ad5230cff1b2747b3d20491343f15e3385765e80262c69637ae8dc401e30d79ce364a0355c9a9911aa80bec07cc9c2b28d31e704beebd090ab1c64fd7dee743e1bfdfdddb18d639323b2350bc220f008f5244b5db486711bd760cee10e2879333c01eb6865c1c82d8ba1bf2caa949782b8860338ec611555bda4ac63eb5b57b6c354d7e3d1fcbb0ce8c2bfe98d3749c0cf9f4b1aeac608454c68c694d59c985437bc6666a5f2684e80d29ed90e11ece53b05509df68c9d7d69f15c9704f01d32fe6f5d54e9a5ad9e9921f352f2bae3248e3c7243c8149475ce67784c4d84d72cba79b3369f0fc4b92332180063a35ab1dd160c5f25ddf2b03111f00bfb6a4e6c2f3651d4bd405b9a93e25db56fa55febc67a7c330a3455f3becbc72549770f9bf8e3190c4a63ffd7c8419284ec4b562585a15ce7bea5b2dc9fa0a94722bd4d3f0e3078a7d2d55cd43c51bb65407c6ee1a395bb8fc0af25e81ef717ae534c4142cd0bd9f6aa3ec10c2e10778b7c36e57f9ef1e87ad80e1889e989dcb2c752bb0a2a810382daddc8cc6850938ada93036a4d854294032431d9f48d9ddcb74e5313869638448f5f8e0c78e223f4093ecd61a5d07b79f7c943131c637d471ced11e1e187706514a67598df014dd87aa8fdf88d921eeb618e4a0c54030e525d794d65867a17cb0f8c7da64852ea7fb0ae365329ed7083e360903d3ad2edc665f8a001ca81a8367b8c17f75821d8968cce0bac555151a4b0c70276f28e80210d8b776c7e606e0eb1ec34abeac1951269d27cceb81efb7d5dd4c2b7f5639b815d5320d96b8ef19a9890b327ccf23357f6ed57047f065d168050225dc1089baa5eb28e7273240c1d3a5cc5e27482a5cfd61b40625fba93d32883ac57045a5d14186c105a0c72e9f3e464007ebbe0da707a4fa8648fab3665a145177f859c39045cf75739574905ee6b5b8fdf1612f117ae1dd14d14f5b1e7fde269405c291f5c3f2837ddb66dd1f1fa83fcb989322c00c5317aea0c57862af02b814231eba3d150f5016e92b628e541c24f53b042f799d001e5e79dbc86ee5d3750b3377fbff65ec7e3b9b0d6035cb31a31e78ee0cee6720480f126816f8cc4b92d867b599afac12bd3cfbae0529b3f9b80aa0ed27c3e6385893dff878ed762aa14ef58fd4ea8bb009f75c2348ec3f36862455b413ccf7fee8b7bca236d6270c841df5553b15ca8a7613ad7ca9f10ef5352285f423233f100051609ce8c93ae8119decf29331276af5902c8b37449192d0c2c0ad0dda1e058ad892ab72ac37cbea09aa2f75e201ed18b82c12f2c2da8e0ed5888a09060135c283c9e884adb5a71241b361b3428eb09eacb346d792e7a4c785426e121c600c1d2b083ffb985f859c27203dad47121ddaa50a470524269397193b7d0acba3f83086a20cbb42b3866b10deea0188b9e4fc7e00d55ac207a95cd9bfa6184f949fabb6546474b4349f0029409887485014cdc2409ca7c3cd5d1d07192f1423884729e1e66856f8c02ea7703c66784bbd06b7d44b05396b866f237ce5700c2b210764711e8cd43b12b4838097c46374a040f90815747a6989f67f1eb20127a12c73219643c387859c9b726063729b72cb89ddf69daaa956c4e76e8905ac0d092f6dbe530e72ee53430b2274e97aa21156fcec1d2751147a28e62eb849a500d170a9bdcb64640f25ece5165a333058f64b146ecbbc6deb2001910c4c107e2b19000dfdeab9c14512162d9dc09ea57c4b0536042f283dee38624a67e8a5fc1816711a57fd4dd08d98395ddc35613af273ea11c189eff1502cf46d587b604a04eccfd8769a64134f466ddacdb385554a77521281c7cba3fded10010a1af29003ad51a3a004e22b6109eee58f086bbafd95f29b50b64ecd29193dcd839ba1da00cd0ba82e4aa6c5c247166e2fc65484cd322b22e8c0dbe7e4bb8f6d6a3ec68f07bba7510472a5b41f3633a88cea817d702b6d3519815a615b1298d9d3a50652024d9b74e045d0b052a5ff9456dff68f6f4453639c5f90ac5a269fa1de776f6f2f1c5ecc4823ab90a4f38b8b7e173ad47b68a9b08cec140921614a76af59a78205153158f461750596b50d761455f15106fd20d163471586443c54e786492ecd287add7337ba607d788a559c0dff0f65772b78604ebb4b5d38c565e9eb621b4a049caaf97595ccdde3ed73b85a88f0abdb32bdbfe30d5c54f6fcc39d704d15d414c6dbc3b62211999e59020e61819b9d73135411f9940ea92d04465793b3982a2c17a07067acb8d3ea0ffccd61b774b0383bb6112738e68e35f3d3b44de3559f037a59e84b3e56a49b8078e408a444a51adfc55fbae8ff2621b4b125169e62a925cecf67dadf76612987236d680e4724b4cc13ea35dc658e0f8dada2bc86635616a5d5498d9e1823a8f9dcfb005c577fc9c9652ce7d118a8c01a99255057c0b8152fedbc2596259d5865a78ca63c55895a834e04b1f87a5d5c08199fefbb64fb2180c1a36e3e8e58cb85aaabaca067825c66eead6ce9df803e77cfcf43b98d502f2645919e9be3aa0cca1ef472bc8c39cbd9b22b116ce05473f0fbb5dbc1d5081706e18f56f50b12a4a9fceda8e2b51396046ca57026c048a8aa5616a07f992d15ee990a57b2aa1db839f96c38430cd243d1960e4e853154f96fd111c014a296165587c8fc3807acecc3138f461728c34cfa670aac94510b7babfb0e06b949640811b2704aab15b49a5d4bb80d73f8ec92764e817e16d286d2d672e8111c7c7c13325e02dc0bf8e8b8e0075c3af89bd3a46561a215aabbd350ea151ac0e714ef10466aa04bb93a3422a298d27612edb8433fca232d46f74d0d85c45e4dcfb75d2558d3b676f5299c1ff1e40724d545ab8af0a3f659379bf55a4a1929a41b253f2d3b7476f3650f099b13e275ec15ec024451225540cb52bcff098af49189ab5021961aea4f15126c878b64cf6dbc4eadb6a177c4265e9892f87bee31defce8e2199cbe423cd34c4fa28270067abe3b8a09301ad4c8accc77a60cc3b7a2cab1c60ae847cc6b88ea29e25e5d0be3788879061e8ec22015f354eb8869f49f8620272cca089fa324273d882faa3b26a8bd4c9beb5fe67f983f18083a4b87d16b763b2885bfb51698e3e24ad4d3a6a829db7e62d8238af94873266c819df65b2b135805a7a709ff386fe77620444729d0b1b59e5bd1264316bf6ba6075db597d414a02d350a79faf9dc61c4140972c515a947d0db3618f68a646d1452b527b94195d9259769a9ea9d314cd5f0ea286e4be9df1aa980980dbde276528d7e8ad0d2deb519448b3be2e69c9811333d1cef45e8a749c6194d81612a3780e1fa6b405a74bc0cc0d3606bb70dedec5cac17b15a53f46a36847ac44e3da42c9199d75f901f7c203352c848bb9ae55e1ea15e2c9d801adff1947cc1898d5075029a533cdedc501bffedf98d517fdb7a94add4a93957ffe189b5b97a7ada0ac8efa8a9480207680078e752e0617d9c439acfa643430fda8a2e7a9414067fe26d01680288f7d477052e2d537f7cfe92b6c6912137879aac0c8d377b3ad09e6b9055c82afbbffd762fec07bc335ddfe7d24ee48bc988b3823fa6932f9d0fda2fe094e22b3759d11900625e25bb4ae6ce76bd092f0ac479268fa53ad4a874e397cd5470ad59bdf52529de3ab3c88dcf8487163541c3be541465cda34a1a58c6b5e1d139057e45a300212391cc7ac7c196c23049399d9fa106655ae7dab6841b372631c84606a3adcc08c38429fde36d57651b5529504ca651343d5dbb007d072a13bc1c868e52fa8004d42eca5fad2c6b71929bce470e3d9df006c23d14db1b76005a65c665d2ea0501cefb8de10411fb89055aed2ca1770dfb1ce29f278daaed142dba6744cf5959164462a8ae7dccf4d564617ff3b0e4eca06830239098a06bc74c49e1d5c568120700f027bccfc262842283f69ad8c36d356a9599ad2792fad7716bc3f27e70522c53345537c11a28bf5a17bb3d2c8aca1a4ac7ffc865437fada2b8b1f602da10301c295a17495254bbde0abb6c3265d67e448b3b8843f15f5ec201c07a88162c037507ebdbbaf585015d984b18864f9517ec5b0f492520804a2b4cee4e37328b2dce69dc7e42b53d25560d3f8efd97447bd466436bdfc9ec7f85cc0b1a0a376512945fb9a11d8ff51ebbeec3b16e19c9f91ef8b275aa7204553b7f6f230507e7267c296ff968bd8c47f38c95ec4a0fb7cfeae50c8192f6a1221fe2b40f51491e070880ed9cc03e5665e41d2f9502e0f30a70ecdd7000efc9f0ca45e0ae795ca72cf75838b345b08ef2048bc962612dda9876ac2c1c5bdb836da371ec730186330accea226a30862eb9f18b1d84ce47b65274e759a8f74d21c1ce41d579b66ad32039abb2307d7aeec6aa91d448b86c8c67e6bfdcced34188eee1898290ac74542039abb2307d7aeec6aa91d448b86c8c67e6bfdcced34188eee1898290ac74541ba62324d0b5dddf8dfcd447d1ea12fcd08f9297ebaa47748c77cc5b2ff330cf218efc799fb87ab915713189d2d0659c369a65e4e772f4b1fcacf468f0ceb4230aae06084cfb9a91ecb7fe763af24d3d0cbd883efed43313f93e46782525d54f05087ca499e2653e5d932fb7ba1dbd4f771677b7310567c759154007461ae04529beec13e2b4f89c45a4bfc08c7074b3c1d1830e0decb49a79f71e8f6cedc5560c0cbebaa45e923846d8bca385bc3cae035b5a6c5434d5ac3f1c6c8f6238dc9a28ae145b4919dc48d0800dfe9da0a4800cb8c53bf7185ea9c7e1a0fc3e3f58460a28d633a55e43230c7fc5dfec31d1d7c5c772416ecc816ba83c10d021e207d22d51aa1c795c2f4b3d997b2b2b26dad2640942142d2c173e43a521448db5da0d17c208c9077b11accb07982635eb9352e6724a95af2ea1f90a6c841c54b7198b00f36a8a0e3fb3fb36c57502e78c6b708adc6896bbcc2499e4602dc3dbf7d44c1a81cda4e4e4c675137b8a222b4f5e4ae40cef249035d98c2c4d654585c819dc07ef6bddd6207945ac00e3aeccb1f742eedd1d7c9e64e00ec6e06961334b338b0106afa8a3223289a5953c5777d6355ca31e974b07ebed93cf82458f8deccdb710eb3a72eb5bbce9b7963b710d13ab577996f4c33ee7e95cbbbe6c166d05a6f00cfa1041b2e16ada38876d40462daacd71317e5f8bde78538b0cb8c688720f2c18b8b8b3d519cc86be43c25b2221843af7f420c58cc28dff3c974d77e4efaeae107c85cd4a21e320a5841f82d85431a813f0e844cf4d41f9f14f6ca1eb35fc5b2dcdebd223cc0f740905d9af26c6eb5f772f82e9d28b9f5e170062f7f31cf7be265b044d533cc973e42a17060ee4e0bbd1775b0bb0fbd652a1dccb713f29fbb41267a946843e21a55122b12800bf6286116aeceb25883a08abebee958bc3b43d058c19f56c37efadcb20959c957fe12dc86e277f87c45a735ff18408a7e4a3c52539d5d80e596dfff9c5165097da8867ec16966b9ef655ef22d00895e1be564d225a59402a43acab00af80d6566a6b5bc076edc37f66711790e830f9c6e467ac133af41089aa380b9787f79785e5140f79d33da0963a6d311897df5415f0e8731e49e234c9def7e17dff72f32a2a79e993ede67c14be4d14f0e9945beadcaf51293783d0dc0a20261633967628a804241dde4954feeb805ddac10b7087d306940c714194507027dedd082ffa5a348038cf98ec6872386249400922c72ee0d7a225cdc2341b19769b984ddd55b072107a42358f112bb0a366de1193d4d5212edb147c764af3612c7ca9e03b5cab160df52617553fba97135143bbdcce5f0a81be2e098169efa2738598b1e31a7577dd94e731201b6d3ac3cb6e94446857b81f3c1eddf9f7aa18bf2a20659375c5fae21555535a37e09f13e1b6dc81e955b0fe5512e6b458e9b5abd4f1c86539e5c22f5d317bb6227ef949d2d73ef146c52658ab0a12b310bf5c7c5c00a8cacfc8a51b2048a1592b0277e5eca1f452fe9cadc38907f5c082fd0baa9af79cf81929b2acc7a65ff3a33c19e5b4fa60c51b9f5ab56a1b8f8bd973b0bed3bc9c38ca5f6918b66015d004afb03a220f06728b4e0c818d2fa8d5b0847b115ead9ed8eeb3270efe4115815819f36c77db77918d70e519a5159cc4d6b31bc4724e1d07631600d5c9623cbf3c559f2e90b781286373e8828b0c5ff3488fe63d0736f5574942351561a6da326b7bdc6f7e047b6c65e0f11de8067eaac5bed94b692c726fe0884593c15380702559978824cd7f46e4247612390ee2b7c19d61d7e6d39de87e5d95b40bd9cc4b4d1384002765688b605741f74a19baa739ccd7c8f5b4f2eba06e810ce2cc9c27ee8f7cd48980057dd33feab20b24775c1e468d31e3bce28e4e45ff316f58ac177a496715c2dbe99b49ce02552925e8214b15f0977f1fe155258a3b5cb2ac2cd15feda1cb4a42647f186494d55c2afc7188294b7cbd387dc7c716b7c1b99449553d1af55fb4d6ed4642641f3ba119806913b5e206955349269a61561c0414aae70e7056aeaa0489cb92fb58597812103aef163213ebf3a53dd3de58f9567630547474fe10fdf1e596c69a5d43ef0ca8d480c2e658eae67b5fd91a6ad474cf2a68f73edc26c4e906bf08f7b08a2526284ebe67a96041fed93cd239537993c456c12f71d61a7a6a0f94d62d3c66b12023308e18b23a455f6e97ffeb9a6f770bd51927483bc1cb8813a35a7433d6962b46b7a8505e4f6f2dbdfbf91aed01a14ddfbee54a91e4f7ed434959daf359d30e62ac285dd3cbca26567bbfc8ea9b4fd982d8ecd882a8b4d1d2fe0d573f2e471e24dcd467e52379566b821feed247d9c3ce6b11b519ec7a563ef2944b41f756214238675f6c04b010665f1ffc826fbb08a176fb2c2a1a7b667b7ec844a919dd08f426d879df3490f1d2a1b137eb06ceb7ed7106ae78a0b8fe7416e95c8151fe278be91bb459a08037bcce480502e81d7690ca590355882045fcd8a521b9ffca22f014b6dedd48f270704dcdf4ae70bb7146a6cab87f3733819783cd0b07323a062cbbbfb490db19700027d6fe42616990047ef54624118c98a93c90e4a413f40d29e0506c4cb77f64ccb392abdcf8e5cbeb6dc3c75d2579d645f160f2b02bd02c8bcf96aad953847d71d3b7c5c5e5bb181357eb6328377afe3394466fe40147029fa47508baba5448e5710e7aa0823048b8b0f72ecb7b5aa568d55ceeec51b11a8fff8acf7e19e78c7e11653ad651032c76b756abdd11f91d5e6a0e617284221c449dc19347dfc2f2c55a44f554a22cce11c4e85ccfdfc2cbbe9c498320919900ec30e7b5abd6d228aae2b9cfbbc3083c2c86fac1354740ab750e9c19d5ec0c1a5efc1c7a8980f775eb958dba693d1832e8d13db65096cc813cc27abcad9186102e8b088302ec238bb751c4485dbfc2d81faa1bb67c37e78f9ce741e93947580fcb5b2b4eb3f98b88ec3a237b85ebf3f42651627120fb39b4aa82d91c29e0691c5089799d895970f25d5e485d3a074101a23afb796a3208d5b770d236bc91ba0733ed5387ba855037cb1985eb960d9f5ae72b4353c331fcce3787d1ad9f0cb916d23589b29c244031cabf85f96d175c30d661ec31e729e5bf98dab98c868831132c02bea08c55af90399e9258245070bd36bdbae5e9644fbee81b6becf7692c03db81ee8017239c3f875e82c3efa231f84961c4d2796a1d9c7e4b78f8d011a9057b7ed4fadb22ffd3d76ef76717485988c823601126b7305eaa135cd5752b6a05ca7c1adafeb8443ce5cb8d9f71ace5729fefe31416f7f4245f204e9cb835b7077f532285884119184db144773c36e44283539955c322e5b05f874f4a4a04521e1ee5be4e224a839d47cfd7392c75dbdc35e08c81031139dc00d0ad63ca66e8", + "publicInputs": [ + "0x2f696abafd61692fe9c82281fd461431f5ff1d3ec31c10b2258b3151d89b9c6d", + "0x2c0ba69927ad2b3737a57195469c8185f0bcf42ea920cb0ac4963981f23f9e87", + "0x0000000000000000000000000000000000000000000000000000000069aa86df", + "0x0049960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97", + "0x00afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce28", + "0x0001d056a1fd5ece845559d52d6b09ad23b63488b95f5c74e708c6615c124417", + "0x007e54c9c86ca576f0db71833eb6c815d41ac3372f465221f9bc6a5bea553758", + "0x00ed19a8809304eda854b241e3841938f2011b9ce7a9c392fdbaa02382539b8d", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x12304d7feba021ee21ddd4ab53402116177980f3fc3183c2dcb5f731df55eb20" + ] + }, + "committedInputs": "0x05025849524e4952514c425950524b53444e534f4d53595255534159454d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000801fd0100145ff4e90efa2b88cf3ca92d63d244a78a88219abf020003aa36a70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900212dfcc0ca426d9d8e751bb00fc9ab502bfb081ba8d2ce3f5f94a8f1712b3afca800", + "serviceConfig": { + "validityPeriodInSeconds": 604800, + "domain": "localhost", + "scope": "hello-world", + "devMode": false + } + } + ], + "account": "0x5ff4e90Efa2B88cf3cA92D63d244a78a88219Abf" +} \ No newline at end of file