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

Refactor/resolving #10

Merged
merged 11 commits into from
Oct 28, 2023
4 changes: 2 additions & 2 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ version = "0.1.0"
# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest

[dependencies]
starknet = "2.2.0"
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", branch = "main" }
starknet = "2.3.0-rc0"
openzeppelin = { git = "https://github.com/andrew-fleming/cairo-contracts.git", branch = "component-erc20" }
identity = { git = "https://github.com/starknet-id/identity.git", branch = "master" }

[[target.starknet-contract]]
Expand Down
6 changes: 5 additions & 1 deletion src/interface/naming.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ trait INaming<TContractState> {

fn domain_to_address(self: @TContractState, domain: Span<felt252>) -> ContractAddress;

fn address_to_domain(self: @TContractState, address: ContractAddress) -> Array<felt252>;
fn address_to_domain(self: @TContractState, address: ContractAddress) -> Span<felt252>;

// external
fn buy(
Expand All @@ -39,6 +39,10 @@ trait INaming<TContractState> {

fn reset_subdomains(ref self: TContractState, domain: Span<felt252>);

fn set_address_to_domain(ref self: TContractState, domain: Span<felt252>);

fn reset_address_to_domain(ref self: TContractState);

// admin
fn set_admin(ref self: TContractState, new_admin: ContractAddress);

Expand Down
3 changes: 3 additions & 0 deletions src/naming.cairo
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
mod main;
mod internal;
mod asserts;
mod utils;
107 changes: 107 additions & 0 deletions src/naming/asserts.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use naming::{
interface::{
naming::{INaming, INamingDispatcher, INamingDispatcherTrait},
resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait},
pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait},
referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait},
},
naming::main::{
Naming,
Naming::{
EventEmitter, _hash_to_domain, _hash_to_domainContractMemberStateTrait, _domain_data,
_domain_dataContractMemberStateTrait, starknetid_contract,
starknetid_contractContractMemberStateTrait, discounts,
discountsContractMemberStateTrait, _address_to_domain,
_address_to_domainContractMemberStateTrait, _referral_contract,
_referral_contractContractMemberStateTrait,
}
},
};
use identity::interface::identity::{IIdentity, IIdentityDispatcher, IIdentityDispatcherTrait};
use starknet::{
contract_address::ContractAddressZeroable, ContractAddress, get_caller_address,
get_contract_address, get_block_timestamp
};
use openzeppelin::token::erc20::interface::{
IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait
};
use integer::{u256_safe_divmod, u256_as_non_zero};
use naming::naming::utils::UtilsTrait;


#[generate_trait]
impl AssertionsImpl of AssertionsTrait {
fn assert_purchase_is_possible(
self: @Naming::ContractState, identity: u128, domain: felt252, days: u16
) -> (felt252, u64, u64) {
let now = get_block_timestamp();

// Verify that the starknet.id doesn't already manage a domain
self.assert_id_availability(identity, now);

// Verify that the domain is not already taken or expired
let hashed_domain = self.hash_domain(array![domain].span());
let data = self._domain_data.read(hashed_domain);
assert(data.owner == 0 || data.expiry < now, 'unexpired domain');

// Verify expiration range
assert(days < 365 * 25, 'max purchase of 25 years');
assert(days > 2 * 30, 'min purchase of 2 month');
return (hashed_domain, now, now + 86400 * days.into());
}

fn assert_control_domain(
self: @Naming::ContractState, domain: Span<felt252>, account: ContractAddress
) {
// 1. account owns the domain
self.assert_is_owner(domain, account);
// 2. check domain expiration
let hashed_root_domain = self.hash_domain(domain.slice(domain.len() - 1, 1));
let root_domain_data = self._domain_data.read(hashed_root_domain);
assert(get_block_timestamp() <= root_domain_data.expiry, 'this domain has expired');
}

fn assert_is_owner(
self: @Naming::ContractState, domain: Span<felt252>, account: ContractAddress
) -> u32 {
let hashed_domain = self.hash_domain(domain);
let data = self._domain_data.read(hashed_domain);

// because erc721 crashes on zero
let owner = if data.owner == 0 {
ContractAddressZeroable::zero()
} else {
IIdentityDispatcher { contract_address: self.starknetid_contract.read() }
.owner_of(data.owner)
};

// if caller owns the starknet id, he owns the domain, we return the key
if owner == account {
return data.key;
};

// otherwise, if it is a root domain, he doesn't own it
assert(domain.len() != 1 && domain.len() != 0, 'you don\'t own this domain');

// if he doesn't own the starknet id, and doesn't own the domain, he might own the parent domain
let parent_key = self.assert_is_owner(domain.slice(1, domain.len() - 1), account);
// we ensure that the key is the same as the parent key
// this is to allow to revoke all subdomains in o(1) writes, by juste updating the key of the parent
if (data.parent_key != 0) {
assert(parent_key == data.parent_key, 'you no longer own this domain');
};
data.key
}

// this ensures a non expired domain is not already written on this identity
fn assert_id_availability(self: @Naming::ContractState, identity: u128, timestamp: u64) {
let id_hashed_domain = IIdentityDispatcher {
contract_address: self.starknetid_contract.read()
}
.get_verifier_data(identity, 'name', get_contract_address(), 0);
assert(
id_hashed_domain == 0 || self._domain_data.read(id_hashed_domain).expiry < timestamp,
'this id holds a domain'
);
}
}
185 changes: 185 additions & 0 deletions src/naming/internal.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use naming::{
interface::{
naming::{INaming, INamingDispatcher, INamingDispatcherTrait},
resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait},
pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait},
referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait},
},
naming::main::{
Naming,
Naming::{
EventEmitter, _hash_to_domain, _hash_to_domainContractMemberStateTrait, _domain_data,
_domain_dataContractMemberStateTrait, starknetid_contract,
starknetid_contractContractMemberStateTrait, discounts,
discountsContractMemberStateTrait, _address_to_domain,
_address_to_domainContractMemberStateTrait, _referral_contract,
_referral_contractContractMemberStateTrait,
}
}
};
use identity::interface::identity::{IIdentity, IIdentityDispatcher, IIdentityDispatcherTrait};
use starknet::{
contract_address::ContractAddressZeroable, ContractAddress, get_caller_address,
get_contract_address, get_block_timestamp
};
use openzeppelin::token::erc20::interface::{
IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait
};
use naming::naming::utils::UtilsTrait;

#[generate_trait]
impl InternalImpl of InternalTrait {

fn read_address_to_domain(
self: @Naming::ContractState, address: ContractAddress, ref domain: Array<felt252>
) -> usize {
let subdomain = self._address_to_domain.read((address, domain.len()));
if subdomain == 0 {
domain.len()
} else {
domain.append(subdomain);
self.read_address_to_domain(address, ref domain)
}
}

fn set_address_to_domain_util(
ref self: Naming::ContractState, address: ContractAddress, mut domain: Span<felt252>
) {
match domain.pop_back() {
Option::Some(domain_part) => {
self._address_to_domain.write((address, domain.len()), *domain_part);
self.set_address_to_domain_util(address, domain)
},
Option::None => {}
}
}

fn domain_to_resolver(
self: @Naming::ContractState, domain: Span<felt252>, parent_start_id: u32
) -> (ContractAddress, u32) {
if parent_start_id == domain.len() {
return (ContractAddressZeroable::zero(), 0);
};

// hashing parent_domain
let hashed_domain = self
.hash_domain(domain.slice(parent_start_id, domain.len() - parent_start_id));

let domain_data = self._domain_data.read(hashed_domain);

if domain_data.resolver.into() != 0 {
return (domain_data.resolver, parent_start_id);
} else {
return self.domain_to_resolver(domain, parent_start_id + 1);
}
}

fn pay_domain(
self: @Naming::ContractState,
domain_len: usize,
erc20: ContractAddress,
price: u256,
now: u64,
days: u16,
domain: felt252,
sponsor: ContractAddress,
discount_id: felt252
) -> () {
// check the discount
let discounted_price = if (discount_id == 0) {
price
} else {
let discount = self.discounts.read(discount_id);
let (min, max) = discount.domain_len_range;
assert(min <= domain_len && domain_len <= max, 'invalid length for discount');

let (min, max) = discount.days_range;
assert(min <= days && days <= max, 'days out of discount range');

let (min, max) = discount.timestamp_range;
assert(min <= now && now <= max, 'time out of discount range');
// discount.amount won't overflow as it's a value chosen by the admin to be in range (0, 100)
(price * discount.amount) / 100
};

// pay the price
IERC20CamelDispatcher { contract_address: erc20 }
.transferFrom(get_caller_address(), get_contract_address(), discounted_price);
// add sponsor commission if eligible
if sponsor.into() != 0 {
IReferralDispatcher { contract_address: self._referral_contract.read() }
.add_commission(discounted_price, sponsor, sponsored_addr: get_caller_address());
}
}

fn mint_domain(
ref self: Naming::ContractState,
expiry: u64,
resolver: ContractAddress,
hashed_domain: felt252,
id: u128,
domain: felt252
) {
let data = Naming::DomainData {
owner: id,
resolver,
address: ContractAddressZeroable::zero(), // legacy native address
expiry,
key: 1,
parent_key: 0,
};
self._hash_to_domain.write((hashed_domain, 0), domain);
self._domain_data.write(hashed_domain, data);
self.emit(Naming::Event::DomainMint(Naming::DomainMint { domain, owner: id, expiry }));

IIdentityDispatcher { contract_address: self.starknetid_contract.read() }
.set_verifier_data(id, 'name', hashed_domain, 0);
if (resolver.into() != 0) {
self
.emit(
Naming::Event::DomainResolverUpdate(
Naming::DomainResolverUpdate { domain: array![domain].span(), resolver }
)
);
}
}

// returns domain_hash (or zero) and its value for a specific field
fn resolve_util(
self: @Naming::ContractState, domain: Span<felt252>, field: felt252
) -> (felt252, felt252) {
let (resolver, parent_start) = self.domain_to_resolver(domain, 0);
if (resolver != ContractAddressZeroable::zero()) {
(
0,
IResolverDispatcher { contract_address: resolver }
.resolve(domain.slice(parent_start, domain.len() - parent_start), field)
)
} else {
let hashed_domain = self.hash_domain(domain);
let domain_data = self._domain_data.read(hashed_domain);
// circuit breaker for root domain
(
hashed_domain,
if (domain.len() == 1) {
IIdentityDispatcher { contract_address: self.starknetid_contract.read() }
.get_crosschecked_user_data(domain_data.owner, field)
// handle reset subdomains
} else {
// todo: optimize by changing the hash definition from H(b, a) to H(a, b)
let parent_key = self
._domain_data
.read(self.hash_domain(domain.slice(1, domain.len() - 1)))
.key;

if parent_key == domain_data.parent_key {
IIdentityDispatcher { contract_address: self.starknetid_contract.read() }
.get_crosschecked_user_data(domain_data.owner, field)
} else {
0
}
}
)
}
}
}
Loading
Loading