diff --git a/src/interface.cairo b/src/interface.cairo index 681cf75..05f90c0 100644 --- a/src/interface.cairo +++ b/src/interface.cairo @@ -2,3 +2,4 @@ mod naming; mod resolver; mod pricing; mod referral; +mod auto_renewal; \ No newline at end of file diff --git a/src/interface/auto_renewal.cairo b/src/interface/auto_renewal.cairo new file mode 100644 index 0000000..fffc789 --- /dev/null +++ b/src/interface/auto_renewal.cairo @@ -0,0 +1,13 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IAutoRenewal { + fn get_renewing_allowance( + self: @TContractState, domain: felt252, renewer: starknet::ContractAddress, + ) -> u256; + + // naming, erc20, tax + fn get_contracts( + self: @TContractState + ) -> (starknet::ContractAddress, starknet::ContractAddress, starknet::ContractAddress); +} diff --git a/src/interface/naming.cairo b/src/interface/naming.cairo index ec6eaaf..170b443 100644 --- a/src/interface/naming.cairo +++ b/src/interface/naming.cairo @@ -19,7 +19,9 @@ trait INaming { self: @TContractState, domain: Span, hint: Span ) -> ContractAddress; - fn address_to_domain(self: @TContractState, address: ContractAddress, hint: Span) -> Span; + fn address_to_domain( + self: @TContractState, address: ContractAddress, hint: Span + ) -> Span; // external fn buy( @@ -70,6 +72,8 @@ trait INaming { sig: (felt252, felt252), ); + fn ar_discount_renew(ref self: TContractState, domain: felt252, ar_contract: ContractAddress,); + fn auto_renew_altcoin( ref self: TContractState, domain: felt252, @@ -117,4 +121,7 @@ trait INaming { fn whitelist_renewal_contract(ref self: TContractState, contract: ContractAddress); fn blacklist_renewal_contract(ref self: TContractState, contract: ContractAddress); + + fn toggle_ar_discount_renew(ref self: TContractState); + } diff --git a/src/naming/main.cairo b/src/naming/main.cairo index e57e3f9..7d66c14 100644 --- a/src/naming/main.cairo +++ b/src/naming/main.cairo @@ -20,6 +20,7 @@ mod Naming { resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait}, pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait}, referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait}, + auto_renewal::{IAutoRenewal, IAutoRenewalDispatcher, IAutoRenewalDispatcherTrait} } }; use clone::Clone; @@ -136,6 +137,9 @@ mod Naming { _address_to_domain: LegacyMap<(ContractAddress, usize), felt252>, _server_pub_key: felt252, _whitelisted_renewal_contracts: LegacyMap, + // a for alpha, as we will probably do this campaign again in the future + _ar_discount_blacklist_a: LegacyMap, + _ar_discount_renew_enabled: bool, #[substorage(v0)] storage_read: storage_read_component::Storage, } @@ -463,6 +467,53 @@ mod Naming { self.emit(Event::DomainRenewal(DomainRenewal { domain, new_expiry })); } + fn ar_discount_renew( + ref self: ContractState, domain: felt252, ar_contract: ContractAddress, + ) { + // First we check the discount is enabled + assert(self._ar_discount_renew_enabled.read(), 'Discount disabled'); + + // We check that domain didn't already claim the discount + assert(!self._ar_discount_blacklist_a.read(domain), 'You can\'t claim this twice'); + + // We check it's a valid AR contract, then we check that AR is enabled, + // we don't validate the pricing because it could change + assert(self._whitelisted_renewal_contracts.read(ar_contract), 'AR not whitelisted'); + let auto_renewal_dispatcher = IAutoRenewalDispatcher { contract_address: ar_contract }; + let caller = get_caller_address(); + let ar_allowance = auto_renewal_dispatcher.get_renewing_allowance(domain, caller); + assert(ar_allowance != 0, 'Invalid AR allowance'); + let (_, erc20, _) = auto_renewal_dispatcher.get_contracts(); + let erc20_allowance = IERC20CamelDispatcher { contract_address: erc20 } + .allowance(caller, ar_contract); + assert(erc20_allowance != 0, 'Invalid ERC20 allowance'); + + // We then blacklist that domain for this discount + self._ar_discount_blacklist_a.write(domain, true); + + // We can finally renew the domain with no SaleMetadata event since it's free + let now = get_block_timestamp(); + let hashed_domain = self.hash_domain(array![domain].span()); + let domain_data = self._domain_data.read(hashed_domain); + // we extended its expiry by 90 days (~3 months) + let new_expiry = if domain_data.expiry <= now { + now + 86400 * 90 + } else { + domain_data.expiry + 86400 * 90 + }; + + 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, @@ -721,6 +772,12 @@ mod Naming { assert(get_caller_address() == self._admin_address.read(), 'you are not admin'); self._whitelisted_renewal_contracts.write(contract, false); } + + + fn toggle_ar_discount_renew(ref self: ContractState) { + assert(get_caller_address() == self._admin_address.read(), 'you are not admin'); + self._ar_discount_renew_enabled.write(!self._ar_discount_renew_enabled.read()); + } } } diff --git a/src/tests/naming.cairo b/src/tests/naming.cairo index 0e0700c..a226146 100644 --- a/src/tests/naming.cairo +++ b/src/tests/naming.cairo @@ -4,3 +4,4 @@ mod test_custom_resolver; mod test_usecases; mod test_features; mod test_altcoin; +mod test_ar_discount; diff --git a/src/tests/naming/test_ar_discount.cairo b/src/tests/naming/test_ar_discount.cairo new file mode 100644 index 0000000..608cc7d --- /dev/null +++ b/src/tests/naming/test_ar_discount.cairo @@ -0,0 +1,194 @@ +use array::ArrayTrait; +use array::SpanTrait; +use option::OptionTrait; +use zeroable::Zeroable; +use traits::Into; +use starknet::testing; +use starknet::ContractAddress; +use starknet::contract_address::ContractAddressZeroable; +use starknet::contract_address_const; +use starknet::testing::set_contract_address; +use super::super::utils; +use openzeppelin::token::erc20::{ + interface::{IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait} +}; +use identity::{ + identity::main::Identity, interface::identity::{IIdentityDispatcher, IIdentityDispatcherTrait} +}; +use naming::interface::naming::{INamingDispatcher, INamingDispatcherTrait}; +use naming::interface::pricing::{IPricingDispatcher, IPricingDispatcherTrait}; +use naming::interface::auto_renewal::{ + IAutoRenewal, IAutoRenewalDispatcher, IAutoRenewalDispatcherTrait +}; +use naming::naming::main::Naming; +use naming::pricing::Pricing; +use super::common::deploy; +use naming::naming::main::Naming::Discount; + + +#[starknet::contract] +mod DummyAutoRenewal { + use core::array::ArrayTrait; + use starknet::ContractAddress; + use starknet::{contract_address_const, get_caller_address, get_contract_address}; + + #[storage] + struct Storage { + erc20: starknet::ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, erc20: starknet::ContractAddress) { + self.erc20.write(erc20); + } + + #[abi(embed_v0)] + impl DummyImpl of naming::interface::auto_renewal::IAutoRenewal { + fn get_renewing_allowance( + self: @ContractState, domain: felt252, renewer: starknet::ContractAddress, + ) -> u256 { + 1 + } + + // naming, erc20, tax + fn get_contracts( + self: @ContractState + ) -> (starknet::ContractAddress, starknet::ContractAddress, starknet::ContractAddress) { + (contract_address_const::<0x0>(), self.erc20.read(), contract_address_const::<0x0>()) + } + } +} + +#[test] +#[available_gas(2000000000)] +fn test_ar_discount() { + // setup + let (eth, pricing, identity, naming) = deploy(); + let caller = contract_address_const::<0x123>(); + set_contract_address(caller); + let id: u128 = 1; + let th0rgal: felt252 = 33133781693; + + //we mint an id + identity.mint(id); + + // we check how much a domain costs + let (_, price) = pricing.compute_buy_price(7, 365); + + // we allow the naming to take our money + eth.approve(naming.contract_address, price); + + // we buy with no resolver, no sponsor, no discount and empty metadata + naming + .buy( + id, th0rgal, 365, ContractAddressZeroable::zero(), ContractAddressZeroable::zero(), 0, 0 + ); + + let auto_renewal = utils::deploy( + DummyAutoRenewal::TEST_CLASS_HASH, array![eth.contract_address.into()] + ); + + let current_expiry = naming.domain_to_expiry(array![th0rgal].span()); + + // we set the renewal contract and enable the discount + naming.whitelist_renewal_contract(auto_renewal); + naming.toggle_ar_discount_renew(); + let (_, yearly_renewal_price) = pricing.compute_renew_price(7, 365); + eth.approve(auto_renewal, yearly_renewal_price); + let _allowance = eth.allowance(caller, auto_renewal); + naming.ar_discount_renew(th0rgal, auto_renewal); + + // we don't set the auto renewal allowance in this test because we + // use a dummy contract which always return 1, theoretically we should + // set it to infinity (2**256-1) + let new_expiry = naming.domain_to_expiry(array![th0rgal].span()); + assert(new_expiry - current_expiry == 86400 * 90, 'Invalid expiry'); +} + + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('Discount disabled', 'ENTRYPOINT_FAILED'))] +fn test_ar_discount_not_enabled() { + // setup + let (eth, pricing, identity, naming) = deploy(); + let caller = contract_address_const::<0x123>(); + set_contract_address(caller); + let id: u128 = 1; + let th0rgal: felt252 = 33133781693; + + //we mint an id + identity.mint(id); + + // we check how much a domain costs + let (_, price) = pricing.compute_buy_price(7, 365); + + // we allow the naming to take our money + eth.approve(naming.contract_address, price); + + // we buy with no resolver, no sponsor, no discount and empty metadata + naming + .buy( + id, th0rgal, 365, ContractAddressZeroable::zero(), ContractAddressZeroable::zero(), 0, 0 + ); + + let auto_renewal = utils::deploy( + DummyAutoRenewal::TEST_CLASS_HASH, array![eth.contract_address.into()] + ); + + // we set the renewal contract and don't enable the discount + naming.whitelist_renewal_contract(auto_renewal); + //naming.toggle_ar_discount_renew(); + let (_, yearly_renewal_price) = pricing.compute_renew_price(7, 365); + eth.approve(auto_renewal, yearly_renewal_price); + let _allowance = eth.allowance(caller, auto_renewal); + naming.ar_discount_renew(th0rgal, auto_renewal); +} + + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('Invalid ERC20 allowance', 'ENTRYPOINT_FAILED'))] +fn test_ar_discount_wrong_ar_allowance() { + // setup + let (eth, pricing, identity, naming) = deploy(); + let caller = contract_address_const::<0x123>(); + set_contract_address(caller); + let id: u128 = 1; + let th0rgal: felt252 = 33133781693; + + //we mint an id + identity.mint(id); + + // we check how much a domain costs + let (_, price) = pricing.compute_buy_price(7, 365); + + // we allow the naming to take our money + eth.approve(naming.contract_address, price); + + // we buy with no resolver, no sponsor, no discount and empty metadata + naming + .buy( + id, th0rgal, 365, ContractAddressZeroable::zero(), ContractAddressZeroable::zero(), 0, 0 + ); + + let auto_renewal = utils::deploy( + DummyAutoRenewal::TEST_CLASS_HASH, array![eth.contract_address.into()] + ); + + let current_expiry = naming.domain_to_expiry(array![th0rgal].span()); + + // we set the renewal contract and enable the discount + naming.whitelist_renewal_contract(auto_renewal); + naming.toggle_ar_discount_renew(); + let (_, _yearly_renewal_price) = pricing.compute_renew_price(7, 365); + //eth.approve(auto_renewal, yearly_renewal_price); + let _allowance = eth.allowance(caller, auto_renewal); + naming.ar_discount_renew(th0rgal, auto_renewal); + + // we don't set the auto renewal allowance in this test because we + // use a dummy contract which always return 1, theoretically we should + // set it to infinity (2**256-1) + let new_expiry = naming.domain_to_expiry(array![th0rgal].span()); + assert(new_expiry - current_expiry == 86400 * 90, 'Invalid expiry'); +}