Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Merkle Airdrop Support for Initial Token Distribution #73

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ starknet = "2.3.1"

# External dependencies
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.0" }
alexandria_merkle_tree = { git = "https://github.com/keep-starknet-strange/alexandria" }

[[target.starknet-contract]]
sierra = true
Expand Down
59 changes: 59 additions & 0 deletions contracts/scripts/merkletree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { merkle, ec } from "starknet";
//To run this script -> npx ts-node ./merkletree.ts

// Create an empty array to store the hashes
const addressAmountPairs = [
{
address:
"0x0437Fce03E7fAcd55Df8d3B2774D21eAf3bA1ECd0e7043a6DC4E743D408d8D80",
amount: BigInt(100),
},
{
address:
"0x02038e178565b977c99f3e6c8d4ba327356e1b279e84cdc2f1949022c91653bd",
amount: BigInt(500),
},
// Add more pairs as needed
];

// An array to store the hash results as strings
const hashes: string[] = [];

// Iterate through the address-amount pairs and calculate the hashes
for (const pair of addressAmountPairs) {
const address = pair.address;
const amount = pair.amount;

// Calculate the Pedersen hash and push it to the 'hashes' array
const hashResult = ec.starkCurve.pedersen(address, amount);
hashes.push(hashResult);
}

// Now 'hashes' contains the Pedersen hashes for each pair
console.log(hashes, " These are the hashes");

// Creating a Merkle tree with these addresses

const merkleTree = new merkle.MerkleTree(hashes);

console.log("Merkle Tree Root --->:", merkleTree.root);
//0x4c5b879125d0fe0e0359dc87eea9c7370756635ca87c59148fb313c2cfb0579 - Produced merkle root for the above

const addressToProve = hashes[1]; // For example, the first address in the list
console.log("Leaf:", addressToProve);
//0x57d0e61d7b5849581495af551721710023a83e705710c58facfa3f4e36e8fac

// Get the Merkle proof for this address
const proof = merkleTree.getProof(addressToProve);
//0x3bf438e95d7428d14eb4270528ff8b1e2f9cb30113724626d5cf9943551ee4d

console.log("Merkle Proof:", proof);

const isPartOfTree = merkle.proofMerklePath(
merkleTree.root,
addressToProve,
proof
);

console.log("Is address part of the tree with the given root?", isPartOfTree);
//yes
10 changes: 10 additions & 0 deletions contracts/src/tokens/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ trait IUnruggableMemecoin<TState> {
fn launched(self: @TState) -> bool;
fn launch_memecoin(ref self: TState);
fn get_team_allocation(self: @TState) -> u256;
fn set_merkle_root(ref self: TState, merkle_root: felt252);
fn get_merkle_root(self: @TState) -> felt252;
fn claim_airdrop(
ref self: TState, to: ContractAddress, amount: u256, leaf: felt252, proof: Span<felt252>,
);
}

#[starknet::interface]
Expand Down Expand Up @@ -75,4 +80,9 @@ trait IUnruggableAdditional<TState> {
fn launched(self: @TState) -> bool;
fn launch_memecoin(ref self: TState);
fn get_team_allocation(self: @TState) -> u256;
fn set_merkle_root(ref self: TState, merkle_root: felt252);
fn get_merkle_root(self: @TState) -> felt252;
fn claim_airdrop(
ref self: TState, to: ContractAddress, amount: u256, leaf: felt252, proof: Span<felt252>,
);
}
92 changes: 90 additions & 2 deletions contracts/src/tokens/memecoin.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ mod UnruggableMemecoin {
IUnruggableMemecoinSnake, IUnruggableMemecoinCamel, IUnruggableAdditional
};
use zeroable::Zeroable;
use alexandria_merkle_tree::merkle_tree::{
Hasher, MerkleTree, pedersen::PedersenHasherImpl, MerkleTreeTrait, MerkleTreeImpl
Akashneelesh marked this conversation as resolved.
Show resolved Hide resolved
};
use openzeppelin::security::initializable::InitializableComponent::InternalTrait as InitializableTrait;
use openzeppelin::security::initializable::InitializableComponent;

// Components.
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

component!(path: ERC20Component, storage: erc20, event: ERC20Event);

component!(path: InitializableComponent, storage: initializable, event: InitializableEvent);
// Internals
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;

Expand Down Expand Up @@ -46,7 +53,12 @@ mod UnruggableMemecoin {
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
erc20: ERC20Component::Storage
erc20: ERC20Component::Storage,
#[substorage(v0)]
initializable: InitializableComponent::Storage,
//Contract Storage
merkle_root: felt252,
has_claimed: LegacyMap::<ContractAddress, bool>,
}

#[event]
Expand All @@ -55,14 +67,24 @@ mod UnruggableMemecoin {
#[flat]
OwnableEvent: OwnableComponent::Event,
#[flat]
ERC20Event: ERC20Component::Event
ERC20Event: ERC20Component::Event,
#[flat]
InitializableEvent: InitializableComponent::Event,
//Contract Events
ClaimedAirdrop: ClaimedAirdrop,
}

mod Errors {
const MAX_HOLDERS_REACHED: felt252 = 'Unruggable: max holders reached';
const ARRAYS_LEN_DIF: felt252 = 'Unruggable: arrays len dif';
}

#[derive(Drop, starknet::Event)]
struct ClaimedAirdrop {
account: ContractAddress,
amount: u256
}


/// Constructor called once when the contract is deployed.
/// # Arguments
Expand Down Expand Up @@ -137,6 +159,72 @@ mod UnruggableMemecoin {
* MAX_SUPPLY_PERCENTAGE_TEAM_ALLOCATION.into()
/ 100
}

/// Sets the Merkle root for the contract.
/// This function updates the Merkle root stored in the contract's state.
/// It is essential for maintaining the integrity of the Merkle tree used in various contract functionalities.

/// # Arguments
/// * `merkle_root` - The new Merkle root to be set, represented as a `felt252`.

fn set_merkle_root(ref self: ContractState, merkle_root: felt252) {
Akashneelesh marked this conversation as resolved.
Show resolved Hide resolved
self.ownable.assert_only_owner();
self.initializable.initialize();
self.merkle_root.write(merkle_root);
}

/// Retrieves the current Merkle root from the contract.
/// This function allows the contract owner to obtain the current Merkle root stored in the contract's state.
/// The Merkle root is crucial for verifying proofs in various contract operations.

/// # Returns
/// * `felt252` - The current Merkle root stored in the contract.

fn get_merkle_root(self: @ContractState) -> felt252 {
//Getting the merkle root
self.ownable.assert_only_owner();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there an access control on this view?

self.merkle_root.read()
}

/// Claims an airdrop for a specific account.
/// This function is part of the contract's state and is used to claim airdrops for accounts.
/// It involves a Merkle tree verification process to ensure the legitimacy of the claim.

/// # Arguments
/// * `to` - The address of the contract for which the airdrop is being claimed.
/// * `amount` - The amount of tokens to be airdropped, represented as a `u256`.
/// * `leaf` - A mutable leaf node in the Merkle tree, represented as a `felt252`.
/// * `proof` - A mutable span of `felt252` elements representing the Merkle proof.
fn claim_airdrop(
ref self: ContractState,
to: ContractAddress,
amount: u256,
mut leaf: felt252,
mut proof: Span<felt252>,
) {
//Initializing the Merkletree
let mut merkle_tree: MerkleTree<Hasher> = MerkleTreeTrait::new();
//Pedersen Hashing of the ContractAddress and Amount
let to_felt252: felt252 = starknet::contract_address_to_felt252(to);
Akashneelesh marked this conversation as resolved.
Show resolved Hide resolved
let amount_felt252: felt252 = amount.try_into().unwrap();
Akashneelesh marked this conversation as resolved.
Show resolved Hide resolved
let hashed_value: felt252 = pedersen::pedersen(to_felt252, amount_felt252);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contractAddress fits into felt252, but not amount. Here you're performing a hash, not on the original data, but on a truncating value. why not hashing the real amount (on 256 bits)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah true, I had casted amount to felt252 because, the pedersen::pedersend requires (felt252,felt252) as inputs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should hash the full amount 🤔 I will look into alexandria's merkle tree implementation but I think it can be improved 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo you should

let mut hash_state = pedersen(to_felt252, amount.low.into())
hash_state = pedersen(hash_state, amount.high.into())

that way you are hashing the correct data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh fine, I'll do this change


//Verifying if the leaf and hashed value are equal
assert(hashed_value == leaf, 'Invalid leaf');

//Verifying the proof
let valid_proof: bool = merkle_tree.verify(self.merkle_root.read(), leaf, proof);
assert(self.has_claimed.read(to) == false, 'Already Claimed');
assert(valid_proof == true, 'Invalid proof');

//Changing the has_claimed state to true
self.has_claimed.write(to, true);

//Minting the tokens
self.erc20._mint(to, amount);
//Emitting an event of ClaimedAirdrop
self.emit(ClaimedAirdrop { account: to, amount: amount });
}
}

#[abi(embed_v0)]
Expand Down
Loading
Loading