From 0b2dbfac8d1567e5feff50f510ba551a74d7adfd Mon Sep 17 00:00:00 2001 From: motemotech Date: Mon, 7 Oct 2024 19:58:01 +0900 Subject: [PATCH 1/3] add OneTimeSBT.sol and VerifiersManager.sol --- contracts/contracts/OneTimeSBT.sol | 393 ++++++++++++++++++ contracts/contracts/constants/constants.sol | 13 + .../interfaces/IVerifiersManager.sol | 50 +++ .../contracts/verifiers/VerifiersManager.sol | 65 +++ 4 files changed, 521 insertions(+) create mode 100644 contracts/contracts/OneTimeSBT.sol create mode 100644 contracts/contracts/constants/constants.sol create mode 100644 contracts/contracts/interfaces/IVerifiersManager.sol create mode 100644 contracts/contracts/verifiers/VerifiersManager.sol diff --git a/contracts/contracts/OneTimeSBT.sol b/contracts/contracts/OneTimeSBT.sol new file mode 100644 index 00000000..9079add0 --- /dev/null +++ b/contracts/contracts/OneTimeSBT.sol @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +import {IVerifiersManager} from "./interfaces/IVerifiersManager.sol"; +import "./constants/constants.sol"; + +import {Base64} from "./libraries/Base64.sol"; +import {Formatter} from "./Formatter.sol"; +import {IRegister} from "./interfaces/IRegister.sol"; +import "hardhat/console.sol"; + +contract OneTimeSBT is ERC721Enumerable, Ownable { + using Strings for uint256; + using Base64 for *; + + IVerifiersManager public verifiersManager; + Formatter public formatter; + + uint constant scope = 1; + + mapping(uint256 => bool) public nullifiers; + mapping(uint256 => uint256) public sbtExpiration; + + struct AttributePosition { + string name; + uint256 start; + uint256 end; + uint256 index; + } + + struct Attributes { + string[8] values; + } + + AttributePosition[] public attributePositions; + + mapping(uint256 => Attributes) private tokenAttributes; + + constructor( + IVerifiersManager v, + Formatter f + ) ERC721("OpenPassport", "OpenPassport") { + verifiersManager = v; + formatter = f; + setupAttributes(); + transferOwnership(msg.sender); + } + + function setupAttributes() internal { + attributePositions.push(AttributePosition("issuing_state", 2, 4, 0)); + attributePositions.push(AttributePosition("name", 5, 43, 1)); + attributePositions.push( + AttributePosition("passport_number", 44, 52, 2) + ); + attributePositions.push(AttributePosition("nationality", 54, 56, 3)); + attributePositions.push(AttributePosition("date_of_birth", 57, 62, 4)); + attributePositions.push(AttributePosition("gender", 64, 64, 5)); + attributePositions.push(AttributePosition("expiry_date", 65, 70, 6)); + attributePositions.push(AttributePosition("older_than", 88, 89, 7)); + } + + function mint( + uint256 prove_verifier_id, + uint256 dsc_verifier_id, + IVerifiersManager.RSAProveCircuitProof memory p_proof, + IVerifiersManager.DscCircuitProof memory d_proof + ) public { + // require that the current date is valid + // Convert the last four parameters into a valid timestamp, adding 30 years to adjust for block.timestamp starting in 1970 + uint[6] memory dateNum; + for (uint i = 0; i < 6; i++) { + dateNum[i] = p_proof.pubSignals[PROVE_RSA_COMMITMENT_INDEX + i]; + } + uint currentTimestamp = getCurrentTimestamp(dateNum); + + // Check that the current date is within a +/- 1 day range + require( + currentTimestamp >= block.timestamp - 1 days && + currentTimestamp <= block.timestamp + 1 days, + "Current date is not within the valid range" + ); + + // check blinded dcs + if (keccak256(abi.encodePacked(p_proof.pubSignals[PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX])) != keccak256(abi.encodePacked(d_proof.pubSignals[DSC_BLINDED_DSC_COMMITMENT_INDEX]))) { + revert ("Blinded DSC commitments are not equal"); + } + + if (!verifiersManager.verifyWithProveVerifier( + prove_verifier_id, + p_proof + )) { + revert ("Invalid Prove Proof"); + } + + if (!verifiersManager.verifyWithDscVerifier( + dsc_verifier_id, + d_proof + )) { + revert ("Invalid DSC Proof"); + } + + nullifiers[p_proof.pubSignals[PROVE_RSA_NULLIFIER_INDEX]] = true; + + // Effects: Mint token + address addr = address(uint160(p_proof.pubSignals[PROVE_RSA_USER_IDENTIFIER_INDEX])); + uint256 newTokenId = totalSupply(); + _mint(addr, newTokenId); + + // Set attributes + uint[3] memory revealedData_packed; + for (uint256 i = 0; i < 3; i++) { + revealedData_packed[i] = p_proof.pubSignals[PROVE_RSA_REVEALED_DATA_PACKED_INDEX + i]; + } + bytes memory charcodes = fieldElementsToBytes( + revealedData_packed + ); + + Attributes storage attributes = tokenAttributes[newTokenId]; + + for (uint i = 0; i < attributePositions.length; i++) { + AttributePosition memory attribute = attributePositions[i]; + bytes memory attributeBytes = new bytes( + attribute.end - attribute.start + 1 + ); + for (uint j = attribute.start; j <= attribute.end; j++) { + attributeBytes[j - attribute.start] = charcodes[j]; + } + string memory attributeValue = string(attributeBytes); + attributes.values[i] = attributeValue; + // console.log(attribute.name, attributes.values[i]); + } + + sbtExpiration[newTokenId] = block.timestamp + 90 days; + } + + function fieldElementsToBytes( + uint256[3] memory publicSignals + ) public pure returns (bytes memory) { + uint8[3] memory bytesCount = [31, 31, 28]; + bytes memory bytesArray = new bytes(90); // 31 + 31 + 28 + + uint256 index = 0; + for (uint256 i = 0; i < 3; i++) { + uint256 element = publicSignals[i]; + for (uint8 j = 0; j < bytesCount[i]; j++) { + bytesArray[index++] = bytes1(uint8(element & 0xFF)); + element = element >> 8; + } + } + + return bytesArray; + } + + function sliceFirstThree( + uint256[12] memory input + ) public pure returns (uint256[3] memory) { + uint256[3] memory sliced; + + for (uint256 i = 0; i < 3; i++) { + sliced[i] = input[i]; + } + + return sliced; + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId, + uint256 batchSize + ) internal virtual override { + super._beforeTokenTransfer(from, to, tokenId, batchSize); + require( + from == address(0), + "Cannot transfer - SBT is soulbound" + ); + } + + function isExpired(string memory date) public view returns (bool) { + if (isAttributeEmpty(date)) { + return false; // this is disregarded anyway in the next steps + } + uint256 expiryDate = formatter.dateToUnixTimestamp(date); + + return block.timestamp > expiryDate; + } + + function getIssuingStateOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[0]; + } + + function getNameOf(uint256 _tokenId) public view returns (string memory) { + return tokenAttributes[_tokenId].values[1]; + } + + function getPassportNumberOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[2]; + } + + function getNationalityOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[3]; + } + + function getDateOfBirthOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[4]; + } + + function getGenderOf(uint256 _tokenId) public view returns (string memory) { + return tokenAttributes[_tokenId].values[5]; + } + + function getExpiryDateOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[6]; + } + + function getOlderThanOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[7]; + } + + // This is the function to check if the sbt is not expired or not + function isSbtValid( + uint256 _tokenId + ) public view returns (bool) { + uint256 expirationDate = sbtExpiration[_tokenId]; + if (expirationDate < block.timestamp) { + return true; + } + return false; + } + + function getCurrentTimestamp( + uint256[6] memory dateNum + ) public view returns (uint256) { + string memory date = ""; + for (uint i = 0; i < 6; i++) { + date = string( + abi.encodePacked(date, bytes1(uint8(48 + (dateNum[i] % 10)))) + ); + } + uint256 currentTimestamp = formatter.dateToUnixTimestamp(date); + return currentTimestamp; + } + + function isAttributeEmpty( + string memory attribute + ) private pure returns (bool) { + for (uint i = 0; i < bytes(attribute).length; i++) { + if (bytes(attribute)[i] != 0) { + return false; + } + } + return true; + } + + function appendAttribute( + bytes memory baseURI, + string memory traitType, + string memory value + ) private view returns (bytes memory) { + if (!isAttributeEmpty(value)) { + baseURI = abi.encodePacked( + baseURI, + '{"trait_type": "', + traitType, + '", "value": "', + formatAttribute(traitType, value), + '"},' + ); + } + return baseURI; + } + + function formatAttribute( + string memory traitType, + string memory value + ) private view returns (string memory) { + if ( + isStringEqual(traitType, "Issuing State") || + isStringEqual(traitType, "Nationality") + ) { + return formatter.formatCountryName(value); + } else if (isStringEqual(traitType, "First Name")) { + return formatter.formatName(value)[0]; + } else if (isStringEqual(traitType, "Last Name")) { + return formatter.formatName(value)[1]; + } else if ( + isStringEqual(traitType, "Date of birth") || + isStringEqual(traitType, "Expiry date") + ) { + return formatter.formatDate(value); + } else if (isStringEqual(traitType, "Older Than")) { + return formatter.formatAge(value); + } else if (isStringEqual(traitType, "Expired")) { + return isExpired(value) ? "Yes" : "No"; + } else { + return value; + } + } + + function isStringEqual( + string memory a, + string memory b + ) public pure returns (bool) { + return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } + + function substring( + bytes memory str, + uint startIndex, + uint endIndex + ) public pure returns (bytes memory) { + bytes memory strBytes = bytes(str); + bytes memory result = new bytes(endIndex - startIndex); + for (uint i = startIndex; i < endIndex; i++) { + result[i - startIndex] = strBytes[i]; + } + return result; + } + + function tokenURI( + uint256 _tokenId + ) public view virtual override returns (string memory) { + require( + _exists(_tokenId), + "ERC721Metadata: URI query for nonexistent token" + ); + Attributes memory attributes = tokenAttributes[_tokenId]; + + bytes memory baseURI = abi.encodePacked('{ "attributes": ['); + + baseURI = appendAttribute( + baseURI, + "Issuing State", + attributes.values[0] + ); + baseURI = appendAttribute(baseURI, "First Name", attributes.values[1]); + baseURI = appendAttribute(baseURI, "Last Name", attributes.values[1]); + baseURI = appendAttribute( + baseURI, + "Passport Number", + attributes.values[2] + ); + baseURI = appendAttribute(baseURI, "Nationality", attributes.values[3]); + baseURI = appendAttribute( + baseURI, + "Date of birth", + attributes.values[4] + ); + baseURI = appendAttribute(baseURI, "Gender", attributes.values[5]); + baseURI = appendAttribute(baseURI, "Expiry date", attributes.values[6]); + baseURI = appendAttribute(baseURI, "Expired", attributes.values[6]); + baseURI = appendAttribute(baseURI, "Older Than", attributes.values[7]); + + // Remove the trailing comma if baseURI has one + if ( + keccak256(abi.encodePacked(baseURI[baseURI.length - 1])) == + keccak256(abi.encodePacked(",")) + ) { + baseURI = substring(baseURI, 0, bytes(baseURI).length - 1); + } + + baseURI = abi.encodePacked( + baseURI, + '],"description": "OpenPassport guarantees possession of a valid passport.","external_url": "https://openpassport.app","image": "https://i.imgur.com/9kvetij.png","name": "OpenPassport #', + _tokenId.toString(), + '"}' + ); + + return + string( + abi.encodePacked( + "data:application/json;base64,", + baseURI.encode() + ) + ); + } +} \ No newline at end of file diff --git a/contracts/contracts/constants/constants.sol b/contracts/contracts/constants/constants.sol new file mode 100644 index 00000000..6b86e9b8 --- /dev/null +++ b/contracts/contracts/constants/constants.sol @@ -0,0 +1,13 @@ +uint256 constant PROVE_RSA_NULLIFIER_INDEX = 0; +uint256 constant PROVE_RSA_REVEALED_DATA_PACKED_INDEX = 1; +uint256 constant PROVE_RSA_OLDER_THAN_INDEX = 4; +uint256 constant PROVE_RSA_PUBKEY_DISCLOSED_INDEX = 6; +uint256 constant PROVE_RSA_FORBIDDEN_COUNTRIES_LIST_PACKED_DISCLOSED_INDEX = 38; +uint256 constant PROVE_RSA_OFAC_RESULT_INDEX = 40; +uint256 constant PROVE_RSA_COMMITMENT_INDEX = 41; +uint256 constant PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX = 42; +uint256 constant PROVE_RSA_CURRENT_DATE_INDEX = 43; +uint256 constant PROVE_RSA_USER_IDENTIFIER_INDEX = 49; +uint256 constant PROVE_RSA_SCOPE_INDEX = 50; + +uint256 constant DSC_BLINDED_DSC_COMMITMENT_INDEX = 0; \ No newline at end of file diff --git a/contracts/contracts/interfaces/IVerifiersManager.sol b/contracts/contracts/interfaces/IVerifiersManager.sol new file mode 100644 index 00000000..865fb383 --- /dev/null +++ b/contracts/contracts/interfaces/IVerifiersManager.sol @@ -0,0 +1,50 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IVerifiersManager { + + error ZERO_ADDRESS(); + + struct RSAProveCircuitProof { + uint[2] a; + uint[2][2] b; + uint[2] c; + uint[51] pubSignals; + } + + struct DscCircuitProof { + uint[2] a; + uint[2][2] b; + uint[2] c; + uint[1] pubSignals; + } + + function verifyWithProveVerifier( + uint256 verifier_id, + RSAProveCircuitProof memory proof + ) external view returns (bool); + + function verifyWithDscVerifier( + uint256 verifier_id, + DscCircuitProof memory proof + ) external view returns (bool); + +} + +interface IProveVerifier { + function verifyProof ( + uint[2] calldata _pA, + uint[2][2] calldata _pB, + uint[2] calldata _pC, + uint[51] calldata _pubSignals + ) external view returns (bool); +} + +interface IDscVerifier { + function verifyProof ( + uint[2] calldata _pA, + uint[2][2] calldata _pB, + uint[2] calldata _pC, + uint[1] calldata _pubSignals + ) external view returns (bool); +} \ No newline at end of file diff --git a/contracts/contracts/verifiers/VerifiersManager.sol b/contracts/contracts/verifiers/VerifiersManager.sol new file mode 100644 index 00000000..42d35ab6 --- /dev/null +++ b/contracts/contracts/verifiers/VerifiersManager.sol @@ -0,0 +1,65 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import {IVerifiersManager, IProveVerifier, IDscVerifier} from "../interfaces/IVerifiersManager.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract VerifiersManager is IVerifiersManager, Ownable { + + enum VerificationType { + Prove, + Dsc + } + + mapping(uint256 => address) public prove_verifiers; + mapping(uint256 => address) public dsc_verifiers; + + constructor () { + transferOwnership(msg.sender); + } + + function verifyWithProveVerifier( + uint256 verifier_id, + RSAProveCircuitProof memory proof + ) public view returns (bool) { + bool result = IProveVerifier(prove_verifiers[verifier_id]) + .verifyProof( + proof.a, + proof.b, + proof.c, + proof.pubSignals + ); + return result; + } + + function verifyWithDscVerifier( + uint256 verifier_id, + DscCircuitProof memory proof + ) public view returns (bool) { + bool result = IDscVerifier(dsc_verifiers[verifier_id]) + .verifyProof( + proof.a, + proof.b, + proof.c, + proof.pubSignals + ); + return result; + } + + function updateVerifier( + VerificationType v_type, + uint256 verifier_id, + address verifier_address + ) external onlyOwner { + if (verifier_address == address(0)) { + revert ZERO_ADDRESS(); + } + if (v_type == VerificationType.Prove) { + prove_verifiers[verifier_id] = verifier_address; + } + if (v_type == VerificationType.Dsc) { + dsc_verifiers[verifier_id] = verifier_address; + } + } + +} \ No newline at end of file From f50e7eeeb0ce8f80edc11c6cbe4229a81212fc10 Mon Sep 17 00:00:00 2001 From: motemotech Date: Tue, 8 Oct 2024 00:59:40 +0900 Subject: [PATCH 2/3] delete attributePositions and made library to get attribute from passport format --- contracts/.gitignore | 5 +- contracts/contracts/OneTimeSBT.sol | 88 ++++--------- contracts/contracts/constants/constants.sol | 9 ++ .../contracts/libraries/AttributeLibrary.sol | 117 ++++++++++++++++++ .../contracts/mocks/VerifiersManagerMock.sol | 30 +++++ contracts/test/OneTimeSBT.ts | 60 +++++++++ 6 files changed, 245 insertions(+), 64 deletions(-) create mode 100644 contracts/contracts/libraries/AttributeLibrary.sol create mode 100644 contracts/contracts/mocks/VerifiersManagerMock.sol create mode 100644 contracts/test/OneTimeSBT.ts diff --git a/contracts/.gitignore b/contracts/.gitignore index 3591bac9..27eeebd3 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -10,4 +10,7 @@ cache artifacts ignition/deployed_addresses.json ignition/parameters.json -ignition/deployments \ No newline at end of file +ignition/deployments + +#Local verifier +contracts/verifiers/local/* \ No newline at end of file diff --git a/contracts/contracts/OneTimeSBT.sol b/contracts/contracts/OneTimeSBT.sol index 9079add0..60f901b6 100644 --- a/contracts/contracts/OneTimeSBT.sol +++ b/contracts/contracts/OneTimeSBT.sol @@ -2,42 +2,28 @@ pragma solidity ^0.8.18; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import {IVerifiersManager} from "./interfaces/IVerifiersManager.sol"; -import "./constants/constants.sol"; - import {Base64} from "./libraries/Base64.sol"; import {Formatter} from "./Formatter.sol"; -import {IRegister} from "./interfaces/IRegister.sol"; +import "./constants/constants.sol"; +import "./libraries/AttributeLibrary.sol"; import "hardhat/console.sol"; -contract OneTimeSBT is ERC721Enumerable, Ownable { +contract OneTimeSBT is ERC721Enumerable { using Strings for uint256; using Base64 for *; IVerifiersManager public verifiersManager; Formatter public formatter; - uint constant scope = 1; - - mapping(uint256 => bool) public nullifiers; mapping(uint256 => uint256) public sbtExpiration; - struct AttributePosition { - string name; - uint256 start; - uint256 end; - uint256 index; - } - struct Attributes { string[8] values; } - AttributePosition[] public attributePositions; - mapping(uint256 => Attributes) private tokenAttributes; constructor( @@ -46,21 +32,6 @@ contract OneTimeSBT is ERC721Enumerable, Ownable { ) ERC721("OpenPassport", "OpenPassport") { verifiersManager = v; formatter = f; - setupAttributes(); - transferOwnership(msg.sender); - } - - function setupAttributes() internal { - attributePositions.push(AttributePosition("issuing_state", 2, 4, 0)); - attributePositions.push(AttributePosition("name", 5, 43, 1)); - attributePositions.push( - AttributePosition("passport_number", 44, 52, 2) - ); - attributePositions.push(AttributePosition("nationality", 54, 56, 3)); - attributePositions.push(AttributePosition("date_of_birth", 57, 62, 4)); - attributePositions.push(AttributePosition("gender", 64, 64, 5)); - attributePositions.push(AttributePosition("expiry_date", 65, 70, 6)); - attributePositions.push(AttributePosition("older_than", 88, 89, 7)); } function mint( @@ -85,26 +56,21 @@ contract OneTimeSBT is ERC721Enumerable, Ownable { ); // check blinded dcs - if (keccak256(abi.encodePacked(p_proof.pubSignals[PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX])) != keccak256(abi.encodePacked(d_proof.pubSignals[DSC_BLINDED_DSC_COMMITMENT_INDEX]))) { + if ( + keccak256(abi.encodePacked(p_proof.pubSignals[PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX])) != + keccak256(abi.encodePacked(d_proof.pubSignals[DSC_BLINDED_DSC_COMMITMENT_INDEX])) + ) { revert ("Blinded DSC commitments are not equal"); } - if (!verifiersManager.verifyWithProveVerifier( - prove_verifier_id, - p_proof - )) { + if (!verifiersManager.verifyWithProveVerifier(prove_verifier_id, p_proof)) { revert ("Invalid Prove Proof"); } - if (!verifiersManager.verifyWithDscVerifier( - dsc_verifier_id, - d_proof - )) { + if (!verifiersManager.verifyWithDscVerifier(dsc_verifier_id, d_proof)) { revert ("Invalid DSC Proof"); } - nullifiers[p_proof.pubSignals[PROVE_RSA_NULLIFIER_INDEX]] = true; - // Effects: Mint token address addr = address(uint160(p_proof.pubSignals[PROVE_RSA_USER_IDENTIFIER_INDEX])); uint256 newTokenId = totalSupply(); @@ -121,18 +87,14 @@ contract OneTimeSBT is ERC721Enumerable, Ownable { Attributes storage attributes = tokenAttributes[newTokenId]; - for (uint i = 0; i < attributePositions.length; i++) { - AttributePosition memory attribute = attributePositions[i]; - bytes memory attributeBytes = new bytes( - attribute.end - attribute.start + 1 - ); - for (uint j = attribute.start; j <= attribute.end; j++) { - attributeBytes[j - attribute.start] = charcodes[j]; - } - string memory attributeValue = string(attributeBytes); - attributes.values[i] = attributeValue; - // console.log(attribute.name, attributes.values[i]); - } + attributes.values[0] = AttributeLibrary.getIssuingState(charcodes); + attributes.values[1] = AttributeLibrary.getName(charcodes); + attributes.values[2] = AttributeLibrary.getPassportNumber(charcodes); + attributes.values[3] = AttributeLibrary.getNationality(charcodes); + attributes.values[4] = AttributeLibrary.getDateOfBirth(charcodes); + attributes.values[5] = AttributeLibrary.getGender(charcodes); + attributes.values[6] = AttributeLibrary.getExpiryDate(charcodes); + attributes.values[7] = AttributeLibrary.getOlderThan(charcodes); sbtExpiration[newTokenId] = block.timestamp + 90 days; } @@ -192,45 +154,45 @@ contract OneTimeSBT is ERC721Enumerable, Ownable { function getIssuingStateOf( uint256 _tokenId ) public view returns (string memory) { - return tokenAttributes[_tokenId].values[0]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_ISSUING_STATE_INDEX]; } function getNameOf(uint256 _tokenId) public view returns (string memory) { - return tokenAttributes[_tokenId].values[1]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_NAME_INDEX]; } function getPassportNumberOf( uint256 _tokenId ) public view returns (string memory) { - return tokenAttributes[_tokenId].values[2]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_PASSPORT_NUMBER_INDEX]; } function getNationalityOf( uint256 _tokenId ) public view returns (string memory) { - return tokenAttributes[_tokenId].values[3]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_NATIONALITY_INDEX]; } function getDateOfBirthOf( uint256 _tokenId ) public view returns (string memory) { - return tokenAttributes[_tokenId].values[4]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_DATE_OF_BIRTH_INDEX]; } function getGenderOf(uint256 _tokenId) public view returns (string memory) { - return tokenAttributes[_tokenId].values[5]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_GENDER_INDEX]; } function getExpiryDateOf( uint256 _tokenId ) public view returns (string memory) { - return tokenAttributes[_tokenId].values[6]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_EXPIRY_DATE_INDEX]; } function getOlderThanOf( uint256 _tokenId ) public view returns (string memory) { - return tokenAttributes[_tokenId].values[7]; + return tokenAttributes[_tokenId].values[ATTRIBUTE_OLDER_THAN_INDEX]; } // This is the function to check if the sbt is not expired or not diff --git a/contracts/contracts/constants/constants.sol b/contracts/contracts/constants/constants.sol index 6b86e9b8..915c802b 100644 --- a/contracts/contracts/constants/constants.sol +++ b/contracts/contracts/constants/constants.sol @@ -1,3 +1,12 @@ +uint256 constant ATTRIBUTE_ISSUING_STATE_INDEX = 0; +uint256 constant ATTRIBUTE_NAME_INDEX = 1; +uint256 constant ATTRIBUTE_PASSPORT_NUMBER_INDEX = 2; +uint256 constant ATTRIBUTE_NATIONALITY_INDEX = 3; +uint256 constant ATTRIBUTE_DATE_OF_BIRTH_INDEX = 4; +uint256 constant ATTRIBUTE_GENDER_INDEX = 5; +uint256 constant ATTRIBUTE_EXPIRY_DATE_INDEX = 6; +uint256 constant ATTRIBUTE_OLDER_THAN_INDEX = 7; + uint256 constant PROVE_RSA_NULLIFIER_INDEX = 0; uint256 constant PROVE_RSA_REVEALED_DATA_PACKED_INDEX = 1; uint256 constant PROVE_RSA_OLDER_THAN_INDEX = 4; diff --git a/contracts/contracts/libraries/AttributeLibrary.sol b/contracts/contracts/libraries/AttributeLibrary.sol new file mode 100644 index 00000000..c67666fd --- /dev/null +++ b/contracts/contracts/libraries/AttributeLibrary.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +library AttributeLibrary { + // Define constant start and end positions for each attribute + uint256 private constant ISSUING_STATE_START = 2; + uint256 private constant ISSUING_STATE_END = 4; + + uint256 private constant NAME_START = 5; + uint256 private constant NAME_END = 43; + + uint256 private constant PASSPORT_NUMBER_START = 44; + uint256 private constant PASSPORT_NUMBER_END = 52; + + uint256 private constant NATIONALITY_START = 54; + uint256 private constant NATIONALITY_END = 56; + + uint256 private constant DATE_OF_BIRTH_START = 57; + uint256 private constant DATE_OF_BIRTH_END = 62; + + uint256 private constant GENDER_START = 64; + uint256 private constant GENDER_END = 64; + + uint256 private constant EXPIRY_DATE_START = 65; + uint256 private constant EXPIRY_DATE_END = 70; + + uint256 private constant OLDER_THAN_START = 88; + uint256 private constant OLDER_THAN_END = 89; + + /** + * @notice Extracts the issuing state from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The issuing state as a string. + */ + function getIssuingState(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, ISSUING_STATE_START, ISSUING_STATE_END); + } + + /** + * @notice Extracts the name from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The name as a string. + */ + function getName(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, NAME_START, NAME_END); + } + + /** + * @notice Extracts the passport number from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The passport number as a string. + */ + function getPassportNumber(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, PASSPORT_NUMBER_START, PASSPORT_NUMBER_END); + } + + /** + * @notice Extracts the nationality from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The nationality as a string. + */ + function getNationality(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, NATIONALITY_START, NATIONALITY_END); + } + + /** + * @notice Extracts the date of birth from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The date of birth as a string. + */ + function getDateOfBirth(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, DATE_OF_BIRTH_START, DATE_OF_BIRTH_END); + } + + /** + * @notice Extracts the gender from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The gender as a string. + */ + function getGender(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, GENDER_START, GENDER_END); + } + + /** + * @notice Extracts the expiry date from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The expiry date as a string. + */ + function getExpiryDate(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, EXPIRY_DATE_START, EXPIRY_DATE_END); + } + + /** + * @notice Extracts the "older than" attribute from the charcodes. + * @param charcodes The bytes array containing packed attribute data. + * @return The "older than" value as a string. + */ + function getOlderThan(bytes memory charcodes) internal pure returns (string memory) { + return extractAttribute(charcodes, OLDER_THAN_START, OLDER_THAN_END); + } + + /** + * @notice Extracts a substring from the charcodes based on start and end indices. + * @param charcodes The bytes array containing packed attribute data. + * @param start The starting index (inclusive). + * @param end The ending index (inclusive). + * @return The extracted substring as a string. + */ + function extractAttribute(bytes memory charcodes, uint256 start, uint256 end) internal pure returns (string memory) { + require(charcodes.length > end, "AttributeLibrary: charcodes length insufficient"); + bytes memory attributeBytes = new bytes(end - start + 1); + for (uint256 i = start; i <= end; i++) { + attributeBytes[i - start] = charcodes[i]; + } + return string(attributeBytes); + } +} \ No newline at end of file diff --git a/contracts/contracts/mocks/VerifiersManagerMock.sol b/contracts/contracts/mocks/VerifiersManagerMock.sol new file mode 100644 index 00000000..23c17d69 --- /dev/null +++ b/contracts/contracts/mocks/VerifiersManagerMock.sol @@ -0,0 +1,30 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import {IVerifiersManager, IProveVerifier, IDscVerifier} from "../interfaces/IVerifiersManager.sol"; + +contract VerifiersManagerMock is IVerifiersManager { + + constructor() {} + + function verifyWithProveVerifier( + uint256 verifier_id, + RSAProveCircuitProof memory proof + ) public view returns (bool) { + if (verifier_id == 1) { + return false; + } + return true; + } + + function verifyWithDscVerifier( + uint256 verifier_id, + DscCircuitProof memory proof + ) public view returns (bool) { + if (verifier_id == 1) { + return false; + } + return true; + } + +} \ No newline at end of file diff --git a/contracts/test/OneTimeSBT.ts b/contracts/test/OneTimeSBT.ts new file mode 100644 index 00000000..9b555c46 --- /dev/null +++ b/contracts/test/OneTimeSBT.ts @@ -0,0 +1,60 @@ +import { ethers } from "hardhat"; +import { expect, assert } from "chai"; +import { BigNumberish, Block, dataLength } from "ethers"; +import { genMockPassportData } from "../../common/src/utils/genMockPassportData"; +import { + generateCircuitInputsProve, + generateCircuitInputsDisclose +} from "../../common/src/utils/generateInputs"; +import { getCSCAModulusMerkleTree } from "../../common/src/utils/csca"; +import { formatRoot } from "../../common/src/utils/utils"; +import { IMT } from "../../common/node_modules/@zk-kit/imt"; + +describe("Unit test for OneTimeSBT.sol", function() { + + let verifiersManager: any; + let formatter: any; + let oneTimeSBT: any; + + let owner: any; + let addr1: any; + let addr2: any; + + before(async function() { + [owner, addr1, addr2] = await ethers.getSigners(); + + const verifiersManagerFactory = await ethers.getContractFactory("VerifiersManagerMock"); + verifiersManager = await verifiersManagerFactory.deploy(); + await verifiersManager.waitForDeployment(); + console.log('\x1b[34m%s\x1b[0m', `Verifier_disclose deployed to ${verifiersManager.target}`); + + const formatterFactory = await ethers.getContractFactory("Formatter"); + formatter = await formatterFactory.deploy(); + await formatter.waitForDeployment(); + console.log('\x1b[34m%s\x1b[0m', `formatter deployed to ${formatter.target}`); + + const sbtFactory = await ethers.getContractFactory("OneTimeSBT"); + oneTimeSBT = await sbtFactory.deploy( + verifiersManager, + formatter + ); + await oneTimeSBT.waitForDeployment(); + console.log('\x1b[34m%s\x1b[0m', `sbt deployed to ${oneTimeSBT.target}`); + }); + + describe("Test mint function", async function () { + + }); + + describe("Test util functions", async function () { + + describe("Test fieldElementsToBytes function", async function () { + + }); + + describe("Test sliceFirstThree function", async function () { + + }); + + }) +}); \ No newline at end of file From bb0054f50d97fff2003b940c2873f6fa854fd8b4 Mon Sep 17 00:00:00 2001 From: motemotech Date: Tue, 8 Oct 2024 01:40:52 +0900 Subject: [PATCH 3/3] update require to custom errors --- contracts/contracts/OneTimeSBT.sol | 33 +++++++++++-------- .../contracts/libraries/AttributeLibrary.sol | 7 +++- contracts/test/OneTimeSBT.ts | 6 ++++ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/contracts/contracts/OneTimeSBT.sol b/contracts/contracts/OneTimeSBT.sol index 60f901b6..9b30a03f 100644 --- a/contracts/contracts/OneTimeSBT.sol +++ b/contracts/contracts/OneTimeSBT.sol @@ -7,7 +7,7 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import {IVerifiersManager} from "./interfaces/IVerifiersManager.sol"; import {Base64} from "./libraries/Base64.sol"; import {Formatter} from "./Formatter.sol"; -import "./constants/constants.sol"; +import "./constants/Constants.sol"; import "./libraries/AttributeLibrary.sol"; import "hardhat/console.sol"; @@ -23,9 +23,14 @@ contract OneTimeSBT is ERC721Enumerable { struct Attributes { string[8] values; } - mapping(uint256 => Attributes) private tokenAttributes; + error CURRENT_DATE_NOT_IN_VALID_RANGE(); + error UNEQUAL_BLINDED_DSC_COMMITMENT(); + error INVALID_PROVE_PROOF(); + error INVALID_DSC_PROOF(); + error SBT_CAN_BE_TRANSFERED(); + constructor( IVerifiersManager v, Formatter f @@ -49,26 +54,27 @@ contract OneTimeSBT is ERC721Enumerable { uint currentTimestamp = getCurrentTimestamp(dateNum); // Check that the current date is within a +/- 1 day range - require( - currentTimestamp >= block.timestamp - 1 days && - currentTimestamp <= block.timestamp + 1 days, - "Current date is not within the valid range" - ); + if( + currentTimestamp < block.timestamp - 1 days || + currentTimestamp > block.timestamp + 1 days + ) { + revert CURRENT_DATE_NOT_VALID_RANGE(); + }; // check blinded dcs if ( keccak256(abi.encodePacked(p_proof.pubSignals[PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX])) != keccak256(abi.encodePacked(d_proof.pubSignals[DSC_BLINDED_DSC_COMMITMENT_INDEX])) ) { - revert ("Blinded DSC commitments are not equal"); + revert UNEQUAL_BLINDED_DSC_COMMITMENT(); } if (!verifiersManager.verifyWithProveVerifier(prove_verifier_id, p_proof)) { - revert ("Invalid Prove Proof"); + revert INVALID_PROVE_PROOF(); } if (!verifiersManager.verifyWithDscVerifier(dsc_verifier_id, d_proof)) { - revert ("Invalid DSC Proof"); + revert INVALID_DSC_PROOF(); } // Effects: Mint token @@ -136,10 +142,9 @@ contract OneTimeSBT is ERC721Enumerable { uint256 batchSize ) internal virtual override { super._beforeTokenTransfer(from, to, tokenId, batchSize); - require( - from == address(0), - "Cannot transfer - SBT is soulbound" - ); + if (from != address(0)) { + revert SBT_CAN_BE_TRANSFERED(); + } } function isExpired(string memory date) public view returns (bool) { diff --git a/contracts/contracts/libraries/AttributeLibrary.sol b/contracts/contracts/libraries/AttributeLibrary.sol index c67666fd..11233781 100644 --- a/contracts/contracts/libraries/AttributeLibrary.sol +++ b/contracts/contracts/libraries/AttributeLibrary.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.18; library AttributeLibrary { + + error INSUFFICIENT_CHARCODE_LEN(); + // Define constant start and end positions for each attribute uint256 private constant ISSUING_STATE_START = 2; uint256 private constant ISSUING_STATE_END = 4; @@ -107,7 +110,9 @@ library AttributeLibrary { * @return The extracted substring as a string. */ function extractAttribute(bytes memory charcodes, uint256 start, uint256 end) internal pure returns (string memory) { - require(charcodes.length > end, "AttributeLibrary: charcodes length insufficient"); + if (charcodes.length <= end) { + revert INSUFFICIENT_CHARCODE_LEN(); + } bytes memory attributeBytes = new bytes(end - start + 1); for (uint256 i = start; i <= end; i++) { attributeBytes[i - start] = charcodes[i]; diff --git a/contracts/test/OneTimeSBT.ts b/contracts/test/OneTimeSBT.ts index 9b555c46..31d7d545 100644 --- a/contracts/test/OneTimeSBT.ts +++ b/contracts/test/OneTimeSBT.ts @@ -56,5 +56,11 @@ describe("Unit test for OneTimeSBT.sol", function() { }); + describe("") + + }); + + describe("Test attrs are correctly registerd", async function () { + }) }); \ No newline at end of file