Skip to content

Commit

Permalink
feat: add doc for MerkleVerify contract
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeR26 committed Jan 14, 2024
1 parent badc0b3 commit dc0988f
Show file tree
Hide file tree
Showing 11 changed files with 6,008 additions and 14 deletions.
67 changes: 55 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

**This library has not been audited ; use at your own risks.**

This library is able to create Merkle trees, using very efficient and specifics hashes for the Starknet blockchain. You can use :
This library is able to create and handle Merkle trees, using very efficient and specifics hashes for the Starknet blockchain. You can use :
- Pedersen hash (by default)
- Poseidon hash (the most efficient one)

Expand Down Expand Up @@ -37,38 +37,62 @@ For an Airdrop, you needs a list of granted addresses, and optionally the quanti
```typescript
// address + quantity (u256.low + u256.high)
const airdrop: Merkle.InputForMerkle[] = [
['0x69b49c2cc8b16e80e86bfc5b0614a59aa8c9b601569c7b80dde04d3f3151b79', '256','0'],
['0x3cad9a072d3cf29729ab2fad2e08972b8cfde01d4979083fb6d15e8e66f8ab1', '25','0'],
['0x27d32a3033df4277caa9e9396100b7ca8c66a4ef8ea5f6765b91a7c17f0109c', '56','0'],
['0x69b49c2cc8b16e80e86bfc5b0614a59aa8c9b601569c7b80dde04d3f3151b79', '256','0'],
['0x3cad9a072d3cf29729ab2fad2e08972b8cfde01d4979083fb6d15e8e66f8ab1', '25','0'],
['0x27d32a3033df4277caa9e9396100b7ca8c66a4ef8ea5f6765b91a7c17f0109c', '56','0'],
['0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', '26','0'],
['0x53c615080d35defd55569488bc48c1a91d82f2d2ce6199463e095b4a4ead551', '56','0'],
];
const tree = Merkle.StarknetMerkleTree.create(airdrop, Merkle.HashType.Poseidon);
```
### 🫚 Get the root of the tree :
```typescript
const root = tree.root;
```

### 🎰 Create a Merkle proof :
```typescript
const inp = 3; // Nth leaf of the input list
// or
const inp = ['0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', '26','0']; // leaf content

const proof = tree.getProof(inp);
```

### Hash a leaf :
```typescript
const inp = 3; // Nth leaf of the input list
const inpData = tree.getInputData(inp);
const leafHash = Merkle.StarknetMerkleTree.leafHash(inpData, Merkle.HashType.Poseidon);
```
### 🔎 Verify a proof with Starknet.js :
### 🔎 Verify a proof with your JS/TS script :
```typescript
const inp = 3; // Nth leaf of the input list
const inpData = tree.getInputData(inp);
const isValid = tree.verify(inpData, proof);
```

### 🔎 Verify a proof in the Starknet blockchain :

A dedicated smart-contract is created, including only one value of the tree : the root. You send a proof (few numbers) to this contract, and it can say if an address is included in the tree or not. Just by storing a felt252 in Starknet, you can check that an address is included in a list of thousand of addresses, and trigger a distribution of token to this address.
> A Cairo 1 smart-contract is in preparation and will be released soon. It will show how to verify a Merkle proof in Starknet. Stay tune ; just need a couple of weeks to release it.
You have to deploy a new instance of an existing smart-contract. There is one contract dedicated for Pedersen hash, and one other for Poseidon hash. These contracts are already declared in Starknet Mainnet, Goerli Testnet and Sepolia Testnet, with the following class hashes :

| Tree hash | Class hash |
| :---: | :--- |
| **Pedersen class hash** | Soon |
| **Poseidon class hash** | `0x03e2efc98f902c0b33eee6c3daa97b941912bcab61b6162884380c682e594eaf`|

So, you will not have to pay fees to declare this part of the airdrop code ; it's already made.
You have to deploy this contract (called here contract 1) with only one parameter in the constructor : the tree root.

You have to create/declare/deploy your dedicated smart-contract (called here contract 2) to handle the Airdrop (list of already performed airdrops, distribution of tokens, timing, administration, etc..).
This contract 2 has to call the contract 1 to verify if the data are correct and are part of the Merkle tree.
Contract 1 is able to say if an address and the corresponding data are included in the tree or not. Just by storing a felt252 in Starknet, you can check that an address is included in a list of thousand of addresses, and trigger a distribution of token to this address.

You can find a documentation of this contract 1 [here](./cairo/merkleTreeVerify.md).

> Additional scripts will be released soon.
> Some demo Typescript files are available [here](./typescript).
> A demo DAPP for an Airdrop is in preparation, and will be listed here.
> A demo DAPP for an Airdrop is in preparation, and you will find the link here as soon as released.
## API :

Expand Down Expand Up @@ -103,17 +127,24 @@ Returns a proof for the Nth value in the tree. Indices refer to the position of
```typescript
const proof1 = tree.getProof(3);
const proof2 = tree.getProof(["0x43af5", '100']);
// result =
[
'0x40a6dba21b22596e979a1555a278ca58c11b5cd5e46f5801c1af8c4ab518845',
'0x7957d036cf1e60858a601df12e0fb2921114d4b5facccf638163e0bb2be3c34',
'0x12677ed42d2f73c92413c30d04d0b88e771bf2595c7060df46f095f2132eca2'
]
```

### verify() :
Returns a boolean that is `true` when the proof verifies that the value is contained in the tree.
```typescript
const result1 = tree.verify(3, proof);
const result2 = tree.verify(["0x34e67d", '100'], proof);
// result = true
```

### dump() :
Returns a description of the merkle tree for distribution. It contains all the necessary information to reproduce the tree, find the relevant leaves, and generate proofs. You should distribute this to users in a web application so they can generate proofs for their leaves of interest.
Returns a description of the Merkle tree for distribution. It contains all the necessary information to reproduce the tree, find the relevant leaves, and generate proofs. You should distribute this to users in a web application so they can generate proofs for some leaves.
```typescript
fs.writeFileSync('data/treeTestPoseidon.json', JSON.stringify(tree.dump(),undefined,2));
```
Expand All @@ -136,12 +167,23 @@ tree.validate();
The root of the tree is a commitment on the values of the tree. It can be published in a smart contract, to later prove that its values are part of the tree.
```typescript
console.log(tree.root);
// result = 0x4bad3f80e8041eb3d32432fa4aed9f904db8c8ab34109879a99da696a0c5a81
```

### render() :
Returns a visual representation of the tree that can be useful for debugging.
```typescript
console.log(tree.render());
// result =
0) 0x4bad3f80e8041eb3d32432fa4aed9f904db8c8ab34109879a99da696a0c5a81
├─ 1) 0x4f9ffba9cb60723ecb53299f6b2359a9d32a1aa316ffcf83022c58d822abc55
│ ├─ 3) 0x1cd0fa9d323f2de54979140bab80cb8077ac24e098c685da5ac6a4d9a17c25c
│ │ ├─ 7) 0x6e5bfc0a35b74af4395c2a60a7735c0f0cbcfba515e91d4edd3f7ea70287cbc
│ │ └─ 8) 0x40a6dba21b22596e979a1555a278ca58c11b5cd5e46f5801c1af8c4ab518845
│ └─ 4) 0x7957d036cf1e60858a601df12e0fb2921114d4b5facccf638163e0bb2be3c34
└─ 2) 0x12677ed42d2f73c92413c30d04d0b88e771bf2595c7060df46f095f2132eca2
├─ 5) 0x77dc74ab2217383b4c2a772e491f8177277af576fd426f8c59f9c64d7ef258b
└─ 6) 0x707142fb4ad00584910740c7d8207669b429cb93ce1985870b5fa5096ced91c
```

### getInputData() :
Expand All @@ -155,8 +197,9 @@ const data= tree.getInputData(3);

Hash a leaf. Returns an hex string.
```typescript
const leaf: InputForMerkle = ['0x27d32a3033df4277caa9e9396100b7ca8c66a4ef8ea5f6765b91a7c17f0109c', '56','0'];
const leaf: InputForMerkle = ['0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a', '26', '0'];
const hashedLeaf: string = Merkle.hashDataToHex(leaf, Merkle.HashType.Pedersen);
// result = 0x6e5bfc0a35b74af4395c2a60a7735c0f0cbcfba515e91d4edd3f7ea70287cbc
```
> Identical to `Merkle.StarknetMerkleTree.leafHash()` .
> `Merkle.hashDataToBigint()` is similar, with a `bigint` result.
Expand All @@ -173,7 +216,7 @@ const hash: bigint = Merkle.computePoseidonHashOnElements(["0x10e", "0xc4", "0x1
Calculate the hash of 2 bigint.

```typescript
const hash: bigint = Merkle.hashPair(200n, 300n,Merkle.HashType.Pedersen);
const hash: bigint = Merkle.hashPair(200n, 300n, Merkle.HashType.Pedersen);
```

## ⚖️ License :
Expand Down
100 changes: 100 additions & 0 deletions cairo/airdrop_poseidon.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Example of Airdrop contract
// Coded with Cairo 2.4.0
// contract not audited ; use at your own risks.

use starknet::ContractAddress;

#[starknet::interface]
trait IMerkleVerify<TContractState> {
fn get_merkle_address(self: @TContractState) -> ContractAddress;
fn get_time(self: @TContractState) -> u64;
fn is_address_airdropped(self: @TContractState, address: ContractAddress) -> bool;
fn request_airdrop(
ref self: TContractState, address: ContractAddress, amount: u256, proof: Array<felt252>
);
}

#[starknet::contract]
mod merkle_verify {
use core::option::OptionTrait;
use super::IMerkleVerify;
use starknet::{ContractAddress, SyscallResultTrait, contract_address_const};
use starknet::get_block_timestamp;
use core::poseidon::poseidon_hash_span;
use core::hash::HashStateExTrait;
use poseidon::{PoseidonTrait, HashState};
use hash::{HashStateTrait, Hash};
use array::{ArrayTrait, SpanTrait};

#[storage]
struct Storage {
erc20_address: ContractAddress,
start_time: u64,
merkle_address: ContractAddress,
merkle_root: felt252,
airdrop_performed: LegacyMap::<ContractAddress, bool>,
}

#[constructor]
fn constructor(
ref self: ContractState,
erc20_address: ContractAddress,
merkle_address: ContractAddress,
start_time: u64,
) {
self.erc20_address.write(erc20_address);
self.merkle_address.write(merkle_address);
self.start_time.write(start_time);
}

#[external(v0)]
impl MerkleVerifyContract of super::IMerkleVerify<ContractState> {
// returns the address of the merkle verify contract for this airdrop
fn get_merkle_address(self: @ContractState) -> ContractAddress {
self.merkle_address.read()
}

// returns the time of start of the airdrop
fn get_time(self: @ContractState) -> u64 {
get_block_timestamp()
}

fn is_address_airdropped(self: @ContractState, address: ContractAddress) -> bool {
self.airdrop_performed.read(address)
}

fn request_airdrop(
ref self: ContractState, address: ContractAddress, amount: u256, proof: Array<felt252>
) {
let already_airdropped: bool = self.airdrop_performed.read(address);
assert(!already_airdropped, 'Address already airdropped');
let current_time: u64 = get_block_timestamp();
let airdrop_start_time: u64 = self.start_time.read();
assert(current_time >= airdrop_start_time, 'Airdrop has not started yet.');
let mut call_data: Array<felt252> = ArrayTrait::new();
call_data.append(address.into());
call_data.append(amount.low.into());
call_data.append(amount.high.into());
Serde::serialize(@proof, ref call_data);
let mut is_leave_valid = starknet::call_contract_syscall(
self.merkle_address.read(), selector!("verify_from_leaf_airdrop"), call_data.span()
)
.unwrap_syscall();
let mut is_request_valid: bool = Serde::<bool>::deserialize(ref is_leave_valid)
.unwrap();

assert(is_request_valid, 'Proof not valid.'); // revert if not valid

// Airdrop
// Register the address as already airdropped
self.airdrop_performed.write(address, true);
// to be sure to perform the airdrop only once per address.

// Perform here your transfer of token.

// if needed, create some events.

return ();
}
}
}
Loading

0 comments on commit dc0988f

Please sign in to comment.