From 760de5de8114826df8922373778cec65f287fcc4 Mon Sep 17 00:00:00 2001 From: Jordy Romuald <87231934+JordyRo1@users.noreply.github.com> Date: Sat, 24 Feb 2024 01:10:22 +0100 Subject: [PATCH] Feat succinct fee vault (#106) * function registry and gateway * init * fix: native fee * fix: fmt * fix: rebase with main branch --------- Co-authored-by: Ben Goebel --- src/blobstreamx.cairo | 4 +- src/lib.cairo | 6 + src/succinctx/fee_vault.cairo | 244 ++++++++++++++ .../function_registry/erc20_mock.cairo | 81 +++++ src/succinctx/gateway.cairo | 25 +- src/succinctx/interfaces.cairo | 25 ++ src/succinctx/tests/test_fee_vault.cairo | 307 ++++++++++++++++++ src/tests/common.cairo | 31 +- 8 files changed, 715 insertions(+), 8 deletions(-) create mode 100644 src/succinctx/fee_vault.cairo create mode 100644 src/succinctx/function_registry/erc20_mock.cairo create mode 100644 src/succinctx/tests/test_fee_vault.cairo diff --git a/src/blobstreamx.cairo b/src/blobstreamx.cairo index a5b6162..983711c 100644 --- a/src/blobstreamx.cairo +++ b/src/blobstreamx.cairo @@ -12,8 +12,8 @@ mod blobstreamx { use core::traits::Into; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; - use starknet::info::{get_block_number, get_contract_address}; - use starknet::{ClassHash, ContractAddress}; + use starknet::info::get_block_number; + use starknet::{ClassHash, ContractAddress, get_contract_address}; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); diff --git a/src/lib.cairo b/src/lib.cairo index 9d271fc..6c1ddb3 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -7,13 +7,19 @@ mod mocks { } mod succinctx { + mod fee_vault; mod gateway; mod interfaces; mod function_registry { mod component; + mod erc20_mock; mod interfaces; mod mock; } + #[cfg(test)] + mod tests { + mod test_fee_vault; + } } #[cfg(test)] diff --git a/src/succinctx/fee_vault.cairo b/src/succinctx/fee_vault.cairo new file mode 100644 index 0000000..b4ac01a --- /dev/null +++ b/src/succinctx/fee_vault.cairo @@ -0,0 +1,244 @@ +#[starknet::contract] +mod succinct_fee_vault { + use blobstream_sn::succinctx::interfaces::IFeeVault; + use core::starknet::event::EventEmitter; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, get_caller_address, ClassHash}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + balances: LegacyMap::<(ContractAddress, ContractAddress), u256>, + allowed_deductors: LegacyMap::, + native_currency_address: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Received: Received, + Deducted: Deducted, + Collected: Collected, + // COMPONENT EVENTS + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + #[derive(Drop, starknet::Event)] + struct Received { + account: ContractAddress, + token: ContractAddress, + amount: u256 + } + #[derive(Drop, starknet::Event)] + struct Deducted { + account: ContractAddress, + token: ContractAddress, + amount: u256 + } + + #[derive(Drop, starknet::Event)] + struct Collected { + to: ContractAddress, + token: ContractAddress, + amount: u256, + } + + + mod Errors { + /// Data commitment for specified block range does not exist + const InvalidAccount: felt252 = 'Invalid account'; + const InvalidToken: felt252 = 'Invalid token'; + const InsufficentAllowance: felt252 = 'Insufficent allowance'; + const OnlyDeductor: felt252 = 'Only deductor allowed'; + const InsufficentBalance: felt252 = 'Insufficent balance'; + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + } + + #[constructor] + fn constructor( + ref self: ContractState, native_currency_address: ContractAddress, owner: ContractAddress + ) { + self.native_currency_address.write(native_currency_address); + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl IFeeVaultImpl of IFeeVault { + /// Get the current native currency address + /// # Returns + /// The native currency address defined. + fn get_native_currency(self: @ContractState) -> ContractAddress { + self.native_currency_address.read() + } + + /// Set the native currency address + /// # Arguments + /// * `_new_native_address`- The new native currency address to be set + fn set_native_currency(ref self: ContractState, _new_native_address: ContractAddress) { + self.ownable.assert_only_owner(); + assert(!_new_native_address.is_zero(), Errors::InvalidToken); + self.native_currency_address.write(_new_native_address); + } + + + /// Get the deductor status + /// # Arguments + /// * `_deductor` - The deductor to retrieve the status. + /// # Returns + /// The boolean associated with the deductor status + fn get_deductor_status(self: @ContractState, _deductor: ContractAddress) -> bool { + self.allowed_deductors.read(_deductor) + } + + /// Get the balance for a given token and account + /// # Arguments + /// * `_account` - The account to retrieve the balance information. + /// * `_token` - The token address to consider. + /// # Returns + /// The associated balance. + fn get_balances_infos( + self: @ContractState, _account: ContractAddress, _token: ContractAddress + ) -> u256 { + self.balances.read((_token, _account)) + } + /// Add the specified deductor + /// # Arguments + /// * `_deductor` - The address of the deductor to add. + fn add_deductor(ref self: ContractState, _deductor: ContractAddress) { + self.ownable.assert_only_owner(); + self.allowed_deductors.write(_deductor, true); + } + + /// Remove the specified deductor + /// # Arguments + /// * `_deductor` - The address of the deductor to remove. + fn remove_deductor(ref self: ContractState, _deductor: ContractAddress) { + self.ownable.assert_only_owner(); + self.allowed_deductors.write(_deductor, false); + } + + /// Deposit the specified amount of native currency from the caller. + /// Dev: the native currency address is defined in the storage slot native_currency + /// Dev: MUST approve this contract to spend at least _amount of the native_currency before calling this. + /// # Arguments + /// * `_account` - The account to deposit the native currency for. + fn deposit_native(ref self: ContractState, _account: ContractAddress) { + let native_currency = self.native_currency_address.read(); + self + .deposit( + _account, native_currency, starknet::info::get_tx_info().unbox().max_fee.into() + ); + } + + /// Deposit the specified amount of the specified token from the caller. + /// Dev: MUST approve this contract to spend at least _amount of _token before calling this. + /// # Arguments + /// * `_account` - The account to deposit the native currency for. + /// * `_token` - The address of the token to deposit. + /// * `_amount` - The amoun to deposit. + fn deposit( + ref self: ContractState, + _account: ContractAddress, + _token: ContractAddress, + _amount: u256 + ) { + let caller_address = get_caller_address(); + let contract_address = starknet::info::get_contract_address(); + assert(!_account.is_zero(), Errors::InvalidAccount); + assert(!_token.is_zero(), Errors::InvalidToken); + let erc20_dispatcher = IERC20Dispatcher { contract_address: _token }; + let allowance = erc20_dispatcher.allowance(caller_address, contract_address); + assert(allowance >= _amount, Errors::InsufficentAllowance); + erc20_dispatcher.transfer_from(caller_address, contract_address, _amount); + let current_balance = self.balances.read((_token, _account)); + self.balances.write((_token, _account), current_balance + _amount); + self.emit(Received { account: _account, token: _token, amount: _amount }); + } + + /// Deduct the specified amount of native currency from the specified account. + /// # Arguments + /// * `_account` - The account to deduct the native currency from. + fn deduct_native(ref self: ContractState, _account: ContractAddress) { + let caller_address = get_caller_address(); + let native_currency = self.native_currency_address.read(); + assert(self.allowed_deductors.read(caller_address), Errors::OnlyDeductor); + self + .deduct( + _account, native_currency, starknet::info::get_tx_info().unbox().max_fee.into() + ); + } + + /// Deduct the specified amount of native currency from the specified account. + /// # Arguments + /// * `_account` - The account to deduct the native currency from. + /// * `_token` - The address of the token to deduct. + /// * `_amount` - The amount of the token to deduct. + fn deduct( + ref self: ContractState, + _account: ContractAddress, + _token: ContractAddress, + _amount: u256 + ) { + let caller_address = get_caller_address(); + assert(self.allowed_deductors.read(caller_address), Errors::OnlyDeductor); + assert(!_account.is_zero(), Errors::InvalidAccount); + assert(!_token.is_zero(), Errors::InvalidToken); + let current_balance = self.balances.read((_token, _account)); + assert(current_balance >= _amount, Errors::InsufficentBalance); + self.balances.write((_token, _account), current_balance - _amount); + self.emit(Deducted { account: _account, token: _token, amount: _amount }); + } + + /// Collect the specified amount of native currency. + /// * `_to`- The address to send the collected native currency to. + /// * `_amount`- The amount of native currency to collect. + fn collect_native(ref self: ContractState, _to: ContractAddress, _amount: u256) { + self.ownable.assert_only_owner(); + let native_currency = self.native_currency_address.read(); + self.collect(_to, native_currency, _amount); + } + + /// Collect the specified amount of the specified token. + /// * `_to`- The address to send the collected tokens to. + /// * `_token` - The address of the token to collect. + /// * `_amount`- The amount of the token to collect. + fn collect( + ref self: ContractState, _to: ContractAddress, _token: ContractAddress, _amount: u256 + ) { + self.ownable.assert_only_owner(); + let contract_address = starknet::info::get_contract_address(); + assert(!_token.is_zero(), Errors::InvalidToken); + let erc20_dispatcher = IERC20Dispatcher { contract_address: _token }; + assert( + erc20_dispatcher.balance_of(contract_address) >= _amount, Errors::InsufficentBalance + ); + erc20_dispatcher.transfer(_to, _amount); + self.emit(Collected { to: _to, token: _token, amount: _amount }) + } + } +} + diff --git a/src/succinctx/function_registry/erc20_mock.cairo b/src/succinctx/function_registry/erc20_mock.cairo new file mode 100644 index 0000000..e904409 --- /dev/null +++ b/src/succinctx/function_registry/erc20_mock.cairo @@ -0,0 +1,81 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IMockERC20 { + fn mint_to(ref self: TContractState, recipient: ContractAddress, amount: u256); + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; +} + +#[starknet::contract] +mod MockERC20 { + use openzeppelin::token::erc20::ERC20Component; + use starknet::{ContractAddress, get_caller_address}; + use super::IMockERC20; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, name: felt252, symbol: felt252) { + self.erc20.initializer(name, symbol); + } + + #[abi(embed_v0)] + impl IMockERC20Impl of IMockERC20 { + fn mint_to(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20._mint(recipient, amount); + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let caller = get_caller_address(); + self.erc20._approve(caller, spender, amount); + true + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.erc20.transfer_from(sender, recipient, amount) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + self.erc20.transfer(recipient, amount) + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.erc20.allowance(owner, spender) + } + } +} diff --git a/src/succinctx/gateway.cairo b/src/succinctx/gateway.cairo index 47d3492..b94ba05 100644 --- a/src/succinctx/gateway.cairo +++ b/src/succinctx/gateway.cairo @@ -4,7 +4,8 @@ mod succinct_gateway { use blobstream_sn::succinctx::function_registry::component::function_registry_cpt; use blobstream_sn::succinctx::function_registry::interfaces::IFunctionRegistry; use blobstream_sn::succinctx::interfaces::{ - ISuccinctGateway, IFunctionVerifierDispatcher, IFunctionVerifierDispatcherTrait + ISuccinctGateway, IFunctionVerifierDispatcher, IFunctionVerifierDispatcherTrait, + IFeeVaultDispatcher, IFeeVaultDispatcherTrait }; use core::array::SpanTrait; use openzeppelin::access::ownable::{OwnableComponent as ownable_cpt, interface::IOwnable}; @@ -36,6 +37,7 @@ mod succinct_gateway { verified_function_id: u256, verified_input_hash: u256, verified_output: (u256, u256), + fee_vault_address: ContractAddress, #[substorage(v0)] function_registry: function_registry_cpt::Storage, #[substorage(v0)] @@ -107,11 +109,15 @@ mod succinct_gateway { const INVALID_CALL: felt252 = 'Invalid call to verify'; const INVALID_REQUEST: felt252 = 'Invalid request for fullfilment'; const INVALID_PROOF: felt252 = 'Invalid proof provided'; + const FEE_VAULT_NOT_INITIALIZED: felt252 = 'Fee vault not initialized'; } #[constructor] - fn constructor(ref self: ContractState, owner: ContractAddress) { + fn constructor( + ref self: ContractState, owner: ContractAddress, fee_vault_address: ContractAddress + ) { self.ownable.initializer(owner); + self.fee_vault_address.write(fee_vault_address); } #[abi(embed_v0)] @@ -162,7 +168,12 @@ mod succinct_gateway { self.nonce.write(nonce + 1); - // TODO(#80): Implement Fee Vault and send fee + // Fee Vault + + let fee_vault_address = self.fee_vault_address.read(); + assert(!fee_vault_address.is_zero(), Errors::FEE_VAULT_NOT_INITIALIZED); + let fee_vault = IFeeVaultDispatcher { contract_address: fee_vault_address }; + fee_vault.deposit_native(callback_addr); request_hash } @@ -195,7 +206,13 @@ mod succinct_gateway { fee_amount: starknet::info::get_tx_info().unbox().max_fee, } ); - // TODO(#80): Implement Fee Vault and send fee + + // Fee Vault + + let fee_vault_address = self.fee_vault_address.read(); + assert(!fee_vault_address.is_zero(), Errors::FEE_VAULT_NOT_INITIALIZED); + let fee_vault = IFeeVaultDispatcher { contract_address: fee_vault_address }; + fee_vault.deposit_native(starknet::info::get_caller_address()); } /// If the call matches the currently verified function, returns the output. diff --git a/src/succinctx/interfaces.cairo b/src/succinctx/interfaces.cairo index 00bf504..1b58ad3 100644 --- a/src/succinctx/interfaces.cairo +++ b/src/succinctx/interfaces.cairo @@ -50,3 +50,28 @@ trait ISuccinctGateway { callback_calldata: Span, ); } + + +#[starknet::interface] +trait IFeeVault { + fn get_native_currency(self: @TContractState) -> ContractAddress; + fn set_native_currency(ref self: TContractState, _new_native_address: ContractAddress); + fn get_deductor_status(self: @TContractState, _deductor: ContractAddress) -> bool; + fn get_balances_infos( + self: @TContractState, _account: ContractAddress, _token: ContractAddress + ) -> u256; + fn add_deductor(ref self: TContractState, _deductor: ContractAddress); + fn remove_deductor(ref self: TContractState, _deductor: ContractAddress); + fn deposit_native(ref self: TContractState, _account: ContractAddress); + fn deposit( + ref self: TContractState, _account: ContractAddress, _token: ContractAddress, _amount: u256 + ); + fn deduct_native(ref self: TContractState, _account: ContractAddress); + fn deduct( + ref self: TContractState, _account: ContractAddress, _token: ContractAddress, _amount: u256 + ); + fn collect_native(ref self: TContractState, _to: ContractAddress, _amount: u256); + fn collect( + ref self: TContractState, _to: ContractAddress, _token: ContractAddress, _amount: u256 + ); +} diff --git a/src/succinctx/tests/test_fee_vault.cairo b/src/succinctx/tests/test_fee_vault.cairo new file mode 100644 index 0000000..66647df --- /dev/null +++ b/src/succinctx/tests/test_fee_vault.cairo @@ -0,0 +1,307 @@ +use blobstream_sn::succinctx::fee_vault::succinct_fee_vault; +use blobstream_sn::succinctx::function_registry::erc20_mock::{ + IMockERC20Dispatcher, IMockERC20DispatcherTrait, MockERC20 +}; +use blobstream_sn::succinctx::interfaces::{IFeeVaultDispatcher, IFeeVaultDispatcherTrait}; +use debug::PrintTrait; +use openzeppelin::tests::utils::constants::{OWNER, NEW_OWNER, SPENDER}; +use snforge_std::{ + declare, ContractClassTrait, start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy +}; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::get_caller_address; +const TOTAL_SUPPPLY: u256 = 0x100000000000000000000000000000001; + + +fn setup_contracts() -> (IMockERC20Dispatcher, IFeeVaultDispatcher) { + let token_class = declare('MockERC20'); + let token_calldata = array!['FeeToken', 'FT']; + let token_address = token_class.deploy(@token_calldata).unwrap(); + let fee_vault_class = declare('succinct_fee_vault'); + let fee_calldata = array![token_address.into(), OWNER().into()]; + let fee_vault_address = fee_vault_class.deploy(@fee_calldata).unwrap(); + ( + IMockERC20Dispatcher { contract_address: token_address }, + IFeeVaultDispatcher { contract_address: fee_vault_address } + ) +} + +#[test] +fn fee_vault_set_native_currency_address() { + let (_, fee_vault) = setup_contracts(); + let new_currency_address = contract_address_const::<0x12345>(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.set_native_currency(new_currency_address); + assert( + fee_vault.get_native_currency() == new_currency_address, 'wrong initial native currency' + ); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +#[should_panic(expected: ('Invalid token',))] +fn fee_vault_set_native_currency_address_fails_null_address() { + let (_, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + let new_currency_address = contract_address_const::<0>(); + fee_vault.set_native_currency(new_currency_address); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_vault_set_native_currency_fails_not_owner() { + let (_, fee_vault) = setup_contracts(); + let new_currency_address = contract_address_const::<0x1234>(); + fee_vault.set_native_currency(new_currency_address); +} + +#[test] +fn fee_vault_deductor_operations() { + let (_, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + // Adding a new deductor + fee_vault.add_deductor(SPENDER()); + assert(fee_vault.get_deductor_status(SPENDER()), 'deductor status not updated'); + + // Removing the same deductor + fee_vault.remove_deductor(SPENDER()); + assert(!fee_vault.get_deductor_status(SPENDER()), 'deductor status not updated'); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn fee_vault_add_deductor_fails_if_not_owner() { + let (_, fee_vault) = setup_contracts(); + // Adding a new deductor + fee_vault.add_deductor(SPENDER()); + assert(fee_vault.get_deductor_status(SPENDER()), 'deductor status not updated'); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn fee_vault_remove_deductor_fails_if_not_owner() { + let (_, fee_vault) = setup_contracts(); + // Adding a new deductor + fee_vault.remove_deductor(SPENDER()); + assert(!fee_vault.get_deductor_status(SPENDER()), 'deductor status not updated'); +} + +#[test] +fn fee_vault_deposit_native() { + let (erc20, fee_vault) = setup_contracts(); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + let fee = starknet::info::get_tx_info().unbox().max_fee.into(); + erc20.approve(fee_vault.contract_address, fee); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit_native(SPENDER()); + assert( + fee_vault.get_balances_infos(SPENDER(), erc20.contract_address) == fee, + 'balances not updated' + ); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +fn fee_vault_deposit() { + let (erc20, fee_vault) = setup_contracts(); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x10000); + assert( + fee_vault.get_balances_infos(SPENDER(), erc20.contract_address) == 0x10000, + 'balances deposit not updated' + ); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +#[should_panic(expected: ('Invalid account',))] +fn fee_vault_deposit_fails_if_null_account() { + let (erc20, fee_vault) = setup_contracts(); + fee_vault.deposit(contract_address_const::<0>(), erc20.contract_address, 0x10000); +} + +#[test] +#[should_panic(expected: ('Invalid token',))] +fn fee_vault_deposit_fails_if_null_token() { + let (_, fee_vault) = setup_contracts(); + fee_vault.deposit(SPENDER(), contract_address_const::<0>(), 0x10000); +} + +#[test] +#[should_panic(expected: ('Insufficent allowance',))] +fn fee_vault_deposit_fails_if_insufficent_allowance() { + let (erc20, fee_vault) = setup_contracts(); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x10000); + assert( + fee_vault.get_balances_infos(SPENDER(), erc20.contract_address) == 0x10000, + 'balances deposit not updated' + ); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +fn fee_vault_deduct_native() { + let (erc20, fee_vault) = setup_contracts(); + let fee = starknet::info::get_tx_info().unbox().max_fee.into(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.add_deductor(SPENDER()); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit_native(SPENDER()); + assert( + fee_vault.get_balances_infos(SPENDER(), erc20.contract_address) == fee, + 'balances deposit not updated' + ); + fee_vault.deduct_native(SPENDER()); + assert( + fee_vault.get_balances_infos(SPENDER(), erc20.contract_address) == 0, + 'balances deduct not updated' + ); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +fn fee_vault_deduct() { + let (erc20, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.add_deductor(SPENDER()); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x10000); + fee_vault.deduct(SPENDER(), erc20.contract_address, 0x8000); + assert( + fee_vault.get_balances_infos(SPENDER(), erc20.contract_address) == 0x10000 - 0x8000, + 'balances deduct not updated' + ); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +#[should_panic(expected: ('Only deductor allowed',))] +fn fee_vault_deduct_fails_if_not_deductor() { + let (erc20, fee_vault) = setup_contracts(); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x10000); + fee_vault.deduct(SPENDER(), erc20.contract_address, 0x8000); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +#[should_panic(expected: ('Insufficent balance',))] +fn fee_vault_deduct_fails_if_insufficent_balance() { + let (erc20, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.add_deductor(SPENDER()); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x1000); + fee_vault.deduct(SPENDER(), erc20.contract_address, 0x8000); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + + +#[test] +fn fee_vault_collect() { + let (erc20, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.add_deductor(SPENDER()); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x10000); + fee_vault.deduct(SPENDER(), erc20.contract_address, 0x8000); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.collect(OWNER(), erc20.contract_address, 0x10000); + assert(erc20.balance_of(OWNER()) == 0x10000, 'balance collect failed'); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + + +#[test] +fn fee_vault_collect_native() { + let (erc20, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.add_deductor(SPENDER()); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x10000); + fee_vault.deduct(SPENDER(), erc20.contract_address, 0x8000); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.collect_native(OWNER(), 0x10000); + assert(erc20.balance_of(OWNER()) == 0x10000, 'balance collect failed'); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn fee_vault_collect_fails_if_not_owner() { + let (erc20, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.add_deductor(SPENDER()); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x10000); + fee_vault.collect(SPENDER(), erc20.contract_address, 0x10000); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} + +#[test] +#[should_panic(expected: ('Insufficent balance',))] +fn fee_vault_collect_fails_if_insufficent_balance() { + let (erc20, fee_vault) = setup_contracts(); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.add_deductor(SPENDER()); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + erc20.mint_to(SPENDER(), 0x10000); + start_prank(CheatTarget::One(erc20.contract_address), SPENDER()); + erc20.approve(fee_vault.contract_address, 0x10000); + stop_prank(CheatTarget::One(erc20.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), SPENDER()); + fee_vault.deposit(SPENDER(), erc20.contract_address, 0x1000); + stop_prank(CheatTarget::One(fee_vault.contract_address)); + start_prank(CheatTarget::One(fee_vault.contract_address), OWNER()); + fee_vault.collect(OWNER(), erc20.contract_address, 0x10000); + stop_prank(CheatTarget::One(fee_vault.contract_address)); +} diff --git a/src/tests/common.cairo b/src/tests/common.cairo index b5eda6a..ead239a 100644 --- a/src/tests/common.cairo +++ b/src/tests/common.cairo @@ -1,6 +1,11 @@ +use blobstream_sn::succinctx::fee_vault::succinct_fee_vault; +use blobstream_sn::succinctx::function_registry::erc20_mock::{ + IMockERC20Dispatcher, IMockERC20DispatcherTrait, MockERC20 +}; use blobstream_sn::succinctx::function_registry::interfaces::{ IFunctionRegistryDispatcher, IFunctionRegistryDispatcherTrait }; +use blobstream_sn::succinctx::interfaces::{IFeeVaultDispatcher, IFeeVaultDispatcherTrait}; use openzeppelin::tests::utils::constants::OWNER; use snforge_std::{ declare, ContractClassTrait, start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy @@ -15,9 +20,21 @@ const HEADER_RANGE_DIGEST: u256 = 0xb646edd6dbb2e5482b2449404cf1888b8f4cd6958c79 const NEXT_HEADER_DIGEST: u256 = 0xfd6c88812a160ff288fe557111815b3433c539c77a3561086cfcdd9482bceb8; fn setup_base() -> ContractAddress { + // deploy the token associated with the fee vault + let token_class = declare('MockERC20'); + let token_calldata = array!['FeeToken', 'FT']; + let token_address = token_class.deploy(@token_calldata).unwrap(); + + // deploy the fee vault + let fee_vault_class = declare('succinct_fee_vault'); + let fee_calldata = array![token_address.into(), OWNER().into()]; + let fee_vault_address = fee_vault_class.deploy(@fee_calldata).unwrap(); + // deploy the succinct gateway let succinct_gateway_class = declare('succinct_gateway'); - let gateway_addr = succinct_gateway_class.deploy(@array![OWNER().into()]).unwrap(); + let gateway_addr = succinct_gateway_class + .deploy(@array![OWNER().into(), fee_vault_address.into()]) + .unwrap(); let gateway = IFunctionRegistryDispatcher { contract_address: gateway_addr }; // deploy the mock function verifier @@ -59,7 +76,17 @@ fn setup_spied() -> (ContractAddress, EventSpy) { fn setup_succinct_gateway() -> ContractAddress { + // deploy the token associated with the fee vault + let token_class = declare('MockERC20'); + let token_calldata = array!['FeeToken', 'FT']; + let token_address = token_class.deploy(@token_calldata).unwrap(); + + // deploy the fee vault + let fee_vault_class = declare('succinct_fee_vault'); + let fee_calldata = array![token_address.into(), OWNER().into()]; + let fee_vault_address = fee_vault_class.deploy(@fee_calldata).unwrap(); + let succinct_gateway_class = declare('succinct_gateway'); - let calldata = array![OWNER().into()]; + let calldata = array![OWNER().into(), fee_vault_address.into()]; succinct_gateway_class.deploy(@calldata).unwrap() }