Skip to content

Commit

Permalink
Add merkle user registry
Browse files Browse the repository at this point in the history
  • Loading branch information
yuetloo committed Aug 23, 2023
1 parent 2f4cfc2 commit cebb59e
Show file tree
Hide file tree
Showing 21 changed files with 457 additions and 130 deletions.
1 change: 1 addition & 0 deletions common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"typescript": "^4.9.3"
},
"dependencies": {
"@openzeppelin/merkle-tree": "^1.0.5",
"ethers": "^5.7.2"
},
"repository": {
Expand Down
2 changes: 2 additions & 0 deletions common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './block'
export * from './proof'
export * from './merkle'
export * from './ipfs'
18 changes: 18 additions & 0 deletions common/src/ipfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { utils } from 'ethers'

const IPFS_BASE_URL = 'https://ipfs.io'

/**
* Get the IPFS content given the IPFS hash
* @param hash The IPFS hash
* @param gatewayUrl The IPFS gateway url
* @returns The IPFS content
*/
export async function getIpfsContent(
hash: string,
gatewayUrl = IPFS_BASE_URL
): Promise<any> {
const url = `${gatewayUrl}/ipfs/${hash}`
const result = utils.fetchJson(url)
return result
}
37 changes: 37 additions & 0 deletions common/src/merkle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { StandardMerkleTree } from '@openzeppelin/merkle-tree'

/**
* Load users into a merkle tree
* @param users Users to load
* @returns user merkle tree
*/
export function loadUserMerkleTree(
users: string[]
): StandardMerkleTree<string[]> {
const tree = StandardMerkleTree.of(
users.map((user) => [user.toLowerCase()]),
['address']
)
return tree
}

/**
* Get the merkle proof for the user
* @param userAccount User wallet address to get the proof for
* @param userMerkleTree The merkle tree containing all approved users
* @returns
*/
export function getUserMerkleProof(
userAccount: string,
userMerkleTree: StandardMerkleTree<string[]>
): string[] | null {
try {
return userMerkleTree.getProof([userAccount.toLowerCase()])
} catch (err) {
console.log('userAccount', userAccount.toLowerCase())
console.log('getUserMerkleProof error', err)
return null
}
}

export { StandardMerkleTree }
22 changes: 18 additions & 4 deletions contracts/contracts/userRegistry/MerkleUserRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,38 @@ import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
* the funding round coordinator.
*/
contract MerkleUserRegistry is Ownable, IUserRegistry {

// verified users
mapping(address => bool) private users;

// merkle root
bytes32 public merkleRoot;

// ipfs hash of the merkle tree file
string public merkleHash;

// Events
event UserAdded(address indexed _user);
event UserRemoved(address indexed _user);
event MerkleRootChanged(bytes32 indexed root, string ipfsHash);

function setMerkleRoot(bytes32 root) external onlyOwner {
/**
* @dev Set the merkle root used to verify users
* @param root Merkle root
* @param ipfsHash The IPFS hash of the merkle tree file
*/
function setMerkleRoot(bytes32 root, string calldata ipfsHash) external onlyOwner {
require(root != bytes32(0), 'MerkleUserRegistry: Merkle root is zero');
merkleRoot = root;
merkleHash = ipfsHash;

emit MerkleRootChanged(root, ipfsHash);
}

/**
* @dev Add verified unique user to the registry.
*/
function addUser(bytes32[] calldata proof, address _user)
function addUser(address _user, bytes32[] calldata proof)
external
onlyOwner
{
require(merkleRoot != bytes32(0), 'MerkleUserRegistry: Merkle root is not initialized');
require(_user != address(0), 'MerkleUserRegistry: User address is zero');
Expand Down
43 changes: 12 additions & 31 deletions contracts/scripts/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ethers } from 'hardhat'
import { Contract, utils, Wallet } from 'ethers'
import { Contract, Wallet } from 'ethers'

import { UNIT } from '../utils/constants'
import { deployMaciFactory } from '../utils/deployment'
import {
deployMaciFactory,
deployUserRegistry,
getBrightIdParams,
} from '../utils/deployment'
import { Keypair, PrivKey } from '@clrfund/maci-utils'

// Number.MAX_SAFE_INTEGER - 1
Expand Down Expand Up @@ -63,35 +67,12 @@ async function main() {
await transferOwnershipTx.wait()

const userRegistryType = process.env.USER_REGISTRY_TYPE || 'simple'
let userRegistry: Contract
if (userRegistryType === 'simple') {
const SimpleUserRegistry = await ethers.getContractFactory(
'SimpleUserRegistry',
deployer
)
userRegistry = await SimpleUserRegistry.deploy()
} else if (userRegistryType === 'brightid') {
const BrightIdUserRegistry = await ethers.getContractFactory(
'BrightIdUserRegistry',
deployer
)

userRegistry = await BrightIdUserRegistry.deploy(
utils.formatBytes32String(process.env.BRIGHTID_CONTEXT || 'clr.fund'),
process.env.BRIGHTID_VERIFIER_ADDR,
process.env.BRIGHTID_SPONSOR
)
} else if (userRegistryType === 'snapshot') {
const SnapshotUserRegistry = await ethers.getContractFactory(
'SnapshotUserRegistry',
deployer
)
userRegistry = await SnapshotUserRegistry.deploy()
} else {
throw new Error('unsupported user registry type')
}

await userRegistry.deployTransaction.wait()
const brightidParams = getBrightIdParams(userRegistryType)
const userRegistry: Contract = await deployUserRegistry(
userRegistryType,
deployer,
brightidParams
)
console.log(
`User registry (${userRegistryType}) deployed: ${userRegistry.address}`
)
Expand Down
42 changes: 9 additions & 33 deletions contracts/scripts/deployUserRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ethers } from 'hardhat'
import { Contract, utils } from 'ethers'
import { deployUserRegistry, getBrightIdParams } from '../utils/deployment'

async function main() {
console.log('*******************')
Expand All @@ -20,46 +20,22 @@ async function main() {
console.log('funding round factory address ', fundingRoundFactory.address)

const userRegistryType = process.env.USER_REGISTRY_TYPE || 'simple'
let userRegistry: Contract
if (userRegistryType === 'simple') {
const SimpleUserRegistry = await ethers.getContractFactory(
'SimpleUserRegistry',
deployer
)
userRegistry = await SimpleUserRegistry.deploy()
} else if (userRegistryType === 'brightid') {
console.log('deploying brightid user registry')
const BrightIdUserRegistry = await ethers.getContractFactory(
'BrightIdUserRegistry',
deployer
)

userRegistry = await BrightIdUserRegistry.deploy(
utils.formatBytes32String(process.env.BRIGHTID_CONTEXT || 'clr.fund'),
process.env.BRIGHTID_VERIFIER_ADDR,
process.env.BRIGHTID_SPONSOR
)
console.log('transaction hash', userRegistry.deployTransaction.hash)
} else if (userRegistryType === 'snapshot') {
const SnapshotUserRegistry = await ethers.getContractFactory(
'SnapshotUserRegistry',
deployer
)
userRegistry = await SnapshotUserRegistry.deploy()
} else {
throw new Error('unsupported user registry type')
}
await userRegistry.deployTransaction.wait()
const brightidParams = getBrightIdParams(userRegistryType)
const userRegistry = await deployUserRegistry(
userRegistryType,
deployer,
brightidParams
)
console.log(
`Deployed ${userRegistryType} user registry at ${userRegistry.address}`
`deployed ${userRegistryType} user registry at ${userRegistry.address}`
)

const setUserRegistryTx = await fundingRoundFactory.setUserRegistry(
userRegistry.address
)
await setUserRegistryTx.wait()
console.log(
'set user registry in funding round factory',
'set user registry in funding round factory at tx hash',
setUserRegistryTx.hash
)

Expand Down
1 change: 1 addition & 0 deletions contracts/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ import './loadUsers'
import './tally'
import './findStorageSlot'
import './setStorageRoot'
import './loadMerkleUsers'
111 changes: 111 additions & 0 deletions contracts/tasks/loadMerkleUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { task, types } from 'hardhat/config'
import { Contract, utils } from 'ethers'
import fs from 'fs'
import { StandardMerkleTree } from '@clrfund/common'
import { getIpfsHash } from '../utils/ipfs'

/*
* Load users into the the merkle user registry by generating a merkle tree and
* setting the merkle root of the merkle user registry. It will generate an output
* file with the dump of the merkle tree. This file should be uploaded to the IPFS.
*
* File path can be relative or absolute path.
* The script can only be run by the owner of the user registry
*
* Sample usage:
*
* yarn hardhat load-merkle-users --address-file addresses.txt --user-registry <address> --network goerli
*/

/**
* Load users in the file into the simple user registry
*
* @param registry Merkle user registry contract
* @param addressFile The path of the file containing the addresses
* @param output The path for the merkle tree output file
*/
async function loadFile(
registry: Contract,
addressFile: string,
output: string
) {
let content: string | null = null
try {
content = fs.readFileSync(addressFile, 'utf8')
} catch (err) {
console.error('Failed to read file', addressFile, err)
return
}

const addresses: string[] = []
content.split(/\r?\n/).forEach(async (address) => {
addresses.push(address)
})

const validAddresses: string[] = []

for (let i = 0; i < addresses.length; i++) {
const address = addresses[i]
const isValidAddress = Boolean(address) && utils.isAddress(address)
if (isValidAddress) {
console.log('Adding address', address)
try {
validAddresses.push(address)
} catch (err: any) {
if (err.reason) {
console.error('Failed to add address', address, err.reason)
} else {
console.error('Failed to add address', address, err)
}
}
} else {
console.warn('Skipping invalid address', address)
}
}

if (validAddresses.length > 0) {
const tree = StandardMerkleTree.of(
validAddresses.map((address) => [address]),
['address']
)

const treeDump = tree.dump()
fs.writeFileSync(output, JSON.stringify(treeDump, null, 4))

const ipfsHash = await getIpfsHash(treeDump)

const tx = await registry.setMerkleRoot(tree.root, ipfsHash)
return tx
}
}

task('load-merkle-users', 'Bulkload recipients into the simple user registry')
.addParam('userRegistry', 'The merkle user registry contract address')
.addParam(
'addressFile',
'The path of the file containing addresses separated by newline'
)
.addOptionalParam(
'output',
'The output json file path for the merkle tree',
undefined,
types.string
)
.setAction(async ({ userRegistry, addressFile, output }, { ethers }) => {
const registry = await ethers.getContractAt(
'MerkleUserRegistry',
userRegistry
)

console.log('User merkle registry', userRegistry)
console.log('Deployer', await registry.signer.getAddress())
const timeMs = new Date().getTime()
const outputFile = output ? output : `./merkle_users_${timeMs}.json`
const tx = await loadFile(registry, addressFile, outputFile)
console.log('User merkle root updated at tx hash', tx.hash)
await tx.wait()

console.log(
`User merkle tree file generated at ${outputFile}, make sure to upload it to IPFS.`
)
})
Loading

0 comments on commit cebb59e

Please sign in to comment.