diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 00000000..0bae806c --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,178 @@ +name: Integration Test +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + Test: + runs-on: ubuntu-latest + environment: Test + + services: + postgres: + image: postgres:latest + env: + POSTGRES_DB: postgres_db # optional (defaults to `postgres`) + POSTGRES_PASSWORD: postgres_password # required + POSTGRES_PORT: 5432 # optional (defaults to `5432`) + POSTGRES_USER: postgres_user # optional (defaults to `postgres`) + # `POSTGRES_HOST` is `localhost` + ports: + - 5432:5432 # maps tcp port 5432 on service container to the host + options: >- # set health checks to wait until postgres has started + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + # Python setup TODO activate when mm-bot is ready +# - name: Setup Python +# uses: actions/setup-python@v5 +# with: +# python-version: 3.10.12 +# architecture: x64 +# +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r mm-bot/requirements.txt + + # Postgres setup TODO activate when mm-bot is ready +# - name: Install PSQL +# run: | +# sudo apt-get update +# sudo apt-get install --yes postgresql-client +# +# - name: Create tables +# run: | +# psql -h localhost -d postgres_db -U postgres_user -f mm-bot/resources/schema.sql +# psql -h localhost -d postgres_db -U postgres_user -c "SELECT * FROM block;" +# env: +# PGPASSWORD: postgres_password + + # Ethereum Setup + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install Forge + run: | + cd contracts/solidity + forge install + + # Starknet Setup + - name: Install scarb + uses: software-mansion/setup-scarb@v1 + with: + scarb-version: "2.3.1" + + - name: Install starkliup + run: curl https://get.starkli.sh | sh + + - name: Install Starkli + run: | + /home/runner/.config/.starkli/bin/starkliup --version 0.1.20 + sudo mv /home/runner/.config/.starkli/bin/starkli /usr/local/bin/ + + - name: Setup Katana .env + run: | + cp .github/workflows/katana/katana.env .env + + - name: Download Katana + run: | + wget https://github.com/dojoengine/dojo/releases/download/v0.4.4/dojo_v0.4.4_linux_amd64.tar.gz + tar -xzvf dojo_v0.4.4_linux_amd64.tar.gz + rm sozo torii dojo-language-server + + # Run Anvil + - name: Run Anvil + run: | + anvil & + + # Deploy Starknet Messaging Contract in L1 + - name: Clone https://github.com/glihm/starknet-messaging-dev + uses: GuillaumeFalourd/clone-github-repo-action@v2.3 + with: + branch: 'main' + owner: 'glihm' + repository: 'starknet-messaging-dev' + + - name: Install Forge + run: | + cd starknet-messaging-dev/solidity + forge install + + - name: Deploy Starknet Messaging Contract + run: | + cd starknet-messaging-dev/solidity + cp anvil.env .env + source .env + forge script script/LocalTesting.s.sol:LocalSetup --broadcast --rpc-url ${ETH_RPC_URL} + + # Run Katana + - name: Run Katana + run: | + ./katana --messaging starknet-messaging-dev/anvil.messaging.json & + + - name: Setup Katana Account + run: | + .github/workflows/scripts/setup_katana_account.sh + + # Setup Starknet messaging + - name: Setup Starknet messaging + run: | + cd starknet-messaging-dev/cairo + source katana.env + scarb build + starkli declare ./target/dev/messaging_tuto_contract_msg.contract_class.json --keystore-password "" + starkli deploy 0x02d6b666ade3a9ee98430d565830604b90954499c590fa05a9844bdf4d3a574b \ + --salt 0x1234 \ + --keystore-password "" + + # Build Ethereum Contract + - name: Build Ethereum Contract + run: | + make ethereum-build + + # Build Starknet Contract + - name: Build Starknet Contract + run: | + make starknet-build + + # Deploy PaymentRegistry, Escrow, set escrow, set withdraw selector and test complete flow + - name: Deploy and test complete flow + run: | + export ETH_RPC_URL=${{vars.ETH_RPC_URL}} + export ETHERSCAN_API_KEY=${{vars.ETHERSCAN_API_KEY}} + export ETH_PRIVATE_KEY=${{vars.ETH_PRIVATE_KEY}} + export SN_MESSAGING_ADDRESS=${{vars.SN_MESSAGING_ADDRESS}} + export MM_ETHEREUM_WALLET=${{vars.MM_ETHEREUM_WALLET}} + export SKIP_VERIFY=true + . ./contracts/solidity/deploy.sh + + export STARKNET_RPC=${{vars.STARKNET_RPC}} + source ./contracts/cairo/.env.test + . ./contracts/cairo/deploy.sh + + . ./contracts/solidity/set_escrow.sh + . ./contracts/solidity/set_withdraw_selector.sh + + export AMOUNT=1000000000000000000 + . ./.github/workflows/scripts/set_order.sh + + . ./.github/workflows/scripts/transfer.sh + . ./.github/workflows/scripts/withdraw.sh + + . ./.github/workflows/scripts/assert.sh + + # Run mm-bot (it should run a single process order listening to the contract once) diff --git a/.github/workflows/scripts/assert.sh b/.github/workflows/scripts/assert.sh new file mode 100755 index 00000000..3200ccae --- /dev/null +++ b/.github/workflows/scripts/assert.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +. contracts/utils/colors.sh #for ANSI colors + +assert() { + # Usage: assert + if eval "$1"; then + printf "${GREEN}✓ $2 passed.${RESET}\n" + else + printf "${RED}⨯ $2 assertion failed: Expected value: $3, Obtained value: $4.${RESET}\n" + exit 1 + fi +} + +echo "" + +DESTINATION_FINAL_BALANCE=$(cast balance --rpc-url $ETH_RPC_URL $DESTINATION_ADDRESS) +EXPECTED_DESTINATION_FINAL_BALANCE=10001000000000000000000 +assert "[[ $DESTINATION_FINAL_BALANCE -eq $EXPECTED_DESTINATION_FINAL_BALANCE ]]" "Destination balance" "$EXPECTED_DESTINATION_FINAL_BALANCE" "$DESTINATION_FINAL_BALANCE" + +ESCROW_FINAL_BALANCE=$(starkli balance --raw $ESCROW_CONTRACT_ADDRESS) +EXPECTED_ESCROW_FINAL_BALANCE=0 +assert "[[ $ESCROW_FINAL_BALANCE -eq $EXPECTED_ESCROW_FINAL_BALANCE ]]" "Escrow balance" "$EXPECTED_ESCROW_FINAL_BALANCE" "$ESCROW_FINAL_BALANCE" + +MM_FINAL_BALANCE=$(starkli balance --raw $MM_SN_WALLET_ADDR) +EXPECTED_MM_FINAL_BALANCE=1001000025000000000000 +assert "[[ $MM_FINAL_BALANCE -eq $EXPECTED_MM_FINAL_BALANCE ]]" "MM balance" "$EXPECTED_MM_FINAL_BALANCE" "$MM_FINAL_BALANCE" diff --git a/.github/workflows/scripts/set_order.sh b/.github/workflows/scripts/set_order.sh new file mode 100755 index 00000000..373899e7 --- /dev/null +++ b/.github/workflows/scripts/set_order.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +. contracts/utils/colors.sh #for ANSI colors + +#fee=24044002524012 +FEE=25000000000000 +APPROVE_AMOUNT=$((${AMOUNT}+${FEE})) + +echo -e "${GREEN}\n=> [SN] Making transfer to Escrow${COLOR_RESET}" + +starkli invoke \ + $NATIVE_TOKEN_ETH_STARKNET approve $ESCROW_CONTRACT_ADDRESS u256:$APPROVE_AMOUNT \ + / $ESCROW_CONTRACT_ADDRESS set_order 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \ + u256:$AMOUNT u256:$FEE --private-key $STARKNET_PRIVATE_KEY --account $STARKNET_ACCOUNT diff --git a/.github/workflows/scripts/setup_katana_account.sh b/.github/workflows/scripts/setup_katana_account.sh new file mode 100755 index 00000000..44179a76 --- /dev/null +++ b/.github/workflows/scripts/setup_katana_account.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +. contracts/utils/colors.sh #for ANSI colors + +source .github/workflows/katana/katana.env +set -e + +export STARKNET_ACCOUNT=$ACCOUNT_SRC +export STARKNET_RPC=$RPC_URL + +# Check if the JSON file exists +if [ ! -f "$ACCOUNT_SRC" ]; then + $(starkli account fetch --output $ACCOUNT_SRC $ACCOUNT_ADDRESS) + echo -e "$GREEN\n==> Katana JSON account file created at: $ACCOUNT_SRC$RESET" +else + echo -e "$GREEN\n==> Katana JSON account file already exists at: $ACCOUNT_SRC$RESET" +fi diff --git a/.github/workflows/scripts/transfer.sh b/.github/workflows/scripts/transfer.sh new file mode 100755 index 00000000..c04c5720 --- /dev/null +++ b/.github/workflows/scripts/transfer.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +. contracts/utils/colors.sh #for ANSI colors + +DESTINATION_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +DESTINATION_ADDRESS_UINT=642829559307850963015472508762062935916233390536 + +echo -e "${GREEN}\n=> [SN] Making transfer to Destination account${COLOR_RESET}" # 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 -> 642829559307850963015472508762062935916233390536 + +MM_INITIAL_BALANCE=$(cast balance --rpc-url $ETH_RPC_URL --ether $MM_ETHEREUM_WALLET) +DESTINATION_INITIAL_BALANCE=$(cast balance --rpc-url $ETH_RPC_URL --ether $DESTINATION_ADDRESS) +echo "Initial MM balance: $MM_INITIAL_BALANCE" +echo "Initial Destination balance: $DESTINATION_INITIAL_BALANCE" + +echo "Transferring $AMOUNT to $DESTINATION_ADDRESS" +cast send --rpc-url $ETH_RPC_URL --private-key $ETH_PRIVATE_KEY \ + $YAB_TRANSFER_PROXY_ADDRESS "transfer(uint256, uint256, uint256)" \ + "0" "$DESTINATION_ADDRESS_UINT" "$AMOUNT" \ + --value $AMOUNT >> /dev/null + +MM_FINAL_BALANCE=$(cast balance --rpc-url $ETH_RPC_URL --ether $MM_ETHEREUM_WALLET) +DESTINATION_FINAL_BALANCE=$(cast balance --rpc-url $ETH_RPC_URL --ether $DESTINATION_ADDRESS) +echo "Final MM balance: $MM_FINAL_BALANCE" +echo "Final Destination balance: $DESTINATION_FINAL_BALANCE" diff --git a/.github/workflows/scripts/withdraw.sh b/.github/workflows/scripts/withdraw.sh new file mode 100755 index 00000000..1b370cda --- /dev/null +++ b/.github/workflows/scripts/withdraw.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +. contracts/utils/colors.sh #for ANSI colors + +echo -e "${GREEN}\n=> [SN] Making withdraw${COLOR_RESET}" # 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 -> 642829559307850963015472508762062935916233390536 + +ESCROW_INITIAL_BALANCE=$(starkli balance $ESCROW_CONTRACT_ADDRESS) +MM_INITIAL_BALANCE=$(starkli balance $MM_SN_WALLET_ADDR) +echo "Initial Escrow balance: $ESCROW_INITIAL_BALANCE" +echo "Initial MM balance: $MM_INITIAL_BALANCE" + +echo "Withdrawing $AMOUNT" +cast send --rpc-url $ETH_RPC_URL --private-key $ETH_PRIVATE_KEY \ + $YAB_TRANSFER_PROXY_ADDRESS "withdraw(uint256, uint256, uint256)" \ + "0" "642829559307850963015472508762062935916233390536" "$AMOUNT" \ + --value $AMOUNT >> /dev/null + +sleep 15 + +starkli call $ESCROW_CONTRACT_ADDRESS get_order_used u256:0 + +ESCROW_FINAL_BALANCE=$(starkli balance $ESCROW_CONTRACT_ADDRESS) +MM_FINAL_BALANCE=$(starkli balance $MM_SN_WALLET_ADDR) +echo "Final Escrow balance: $ESCROW_FINAL_BALANCE" +echo "Final MM balance: $MM_FINAL_BALANCE" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 7d55ba1f..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Integration Test -on: - push: - branches: - - master - pull_request: - branches: - - master - workflow_dispatch: - -defaults: - run: - shell: bash - -jobs: - test: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:latest - env: - POSTGRES_DB: postgres_db # optional (defaults to `postgres`) - POSTGRES_PASSWORD: postgres_password # required - POSTGRES_PORT: 5432 # optional (defaults to `5432`) - POSTGRES_USER: postgres_user # optional (defaults to `postgres`) - # `POSTGRES_HOST` is `localhost` - ports: - - 5432:5432 # maps tcp port 5432 on service container to the host - options: >- # set health checks to wait until postgres has started - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: 3.10 - architecture: x64 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r mm-bot/requirements.txt - - - name: Install PSQL - run: | - apt-get update - apt-get install --yes postgresql-client - - - name: Create tables - run: | - psql -h localhost -d postgres_db -U postgres_user -f mm-bot/resources/schema.sql - psql -h localhost -d postgres_db -U postgres_user -c "SELECT * FROM block;" - env: - PGPASSWORD: postgres_password - - - name: Install scarb - uses: software-mansion/setup-scarb@v1 - with: - scarb-version: "0.7.0" - - - name: Install starkliup - run: curl https://get.starkli.sh | sh - - - name: Install Starkli - run: | - /home/runner/.config/.starkli/bin/starkliup --version 0.1.20 - sudo mv /home/runner/.config/.starkli/bin/starkli /usr/local/bin/ - - - name: Setup Katana .env - run: | - cp .github/workflows/katana/katana.env .env - - - name: Download Katana - run: | - wget https://github.com/dojoengine/dojo/releases/download/v0.3.1/dojo_v0.3.1_linux_amd64.tar.gz - tar -xzvf dojo_v0.3.1_linux_amd64.tar.gz - rm sozo torii dojo-language-server diff --git a/contracts/cairo/.env.test b/contracts/cairo/.env.test new file mode 100644 index 00000000..a61ed383 --- /dev/null +++ b/contracts/cairo/.env.test @@ -0,0 +1,9 @@ +STARKNET_ACCOUNT=/home/runner/.config/.starkli/account_katana.json +STARKNET_ACCOUNT_ADDRESS=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 +STARKNET_PRIVATE_KEY=0x1800000000300000180000000000030000000000003006001800006600 +SN_RPC_URL=http://0.0.0.0:5050 +SN_ESCROW_OWNER=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 +MM_SN_WALLET_ADDR=0x5686a647a9cdd63ade617e0baf3b364856b813b508f03903eb58a7e622d5855 +WITHDRAW_NAME=withdraw +MM_ETHEREUM_WALLET=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +NATIVE_TOKEN_ETH_STARKNET=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 diff --git a/contracts/cairo/deploy.sh b/contracts/cairo/deploy.sh index 5f4c3dbd..c98f4be0 100755 --- a/contracts/cairo/deploy.sh +++ b/contracts/cairo/deploy.sh @@ -6,9 +6,9 @@ if [ -z "$STARKNET_ACCOUNT" ]; then echo "STARKNET_ACCOUNT Variable is empty. Aborting execution.\n" exit 1 fi -if [ -z "$STARKNET_KEYSTORE" ]; then +if [ -z "$STARKNET_KEYSTORE" ] && [ -z "$STARKNET_PRIVATE_KEY" ]; then echo "\n${RED}ERROR:${COLOR_RESET}" - echo "STARKNET_KEYSTORE Variable is empty. Aborting execution.\n" + echo "STARKNET_KEYSTORE and STARKNET_PRIVATE_KEY Variables are empty. Aborting execution.\n" exit 1 fi if [ -z "$MM_SN_WALLET_ADDR" ]; then @@ -35,7 +35,9 @@ fi echo "${GREEN}\n=> [SN] Declaring Escrow${COLOR_RESET}" ESCROW_CLASS_HASH=$(starkli declare \ - --account $STARKNET_ACCOUNT --keystore $STARKNET_KEYSTORE \ + --account $STARKNET_ACCOUNT \ + $(if [ -n "$STARKNET_KEYSTORE" ]; then echo "--keystore $STARKNET_KEYSTORE"; fi) \ + $(if [ -n "$STARKNET_PRIVATE_KEY" ]; then echo "--private-key $STARKNET_PRIVATE_KEY"; fi) \ --watch contracts/cairo/target/dev/yab_Escrow.contract_class.json) @@ -62,7 +64,9 @@ printf "${PINK}[ETH] Market Maker ETH Wallet: $MM_ETHEREUM_WALLET${COLOR_RESET}\ printf "${GREEN}\n=> [SN] Deploying Escrow${COLOR_RESET}\n" ESCROW_CONTRACT_ADDRESS=$(starkli deploy \ - --account $STARKNET_ACCOUNT --keystore $STARKNET_KEYSTORE \ + --account $STARKNET_ACCOUNT \ + $(if [ -n "$STARKNET_KEYSTORE" ]; then echo "--keystore $STARKNET_KEYSTORE"; fi) \ + $(if [ -n "$STARKNET_PRIVATE_KEY" ]; then echo "--private-key $STARKNET_PRIVATE_KEY"; fi) \ --watch $ESCROW_CLASS_HASH \ $SN_ESCROW_OWNER \ $YAB_TRANSFER_PROXY_ADDRESS \ diff --git a/contracts/cairo/src/escrow.cairo b/contracts/cairo/src/escrow.cairo index 04a8619f..ca663315 100644 --- a/contracts/cairo/src/escrow.cairo +++ b/contracts/cairo/src/escrow.cairo @@ -20,7 +20,7 @@ trait IEscrow { 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); @@ -114,7 +114,7 @@ mod Escrow { #[substorage(v0)] ownable: OwnableComponent::Storage, #[substorage(v0)] - upgradeable: UpgradeableComponent::Storage, + upgradeable: UpgradeableComponent::Storage, #[substorage(v0)] pausable: PausableComponent::Storage } @@ -217,7 +217,7 @@ mod Escrow { fn set_mm_starknet_contract(ref self: ContractState, new_contract: ContractAddress) { self.pausable.assert_not_paused(); - self.ownable.assert_only_owner(); + self.ownable.assert_only_owner(); self.mm_starknet_wallet.write(new_contract); } diff --git a/contracts/cairo/src/escrow_herodotus.cairo b/contracts/cairo/src/escrow_herodotus.cairo new file mode 100644 index 00000000..9b78e6fd --- /dev/null +++ b/contracts/cairo/src/escrow_herodotus.cairo @@ -0,0 +1,309 @@ +use starknet::{ContractAddress, ClassHash, EthAddress}; + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct Order { + recipient_address: EthAddress, + amount: u256, + fee: u256 +} + +#[starknet::interface] +trait IEscrow { + 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, 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); +} + +#[starknet::contract] +mod Escrow { + use super::{IEscrow, 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; + + #[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 of IEscrow { + fn get_order(self: @ContractState, order_id: u256) -> Order { + self.orders.read(order_id) + } + + fn set_order(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 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); + } + } + + #[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_EscrowV2.cairo b/contracts/cairo/src/mocks/mock_EscrowV2.cairo new file mode 100644 index 00000000..2a9c8c49 --- /dev/null +++ b/contracts/cairo/src/mocks/mock_EscrowV2.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 IEscrowV2 { + 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 EscrowV2 { + use super::{IEscrowV2, 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 of IEscrowV2 { + 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/tests/test_escrow_allowance.cairo b/contracts/cairo/src/tests/test_escrow_allowance.cairo index 14b1a973..555169eb 100644 --- a/contracts/cairo/src/tests/test_escrow_allowance.cairo +++ b/contracts/cairo/src/tests/test_escrow_allowance.cairo @@ -79,7 +79,7 @@ mod Escrow { let address = escrow.deploy(@calldata).unwrap(); return IEscrowDispatcher { contract_address: address }; } - + fn deploy_erc20( name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress ) -> IERC20Dispatcher { diff --git a/contracts/solidity/.env.test b/contracts/solidity/.env.test new file mode 100644 index 00000000..e37f6425 --- /dev/null +++ b/contracts/solidity/.env.test @@ -0,0 +1,6 @@ +ETH_RPC_URL=http://127.0.0.1:8545 +ETHERSCAN_API_KEY=0x1 +ETH_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +SN_MESSAGING_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 +MM_ETHEREUM_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +SKIP_VERIFY=true diff --git a/contracts/solidity/deploy.sh b/contracts/solidity/deploy.sh index 6b151135..017cfb60 100755 --- a/contracts/solidity/deploy.sh +++ b/contracts/solidity/deploy.sh @@ -5,7 +5,7 @@ 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) +RESULT_LOG=$(forge script ./script/Deploy.s.sol --rpc-url $ETH_RPC_URL --broadcast ${SKIP_VERIFY:---verify}) # echo "$RESULT_LOG" #uncomment this line for debugging in detail # Getting result addresses