Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feat/snapshot-user-registry' int…
Browse files Browse the repository at this point in the history
…o feat/merkle-user-registry
  • Loading branch information
yuetloo committed Aug 21, 2023
2 parents 5035481 + 0e59c13 commit 2f4cfc2
Show file tree
Hide file tree
Showing 36 changed files with 1,247 additions and 174 deletions.
2 changes: 2 additions & 0 deletions common/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build
node_modules
1 change: 1 addition & 0 deletions common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Common clr.fund utility functions used by contracts and vue-app
28 changes: 28 additions & 0 deletions common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@clrfund/common",
"version": "0.0.1",
"description": "Common utility functions used by clrfund scripts and app",
"main": "src/index",
"scripts": {
"build": "tsc",
"lint": "eslint 'src/**/*.ts'",
"clean": "rm -rf build"
},
"license": "GPL-3.0",
"devDependencies": {
"eslint": "^8.31.0",
"typescript": "^4.9.3"
},
"dependencies": {
"ethers": "^5.7.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/clrfund/monorepo.git"
},
"author": "",
"bugs": {
"url": "https://github.com/clrfund/monorepo/issues"
},
"homepage": "https://github.com/clrfund/monorepo#readme"
}
20 changes: 20 additions & 0 deletions common/src/block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { providers } from 'ethers'

export interface Block {
blockNumber: number
hash: string
stateRoot: string
}

/*
* get the block stateRoot using eth_getBlockByHash
*/
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 }
}
2 changes: 2 additions & 0 deletions common/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './block'
export * from './proof'
85 changes: 85 additions & 0 deletions common/src/proof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { utils, providers } from 'ethers'

/**
* RLP encode the proof returned from eth_getProof
* @param proof proof from the eth_getProof
* @returns
*/
export function rlpEncodeProof(proof: string[]) {
const decodedProof = proof.map((node: string) => utils.RLP.decode(node))

return utils.RLP.encode(decodedProof)
}

/**
* The storage key used in eth_getProof and eth_getStorageAt
* @param account Account address
* @param slotIndex Slot index of the balanceOf storage
* @returns storage key used in the eth_getProof params
*/
export function getStorageKey(account: string, slotIndex: number) {
return utils.keccak256(
utils.concat([
utils.hexZeroPad(account, 32),
utils.hexZeroPad(utils.hexValue(slotIndex), 32),
])
)
}

/**
* Get proof from eth_getProof
* @param params Parameter fro eth_getProof
* @returns proof returned from eth_getProof
*/
async function getProof(
params: Array<string | string[]>,
provider: providers.JsonRpcProvider
): Promise<any> {
try {
const proof = await provider.send('eth_getProof', params)
return proof
} catch (err) {
console.error(
'Unable to get proof. Your node may not support eth_getProof. Try a different provider such as Infura',
err
)
throw err
}
}
/**
* Get the storage proof
* @param token Token contract address
* @param blockHash The block hash to get the proof for
* @param provider provider to connect to the node
* @returns proof returned from eth_getProof
*/
export async function getAccountProof(
token: string,
blockHash: string,
provider: providers.JsonRpcProvider
): Promise<any> {
const params = [token, [], blockHash]
return getProof(params, provider)
}

/**
* Get the storage proof
* @param token Token contract address
* @param blockHash The block hash to get the storage proof for
* @param userAccount User account to get the proof for
* @param storageSlotIndex The storage index for the balanceOf storage
* @param provider provider to connect to the node
* @returns proof returned from eth_getProof
*/
export async function getStorageProof(
token: string,
blockHash: string,
userAccount: string,
storageSlotIndex: number,
provider: providers.JsonRpcProvider
): Promise<any> {
const storageKey = getStorageKey(userAccount, storageSlotIndex)

const params = [token, [storageKey], blockHash]
return getProof(params, provider)
}
22 changes: 22 additions & 0 deletions common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"skipLibCheck": true,
"experimentalDecorators": true,
"alwaysStrict": true,
"noImplicitAny": false,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"strict": true,
"outDir": "./build",
"target": "es2018",
"esModuleInterop": true,
"module": "commonjs",
"declaration": true
},
"exclude": ["node_modules/**"],
"include": ["./src"]
}
2 changes: 1 addition & 1 deletion contracts/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Recipient registry type for local deployment: simple, optimistic
RECIPIENT_REGISTRY_TYPE=optimistic

# Supported values: simple, brightid
# Supported values: simple, brightid, snapshot
USER_REGISTRY_TYPE=simple
# clr.fund (prod) or CLRFundTest (testing)
BRIGHTID_CONTEXT=clr.fund
Expand Down
47 changes: 8 additions & 39 deletions contracts/contracts/userRegistry/README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,21 @@
## Description

### BrightIdUserRegistry

This is a contract to register verified users context ids by BrightID node's verification data, and be able to query a user verification.
This contract consist of:

- Set BrightID settings <br />`function setSettings(bytes32 _context, address _verifier) external onlyOwner;`
- Check a user is verified or not <br />`function isVerifiedUser(address _user) override external view returns (bool);`
- Register a user by BrightID node's verification data <br />`function register(bytes32 _context, address[] calldata _addrs, uint _timestamp, uint8 _v, bytes32 _r, bytes32 _s external;`

## Demonstration

> TODO: update the following with a goerli contract
[Demo contract on the Rinkeby](https://rinkeby.etherscan.io/address/0xf99e2173db1f341a947ce9bd7779af2245309f91)
Sample of Registered Data:

```
{
"data": {
"unique": true,
"context": "clr.fund",
"contextIds": [
"0xb1775295f3b250c2849366801149479471fa7362",
"0x9ed6d9086f5ee9edc14dd2caca44d65ee8cabdde",
"0x79af508c9698076bc1c2dfa224f7829e9768b11e"
],
"sig": {
"r": "ec6a9c3e10f238acb757ceea5507cf33366acd05356d513ca80cd1148297d079",
"s": "0e918c709ea7a458f7c95769145f475df94c01f3bc9e9ededf38153aa5b9041b",
"v": 28
},
"timestamp": 1602353670884,
"publicKey": "03ab573225151072be57d4808861e0f706595fb143c71630e188051fe4a6bda594"
}
}
```

You can see the contract settings [here](https://rinkeby.etherscan.io/address/0xf99e2173db1f341a947ce9bd7779af2245309f91#readContract)

You can update the BrightID settings and test register [here](https://rinkeby.etherscan.io/address/0xf99e2173db1f341a947ce9bd7779af2245309f91#writeContract)
### SnapshotUserRegistry

## Deploy contract
This is a contract to register verified users by the proof that the users held the minimum amount of tokens at a given block.

This contract needs two constructor arguments
The main functions:

- `context bytes32` <br /> BrightID context used for verifying users.

- `verifier address` <br /> BrightID verifier address that signs BrightID verifications.

## Points

We can simply use an ERC20 token as authorization for the verifiers to be able have multiple verifiers.
- Set storage root <br />`function setStorageRoot(address tokenAddress, bytes32 stateRoot uint256 slotIndex, bytes memory accountProofRlpBytes) external onlyOwner;`
- Check a user is verified or not <br />`function isVerifiedUser(address _user) override external view returns (bool);`
- Add a user with the proof from eth_getProof <br />`function addUser(address _user, bytes memory storageProofRlpBytes) external;`
136 changes: 136 additions & 0 deletions contracts/contracts/userRegistry/SnapshotUserRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.6.12;

import '@openzeppelin/contracts/access/Ownable.sol';

import './IUserRegistry.sol';

import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol";
import {StateProofVerifier} from "../utils/cryptography/StateProofVerifier.sol";


/**
* @dev A user registry that verifies users based on ownership of a token
* at a specific block snapshot
*/
contract SnapshotUserRegistry is Ownable, IUserRegistry {
using RLPReader for RLPReader.RLPItem;
using RLPReader for bytes;

enum Status {
Unverified,
Verified,
Rejected
}

// User must hold this token at a specific block to be added to this registry
address public token;

// block hash of the snapshot block
bytes32 public blockHash;

// The storage root for the token at a specified block
bytes32 public storageRoot;

// The slot index for the token balance
uint256 public storageSlot;

// The minimum balance the user must hold to be verified
uint256 public minBalance = 1;

// verified users
mapping(address => Status) public users;

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

/**
* @dev Set the storage root for the token contract at a specific block
* @param _tokenAddress Token address
* @param _blockHash Block hash
* @param _stateRoot Block state root
* @param _slotIndex slot index of the token balances storage
* @param _accountProofRlpBytes RLP encoded accountProof from eth_getProof
*/
function setStorageRoot(
address _tokenAddress,
bytes32 _blockHash,
bytes32 _stateRoot,
uint256 _slotIndex,
bytes memory _accountProofRlpBytes
)
external
onlyOwner
{

RLPReader.RLPItem[] memory proof = _accountProofRlpBytes.toRlpItem().toList();
bytes32 addressHash = keccak256(abi.encodePacked(uint160(_tokenAddress)));

StateProofVerifier.Account memory account = StateProofVerifier.extractAccountFromProof(
addressHash,
_stateRoot,
proof
);

token = _tokenAddress;
blockHash = _blockHash;
storageRoot = account.storageRoot;
storageSlot = _slotIndex;

emit StorageRootChanged(token, blockHash, storageSlot);
}

/**
* @dev Add a verified user to the registry.
* @param _user user account address
* @param storageProofRlpBytes RLP-encoded storage proof from eth_getProof
*/
function addUser(
address _user,
bytes memory storageProofRlpBytes
)
external
{
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');

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

bytes32 userSlotHash = keccak256(abi.encodePacked(uint256(uint160(_user)), storageSlot));
bytes32 proofPath = keccak256(abi.encodePacked(userSlotHash));
StateProofVerifier.SlotValue memory slotValue = StateProofVerifier.extractSlotValueFromProof(proofPath, storageRoot, proof);
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);
}

/**
* @dev Check if the user is verified.
*/
function isVerifiedUser(address _user)
override
external
view
returns (bool)
{
return users[_user] == Status.Verified;
}

/**
* @dev Change the minimum balance a user must hold to be verified
* @param newMinBalance The new minimum balance
*/
function setMinBalance(uint256 newMinBalance) external onlyOwner {
require(newMinBalance > 0, 'SnapshotUserRegistry: The minimum balance must be greater than 0');

minBalance = newMinBalance;

emit MinBalanceChanged(minBalance);
}
}
Loading

0 comments on commit 2f4cfc2

Please sign in to comment.