Skip to content

Commit

Permalink
update snapshot and merkle user registries users mapping structure
Browse files Browse the repository at this point in the history
  • Loading branch information
yuetloo committed Aug 28, 2023
1 parent c9bcbde commit 3a83591
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 65 deletions.
10 changes: 5 additions & 5 deletions common/src/block.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { providers } from 'ethers'
import { providers, utils } from 'ethers'

export interface Block {
blockNumber: number
Expand All @@ -13,8 +13,8 @@ export async function getBlock(
blockNumber: number,
provider: providers.JsonRpcProvider
): Promise<Block> {
const block = await provider.getBlock(blockNumber)
const blockParams = [block.hash, false]
const rawBlock = await provider.send('eth_getBlockByHash', blockParams)
return { blockNumber, hash: block.hash, stateRoot: rawBlock.stateRoot }
const blockNumberHex = utils.hexValue(blockNumber)
const blockParams = [blockNumberHex, false]
const rawBlock = await provider.send('eth_getBlockByNumber', blockParams)
return { blockNumber, hash: rawBlock.hash, stateRoot: rawBlock.stateRoot }
}
17 changes: 10 additions & 7 deletions contracts/contracts/userRegistry/MerkleUserRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
*/
contract MerkleUserRegistry is Ownable, IUserRegistry {

// verified users
mapping(address => bool) private users;
// verified users grouped by merkleRoot
// merkleRoot -> user -> status
mapping(bytes32 => mapping(address => bool)) private users;

// merkle root
bytes32 public merkleRoot;
Expand All @@ -25,7 +26,7 @@ contract MerkleUserRegistry is Ownable, IUserRegistry {
string public merkleHash;

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

/**
Expand All @@ -35,6 +36,8 @@ contract MerkleUserRegistry is Ownable, IUserRegistry {
*/
function setMerkleRoot(bytes32 root, string calldata ipfsHash) external onlyOwner {
require(root != bytes32(0), 'MerkleUserRegistry: Merkle root is zero');
require(bytes(ipfsHash).length != 0, 'MerkleUserRegistry: Merkle hash is empty string');

merkleRoot = root;
merkleHash = ipfsHash;

Expand All @@ -49,15 +52,15 @@ contract MerkleUserRegistry is Ownable, IUserRegistry {
{
require(merkleRoot != bytes32(0), 'MerkleUserRegistry: Merkle root is not initialized');
require(_user != address(0), 'MerkleUserRegistry: User address is zero');
require(!users[_user], 'MerkleUserRegistry: User already verified');
require(!users[merkleRoot][_user], 'MerkleUserRegistry: User already verified');

// verifies user against the merkle root
bytes32 leaf = keccak256(abi.encodePacked(keccak256(abi.encode(_user))));
bool verified = MerkleProof.verifyCalldata(proof, merkleRoot, leaf);
require(verified, 'MerkleUserRegistry: User is not authorized');

users[_user] = true;
emit UserAdded(_user);
users[merkleRoot][_user] = true;
emit UserAdded(_user, merkleRoot);

}

Expand All @@ -70,6 +73,6 @@ contract MerkleUserRegistry is Ownable, IUserRegistry {
view
returns (bool)
{
return users[_user];
return users[merkleRoot][_user];
}
}
13 changes: 7 additions & 6 deletions contracts/contracts/userRegistry/SnapshotUserRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ contract SnapshotUserRegistry is Ownable, IUserRegistry {
uint256 public minBalance = 1;

// verified users
mapping(address => Status) public users;
// blockHash -> user -> status
mapping(bytes32 => mapping(address => Status)) public users;

// Events
event UserAdded(address indexed _user);
event UserAdded(address indexed _user, bytes32 indexed blockHash);
event MinBalanceChanged(uint256 newBalance);
event StorageRootChanged(address indexed _token, bytes32 indexed _blockHash, uint256 storageSlot);

Expand Down Expand Up @@ -96,7 +97,7 @@ contract SnapshotUserRegistry is Ownable, IUserRegistry {
{
require(storageRoot != bytes32(0), 'SnapshotUserRegistry: Registry is not initialized');
require(_user != address(0), 'SnapshotUserRegistry: User address is zero');
require(users[_user] == Status.Unverified, 'SnapshotUserRegistry: User already added');
require(users[blockHash][_user] == Status.Unverified, 'SnapshotUserRegistry: User already added');

RLPReader.RLPItem[] memory proof = storageProofRlpBytes.toRlpItem().toList();

Expand All @@ -106,8 +107,8 @@ contract SnapshotUserRegistry is Ownable, IUserRegistry {
require(slotValue.exists, 'SnapshotUserRegistry: User is not qualified');
require(slotValue.value >= minBalance , 'SnapshotUserRegistry: User did not meet the minimum balance requirement');

users[_user] = Status.Verified;
emit UserAdded(_user);
users[blockHash][_user] = Status.Verified;
emit UserAdded(_user, blockHash);
}

/**
Expand All @@ -119,7 +120,7 @@ contract SnapshotUserRegistry is Ownable, IUserRegistry {
view
returns (bool)
{
return users[_user] == Status.Verified;
return users[blockHash][_user] == Status.Verified;
}

/**
Expand Down
53 changes: 34 additions & 19 deletions contracts/tasks/loadMerkleUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import { getIpfsHash } from '../utils/ipfs'
* @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
* @param silent true - do not print every address as it's being processed
*/
async function loadFile(
registry: Contract,
addressFile: string,
output: string
output: string,
silent: boolean
) {
let content: string | null = null
try {
Expand All @@ -44,13 +46,16 @@ async function loadFile(

const validAddresses: string[] = []

console.log('Processing addresses...')
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)
if (!silent) {
console.log('Added address', address)
}
} catch (err: any) {
if (err.reason) {
console.error('Failed to add address', address, err.reason)
Expand All @@ -59,7 +64,9 @@ async function loadFile(
}
}
} else {
console.warn('Skipping invalid address', address)
if (address) {
console.warn('Skipping invalid address', address)
}
}
}

Expand Down Expand Up @@ -91,21 +98,29 @@ task('load-merkle-users', 'Bulkload recipients into the simple user registry')
undefined,
types.string
)
.setAction(async ({ userRegistry, addressFile, output }, { ethers }) => {
const registry = await ethers.getContractAt(
'MerkleUserRegistry',
userRegistry
)
.addOptionalParam(
'silent',
'Do not log every address being processed',
true,
types.boolean
)
.setAction(
async ({ userRegistry, addressFile, output, silent }, { 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 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, silent)
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.`
)
})
console.log(
`User merkle tree file generated at ${outputFile}, make sure to upload it to IPFS.`
)
}
)
26 changes: 23 additions & 3 deletions contracts/tests/userRegistryMerkle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ethers, waffle } from 'hardhat'
import { use, expect } from 'chai'
import { solidity } from 'ethereum-waffle'
import { Contract, utils } from 'ethers'
import { Contract, utils, Wallet } from 'ethers'
import { loadUserMerkleTree, getUserMerkleProof } from '@clrfund/common'

use(solidity)
Expand Down Expand Up @@ -35,7 +35,7 @@ describe('Merkle User Registry', () => {
it('should not allow non-owner to set the merkle root', async () => {
const registryAsUser = registry.connect(signers[user1.address])
await expect(
registryAsUser.setMerkleRoot(utils.hexZeroPad('0x0', 32), 'non owner')
registryAsUser.setMerkleRoot(utils.hexZeroPad('0x1', 32), 'non owner')
).to.be.revertedWith('Ownable: caller is not the owner')
})

Expand All @@ -47,7 +47,7 @@ describe('Merkle User Registry', () => {
const registryAsUser = registry.connect(signers[user])
await expect(registryAsUser.addUser(user, proof))
.to.emit(registryAsUser, 'UserAdded')
.withArgs(user)
.withArgs(user, tree.root)
expect(await registryAsUser.isVerifiedUser(user)).to.equal(true)
}
})
Expand All @@ -61,5 +61,25 @@ describe('Merkle User Registry', () => {
).to.be.revertedWith('MerkleUserRegistry: User is not authorized')
expect(await registryAsUser.isVerifiedUser(user.address)).to.equal(false)
})

it('should be able load 10k users', async function () {
this.timeout(200000)

const allAuthorizedUsers = Array.from(authorizedUsers)
for (let i = 0; i < 10000; i++) {
const randomWallet = new Wallet(utils.randomBytes(32))
allAuthorizedUsers.push(randomWallet.address)
}
tree = loadUserMerkleTree(allAuthorizedUsers)
const tx = await registry.setMerkleRoot(tree.root, 'test')
await tx.wait()

const registryAsUser = registry.connect(user1)
const proof = getUserMerkleProof(user1.address, tree)
await expect(registryAsUser.addUser(user1.address, proof))
.to.emit(registryAsUser, 'UserAdded')
.withArgs(user1.address, tree.root)
expect(await registryAsUser.isVerifiedUser(user1.address)).to.equal(true)
})
})
})
4 changes: 2 additions & 2 deletions contracts/tests/userRegistrySnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('SnapshotUserRegistry', function () {
)
)
.to.emit(userRegistry, 'UserAdded')
.withArgs(userAccount)
.withArgs(userAccount, block.hash)
expect(await userRegistry.isVerifiedUser(userAccount)).to.equal(true)
})

Expand Down Expand Up @@ -199,7 +199,7 @@ describe('SnapshotUserRegistry', function () {
)
)
.to.emit(userRegistry, 'UserAdded')
.withArgs(userAddress)
.withArgs(userAddress, block.hash)
expect(await userRegistry.isVerifiedUser(userAddress)).to.equal(true)
})
})
Expand Down
60 changes: 48 additions & 12 deletions vue-app/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import makeBlockie from 'ethereum-blockies-base64'
import { BigNumber, Contract, Signer, type ContractTransaction } from 'ethers'
import type { Web3Provider } from '@ethersproject/providers'

import { UserRegistry, ERC20 } from './abi'
import { FundingRound, UserRegistry, ERC20 } from './abi'
import { factory, ipfsGatewayUrl, provider, operator } from './core'
import type { BrightId } from './bright-id'
import { SnapshotUserRegistry, MerkleUserRegistry } from './abi'
Expand Down Expand Up @@ -53,6 +53,12 @@ export async function isVerifiedUser(userRegistryAddress: string, walletAddress:
return await registry.isVerifiedUser(walletAddress)
}

export async function isRegisteredUser(fundingRoundAddress: string, walletAddress: string): Promise<boolean> {
const round = new Contract(fundingRoundAddress, FundingRound, provider)
const contributor = await round.contributors(walletAddress)
return contributor.isRegistered
}

export async function getTokenBalance(tokenAddress: string, walletAddress: string): Promise<BigNumber> {
const token = new Contract(tokenAddress, ERC20, provider)
return await token.balanceOf(walletAddress)
Expand All @@ -65,10 +71,27 @@ export async function getEtherBalance(walletAddress: string): Promise<BigNumber>
/**
* Register a user in the Snapshot user registry
* @param registryAddress The snapshot user registry contract address
* @param proofRlpBytes The RLP encoded proof
* @param signer The signer
* @returns The contract transaction
*/
export async function registerSnapshotUser(registryAddress: string, signer: Signer): Promise<ContractTransaction> {
export async function registerUserSnapshot(
registryAddress: string,
proofRlpBytes: string,
signer: Signer,
): Promise<ContractTransaction> {
const registry = new Contract(registryAddress, SnapshotUserRegistry, signer)
const walletAddress = await signer.getAddress()
return registry.addUser(walletAddress, proofRlpBytes)
}

/**
* Get the snapshot proof for the signer
* @param registryAddress Th snapshot user registry address
* @param signer Th user to get the proof for
* @returns RLP encoded proof
*/
export async function getProofSnapshot(registryAddress: string, signer: Signer) {
const registry = new Contract(registryAddress, SnapshotUserRegistry, signer)
const [tokenAddress, blockHash, storageSlot] = await Promise.all([
registry.token(),
Expand All @@ -78,28 +101,41 @@ export async function registerSnapshotUser(registryAddress: string, signer: Sign

const walletAddress = await signer.getAddress()
const proof = await getStorageProof(tokenAddress, blockHash, walletAddress, storageSlot, provider)
const proofRlpBytes = rlpEncodeProof(proof.storageProof[0].proof)

return registry.addUser(walletAddress, proofRlpBytes)
return rlpEncodeProof(proof.storageProof[0].proof)
}

/**
* Register a user in the merkle user registry
* @param registryAddress The merkle user registry
* @param proof The merkle proof
* @param signer The user to be registered
* @returns The contract transaction
*/
export async function registerMerkleUser(registryAddress: string, signer: Signer): Promise<ContractTransaction> {
export async function registerUserMerkle(
registryAddress: string,
proof: string[],
signer: Signer,
): Promise<ContractTransaction> {
const registry = new Contract(registryAddress, MerkleUserRegistry, signer)
const walletAddress = await signer.getAddress()
return registry.addUser(walletAddress, proof)
}

/**
* Get the merkle proof for the signer
* @param registryAddress The merkle user registry
* @param signer The user to get the proof for
* @returns proof
*/
export async function getProofMerkle(registryAddress: string, signer: Signer): Promise<string[] | null> {
const registry = new Contract(registryAddress, MerkleUserRegistry, signer)
const merkleHash = await registry.merkleHash()
if (!merkleHash) {
throw new Error('User registry is not initialized, missing merkle hash')
}

const treeRaw = await getIpfsContent(merkleHash, ipfsGatewayUrl)
const tree = StandardMerkleTree.load(treeRaw)
const walletAddress = await signer.getAddress()
const proof = await getUserMerkleProof(walletAddress, tree)
if (!proof) {
throw new Error('User is not authorized')
}

return registry.addUser(walletAddress, proof)
return getUserMerkleProof(walletAddress, tree)
}
Loading

0 comments on commit 3a83591

Please sign in to comment.