diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index f3cdbdfb..6ac58708 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -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 diff --git a/contracts/scripts/merkletree.ts b/contracts/scripts/merkletree.ts new file mode 100644 index 00000000..e0a267d6 --- /dev/null +++ b/contracts/scripts/merkletree.ts @@ -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 diff --git a/contracts/src/tokens/interface.cairo b/contracts/src/tokens/interface.cairo index fb02f9ca..2dfa10d0 100644 --- a/contracts/src/tokens/interface.cairo +++ b/contracts/src/tokens/interface.cairo @@ -43,6 +43,11 @@ trait IUnruggableMemecoin { 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, + ); } #[starknet::interface] @@ -75,4 +80,9 @@ trait IUnruggableAdditional { 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, + ); } diff --git a/contracts/src/tokens/memecoin.cairo b/contracts/src/tokens/memecoin.cairo index 577436f1..f7d2cd96 100644 --- a/contracts/src/tokens/memecoin.cairo +++ b/contracts/src/tokens/memecoin.cairo @@ -13,12 +13,19 @@ mod UnruggableMemecoin { IUnruggableMemecoinSnake, IUnruggableMemecoinCamel, IUnruggableAdditional }; use zeroable::Zeroable; + use alexandria_merkle_tree::merkle_tree::{ + Hasher, MerkleTree, pedersen::PedersenHasherImpl, MerkleTreeTrait, MerkleTreeImpl + }; + 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; component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); // Internals impl ERC20InternalImpl = ERC20Component::InternalImpl; @@ -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::, } #[event] @@ -55,7 +67,11 @@ mod UnruggableMemecoin { #[flat] OwnableEvent: OwnableComponent::Event, #[flat] - ERC20Event: ERC20Component::Event + ERC20Event: ERC20Component::Event, + #[flat] + InitializableEvent: InitializableComponent::Event, + //Contract Events + ClaimedAirdrop: ClaimedAirdrop, } mod Errors { @@ -63,6 +79,12 @@ mod UnruggableMemecoin { 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 @@ -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) { + 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(); + 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, + ) { + //Initializing the Merkletree + let mut merkle_tree: MerkleTree = MerkleTreeTrait::new(); + //Pedersen Hashing of the ContractAddress and Amount + let to_felt252: felt252 = starknet::contract_address_to_felt252(to); + let amount_felt252: felt252 = amount.try_into().unwrap(); + let hashed_value: felt252 = pedersen::pedersen(to_felt252, amount_felt252); + + //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)] diff --git a/contracts/tests/test_unruggable_memecoin.cairo b/contracts/tests/test_unruggable_memecoin.cairo index 6dc1724f..97ab3c29 100644 --- a/contracts/tests/test_unruggable_memecoin.cairo +++ b/contracts/tests/test_unruggable_memecoin.cairo @@ -1221,4 +1221,249 @@ mod memecoin_internals { index += 1; }; } + #[test] + fn test_set_merkle_root() { + let ( + owner, + recipient, + name, + symbol, + initial_supply, + initial_holder_1, + initial_holder_2, + _, + initial_holders_amounts + ) = + instantiate_params(); + let alice = contract_address_const::<53>(); + let initial_holders = array![owner, initial_holder_1, initial_holder_2].span(); + + let contract_address = + match deploy_contract( + owner, owner, name, symbol, initial_supply, initial_holders, initial_holders_amounts + ) { + Result::Ok(address) => address, + Result::Err(msg) => panic(msg.panic_data), + }; + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + start_prank(CheatTarget::One(memecoin.contract_address), owner); + memecoin.set_merkle_root(0x3022e5c16bd1bf6e9c44b0d0de23ef6eb0bc84bd2b4eaca75306076eb99239a); + + assert( + memecoin + .get_merkle_root() == 0x3022e5c16bd1bf6e9c44b0d0de23ef6eb0bc84bd2b4eaca75306076eb99239a, + 'Incorrect Merkle Root' + ) + } + + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_set_merkle_root_not_owner() { + let ( + owner, + recipient, + name, + symbol, + initial_supply, + initial_holder_1, + initial_holder_2, + _, + initial_holders_amounts + ) = + instantiate_params(); + let alice = contract_address_const::<53>(); + let initial_holders = array![owner, initial_holder_1, initial_holder_2].span(); + + let contract_address = + match deploy_contract( + owner, owner, name, symbol, initial_supply, initial_holders, initial_holders_amounts + ) { + Result::Ok(address) => address, + Result::Err(msg) => panic(msg.panic_data), + }; + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + memecoin.set_merkle_root(0x4c5b879125d0fe0e0359dc87eea9c7370756635ca87c59148fb313c2cfb0579); + } + + #[test] + #[should_panic(expected: ('Initializable: is initialized',))] + fn test_set_merkle_root_initialized() { + let ( + owner, + recipient, + name, + symbol, + initial_supply, + initial_holder_1, + initial_holder_2, + _, + initial_holders_amounts + ) = + instantiate_params(); + let alice = contract_address_const::<53>(); + let initial_holders = array![owner, initial_holder_1, initial_holder_2].span(); + + let contract_address = + match deploy_contract( + owner, owner, name, symbol, initial_supply, initial_holders, initial_holders_amounts + ) { + Result::Ok(address) => address, + Result::Err(msg) => panic(msg.panic_data), + }; + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + start_prank(CheatTarget::One(memecoin.contract_address), owner); + + memecoin.set_merkle_root(0x4c5b879125d0fe0e0359dc87eea9c7370756635ca87c59148fb313c2cfb0579); + memecoin.set_merkle_root(0x4c5b879125d0fe0e0359dc87eea9c7370756635ca87c59148fb313c2cfb0579); + } + + #[test] + fn test_claimairdrop_memecoin() { + let ( + owner, + recipient, + name, + symbol, + initial_supply, + initial_holder_1, + initial_holder_2, + _, + initial_holders_amounts + ) = + instantiate_params(); + let alice = contract_address_const::<53>(); + let initial_holders = array![owner, initial_holder_1, initial_holder_2].span(); + + let contract_address = + match deploy_contract( + owner, owner, name, symbol, initial_supply, initial_holders, initial_holders_amounts + ) { + Result::Ok(address) => address, + Result::Err(msg) => panic(msg.panic_data), + }; + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + start_prank(CheatTarget::One(memecoin.contract_address), owner); + + //These values have been generated through the merkle.ts script + memecoin.set_merkle_root(0x4c5b879125d0fe0e0359dc87eea9c7370756635ca87c59148fb313c2cfb0579); + + let to = contract_address_const::< + 0x02038e178565b977c99f3e6c8d4ba327356e1b279e84cdc2f1949022c91653bd + >(); + let amount = 500_u256; + + let leaf = 0x57d0e61d7b5849581495af551721710023a83e705710c58facfa3f4e36e8fac; + let valid_proof = array![0x3bf438e95d7428d14eb4270528ff8b1e2f9cb30113724626d5cf9943551ee4d] + .span(); + + memecoin.claim_airdrop(to, amount, leaf, valid_proof); + + let balance = memecoin.balanceOf(to); + assert(balance == amount, 'No balance'); + } + + #[test] + #[should_panic(expected: ('Already Claimed',))] + fn test_claimairdrop_memecoin_should_fail() { //Already Claimed + let ( + owner, + recipient, + name, + symbol, + initial_supply, + initial_holder_1, + initial_holder_2, + _, + initial_holders_amounts + ) = + instantiate_params(); + let alice = contract_address_const::<53>(); + let initial_holders = array![owner, initial_holder_1, initial_holder_2].span(); + + let contract_address = + match deploy_contract( + owner, owner, name, symbol, initial_supply, initial_holders, initial_holders_amounts + ) { + Result::Ok(address) => address, + Result::Err(msg) => panic(msg.panic_data), + }; + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + start_prank(CheatTarget::One(memecoin.contract_address), owner); + + //These values have been generated through the merkle.ts script + memecoin.set_merkle_root(0x4c5b879125d0fe0e0359dc87eea9c7370756635ca87c59148fb313c2cfb0579); + + let to = contract_address_const::< + 0x02038e178565b977c99f3e6c8d4ba327356e1b279e84cdc2f1949022c91653bd + >(); + let amount = 500_u256; + + let leaf = 0x57d0e61d7b5849581495af551721710023a83e705710c58facfa3f4e36e8fac; + let valid_proof = array![0x3bf438e95d7428d14eb4270528ff8b1e2f9cb30113724626d5cf9943551ee4d] + .span(); + + memecoin.claim_airdrop(to, amount, leaf, valid_proof); + + let balance = memecoin.balanceOf(to); + assert(balance == amount, 'No balance'); + + memecoin.claim_airdrop(to, amount, leaf, valid_proof); + } + + #[test] + #[should_panic(expected: ('Invalid proof',))] + fn test_claimairdrop_memecoin_should_fail_2() { //Invalid Proof + let ( + owner, + recipient, + name, + symbol, + initial_supply, + initial_holder_1, + initial_holder_2, + _, + initial_holders_amounts + ) = + instantiate_params(); + let alice = contract_address_const::<53>(); + let initial_holders = array![owner, initial_holder_1, initial_holder_2].span(); + + let contract_address = + match deploy_contract( + owner, owner, name, symbol, initial_supply, initial_holders, initial_holders_amounts + ) { + Result::Ok(address) => address, + Result::Err(msg) => panic(msg.panic_data), + }; + + let memecoin = IUnruggableMemecoinDispatcher { contract_address }; + + start_prank(CheatTarget::One(memecoin.contract_address), owner); + + //These values have been generated through the merkle.ts script + memecoin.set_merkle_root(0x3022e5c16bd1bf6e9c44b0d0de23ef6eb0bc84bd2b4eaca75306076eb99239a); + + let to = contract_address_const::< + 0x02038e178565b977c99f3e6c8d4ba327356e1b279e84cdc2f1949022c91653bd + >(); + let amount = 500_u256; + + let leaf = 0x57d0e61d7b5849581495af551721710023a83e705710c58facfa3f4e36e8fac; + let valid_proof = array![0x3bf438e95d7428d14eb4270528ff8b1e2f9cb30113724626d5cf9943551ee4d] + .span(); + + memecoin.claim_airdrop(to, amount, leaf, valid_proof); + + let balance = memecoin.balanceOf(to); + assert(balance == amount, 'No balance'); + } }