Skip to content

Instantly share code, notes, and snippets.

@AE-0h
Created June 7, 2024 23:46
Show Gist options
  • Save AE-0h/9c2e163d85173627f768ca63d5a02db5 to your computer and use it in GitHub Desktop.
Save AE-0h/9c2e163d85173627f768ca63d5a02db5 to your computer and use it in GitHub Desktop.
IFriendtechSharesV1 by private_ft. Find it at https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
pragma solidity ^0.8.0;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
library PoseidonT3 {
function poseidon(uint256[2] memory) public pure returns (uint256) {}
}
library PoseidonT6 {
function poseidon(uint256[5] memory) public pure returns (uint256) {}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import {PoseidonT3} from "./Hashes.sol";
// Each incremental tree has certain properties and data that will
// be used to add new leaves.
struct IncrementalTreeData {
uint256 depth; // Depth of the tree (levels - 1).
uint256 root; // Root hash of the tree.
uint256 numberOfLeaves; // Number of leaves of the tree.
mapping(uint256 => uint256) zeroes; // Zero hashes used for empty nodes (level -> zero hash).
// The nodes of the subtrees used in the last addition of a leaf (level -> [left node, right node]).
mapping(uint256 => uint256[2]) lastSubtrees; // Caching these values is essential to efficient appends.
}
/// @title Incremental binary Merkle tree.
/// @dev The incremental tree allows to calculate the root hash each time a leaf is added, ensuring
/// the integrity of the tree.
library IncrementalBinaryTree {
uint8 internal constant MAX_DEPTH = 32;
uint256 internal constant SNARK_SCALAR_FIELD =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
/// @dev Initializes a tree.
/// @param self: Tree data.
/// @param depth: Depth of the tree.
/// @param zero: Zero value to be used.
function init(
IncrementalTreeData storage self,
uint256 depth,
uint256 zero
) public {
require(zero < SNARK_SCALAR_FIELD, "IncrementalBinaryTree: leaf must be < SNARK_SCALAR_FIELD");
require(depth > 0 && depth <= MAX_DEPTH, "IncrementalBinaryTree: tree depth must be between 1 and 32");
self.depth = depth;
for (uint8 i = 0; i < depth; ) {
self.zeroes[i] = zero;
zero = PoseidonT3.poseidon([zero, zero]);
unchecked {
++i;
}
}
self.root = zero;
}
/// @dev Inserts a leaf in the tree.
/// @param self: Tree data.
/// @param leaf: Leaf to be inserted.
function insert(IncrementalTreeData storage self, uint256 leaf) public {
uint256 depth = self.depth;
require(leaf < SNARK_SCALAR_FIELD, "IncrementalBinaryTree: leaf must be < SNARK_SCALAR_FIELD");
require(self.numberOfLeaves < 2**depth, "IncrementalBinaryTree: tree is full");
uint256 index = self.numberOfLeaves;
uint256 hash = leaf;
for (uint8 i = 0; i < depth; ) {
if (index & 1 == 0) {
self.lastSubtrees[i] = [hash, self.zeroes[i]];
} else {
self.lastSubtrees[i][1] = hash;
}
hash = PoseidonT3.poseidon(self.lastSubtrees[i]);
index >>= 1;
unchecked {
++i;
}
}
self.root = hash;
self.numberOfLeaves += 1;
}
/// @dev Updates a leaf in the tree.
/// @param self: Tree data.
/// @param leaf: Leaf to be updated.
/// @param newLeaf: New leaf.
/// @param proofSiblings: Array of the sibling nodes of the proof of membership.
/// @param proofPathIndices: Path of the proof of membership.
function update(
IncrementalTreeData storage self,
uint256 leaf,
uint256 newLeaf,
uint256[] calldata proofSiblings,
uint8[] calldata proofPathIndices
) public {
require(newLeaf != leaf, "IncrementalBinaryTree: new leaf cannot be the same as the old one");
require(newLeaf < SNARK_SCALAR_FIELD, "IncrementalBinaryTree: new leaf must be < SNARK_SCALAR_FIELD");
require(
verify(self, leaf, proofSiblings, proofPathIndices),
"IncrementalBinaryTree: leaf is not part of the tree"
);
uint256 depth = self.depth;
uint256 hash = newLeaf;
uint256 updateIndex;
for (uint8 i = 0; i < depth; ) {
updateIndex |= uint256(proofPathIndices[i]) << uint256(i);
if (proofPathIndices[i] == 0) {
if (proofSiblings[i] == self.lastSubtrees[i][1]) {
self.lastSubtrees[i][0] = hash;
}
hash = PoseidonT3.poseidon([hash, proofSiblings[i]]);
} else {
if (proofSiblings[i] == self.lastSubtrees[i][0]) {
self.lastSubtrees[i][1] = hash;
}
hash = PoseidonT3.poseidon([proofSiblings[i], hash]);
}
unchecked {
++i;
}
}
require(updateIndex < self.numberOfLeaves, "IncrementalBinaryTree: leaf index out of range");
self.root = hash;
}
/// @dev Removes a leaf from the tree.
/// @param self: Tree data.
/// @param leaf: Leaf to be removed.
/// @param proofSiblings: Array of the sibling nodes of the proof of membership.
/// @param proofPathIndices: Path of the proof of membership.
function remove(
IncrementalTreeData storage self,
uint256 leaf,
uint256[] calldata proofSiblings,
uint8[] calldata proofPathIndices
) public {
update(self, leaf, self.zeroes[0], proofSiblings, proofPathIndices);
}
/// @dev Verify if the path is correct and the leaf is part of the tree.
/// @param self: Tree data.
/// @param leaf: Leaf to be removed.
/// @param proofSiblings: Array of the sibling nodes of the proof of membership.
/// @param proofPathIndices: Path of the proof of membership.
/// @return True or false.
function verify(
IncrementalTreeData storage self,
uint256 leaf,
uint256[] calldata proofSiblings,
uint8[] calldata proofPathIndices
) private view returns (bool) {
require(leaf < SNARK_SCALAR_FIELD, "IncrementalBinaryTree: leaf must be < SNARK_SCALAR_FIELD");
uint256 depth = self.depth;
require(
proofPathIndices.length == depth && proofSiblings.length == depth,
"IncrementalBinaryTree: length of path is not correct"
);
uint256 hash = leaf;
for (uint8 i = 0; i < depth; ) {
require(
proofSiblings[i] < SNARK_SCALAR_FIELD,
"IncrementalBinaryTree: sibling node must be < SNARK_SCALAR_FIELD"
);
require(
proofPathIndices[i] == 1 || proofPathIndices[i] == 0,
"IncrementalBinaryTree: path index is neither 0 nor 1"
);
if (proofPathIndices[i] == 0) {
hash = PoseidonT3.poseidon([hash, proofSiblings[i]]);
} else {
hash = PoseidonT3.poseidon([proofSiblings[i], hash]);
}
unchecked {
++i;
}
}
return hash == self.root;
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
//SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
/// @title SemaphoreGroups contract interface.
interface ISemaphoreGroups {
error Semaphore__GroupDoesNotExist();
error Semaphore__GroupAlreadyExists();
/// @dev Emitted when a new group is created.
/// @param groupId: Id of the group.
/// @param merkleTreeDepth: Depth of the tree.
/// @param zeroValue: Zero value of the tree.
event GroupCreated(uint256 indexed groupId, uint256 merkleTreeDepth, uint256 zeroValue);
/// @dev Emitted when a new identity commitment is added.
/// @param groupId: Group id of the group.
/// @param index: Identity commitment index.
/// @param identityCommitment: New identity commitment.
/// @param merkleTreeRoot: New root hash of the tree.
event MemberAdded(uint256 indexed groupId, uint256 index, uint256 identityCommitment, uint256 merkleTreeRoot);
/// @dev Emitted when an identity commitment is updated.
/// @param groupId: Group id of the group.
/// @param index: Identity commitment index.
/// @param identityCommitment: Existing identity commitment to be updated.
/// @param newIdentityCommitment: New identity commitment.
/// @param merkleTreeRoot: New root hash of the tree.
event MemberUpdated(
uint256 indexed groupId,
uint256 index,
uint256 identityCommitment,
uint256 newIdentityCommitment,
uint256 merkleTreeRoot
);
/// @dev Emitted when a new identity commitment is removed.
/// @param groupId: Group id of the group.
/// @param index: Identity commitment index.
/// @param identityCommitment: Existing identity commitment to be removed.
/// @param merkleTreeRoot: New root hash of the tree.
event MemberRemoved(uint256 indexed groupId, uint256 index, uint256 identityCommitment, uint256 merkleTreeRoot);
/// @dev Returns the last root hash of a group.
/// @param groupId: Id of the group.
/// @return Root hash of the group.
function getMerkleTreeRoot(uint256 groupId) external view returns (uint256);
/// @dev Returns the depth of the tree of a group.
/// @param groupId: Id of the group.
/// @return Depth of the group tree.
function getMerkleTreeDepth(uint256 groupId) external view returns (uint256);
/// @dev Returns the number of tree leaves of a group.
/// @param groupId: Id of the group.
/// @return Number of tree leaves.
function getNumberOfMerkleTreeLeaves(uint256 groupId) external view returns (uint256);
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
//SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
import "./Pairing.sol";
/// @title SemaphoreVerifier contract interface.
interface ISemaphoreVerifier {
struct VerificationKey {
Pairing.G1Point alfa1;
Pairing.G2Point beta2;
Pairing.G2Point gamma2;
Pairing.G2Point delta2;
Pairing.G1Point[] IC;
}
struct Proof {
Pairing.G1Point A;
Pairing.G2Point B;
Pairing.G1Point C;
}
/// @dev Verifies whether a Semaphore proof is valid.
/// @param merkleTreeRoot: Root of the Merkle tree.
/// @param nullifierHash: Nullifier hash.
/// @param signal: Semaphore signal.
/// @param externalNullifier: External nullifier.
/// @param proof: Zero-knowledge proof.
/// @param merkleTreeDepth: Depth of the tree.
function verifyProof(
uint256 merkleTreeRoot,
uint256 nullifierHash,
uint256 signal,
uint256 externalNullifier,
uint256[8] calldata proof,
uint256 merkleTreeDepth
) external view;
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
// Copyright 2017 Christian Reitwiessner
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// The following Pairing library is a modified version adapted to Semaphore.
//
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
library Pairing {
error InvalidProof();
// The prime q in the base field F_q for G1
uint256 constant BASE_MODULUS = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
// The prime modulus of the scalar field of G1.
uint256 constant SCALAR_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
struct G1Point {
uint256 X;
uint256 Y;
}
// Encoding of field elements is: X[0] * z + X[1]
struct G2Point {
uint256[2] X;
uint256[2] Y;
}
/// @return the generator of G1
function P1() public pure returns (G1Point memory) {
return G1Point(1, 2);
}
/// @return the generator of G2
function P2() public pure returns (G2Point memory) {
return
G2Point(
[
11559732032986387107991004021392285783925812861821192530917403151452391805634,
10857046999023057135944570762232829481370756359578518086990519993285655852781
],
[
4082367875863433681332203403145435568316851327593401208105741076214120093531,
8495653923123431417604973247489272438418190587263600148770280649306958101930
]
);
}
/// @return r the negation of p, i.e. p.addition(p.negate()) should be zero.
function negate(G1Point memory p) public pure returns (G1Point memory r) {
if (p.X == 0 && p.Y == 0) {
return G1Point(0, 0);
}
// Validate input or revert
if (p.X >= BASE_MODULUS || p.Y >= BASE_MODULUS) {
revert InvalidProof();
}
// We know p.Y > 0 and p.Y < BASE_MODULUS.
return G1Point(p.X, BASE_MODULUS - p.Y);
}
/// @return r the sum of two points of G1
function addition(G1Point memory p1, G1Point memory p2) public view returns (G1Point memory r) {
// By EIP-196 all input is validated to be less than the BASE_MODULUS and form points
// on the curve.
uint256[4] memory input;
input[0] = p1.X;
input[1] = p1.Y;
input[2] = p2.X;
input[3] = p2.Y;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60)
}
if (!success) {
revert InvalidProof();
}
}
/// @return r the product of a point on G1 and a scalar, i.e.
/// p == p.scalar_mul(1) and p.addition(p) == p.scalar_mul(2) for all points p.
function scalar_mul(G1Point memory p, uint256 s) public view returns (G1Point memory r) {
// By EIP-196 the values p.X and p.Y are verified to be less than the BASE_MODULUS and
// form a valid point on the curve. But the scalar is not verified, so we do that explicitly.
if (s >= SCALAR_MODULUS) {
revert InvalidProof();
}
uint256[3] memory input;
input[0] = p.X;
input[1] = p.Y;
input[2] = s;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60)
}
if (!success) {
revert InvalidProof();
}
}
/// Asserts the pairing check
/// e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1
/// For example pairing([P1(), P1().negate()], [P2(), P2()]) should succeed
function pairingCheck(G1Point[] memory p1, G2Point[] memory p2) public view {
// By EIP-197 all input is verified to be less than the BASE_MODULUS and form elements in their
// respective groups of the right order.
if (p1.length != p2.length) {
revert InvalidProof();
}
uint256 elements = p1.length;
uint256 inputSize = elements * 6;
uint256[] memory input = new uint256[](inputSize);
for (uint256 i = 0; i < elements; i++) {
input[i * 6 + 0] = p1[i].X;
input[i * 6 + 1] = p1[i].Y;
input[i * 6 + 2] = p2[i].X[0];
input[i * 6 + 3] = p2[i].X[1];
input[i * 6 + 4] = p2[i].Y[0];
input[i * 6 + 5] = p2[i].Y[1];
}
uint256[1] memory out;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20)
}
if (!success || out[0] != 1) {
revert InvalidProof();
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import {SemaphoreGroups} from "./SemaphoreGroups.sol";
import {ISemaphoreVerifier} from "./ISemaphoreVerifier.sol";
interface IFriendtechSharesV1 {
// SharesSubject => (Holder => Balance)
function sharesBallance(address, address) external returns (uint256);
}
/// @title Friend.Tech private polls
/// @notice Create Private polls for your Friend.Tech key owners
/// @dev Code largely adapted from @semaphore-protocol/contracts/extensions/SemaphoreVoting.sol
/// @author Odysseas.eth <me@odyslam.com>
contract PrivateFT is SemaphoreGroups {
IFriendtechSharesV1 public $friendTech;
ISemaphoreVerifier public $verifier;
uint256 immutable TREE_DEPTH;
enum PollState {
Created,
Ongoing,
Ended
}
struct Verifier {
address contractAddress;
uint256 merkleTreeDepth;
}
struct Poll {
address coordinator;
PollState state;
mapping(uint256 => bool) nullifierHashes;
}
/// @dev Emitted when a new poll is created.
/// @param pollId: Id of the poll.
/// @param coordinator: Coordinator of the poll.
event PollCreated(uint256 pollId, address indexed coordinator);
/// @dev Emitted when a poll is started.
/// @param pollId: Id of the poll.
/// @param coordinator: Coordinator of the poll.
/// @param encryptionKey: Key to encrypt the poll votes.
event PollStarted(
uint256 pollId,
address indexed coordinator,
uint256 encryptionKey
);
/// @dev Emitted when a user votes on a poll.
/// @param pollId: Id of the poll.
/// @param vote: User encrypted vote.
event VoteAdded(uint256 indexed pollId, uint256 vote);
/// @dev Emitted when a poll is ended.
/// @param pollId: Id of the poll.
/// @param coordinator: Coordinator of the poll.
/// @param decryptionKey: Key to decrypt the poll votes.
event PollEnded(
uint256 pollId,
address indexed coordinator,
uint256 decryptionKey
);
/// @dev Gets a poll id and returns the poll data.
mapping(uint256 => Poll) internal $polls;
/// @dev Checks if the poll coordinator is the transaction sender.
/// @param pollId: Id of the poll.
modifier onlyCoordinator(uint256 pollId) {
require($polls[pollId].coordinator == msg.sender, "Only coordinator");
_;
}
constructor(
IFriendtechSharesV1 _ft,
ISemaphoreVerifier _verifier,
uint256 _depth
) {
$friendTech = _ft;
$verifier = _verifier;
TREE_DEPTH = _depth;
}
/// @notice Create a poll for the Friend.Tech key holders of your account
function createPoll(uint256 pollId, uint256 encryptionKey) external {
// It will revert if pollId already exists
_createGroup(pollId, TREE_DEPTH);
$polls[pollId].coordinator = msg.sender;
emit PollCreated(pollId, msg.sender);
}
/// @notice Start the poll
function startPoll(
uint256 pollId,
uint256 encryptionKey
) public onlyCoordinator(pollId) {
require($polls[pollId].state != PollState.Created, "Poll has started");
$polls[pollId].state = PollState.Ongoing;
emit PollStarted(pollId, msg.sender, encryptionKey);
}
/// @notice Join a poll of a person for whom you own keys in Friend.Tech
function joinPoll(
uint pollId,
address subject,
uint256 identityCommitment
) public {
// Semaphore requires
require($polls[pollId].state == PollState.Created, "Poll has started");
// FT requires
require(
$polls[pollId].coordinator != subject,
"pollId belongs to another subject"
);
require(
$friendTech.sharesBallance(subject, msg.sender) > 0,
"sender doesn't own any keys of subject"
);
_addMember(pollId, identityCommitment);
}
function castVote(
uint256 vote,
uint256 nullifierHash,
uint256 pollId,
uint256[8] calldata proof
) public {
require(
$polls[pollId].state == PollState.Ongoing,
"Poll is not ongoing"
);
require(
!$polls[pollId].nullifierHashes[nullifierHash],
"nullifier already used"
);
uint256 merkleTreeDepth = getMerkleTreeDepth(pollId);
uint256 merkleTreeRoot = getMerkleTreeRoot(pollId);
$verifier.verifyProof(
merkleTreeRoot,
nullifierHash,
vote,
pollId,
proof,
merkleTreeDepth
);
$polls[pollId].nullifierHashes[nullifierHash] = true;
emit VoteAdded(pollId, vote);
}
function endPoll(
uint256 pollId,
uint256 decryptionKey
) public onlyCoordinator(pollId) {
require(
$polls[pollId].state == PollState.Ongoing,
"Poll is not ongoing"
);
$polls[pollId].state = PollState.Ended;
emit PollEnded(pollId, _msgSender(), decryptionKey);
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/private_ft-IFriendtechSharesV1/?utm=code
*/
//SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
import "./ISemaphoreGroups.sol";
import "./IncrementalBinaryTree.sol";
import "./Context.sol";
/// @title Semaphore groups contract.
/// @dev This contract allows you to create groups, add, remove and update members.
/// You can use getters to obtain informations about groups (root, depth, number of leaves).
abstract contract SemaphoreGroups is Context, ISemaphoreGroups {
using IncrementalBinaryTree for IncrementalTreeData;
/// @dev Gets a group id and returns the tree data.
mapping(uint256 => IncrementalTreeData) internal merkleTrees;
/// @dev Creates a new group by initializing the associated tree.
/// @param groupId: Id of the group.
/// @param merkleTreeDepth: Depth of the tree.
function _createGroup(uint256 groupId, uint256 merkleTreeDepth) internal virtual {
if (getMerkleTreeDepth(groupId) != 0) {
revert Semaphore__GroupAlreadyExists();
}
// The zeroValue is an implicit member of the group, or an implicit leaf of the Merkle tree.
// Although there is a remote possibility that the preimage of
// the hash may be calculated, using this value we aim to minimize the risk.
uint256 zeroValue = uint256(keccak256(abi.encodePacked(groupId))) >> 8;
merkleTrees[groupId].init(merkleTreeDepth, zeroValue);
emit GroupCreated(groupId, merkleTreeDepth, zeroValue);
}
/// @dev Adds an identity commitment to an existing group.
/// @param groupId: Id of the group.
/// @param identityCommitment: New identity commitment.
function _addMember(uint256 groupId, uint256 identityCommitment) internal virtual {
if (getMerkleTreeDepth(groupId) == 0) {
revert Semaphore__GroupDoesNotExist();
}
merkleTrees[groupId].insert(identityCommitment);
uint256 merkleTreeRoot = getMerkleTreeRoot(groupId);
uint256 index = getNumberOfMerkleTreeLeaves(groupId) - 1;
emit MemberAdded(groupId, index, identityCommitment, merkleTreeRoot);
}
/// @dev Updates an identity commitment of an existing group. A proof of membership is
/// needed to check if the node to be updated is part of the tree.
/// @param groupId: Id of the group.
/// @param identityCommitment: Existing identity commitment to be updated.
/// @param newIdentityCommitment: New identity commitment.
/// @param proofSiblings: Array of the sibling nodes of the proof of membership.
/// @param proofPathIndices: Path of the proof of membership.
function _updateMember(
uint256 groupId,
uint256 identityCommitment,
uint256 newIdentityCommitment,
uint256[] calldata proofSiblings,
uint8[] calldata proofPathIndices
) internal virtual {
if (getMerkleTreeDepth(groupId) == 0) {
revert Semaphore__GroupDoesNotExist();
}
merkleTrees[groupId].update(identityCommitment, newIdentityCommitment, proofSiblings, proofPathIndices);
uint256 merkleTreeRoot = getMerkleTreeRoot(groupId);
uint256 index = proofPathIndicesToMemberIndex(proofPathIndices);
emit MemberUpdated(groupId, index, identityCommitment, newIdentityCommitment, merkleTreeRoot);
}
/// @dev Removes an identity commitment from an existing group. A proof of membership is
/// needed to check if the node to be deleted is part of the tree.
/// @param groupId: Id of the group.
/// @param identityCommitment: Existing identity commitment to be removed.
/// @param proofSiblings: Array of the sibling nodes of the proof of membership.
/// @param proofPathIndices: Path of the proof of membership.
function _removeMember(
uint256 groupId,
uint256 identityCommitment,
uint256[] calldata proofSiblings,
uint8[] calldata proofPathIndices
) internal virtual {
if (getMerkleTreeDepth(groupId) == 0) {
revert Semaphore__GroupDoesNotExist();
}
merkleTrees[groupId].remove(identityCommitment, proofSiblings, proofPathIndices);
uint256 merkleTreeRoot = getMerkleTreeRoot(groupId);
uint256 index = proofPathIndicesToMemberIndex(proofPathIndices);
emit MemberRemoved(groupId, index, identityCommitment, merkleTreeRoot);
}
/// @dev See {ISemaphoreGroups-getMerkleTreeRoot}.
function getMerkleTreeRoot(uint256 groupId) public view virtual override returns (uint256) {
return merkleTrees[groupId].root;
}
/// @dev See {ISemaphoreGroups-getMerkleTreeDepth}.
function getMerkleTreeDepth(uint256 groupId) public view virtual override returns (uint256) {
return merkleTrees[groupId].depth;
}
/// @dev See {ISemaphoreGroups-getNumberOfMerkleTreeLeaves}.
function getNumberOfMerkleTreeLeaves(uint256 groupId) public view virtual override returns (uint256) {
return merkleTrees[groupId].numberOfLeaves;
}
/// @dev Converts the path indices of a Merkle proof to the identity commitment index in the tree.
/// @param proofPathIndices: Path of the proof of membership.
/// @return Index of a group member.
function proofPathIndicesToMemberIndex(uint8[] calldata proofPathIndices) private pure returns (uint256) {
uint256 memberIndex = 0;
for (uint8 i = uint8(proofPathIndices.length); i > 0; ) {
if (memberIndex > 0 || proofPathIndices[i - 1] != 0) {
memberIndex *= 2;
if (proofPathIndices[i - 1] == 1) {
memberIndex += 1;
}
}
unchecked {
--i;
}
}
return memberIndex;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment