# Silent impact contract UnirepSocial.sol ```javascript // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; import '@openzeppelin/contracts/utils/Address.sol'; import {EpochKeyLiteVerifierHelper} from '@unirep/contracts/verifierHelpers/EpochKeyLiteVerifierHelper.sol'; import {ReputationVerifierHelper} from '@unirep/contracts/verifierHelpers/ReputationVerifierHelper.sol'; import {Unirep} from '@unirep/contracts/Unirep.sol'; import {IUnirep} from './interfaces/IUnirep.sol'; interface IVerifier { /** * @return bool Whether the proof is valid given the hardcoded verifying key * above and the public inputs */ function verifyProof( uint256[17] calldata publicSignals, uint256[8] calldata proof ) external view returns (bool); } contract UnirepSocial is IUnirep{ Unirep public unirep; IVerifier internal actionVerifier; EpochKeyLiteVerifierHelper public epkHelper; ReputationVerifierHelper public repHelper; // Before Unirep integrates with InterRep // We use an admin to controll user sign up address internal admin; // Unirep social's attester ID uint160 public immutable attesterId; // The amount of karma required to publish a post uint256 public immutable postReputation; // The amount of karma required to submit a comment uint256 public immutable commentReputation; // The amount of karma airdropped to user when user signs up and executes user state transition // TODO: init reputation // uint256 immutable public airdroppedReputation; // The epoch length of Unirep Social uint256 public immutable epochLength; // The most reputation can be spent in a vote uint256 public immutable maxReputationBudget; // Positive Reputation field index in Unirep protocol uint256 public immutable posRepFieldIndex = 0; // Nagative Reputation field index in Unirep protocol uint256 public immutable negRepFieldIndex = 1; // Graffiti field index in Unirep protocol uint256 public immutable graffitiFieldIndex; // A mapping between user’s epoch key and if they request airdrop in the current epoch; // One epoch key is allowed to get airdrop once an epoch mapping(uint256 => bool) public isEpochKeyGotAirdrop; // A mapping between username and if they're already claimed; mapping(uint256 => bool) public usernames; // epoch number to epoch key to amount spent mapping(uint256 => mapping(uint256 => uint256)) public subsidies; // proof nullifier mapping(bytes32 => bool) public usedProofNullifier; // reputation nullifier mapping(uint256 => bool) public usedRepNullifier; //stateTreeRoot to claimed ETH mapping(uint256 => uint256) claimed; // Attester id == address mapping(uint160 => AttesterData) attesters; uint256 public immutable subsidy; // assign posts/comments with an id uint256 public contentId = 1; // post/comment id => hashed content => epoch key mapping(uint256 => mapping(bytes32 => uint256)) public hashedContentMapping; // help Unirep Social track event event UserSignedUp( uint256 indexed epoch, uint256 indexed identityCommitment ); event AirdropSubmitted(uint256 indexed epoch, uint256 indexed epochKey); event PostSubmitted( uint256 indexed epoch, uint256 indexed postId, uint256 indexed epochKey, bytes32 contentHash, uint256 minRep ); event CommentSubmitted( uint256 indexed epoch, uint256 indexed postId, uint256 indexed epochKey, uint256 commentId, bytes32 contentHash, uint256 minRep ); event ContentUpdated( uint256 indexed id, bytes32 oldContentHash, bytes32 newContentHash ); event VoteSubmitted( uint256 indexed epoch, uint256 indexed fromEpochKey, uint256 indexed toEpochKey, uint256 upvoteValue, uint256 downvoteValue, uint256 minRep ); struct ActionSignals { uint256 epochKey; uint256 stateTreeRoot; uint256 graffiti; bool proveGraffiti; uint160 attesterId; uint8 nonce; uint48 epoch; bool revealNonce; bool proveMinRep; bool proveMaxRep; bool proveZeroRep; uint256 minRep; uint256 maxRep; uint256 notEpochKey; uint256 data; uint256[] nullifiers; } constructor( Unirep _unirepContract, IVerifier _actionVerifier, EpochKeyLiteVerifierHelper _epkHelper, ReputationVerifierHelper _repHelper, uint256 _postReputation, uint256 _commentReputation, // uint256 _airdroppedReputation, uint256 _subsidy, uint48 _epochLength, uint256 _maxReputationBudget ) { // Set the unirep contracts unirep = _unirepContract; actionVerifier = _actionVerifier; epkHelper = _epkHelper; repHelper = _repHelper; // Set admin user admin = msg.sender; // signup Unirep Social contract as an attester in Unirep contract unirep.attesterSignUp(_epochLength); attesterId = uint160(address(this)); postReputation = _postReputation; commentReputation = _commentReputation; // airdroppedReputation = _airdroppedReputation; subsidy = _subsidy; epochLength = _epochLength; maxReputationBudget = _maxReputationBudget; graffitiFieldIndex = unirep.sumFieldCount(); } /* * Call Unirep contract to perform user signing up if user hasn't signed up in Unirep */ function userSignUp( uint256[] memory publicSignals, uint256[8] memory proof ) public{ require( msg.sender == admin, 'Unirep Social: sign up should through an admin' ); unirep.userSignUp(publicSignals, proof); emit UserSignedUp( unirep.attesterCurrentEpoch(attesterId), publicSignals[0] ); } // /* // * Try to spend subsidy for an epoch key in an epoch // * @param epoch The epoch the subsidy belongs to // * @param epochKey The epoch key that receives the subsidy // * @param amount The amount requesting to be spent // */ // function trySpendSubsidy( // uint256 epoch, // uint256 subsidyKey, // uint256 amount // ) private { // uint256 spentSubsidy = subsidies[epoch][subsidyKey]; // require( // spentSubsidy <= subsidy, // 'Unirep Social: invalid subsidy value' // ); // uint256 remainingSubsidy = subsidy - spentSubsidy; // require( // amount <= remainingSubsidy, // 'Unirep Social: requesting too much subsidy' // ); // subsidies[epoch][subsidyKey] += amount; // } function _checkProofNullifier(bytes32 nullifier) internal { require( !usedProofNullifier[nullifier], 'Unirep Social: the proof is submitted before' ); usedProofNullifier[nullifier] = true; } function verifyActionProof( uint256[17] memory publicSignals, uint256[8] memory proof ) public view returns (bool) { ActionSignals memory signals = decodeActionSignals(publicSignals); require( unirep.attesterStateTreeRootExists( attesterId, uint48(signals.epoch), signals.stateTreeRoot ), 'Unirep Social: GST root does not exist in epoch' ); require( signals.attesterId == attesterId, 'Unirep Social: attesterId mismatches' ); require( signals.epoch == unirep.attesterCurrentEpoch(attesterId), 'Unirep Social: epoch mismatches' ); return actionVerifier.verifyProof(publicSignals, proof); } function decodeActionSignals( uint256[17] memory publicSignals ) public view returns (ActionSignals memory) { ActionSignals memory signals; signals.epochKey = publicSignals[0]; signals.stateTreeRoot = publicSignals[1]; ( signals.revealNonce, signals.attesterId, signals.epoch, signals.nonce ) = epkHelper.decodeEpochKeyControl(publicSignals[2]); ( signals.minRep, signals.maxRep, signals.proveMinRep, signals.proveMaxRep, signals.proveZeroRep, signals.proveGraffiti ) = repHelper.decodeReputationControl(publicSignals[3]); uint256[] memory nullifiers = new uint[](maxReputationBudget); for (uint256 i = 0; i < maxReputationBudget; i++) { nullifiers[i] = publicSignals[4 + i]; } signals.nullifiers = nullifiers; signals.notEpochKey = publicSignals[maxReputationBudget + 4]; signals.graffiti = publicSignals[maxReputationBudget + 5]; signals.data = publicSignals[maxReputationBudget + 6]; return signals; } // /** // * Accepts an action proof // * publicSignals[0] - epoch key // * publicSignals[1] - state tree root // * publicSignals[2] - control0 (epoch key control) // * publicSignals[3] - control1 (reputation control) // * publicSignals[4,14] - reputation nullifiers // * publicSignals[14] - not epoch key // * publicSignals[15] - graffiti // * publicSignals[16] - data // **/ // function getSubsidyAirdrop( // uint256[17] memory publicSignals, // uint256[8] memory proof // ) public payable { // // check if proof is submitted before // bytes32 proofNullifier = keccak256( // abi.encodePacked(publicSignals, proof) // ); // _checkProofNullifier(proofNullifier); // // verify the proof // require( // verifyActionProof(publicSignals, proof), // 'Unirep Social: proof is invalid' // ); // ActionSignals memory signals = decodeActionSignals(publicSignals); // require( // signals.proveMaxRep, // 'Unirep Social: should prove max reputation' // ); // require( // signals.revealNonce && (signals.nonce == 0), // 'Unirep Social: epoch key nonce is not valid' // ); // uint requestedSubsidy = signals.maxRep; // the amount proved // uint receivedSubsidy = subsidy < requestedSubsidy // ? subsidy // : requestedSubsidy; // uint epochKey = signals.epochKey; // uint48 epoch = signals.epoch; // trySpendSubsidy(epoch, epochKey, receivedSubsidy); // subsidies[epoch][epochKey] = subsidy; // don't allow a user to double request or spend more // // Submit attestation to receiver's first epoch key // unirep.attest(epochKey, epoch, posRepFieldIndex, receivedSubsidy); // } // /** // * Accepts an action proof // * publicSignals[0] - epoch key // * publicSignals[1] - state tree root // * publicSignals[2] - control0 (epoch key control) // * publicSignals[3] - control1 (reputation control) // * publicSignals[4,14] - reputation nullifiers // * publicSignals[14] - not epoch key // * publicSignals[15] - graffiti preimage // * publicSignals[16] - data // **/ // function publishPostSubsidy( // bytes32 contentHash, // uint256[17] memory publicSignals, // uint256[8] memory proof // ) external payable { // // check if proof is submitted before // bytes32 proofNullifier = keccak256( // abi.encodePacked(publicSignals, proof) // ); // _checkProofNullifier(proofNullifier); // ActionSignals memory signals = decodeActionSignals(publicSignals); // require( // signals.revealNonce && (signals.nonce == 0), // 'Unirep Social: epoch key nonce is not valid' // ); // require( // verifyActionProof(publicSignals, proof), // 'Unirep Social: proof is invalid' // ); // uint epoch = signals.epoch; // uint epochKey = signals.epochKey; // trySpendSubsidy(epoch, epochKey, postReputation); // // saved post id and hashed content // uint256 postId = contentId; // hashedContentMapping[postId][contentHash] = epochKey; // emit PostSubmitted( // epoch, // postId, // epochKey, // contentHash, // signals.proveMinRep ? signals.minRep : 0 // min rep // ); // // update content Id // contentId++; // } // function publishCommentSubsidy( // uint256 postId, // bytes32 contentHash, // uint256[17] memory publicSignals, // uint256[8] memory proof // ) external payable { // // check if proof is submitted before // bytes32 proofNullifier = keccak256( // abi.encodePacked(publicSignals, proof) // ); // _checkProofNullifier(proofNullifier); // ActionSignals memory signals = decodeActionSignals(publicSignals); // require( // signals.revealNonce && (signals.nonce == 0), // 'Unirep Social: epoch key nonce is not valid' // ); // require( // verifyActionProof(publicSignals, proof), // 'Unirep Social: proof is invalid' // ); // require(postId < contentId, 'Unirep Social: post ID is invalid'); // uint epoch = signals.epoch; // uint epochKey = signals.epochKey; // trySpendSubsidy(epoch, epochKey, commentReputation); // // saved post id and hashed content // uint256 commentId = contentId; // hashedContentMapping[commentId][contentHash] = epochKey; // emit CommentSubmitted( // epoch, // postId, // epochKey, // commentId, // contentHash, // signals.proveMinRep ? signals.minRep : 0 // min rep // ); // // update content Id // contentId++; // } // function voteSubsidy( // uint256 upvoteValue, // uint256 downvoteValue, // uint256 toEpochKey, // uint256[17] memory publicSignals, // uint256[8] memory proof // ) external payable { // // check if proof is submitted before // bytes32 proofNullifier = keccak256( // abi.encodePacked(publicSignals, proof) // ); // _checkProofNullifier(proofNullifier); // ActionSignals memory signals = decodeActionSignals(publicSignals); // require( // signals.revealNonce && (signals.nonce == 0), // 'Unirep Social: epoch key nonce is not valid' // ); // require( // verifyActionProof(publicSignals, proof), // 'Unirep Social: proof is invalid' // ); // uint256 voteValue = upvoteValue + downvoteValue; // require( // voteValue > 0, // 'Unirep Social: should submit a positive vote value' // ); // require( // upvoteValue * downvoteValue == 0, // 'Unirep Social: should only choose to upvote or to downvote' // ); // require( // signals.notEpochKey == toEpochKey, // 'Unirep Social: must prove non-ownership of epk' // ); // uint48 epoch = signals.epoch; // uint epochKey = signals.epochKey; // trySpendSubsidy(epoch, epochKey, voteValue); // // Submit attestation to receiver's epoch key // if (upvoteValue > 0) { // unirep.attest( // toEpochKey, // epoch, // posRepFieldIndex, // field index: posRep // upvoteValue // ); // } else { // unirep.attest( // toEpochKey, // epoch, // negRepFieldIndex, // field index: negRep // downvoteValue // ); // } // emit VoteSubmitted( // epoch, // epochKey, // from epoch key // toEpochKey, // upvoteValue, // downvoteValue, // signals.proveMinRep ? signals.minRep : 0 // min rep // ); // } /* * Publish a post on chain with a reputation proof to prove that the user has enough karma to spend * @param contentHash The hashed content of the post * @param _proofRelated The reputation proof that the user proves that he has enough karma to post */ function publishPost( bytes32 contentHash, uint256[17] memory publicSignals, uint256[8] memory proof ) external payable { // check if proof is submitted before bytes32 proofNullifier = keccak256( abi.encodePacked(publicSignals, proof) ); _checkProofNullifier(proofNullifier); ActionSignals memory signals = decodeActionSignals(publicSignals); uint256 epochKey = signals.epochKey; require( verifyActionProof(publicSignals, proof), 'Unirep Social: proof is invalid' ); // Spend reputation uint48 epoch = signals.epoch; for (uint256 i = 0; i < postReputation; i++) { uint nullifier = signals.nullifiers[i]; require( !usedRepNullifier[nullifier] && nullifier > 0, 'Unirep Social: invalid rep nullifier' ); usedRepNullifier[nullifier] = true; } // unirep.attest( // epochKey, // epoch, // negRepFieldIndex, // field index: posRep // postReputation // ); // saved post id and hashed content uint256 postId = contentId; hashedContentMapping[postId][contentHash] = epochKey; emit PostSubmitted( epoch, postId, epochKey, contentHash, signals.proveMinRep ? signals.minRep : 0 // min rep ); // update content Id contentId++; } /* * Leave a comment on chain with a reputation proof to prove that the user has enough karma to spend * @param postId The transaction hash of the post * @param contentHash The hashed content of the post * @param _proofRelated The reputation proof that the user proves that he has enough karma to comment */ // function leaveComment( // uint256 postId, // bytes32 contentHash, // uint256[17] memory publicSignals, // uint256[8] memory proof // ) external payable { // // check if proof is submitted before // bytes32 proofNullifier = keccak256( // abi.encodePacked(publicSignals, proof) // ); // _checkProofNullifier(proofNullifier); // ActionSignals memory signals = decodeActionSignals(publicSignals); // uint256 epochKey = signals.epochKey; // require( // verifyActionProof(publicSignals, proof), // 'Unirep Social: proof is invalid' // ); // require(postId < contentId, 'Unirep Social: post ID is invalid'); // // Spend reputation // uint48 epoch = signals.epoch; // for (uint256 i = 0; i < commentReputation; i++) { // uint nullifier = signals.nullifiers[i]; // require( // !usedRepNullifier[nullifier] && nullifier > 0, // 'Unirep Social: invalid rep nullifier' // ); // usedRepNullifier[nullifier] = true; // } // unirep.attest( // epochKey, // epoch, // negRepFieldIndex, // field index: posRep // commentReputation // ); // // saved post id and hashed content // uint256 commentId = contentId; // hashedContentMapping[commentId][contentHash] = epochKey; // emit CommentSubmitted( // epoch, // postId, // epochKey, // commentId, // contentHash, // signals.proveMinRep ? signals.minRep : 0 // ); // // update comment Id // contentId++; // } /* * Update a published post/comment content * @param id The post ID or the comment ID * @param oldContentHash The old hashed content of the post/comment * @param newContentHash The new hashed content of the post/comment * @param publicSignals The public signals of the epoch key proof of the author of the post/comment * @param proof The epoch key proof of the author of the post/comment */ function edit( uint256 id, bytes32 oldContentHash, bytes32 newContentHash, uint256[] memory publicSignals, uint256[8] memory proof ) external payable { // check if proof is submitted before bytes32 proofNullifier = keccak256( abi.encodePacked(publicSignals, proof) ); _checkProofNullifier(proofNullifier); EpochKeyLiteVerifierHelper.EpochKeySignals memory signals = epkHelper .decodeEpochKeyLiteSignals(publicSignals); epkHelper.verifyAndCheck(publicSignals, proof); require(id < contentId, 'Unirep Social: content ID is invalid'); require( hashedContentMapping[id][oldContentHash] == signals.epochKey, 'Unirep Social: Mismatched epoch key proof to the post or the comment id' ); hashedContentMapping[id][oldContentHash] = 0; hashedContentMapping[id][newContentHash] = signals.epochKey; emit ContentUpdated(id, oldContentHash, newContentHash); } /* * Vote an epoch key with a reputation proof to prove that the user has enough karma to spend * @param upvoteValue How much the user wants to upvote the epoch key receiver * @param downvoteValue How much the user wants to downvote the epoch key receiver * @param toEpochKey The vote receiver * @param toEPochKeyProofIndex the proof index of the epoch key on unirep * @param _proofRelated The reputation proof that the user proves that he has enough karma to vote */ function vote( uint256 voteValue, uint256 toEpochKey, uint256[17] memory publicSignals, uint256[8] memory proof ) external payable { // check if proof is submitted before bytes32 proofNullifier = keccak256( abi.encodePacked(publicSignals, proof) ); _checkProofNullifier(proofNullifier); require( voteValue > 0, 'Unirep Social: should submit a positive vote value' ); ActionSignals memory signals = decodeActionSignals(publicSignals); require( verifyActionProof(publicSignals, proof), 'Unirep Social: proof is invalid' ); // Spend reputation uint48 epoch = signals.epoch; for (uint256 i = 0; i < voteValue; i++) { uint nullifier = signals.nullifiers[i]; require( !usedRepNullifier[nullifier] && nullifier > 0, 'Unirep Social: invalid rep nullifier' ); usedRepNullifier[nullifier] = true; } unirep.attest( signals.epochKey, epoch, negRepFieldIndex, // field index: posRep voteValue ); // Submit attestation to receiver's epoch key // Submit attestation to receiver's epoch key unirep.attest( toEpochKey, epoch, posRepFieldIndex, // field index: posRep voteValue ); emit VoteSubmitted( epoch, signals.epochKey, // from epoch key toEpochKey, voteValue, 0, signals.proveMinRep ? signals.minRep : 0 ); } /* * Set new user name for an epochKey * @param epochKey epoch key that attempts to set a new uername * @param oldUsername oldusername that the eppch key previously claimed * @param newUsername requested new user name */ function setUsername( uint256 epochKey, uint256 oldUsername, uint256 newUsername ) external payable { // check if the new username is not taken require( usernames[newUsername] == false, 'Unirep Social: This username is already taken' ); // only admin can call this function require( msg.sender == admin, 'Unirep Social: Only admin can send transactions to this contract' ); usernames[oldUsername] = false; usernames[newUsername] = true; uint48 epoch = unirep.attesterCurrentEpoch(attesterId); // attest to the epoch key to give the key the username unirep.attest( epochKey, epoch, graffitiFieldIndex, // field index: graffiti newUsername ); } function claimPrize( address receiver, uint256 claimEpochKey, uint256 claimValue, uint256[17] memory publicSignals, uint256[8] calldata proof ) public payable{ require(unirep.attesterCurrentEpoch(attesterId) > 0); require(verifyActionProof(publicSignals, proof)); uint48 epoch = 0; // the voting epoch uint256 stateTreeRoot = publicSignals[0]; unirep.attesterStateTreeRootExists(attesterId, epoch, stateTreeRoot); EpochKeyData storage epkData = attesters[uint160(msg.sender)].epkData[ claimEpochKey ]; uint256 claimableETH = epkData.data[posRepFieldIndex]; uint256 remainedClaimableETH = claimableETH - claimed[publicSignals[1]]; require(remainedClaimableETH >= claimValue, "Not enough"); claimed[publicSignals[1]] += remainedClaimableETH; (bool success, ) = receiver.call{value: claimValue}(""); require( success, "Unable to send value, recipient may have reverted" ); } } ``` IUnirep.sol ```javascript= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IncrementalBinaryTree, IncrementalTreeData} from '@zk-kit/incremental-merkle-tree.sol/IncrementalBinaryTree.sol'; import {ReusableMerkleTree, ReusableTreeData} from '../libraries/ReusableMerkleTree.sol'; import {LazyMerkleTree, LazyTreeData} from '../libraries/LazyMerkleTree.sol'; /// @title IUnirep /// @dev https://developer.unirep.io/docs/contracts-api/iunirep-sol interface IUnirep { event AttesterSignedUp( uint160 indexed attesterId, uint48 epochLength, uint48 timestamp ); event UserSignedUp( uint48 indexed epoch, uint256 indexed identityCommitment, uint160 indexed attesterId, uint256 leafIndex ); event UserStateTransitioned( uint48 indexed epoch, uint160 indexed attesterId, uint256 indexed leafIndex, uint256 hashedLeaf, uint256 nullifier ); event Attestation( uint48 indexed epoch, uint256 indexed epochKey, uint160 indexed attesterId, uint256 fieldIndex, uint256 change ); event StateTreeLeaf( uint48 indexed epoch, uint160 indexed attesterId, uint256 indexed index, uint256 leaf ); event EpochTreeLeaf( uint48 indexed epoch, uint160 indexed attesterId, uint256 indexed index, uint256 leaf ); event HistoryTreeLeaf(uint160 indexed attesterId, uint256 leaf); event EpochEnded(uint48 indexed epoch, uint160 indexed attesterId); // error error UserAlreadySignedUp(uint256 identityCommitment); error AttesterAlreadySignUp(uint160 attester); error AttesterNotSignUp(uint160 attester); error AttesterInvalid(); error NullifierAlreadyUsed(uint256 nullilier); error AttesterIdNotMatch(uint160 attesterId); error OutOfRange(); error InvalidField(); error EpochKeyNotProcessed(); error InvalidSignature(); error InvalidEpochKey(); error EpochNotMatch(); error InvalidEpoch(uint256 epoch); error ChainIdNotMatch(uint48 chainId); error InvalidProof(); error InvalidHistoryTreeRoot(uint256 historyTreeRoot); struct SignupSignals { uint48 epoch; uint48 chainId; uint160 attesterId; uint256 stateTreeLeaf; uint256 identityCommitment; } struct UserStateTransitionSignals { uint256 historyTreeRoot; uint256 stateTreeLeaf; uint48 toEpoch; uint160 attesterId; uint256[] epochKeys; } struct EpochKeyData { uint40 leafIndex; uint48 epoch; uint256 leaf; // use a constant because compile time variables are not supported uint256[128] data; } struct AttesterData { uint48 startTimestamp; uint48 currentEpoch; uint48 epochLength; // epoch keyed to root keyed to whether it's valid mapping(uint256 => mapping(uint256 => bool)) stateTreeRoots; mapping(uint256 => bool) historyTreeRoots; // epoch keyed to root mapping(uint256 => uint256) epochTreeRoots; mapping(uint256 => bool) identityCommitments; IncrementalTreeData semaphoreGroup; // epoch key management mapping(uint256 => EpochKeyData) epkData; IncrementalTreeData historyTree; ReusableTreeData stateTree; LazyTreeData epochTree; } struct Config { // circuit config uint8 stateTreeDepth; uint8 epochTreeDepth; uint8 historyTreeDepth; uint8 fieldCount; uint8 sumFieldCount; uint8 numEpochKeyNoncePerEpoch; uint8 replNonceBits; uint8 replFieldBits; } } ```