Skip to content

Commit

Permalink
create a contract for deploying the set of contracts all together (#18)
Browse files Browse the repository at this point in the history
* create a contract for deploying the set of contracts all together

* optionally deploy an airdrop contract

* finish the deployment

* more tests

* add some methods to the airdrop and test some attributes of the airdrop in the factory

* transfer tokens to the airdrop
  • Loading branch information
moodysalem authored Nov 7, 2023
1 parent 9732cba commit b09f6d1
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 57 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Even the token contract can be migrated, if necessary, by deploying a new contra
- Compute a merkle root by computing a list of amounts and recipients, hashing them, and arranging them into a merkle binary tree
- Deploy the airdrop with the root and the token address
- Transfer the total amount of tokens to the `Airdrop` contract
- `Factory` allows creating the entire set of contracts with one call

## Testing

Expand Down
26 changes: 24 additions & 2 deletions src/airdrop.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use governance::interfaces::erc20::{IERC20Dispatcher};
use starknet::{ContractAddress};
use array::{Array};

Expand All @@ -11,12 +12,21 @@ struct Claim {
trait IAirdrop<TStorage> {
// Claims the given allotment of tokens
fn claim(ref self: TStorage, claim: Claim, proof: Array::<felt252>);

// Return the root of the airdrop
fn get_root(self: @TStorage) -> felt252;

// Return the token being dropped
fn get_token(self: @TStorage) -> IERC20Dispatcher;

// Return whether the claim has been claimed (always false for invalid claims)
fn is_claimed(self: @TStorage, claim: Claim) -> bool;
}

#[starknet::contract]
mod Airdrop {
use super::{IAirdrop, ContractAddress, Claim};
use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use super::{IAirdrop, ContractAddress, Claim, IERC20Dispatcher};
use governance::interfaces::erc20::{IERC20DispatcherTrait};
use hash::{LegacyHash};
use array::{ArrayTrait, SpanTrait};
use starknet::{ContractAddressIntoFelt252};
Expand Down Expand Up @@ -81,5 +91,17 @@ mod Airdrop {

self.emit(Claimed { claim });
}

fn get_root(self: @ContractState) -> felt252 {
self.root.read()
}

fn get_token(self: @ContractState) -> IERC20Dispatcher {
self.token.read()
}

fn is_claimed(self: @ContractState, claim: Claim) -> bool {
self.claimed.read(LegacyHash::hash(0, claim))
}
}
}
156 changes: 156 additions & 0 deletions src/factory.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use starknet::{ContractAddress};
use governance::governor::{Config as GovernorConfig};
use governance::governor::{IGovernorDispatcher};
use governance::governance_token::{IGovernanceTokenDispatcher};
use governance::airdrop::{IAirdropDispatcher};
use governance::timelock::{ITimelockDispatcher};

#[derive(Copy, Drop, Serde)]
struct AirdropConfig {
root: felt252,
total: u128,
}

#[derive(Copy, Drop, Serde)]
struct TimelockConfig {
delay: u64,
window: u64,
}

#[derive(Copy, Drop, Serde)]
struct DeploymentParameters {
name: felt252,
symbol: felt252,
total_supply: u128,
governor_config: GovernorConfig,
timelock_config: TimelockConfig,
airdrop_config: Option<AirdropConfig>,
}

#[derive(Copy, Drop, Serde)]
struct DeploymentResult {
token: IGovernanceTokenDispatcher,
governor: IGovernorDispatcher,
timelock: ITimelockDispatcher,
airdrop: Option<IAirdropDispatcher>,
}

// This contract makes it easy to deploy a set of governance contracts from a block explorer just by specifying parameters
#[starknet::interface]
trait IFactory<TContractState> {
fn deploy(self: @TContractState, params: DeploymentParameters) -> DeploymentResult;
}

#[starknet::contract]
mod Factory {
use super::{
IFactory, DeploymentParameters, DeploymentResult, ContractAddress,
IGovernanceTokenDispatcher, IAirdropDispatcher, IGovernorDispatcher, ITimelockDispatcher
};
use core::result::{ResultTrait};
use starknet::{ClassHash, deploy_syscall, get_caller_address};
use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};

#[storage]
struct Storage {
governance_token: ClassHash,
airdrop: ClassHash,
governor: ClassHash,
timelock: ClassHash,
}

#[constructor]
fn constructor(
ref self: ContractState,
governance_token: ClassHash,
airdrop: ClassHash,
governor: ClassHash,
timelock: ClassHash
) {
self.governance_token.write(governance_token);
self.airdrop.write(airdrop);
self.governor.write(governor);
self.timelock.write(timelock);
}

#[external(v0)]
impl FactoryImpl of IFactory<ContractState> {
fn deploy(self: @ContractState, params: DeploymentParameters) -> DeploymentResult {
let mut token_constructor_args: Array<felt252> = ArrayTrait::new();
Serde::serialize(
@(params.name, params.symbol, params.total_supply), ref token_constructor_args
);

let (token_address, _) = deploy_syscall(
class_hash: self.governance_token.read(),
contract_address_salt: 0,
calldata: token_constructor_args.span(),
deploy_from_zero: false,
)
.unwrap();

let erc20 = IERC20Dispatcher { contract_address: token_address };

let mut governor_constructor_args: Array<felt252> = ArrayTrait::new();
Serde::serialize(
@(token_address, params.governor_config), ref governor_constructor_args
);

let (governor_address, _) = deploy_syscall(
class_hash: self.governor.read(),
contract_address_salt: 0,
calldata: governor_constructor_args.span(),
deploy_from_zero: false,
)
.unwrap();

let (airdrop, remaining_amount) = match params.airdrop_config {
Option::Some(config) => {
let mut airdrop_constructor_args: Array<felt252> = ArrayTrait::new();
Serde::serialize(@(token_address, config.root), ref airdrop_constructor_args);

let (airdrop_address, _) = deploy_syscall(
class_hash: self.airdrop.read(),
contract_address_salt: 0,
calldata: airdrop_constructor_args.span(),
deploy_from_zero: false,
)
.unwrap();

assert(config.total <= params.total_supply, 'AIRDROP_GT_SUPPLY');

erc20.transfer(airdrop_address, config.total.into());

(
Option::Some(IAirdropDispatcher { contract_address: airdrop_address }),
params.total_supply - config.total
)
},
Option::None => { (Option::None, params.total_supply) }
};

erc20.transfer(get_caller_address(), remaining_amount.into());

let mut timelock_constructor_args: Array<felt252> = ArrayTrait::new();
Serde::serialize(
@(governor_address, params.timelock_config.delay, params.timelock_config.window),
ref timelock_constructor_args
);

let (timelock_address, _) = deploy_syscall(
class_hash: self.timelock.read(),
contract_address_salt: 0,
calldata: timelock_constructor_args.span(),
deploy_from_zero: false,
)
.unwrap();

DeploymentResult {
token: IGovernanceTokenDispatcher { contract_address: token_address },
airdrop,
governor: IGovernorDispatcher { contract_address: governor_address },
timelock: ITimelockDispatcher { contract_address: timelock_address }
}
}
}
}
104 changes: 104 additions & 0 deletions src/factory_test.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use array::{ArrayTrait};
use debug::PrintTrait;
use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use governance::governor::{Config as GovernorConfig};
use governance::factory::{
IFactoryDispatcher, IFactoryDispatcherTrait, Factory, DeploymentParameters, AirdropConfig,
TimelockConfig,
};
use governance::governance_token::{GovernanceToken};
use governance::governor::{Governor};
use governance::timelock::{Timelock};
use governance::airdrop::{Airdrop};
use starknet::{
get_contract_address, deploy_syscall, ClassHash, contract_address_const, ContractAddress,
};
use starknet::class_hash::{Felt252TryIntoClassHash};
use starknet::testing::{set_contract_address, set_block_timestamp, pop_log};
use traits::{TryInto};

use governance::governor::{IGovernorDispatcherTrait};
use governance::governance_token::{IGovernanceTokenDispatcherTrait};
use governance::airdrop::{IAirdropDispatcherTrait};
use governance::timelock::{ITimelockDispatcherTrait};

use result::{Result, ResultTrait};
use option::{OptionTrait};

fn deploy() -> IFactoryDispatcher {
let mut constructor_args: Array<felt252> = ArrayTrait::new();
Serde::serialize(
@(
GovernanceToken::TEST_CLASS_HASH,
Airdrop::TEST_CLASS_HASH,
Governor::TEST_CLASS_HASH,
Timelock::TEST_CLASS_HASH
),
ref constructor_args
);

let (address, _) = deploy_syscall(
class_hash: Factory::TEST_CLASS_HASH.try_into().unwrap(),
contract_address_salt: 0,
calldata: constructor_args.span(),
deploy_from_zero: true
)
.expect('DEPLOY_FAILED');
return IFactoryDispatcher { contract_address: address };
}


#[test]
#[available_gas(30000000)]
fn test_deploy() {
let factory = deploy();

let result = factory
.deploy(
DeploymentParameters {
name: 'token',
symbol: 'tk',
total_supply: 5678,
airdrop_config: Option::Some(AirdropConfig { root: 'root', total: 1111 }),
governor_config: GovernorConfig {
voting_start_delay: 0,
voting_period: 180,
voting_weight_smoothing_duration: 30,
quorum: 1000,
proposal_creation_threshold: 100,
},
timelock_config: TimelockConfig { delay: 320, window: 60, }
}
);

let erc20 = IERC20Dispatcher { contract_address: result.token.contract_address };

assert(erc20.name() == 'token', 'name');
assert(erc20.symbol() == 'tk', 'symbol');
assert(erc20.decimals() == 18, 'decimals');
assert(erc20.totalSupply() == 5678, 'totalSupply');
assert(erc20.balance_of(get_contract_address()) == 5678 - 1111, 'deployer balance');
assert(erc20.balance_of(result.airdrop.unwrap().contract_address) == 1111, 'airdrop balance');

let drop = result.airdrop.unwrap();
assert(drop.get_root() == 'root', 'airdrop root');
assert(drop.get_token().contract_address == result.token.contract_address, 'airdrop token');

assert(
result.governor.get_voting_token().contract_address == result.token.contract_address,
'voting_token'
);
assert(
result
.governor
.get_config() == GovernorConfig {
voting_start_delay: 0,
voting_period: 180,
voting_weight_smoothing_duration: 30,
quorum: 1000,
proposal_creation_threshold: 100,
},
'governor.config'
);
assert(result.timelock.get_configuration() == (320, 60), 'timelock config');
}
Loading

0 comments on commit b09f6d1

Please sign in to comment.