diff --git a/.gitmodules b/.gitmodules index bfa79137..6e51684e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/solidity/lib/forge-std"] path = contracts/solidity/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "contracts/solidity/lib/openzeppelin-contracts-upgradeable"] + path = contracts/solidity/lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/Makefile b/Makefile index 92019ee1..430543db 100644 --- a/Makefile +++ b/Makefile @@ -21,14 +21,17 @@ ethereum-build: ethereum-clean ethereum-test: ethereum-clean @cd ./contracts/solidity/ && forge test -ethereum-deploy: ethereum-clean - @./contracts/solidity/deploy.sh +ethereum-deploy: ethereum-build + @. ./contracts/solidity/.env && . ./contracts/solidity/deploy.sh + +ethereum-upgrade: ethereum-build + @. ./contracts/solidity/.env && . ./contracts/solidity/upgrade.sh ethereum-set-escrow: - @./contracts/solidity/set_escrow.sh + @. ./contracts/solidity/.env && . ./contracts/solidity/set_escrow.sh ethereum-set-withdraw-selector: - @./contracts/solidity/set_withdraw_selector.sh + @. ./contracts/solidity/.env && . ./contracts/cairo/.env && . ./contracts/solidity/set_withdraw_selector.sh starknet-clean: @cd ./contracts/cairo/ && scarb clean @@ -40,9 +43,25 @@ starknet-test: starknet-clean @cd ./contracts/cairo/ && snforge test starknet-deploy: starknet-build - @./contracts/cairo/deploy.sh + @. ./contracts/cairo/.env && . ./contracts/cairo/deploy.sh + +starknet-upgrade: starknet-build + @. ./contracts/cairo/.env && . ./contracts/cairo/upgrade.sh +.ONESHELL: starknet-deploy-and-connect: starknet-build - @$(MAKE) starknet-deploy - @$(MAKE) ethereum-set-escrow - @$(MAKE) ethereum-set-withdraw-selector + @. ./contracts/solidity/.env && . ./contracts/cairo/.env + @. ./contracts/cairo/deploy.sh + @. ./contracts/solidity/set_escrow.sh + @. ./contracts/solidity/set_withdraw_selector.sh + +.ONESHELL: +deploy-all: + @. ./contracts/solidity/.env && . ./contracts/cairo/.env + @make ethereum-build + @. ./contracts/solidity/deploy.sh + @make starknet-build + @. ./contracts/cairo/deploy.sh + @. ./contracts/solidity/set_escrow.sh + @. ./contracts/solidity/set_withdraw_selector.sh + @. ./contracts/display_info.sh diff --git a/README.md b/README.md index d7cafec5..f6783ec7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ This will end up installing: - [Starknet Foundry](https://foundry-rs.github.io/starknet-foundry/) - Is a toolchain for developing Starknet smart contracts. - [Ethereum Foundry](https://book.getfoundry.sh/) - Is a toolchain for developing Ethereum smart contracts. +### Starknet dependencies + +- [OpenZeppelin cairo contracts](https://github.com/OpenZeppelin/cairo-contracts/) + ## Setting up a Starknet Testnet Wallet **This guide will help you declare and deploy contracts on a testnet. Please @@ -107,7 +111,7 @@ Follow the steps below to set up a testnet smart wallet using `starkli`: ### Ethereum smart contract -First, the Ethereum smart contracts must be deployed. For Ethereum the deployment process you will need to: +Once we have the dependencies installed, we can proceed. For the Ethereum the deployment process you will need to: 1. Create your `.env` file: you need to configure the following variables in your own .env file on the contracts/solidity/ folder. You can use the env.example file as a template for creating your .env file, paying special attention to the formats provided @@ -116,6 +120,7 @@ First, the Ethereum smart contracts must be deployed. For Ethereum the deploymen ETH_PRIVATE_KEY = private key of your ETH wallet ETHERSCAN_API_KEY = API Key to use etherscan to read the Ethereum blockchain SN_MESSAGING_ADDRESS = Starknet Messaging address + YAB_TRANSFER_PROXY_ADDRESS = Address of the Ethereum Proxy smart contract, this value is automatically created and/or updated after deploy.sh is executed ``` **NOTE**: @@ -134,6 +139,8 @@ First, the Ethereum smart contracts must be deployed. For Ethereum the deploymen make ethereum-deploy ``` + This will deploy a Proxy smart contract, a YABTransfer smart contract, and it will link them both. The purpose of having a proxy in front of our smart contract is so that it is upgradeable, by simply deploying another smart contract and changing the Proxy's stored address. + ### Starknet smart contracts After the Ethereum smart contract is deployed, the Starknet smart contracts must be declared and deployed. @@ -148,31 +155,28 @@ For this, you will need to: 1. Create your `.env` file: you need to configure the following variables in your own .env file on the contracts/solidity folder. You can use the env.example file as a template for creating your .env file, paying special attention to the formats provided - ``` - STARKNET_ACCOUNT = Absolute path of your starknet testnet account, created at the start of this README - STARKNET_KEYSTORE = Absolute path of your starknet testnet keystore, created at the start of this README + ```env + STARKNET_ACCOUNT = Path of your starknet testnet account, created at the start of this README + STARKNET_KEYSTORE = Path of your starknet testnet keystore, created at the start of this README SN_RPC_URL = Infura or Alchemy RPC URL - ETH_CONTRACT_ADDR = newly created ETH contract address + SN_ESCROW_OWNER = Public address of the owner of the Escrow contract MM_SN_WALLET_ADDR = Starknet wallet of the MarketMaker - WITHDRAW_NAME = The exact name of the withdraw function that is called from L1, case sensitive. Example: withdraw_fallback - HERODOTUS_FACTS_REGISTRY = Herodotus' Facts Registry Smart Contract in Starknet + WITHDRAW_NAME = Exact name of the withdraw function that is called from L1, case sensitive. Example: withdraw_fallback MM_ETHEREUM_WALLET = Ethereum wallet of the MarketMaker - NATIVE_TOKEN_ETH_STARKNET = Ethereum's erc20 token handler contract in Starknet - ESCROW_CONTRACT_ADDRESS = Address of the Starknet smart contract, this value should be empty, and is automatically updated after deploy.sh is run + NATIVE_TOKEN_ETH_STARKNET = Ethereum's erc20 token handler contract in Starknet, this value is automatically updated after solidity/deploy.sh is run + YAB_TRANSFER_PROXY_ADDRESS = Address of ETH smart contract Proxy + ESCROW_CONTRACT_ADDRESS = Address of the Starknet smart contract, this value is automatically updated after cairo/deploy.sh is run ``` **Note** - - Herodotus Facts Registry: - - Starknet Goerli: `0x01b2111317EB693c3EE46633edd45A4876db14A3a53ACDBf4E5166976d8e869d` - - Starknet Sepolia: `0x07d3550237ecf2d6ddef9b78e59b38647ee511467fe000ce276f245a006b40bc` - - Starknet Mainnet: `0x014bf62fadb41d8f899bb5afeeb2da486fcfd8431852def56c5f10e45ae72765` + - SN_ESCROW_OWNER is the only one who can perform upgrades of the smart contract. If not defined, this value will be set (in deploy.sh) to the deployer of the smart contract. 2. Declare and Deploy: We sequentially declare and deploy the contracts, and connect it to our Ethereum smart contract. -### First alternative: automatic deploy and connect of Escrow and YABTransfer. +### First alternative: automatic deploy and connect of Escrow and YABTransfer ```bash - make starknet-deploy-and-connect + make starknet-deploy-and-connect ``` This make target consists of 4 steps: @@ -200,7 +204,7 @@ This may be better suited for you if you plan to change some of the automaticall To do this, you can use - ``` + ```bash make ethereum-set-escrow ``` @@ -211,12 +215,84 @@ This may be better suited for you if you plan to change some of the automaticall Ethereum's smart contract has another variable that must be configured, _EscrowWithdrawSelector_, which is for specifying the _withdraw_ function's name in the Starknet Escrow smart contract. You can set and change Ethereum's _EscrowWithdrawSelector_ variable, doing the following: - ``` + ```bash make ethereum-set-withdraw-selector ``` This script uses the WITHDRAW_NAME .env variable to automatically generate the selector in the necesary format + +### Note on Starknet Smart Contract + +_Note: this is a temporary solution, there is WIP on a better solution_ + +If you want to use the Herodotus version of the smart contract, rename the `escrow_herodotus.cairo` into `escrow.cairo`. Then you must also set the following .env variable before using any deployment script: +```env + HERODOTUS_FACTS_REGISTRY = Herodotus' Facts Registry Smart Contract in Starknet +``` + +**Note** + - Herodotus Facts Registry: + - Starknet Goerli: `0x01b2111317EB693c3EE46633edd45A4876db14A3a53ACDBf4E5166976d8e869d` + - Starknet Sepolia: `0x07d3550237ecf2d6ddef9b78e59b38647ee511467fe000ce276f245a006b40bc` + - Starknet Mainnet: `0x014bf62fadb41d8f899bb5afeeb2da486fcfd8431852def56c5f10e45ae72765` + ## Recap After following this complete README, we should have an ETH smart contract as well as a Starknet smart contract, both connected to act as a bridge between these two chains. + +## Upgrade Contracts in Testnet + +### Ethereum + +After deploying the `YABTransfer` contract, you can perform upgrades to it. To do this you must: + +1. Configure the `contracts/solidity/.env` file. + + ``` + ETH_RPC_URL = Infura or Alchemy RPC URL + ETH_PRIVATE_KEY = private key of your ETH wallet + ETHERSCAN_API_KEY = API Key to use etherscan to read the Ethereum blockchain + SN_MESSAGING_ADDRESS = Starknet Messaging address + YAB_TRANSFER_PROXY_ADDRESS = Address of the Ethereum Proxy smart contract, this value is automatically created and/or updated after deploy.sh is executed + ``` + +2. Use the Makefile command to upgrade `YABTransfer` contract + + ```bash + make ethereum-upgrade + ``` + + **Note** + - You must be the **owner** of the contract to upgrade it. + - This command will: + - Rebuild `YABTransfer.sol` + - Deploy the new contract to the network + - Utilize Foundry to upgrade the contract by changing the proxy's pointing address to the newly deployed contract + + +### Starknet + +If you want to upgrade a previously deployed `Escrow` contract, it is possible through a command. We will perform the upgrade using the `starkli` tool, so the same configuration used for deployment is necessary. + +1. Configure `contracts/cairo/.env` file. + + ```env + STARKNET_ACCOUNT = Path of your starknet testnet account, created at the start of this README + STARKNET_KEYSTORE = Path of your starknet testnet keystore, created at the start of this README + ESCROW_CONTRACT_ADDRESS = You can either set an escrow address manually, or use the value automatically set by deploying the Escrow, as mentioned previously + ``` + +2. Use the Makefile command to upgrade `Escrow` contract + + ```bash + make starknet-upgrade + ``` + + **Note** + +- You must be the **owner** of the contract to upgrade it. +- This command will: + - **rebuild** `Escrow.cairo` + - **declare** it on Starknet + - Call the external **upgrade()** function with the new class hash diff --git a/contracts/cairo/.env.example b/contracts/cairo/.env.example index 34bfcff6..0ca67962 100644 --- a/contracts/cairo/.env.example +++ b/contracts/cairo/.env.example @@ -1,10 +1,11 @@ +## Starkli STARKNET_ACCOUNT= STARKNET_KEYSTORE= SN_RPC_URL= -ETH_CONTRACT_ADDR= #in hexa with the 0x prefix, with upper and lower cases like shown by etherscan + +## Required for Escrow Contract +SN_ESCROW_OWNER= #in lowercase hexa with the 0x prefix MM_SN_WALLET_ADDR= #in lowercase hexa with the 0x prefix WITHDRAW_NAME= #must match the exact name of the function that withdraws from the starknet smart contract -HERODOTUS_FACTS_REGISTRY=<0x01b2111317EB693c3EE46633edd45A4876db14A3a53ACDBf4E5166976d8e869d|0x07d3550237ecf2d6ddef9b78e59b38647ee511467fe000ce276f245a006b40bc|0x014bf62fadb41d8f899bb5afeeb2da486fcfd8431852def56c5f10e45ae72765> # Goerli | Sepolia | Mainnet MM_ETHEREUM_WALLET= #in lowercase hexa with the 0x prefix NATIVE_TOKEN_ETH_STARKNET= #in lowercase hexa with the 0x prefix -ESCROW_CONTRACT_ADDRESS= #this value is automatically updated after deploy.sh is run diff --git a/contracts/cairo/.gitignore b/contracts/cairo/.gitignore index ef21a7cc..7d2c587c 100644 --- a/contracts/cairo/.gitignore +++ b/contracts/cairo/.gitignore @@ -1,2 +1,3 @@ target -.snfoundry_cache \ No newline at end of file +.snfoundry_cache +.env diff --git a/contracts/cairo/Scarb.lock b/contracts/cairo/Scarb.lock index 5d950cfe..b1710c1d 100644 --- a/contracts/cairo/Scarb.lock +++ b/contracts/cairo/Scarb.lock @@ -1,6 +1,11 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "openzeppelin" +version = "0.8.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.8.0#c23e8e96de60e6e3159b1ff8591a1187269c0eb7" + [[package]] name = "snforge_std" version = "0.1.0" @@ -10,5 +15,6 @@ source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.12.0#0c3 name = "yab" version = "0.1.0" dependencies = [ + "openzeppelin", "snforge_std", ] diff --git a/contracts/cairo/Scarb.toml b/contracts/cairo/Scarb.toml index 158660e4..ffa6843d 100644 --- a/contracts/cairo/Scarb.toml +++ b/contracts/cairo/Scarb.toml @@ -5,6 +5,7 @@ version = "0.1.0" [dependencies] starknet = "2.3.1" snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.12.0" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.0"} [[target.starknet-contract]] sierra = true diff --git a/contracts/cairo/deploy.sh b/contracts/cairo/deploy.sh index a05b6011..4d249cf3 100755 --- a/contracts/cairo/deploy.sh +++ b/contracts/cairo/deploy.sh @@ -2,41 +2,88 @@ # ANSI format GREEN='\e[32m' -PURPLE='\033[1;34m' +CYAN='\033[36m' PINK='\033[1;35m' +ORANGE='\033[1;33m' +RED='\033[0;31m' COLOR_RESET='\033[0m' -cd "$(dirname "$0")" - -if [ -f .env ]; then - echo "Sourcing cairo/.env file..." - source .env -else - echo "Error: cairo/.env file not found!" +if [ -z "$STARKNET_ACCOUNT" ]; then + echo "\n${RED}ERROR:${COLOR_RESET}" + echo "STARKNET_ACCOUNT Variable is empty. Aborting execution.\n" + exit 1 +fi +if [ -z "$STARKNET_KEYSTORE" ]; then + echo "\n${RED}ERROR:${COLOR_RESET}" + echo "STARKNET_KEYSTORE Variable is empty. Aborting execution.\n" + exit 1 +fi +if [ -z "$MM_SN_WALLET_ADDR" ]; then + echo "\n${RED}ERROR:${COLOR_RESET}" + echo "MM_SN_WALLET_ADDR Variable is empty. Aborting execution.\n" + exit 1 +fi +if [ -z "$NATIVE_TOKEN_ETH_STARKNET" ]; then + echo "\n${RED}ERROR:${COLOR_RESET}" + echo "NATIVE_TOKEN_ETH_STARKNET Variable is empty. Aborting execution.\n" + exit 1 +fi +if [ -z "$YAB_TRANSFER_PROXY_ADDRESS" ]; then + echo "\n${RED}ERROR:${COLOR_RESET}" + echo "YAB_TRANSFER_PROXY_ADDRESS Variable is empty. Aborting execution.\n" + exit 1 +fi +if [ -z "$MM_ETHEREUM_WALLET" ]; then + echo "\n${RED}ERROR:${COLOR_RESET}" + echo "MM_SN_MM_ETHEREUM_WALLETWALLET_ADDR Variable is empty. Aborting execution.\n" exit 1 fi -echo -e "${GREEN}\n=> [SN] Declare Escrow${COLOR_RESET}" + +echo "${GREEN}\n=> [SN] Declaring Escrow${COLOR_RESET}" ESCROW_CLASS_HASH=$(starkli declare \ --account $STARKNET_ACCOUNT --keystore $STARKNET_KEYSTORE \ - --watch target/dev/yab_Escrow.contract_class.json) + --watch contracts/cairo/target/dev/yab_Escrow.contract_class.json) + + +if [ -z "$ESCROW_CLASS_HASH" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "ESCROW_CLASS_HASH Variable is empty. Aborting execution.\n" + exit 1 +fi -echo -e "- ${PURPLE}[SN] Escrow ClassHash: $ESCROW_CLASS_HASH${COLOR_RESET}" -echo -e "- ${PURPLE}[SN] Herodotus Facts Registry: $HERODOTUS_FACTS_REGISTRY${COLOR_RESET}" -echo -e "- ${PURPLE}[SN] Market Maker: $MM_SN_WALLET_ADDR${COLOR_RESET}" -echo -e "- ${PURPLE}[SN] Ethereum ContractAddress $NATIVE_TOKEN_ETH_STARKNET${COLOR_RESET}" -echo -e "- ${PINK}[ETH] Ethereum ContractAddress: $ETH_CONTRACT_ADDR${COLOR_RESET}" -echo -e "- ${PINK}[ETH] Market Maker: $MM_ETHEREUM_WALLET${COLOR_RESET}" +if [ -z "$SN_ESCROW_OWNER" ]; then + echo "" #\n + printf "${ORANGE}WARNING:${COLOR_RESET} no SN_ESCROW_OWNER defined in .env, declaring deployer as the owner of the contract\n" + SN_ESCROW_OWNER=$(cat "$STARKNET_ACCOUNT" | grep '"address"' | sed -E 's/.*"address": "([^"]+)".*/\1/') +fi -echo -e "${GREEN}\n=> [SN] Deploy Escrow${COLOR_RESET}" + +printf "${GREEN}\n=> [SN] Escrow Declared${COLOR_RESET}\n" + +printf "${CYAN}[SN] Escrow ClassHash: $ESCROW_CLASS_HASH${COLOR_RESET}\n" +printf "${CYAN}[SN] Market Maker SN Wallet: $MM_SN_WALLET_ADDR${COLOR_RESET}\n" +printf "${CYAN}[SN] Ethereum ERC20 ContractAddress $NATIVE_TOKEN_ETH_STARKNET${COLOR_RESET}\n" +printf "${PINK}[ETH] YABTransfer Proxy Address: $YAB_TRANSFER_PROXY_ADDRESS${COLOR_RESET}\n" +printf "${PINK}[ETH] Market Maker ETH Wallet: $MM_ETHEREUM_WALLET${COLOR_RESET}\n" + +printf "${GREEN}\n=> [SN] Deploying Escrow${COLOR_RESET}\n" ESCROW_CONTRACT_ADDRESS=$(starkli deploy \ --account $STARKNET_ACCOUNT --keystore $STARKNET_KEYSTORE \ --watch $ESCROW_CLASS_HASH \ - $HERODOTUS_FACTS_REGISTRY \ - $ETH_CONTRACT_ADDR \ + $SN_ESCROW_OWNER \ + $YAB_TRANSFER_PROXY_ADDRESS \ $MM_ETHEREUM_WALLET \ $MM_SN_WALLET_ADDR \ $NATIVE_TOKEN_ETH_STARKNET) -echo -e "- ${PURPLE}[SN] Escrow ContractAddress: $ESCROW_CONTRACT_ADDRESS${COLOR_RESET}" +echo $ESCROW_CONTRACT_ADDRESS + +printf "${GREEN}\n=> [SN] Escrow Deployed${COLOR_RESET}\n" + +printf "${CYAN}[SN] Escrow Address: $ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" -sed -i "s/^ESCROW_CONTRACT_ADDRESS=.*/ESCROW_CONTRACT_ADDRESS=$ESCROW_CONTRACT_ADDRESS/" ".env" || echo "ESCROW_CONTRACT_ADDRESS=$ESCROW_CONTRACT_ADDRESS" >> ".env" +echo "\nIf you now wish to finish the configuration of this deploy, you will need to run the following commands:" +echo "export YAB_TRANSFER_PROXY_ADDRESS=$YAB_TRANSFER_PROXY_ADDRESS" +echo "export ESCROW_CONTRACT_ADDRESS=$ESCROW_CONTRACT_ADDRESS" +echo "make ethereum-set-escrow" +echo "make ethereum-set-withdraw-selector" diff --git a/contracts/cairo/src/escrow.cairo b/contracts/cairo/src/escrow.cairo index 66b1baff..b64a4bac 100644 --- a/contracts/cairo/src/escrow.cairo +++ b/contracts/cairo/src/escrow.cairo @@ -1,4 +1,4 @@ -use starknet::{ContractAddress, EthAddress}; +use starknet::{ContractAddress, ClassHash, EthAddress}; #[derive(Copy, Drop, Serde, starknet::Store)] struct Order { @@ -19,26 +19,31 @@ trait IEscrow { fn get_order_fee(self: @ContractState, order_id: u256) -> u256; - fn withdraw(ref self: ContractState, order_id: u256, block: u256, slot: u256); - - fn get_herodotus_facts_registry_contract(self: @ContractState) -> ContractAddress; fn get_eth_transfer_contract(self: @ContractState) -> EthAddress; fn get_mm_ethereum_contract(self: @ContractState) -> EthAddress; fn get_mm_starknet_contract(self: @ContractState) -> ContractAddress; - fn set_herodotus_facts_registry_contract( - ref self: ContractState, new_contract: ContractAddress - ); + fn set_eth_transfer_contract(ref self: ContractState, new_contract: EthAddress); fn set_mm_ethereum_contract(ref self: ContractState, new_contract: EthAddress); fn set_mm_starknet_contract(ref self: ContractState, new_contract: ContractAddress); + + fn pause(ref self: ContractState); + fn unpause(ref self: ContractState); + fn pause_state(ref self: ContractState) -> bool; } #[starknet::contract] mod Escrow { use super::{IEscrow, Order}; + use openzeppelin::{ + access::ownable::OwnableComponent, + upgrades::{UpgradeableComponent, interface::IUpgradeable}, + security::PausableComponent + }; use starknet::{ - ContractAddress, EthAddress, get_caller_address, get_contract_address, get_block_timestamp + ContractAddress, EthAddress, ClassHash, get_caller_address, get_contract_address, + get_block_timestamp }; use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -46,6 +51,24 @@ mod Escrow { IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait }; + /// Components + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + + /// (Ownable) + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + /// (Upgradeable) + impl InternalImpl = UpgradeableComponent::InternalImpl; + + // Pausable + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + // https://github.com/starknet-io/starknet-addresses // MAINNET = GOERLI = GOERLI2 // 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 @@ -56,7 +79,13 @@ mod Escrow { #[derive(Drop, starknet::Event)] enum Event { Withdraw: Withdraw, - SetOrder: SetOrder + SetOrder: SetOrder, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + PausableEvent: PausableComponent::Event } #[derive(Drop, starknet::Event)] @@ -76,38 +105,49 @@ mod Escrow { #[storage] struct Storage { - owner: ContractAddress, current_order_id: u256, orders: LegacyMap::, orders_used: LegacyMap::, orders_senders: LegacyMap::, orders_timestamps: LegacyMap::, - herodotus_facts_registry_contract: ContractAddress, eth_transfer_contract: EthAddress, // our transfer contract in L1 mm_ethereum_wallet: EthAddress, mm_starknet_wallet: ContractAddress, - native_token_eth_starknet: ContractAddress + native_token_eth_starknet: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + pausable: PausableComponent::Storage } #[constructor] fn constructor( ref self: ContractState, - herodotus_facts_registry_contract: ContractAddress, + owner: ContractAddress, eth_transfer_contract: EthAddress, mm_ethereum_wallet: EthAddress, mm_starknet_wallet: ContractAddress, native_token_eth_starknet: ContractAddress ) { - self.owner.write(get_caller_address()); + self.ownable.initializer(owner); self.current_order_id.write(0); - self.herodotus_facts_registry_contract.write(herodotus_facts_registry_contract); self.eth_transfer_contract.write(eth_transfer_contract); self.mm_ethereum_wallet.write(mm_ethereum_wallet); self.mm_starknet_wallet.write(mm_starknet_wallet); self.native_token_eth_starknet.write(native_token_eth_starknet); } + #[external(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); + } + } + #[external(v0)] impl Escrow of IEscrow { fn get_order(self: @ContractState, order_id: u256) -> Order { @@ -115,6 +155,7 @@ mod Escrow { } fn set_order(ref self: ContractState, order: Order) -> u256 { + self.pausable.assert_not_paused(); assert(order.amount > 0, 'Amount must be greater than 0'); let payment_amount = order.amount + order.fee; @@ -145,7 +186,8 @@ mod Escrow { } fn cancel_order(ref self: ContractState, order_id: u256) { - assert(!self.orders_used.read(order_id), 'Order withdrew or nonexistent'); + self.pausable.assert_not_paused(); + assert(!self.orders_used.read(order_id), 'Order withdrawn or nonexistent'); assert( get_block_timestamp() - self.orders_timestamps.read(order_id) > 43200, 'Not enough time has passed' @@ -169,60 +211,6 @@ mod Escrow { order.fee } - fn withdraw(ref self: ContractState, order_id: u256, block: u256, slot: u256) { - assert(!self.orders_used.read(order_id), 'Order already withdrawed'); - - // Read transfer info from the facts registry - // struct TransferInfo { - // uint256 destAddress; - // uint256 amount; - // bool isUsed; - // } - - let mut slot_1 = slot.clone(); - slot_1 += 1; - - let slot_0 = slot; - - // Slot n contains the address of the recipient - let slot_0_value = IEVMFactsRegistryDispatcher { - contract_address: self.herodotus_facts_registry_contract.read() - } - .get_slot_value(self.eth_transfer_contract.read().into(), block, slot_0) - .unwrap(); - - let recipient_address: felt252 = slot_0_value - .try_into() - .expect('Invalid address parse felt252'); - let recipient_address: EthAddress = recipient_address - .try_into() - .expect('Invalid address parse EthAddres'); - - let order = self.orders.read(order_id); - assert(order.recipient_address == recipient_address, 'recipient_address not match L1'); - - // Slot n+1 contains the amount and isUsed - let amount = IEVMFactsRegistryDispatcher { - contract_address: self.herodotus_facts_registry_contract.read() - } - .get_slot_value(self.eth_transfer_contract.read().into(), block, slot_1) - .unwrap(); - - assert(order.amount == amount, 'amount not match L1'); - - self.orders_used.write(order_id, true); - let payment_amount = order.amount + order.fee; - - IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() } - .transfer(self.mm_starknet_wallet.read(), payment_amount); - - self.emit(Withdraw { order_id, address: self.mm_starknet_wallet.read(), amount }); - } - - fn get_herodotus_facts_registry_contract(self: @ContractState) -> ContractAddress { - self.herodotus_facts_registry_contract.read() - } - fn get_eth_transfer_contract(self: @ContractState) -> EthAddress { self.eth_transfer_contract.read() } @@ -235,40 +223,51 @@ mod Escrow { self.mm_starknet_wallet.read() } - fn set_herodotus_facts_registry_contract( - ref self: ContractState, new_contract: ContractAddress - ) { - assert(self.owner.read() == get_caller_address(), 'Only owner allowed'); - self.herodotus_facts_registry_contract.write(new_contract); - } - fn set_eth_transfer_contract(ref self: ContractState, new_contract: EthAddress) { - assert(self.owner.read() == get_caller_address(), 'Only owner allowed'); + self.pausable.assert_not_paused(); + self.ownable.assert_only_owner(); self.eth_transfer_contract.write(new_contract); } fn set_mm_ethereum_contract(ref self: ContractState, new_contract: EthAddress) { - assert(self.owner.read() == get_caller_address(), 'Only owner allowed'); + self.pausable.assert_not_paused(); + self.ownable.assert_only_owner(); self.mm_ethereum_wallet.write(new_contract); } fn set_mm_starknet_contract(ref self: ContractState, new_contract: ContractAddress) { - assert(self.owner.read() == get_caller_address(), 'Only owner allowed'); + self.pausable.assert_not_paused(); + self.ownable.assert_only_owner(); self.mm_starknet_wallet.write(new_contract); } + + fn pause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._pause(); + } + + fn unpause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._unpause(); + } + + fn pause_state(ref self: ContractState) -> bool { + self.pausable.is_paused() + } } #[l1_handler] - fn withdraw_fallback( + fn withdraw( ref self: ContractState, from_address: felt252, order_id: u256, recipient_address: EthAddress, amount: u256 ) { + self.pausable.assert_not_paused(); let eth_transfer_contract_felt: felt252 = self.eth_transfer_contract.read().into(); - assert(eth_transfer_contract_felt == from_address, 'Only ETH_TRANSFER_CONTRACT'); - assert(!self.orders_used.read(order_id), 'Order already withdrawed'); + assert(from_address == eth_transfer_contract_felt, 'Only YAB_TRANSFER_CONTRACT'); + assert(!self.orders_used.read(order_id), 'Order already withdrawn'); let order = self.orders.read(order_id); assert(order.recipient_address == recipient_address, 'recipient_address not match L1'); diff --git a/contracts/cairo/src/lib.cairo b/contracts/cairo/src/lib.cairo index 8b6e1793..a7188ff1 100644 --- a/contracts/cairo/src/lib.cairo +++ b/contracts/cairo/src/lib.cairo @@ -8,12 +8,18 @@ mod interfaces { mod mocks { mod mock_EVMFactsRegistry; + mod mock_Escrow_changed_functions; + mod mock_pausableEscrow; + } #[cfg(test)] mod tests { - mod test_escrow; - + mod test_escrow_allowance; + mod test_escrow_cancel; + mod test_escrow_pause; + mod test_escrow_upgrade; + mod test_escrow_ownable; mod utils { mod constants; } diff --git a/contracts/cairo/src/mocks/mock_Escrow_changed_functions.cairo b/contracts/cairo/src/mocks/mock_Escrow_changed_functions.cairo new file mode 100644 index 00000000..e3897149 --- /dev/null +++ b/contracts/cairo/src/mocks/mock_Escrow_changed_functions.cairo @@ -0,0 +1,337 @@ +use starknet::{ContractAddress, ClassHash, EthAddress}; + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct Order { + recipient_address: EthAddress, + amount: u256, + fee: u256 +} + +#[starknet::interface] +trait IEscrow_mock_changed_functions { + fn get_orderV2(self: @ContractState, order_id: u256) -> Order; + + fn set_orderV2(ref self: ContractState, order: Order) -> u256; + + fn cancel_order(ref self: ContractState, order_id: u256); + + fn get_order_used(self: @ContractState, order_id: u256) -> bool; + + fn get_order_fee(self: @ContractState, order_id: u256) -> u256; + + fn withdraw(ref self: ContractState, order_id: u256, block: u256, slot: u256); + + fn get_herodotus_facts_registry_contract(self: @ContractState) -> ContractAddress; + fn get_eth_transfer_contract(self: @ContractState) -> EthAddress; + fn get_mm_ethereum_contract(self: @ContractState) -> EthAddress; + fn get_mm_starknet_contract(self: @ContractState) -> ContractAddress; + fn set_herodotus_facts_registry_contract( + ref self: ContractState, new_contract: ContractAddress + ); + fn get_mm_starknet_contractv2(self: @ContractState) -> ContractAddress; + fn set_herodotus_facts_registry_contractv2( + ref self: ContractState, new_contract: ContractAddress + ); + fn set_eth_transfer_contract(ref self: ContractState, new_contract: EthAddress); + fn set_mm_ethereum_contract(ref self: ContractState, new_contract: EthAddress); + fn set_mm_starknet_contract(ref self: ContractState, new_contract: ContractAddress); + + fn new_mock_function(self: @ContractState) -> bool; +} + +#[starknet::contract] +mod Escrow_mock_changed_functions { + use super::{IEscrow_mock_changed_functions, Order}; + + use openzeppelin::{ + access::ownable::OwnableComponent, + upgrades::{UpgradeableComponent, interface::IUpgradeable} + }; + use starknet::{ + ContractAddress, EthAddress, ClassHash, get_caller_address, get_contract_address, + get_block_timestamp + }; + + use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use yab::interfaces::IEVMFactsRegistry::{ + IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait + }; + + /// Components + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + /// (Ownable) + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + /// (Upgradeable) + impl InternalImpl = UpgradeableComponent::InternalImpl; + + // https://github.com/starknet-io/starknet-addresses + // MAINNET = GOERLI = GOERLI2 + // 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + // const NATIVE_TOKEN: felt252 = + // 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Withdraw: Withdraw, + SetOrder: SetOrder, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[derive(Drop, starknet::Event)] + struct SetOrder { + order_id: u256, + recipient_address: EthAddress, + amount: u256, + fee: u256 + } + + #[derive(Drop, starknet::Event)] + struct Withdraw { + order_id: u256, + address: ContractAddress, + amount: u256, + } + + #[storage] + struct Storage { + current_order_id: u256, + orders: LegacyMap::, + orders_used: LegacyMap::, + orders_senders: LegacyMap::, + orders_timestamps: LegacyMap::, + herodotus_facts_registry_contract: ContractAddress, + eth_transfer_contract: EthAddress, // our transfer contract in L1 + mm_ethereum_wallet: EthAddress, + mm_starknet_wallet: ContractAddress, + native_token_eth_starknet: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[constructor] + fn constructor( + ref self: ContractState, + owner: ContractAddress, + herodotus_facts_registry_contract: ContractAddress, + eth_transfer_contract: EthAddress, + mm_ethereum_wallet: EthAddress, + mm_starknet_wallet: ContractAddress, + native_token_eth_starknet: ContractAddress + ) { + self.ownable.initializer(owner); + + self.current_order_id.write(0); + self.herodotus_facts_registry_contract.write(herodotus_facts_registry_contract); + self.eth_transfer_contract.write(eth_transfer_contract); + self.mm_ethereum_wallet.write(mm_ethereum_wallet); + self.mm_starknet_wallet.write(mm_starknet_wallet); + self.native_token_eth_starknet.write(native_token_eth_starknet); + } + + #[external(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); + } + } + + #[external(v0)] + impl Escrow_mock_changed_functions of IEscrow_mock_changed_functions { + fn get_orderV2(self: @ContractState, order_id: u256) -> Order { + self.orders.read(order_id) + } + + fn set_orderV2(ref self: ContractState, order: Order) -> u256 { + assert(order.amount > 0, 'Amount must be greater than 0'); + + let mut order_id = self.current_order_id.read(); + self.orders.write(order_id, order); + self.orders_used.write(order_id, false); + self.orders_senders.write(order_id, get_caller_address()); + self.orders_timestamps.write(order_id, get_block_timestamp()); + let payment_amount = order.amount + order.fee; + + IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() } + .transferFrom(get_caller_address(), get_contract_address(), payment_amount); + + self + .emit( + SetOrder { + order_id, + recipient_address: order.recipient_address, + amount: order.amount, + fee: order.fee + } + ); + + self.current_order_id.write(order_id + 1); + order_id + } + + fn cancel_order(ref self: ContractState, order_id: u256) { + assert(!self.orders_used.read(order_id), 'Order already withdrawed'); + assert( + get_block_timestamp() - self.orders_timestamps.read(order_id) < 43200, + 'Didnt passed enough time' + ); + + let sender = self.orders_senders.read(order_id); + assert(sender == get_caller_address(), 'Only sender allowed'); + let order = self.orders.read(order_id); + let payment_amount = order.amount + order.fee; + + IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() } + .transfer(sender, payment_amount); + } + + fn get_order_used(self: @ContractState, order_id: u256) -> bool { + self.orders_used.read(order_id) + } + + fn get_order_fee(self: @ContractState, order_id: u256) -> u256 { + let order: Order = self.orders.read(order_id); + order.fee + } + + fn withdraw(ref self: ContractState, order_id: u256, block: u256, slot: u256) { + assert(!self.orders_used.read(order_id), 'Order already withdrawed'); + + // Read transfer info from the facts registry + // struct TransferInfo { + // uint256 destAddress; + // uint256 amount; + // bool isUsed; + // } + + let mut slot_1 = slot.clone(); + slot_1 += 1; + + let slot_0 = slot; + + // Slot n contains the address of the recipient + let slot_0_value = IEVMFactsRegistryDispatcher { + contract_address: self.herodotus_facts_registry_contract.read() + } + .get_slot_value(self.eth_transfer_contract.read().into(), block, slot_0) + .unwrap(); + + let recipient_address: felt252 = slot_0_value + .try_into() + .expect('Invalid address parse felt252'); + let recipient_address: EthAddress = recipient_address + .try_into() + .expect('Invalid address parse EthAddres'); + + let order = self.orders.read(order_id); + assert(order.recipient_address == recipient_address, 'recipient_address not match L1'); + + // Slot n+1 contains the amount and isUsed + let amount = IEVMFactsRegistryDispatcher { + contract_address: self.herodotus_facts_registry_contract.read() + } + .get_slot_value(self.eth_transfer_contract.read().into(), block, slot_1) + .unwrap(); + + assert(order.amount == amount, 'amount not match L1'); + + self.orders_used.write(order_id, true); + let payment_amount = order.amount + order.fee; + + IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() } + .transfer(self.mm_starknet_wallet.read(), payment_amount); + + self.emit(Withdraw { order_id, address: self.mm_starknet_wallet.read(), amount }); + } + + fn get_herodotus_facts_registry_contract(self: @ContractState) -> ContractAddress { + self.herodotus_facts_registry_contract.read() + } + + fn get_eth_transfer_contract(self: @ContractState) -> EthAddress { + self.eth_transfer_contract.read() + } + + fn get_mm_ethereum_contract(self: @ContractState) -> EthAddress { + self.mm_ethereum_wallet.read() + } + + fn get_mm_starknet_contract(self: @ContractState) -> ContractAddress { + self.mm_starknet_wallet.read() + } + + fn set_herodotus_facts_registry_contract( + ref self: ContractState, new_contract: ContractAddress + ) { + self.ownable.assert_only_owner(); + self.herodotus_facts_registry_contract.write(new_contract); + } + + fn get_mm_starknet_contractv2(self: @ContractState) -> ContractAddress { + self.mm_starknet_wallet.read() + } + + fn set_herodotus_facts_registry_contractv2( + ref self: ContractState, new_contract: ContractAddress + ) { + self.ownable.assert_only_owner(); + self.herodotus_facts_registry_contract.write(new_contract); + } + + fn set_eth_transfer_contract(ref self: ContractState, new_contract: EthAddress) { + self.ownable.assert_only_owner(); + self.eth_transfer_contract.write(new_contract); + } + + fn set_mm_ethereum_contract(ref self: ContractState, new_contract: EthAddress) { + self.ownable.assert_only_owner(); + self.mm_ethereum_wallet.write(new_contract); + } + + fn set_mm_starknet_contract(ref self: ContractState, new_contract: ContractAddress) { + self.ownable.assert_only_owner(); + self.mm_starknet_wallet.write(new_contract); + } + + fn new_mock_function(self: @ContractState) -> bool{ + true + } + + } + + #[l1_handler] + fn withdraw_fallback( + ref self: ContractState, + from_address: felt252, + order_id: u256, + recipient_address: EthAddress, + amount: u256 + ) { + let eth_transfer_contract_felt: felt252 = self.eth_transfer_contract.read().into(); + assert(eth_transfer_contract_felt == from_address, 'Only ETH_TRANSFER_CONTRACT'); + assert(!self.orders_used.read(order_id), 'Order already withdrawed'); + + let order = self.orders.read(order_id); + assert(order.recipient_address == recipient_address, 'recipient_address not match L1'); + assert(order.amount == amount, 'amount not match L1'); + + self.orders_used.write(order_id, true); + let payment_amount = order.amount + order.fee; + + IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() } + .transfer(self.mm_starknet_wallet.read(), payment_amount); + + self.emit(Withdraw { order_id, address: self.mm_starknet_wallet.read(), amount }); + } +} diff --git a/contracts/cairo/src/mocks/mock_pausableEscrow.cairo b/contracts/cairo/src/mocks/mock_pausableEscrow.cairo new file mode 100644 index 00000000..31fb6933 --- /dev/null +++ b/contracts/cairo/src/mocks/mock_pausableEscrow.cairo @@ -0,0 +1,291 @@ +use starknet::{ContractAddress, ClassHash, EthAddress}; + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct Order { + recipient_address: EthAddress, + amount: u256, + fee: u256 +} + +#[starknet::interface] +trait IEscrow_mockPausable { + fn get_order(self: @ContractState, order_id: u256) -> Order; + + fn set_order(ref self: ContractState, order: Order) -> u256; + + fn cancel_order(ref self: ContractState, order_id: u256); + + fn get_order_used(self: @ContractState, order_id: u256) -> bool; + + fn get_order_fee(self: @ContractState, order_id: u256) -> u256; + + // fn withdraw(ref self: ContractState, from_address: felt252, order_id: u256, recipient_address: EthAddress, amount: u256); + + fn get_eth_transfer_contract(self: @ContractState) -> EthAddress; + fn get_mm_ethereum_contract(self: @ContractState) -> EthAddress; + fn get_mm_starknet_contract(self: @ContractState) -> ContractAddress; + + fn set_eth_transfer_contract(ref self: ContractState, new_contract: EthAddress); + fn set_mm_ethereum_contract(ref self: ContractState, new_contract: EthAddress); + fn set_mm_starknet_contract(ref self: ContractState, new_contract: ContractAddress); + + fn pause(ref self: ContractState); + fn unpause(ref self: ContractState); + fn pause_state(ref self: ContractState) -> bool; +} + +#[starknet::contract] +mod Escrow_mockPausable { + + use super::{IEscrow_mockPausable, Order}; + + use openzeppelin::{ + access::ownable::OwnableComponent, + upgrades::{UpgradeableComponent, interface::IUpgradeable}, + security::PausableComponent + }; + use starknet::{ + ContractAddress, EthAddress, ClassHash, get_caller_address, get_contract_address, + get_block_timestamp + }; + + use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use yab::interfaces::IEVMFactsRegistry::{ + IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait + }; + + /// Components + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + + /// (Ownable) + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + /// (Upgradeable) + impl InternalImpl = UpgradeableComponent::InternalImpl; + + // Pausable + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + // https://github.com/starknet-io/starknet-addresses + // MAINNET = GOERLI = GOERLI2 + // 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + // const NATIVE_TOKEN: felt252 = + // 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Withdraw: Withdraw, + SetOrder: SetOrder, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + PausableEvent: PausableComponent::Event + } + + #[derive(Drop, starknet::Event)] + struct SetOrder { + order_id: u256, + recipient_address: EthAddress, + amount: u256, + fee: u256 + } + + #[derive(Drop, starknet::Event)] + struct Withdraw { + order_id: u256, + address: ContractAddress, + amount: u256, + } + + #[storage] + struct Storage { + current_order_id: u256, + orders: LegacyMap::, + orders_used: LegacyMap::, + orders_senders: LegacyMap::, + orders_timestamps: LegacyMap::, + eth_transfer_contract: EthAddress, // our transfer contract in L1 + mm_ethereum_wallet: EthAddress, + mm_starknet_wallet: ContractAddress, + native_token_eth_starknet: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + pausable: PausableComponent::Storage + } + + #[constructor] + fn constructor( + ref self: ContractState, + owner: ContractAddress, + eth_transfer_contract: EthAddress, + mm_ethereum_wallet: EthAddress, + mm_starknet_wallet: ContractAddress, + native_token_eth_starknet: ContractAddress + ) { + self.ownable.initializer(owner); + + self.current_order_id.write(0); + self.eth_transfer_contract.write(eth_transfer_contract); + self.mm_ethereum_wallet.write(mm_ethereum_wallet); + self.mm_starknet_wallet.write(mm_starknet_wallet); + self.native_token_eth_starknet.write(native_token_eth_starknet); + + if (self.pausable.is_paused()) { + self.pausable._unpause(); + } + } + + #[external(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); + } + } + + #[external(v0)] + impl Escrow_mockPausable of IEscrow_mockPausable { + fn get_order(self: @ContractState, order_id: u256) -> Order { + self.orders.read(order_id) + } + + fn set_order(ref self: ContractState, order: Order) -> u256 { + self.pausable.assert_not_paused(); + assert(order.amount > 0, 'Amount must be greater than 0'); + + let payment_amount = order.amount + order.fee; + let dispatcher = IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() }; + assert(dispatcher.allowance(get_caller_address(), get_contract_address()) >= payment_amount, 'Not enough allowance'); + assert(dispatcher.balanceOf(get_caller_address()) >= payment_amount, 'Not enough balance'); + + let mut order_id = self.current_order_id.read(); + self.orders.write(order_id, order); + self.orders_used.write(order_id, false); + self.orders_senders.write(order_id, get_caller_address()); + self.orders_timestamps.write(order_id, get_block_timestamp()); + + dispatcher.transferFrom(get_caller_address(), get_contract_address(), payment_amount); + + self + .emit( + SetOrder { + order_id, + recipient_address: order.recipient_address, + amount: order.amount, + fee: order.fee + } + ); + + self.current_order_id.write(order_id + 1); + order_id + } + + fn cancel_order(ref self: ContractState, order_id: u256) { + self.pausable.assert_not_paused(); + assert(!self.orders_used.read(order_id), 'Order already withdrawed'); + assert( + get_block_timestamp() - self.orders_timestamps.read(order_id) < 43200, + 'Didnt passed enough time' + ); + + let sender = self.orders_senders.read(order_id); + assert(sender == get_caller_address(), 'Only sender allowed'); + let order = self.orders.read(order_id); + let payment_amount = order.amount + order.fee; + + IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() } + .transfer(sender, payment_amount); + } + + fn get_order_used(self: @ContractState, order_id: u256) -> bool { + self.orders_used.read(order_id) + } + + fn get_order_fee(self: @ContractState, order_id: u256) -> u256 { + let order: Order = self.orders.read(order_id); + order.fee + } + + fn get_eth_transfer_contract(self: @ContractState) -> EthAddress { + self.eth_transfer_contract.read() + } + + fn get_mm_ethereum_contract(self: @ContractState) -> EthAddress { + self.mm_ethereum_wallet.read() + } + + fn get_mm_starknet_contract(self: @ContractState) -> ContractAddress { + self.mm_starknet_wallet.read() + } + + fn set_eth_transfer_contract(ref self: ContractState, new_contract: EthAddress) { + self.pausable.assert_not_paused(); + self.ownable.assert_only_owner(); + self.eth_transfer_contract.write(new_contract); + } + + fn set_mm_ethereum_contract(ref self: ContractState, new_contract: EthAddress) { + self.pausable.assert_not_paused(); + self.ownable.assert_only_owner(); + self.mm_ethereum_wallet.write(new_contract); + } + + fn set_mm_starknet_contract(ref self: ContractState, new_contract: ContractAddress) { + self.pausable.assert_not_paused(); + self.ownable.assert_only_owner(); + self.mm_starknet_wallet.write(new_contract); + } + + fn pause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._pause(); + } + + fn unpause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable._unpause(); + } + + fn pause_state(ref self: ContractState) -> bool { + self.pausable.is_paused() + } + } + + #[l1_handler] + fn withdraw( + ref self: ContractState, + from_address: felt252, + order_id: u256, + recipient_address: EthAddress, + amount: u256 + ) { + self.pausable.assert_not_paused(); + let eth_transfer_contract_felt: felt252 = self.eth_transfer_contract.read().into(); + assert(eth_transfer_contract_felt == from_address, 'Only ETH_TRANSFER_CONTRACT'); + assert(!self.orders_used.read(order_id), 'Order already withdrawed'); + + let order = self.orders.read(order_id); + assert(order.recipient_address == recipient_address, 'recipient_address not match L1'); + assert(order.amount == amount, 'amount not match L1'); + + self.orders_used.write(order_id, true); + let payment_amount = order.amount + order.fee; + + IERC20Dispatcher { contract_address: self.native_token_eth_starknet.read() } + .transfer(self.mm_starknet_wallet.read(), payment_amount); + + self.emit(Withdraw { order_id, address: self.mm_starknet_wallet.read(), amount }); + } +} diff --git a/contracts/cairo/src/tests/test_escrow.cairo b/contracts/cairo/src/tests/test_escrow_allowance.cairo similarity index 60% rename from contracts/cairo/src/tests/test_escrow.cairo rename to contracts/cairo/src/tests/test_escrow_allowance.cairo index 60a2a716..14b1a973 100644 --- a/contracts/cairo/src/tests/test_escrow.cairo +++ b/contracts/cairo/src/tests/test_escrow_allowance.cairo @@ -1,17 +1,32 @@ mod Escrow { + use core::to_byte_array::FormatAsByteArray; + use core::serde::Serde; + use core::traits::Into; use starknet::{EthAddress, ContractAddress}; use integer::BoundedInt; - use snforge_std::{declare, ContractClassTrait}; + use snforge_std::{declare, ContractClassTrait, L1Handler, L1HandlerTrait}; use snforge_std::{CheatTarget, start_prank, stop_prank, start_warp, stop_warp}; + use yab::mocks::mock_Escrow_changed_functions::{IEscrow_mock_changed_functionsDispatcher, IEscrow_mock_changed_functionsDispatcherTrait}; + use yab::mocks::mock_pausableEscrow::{IEscrow_mockPausableDispatcher, IEscrow_mockPausableDispatcherTrait}; use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; use yab::escrow::{IEscrowDispatcher, IEscrowDispatcherTrait, Order}; use yab::interfaces::IEVMFactsRegistry::{ IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait }; - use yab::tests::utils::constants::EscrowConstants::{ - USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT + + use yab::tests::utils::{ + constants::EscrowConstants::{ + USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT, ETH_USER + }, + }; + + use openzeppelin::{ + upgrades::{ + UpgradeableComponent, + interface::{IUpgradeable, IUpgradeableDispatcher, IUpgradeableDispatcherTrait} + }, }; fn setup() -> (IEscrowDispatcher, IERC20Dispatcher) { @@ -28,9 +43,8 @@ mod Escrow { fn setup_general(balance: u256, approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ let eth_token = deploy_erc20('ETH', '$ETH', BoundedInt::max(), OWNER()); - let evm_facts_registry = deploy_mock_EVMFactsRegistry(); let escrow = deploy_escrow( - evm_facts_registry.contract_address, + OWNER(), ETH_TRANSFER_CONTRACT(), MM_ETHEREUM(), MM_STARKNET(), @@ -49,7 +63,7 @@ mod Escrow { } fn deploy_escrow( - herodotus_facts_registry_contract: ContractAddress, + escrow_owner: ContractAddress, eth_transfer_contract: EthAddress, mm_ethereum_contract: EthAddress, mm_starknet_contract: ContractAddress, @@ -57,7 +71,7 @@ mod Escrow { ) -> IEscrowDispatcher { let escrow = declare('Escrow'); let mut calldata: Array = ArrayTrait::new(); - calldata.append(herodotus_facts_registry_contract.into()); + calldata.append(escrow_owner.into()); calldata.append(eth_transfer_contract.into()); calldata.append(mm_ethereum_contract.into()); calldata.append(mm_starknet_contract.into()); @@ -65,14 +79,7 @@ mod Escrow { let address = escrow.deploy(@calldata).unwrap(); return IEscrowDispatcher { contract_address: address }; } - - fn deploy_mock_EVMFactsRegistry() -> IEVMFactsRegistryDispatcher { - let mock_EVMFactsRegistry = declare('EVMFactsRegistry'); - let calldata: Array = ArrayTrait::new(); - let address = mock_EVMFactsRegistry.deploy(@calldata).unwrap(); - return IEVMFactsRegistryDispatcher { contract_address: address }; - } - + fn deploy_erc20( name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress ) -> IERC20Dispatcher { @@ -108,9 +115,20 @@ mod Escrow { assert(order.amount == order_save.amount, 'wrong amount'); assert(!escrow.get_order_used(order_id), 'wrong order used'); - start_prank(CheatTarget::One(escrow.contract_address), MM_STARKNET()); - escrow.withdraw(order_id, 0, 0); - stop_prank(CheatTarget::One(escrow.contract_address)); + let mut l1_handler = L1HandlerTrait::new( + contract_address: escrow.contract_address, + function_name: 'withdraw' + ); + + let mut payload_buffer: Array = ArrayTrait::new(); + Serde::serialize(@order_id, ref payload_buffer); + Serde::serialize(@order.recipient_address, ref payload_buffer); + Serde::serialize(@order.amount, ref payload_buffer); + + l1_handler.from_address = ETH_TRANSFER_CONTRACT().into(); + l1_handler.payload = payload_buffer.span(); + + l1_handler.execute().expect('Failed to execute l1_handler'); // check Order assert(escrow.get_order_used(order_id), 'wrong order used'); @@ -119,7 +137,6 @@ mod Escrow { assert(eth_token.balanceOf(MM_STARKNET()) == 500, 'withdraw: wrong balance'); } - #[test] fn test_allowance_happy() { let (escrow, eth_token) = setup_approved(500); @@ -157,70 +174,25 @@ mod Escrow { } #[test] - fn test_cancel_order() { - let (escrow, eth_token) = setup_balance(500); - - start_prank(CheatTarget::One(escrow.contract_address), USER()); - let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; - let order_id = escrow.set_order(order); - - // check balance - assert(eth_token.balanceOf(escrow.contract_address) == 500, 'set_order: wrong balance '); - assert(eth_token.balanceOf(MM_STARKNET()) == 0, 'set_order: wrong balance'); - assert(eth_token.balanceOf(USER()) == 0, 'set_order: wrong allowance'); - - start_warp(CheatTarget::One(escrow.contract_address), 43201); - escrow.cancel_order(order_id); - stop_warp(CheatTarget::One(escrow.contract_address)); - - stop_prank(CheatTarget::One(escrow.contract_address)); - - // check balance - assert(eth_token.balanceOf(escrow.contract_address) == 0, 'cancel_order: wrong balance '); - assert(eth_token.balanceOf(MM_STARKNET()) == 0, 'cancel_order: wrong balance'); - assert(eth_token.balanceOf(USER()) == 500, 'cancel_order: wrong allowance'); - } - - #[test] - #[should_panic(expected: ('Not enough time has passed',))] - fn test_cancel_order_fail_time() { - let (escrow, eth_token) = setup_balance(500); - - start_prank(CheatTarget::One(escrow.contract_address), USER()); - let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; - let order_id = escrow.set_order(order); - - escrow.cancel_order(order_id); - stop_prank(CheatTarget::One(escrow.contract_address)); - } - - #[test] - #[should_panic(expected: ('Order withdrew or nonexistent',))] - fn tets_cancel_order_fail_withdrew() { - let (escrow, eth_token) = setup_balance(500); - - start_prank(CheatTarget::One(escrow.contract_address), USER()); - let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; - let order_id = escrow.set_order(order); - - start_prank(CheatTarget::One(escrow.contract_address), MM_STARKNET()); - escrow.withdraw(order_id, 0, 0); - stop_prank(CheatTarget::One(escrow.contract_address)); - - escrow.cancel_order(order_id); - } - - #[test] - #[should_panic(expected: ('Only sender allowed',))] - fn test_cancel_order_fail_sender() { - let (escrow, eth_token) = setup_balance(500); + fn test_fail_random_eth_user_calls_l1_handler() { + let (escrow, _) = setup(); + let data: Array = array![1, MM_ETHEREUM().into(), 3, 4]; + let mut payload_buffer: Array = ArrayTrait::new(); + data.serialize(ref payload_buffer); + let mut l1_handler = L1HandlerTrait::new( + contract_address: escrow.contract_address, + function_name: 'withdraw', + ); + l1_handler.from_address = ETH_USER().into(); - start_prank(CheatTarget::One(escrow.contract_address), USER()); - let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; - let order_id = escrow.set_order(order); + l1_handler.payload = payload_buffer.span(); - start_warp(CheatTarget::One(escrow.contract_address), 43201); - start_prank(CheatTarget::One(escrow.contract_address), MM_STARKNET()); - escrow.cancel_order(order_id); + // same as "Should Panic" but for the L1 handler function + match l1_handler.execute() { + Result::Ok(_) => panic_with_felt252('shouldve panicked'), + Result::Err(RevertedTransaction) => { + assert(*RevertedTransaction.panic_data.at(0) == 'Only YAB_TRANSFER_CONTRACT', *RevertedTransaction.panic_data.at(0)); + } + } } } diff --git a/contracts/cairo/src/tests/test_escrow_cancel.cairo b/contracts/cairo/src/tests/test_escrow_cancel.cairo new file mode 100644 index 00000000..532260bd --- /dev/null +++ b/contracts/cairo/src/tests/test_escrow_cancel.cairo @@ -0,0 +1,145 @@ +mod Escrow { + use core::to_byte_array::FormatAsByteArray; + use core::serde::Serde; + use core::traits::Into; + use starknet::{EthAddress, ContractAddress}; + use integer::BoundedInt; + + use snforge_std::{declare, ContractClassTrait, L1Handler, L1HandlerTrait}; + use snforge_std::{CheatTarget, start_prank, stop_prank, start_warp, stop_warp}; + + use yab::mocks::mock_Escrow_changed_functions::{IEscrow_mock_changed_functionsDispatcher, IEscrow_mock_changed_functionsDispatcherTrait}; + use yab::mocks::mock_pausableEscrow::{IEscrow_mockPausableDispatcher, IEscrow_mockPausableDispatcherTrait}; + use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use yab::escrow::{IEscrowDispatcher, IEscrowDispatcherTrait, Order}; + use yab::interfaces::IEVMFactsRegistry::{ + IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait + }; + + use yab::tests::utils::{ + constants::EscrowConstants::{ + USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT, ETH_USER + }, + }; + + use openzeppelin::{ + upgrades::{ + UpgradeableComponent, + interface::{IUpgradeable, IUpgradeableDispatcher, IUpgradeableDispatcherTrait} + }, + }; + + fn setup() -> (IEscrowDispatcher, IERC20Dispatcher) { + setup_general(BoundedInt::max(), BoundedInt::max()) + } + + fn setup_approved(approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(BoundedInt::max(), approved) + } + + fn setup_balance(balance: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(balance, BoundedInt::max()) + } + + fn setup_general(balance: u256, approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + let eth_token = deploy_erc20('ETH', '$ETH', BoundedInt::max(), OWNER()); + let escrow = deploy_escrow( + OWNER(), + ETH_TRANSFER_CONTRACT(), + MM_ETHEREUM(), + MM_STARKNET(), + eth_token.contract_address + ); + + start_prank(CheatTarget::One(eth_token.contract_address), OWNER()); + eth_token.transfer(USER(), balance); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + start_prank(CheatTarget::One(eth_token.contract_address), USER()); + eth_token.approve(escrow.contract_address, approved); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + (escrow, eth_token) + } + + fn deploy_escrow( + escrow_owner: ContractAddress, + eth_transfer_contract: EthAddress, + mm_ethereum_contract: EthAddress, + mm_starknet_contract: ContractAddress, + native_token_eth_starknet: ContractAddress + ) -> IEscrowDispatcher { + let escrow = declare('Escrow'); + let mut calldata: Array = ArrayTrait::new(); + calldata.append(escrow_owner.into()); + calldata.append(eth_transfer_contract.into()); + calldata.append(mm_ethereum_contract.into()); + calldata.append(mm_starknet_contract.into()); + calldata.append(native_token_eth_starknet.into()); + let address = escrow.deploy(@calldata).unwrap(); + return IEscrowDispatcher { contract_address: address }; + } + + fn deploy_erc20( + name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress + ) -> IERC20Dispatcher { + let erc20 = declare('ERC20'); + let mut calldata = array![name, symbol]; + Serde::serialize(@initial_supply, ref calldata); + calldata.append(recipent.into()); + let address = erc20.deploy(@calldata).unwrap(); + return IERC20Dispatcher { contract_address: address }; + } + + #[test] + fn test_cancel_order() { + let (escrow, eth_token) = setup_balance(500); + + start_prank(CheatTarget::One(escrow.contract_address), USER()); + let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; + let order_id = escrow.set_order(order); + + // check balance + assert(eth_token.balanceOf(escrow.contract_address) == 500, 'set_order: wrong balance '); + assert(eth_token.balanceOf(MM_STARKNET()) == 0, 'set_order: wrong balance'); + assert(eth_token.balanceOf(USER()) == 0, 'set_order: wrong allowance'); + + start_warp(CheatTarget::One(escrow.contract_address), 43201); + escrow.cancel_order(order_id); + stop_warp(CheatTarget::One(escrow.contract_address)); + + stop_prank(CheatTarget::One(escrow.contract_address)); + + // check balance + assert(eth_token.balanceOf(escrow.contract_address) == 0, 'cancel_order: wrong balance '); + assert(eth_token.balanceOf(MM_STARKNET()) == 0, 'cancel_order: wrong balance'); + assert(eth_token.balanceOf(USER()) == 500, 'cancel_order: wrong allowance'); + } + + #[test] + #[should_panic(expected: ('Not enough time has passed',))] + fn test_cancel_order_fail_time() { + let (escrow, eth_token) = setup_balance(500); + + start_prank(CheatTarget::One(escrow.contract_address), USER()); + let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; + let order_id = escrow.set_order(order); + + escrow.cancel_order(order_id); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + #[should_panic(expected: ('Only sender allowed',))] + fn test_cancel_order_fail_sender() { + let (escrow, eth_token) = setup_balance(500); + + start_prank(CheatTarget::One(escrow.contract_address), USER()); + let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; + let order_id = escrow.set_order(order); + + start_warp(CheatTarget::One(escrow.contract_address), 43201); + start_prank(CheatTarget::One(escrow.contract_address), MM_STARKNET()); + escrow.cancel_order(order_id); + } +} diff --git a/contracts/cairo/src/tests/test_escrow_ownable.cairo b/contracts/cairo/src/tests/test_escrow_ownable.cairo new file mode 100644 index 00000000..ef1a9f91 --- /dev/null +++ b/contracts/cairo/src/tests/test_escrow_ownable.cairo @@ -0,0 +1,144 @@ +mod Escrow { + use core::to_byte_array::FormatAsByteArray; + use core::serde::Serde; + use core::traits::Into; + use starknet::{EthAddress, ContractAddress}; + use integer::BoundedInt; + + use snforge_std::{declare, ContractClassTrait, L1Handler, L1HandlerTrait}; + use snforge_std::{CheatTarget, start_prank, stop_prank, start_warp, stop_warp}; + + use yab::mocks::mock_Escrow_changed_functions::{IEscrow_mock_changed_functionsDispatcher, IEscrow_mock_changed_functionsDispatcherTrait}; + use yab::mocks::mock_pausableEscrow::{IEscrow_mockPausableDispatcher, IEscrow_mockPausableDispatcherTrait}; + use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use yab::escrow::{IEscrowDispatcher, IEscrowDispatcherTrait, Order}; + use yab::interfaces::IEVMFactsRegistry::{ + IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait + }; + + use yab::tests::utils::{ + constants::EscrowConstants::{ + USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT, ETH_USER + }, + }; + + use openzeppelin::{ + upgrades::{ + UpgradeableComponent, + interface::{IUpgradeable, IUpgradeableDispatcher, IUpgradeableDispatcherTrait} + }, + }; + + fn setup() -> (IEscrowDispatcher, IERC20Dispatcher) { + setup_general(BoundedInt::max(), BoundedInt::max()) + } + + fn setup_approved(approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(BoundedInt::max(), approved) + } + + fn setup_balance(balance: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(balance, BoundedInt::max()) + } + + fn setup_general(balance: u256, approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + let eth_token = deploy_erc20('ETH', '$ETH', BoundedInt::max(), OWNER()); + let escrow = deploy_escrow( + OWNER(), + ETH_TRANSFER_CONTRACT(), + MM_ETHEREUM(), + MM_STARKNET(), + eth_token.contract_address + ); + + start_prank(CheatTarget::One(eth_token.contract_address), OWNER()); + eth_token.transfer(USER(), balance); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + start_prank(CheatTarget::One(eth_token.contract_address), USER()); + eth_token.approve(escrow.contract_address, approved); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + (escrow, eth_token) + } + + fn deploy_escrow( + escrow_owner: ContractAddress, + eth_transfer_contract: EthAddress, + mm_ethereum_contract: EthAddress, + mm_starknet_contract: ContractAddress, + native_token_eth_starknet: ContractAddress + ) -> IEscrowDispatcher { + let escrow = declare('Escrow'); + let mut calldata: Array = ArrayTrait::new(); + calldata.append(escrow_owner.into()); + calldata.append(eth_transfer_contract.into()); + calldata.append(mm_ethereum_contract.into()); + calldata.append(mm_starknet_contract.into()); + calldata.append(native_token_eth_starknet.into()); + let address = escrow.deploy(@calldata).unwrap(); + return IEscrowDispatcher { contract_address: address }; + } + + fn deploy_erc20( + name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress + ) -> IERC20Dispatcher { + let erc20 = declare('ERC20'); + let mut calldata = array![name, symbol]; + Serde::serialize(@initial_supply, ref calldata); + calldata.append(recipent.into()); + let address = erc20.deploy(@calldata).unwrap(); + return IERC20Dispatcher { contract_address: address }; + } + + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_fail_upgrade_escrow_caller_isnt_the_owner() { + let (escrow, _) = setup(); + let upgradeable = IUpgradeableDispatcher { contract_address: escrow.contract_address }; + start_prank(CheatTarget::One(escrow.contract_address), MM_STARKNET()); + upgradeable.upgrade(declare('Escrow_mock_changed_functions').class_hash); + } + + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_fail_set_eth_transfer_contract() { + let (escrow, _) = setup(); + escrow.set_eth_transfer_contract(MM_ETHEREUM()); + } + + #[test] + fn test_set_eth_transfer_contract() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.set_eth_transfer_contract(MM_ETHEREUM()); + } + + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_fail_set_mm_ethereum_contract() { + let (escrow, _) = setup(); + escrow.set_mm_ethereum_contract(MM_ETHEREUM()); + } + + #[test] + fn test_set_mm_ethereum_contract() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.set_mm_ethereum_contract(MM_ETHEREUM()); + } + + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_fail_set_mm_starknet_contract() { + let (escrow, _) = setup(); + escrow.set_mm_starknet_contract(USER()); + } + + #[test] + fn test_set_mm_starknet_contract() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.set_mm_starknet_contract(USER()); + } +} diff --git a/contracts/cairo/src/tests/test_escrow_pause.cairo b/contracts/cairo/src/tests/test_escrow_pause.cairo new file mode 100644 index 00000000..8e30469f --- /dev/null +++ b/contracts/cairo/src/tests/test_escrow_pause.cairo @@ -0,0 +1,246 @@ +mod Escrow { + use core::to_byte_array::FormatAsByteArray; + use core::serde::Serde; + use core::traits::Into; + use starknet::{EthAddress, ContractAddress}; + use integer::BoundedInt; + + use snforge_std::{declare, ContractClassTrait, L1Handler, L1HandlerTrait}; + use snforge_std::{CheatTarget, start_prank, stop_prank, start_warp, stop_warp}; + + use yab::mocks::mock_Escrow_changed_functions::{IEscrow_mock_changed_functionsDispatcher, IEscrow_mock_changed_functionsDispatcherTrait}; + use yab::mocks::mock_pausableEscrow::{IEscrow_mockPausableDispatcher, IEscrow_mockPausableDispatcherTrait}; + use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use yab::escrow::{IEscrowDispatcher, IEscrowDispatcherTrait, Order}; + use yab::interfaces::IEVMFactsRegistry::{ + IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait + }; + + use yab::tests::utils::{ + constants::EscrowConstants::{ + USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT, ETH_USER + }, + }; + + use openzeppelin::{ + upgrades::{ + UpgradeableComponent, + interface::{IUpgradeable, IUpgradeableDispatcher, IUpgradeableDispatcherTrait} + }, + }; + + fn setup() -> (IEscrowDispatcher, IERC20Dispatcher) { + setup_general(BoundedInt::max(), BoundedInt::max()) + } + + fn setup_approved(approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(BoundedInt::max(), approved) + } + + fn setup_balance(balance: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(balance, BoundedInt::max()) + } + + fn setup_general(balance: u256, approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + let eth_token = deploy_erc20('ETH', '$ETH', BoundedInt::max(), OWNER()); + let escrow = deploy_escrow( + OWNER(), + ETH_TRANSFER_CONTRACT(), + MM_ETHEREUM(), + MM_STARKNET(), + eth_token.contract_address + ); + + start_prank(CheatTarget::One(eth_token.contract_address), OWNER()); + eth_token.transfer(USER(), balance); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + start_prank(CheatTarget::One(eth_token.contract_address), USER()); + eth_token.approve(escrow.contract_address, approved); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + (escrow, eth_token) + } + + fn deploy_escrow( + escrow_owner: ContractAddress, + eth_transfer_contract: EthAddress, + mm_ethereum_contract: EthAddress, + mm_starknet_contract: ContractAddress, + native_token_eth_starknet: ContractAddress + ) -> IEscrowDispatcher { + let escrow = declare('Escrow'); + let mut calldata: Array = ArrayTrait::new(); + calldata.append(escrow_owner.into()); + calldata.append(eth_transfer_contract.into()); + calldata.append(mm_ethereum_contract.into()); + calldata.append(mm_starknet_contract.into()); + calldata.append(native_token_eth_starknet.into()); + let address = escrow.deploy(@calldata).unwrap(); + return IEscrowDispatcher { contract_address: address }; + } + + fn deploy_erc20( + name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress + ) -> IERC20Dispatcher { + let erc20 = declare('ERC20'); + let mut calldata = array![name, symbol]; + Serde::serialize(@initial_supply, ref calldata); + calldata.append(recipent.into()); + let address = erc20.deploy(@calldata).unwrap(); + return IERC20Dispatcher { contract_address: address }; + } + + #[test] + fn test_start_unpaused() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + assert(escrow.pause_state() == false, 'Should start unpaused'); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + fn test_pause() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.pause(); + assert(escrow.pause_state() == true, 'Should be paused'); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + fn test_pause_unpause() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + assert(escrow.pause_state() == false, 'Should start unpaused'); + escrow.pause(); + assert(escrow.pause_state() == true, 'Should be paused'); + escrow.unpause(); + assert(escrow.pause_state() == false, 'Should be unpaused'); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_fail_pause_not_owner() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), USER()); + assert(escrow.pause_state() == false, 'Should start unpaused'); + escrow.pause(); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + #[should_panic(expected: ('Caller is not the owner',))] + fn test_fail_unpause_not_owner() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + assert(escrow.pause_state() == false, 'Should start unpaused'); + escrow.pause(); + assert(escrow.pause_state() == true, 'Should be paused'); + stop_prank(CheatTarget::One(escrow.contract_address)); + + start_prank(CheatTarget::One(escrow.contract_address), USER()); + escrow.unpause(); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + #[should_panic(expected: ('Pausable: paused',))] + fn test_fail_pause_while_paused() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + assert(escrow.pause_state() == false, 'Should start unpaused'); + escrow.pause(); + assert(escrow.pause_state() == true, 'Should be paused'); + escrow.pause(); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + #[should_panic(expected: ('Pausable: not paused',))] + fn test_fail_unpause_while_unpaused() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + assert(escrow.pause_state() == false, 'Should start unpaused'); + escrow.unpause(); + assert(escrow.pause_state() == false, 'Should be unpaused'); + escrow.unpause(); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + #[should_panic(expected: ('Pausable: paused',))] + fn test_fail_set_order_when_paused() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.pause(); + stop_prank(CheatTarget::One(escrow.contract_address)); + + start_prank(CheatTarget::One(escrow.contract_address), USER()); + let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; + let order_id = escrow.set_order(order); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + fn test_set_order_when_unpaused_after_prev_pause() { + let (escrow, _) = setup(); + + start_prank(CheatTarget::One(escrow.contract_address), USER()); + let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; + let order_id = escrow.set_order(order); + stop_prank(CheatTarget::One(escrow.contract_address)); + + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.pause(); + escrow.unpause(); + stop_prank(CheatTarget::One(escrow.contract_address)); + + start_prank(CheatTarget::One(escrow.contract_address), USER()); + let order = Order { recipient_address: 12345.try_into().unwrap(), amount: 500, fee: 0 }; + let order_id = escrow.set_order(order); + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + fn test_upgrade_when_paused() { + let (escrow, _) = setup(); + let upgradeable = IUpgradeableDispatcher { contract_address: escrow.contract_address }; + + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.pause(); + upgradeable.upgrade(declare('Escrow_mockPausable').class_hash); + + let escrow_2 = IEscrow_mockPausableDispatcher { contract_address: escrow.contract_address }; + assert(escrow_2.pause_state() == true, 'Contract should be paused'); + + stop_prank(CheatTarget::One(escrow.contract_address)); + } + + #[test] + fn test_fail_call_l1_handler_while_paused() { + let (escrow, _) = setup(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + escrow.pause(); + stop_prank(CheatTarget::One(escrow.contract_address)); + + let data: Array = array![1, MM_ETHEREUM().into(), 3, 4]; + let mut payload_buffer: Array = ArrayTrait::new(); + data.serialize(ref payload_buffer); + let mut l1_handler = L1HandlerTrait::new( + contract_address: escrow.contract_address, + function_name: 'withdraw', + ); + l1_handler.from_address = ETH_TRANSFER_CONTRACT().into(); + l1_handler.payload = payload_buffer.span(); + + // same as "Should Panic" but for the L1 handler function + match l1_handler.execute() { + Result::Ok(_) => panic_with_felt252('shouldve panicked'), + Result::Err(RevertedTransaction) => { + assert(*RevertedTransaction.panic_data.at(0) == 'Pausable: paused', *RevertedTransaction.panic_data.at(0)); + } + } + } +} diff --git a/contracts/cairo/src/tests/test_escrow_upgrade.cairo b/contracts/cairo/src/tests/test_escrow_upgrade.cairo new file mode 100644 index 00000000..717c82a2 --- /dev/null +++ b/contracts/cairo/src/tests/test_escrow_upgrade.cairo @@ -0,0 +1,105 @@ +mod Escrow { + use core::to_byte_array::FormatAsByteArray; + use core::serde::Serde; + use core::traits::Into; + use starknet::{EthAddress, ContractAddress}; + use integer::BoundedInt; + + use snforge_std::{declare, ContractClassTrait, L1Handler, L1HandlerTrait}; + use snforge_std::{CheatTarget, start_prank, stop_prank, start_warp, stop_warp}; + + use yab::mocks::mock_Escrow_changed_functions::{IEscrow_mock_changed_functionsDispatcher, IEscrow_mock_changed_functionsDispatcherTrait}; + use yab::mocks::mock_pausableEscrow::{IEscrow_mockPausableDispatcher, IEscrow_mockPausableDispatcherTrait}; + use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use yab::escrow::{IEscrowDispatcher, IEscrowDispatcherTrait, Order}; + use yab::interfaces::IEVMFactsRegistry::{ + IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait + }; + + use yab::tests::utils::{ + constants::EscrowConstants::{ + USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT, ETH_USER + }, + }; + + use openzeppelin::{ + upgrades::{ + UpgradeableComponent, + interface::{IUpgradeable, IUpgradeableDispatcher, IUpgradeableDispatcherTrait} + }, + }; + + fn setup() -> (IEscrowDispatcher, IERC20Dispatcher) { + setup_general(BoundedInt::max(), BoundedInt::max()) + } + + fn setup_approved(approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(BoundedInt::max(), approved) + } + + fn setup_balance(balance: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + setup_general(balance, BoundedInt::max()) + } + + fn setup_general(balance: u256, approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ + let eth_token = deploy_erc20('ETH', '$ETH', BoundedInt::max(), OWNER()); + let escrow = deploy_escrow( + OWNER(), + ETH_TRANSFER_CONTRACT(), + MM_ETHEREUM(), + MM_STARKNET(), + eth_token.contract_address + ); + + start_prank(CheatTarget::One(eth_token.contract_address), OWNER()); + eth_token.transfer(USER(), balance); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + start_prank(CheatTarget::One(eth_token.contract_address), USER()); + eth_token.approve(escrow.contract_address, approved); + stop_prank(CheatTarget::One(eth_token.contract_address)); + + (escrow, eth_token) + } + + fn deploy_escrow( + escrow_owner: ContractAddress, + eth_transfer_contract: EthAddress, + mm_ethereum_contract: EthAddress, + mm_starknet_contract: ContractAddress, + native_token_eth_starknet: ContractAddress + ) -> IEscrowDispatcher { + let escrow = declare('Escrow'); + let mut calldata: Array = ArrayTrait::new(); + calldata.append(escrow_owner.into()); + calldata.append(eth_transfer_contract.into()); + calldata.append(mm_ethereum_contract.into()); + calldata.append(mm_starknet_contract.into()); + calldata.append(native_token_eth_starknet.into()); + let address = escrow.deploy(@calldata).unwrap(); + return IEscrowDispatcher { contract_address: address }; + } + + fn deploy_erc20( + name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress + ) -> IERC20Dispatcher { + let erc20 = declare('ERC20'); + let mut calldata = array![name, symbol]; + Serde::serialize(@initial_supply, ref calldata); + calldata.append(recipent.into()); + let address = erc20.deploy(@calldata).unwrap(); + return IERC20Dispatcher { contract_address: address }; + } + + #[test] + fn test_upgrade_escrow() { + let (escrow, _) = setup(); + let upgradeable = IUpgradeableDispatcher { contract_address: escrow.contract_address }; + let value = escrow.get_mm_starknet_contract(); + start_prank(CheatTarget::One(escrow.contract_address), OWNER()); + upgradeable.upgrade(declare('Escrow_mock_changed_functions').class_hash); + let escrow_v2 = IEscrow_mock_changed_functionsDispatcher { contract_address: escrow.contract_address }; + let value_v2 = escrow_v2.get_mm_starknet_contractv2(); //would fail if new function name didn't exist + assert(value == value_v2, 'value should be the same'); + } +} diff --git a/contracts/cairo/src/tests/utils/constants.cairo b/contracts/cairo/src/tests/utils/constants.cairo index a21f6d98..02729ee5 100644 --- a/contracts/cairo/src/tests/utils/constants.cairo +++ b/contracts/cairo/src/tests/utils/constants.cairo @@ -20,4 +20,8 @@ mod EscrowConstants { fn ETH_TRANSFER_CONTRACT() -> EthAddress { 69.try_into().unwrap() } + + fn ETH_USER() -> EthAddress { + 99.try_into().unwrap() + } } diff --git a/contracts/cairo/upgrade.sh b/contracts/cairo/upgrade.sh new file mode 100755 index 00000000..08254777 --- /dev/null +++ b/contracts/cairo/upgrade.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# ANSI format +GREEN='\e[32m' +CYAN='\033[36m' +PINK='\033[1;35m' +COLOR_RESET='\033[0m' + +# Starkli implicitly utilizes these environment variables, so every time we use Starkli, +# we avoid adding flags such as --account, --keystore, and --rpc. +export STARKNET_ACCOUNT=$STARKNET_ACCOUNT +export STARKNET_KEYSTORE=$STARKNET_KEYSTORE +# export STARKNET_RPC=$STARKNET_RPC #todo: this must remain commented until we find a reliable and compatible rpc + +if [ -z "$ESCROW_CONTRACT_ADDRESS" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "ESCROW_CONTRACT_ADDRESS Variable is empty. Aborting execution.\n" + exit 1 +fi + +cd contracts/cairo + +printf "${GREEN}\n=> [SN] Declare Escrow${COLOR_RESET}\n" +NEW_ESCROW_CLASS_HASH=$(starkli declare --watch target/dev/yab_Escrow.contract_class.json) + +if [ -z "$NEW_ESCROW_CLASS_HASH" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "Failed to generate New Escrow Class Hash. Aborting execution.\n" + exit 1 +fi + +printf "${CYAN}[SN] Escrow address: $ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" +printf "${CYAN}[SN] New Escrow ClassHash: $NEW_ESCROW_CLASS_HASH${COLOR_RESET}\n" + +printf "${GREEN}\n=> [SN] Upgrade Escrow${COLOR_RESET}\n" +starkli invoke --watch $ESCROW_CONTRACT_ADDRESS upgrade $NEW_ESCROW_CLASS_HASH + +cd ../.. diff --git a/contracts/display_info.sh b/contracts/display_info.sh new file mode 100644 index 00000000..2bf278c9 --- /dev/null +++ b/contracts/display_info.sh @@ -0,0 +1,4 @@ +printf "${GREEN}\n=> Newly deployed contracts information: ${COLOR_RESET}\n" +printf "${PINK}[ETH] Deployed Proxy address: $YAB_TRANSFER_PROXY_ADDRESS ${COLOR_RESET}\n" +printf "${PINK}[ETH] Deployed YABTransfer address: $YAB_TRANSFER_ADDRESS ${COLOR_RESET}\n" +printf "${CYAN}[SN] Escrow Address: $ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" diff --git a/contracts/solidity/.env.example b/contracts/solidity/.env.example index ea79dc80..4d82cd06 100644 --- a/contracts/solidity/.env.example +++ b/contracts/solidity/.env.example @@ -1,4 +1,7 @@ -ETH_RPC_URL= -ETH_PRIVATE_KEY= #in lowercase hexa with the 0x prefix -ETHERSCAN_API_KEY= +#To interact with ETH +ETH_RPC_URL= #as given by your rpc +ETHERSCAN_API_KEY= #as given by etherscan + +ETH_PRIVATE_KEY= #in hexa with the 0x prefix SN_MESSAGING_ADDRESS=<0xde29d060D45901Fb19ED6C6e959EB22d8626708e|0xE2Bb56ee936fd6433DC0F6e7e3b8365C906AA057|0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4> # Goerli | Sepolia | Mainnet +MM_ETHEREUM_WALLET= #in hexa with the 0x prefix diff --git a/contracts/solidity/deploy.sh b/contracts/solidity/deploy.sh index f45f5ca2..7f8f7e5a 100755 --- a/contracts/solidity/deploy.sh +++ b/contracts/solidity/deploy.sh @@ -4,15 +4,22 @@ GREEN='\e[32m' COLOR_RESET='\033[0m' -cd "$(dirname "$0")" - -if [ -f .env ]; then - echo "Sourcing solidity/.env file..." - source .env -else - echo "Error: solidity/.env file not found!" - exit 1 -fi - -echo -e "${GREEN}\n=> [ETH] Deploy Escrow${COLOR_RESET}" -forge script ./script/Deploy.s.sol --fork-url $ETH_RPC_URL --broadcast --verify -vvvv +cd contracts/solidity + +printf "${GREEN}\n=> [ETH] Deploying ERC1967Proxy & YABTransfer ${COLOR_RESET}\n" + +RESULT_LOG=$(forge script ./script/Deploy.s.sol --rpc-url $ETH_RPC_URL --broadcast --verify) +# echo "$RESULT_LOG" #uncomment this line for debugging in detail + +# Getting result addresses +YAB_TRANSFER_PROXY_ADDRESS=$(echo "$RESULT_LOG" | grep -Eo '0: address ([^\n]+)' | awk '{print $NF}') +YAB_TRANSFER_ADDRESS=$(echo "$RESULT_LOG" | grep -Eo '1: address ([^\n]+)' | awk '{print $NF}') + +printf "${GREEN}\n=> [ETH] Deployed Proxy address: $YAB_TRANSFER_PROXY_ADDRESS ${COLOR_RESET}\n" +printf "${GREEN}\n=> [ETH] Deployed YABTransfer address: $YAB_TRANSFER_ADDRESS ${COLOR_RESET}\n" + +echo "\nIf you now wish to deploy SN Escrow, you will need to run the following commands:" +echo "export YAB_TRANSFER_PROXY_ADDRESS=$YAB_TRANSFER_PROXY_ADDRESS" +echo "make starknet-deploy" + +cd ../.. #to reset working directory diff --git a/contracts/solidity/foundry.toml b/contracts/solidity/foundry.toml index 24707903..1767982b 100644 --- a/contracts/solidity/foundry.toml +++ b/contracts/solidity/foundry.toml @@ -3,6 +3,7 @@ src = "src" out = "out" libs = ["lib"] gas_reports = ["*"] +remappings = ["@openzeppelin/contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts"] [rpc_endpoints] goerli = "${GOERLI_RPC_URL}" diff --git a/contracts/solidity/lib/openzeppelin-contracts-upgradeable b/contracts/solidity/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 00000000..24b2956f --- /dev/null +++ b/contracts/solidity/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 24b2956f2a3d2ca006db5c5dd7d12ab644b62d81 diff --git a/contracts/solidity/script/Deploy.s.sol b/contracts/solidity/script/Deploy.s.sol index 5005cf52..c0e6cfe5 100644 --- a/contracts/solidity/script/Deploy.s.sol +++ b/contracts/solidity/script/Deploy.s.sol @@ -1,24 +1,26 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.21; -import "forge-std/Script.sol"; -import "../src/YABTransfer.sol"; +import {Script} from "forge-std/Script.sol"; +import {YABTransfer} from "../src/YABTransfer.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract Deploy is Script { - function run() external { + function run() external returns (address, address) { uint256 deployerPrivateKey = vm.envUint("ETH_PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); address snMessagingAddress = vm.envAddress("SN_MESSAGING_ADDRESS"); - uint256 snEscrowAddress = 0x0; //this value is set in a call to the smart contract, once deployed - uint256 snEscrowWithdrawSelector = 0x0; //this value is set in a call to the smart contract, once deployed + uint256 snEscrowAddress = 0x0; // this value is set in a call to the smart contract, once deployed + uint256 snWithdrawSelector = 0x0; // this value is set in a call to the smart contract, once deployed + address marketMaker = vm.envAddress("MM_ETHEREUM_WALLET"); - - new YABTransfer( - snMessagingAddress, - snEscrowAddress, - snEscrowWithdrawSelector); + YABTransfer yab = new YABTransfer(); + ERC1967Proxy proxy = new ERC1967Proxy(address(yab), ""); + YABTransfer(address(proxy)).initialize(snMessagingAddress, snEscrowAddress, snWithdrawSelector, marketMaker); vm.stopBroadcast(); + + return (address(proxy), address(yab)); } } diff --git a/contracts/solidity/script/Upgrade.s.sol b/contracts/solidity/script/Upgrade.s.sol new file mode 100644 index 00000000..5f5f5884 --- /dev/null +++ b/contracts/solidity/script/Upgrade.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {console} from "../lib/forge-std/src/console.sol"; +import {Script} from "forge-std/Script.sol"; +import {YABTransfer} from "../src/YABTransfer.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract Upgrade is Script { + function run() external returns (address, address) { + address YABTrasnferProxyAddress = vm.envAddress("YAB_TRANSFER_PROXY_ADDRESS"); + uint256 deployerPrivateKey = vm.envUint("ETH_PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // Deploy new YABTransfer contract to upgrade proxy + YABTransfer yab = new YABTransfer(); + vm.stopBroadcast(); + + return upgrade(YABTrasnferProxyAddress, address(yab)); + } + + function upgrade( + address proxyAddress, + address newImplementationAddress + ) public returns (address, address) { + uint256 deployerPrivateKey = vm.envUint("ETH_PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + YABTransfer proxy = YABTransfer(payable(proxyAddress)); + proxy.upgradeToAndCall(address(newImplementationAddress), ''); + + vm.stopBroadcast(); + return (address(proxy), address(newImplementationAddress)); + + } + +} diff --git a/contracts/solidity/set_escrow.sh b/contracts/solidity/set_escrow.sh index f76f532e..40859a3d 100755 --- a/contracts/solidity/set_escrow.sh +++ b/contracts/solidity/set_escrow.sh @@ -4,25 +4,22 @@ GREEN='\e[32m' COLOR_RESET='\033[0m' - -echo -e "${GREEN}\n=> [ETH] Setting Starknet Escrow Address on ETH Smart Contract${COLOR_RESET}" - -if [ -f ./contracts/solidity/.env ]; then - echo "Sourcing solidity/.env file..." - source ./contracts/solidity/.env -else - echo "Error: solidity/.env file not found!" +if [ -z "$YAB_TRANSFER_PROXY_ADDRESS" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "YAB_TRANSFER_PROXY_ADDRESS Variable is empty. Aborting execution.\n" exit 1 fi -if [ -f ./contracts/cairo/.env ]; then - echo "Sourcing cairo/.env file..." - source ./contracts/cairo/.env -else - echo "Error: cairo/.env file not found!" +if [ -z "$ESCROW_CONTRACT_ADDRESS" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "ESCROW_CONTRACT_ADDRESS Variable is empty. Aborting execution.\n" exit 1 fi -echo "Smart contract being modified:" $ETH_CONTRACT_ADDR + +printf "${GREEN}\n=> [ETH] Setting Starknet Escrow Address on ETH Smart Contract${COLOR_RESET}\n" + +echo "Smart contract being modified:" $YAB_TRANSFER_PROXY_ADDRESS echo "New Escrow address:" $ESCROW_CONTRACT_ADDRESS -cast send --rpc-url $ETH_RPC_URL --private-key $ETH_PRIVATE_KEY $ETH_CONTRACT_ADDR "setEscrowAddress(uint256)" $ESCROW_CONTRACT_ADDRESS +cast send --rpc-url $ETH_RPC_URL --private-key $ETH_PRIVATE_KEY $YAB_TRANSFER_PROXY_ADDRESS "setEscrowAddress(uint256)" $ESCROW_CONTRACT_ADDRESS | grep "transactionHash" +echo "Done setting escrow address" diff --git a/contracts/solidity/set_withdraw_selector.sh b/contracts/solidity/set_withdraw_selector.sh index bd4bdfaa..cdaae9ac 100755 --- a/contracts/solidity/set_withdraw_selector.sh +++ b/contracts/solidity/set_withdraw_selector.sh @@ -1,27 +1,25 @@ #!/bin/bash -if [ -f ./contracts/solidity/.env ]; then - echo "Sourcing solidity/.env file..." - source ./contracts/solidity/.env -else - echo "Error: solidity/.env file not found!" - exit 1 -fi -if [ -f ./contracts/cairo/.env ]; then - echo "Sourcing cairo/.env file..." - source ./contracts/cairo/.env -else - echo "Error: cairo/.env file not found!" - exit 1 -fi # ANSI format GREEN='\e[32m' COLOR_RESET='\033[0m' -echo -e "${GREEN}\n=> [ETH] Setting Starknet Withdraw Selector on ETH Smart Contract${COLOR_RESET}" -echo "Smart contract being modified:" $ETH_CONTRACT_ADDR +if [ -z "$YAB_TRANSFER_PROXY_ADDRESS" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "YAB_TRANSFER_PROXY_ADDRESS Variable is empty. Aborting execution.\n" + exit 1 +fi +if [ -z "$WITHDRAW_NAME" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "WITHDRAW_NAME Variable is empty. Aborting execution.\n" + exit 1 +fi + +printf "${GREEN}\n=> [ETH] Setting Starknet Withdraw Selector on ETH Smart Contract${COLOR_RESET}\n" +echo "Smart contract being modified:" $YAB_TRANSFER_PROXY_ADDRESS WITHDRAW_SELECTOR=$(starkli selector $WITHDRAW_NAME) echo "New Withdraw Selector: ${WITHDRAW_SELECTOR}" -cast send --rpc-url $ETH_RPC_URL --private-key $ETH_PRIVATE_KEY $ETH_CONTRACT_ADDR "setEscrowWithdrawSelector(uint256)" "${WITHDRAW_SELECTOR}" +cast send --rpc-url $ETH_RPC_URL --private-key $ETH_PRIVATE_KEY $YAB_TRANSFER_PROXY_ADDRESS "setEscrowWithdrawSelector(uint256)" "${WITHDRAW_SELECTOR}" | grep "transactionHash" +echo "Done setting withdraw selector" diff --git a/contracts/solidity/src/YABTransfer.sol b/contracts/solidity/src/YABTransfer.sol index 4ca014d4..5801f206 100644 --- a/contracts/solidity/src/YABTransfer.sol +++ b/contracts/solidity/src/YABTransfer.sol @@ -2,8 +2,11 @@ pragma solidity ^0.8.21; import {IStarknetMessaging} from "starknet/IStarknetMessaging.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -contract YABTransfer { +contract YABTransfer is Initializable, OwnableUpgradeable, UUPSUpgradeable { struct TransferInfo { uint256 destAddress; uint256 amount; @@ -13,22 +16,32 @@ contract YABTransfer { event Transfer(uint256 indexed orderId, address srcAddress, TransferInfo transferInfo); mapping(bytes32 => TransferInfo) public transfers; - address private _owner; + address private _marketMaker; IStarknetMessaging private _snMessaging; uint256 private _snEscrowAddress; uint256 private _snEscrowWithdrawSelector; - constructor( + constructor() { + _disableInitializers(); + } + + // no constructors can be used in upgradeable contracts. + function initialize( address snMessaging, uint256 snEscrowAddress, - uint256 snEscrowWithdrawSelector) { - _owner = msg.sender; + uint256 snEscrowWithdrawSelector, + address marketMaker) public initializer { + __Ownable_init(msg.sender); + __UUPSUpgradeable_init(); + _snMessaging = IStarknetMessaging(snMessaging); _snEscrowAddress = snEscrowAddress; _snEscrowWithdrawSelector = snEscrowWithdrawSelector; + _marketMaker = marketMaker; } - function transfer(uint256 orderId, uint256 destAddress, uint256 amount) external payable { + + function transfer(uint256 orderId, uint256 destAddress, uint256 amount) external payable onlyOwnerOrMM { require(destAddress != 0, "Invalid destination address."); require(amount > 0, "Invalid amount, should be higher than 0."); require(msg.value == amount, "Invalid amount, should match msg.value."); @@ -44,7 +57,7 @@ contract YABTransfer { emit Transfer(orderId, msg.sender, transfers[index]); } - function withdraw(uint256 orderId, uint256 destAddress, uint256 amount) external payable { + function withdraw(uint256 orderId, uint256 destAddress, uint256 amount) external payable onlyOwnerOrMM { bytes32 index = keccak256(abi.encodePacked(orderId, destAddress, amount)); TransferInfo storage transferInfo = transfers[index]; require(transferInfo.isUsed == true, "Transfer not found."); @@ -62,13 +75,33 @@ contract YABTransfer { payload); } - function setEscrowAddress(uint256 snEscrowAddress) external { - require(msg.sender == _owner, "Only owner can call this function."); + function setEscrowAddress(uint256 snEscrowAddress) external onlyOwner { _snEscrowAddress = snEscrowAddress; } - function setEscrowWithdrawSelector(uint256 snEscrowWithdrawSelector) external { - require(msg.sender == _owner, "Only owner can call this function."); + function setEscrowWithdrawSelector(uint256 snEscrowWithdrawSelector) external onlyOwner { _snEscrowWithdrawSelector = snEscrowWithdrawSelector; } + + + //// MM ACL: + + function getMMAddress() external view returns (address) { + return _marketMaker; + } + + function setMMAddress(address newMMAddress) external onlyOwner { + _marketMaker = newMMAddress; + } + + function getOwner() external view returns (address) { + return owner(); + } + + modifier onlyOwnerOrMM { + require(msg.sender == owner() || msg.sender == _marketMaker, "Only Owner or MM can call this function"); + _; + } + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} } diff --git a/contracts/solidity/test/MM_ACL.sol b/contracts/solidity/test/MM_ACL.sol new file mode 100644 index 00000000..e13a3018 --- /dev/null +++ b/contracts/solidity/test/MM_ACL.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import "../src/YABTransfer.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract TransferTest is Test { + address public deployer = makeAddr('deployer'); + address public marketMaker = makeAddr('marketMaker'); + uint256 public snEscrowAddress = 0x0; + + YABTransfer public yab; + ERC1967Proxy public proxy; + YABTransfer public yab_caller; + + address SN_MESSAGING_ADDRESS = 0xde29d060D45901Fb19ED6C6e959EB22d8626708e; + uint256 SN_ESCROW_WITHDRAW_SELECTOR = 0x15511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77; + + function setUp() public { + vm.startPrank(deployer); + + yab = new YABTransfer(); + proxy = new ERC1967Proxy(address(yab), ""); + yab_caller = YABTransfer(address(proxy)); + yab_caller.initialize(SN_MESSAGING_ADDRESS, snEscrowAddress, SN_ESCROW_WITHDRAW_SELECTOR, marketMaker); + + vm.stopPrank(); + } + + function test_getMMAddress() public { + address mmAddress = yab_caller.getMMAddress(); + assertEq(mmAddress, marketMaker); + } + + function test_set_and_get_MMAddress_deployer() public { + vm.startPrank(deployer); + address alice = makeAddr("alice"); + yab_caller.setMMAddress(alice); + assertEq(yab_caller.getMMAddress(), alice); + vm.stopPrank(); + } + + function test_set_MMAddress_not_owner() public { + address bob = makeAddr("bob"); + vm.expectRevert(); //setMMAddress is only callable by the owner + yab_caller.setMMAddress(bob); + } + + function test_get_owner() public { + address ownerAddress = yab_caller.getOwner(); + assertEq(ownerAddress, deployer); + } +} diff --git a/contracts/solidity/test/Transfer.sol b/contracts/solidity/test/Transfer.sol index 81d0c731..e2c895b8 100644 --- a/contracts/solidity/test/Transfer.sol +++ b/contracts/solidity/test/Transfer.sol @@ -3,18 +3,27 @@ pragma solidity ^0.8.21; import "forge-std/Test.sol"; import "../src/YABTransfer.sol"; - +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract TransferTest is Test { + address public deployer = makeAddr('deployer'); + address public marketMaker = makeAddr("marketMaker"); + uint256 public snEscrowAddress = 0x0; + YABTransfer public yab; + ERC1967Proxy public proxy; + YABTransfer public yab_caller; + address SN_MESSAGING_ADDRESS = 0xde29d060D45901Fb19ED6C6e959EB22d8626708e; + uint256 SN_ESCROW_WITHDRAW_SELECTOR = 0x15511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77; function setUp() public { - address snMessagingAddress = SN_MESSAGING_ADDRESS; - uint256 snEscrowAddress = 0x0; - uint256 snEscrowWithdrawSelector = 0x15511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77; - - yab = new YABTransfer(snMessagingAddress, snEscrowAddress, snEscrowWithdrawSelector); + vm.startPrank(deployer); + + yab = new YABTransfer(); + proxy = new ERC1967Proxy(address(yab), ""); + yab_caller = YABTransfer(address(proxy)); + yab_caller.initialize(SN_MESSAGING_ADDRESS, snEscrowAddress, SN_ESCROW_WITHDRAW_SELECTOR, marketMaker); // Mock calls to Starknet Messaging contract vm.mockCall( @@ -22,31 +31,62 @@ contract TransferTest is Test { abi.encodeWithSelector(IStarknetMessaging(SN_MESSAGING_ADDRESS).sendMessageToL2.selector), abi.encode(0x0, 0x1) ); + vm.stopPrank(); + } + + function testTransfer_mm() public { + hoax(marketMaker, 100 wei); + yab_caller.transfer{value: 100}(1, 0x1, 100); + } + + function testTransfer_fail_notOwnerOrMM() public { + hoax(makeAddr("bob"), 100 wei); + vm.expectRevert("Only Owner or MM can call this function"); + yab_caller.transfer{value: 100}(1, 0x1, 100); + } + + function testWithdraw_mm() public { + hoax(marketMaker, 100 wei); + yab_caller.transfer{value: 100}(1, 0x1, 100); + + hoax(marketMaker, 100 wei); + yab_caller.withdraw{value: 100}(1, 0x1, 100); + } + function testWithdraw_fail_noOrderId() public { + hoax(marketMaker, 100 wei); + vm.expectRevert("Transfer not found."); //Won't match to a random transfer number + yab_caller.withdraw{value: 100}(1, 0x1, 100); } - function testTransfer() public { - yab.transfer{value: 100}(1, 0x1, 100); + function testWithdraw_fail_notOwnerOrMM() public { + hoax(makeAddr("bob"), 100 wei); + vm.expectRevert("Only Owner or MM can call this function"); + yab_caller.withdraw{value: 100}(1, 0x1, 100); } function testWithdraw() public { - yab.transfer{value: 100}(1, 0x1, 100); - yab.withdraw(1, 0x1, 100); + hoax(marketMaker, 100 wei); + yab_caller.transfer{value: 100}(1, 0x1, 100); + hoax(marketMaker, 100 wei); + yab_caller.withdraw(1, 0x1, 100); } function testWithdrawOver() public { - address alice = makeAddr("alice"); uint256 maxInt = type(uint256).max; - vm.deal(alice, maxInt); - vm.prank(alice); + vm.deal(marketMaker, maxInt); + vm.startPrank(marketMaker); - yab.transfer{value: maxInt}(1, 0x1, maxInt); - yab.withdraw(1, 0x1, maxInt); + yab_caller.transfer{value: maxInt}(1, 0x1, maxInt); + yab_caller.withdraw(1, 0x1, maxInt); + vm.stopPrank(); } function testWithdrawLow() public { - yab.transfer{value: 1}(1, 0x1, 1); - yab.withdraw(1, 0x1, 1); + hoax(marketMaker, 1 wei); + yab_caller.transfer{value: 1}(1, 0x1, 1); + hoax(marketMaker, 1 wei); + yab_caller.withdraw(1, 0x1, 1); } } diff --git a/contracts/solidity/upgrade.sh b/contracts/solidity/upgrade.sh new file mode 100755 index 00000000..9b08c757 --- /dev/null +++ b/contracts/solidity/upgrade.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# ANSI format +GREEN='\e[32m' +COLOR_RESET='\033[0m' + +cd contracts/solidity + +if [ -z "$YAB_TRANSFER_PROXY_ADDRESS" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "YAB_TRANSFER_PROXY_ADDRESS Variable is empty. Aborting execution.\n" + exit 1 +fi +if [ -z "$ETH_PRIVATE_KEY" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "ETH_PRIVATE_KEY Variable is empty. Aborting execution.\n" + exit 1 +fi + +printf "${GREEN}\n=> [ETH] Upgrading YABTransfer ${COLOR_RESET}\n" + +RESULT_LOG=$(forge script ./script/Upgrade.s.sol --rpc-url $ETH_RPC_URL --broadcast --verify) +# echo "$RESULT_LOG" #uncomment this line for debugging in detail + +# Getting result addresses +PROXY_ADDRESS=$(echo "$RESULT_LOG" | grep -oP '0: address \K[^\n]+' | awk '{print $0}') +YAB_TRANSFER_ADDRESS=$(echo "$RESULT_LOG" | grep -oP '1: address \K[^\n]+' | awk '{print $0}') + +if [ -z "$YAB_TRANSFER_ADDRESS" ]; then + printf "\n${RED}ERROR:${COLOR_RESET}\n" + echo "YAB_TRANSFER_ADDRESS Variable is empty. Aborting execution.\n" + exit 1 +fi + +printf "${GREEN}\n=> [ETH] Unchanged YABTransfer Proxy address: $YAB_TRANSFER_PROXY_ADDRESS ${COLOR_RESET}\n" +printf "${GREEN}\n=> [ETH] Newly Deployed YABTransfer contract address: $YAB_TRANSFER_ADDRESS ${COLOR_RESET}\n" + +cd ../.. diff --git a/mm-bot/src/services/withdrawer/ethereum_withdrawer.py b/mm-bot/src/services/withdrawer/ethereum_withdrawer.py index 02808465..21d286f6 100644 --- a/mm-bot/src/services/withdrawer/ethereum_withdrawer.py +++ b/mm-bot/src/services/withdrawer/ethereum_withdrawer.py @@ -28,7 +28,7 @@ async def send_withdraw(self, order: Order, order_service: OrderService): async def estimate_withdraw_fallback_message_fee(order_id, recipient_address, amount): from_address = constants.ETH_CONTRACT_ADDR to_address = constants.SN_CONTRACT_ADDR - entry_point_selector = hex(get_selector_from_name("withdraw_fallback")) + entry_point_selector = hex(get_selector_from_name("withdraw")) payload = [ hex(order_id), "0x0",