diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f3c6a68..c9f3bca7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ on: - 'Scarb.toml' env: - SCARB_VERSION: 2.6.3 - FOUNDRY_VERSION: 0.19.0 + SCARB_VERSION: 2.6.4 + FOUNDRY_VERSION: 0.20.1 jobs: build: diff --git a/Scarb.lock b/Scarb.lock index b83946c8..f0cb3619 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -11,10 +11,16 @@ name = "governance" version = "0.3.0" dependencies = [ "cubit", + "openzeppelin", "snforge_std", ] +[[package]] +name = "openzeppelin" +version = "0.10.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3" + [[package]] name = "snforge_std" -version = "0.19.0" -source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.19.0#a3391dce5bdda51c63237032e6cfc64fb7a346d4" +version = "0.20.1" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.20.1#fea2db8f2b20148cc15ee34b08de12028eb42942" diff --git a/Scarb.toml b/Scarb.toml index 7438a2f3..46c50e4c 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -7,16 +7,24 @@ cairo-version = "2.6.3" [dependencies] cubit = { git = "https://github.com/influenceth/cubit.git", commit = "62756082bf2555d7ab25c69d9c7bc30574ff1ce8" } starknet = ">=1.3.0" -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.19.0" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.20.1" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" } [[target.starknet-contract]] [[tool.snforge.fork]] name = "MAINNET" -url = "http://34.22.208.73:6060/v0_6" +url = "http://34.22.208.73:6060/v0_7" block_id.tag = "Latest" [[tool.snforge.fork]] name = "GOERLI" -url = "http://34.22.208.73:6061/v0_6" +url = "http://34.22.208.73:6061/v0_7" block_id.tag = "Latest" + +[[tool.snforge.fork]] +name = "SEPOLIA" +url = "http://34.22.208.73:6062/v0_7" +block_id.tag = "Latest" + +RUST_BACKTRACE=1 diff --git a/src/lib.cairo b/src/lib.cairo index c46493d6..45044e1d 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -10,3 +10,4 @@ mod proposals; mod traits; mod types; mod upgrades; +mod treasury; diff --git a/src/treasury.cairo b/src/treasury.cairo new file mode 100644 index 00000000..7ba3ca29 --- /dev/null +++ b/src/treasury.cairo @@ -0,0 +1,237 @@ +use starknet::ContractAddress; +use governance::types::OptionType; + +#[starknet::interface] +trait ITreasury { + fn send_tokens_to_address( + ref self: TContractState, + receiver: ContractAddress, + amount: u256, + token_addr: ContractAddress + ) -> bool; + fn update_AMM_address(ref self: TContractState, new_amm_address: ContractAddress); + fn provide_liquidity_to_carm_AMM( + ref self: TContractState, + pooled_token_addr: ContractAddress, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + amount: u256 + ); + fn withdraw_liquidity( + ref self: TContractState, + pooled_token_addr: ContractAddress, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + lp_token_amount: u256 + ); + fn get_amm_address(self: @TContractState) -> ContractAddress; +} + +#[starknet::contract] +mod Treasury { + use core::starknet::event::EventEmitter; + use super::{OptionType}; + use core::num::traits::zero::Zero; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::access::ownable::interface::IOwnableTwoStep; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use openzeppelin::upgrades::interface::IUpgradeable; + use starknet::{ContractAddress, get_caller_address, get_contract_address, ClassHash}; + use governance::airdrop::{IAirdropDispatcher, IAirdropDispatcherTrait}; + use governance::traits::{ + IERC20Dispatcher, IERC20DispatcherTrait, IAMMDispatcher, IAMMDispatcherTrait + }; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + + #[abi(embed_v0)] + impl OwnableTwoStepImpl = OwnableComponent::OwnableTwoStepImpl; + impl InternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + amm_address: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + #[derive(starknet::Event, Drop)] + struct TokenSent { + receiver: ContractAddress, + token_addr: ContractAddress, + amount: u256 + } + + #[derive(starknet::Event, Drop)] + struct AMMAddressUpdated { + previous_address: ContractAddress, + new_amm_address: ContractAddress + } + + #[derive(starknet::Event, Drop)] + struct LiquidityProvided { + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + amount: u256 + } + + #[derive(starknet::Event, Drop)] + struct LiquidityWithdrawn { + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + lp_token_amount: u256 + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + TokenSent: TokenSent, + AMMAddressUpdated: AMMAddressUpdated, + LiquidityProvided: LiquidityProvided, + LiquidityWithdrawn: LiquidityWithdrawn, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + mod Errors { + const INSUFFICIENT_FUNDS: felt252 = 'Insufficient token balance'; + const INSUFFICIENT_POOLED_TOKEN: felt252 = 'Insufficient Pooled balance'; + const INSUFFICIENT_LP_TOKENS: felt252 = 'Insufficient LP token balance'; + const ADDRESS_ZERO_GOVERNANCE: felt252 = 'Governance addr is zero address'; + const ADDRESS_ZERO_AMM: felt252 = 'AMM addr is zero address'; + const ADDRESS_ALREADY_CHANGED: felt252 = 'New Address same as Previous'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + gov_contract_address: ContractAddress, + AMM_contract_address: ContractAddress + ) { + assert(gov_contract_address != zeroable::Zeroable::zero(), Errors::ADDRESS_ZERO_GOVERNANCE); + assert(AMM_contract_address != zeroable::Zeroable::zero(), Errors::ADDRESS_ZERO_AMM); + self.amm_address.write(AMM_contract_address); + self.ownable.initializer(gov_contract_address); + } + + #[abi(embed_v0)] + impl Treasury of super::ITreasury { + fn send_tokens_to_address( + ref self: ContractState, + receiver: ContractAddress, + amount: u256, + token_addr: ContractAddress + ) -> bool { + self.ownable.assert_only_owner(); + let token: IERC20Dispatcher = IERC20Dispatcher { contract_address: token_addr }; + assert(token.balanceOf(get_contract_address()) >= amount, Errors::INSUFFICIENT_FUNDS); + let status: bool = token.transfer(receiver, amount); + self.emit(TokenSent { receiver, token_addr, amount }); + return status; + } + + fn update_AMM_address(ref self: ContractState, new_amm_address: ContractAddress) { + self.ownable.assert_only_owner(); + assert(new_amm_address != zeroable::Zeroable::zero(), Errors::ADDRESS_ZERO_AMM); + assert(new_amm_address != self.amm_address.read(), Errors::ADDRESS_ALREADY_CHANGED); + let previous_address: ContractAddress = self.amm_address.read(); + self.amm_address.write(new_amm_address); + self.emit(AMMAddressUpdated { previous_address, new_amm_address }) + } + + fn provide_liquidity_to_carm_AMM( + ref self: ContractState, + pooled_token_addr: ContractAddress, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + amount: u256 + ) { + self.ownable.assert_only_owner(); + let carm_AMM: IAMMDispatcher = IAMMDispatcher { + contract_address: self.amm_address.read() + }; + + let pooled_token: IERC20Dispatcher = IERC20Dispatcher { + contract_address: pooled_token_addr + }; + + assert( + pooled_token.balanceOf(get_contract_address()) >= amount, + Errors::INSUFFICIENT_POOLED_TOKEN + ); + pooled_token.approve(self.amm_address.read(), amount); + + carm_AMM + .deposit_liquidity( + pooled_token_addr, quote_token_address, base_token_address, option_type, amount + ); + self + .emit( + LiquidityProvided { + quote_token_address, base_token_address, option_type, amount + } + ); + } + + fn withdraw_liquidity( + ref self: ContractState, + pooled_token_addr: ContractAddress, + quote_token_address: ContractAddress, + base_token_address: ContractAddress, + option_type: OptionType, + lp_token_amount: u256 + ) { + self.ownable.assert_only_owner(); + let carm_AMM: IAMMDispatcher = IAMMDispatcher { + contract_address: self.amm_address.read() + }; + + let lp_token_addr = carm_AMM + .get_lptoken_address_for_given_option( + quote_token_address, base_token_address, option_type + ); + let lp_token: IERC20Dispatcher = IERC20Dispatcher { contract_address: lp_token_addr }; + assert( + lp_token.balanceOf(get_contract_address()) >= lp_token_amount, + Errors::INSUFFICIENT_LP_TOKENS + ); + + carm_AMM + .withdraw_liquidity( + pooled_token_addr, + quote_token_address, + base_token_address, + option_type, + lp_token_amount + ); + self + .emit( + LiquidityWithdrawn { + quote_token_address, base_token_address, option_type, lp_token_amount + } + ); + } + + fn get_amm_address(self: @ContractState) -> ContractAddress { + self.amm_address.read() + } + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + } +} diff --git a/tests/add_options.cairo b/tests/add_options.cairo index 1400378d..488efb23 100644 --- a/tests/add_options.cairo +++ b/tests/add_options.cairo @@ -1,4 +1,4 @@ -use tests::basic::submit_44_signal_proposals; +use super::basic::submit_44_signal_proposals; use governance::traits::IAMM; use governance::contract::IGovernanceDispatcher; diff --git a/tests/lib.cairo b/tests/lib.cairo new file mode 100644 index 00000000..4499bc14 --- /dev/null +++ b/tests/lib.cairo @@ -0,0 +1,3 @@ +mod test_treasury; +mod basic; +mod add_options; diff --git a/tests/test_treasury.cairo b/tests/test_treasury.cairo new file mode 100644 index 00000000..452944e2 --- /dev/null +++ b/tests/test_treasury.cairo @@ -0,0 +1,170 @@ +mod testStorage { + use core::traits::TryInto; + use starknet::ContractAddress; + const zero_address: felt252 = 0; + const GOV_CONTRACT_ADDRESS: felt252 = + 0x0304256e5fade73a6fc8f49ed7c1c43ac34e6867426601b01204e1f7ba05b53d; + const AMM_CONTRACT_ADDRESS: felt252 = + 0x018890b58b08f341acd1292e8f67edfb01f539c835ef4a2176946a995fe794a5; +} + +use core::result::ResultTrait; +use core::serde::Serde; +use core::option::OptionTrait; +use core::traits::{TryInto, Into}; +use core::byte_array::ByteArray; +use cubit::f128::types::{Fixed, FixedTrait}; +use array::ArrayTrait; +use debug::PrintTrait; +use starknet::ContractAddress; +use snforge_std::{ + BlockId, declare, ContractClassTrait, ContractClass, prank, CheatSpan, CheatTarget, start_roll, + stop_roll, +}; +use governance::treasury::{ITreasuryDispatcher, ITreasuryDispatcherTrait}; +use governance::traits::{ + IERC20Dispatcher, IERC20DispatcherTrait, IAMMDispatcher, IAMMDispatcherTrait +}; +use openzeppelin::access::ownable::interface::{ + IOwnableTwoStep, IOwnableTwoStepDispatcherTrait, IOwnableTwoStepDispatcher +}; + + +fn get_important_addresses() -> (ContractAddress, ContractAddress, ContractAddress) { + let gov_contract_address: ContractAddress = testStorage::GOV_CONTRACT_ADDRESS + .try_into() + .unwrap(); + let AMM_contract_address: ContractAddress = testStorage::AMM_CONTRACT_ADDRESS + .try_into() + .unwrap(); + let contract = declare("Treasury"); + let mut calldata = ArrayTrait::new(); + gov_contract_address.serialize(ref calldata); + AMM_contract_address.serialize(ref calldata); + + // Precalculate the address to obtain the contract address before the constructor call (deploy) itself + let contract_address = contract.precalculate_address(@calldata); + + prank(CheatTarget::One(contract_address), gov_contract_address, CheatSpan::TargetCalls(1)); + let deployed_contract = contract.deploy(@calldata).unwrap(); + + return (gov_contract_address, AMM_contract_address, deployed_contract,); +} + + +#[test] +#[fork("SEPOLIA")] +fn test_transfer_token() { + let (gov_contract_address, _AMM_contract_address, treasury_contract_address) = + get_important_addresses(); + let user1: ContractAddress = 0x06730c211d67bb7c463190f10baa95529c82de2e32d79dd4cb3b185b6d0ddf86 + .try_into() + .unwrap(); + let user2: ContractAddress = '0xUser2'.try_into().unwrap(); + let token: ContractAddress = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + .try_into() + .unwrap(); + let decimal: u256 = 1_000000000000000000; + + prank(CheatTarget::One(token), user1, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: token }.transfer(treasury_contract_address, 1 * decimal); + + let user2_bal_before_transfer = IERC20Dispatcher { contract_address: token }.balanceOf(user2); + + prank( + CheatTarget::One(treasury_contract_address), gov_contract_address, CheatSpan::TargetCalls(1) + ); + ITreasuryDispatcher { contract_address: treasury_contract_address } + .send_tokens_to_address(user2, 1 * decimal, token); + + let user2_bal_after_transfer = IERC20Dispatcher { contract_address: token }.balanceOf(user2); + + assert(user2_bal_before_transfer != user2_bal_after_transfer, 'token transfer Error'); + assert(user2_bal_after_transfer == 1 * decimal, 'Transfer calculation error'); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +#[fork("SEPOLIA")] +fn test_send_tokens_to_address_by_unauthorized_caller() { + let (_gov_contract_address, _AMM_contract_address, treasury_contract_address) = + get_important_addresses(); + let user1: ContractAddress = 0x06730c211d67bb7c463190f10baa95529c82de2e32d79dd4cb3b185b6d0ddf86 + .try_into() + .unwrap(); + let user2: ContractAddress = '0xUser2'.try_into().unwrap(); + let token: ContractAddress = 0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7 + .try_into() + .unwrap(); + let decimal: u256 = 1_000000000000000000; + + prank(CheatTarget::One(token), user1, CheatSpan::TargetCalls(1)); + IERC20Dispatcher { contract_address: token }.transfer(treasury_contract_address, 1 * decimal); + + let user2_bal_before_transfer = IERC20Dispatcher { contract_address: token }.balanceOf(user2); + + prank(CheatTarget::One(treasury_contract_address), user1, CheatSpan::TargetCalls(1)); + ITreasuryDispatcher { contract_address: treasury_contract_address } + .send_tokens_to_address(user2, 1 * decimal, token); + + let user2_bal_after_transfer = IERC20Dispatcher { contract_address: token }.balanceOf(user2); + + assert(user2_bal_before_transfer != user2_bal_after_transfer, 'token transfer Error'); + assert(user2_bal_after_transfer == 1 * decimal, 'Transfer calculation error'); +} + +#[test] +fn test_update_AMM_contract() { + let (gov_contract_address, _AMM_contract_address, treasury_contract_address) = + get_important_addresses(); + let new_AMM_contract: ContractAddress = '0xnewAMMcontract'.try_into().unwrap(); + + prank( + CheatTarget::One(treasury_contract_address), gov_contract_address, CheatSpan::TargetCalls(1) + ); + ITreasuryDispatcher { contract_address: treasury_contract_address } + .update_AMM_address(new_AMM_contract); + + let recorded_AMM_addr = ITreasuryDispatcher { contract_address: treasury_contract_address } + .get_amm_address(); + assert(new_AMM_contract == recorded_AMM_addr, 'Error updating AMM address'); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_update_AMM_contract_by_unauthorized_caller() { + let (_gov_contract_address, _AMM_contract_address, treasury_contract_address) = + get_important_addresses(); + let user2: ContractAddress = '0xUser2'.try_into().unwrap(); + let new_AMM_contract: ContractAddress = '0xnewAMMcontract'.try_into().unwrap(); + + prank(CheatTarget::One(treasury_contract_address), user2, CheatSpan::TargetCalls(1)); + ITreasuryDispatcher { contract_address: treasury_contract_address } + .update_AMM_address(new_AMM_contract); +} + +#[test] +fn test_ownership_transfer() { + let (gov_contract_address, _AMM_contract_address, treasury_contract_address) = + get_important_addresses(); + let user2: ContractAddress = '0xUser2'.try_into().unwrap(); + + prank( + CheatTarget::One(treasury_contract_address), gov_contract_address, CheatSpan::TargetCalls(1) + ); + IOwnableTwoStepDispatcher { contract_address: treasury_contract_address } + .transfer_ownership(user2); + assert( + IOwnableTwoStepDispatcher { contract_address: treasury_contract_address } + .pending_owner() == user2, + 'Pending transfer failed' + ); + + prank(CheatTarget::One(treasury_contract_address), user2, CheatSpan::TargetCalls(1)); + IOwnableTwoStepDispatcher { contract_address: treasury_contract_address }.accept_ownership(); + assert( + IOwnableTwoStepDispatcher { contract_address: treasury_contract_address }.owner() == user2, + 'Ownership transfer failed' + ); +} +