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 new file mode 100644 index 00000000..9b30a03f --- /dev/null +++ b/contracts/contracts/OneTimeSBT.sol @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +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 "./libraries/AttributeLibrary.sol"; +import "hardhat/console.sol"; + +contract OneTimeSBT is ERC721Enumerable { + using Strings for uint256; + using Base64 for *; + + IVerifiersManager public verifiersManager; + Formatter public formatter; + + mapping(uint256 => uint256) public sbtExpiration; + + 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 + ) ERC721("OpenPassport", "OpenPassport") { + verifiersManager = v; + formatter = f; + } + + 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 + 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 UNEQUAL_BLINDED_DSC_COMMITMENT(); + } + + if (!verifiersManager.verifyWithProveVerifier(prove_verifier_id, p_proof)) { + revert INVALID_PROVE_PROOF(); + } + + if (!verifiersManager.verifyWithDscVerifier(dsc_verifier_id, d_proof)) { + revert INVALID_DSC_PROOF(); + } + + // 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]; + + 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; + } + + 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); + if (from != address(0)) { + revert SBT_CAN_BE_TRANSFERED(); + } + } + + 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[ATTRIBUTE_ISSUING_STATE_INDEX]; + } + + function getNameOf(uint256 _tokenId) public view returns (string memory) { + return tokenAttributes[_tokenId].values[ATTRIBUTE_NAME_INDEX]; + } + + function getPassportNumberOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[ATTRIBUTE_PASSPORT_NUMBER_INDEX]; + } + + function getNationalityOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[ATTRIBUTE_NATIONALITY_INDEX]; + } + + function getDateOfBirthOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[ATTRIBUTE_DATE_OF_BIRTH_INDEX]; + } + + function getGenderOf(uint256 _tokenId) public view returns (string memory) { + return tokenAttributes[_tokenId].values[ATTRIBUTE_GENDER_INDEX]; + } + + function getExpiryDateOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[ATTRIBUTE_EXPIRY_DATE_INDEX]; + } + + function getOlderThanOf( + uint256 _tokenId + ) public view returns (string memory) { + return tokenAttributes[_tokenId].values[ATTRIBUTE_OLDER_THAN_INDEX]; + } + + // 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..915c802b --- /dev/null +++ b/contracts/contracts/constants/constants.sol @@ -0,0 +1,22 @@ +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; +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/libraries/AttributeLibrary.sol b/contracts/contracts/libraries/AttributeLibrary.sol new file mode 100644 index 00000000..11233781 --- /dev/null +++ b/contracts/contracts/libraries/AttributeLibrary.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +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; + + 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) { + 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]; + } + 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/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 diff --git a/contracts/test/OneTimeSBT.ts b/contracts/test/OneTimeSBT.ts new file mode 100644 index 00000000..31d7d545 --- /dev/null +++ b/contracts/test/OneTimeSBT.ts @@ -0,0 +1,66 @@ +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 () { + + }); + + describe("") + + }); + + describe("Test attrs are correctly registerd", async function () { + + }) +}); \ No newline at end of file