generated from proofoftom/buidler-waffle-typechain-quasar
-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/feat/snapshot-user-registry' int…
…o feat/merkle-user-registry
- Loading branch information
Showing
36 changed files
with
1,247 additions
and
174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
build | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Common clr.fund utility functions used by contracts and vue-app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './block' | ||
export * from './proof' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
136
contracts/contracts/userRegistry/SnapshotUserRegistry.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.