Skip to content

Commit

Permalink
Merge pull request #66 from starknet-id/testnet
Browse files Browse the repository at this point in the history
Bring audit
  • Loading branch information
irisdv authored Jun 28, 2024
2 parents ffe7006 + d9020ab commit 01c9bef
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 114 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# StarknetId Naming Contract

This naming contract defines the Stark Naming System. It allows resolving a `stark` domain to a Starknet address or any other field.

## Features

- **Domain Resolution**: Resolve a `stark` domain to a Starknet address or any other field.
- **Native Resolver**: By default, `stark` names are attached to identities where the value associated with any field is stored.
- **Resolver Contracts**: Domain owners can delegate the resolution of their subdomains to resolver contracts.
- **Off-Chain Resolving**: Resolver contracts support reading off-chain data to resolve a name and a field to a target value.
- **On-Chain Resolving**: You can resolve a domain on-chain (and not a hash), allowing you to natively send money to a `.stark` domain instead of resolving off-chain before forging the actual transaction.
- **Optimized Encoding**: This feature forbids homograph attacks and allows for longer shortstrings. For more information, visit the [Encoding Documentation](https://docs.starknet.id/architecture/naming/encoding).

## Ecosystem Support

The Stark Naming System can be integrated into your dApp for seamless domain resolution. Here are some useful resources:

- **Integration Guide**: To integrate the Stark Naming System into your dApp, please check the [Developer Documentation](https://docs.starknet.id/devs).
- **Subdomains**: To create subdomains and determine if you should use the native resolver built on top of identities or create your own contract, visit the [Subdomains Documentation](https://docs.starknet.id/devs/subdomains).
- **Off-Chain Resolver**: To see how you can create an off-chain resolver and access data from web3, check out the [CCIP Architecture Documentation](https://docs.starknet.id/architecture/ccip) and follow the [CCIP Tutorial](https://docs.starknet.id/architecture/ccip/tutorial) which shows how to use Notion to resolve your Stark subdomains.

## Audits

For additional trust and transparency, this contract has been audited by independent third-party security firms. You can view the audit reports below:

- [Cairo Security Clan Audit](./audits/cairo_security_clan.pdf)
- [Subsix Audit](./audits/subsix.pdf)

## How to Build/Test?

This project was built using Scarb.

### Building

To build the project, run the following command:

```
scarb --release build
```

### Testing

To run the tests, use the following command:

```
scarb test
```

For details on the identity contract, see the [StarknetID Identity Contract](https://github.com/starknet-id/identity).
Binary file added audits/cairo_security_clan.pdf
Binary file not shown.
Binary file added audits/subsix.pdf
Binary file not shown.
4 changes: 3 additions & 1 deletion src/naming/asserts.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl AssertionsImpl of AssertionsTrait {
assert(data.owner == 0 || data.expiry < now, 'unexpired domain');

// Verify expiration range
assert(days < 365 * 25, 'max purchase of 25 years');
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());
}
Expand All @@ -60,6 +60,8 @@ impl AssertionsImpl of AssertionsTrait {
let mut i: felt252 = 1;
let stop = (domain.len() + 1).into();
let mut parent_key = 0;
// we start from the top domain and go down until we find you are the owner,
// reach the domain beginning or reach a key mismatch (reset parent domain)
loop {
assert(i != stop, 'you don\'t own this domain');
let i_gas_saver = i.try_into().unwrap();
Expand Down
98 changes: 37 additions & 61 deletions src/naming/internal.cairo
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use core::array::SpanTrait;
use naming::{
interface::{
resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait},
referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait},
},
interface::referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait},
naming::main::{
Naming,
Naming::{
Expand Down Expand Up @@ -51,24 +49,38 @@ impl InternalImpl of InternalTrait {
};
}

// returns the custom resolver to use for a domain (0 if none)
// and the parent domain length. If one parent domain has
// reset its subdomains, it will break and return its length,
// otherwise the parent length would be 0.
fn domain_to_resolver(
self: @Naming::ContractState, domain: Span<felt252>, parent_start_id: u32
self: @Naming::ContractState, mut domain: Span<felt252>
) -> (ContractAddress, u32) {
if parent_start_id == domain.len() {
return (ContractAddressZeroable::zero(), 0);
let mut custom_resolver = ContractAddressZeroable::zero();
let mut parent_length = 0;
let mut domain_parent_key = self._domain_data.read(self.hash_domain(domain)).parent_key;
loop {
if domain.len() == 1 {
break;
};
// will fail on empty domain
let parent_domain = domain.slice(1, domain.len() - 1);
let hashed_parent_domain = self.hash_domain(parent_domain);
let parent_domain_data = self._domain_data.read(hashed_parent_domain);
if parent_domain_data.resolver.into() != 0 {
custom_resolver = parent_domain_data.resolver;
parent_length = parent_domain.len();
break;
}
if domain_parent_key != parent_domain_data.key {
// custom_resolver is zero
parent_length = parent_domain.len();
break;
}
domain = parent_domain;
domain_parent_key = parent_domain_data.parent_key;
};

// 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);
}
(custom_resolver, parent_length)
}

fn pay_domain(
Expand Down Expand Up @@ -106,7 +118,12 @@ impl InternalImpl of InternalTrait {
// 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(), erc20_addr: erc20);
.add_commission(
discounted_price,
sponsor,
sponsored_addr: get_caller_address(),
erc20_addr: erc20
);
}
}

Expand Down Expand Up @@ -141,45 +158,4 @@ impl InternalImpl of InternalTrait {
);
}
}

// returns domain_hash (or zero) and its value for a specific field
fn resolve_util(
self: @Naming::ContractState, domain: Span<felt252>, field: felt252, hint: Span<felt252>
) -> (felt252, felt252) {
let (resolver, parent_start) = self.domain_to_resolver(domain, 1);
if (resolver != ContractAddressZeroable::zero()) {
let resolver_res = IResolverDispatcher { contract_address: resolver }
.resolve(domain.slice(0, parent_start), field, hint);
if resolver_res == 0 {
let hashed_domain = self.hash_domain(domain);
return (0, hashed_domain);
}
return (0, resolver_res);
} 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
}
}
)
}
}
}
121 changes: 79 additions & 42 deletions src/naming/main.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ mod Naming {
interface::{
naming::{INaming, INamingDispatcher, INamingDispatcherTrait},
pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait},
auto_renewal::{IAutoRenewal, IAutoRenewalDispatcher, IAutoRenewalDispatcherTrait}
auto_renewal::{IAutoRenewal, IAutoRenewalDispatcher, IAutoRenewalDispatcherTrait},
resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait}
}
};
use identity::interface::identity::{IIdentity, IIdentityDispatcher, IIdentityDispatcherTrait};
Expand Down Expand Up @@ -177,8 +178,23 @@ mod Naming {
fn resolve(
self: @ContractState, domain: Span<felt252>, field: felt252, hint: Span<felt252>
) -> felt252 {
let (_, value) = self.resolve_util(domain, field, hint);
value
let (resolver, parent_length) = self.domain_to_resolver(domain);
// if there is a resolver starting from the top
if (resolver != ContractAddressZeroable::zero()) {
IResolverDispatcher { contract_address: resolver }
.resolve(domain.slice(0, domain.len() - parent_length), field, hint)
} else {
let hashed_domain = self.hash_domain(domain);
let domain_data = self._domain_data.read(hashed_domain);
// if there was a reset subdomains starting from the top
if parent_length != 0 {
0
// otherwise, we just read the identity
} else {
IIdentityDispatcher { contract_address: self.starknetid_contract.read() }
.get_crosschecked_user_data(domain_data.owner, field)
}
}
}

// This functions allows to resolve a domain to a native address. Its output is designed
Expand All @@ -187,27 +203,44 @@ mod Naming {
fn domain_to_address(
self: @ContractState, domain: Span<felt252>, hint: Span<felt252>
) -> ContractAddress {
// resolve must be performed first because it calls untrusted resolving contracts
let (hashed_domain, value) = self.resolve_util(domain, 'starknet', hint);
if value != 0 {
let addr: Option<ContractAddress> = value.try_into();
return addr.unwrap();
};
let data = self._domain_data.read(hashed_domain);
if data.address.into() != 0 {
if domain.len() != 1 {
let parent_key = self
._domain_data
.read(self.hash_domain(domain.slice(1, domain.len() - 1)))
.key;
if parent_key == data.parent_key {
return data.address;
};
};
return data.address;
};
IIdentityDispatcher { contract_address: self.starknetid_contract.read() }
.owner_from_id(self.domain_to_id(domain))
let (resolver, parent_length) = self.domain_to_resolver(domain);
// if there is a resolver starting from the top
if (resolver != ContractAddressZeroable::zero()) {
let addr: Option<ContractAddress> = IResolverDispatcher {
contract_address: resolver
}
.resolve(domain.slice(0, domain.len() - parent_length), 'starknet', hint)
.try_into();
addr.unwrap()
} else {
// if there was a reset subdomains starting from the top
if parent_length != 0 {
ContractAddressZeroable::zero()
// otherwise we read the identity
} else {
let hashed_domain = self.hash_domain(domain);
let domain_data = self._domain_data.read(hashed_domain);
let identity_address = IIdentityDispatcher {
contract_address: self.starknetid_contract.read()
}
.get_crosschecked_user_data(domain_data.owner, 'starknet');
if identity_address != 0 {
let addr: Option<ContractAddress> = identity_address.try_into();
addr.unwrap()
} else {
if domain_data.address.into() != 0 {
// no need to check for keys as it was checked in domain_to_resolver
return domain_data.address;
} else {
// if no legacy address is found, it returns the identity owner
IIdentityDispatcher {
contract_address: self.starknetid_contract.read()
}
.owner_from_id(self.domain_to_id(domain))
}
}
}
}
}

// This returns the stored DomainData associated to this domain
Expand All @@ -221,22 +254,29 @@ mod Naming {
}

// This returns the identity (StarknetID) owning the domain
fn domain_to_id(self: @ContractState, domain: Span<felt252>) -> u128 {
fn domain_to_id(self: @ContractState, mut domain: Span<felt252>) -> u128 {
let data = self._domain_data.read(self.hash_domain(domain));
// todo: revert when try catch are available
if domain.len() == 0 {
return 0;
};
if domain.len() != 1 {
let parent_key = self
._domain_data
.read(self.hash_domain(domain.slice(1, domain.len() - 1)))
.key;
if parent_key != data.parent_key {
return 0;
};

let mut parent_key = data.parent_key;
let mut output = data.owner;
loop {
if domain.len() == 1 {
break;
}
let parent_domain = domain.slice(1, domain.len() - 1);
let parent_domain_data = self._domain_data.read(self.hash_domain(parent_domain));
if parent_domain_data.key != parent_key {
output = 0;
break;
}
domain = parent_domain;
parent_key = parent_domain_data.parent_key;
};
data.owner
output
}

// This function allows to find which domain to use to display an account
Expand Down Expand Up @@ -383,8 +423,8 @@ mod Naming {
domain_data.expiry + 86400 * days.into()
};
// 25*365 = 9125
assert(new_expiry <= now + 86400 * 9125, 'purchase too long');
assert(days >= 6 * 30, 'purchase too short');
assert(new_expiry <= now + 365 * 25 * 86400, 'max purchase of 25 years');
assert(days >= 6 * 30, 'min purchase of 6 months');

let data = DomainData {
owner: domain_data.owner,
Expand Down Expand Up @@ -456,9 +496,8 @@ mod Naming {
} else {
domain_data.expiry + 86400 * days.into()
};
// 25*365 = 9125
assert(new_expiry <= now + 86400 * 9125, 'purchase too long');
assert(days >= 6 * 30, 'purchase too short');
assert(new_expiry <= now + 365 * 25 * 86400, 'max purchase of 25 years');
assert(days >= 6 * 30, 'min purchase of 6 months');

let data = DomainData {
owner: domain_data.owner,
Expand Down Expand Up @@ -707,9 +746,7 @@ mod Naming {

// ADMIN

fn set_expiry(
ref self: ContractState, root_domain: felt252, expiry: u64
) {
fn set_expiry(ref self: ContractState, root_domain: felt252, expiry: u64) {
self.ownable.assert_only_owner();
let hashed_domain = self.hash_domain(array![root_domain].span());
let domain_data = self._domain_data.read(hashed_domain);
Expand Down
Loading

0 comments on commit 01c9bef

Please sign in to comment.