-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat: add altcoin buy & renew support #22
Changes from 7 commits
6b2206d
ff4ef29
3f7a65d
2e27f08
6cd4a04
10e091f
d97e685
05c7b03
962a73e
66c9e1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/usr/bin/env python3 | ||
from starkware.crypto.signature.signature import private_to_stark_key, get_random_private_key, sign | ||
from starknet_py.hash.utils import pedersen_hash | ||
|
||
priv_key = 123 | ||
pub_key = private_to_stark_key(priv_key) | ||
print("pub_key:", hex(pub_key)) | ||
|
||
user_addr = 0x123 | ||
erc20_addr = 0x5 | ||
quote = 1221805004292776 | ||
max_validity = 1000 | ||
encoded_string = 724720344857006587549020016926517802128122613457935427138661 | ||
data = pedersen_hash(pedersen_hash(pedersen_hash(erc20_addr, quote), max_validity), encoded_string) | ||
|
||
(x, y) = sign(data, priv_key) | ||
print("sig:", hex(x), hex(y)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
mod main; | ||
mod internal; | ||
mod asserts; | ||
mod utils; | ||
mod utils; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,9 @@ mod Naming { | |
use starknet::class_hash::ClassHash; | ||
use integer::{u256_safe_divmod, u256_as_non_zero}; | ||
use core::pedersen; | ||
use hash::LegacyHash; | ||
use ecdsa::check_ecdsa_signature; | ||
use wadray::Wad; | ||
use naming::{ | ||
naming::{asserts::AssertionsTrait, internal::InternalTrait, utils::UtilsTrait}, | ||
interface::{ | ||
|
@@ -131,6 +134,8 @@ mod Naming { | |
_domain_data: LegacyMap<felt252, DomainData>, | ||
_hash_to_domain: LegacyMap<(felt252, usize), felt252>, | ||
_address_to_domain: LegacyMap<(ContractAddress, usize), felt252>, | ||
_server_pub_key: felt252, | ||
_whitelisted_renewal_contracts: LegacyMap<ContractAddress, bool>, | ||
#[substorage(v0)] | ||
storage_read: storage_read_component::Storage, | ||
} | ||
|
@@ -272,6 +277,62 @@ mod Naming { | |
self.mint_domain(expiry, resolver, hashed_domain, id, domain); | ||
} | ||
|
||
fn altcoin_buy( | ||
ref self: ContractState, | ||
id: u128, | ||
domain: felt252, | ||
days: u16, | ||
resolver: ContractAddress, | ||
sponsor: ContractAddress, | ||
discount_id: felt252, | ||
metadata: felt252, | ||
altcoin_addr: ContractAddress, | ||
quote: u128, | ||
max_validity: u64, | ||
sig: (felt252, felt252), | ||
) { | ||
let (hashed_domain, now, expiry) = self.assert_purchase_is_possible(id, domain, days); | ||
// we need a u256 to be able to perform safe divisions | ||
let domain_len = self.get_chars_len(domain.into()); | ||
|
||
// check quote timestamp is still valid | ||
assert(get_block_timestamp() <= max_validity, 'quotation expired'); | ||
|
||
// verify signature | ||
let altcoin: felt252 = altcoin_addr.into(); | ||
let quote_felt: felt252 = quote.into(); | ||
let message_hash = LegacyHash::hash( | ||
LegacyHash::hash(LegacyHash::hash(altcoin, quote_felt), max_validity), | ||
'starknet id altcoin quote' | ||
); | ||
let (sig0, sig1) = sig; | ||
let is_valid = check_ecdsa_signature( | ||
message_hash, self._server_pub_key.read(), sig0, sig1 | ||
); | ||
assert(is_valid, 'Invalid signature'); | ||
|
||
// find domain cost in ETH | ||
let (_, price_in_eth) = IPricingDispatcher { | ||
contract_address: self._pricing_contract.read() | ||
} | ||
.compute_buy_price(domain_len, days); | ||
// compute domain cost in altcoin | ||
let price_in_altcoin = self | ||
.get_altcoin_price(quote.into(), price_in_eth.try_into().unwrap()); | ||
self | ||
.pay_domain( | ||
domain_len, | ||
altcoin_addr, | ||
price_in_altcoin, | ||
now, | ||
days, | ||
domain, | ||
sponsor, | ||
discount_id | ||
); | ||
self.emit(Event::SaleMetadata(SaleMetadata { domain, metadata })); | ||
self.mint_domain(expiry, resolver, hashed_domain, id, domain); | ||
} | ||
|
||
fn renew( | ||
ref self: ContractState, | ||
|
@@ -316,6 +377,137 @@ mod Naming { | |
self.emit(Event::DomainRenewal(DomainRenewal { domain, new_expiry })); | ||
} | ||
|
||
fn altcoin_renew( | ||
ref self: ContractState, | ||
domain: felt252, | ||
days: u16, | ||
sponsor: ContractAddress, | ||
discount_id: felt252, | ||
metadata: felt252, | ||
altcoin_addr: ContractAddress, | ||
quote: u128, | ||
max_validity: u64, | ||
sig: (felt252, felt252), | ||
) { | ||
let now = get_block_timestamp(); | ||
let hashed_domain = self.hash_domain(array![domain].span()); | ||
let domain_data = self._domain_data.read(hashed_domain); | ||
|
||
// check quote timestamp is still valid | ||
assert(get_block_timestamp() <= max_validity, 'quotation expired'); | ||
// verify signature | ||
let altcoin: felt252 = altcoin_addr.into(); | ||
let quote_felt: felt252 = quote.into(); | ||
let message_hash = LegacyHash::hash( | ||
LegacyHash::hash(LegacyHash::hash(altcoin, quote_felt), max_validity), | ||
'starknet id altcoin quote' | ||
); | ||
let (sig0, sig1) = sig; | ||
let is_valid = check_ecdsa_signature( | ||
message_hash, self._server_pub_key.read(), sig0, sig1 | ||
); | ||
assert(is_valid, 'Invalid signature'); | ||
|
||
// we need a u256 to be able to perform safe divisions | ||
let domain_len = self.get_chars_len(domain.into()); | ||
// find domain cost in ETH | ||
let (_, price_in_eth) = IPricingDispatcher { | ||
contract_address: self._pricing_contract.read() | ||
} | ||
.compute_renew_price(domain_len, days); | ||
// compute domain cost in altcoin | ||
let price_in_altcoin = self | ||
.get_altcoin_price(quote.into(), price_in_eth.try_into().unwrap()); | ||
self | ||
.pay_domain( | ||
domain_len, | ||
altcoin_addr, | ||
price_in_altcoin, | ||
now, | ||
days, | ||
domain, | ||
sponsor, | ||
discount_id | ||
); | ||
self.emit(Event::SaleMetadata(SaleMetadata { domain, metadata })); | ||
// find new domain expiry | ||
let new_expiry = if domain_data.expiry <= now { | ||
now + 86400 * days.into() | ||
} 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'); | ||
|
||
let data = DomainData { | ||
owner: domain_data.owner, | ||
resolver: domain_data.resolver, | ||
address: domain_data.address, | ||
expiry: new_expiry, | ||
key: domain_data.key, | ||
parent_key: 0, | ||
}; | ||
self._domain_data.write(hashed_domain, data); | ||
self.emit(Event::DomainRenewal(DomainRenewal { domain, new_expiry })); | ||
} | ||
|
||
fn auto_renew_altcoin( | ||
ref self: ContractState, | ||
domain: felt252, | ||
days: u16, | ||
sponsor: ContractAddress, | ||
discount_id: felt252, | ||
metadata: felt252, | ||
altcoin_addr: ContractAddress, | ||
price_in_altcoin: u256, | ||
) { | ||
let now = get_block_timestamp(); | ||
let hashed_domain = self.hash_domain(array![domain].span()); | ||
let domain_data = self._domain_data.read(hashed_domain); | ||
|
||
// check caller is a whitelisted altcoin auto renewal contract | ||
assert( | ||
self._whitelisted_renewal_contracts.read(get_caller_address()), | ||
'Caller not whitelisted' | ||
); | ||
|
||
// we need a u256 to be able to perform safe divisions | ||
let domain_len = self.get_chars_len(domain.into()); | ||
self | ||
.pay_domain( | ||
domain_len, | ||
altcoin_addr, | ||
price_in_altcoin, | ||
now, | ||
days, | ||
domain, | ||
sponsor, | ||
discount_id | ||
); | ||
self.emit(Event::SaleMetadata(SaleMetadata { domain, metadata })); | ||
// find new domain expiry | ||
let new_expiry = if domain_data.expiry <= now { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could drop these two expiry check because the auto renew contract already ensures you can't do something abusive (your domain should expire soon and we can renew only for one year) |
||
now + 86400 * days.into() | ||
} 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'); | ||
|
||
let data = DomainData { | ||
owner: domain_data.owner, | ||
resolver: domain_data.resolver, | ||
address: domain_data.address, | ||
expiry: new_expiry, | ||
key: domain_data.key, | ||
parent_key: 0, | ||
}; | ||
self._domain_data.write(hashed_domain, data); | ||
self.emit(Event::DomainRenewal(DomainRenewal { domain, new_expiry })); | ||
} | ||
|
||
fn transfer_domain(ref self: ContractState, domain: Span<felt252>, target_id: u128) { | ||
self.assert_control_domain(domain, get_caller_address()); | ||
|
||
|
@@ -507,6 +699,21 @@ mod Naming { | |
assert(!new_class_hash.is_zero(), 'Class hash cannot be zero'); | ||
starknet::replace_class_syscall(new_class_hash).unwrap(); | ||
} | ||
|
||
fn set_server_pub_key(ref self: ContractState, new_key: felt252) { | ||
assert(get_caller_address() == self._admin_address.read(), 'you are not admin'); | ||
self._server_pub_key.write(new_key); | ||
} | ||
|
||
fn whitelist_renewal_contract(ref self: ContractState, contract: ContractAddress) { | ||
assert(get_caller_address() == self._admin_address.read(), 'you are not admin'); | ||
self._whitelisted_renewal_contracts.write(contract, true); | ||
} | ||
|
||
fn blacklist_renewal_contract(ref self: ContractState, contract: ContractAddress) { | ||
assert(get_caller_address() == self._admin_address.read(), 'you are not admin'); | ||
self._whitelisted_renewal_contracts.write(contract, false); | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ use naming::{ | |
naming::main::{Naming, Naming::{_hash_to_domain, _hash_to_domainContractMemberStateTrait}} | ||
}; | ||
use integer::{u256_safe_divmod, u256_as_non_zero}; | ||
use wadray::{Wad, WAD_SCALE}; | ||
|
||
#[generate_trait] | ||
impl UtilsImpl of UtilsTrait { | ||
|
@@ -57,4 +58,10 @@ impl UtilsImpl of UtilsTrait { | |
let next = self.get_chars_len(p); | ||
1 + next | ||
} | ||
|
||
fn get_altcoin_price( | ||
self: @Naming::ContractState, altcoin_quote: Wad, domain_price_eth: Wad | ||
) -> u256 { | ||
(domain_price_eth / altcoin_quote).into() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be a multiplication here. The base currency is ETH, the quoted currency is something like USDC. In that context a quote means the price of 1 ETH in the quoted currency, so for example 3500 USDC. It means you should just have to do a multiplication. If the API called by the backend returns the other way (the altcoin is the base currency), we should modify the backend to return 1/result, this will help keep this code less confusing. Also a multiplication should be significantly cheaper. |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quote could be a Wad directly since you will cast it after anyway