├── .gitbook.yaml ├── .github └── workflows │ ├── eth-test.yml │ ├── integration-test.yml │ ├── katana │ └── katana.env │ ├── production-workflow.yml │ ├── scripts │ ├── assert.sh │ ├── claim_payment.sh │ ├── set_order.sh │ ├── setup_katana_account.sh │ └── transfer.sh │ └── sn-test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── contracts ├── ethereum │ ├── .env.example │ ├── .env.test │ ├── .gitignore │ ├── deploy.sh │ ├── foundry.toml │ ├── lib │ │ └── starknet │ │ │ ├── IStarknetMessaging.sol │ │ │ ├── IStarknetMessagingEvents.sol │ │ │ ├── NamedStorage.sol │ │ │ └── StarknetMessaging.sol │ ├── script │ │ ├── Deploy.s.sol │ │ └── Upgrade.s.sol │ ├── set_starknet_claim_payment_selector.sh │ ├── set_starknet_escrow.sh │ ├── set_zksync_escrow.sh │ ├── src │ │ └── PaymentRegistry.sol │ ├── test │ │ ├── .env.test │ │ ├── ACL.t.sol │ │ ├── Transfer_Claim_SN.t.sol │ │ └── Transfer_Claim_ZKSync.t.sol │ └── upgrade.sh ├── starknet │ ├── .env.example │ ├── .env.test │ ├── .gitignore │ ├── .tool-versions │ ├── Scarb.lock │ ├── Scarb.toml │ ├── change_pause_state.sh │ ├── deploy.sh │ ├── src │ │ ├── ERC20.cairo │ │ ├── escrow.cairo │ │ ├── escrow_herodotus.cairo │ │ ├── interfaces │ │ │ ├── IERC20.cairo │ │ │ └── IEVMFactsRegistry.cairo │ │ ├── lib.cairo │ │ ├── mocks │ │ │ ├── mock_EVMFactsRegistry.cairo │ │ │ ├── mock_EscrowV2.cairo │ │ │ ├── mock_Escrow_changed_functions.cairo │ │ │ └── mock_pausableEscrow.cairo │ │ └── tests │ │ │ ├── test_escrow_allowance.cairo │ │ │ ├── test_escrow_ownable.cairo │ │ │ ├── test_escrow_pause.cairo │ │ │ ├── test_escrow_upgrade.cairo │ │ │ └── utils │ │ │ └── constants.cairo │ └── upgrade.sh ├── starknet_wallet_setup.md ├── utils │ ├── colors.sh │ └── display_info.sh └── zksync │ ├── .env.example │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ └── escrow.sol │ ├── deploy.sh │ ├── deploy │ ├── deploy.ts │ └── utils.ts │ ├── hardhat.config.ts │ ├── package.json │ ├── test │ ├── claim_payment.sh │ ├── main.test.ts │ ├── set_order.sh │ ├── transfer.sh │ └── utils.ts │ ├── tests.md │ └── yarn.lock ├── docs ├── README.md ├── SUMMARY.md ├── about_yab │ ├── FAQ.md │ ├── features.md │ └── how_does_it_work.md ├── contracts │ ├── README.md │ ├── deploy.md │ ├── escrow.md │ ├── pause.md │ ├── payment_registry.md │ └── upgrade.md ├── images │ ├── YAB-diagram.png │ └── YAB-header.jpg └── mm_bot │ ├── README.md │ ├── architecture.md │ ├── deploy.md │ ├── diagrams │ ├── accepted_blocks.puml │ ├── class_diagrams.puml │ ├── failed_orders.puml │ ├── order_processing.puml │ └── state_diagram.puml │ └── images │ ├── accepted_blocks.svg │ ├── architecture.png │ ├── failed_orders.svg │ ├── mm_diagram_class.svg │ ├── mm_diagram_class_full.svg │ ├── order_processing.svg │ ├── physical_view.png │ └── state_diagram.svg └── mm-bot ├── .env.example ├── .gitignore ├── Makefile ├── README.md ├── abi ├── Escrow.json └── PaymentRegistry.json ├── requirements.txt ├── resources ├── schema.sql └── zksync.sql └── src ├── config ├── constants.py ├── database_config.py └── logging_config.py ├── main.py ├── migrate_zksync.py ├── models ├── block.py ├── error.py ├── network.py ├── order.py ├── order_status.py └── set_order_event.py ├── persistence ├── block_dao.py ├── error_dao.py └── order_dao.py └── services ├── decorators └── use_fallback.py ├── ethereum.py ├── executors └── order_executor.py ├── fee_calculators ├── fee_calculator.py ├── starknet_fee_calculator.py └── zksync_fee_calculator.py ├── herodotus.py ├── indexers ├── order_indexer.py ├── starknet_order_indexer.py └── zksync_order_indexer.py ├── mm_full_node_client.py ├── order_service.py ├── payment_claimer ├── ethereum_2_zksync_payment_claimer.py ├── ethereum_payment_claimer.py ├── herodotus_payment_claimer.py └── payment_claimer.py ├── processors ├── accepted_blocks_orders_processor.py ├── failed_orders_processor.py └── orders_processor.py ├── senders └── ethereum_sender.py ├── starknet.py └── zksync.py /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ 2 | -------------------------------------------------------------------------------- /.github/workflows/eth-test.yml: -------------------------------------------------------------------------------- 1 | name: ETH PaymentRegistry Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | test-ETH: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Install Foundry 18 | uses: foundry-rs/foundry-toolchain@v1 19 | 20 | - name: Run tests 21 | run: make ethereum-test 22 | -------------------------------------------------------------------------------- /.github/workflows/katana/katana.env: -------------------------------------------------------------------------------- 1 | #Only for running test in CI 2 | # Katana Prefunded Account 3 | ACCOUNT_ADDRESS=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 4 | ACCOUNT_PRIVATE_KEY=0x1800000000300000180000000000030000000000003006001800006600 5 | ACCOUNT_SRC=/home/runner/.config/.starkli/account_katana.json 6 | RPC_URL=http://0.0.0.0:5050 -------------------------------------------------------------------------------- /.github/workflows/production-workflow.yml: -------------------------------------------------------------------------------- 1 | name: "Production Build" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | Deploy: 9 | name: Deploy to MM Server 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Deploy to MM Server 14 | env: 15 | PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 16 | HOST_NAME: ${{ secrets.HOST_NAME }} 17 | USER_NAME: ${{ secrets.USER_NAME }} 18 | APP_DIR: ${{ secrets.APP_DIR }} 19 | SERVICE_NAME: ${{ secrets.SERVICE_NAME }} 20 | run: 21 | echo "$PRIVATE_KEY" > private_key && 22 | chmod 400 private_key && 23 | ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOST_NAME} " 24 | cd ${APP_DIR} && 25 | sudo systemctl stop ${SERVICE_NAME} && 26 | git pull && 27 | sudo systemctl start ${SERVICE_NAME} " 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/scripts/assert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . contracts/utils/colors.sh #for ANSI colors 4 | 5 | assert() { 6 | # Usage: assert 7 | if eval "$1"; then 8 | printf "${GREEN}✓ $2 passed.${RESET}\n" 9 | else 10 | printf "${RED}⨯ $2 assertion failed: Expected value: $3, Obtained value: $4.${RESET}\n" 11 | exit 1 12 | fi 13 | } 14 | 15 | echo "" 16 | 17 | DESTINATION_FINAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC $DESTINATION_ADDRESS) 18 | EXPECTED_DESTINATION_FINAL_BALANCE=10001000000000000000000 19 | assert "[[ $DESTINATION_FINAL_BALANCE -eq $EXPECTED_DESTINATION_FINAL_BALANCE ]]" "Destination balance" "$EXPECTED_DESTINATION_FINAL_BALANCE" "$DESTINATION_FINAL_BALANCE" 20 | 21 | ESCROW_FINAL_BALANCE=$(starkli balance --raw $ESCROW_CONTRACT_ADDRESS) 22 | EXPECTED_ESCROW_FINAL_BALANCE=0 23 | assert "[[ $ESCROW_FINAL_BALANCE -eq $EXPECTED_ESCROW_FINAL_BALANCE ]]" "Escrow balance" "$EXPECTED_ESCROW_FINAL_BALANCE" "$ESCROW_FINAL_BALANCE" 24 | 25 | MM_FINAL_BALANCE=$(starkli balance --raw $MM_STARKNET_WALLET_ADDRESS) 26 | EXPECTED_MM_FINAL_BALANCE=1001000025000000000000 27 | assert "[[ $MM_FINAL_BALANCE -eq $EXPECTED_MM_FINAL_BALANCE ]]" "MM balance" "$EXPECTED_MM_FINAL_BALANCE" "$MM_FINAL_BALANCE" 28 | -------------------------------------------------------------------------------- /.github/workflows/scripts/claim_payment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . contracts/utils/colors.sh #for ANSI colors 4 | 5 | DESTINATION_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 6 | 7 | echo -e "${GREEN}\n=> [SN] Making ClaimPayment${COLOR_RESET}" # 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 -> 642829559307850963015472508762062935916233390536 8 | 9 | ESCROW_INITIAL_BALANCE=$(starkli balance $ESCROW_CONTRACT_ADDRESS) 10 | MM_INITIAL_BALANCE=$(starkli balance $MM_STARKNET_WALLET_ADDRESS) 11 | echo "Initial Escrow balance: $ESCROW_INITIAL_BALANCE" 12 | echo "Initial MM balance: $MM_INITIAL_BALANCE" 13 | 14 | echo "Withdrawing $AMOUNT" 15 | cast send --rpc-url $ETHEREUM_RPC --private-key $ETHEREUM_PRIVATE_KEY \ 16 | $PAYMENT_REGISTRY_PROXY_ADDRESS "claimPayment(uint256, uint256, uint256)" \ 17 | "0" $DESTINATION_ADDRESS "$AMOUNT" \ 18 | --value $AMOUNT >> /dev/null 19 | 20 | sleep 15 21 | 22 | starkli call $ESCROW_CONTRACT_ADDRESS get_order_pending u256:0 23 | 24 | ESCROW_FINAL_BALANCE=$(starkli balance $ESCROW_CONTRACT_ADDRESS) 25 | MM_FINAL_BALANCE=$(starkli balance $MM_STARKNET_WALLET_ADDRESS) 26 | echo "Final Escrow balance: $ESCROW_FINAL_BALANCE" 27 | echo "Final MM balance: $MM_FINAL_BALANCE" 28 | -------------------------------------------------------------------------------- /.github/workflows/scripts/set_order.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . contracts/utils/colors.sh #for ANSI colors 4 | 5 | #fee=24044002524012 6 | FEE=25000000000000 7 | APPROVE_AMOUNT=$((${AMOUNT}+${FEE})) 8 | 9 | echo -e "${GREEN}\n=> [SN] Making SetOrder on Escrow${COLOR_RESET}" 10 | 11 | starkli invoke \ 12 | $NATIVE_TOKEN_ETH_STARKNET approve $ESCROW_CONTRACT_ADDRESS u256:$APPROVE_AMOUNT \ 13 | / $ESCROW_CONTRACT_ADDRESS set_order 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \ 14 | u256:$AMOUNT u256:$FEE --private-key $STARKNET_PRIVATE_KEY --account $STARKNET_ACCOUNT 15 | -------------------------------------------------------------------------------- /.github/workflows/scripts/setup_katana_account.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . contracts/utils/colors.sh #for ANSI colors 4 | 5 | source .github/workflows/katana/katana.env 6 | set -e 7 | 8 | export STARKNET_ACCOUNT=$ACCOUNT_SRC 9 | export STARKNET_RPC=$RPC_URL 10 | 11 | # Check if the JSON file exists 12 | if [ ! -f "$ACCOUNT_SRC" ]; then 13 | $(starkli account fetch --output $ACCOUNT_SRC $ACCOUNT_ADDRESS) 14 | echo -e "$GREEN\n==> Katana JSON account file created at: $ACCOUNT_SRC$RESET" 15 | else 16 | echo -e "$GREEN\n==> Katana JSON account file already exists at: $ACCOUNT_SRC$RESET" 17 | fi 18 | -------------------------------------------------------------------------------- /.github/workflows/scripts/transfer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . contracts/utils/colors.sh #for ANSI colors 4 | 5 | DESTINATION_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 6 | STARKNET_CHAIN_ID="0" 7 | 8 | echo -e "${GREEN}\n=> [SN] Making transfer to Destination account${COLOR_RESET}" # 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 -> 642829559307850963015472508762062935916233390536 9 | 10 | MM_INITIAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $MM_ETHEREUM_WALLET_ADDRESS) 11 | DESTINATION_INITIAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $DESTINATION_ADDRESS) 12 | echo "Initial MM balance: $MM_INITIAL_BALANCE" 13 | echo "Initial Destination balance: $DESTINATION_INITIAL_BALANCE" 14 | 15 | echo "Transferring $AMOUNT to $DESTINATION_ADDRESS" 16 | cast send --rpc-url $ETHEREUM_RPC --private-key $ETHEREUM_PRIVATE_KEY \ 17 | $PAYMENT_REGISTRY_PROXY_ADDRESS "transfer(uint256, uint256, uint8)" \ 18 | "0" $DESTINATION_ADDRESS $STARKNET_CHAIN_ID \ 19 | --value $AMOUNT >> /dev/null 20 | 21 | MM_FINAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $MM_ETHEREUM_WALLET_ADDRESS) 22 | DESTINATION_FINAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $DESTINATION_ADDRESS) 23 | echo "Final MM balance: $MM_FINAL_BALANCE" 24 | echo "Final Destination balance: $DESTINATION_FINAL_BALANCE" 25 | -------------------------------------------------------------------------------- /.github/workflows/sn-test.yml: -------------------------------------------------------------------------------- 1 | name: SN Escrow Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | test-SN: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Install scarb 18 | uses: software-mansion/setup-scarb@v1 19 | with: 20 | scarb-version: "2.3.1" 21 | 22 | - name: Install starkliup 23 | run: | 24 | curl https://get.starkli.sh | sh 25 | 26 | - name: Install Starkli 27 | run: | 28 | /home/runner/.config/.starkli/bin/starkliup --version 0.1.20 29 | sudo mv /home/runner/.config/.starkli/bin/starkli /usr/local/bin/ 30 | 31 | - name: Install snFoundry 32 | uses: foundry-rs/setup-snfoundry@v2 33 | with: 34 | starknet-foundry-version: 0.12.0 35 | 36 | - name: Run make starknet-test 37 | run: | 38 | make starknet-test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contracts/ethereum/lib/forge-std"] 2 | path = contracts/ethereum/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "contracts/ethereum/lib/openzeppelin-contracts-upgradeable"] 5 | path = contracts/ethereum/lib/openzeppelin-contracts-upgradeable 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 7 | [submodule "contracts/ethereum/lib/era-contracts"] 8 | path = contracts/ethereum/lib/era-contracts 9 | url = https://github.com/matter-labs/era-contracts 10 | [submodule "contracts/zksync_forge/lib/forge-std"] 11 | path = contracts/zksync_forge/lib/forge-std 12 | url = https://github.com/foundry-rs/forge-std 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ### SETUP ### 2 | 3 | deps: install-scarb install-starkli install-starknet-foundry install-ethereum-foundry install-zksync 4 | 5 | install-scarb: 6 | curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.4.1 7 | 8 | install-starkli: 9 | curl https://get.starkli.sh | sh && starkliup 10 | 11 | install-starknet-foundry: 12 | curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh -s -- -v 0.12.0 13 | snfoundryup 14 | 15 | install-ethereum-foundry: 16 | curl -L https://foundry.paradigm.xyz | bash && foundryup 17 | 18 | install-zksync: 19 | cd ./contracts/zksync/ && yarn install 20 | 21 | 22 | ### ETHEREUM ### 23 | 24 | ethereum-clean: 25 | @cd ./contracts/ethereum/ && forge clean 26 | 27 | ethereum-build: ethereum-clean 28 | @cd ./contracts/ethereum/ && forge build 29 | 30 | ethereum-test: ethereum-clean 31 | @cd ./contracts/ethereum/ && forge test 32 | 33 | ethereum-test-gas-report: ethereum-clean 34 | @cd ./contracts/ethereum/ && forge test --gas-report 35 | 36 | ethereum-deploy: ethereum-build 37 | @. ./contracts/ethereum/.env && . ./contracts/ethereum/deploy.sh 38 | 39 | ethereum-upgrade: ethereum-build 40 | @. ./contracts/ethereum/.env && . ./contracts/ethereum/upgrade.sh 41 | 42 | ethereum-set-escrow: 43 | @. ./contracts/ethereum/.env && . ./contracts/ethereum/set_starknet_escrow.sh 44 | 45 | ethereum-set-claim-payment-selector: 46 | @. ./contracts/ethereum/.env && . ./contracts/starknet/.env && . ./contracts/ethereum/set_starknet_claim_payment_selector.sh 47 | 48 | 49 | ### STARKNET ### 50 | 51 | starknet-clean: 52 | @cd ./contracts/starknet/ && scarb clean 53 | 54 | starknet-build: starknet-clean 55 | @cd ./contracts/starknet/ && scarb build 56 | 57 | starknet-test: starknet-clean 58 | @cd ./contracts/starknet/ && snforge test 59 | 60 | starknet-deploy: starknet-build 61 | @. ./contracts/starknet/.env && . ./contracts/starknet/deploy.sh 62 | 63 | starknet-deploy-and-connect: starknet-build 64 | @. ./contracts/ethereum/.env && . ./contracts/starknet/.env && \ 65 | . ./contracts/starknet/deploy.sh && \ 66 | . ./contracts/ethereum/set_starknet_escrow.sh && \ 67 | . ./contracts/ethereum/set_starknet_claim_payment_selector.sh 68 | 69 | starknet-upgrade: starknet-build 70 | @. ./contracts/starknet/.env && . ./contracts/starknet/upgrade.sh 71 | 72 | starknet-pause: 73 | @. ./contracts/starknet/.env && ./contracts/starknet/change_pause_state.sh pause 74 | 75 | starknet-unpause: 76 | @. ./contracts/starknet/.env && ./contracts/starknet/change_pause_state.sh unpause 77 | 78 | 79 | ### ZKSYNC ### 80 | 81 | zksync-clean: 82 | @cd ./contracts/zksync/ && yarn clean 83 | 84 | zksync-build: zksync-clean 85 | @cd ./contracts/zksync/ && yarn compile 86 | 87 | zksync-deploy: zksync-build 88 | @. ./contracts/zksync/.env && . ./contracts/zksync/deploy.sh 89 | 90 | zksync-connect: 91 | @. ./contracts/ethereum/.env && . ./contracts/zksync/.env && \ 92 | . ./contracts/ethereum/set_zksync_escrow.sh 93 | 94 | zksync-deploy-and-connect: zksync-build 95 | @. ./contracts/ethereum/.env && . ./contracts/zksync/.env && \ 96 | . ./contracts/zksync/deploy.sh && \ 97 | . ./contracts/ethereum/set_zksync_escrow.sh 98 | 99 | 100 | zksync-test: zksync-build 101 | @cd ./contracts/zksync/ && yarn test 102 | 103 | #wip: 104 | zksync-test-integration: 105 | @make ethereum-build && make zksync-build && \ 106 | . ./contracts/ethereum/test/.env.test && . ./contracts/zksync/test/.env.test && \ 107 | . ./contracts/ethereum/deploy.sh && \ 108 | . ./contracts/zksync/deploy.sh && \ 109 | . ./contracts/ethereum/set_zksync_escrow.sh && \ 110 | . ./contracts/zksync/test/set_order.sh && \ 111 | . ./contracts/zksync/test/transfer.sh && \ 112 | . ./contracts/zksync/test/claim_payment.sh 113 | 114 | # zksync-upgrade: WIP 115 | 116 | 117 | ### MULTI ### 118 | 119 | ethereum-and-zksync-deploy: 120 | @. ./contracts/ethereum/.env && \ 121 | . ./contracts/zksync/.env && \ 122 | make ethereum-build && \ 123 | make zksync-build && \ 124 | . ./contracts/ethereum/deploy.sh && \ 125 | . ./contracts/zksync/deploy.sh && \ 126 | . ./contracts/ethereum/set_zksync_escrow.sh && \ 127 | . ./contracts/utils/display_info.sh 128 | 129 | ethereum-and-starknet-deploy: 130 | @. ./contracts/ethereum/.env && \ 131 | . ./contracts/starknet/.env && \ 132 | make ethereum-build && \ 133 | make starknet-build && \ 134 | . ./contracts/ethereum/deploy.sh && \ 135 | . ./contracts/starknet/deploy.sh && \ 136 | . ./contracts/ethereum/set_starknet_escrow.sh && \ 137 | . ./contracts/ethereum/set_starknet_claim_payment_selector.sh && \ 138 | . ./contracts/utils/display_info.sh 139 | 140 | deploy-all: 141 | @. ./contracts/ethereum/.env && . ./contracts/starknet/.env && . ./contracts/zksync/.env && \ 142 | make ethereum-build && \ 143 | . ./contracts/ethereum/deploy.sh && \ 144 | make starknet-build && \ 145 | . ./contracts/starknet/deploy.sh && \ 146 | . ./contracts/ethereum/set_starknet_escrow.sh && \ 147 | . ./contracts/ethereum/set_starknet_claim_payment_selector.sh && \ 148 | . ./contracts/utils/display_info.sh && \ 149 | make zksync-build && \ 150 | . ./contracts/zksync/deploy.sh && \ 151 | . ./contracts/ethereum/set_zksync_escrow.sh 152 | 153 | test: 154 | make starknet-test 155 | make ethereum-test 156 | make zksync-test 157 | -------------------------------------------------------------------------------- /contracts/ethereum/.env.example: -------------------------------------------------------------------------------- 1 | #To interact with ETH 2 | ETHEREUM_RPC= #as given by your rpc 3 | ETHERSCAN_API_KEY= #as given by etherscan 4 | 5 | ETHEREUM_PRIVATE_KEY= #in hexa with the 0x prefix 6 | STARKNET_MESSAGING_ADDRESS=<0xde29d060D45901Fb19ED6C6e959EB22d8626708e|0xE2Bb56ee936fd6433DC0F6e7e3b8365C906AA057|0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4> # Goerli | Sepolia | Mainnet 7 | MM_ETHEREUM_WALLET_ADDRESS= #in hexa with the 0x prefix 8 | 9 | ZKSYNC_DIAMOND_PROXY_ADDRESS=<0x9A6DE0f62Aa270A8bCB1e2610078650D539B1Ef9> # Sepolia 10 | -------------------------------------------------------------------------------- /contracts/ethereum/.env.test: -------------------------------------------------------------------------------- 1 | ETHEREUM_RPC=http://127.0.0.1:8545 2 | ETHERSCAN_API_KEY=0x1 3 | ETHEREUM_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 4 | STARKNET_MESSAGING_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 5 | MM_ETHEREUM_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 6 | SKIP_VERIFY=true 7 | -------------------------------------------------------------------------------- /contracts/ethereum/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | /broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | .env.sepolia 16 | .env.goerli 17 | .env.local 18 | -------------------------------------------------------------------------------- /contracts/ethereum/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | cd contracts/ethereum 5 | 6 | printf "${GREEN}\n=> [ETH] Deploying ERC1967Proxy & PaymentRegistry ${COLOR_RESET}\n" 7 | 8 | 9 | export ETHEREUM_PRIVATE_KEY=$ETHEREUM_PRIVATE_KEY 10 | 11 | RESULT_LOG=$(forge script ./script/Deploy.s.sol --rpc-url $ETHEREUM_RPC --broadcast ${SKIP_VERIFY:---verify}) 12 | # echo "$RESULT_LOG" #uncomment this line for debugging in detail 13 | 14 | # Getting result addresses 15 | PAYMENT_REGISTRY_PROXY_ADDRESS=$(echo "$RESULT_LOG" | grep -Eo '0: address ([^\n]+)' | awk '{print $NF}') 16 | PAYMENT_REGISTRY_ADDRESS=$(echo "$RESULT_LOG" | grep -Eo '1: address ([^\n]+)' | awk '{print $NF}') 17 | 18 | if [ -z "$PAYMENT_REGISTRY_PROXY_ADDRESS" ]; then 19 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 20 | echo "PAYMENT_REGISTRY_PROXY_ADDRESS Variable is empty. Aborting execution.\n" 21 | exit 1 22 | fi 23 | if [ -z "$PAYMENT_REGISTRY_ADDRESS" ]; then 24 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 25 | echo "PAYMENT_REGISTRY_ADDRESS Variable is empty. Aborting execution.\n" 26 | exit 1 27 | fi 28 | 29 | printf "${GREEN}\n=> [ETH] Deployed Proxy address: $PAYMENT_REGISTRY_PROXY_ADDRESS ${COLOR_RESET}\n" 30 | printf "${GREEN}\n=> [ETH] Deployed PaymentRegistry address: $PAYMENT_REGISTRY_ADDRESS ${COLOR_RESET}\n" 31 | 32 | echo "\nIf you now wish to deploy an Escrow, you will need to run the following commands:" 33 | echo "export PAYMENT_REGISTRY_PROXY_ADDRESS=$PAYMENT_REGISTRY_PROXY_ADDRESS" 34 | echo "make starknet-deploy" 35 | echo "OR" 36 | echo "make zksync-deploy" 37 | 38 | cd ../.. #to reset working directory 39 | -------------------------------------------------------------------------------- /contracts/ethereum/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | gas_reports = ["*"] 6 | remappings = [ 7 | "@openzeppelin/contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts", 8 | "@matterlabs/interfaces=lib/era-contracts/l1-contracts/contracts/zksync/interfaces", 9 | ] 10 | 11 | [rpc_endpoints] 12 | goerli = "${GOERLI_RPC_URL}" 13 | 14 | [etherscan] 15 | goerli = { key = "${ETHERSCAN_API_KEY}" } 16 | -------------------------------------------------------------------------------- /contracts/ethereum/lib/starknet/IStarknetMessaging.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2022 StarkWare Industries Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.starkware.co/open-source-license/ 9 | 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions 14 | and limitations under the License. 15 | */ 16 | // SPDX-License-Identifier: Apache-2.0. 17 | pragma solidity ^0.8.0; 18 | 19 | import "./IStarknetMessagingEvents.sol"; 20 | 21 | interface IStarknetMessaging is IStarknetMessagingEvents { 22 | /** 23 | Returns the max fee (in Wei) that StarkNet will accept per single message. 24 | */ 25 | function getMaxL1MsgFee() external pure returns (uint256); 26 | 27 | /** 28 | Sends a message to an L2 contract. 29 | This function is payable, the payed amount is the message fee. 30 | 31 | Returns the hash of the message and the nonce of the message. 32 | */ 33 | function sendMessageToL2( 34 | uint256 toAddress, 35 | uint256 selector, 36 | uint256[] calldata payload 37 | ) external payable returns (bytes32, uint256); 38 | 39 | /** 40 | Consumes a message that was sent from an L2 contract. 41 | 42 | Returns the hash of the message. 43 | */ 44 | function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) 45 | external 46 | returns (bytes32); 47 | 48 | /** 49 | Starts the cancellation of an L1 to L2 message. 50 | A message can be canceled messageCancellationDelay() seconds after this function is called. 51 | 52 | Note: This function may only be called for a message that is currently pending and the caller 53 | must be the sender of the that message. 54 | */ 55 | function startL1ToL2MessageCancellation( 56 | uint256 toAddress, 57 | uint256 selector, 58 | uint256[] calldata payload, 59 | uint256 nonce 60 | ) external returns (bytes32); 61 | 62 | /** 63 | Cancels an L1 to L2 message, this function should be called at least 64 | messageCancellationDelay() seconds after the call to startL1ToL2MessageCancellation(). 65 | A message may only be cancelled by its sender. 66 | If the message is missing, the call will revert. 67 | 68 | Note that the message fee is not refunded. 69 | */ 70 | function cancelL1ToL2Message( 71 | uint256 toAddress, 72 | uint256 selector, 73 | uint256[] calldata payload, 74 | uint256 nonce 75 | ) external returns (bytes32); 76 | } 77 | -------------------------------------------------------------------------------- /contracts/ethereum/lib/starknet/IStarknetMessagingEvents.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2022 StarkWare Industries Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.starkware.co/open-source-license/ 9 | 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions 14 | and limitations under the License. 15 | */ 16 | // SPDX-License-Identifier: Apache-2.0. 17 | pragma solidity ^0.8.0; 18 | 19 | interface IStarknetMessagingEvents { 20 | // This event needs to be compatible with the one defined in Output.sol. 21 | event LogMessageToL1(uint256 indexed fromAddress, address indexed toAddress, uint256[] payload); 22 | 23 | // An event that is raised when a message is sent from L1 to L2. 24 | event LogMessageToL2( 25 | address indexed fromAddress, 26 | uint256 indexed toAddress, 27 | uint256 indexed selector, 28 | uint256[] payload, 29 | uint256 nonce, 30 | uint256 fee 31 | ); 32 | 33 | // An event that is raised when a message from L2 to L1 is consumed. 34 | event ConsumedMessageToL1(uint256 indexed fromAddress, address indexed toAddress, uint256[] payload); 35 | 36 | // An event that is raised when a message from L1 to L2 is consumed. 37 | event ConsumedMessageToL2( 38 | address indexed fromAddress, 39 | uint256 indexed toAddress, 40 | uint256 indexed selector, 41 | uint256[] payload, 42 | uint256 nonce 43 | ); 44 | 45 | // An event that is raised when a message from L1 to L2 Cancellation is started. 46 | event MessageToL2CancellationStarted( 47 | address indexed fromAddress, 48 | uint256 indexed toAddress, 49 | uint256 indexed selector, 50 | uint256[] payload, 51 | uint256 nonce 52 | ); 53 | 54 | // An event that is raised when a message from L1 to L2 is canceled. 55 | event MessageToL2Canceled( 56 | address indexed fromAddress, 57 | uint256 indexed toAddress, 58 | uint256 indexed selector, 59 | uint256[] payload, 60 | uint256 nonce 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /contracts/ethereum/lib/starknet/NamedStorage.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2022 StarkWare Industries Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.starkware.co/open-source-license/ 9 | 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions 14 | and limitations under the License. 15 | */ 16 | // SPDX-License-Identifier: Apache-2.0. 17 | pragma solidity ^0.8.0; 18 | 19 | /* 20 | Library to provide basic storage, in storage location out of the low linear address space. 21 | 22 | New types of storage variables should be added here upon need. 23 | */ 24 | library NamedStorage { 25 | function bytes32ToUint256Mapping(string memory tag_) 26 | internal 27 | pure 28 | returns (mapping(bytes32 => uint256) storage randomVariable) 29 | { 30 | bytes32 location = keccak256(abi.encodePacked(tag_)); 31 | assembly { 32 | randomVariable.slot := location 33 | } 34 | } 35 | 36 | function bytes32ToAddressMapping(string memory tag_) 37 | internal 38 | pure 39 | returns (mapping(bytes32 => address) storage randomVariable) 40 | { 41 | bytes32 location = keccak256(abi.encodePacked(tag_)); 42 | assembly { 43 | randomVariable.slot := location 44 | } 45 | } 46 | 47 | function uintToAddressMapping(string memory tag_) 48 | internal 49 | pure 50 | returns (mapping(uint256 => address) storage randomVariable) 51 | { 52 | bytes32 location = keccak256(abi.encodePacked(tag_)); 53 | assembly { 54 | randomVariable.slot := location 55 | } 56 | } 57 | 58 | function addressToBoolMapping(string memory tag_) 59 | internal 60 | pure 61 | returns (mapping(address => bool) storage randomVariable) 62 | { 63 | bytes32 location = keccak256(abi.encodePacked(tag_)); 64 | assembly { 65 | randomVariable.slot := location 66 | } 67 | } 68 | 69 | function getUintValue(string memory tag_) internal view returns (uint256 retVal) { 70 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 71 | assembly { 72 | retVal := sload(slot) 73 | } 74 | } 75 | 76 | function setUintValue(string memory tag_, uint256 value) internal { 77 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 78 | assembly { 79 | sstore(slot, value) 80 | } 81 | } 82 | 83 | function setUintValueOnce(string memory tag_, uint256 value) internal { 84 | require(getUintValue(tag_) == 0, "ALREADY_SET"); 85 | setUintValue(tag_, value); 86 | } 87 | 88 | function getAddressValue(string memory tag_) internal view returns (address retVal) { 89 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 90 | assembly { 91 | retVal := sload(slot) 92 | } 93 | } 94 | 95 | function setAddressValue(string memory tag_, address value) internal { 96 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 97 | assembly { 98 | sstore(slot, value) 99 | } 100 | } 101 | 102 | function setAddressValueOnce(string memory tag_, address value) internal { 103 | require(getAddressValue(tag_) == address(0x0), "ALREADY_SET"); 104 | setAddressValue(tag_, value); 105 | } 106 | 107 | function getBoolValue(string memory tag_) internal view returns (bool retVal) { 108 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 109 | assembly { 110 | retVal := sload(slot) 111 | } 112 | } 113 | 114 | function setBoolValue(string memory tag_, bool value) internal { 115 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 116 | assembly { 117 | sstore(slot, value) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /contracts/ethereum/script/Deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {PaymentRegistry} from "../src/PaymentRegistry.sol"; 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | contract Deploy is Script { 9 | function run() external returns (address, address) { 10 | uint256 deployerPrivateKey = vm.envUint("ETHEREUM_PRIVATE_KEY"); 11 | vm.startBroadcast(deployerPrivateKey); 12 | 13 | address snMessagingAddress = vm.envAddress("STARKNET_MESSAGING_ADDRESS"); 14 | uint256 snEscrowAddress = 0x0; // this value is set in a call to the smart contract, once deployed 15 | uint256 snClaimPaymentSelector = 0x0; // this value is set in a call to the smart contract, once deployed 16 | address marketMaker = vm.envAddress("MM_ETHEREUM_WALLET_ADDRESS"); 17 | address ZKSYNC_DIAMOND_PROXY_ADDRESS = vm.envAddress("ZKSYNC_DIAMOND_PROXY_ADDRESS"); 18 | 19 | PaymentRegistry yab = new PaymentRegistry(); 20 | ERC1967Proxy proxy = new ERC1967Proxy(address(yab), ""); 21 | PaymentRegistry(address(proxy)).initialize(snMessagingAddress, snEscrowAddress, snClaimPaymentSelector, marketMaker, ZKSYNC_DIAMOND_PROXY_ADDRESS); 22 | 23 | vm.stopBroadcast(); 24 | 25 | return (address(proxy), address(yab)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/ethereum/script/Upgrade.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {console} from "../lib/forge-std/src/console.sol"; 5 | import {Script} from "forge-std/Script.sol"; 6 | import {PaymentRegistry} from "../src/PaymentRegistry.sol"; 7 | import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; 8 | 9 | contract Upgrade is Script { 10 | function run() external returns (address, address) { 11 | address PaymentRegistryProxyAddress = vm.envAddress("PAYMENT_REGISTRY_PROXY_ADDRESS"); 12 | uint256 deployerPrivateKey = vm.envUint("ETHEREUM_PRIVATE_KEY"); 13 | vm.startBroadcast(deployerPrivateKey); 14 | 15 | // Deploy new PaymentRegistry contract to upgrade proxy 16 | PaymentRegistry yab = new PaymentRegistry(); 17 | vm.stopBroadcast(); 18 | 19 | return upgrade(PaymentRegistryProxyAddress, address(yab)); 20 | } 21 | 22 | function upgrade( 23 | address proxyAddress, 24 | address newImplementationAddress 25 | ) public returns (address, address) { 26 | uint256 deployerPrivateKey = vm.envUint("ETHEREUM_PRIVATE_KEY"); 27 | vm.startBroadcast(deployerPrivateKey); 28 | 29 | PaymentRegistry proxy = PaymentRegistry(payable(proxyAddress)); 30 | proxy.upgradeToAndCall(address(newImplementationAddress), ''); 31 | 32 | vm.stopBroadcast(); 33 | return (address(proxy), address(newImplementationAddress)); 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /contracts/ethereum/set_starknet_claim_payment_selector.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | if [ -z "$PAYMENT_REGISTRY_PROXY_ADDRESS" ]; then 5 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 6 | echo "PAYMENT_REGISTRY_PROXY_ADDRESS Variable is empty. Aborting execution.\n" 7 | exit 1 8 | fi 9 | if [ -z "$CLAIM_PAYMENT_NAME" ]; then 10 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 11 | echo "CLAIM_PAYMENT_NAME Variable is empty. Aborting execution.\n" 12 | exit 1 13 | fi 14 | 15 | printf "${GREEN}\n=> [ETH] Setting Starknet ClaimPayment Selector on ETH Smart Contract${COLOR_RESET}\n" 16 | echo "Smart contract being modified:" $PAYMENT_REGISTRY_PROXY_ADDRESS 17 | 18 | CLAIM_PAYMENT_SELECTOR=$(starkli selector $CLAIM_PAYMENT_NAME) 19 | echo "New ClaimPayment Selector: ${CLAIM_PAYMENT_SELECTOR}" 20 | 21 | cast send --rpc-url $ETHEREUM_RPC --private-key $ETHEREUM_PRIVATE_KEY $PAYMENT_REGISTRY_PROXY_ADDRESS "setStarknetClaimPaymentSelector(uint256)" "${CLAIM_PAYMENT_SELECTOR}" | grep "transactionHash" 22 | echo "Done setting ClaimPayment selector" 23 | -------------------------------------------------------------------------------- /contracts/ethereum/set_starknet_escrow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | if [ -z "$PAYMENT_REGISTRY_PROXY_ADDRESS" ]; then 5 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 6 | echo "PAYMENT_REGISTRY_PROXY_ADDRESS Variable is empty. Aborting execution.\n" 7 | exit 1 8 | fi 9 | if [ -z "$ESCROW_CONTRACT_ADDRESS" ]; then 10 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 11 | echo "ESCROW_CONTRACT_ADDRESS Variable is empty. Aborting execution.\n" 12 | exit 1 13 | fi 14 | 15 | 16 | printf "${GREEN}\n=> [ETH] Setting Starknet Escrow Address on ETH Smart Contract${COLOR_RESET}\n" 17 | 18 | echo "Smart contract being modified:" $PAYMENT_REGISTRY_PROXY_ADDRESS 19 | echo "New Escrow address:" $ESCROW_CONTRACT_ADDRESS 20 | 21 | cast send --rpc-url $ETHEREUM_RPC --private-key $ETHEREUM_PRIVATE_KEY $PAYMENT_REGISTRY_PROXY_ADDRESS "setStarknetEscrowAddress(uint256)" $ESCROW_CONTRACT_ADDRESS | grep "transactionHash" 22 | echo "Done setting escrow address" 23 | -------------------------------------------------------------------------------- /contracts/ethereum/set_zksync_escrow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | if [ -z "$PAYMENT_REGISTRY_PROXY_ADDRESS" ]; then 5 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 6 | echo "PAYMENT_REGISTRY_PROXY_ADDRESS Variable is empty. Aborting execution.\n" 7 | exit 1 8 | fi 9 | if [ -z "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" ]; then 10 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 11 | echo "ZKSYNC_ESCROW_CONTRACT_ADDRESS Variable is empty. Aborting execution.\n" 12 | fi 13 | if [ -z "$ETHEREUM_RPC" ]; then 14 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 15 | echo "ETHEREUM_RPC Variable is empty. Aborting execution.\n" 16 | fi 17 | if [ -z "$ETHEREUM_PRIVATE_KEY" ]; then 18 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 19 | echo "ETHEREUM_PRIVATE_KEY Variable is empty. Aborting execution.\n" 20 | fi 21 | 22 | 23 | printf "${GREEN}\n=> [ETH] Setting ZKSync Escrow Address on ETH Smart Contract${COLOR_RESET}\n" 24 | 25 | echo "Smart contract being modified:" $PAYMENT_REGISTRY_PROXY_ADDRESS 26 | echo "New ZKSync Escrow address:" $ZKSYNC_ESCROW_CONTRACT_ADDRESS 27 | 28 | cast send --rpc-url $ETHEREUM_RPC --private-key $ETHEREUM_PRIVATE_KEY $PAYMENT_REGISTRY_PROXY_ADDRESS "setZKSyncEscrowAddress(address)" $ZKSYNC_ESCROW_CONTRACT_ADDRESS | grep "transactionHash " 29 | echo "Done setting escrow address" 30 | -------------------------------------------------------------------------------- /contracts/ethereum/test/.env.test: -------------------------------------------------------------------------------- 1 | SKIP_VERIFY=true #cant verify on local devnet 2 | 3 | ETHEREUM_RPC=http://localhost:8545 #local devnet L1 rpc url 4 | 5 | ZKSYNC_DIAMOND_PROXY_ADDRESS=0x97589bcE7727f5D0C8082440681DB6092b6Dda1a 6 | 7 | 8 | ETHEREUM_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 #prefunded 0 9 | 10 | MM_ETHEREUM_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 #prefunded 0 11 | MM_ETHEREUM_PUBLIC_ADDRESS=0x36615Cf349d7F6344891B1e7CA7C72883F5dc049 #prefunded 0 12 | 13 | USER_ETHEREUM_PUBLIC_ADDRESS=0xceee57f2b700c2f37d1476a7974965e149fce2d4 #random address with no funds 14 | USER_ETHEREUM_PUBLIC_ADDRESS_UINT=1181367337507422765615536123397692015769584198356 15 | -------------------------------------------------------------------------------- /contracts/ethereum/test/ACL.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/PaymentRegistry.sol"; 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | contract TransferTest is Test { 9 | address public deployer = makeAddr('deployer'); 10 | address public marketMaker = makeAddr('marketMaker'); 11 | uint256 public snEscrowAddress = 0x0; 12 | 13 | PaymentRegistry public yab; 14 | ERC1967Proxy public proxy; 15 | PaymentRegistry public yab_caller; 16 | 17 | address SN_MESSAGING_ADDRESS = 0xde29d060D45901Fb19ED6C6e959EB22d8626708e; 18 | uint256 SN_ESCROW_CLAIM_PAYMENT_SELECTOR = 0x15511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77; 19 | address ZKSYNC_DIAMOND_PROXY_ADDRESS = 0x2eD8eF54a16bBF721a318bd5a5C0F39Be70eaa65; 20 | 21 | function setUp() public { 22 | vm.startPrank(deployer); 23 | 24 | yab = new PaymentRegistry(); 25 | proxy = new ERC1967Proxy(address(yab), ""); 26 | yab_caller = PaymentRegistry(address(proxy)); 27 | yab_caller.initialize(SN_MESSAGING_ADDRESS, snEscrowAddress, SN_ESCROW_CLAIM_PAYMENT_SELECTOR, marketMaker, ZKSYNC_DIAMOND_PROXY_ADDRESS); 28 | 29 | vm.stopPrank(); 30 | } 31 | 32 | function test_getMarketMaker() public { 33 | address mmAddress = yab_caller.marketMaker(); 34 | assertEq(mmAddress, marketMaker); 35 | } 36 | 37 | function test_set_and_get_MMAddress_deployer() public { 38 | vm.startPrank(deployer); 39 | address alice = makeAddr("alice"); 40 | yab_caller.setMMAddress(alice); 41 | assertEq(yab_caller.marketMaker(), alice); 42 | vm.stopPrank(); 43 | } 44 | 45 | function test_set_MMAddress_not_owner() public { 46 | address bob = makeAddr("bob"); 47 | vm.expectRevert(); //setMMAddress is only callable by the owner 48 | yab_caller.setMMAddress(bob); 49 | } 50 | 51 | function test_get_owner() public { 52 | address ownerAddress = yab_caller.owner(); 53 | assertEq(ownerAddress, deployer); 54 | } 55 | 56 | function test_transfer_sn_fail_notOwnerOrMM() public { 57 | hoax(makeAddr("bob"), 100 wei); 58 | vm.expectRevert("Only Owner or MM can call this function"); 59 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.Starknet); 60 | } 61 | 62 | function test_claimPayment_sn_fail_notOwnerOrMM() public { 63 | hoax(makeAddr("bob"), 100 wei); 64 | vm.expectRevert("Only Owner or MM can call this function"); 65 | yab_caller.claimPayment{value: 100}(1, 0x1, 100); 66 | } 67 | 68 | function test_transfer_zk_fail_notOwnerOrMM() public { 69 | hoax(makeAddr("bob"), 100 wei); 70 | vm.expectRevert("Only Owner or MM can call this function"); 71 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.ZKSync); 72 | } 73 | 74 | function test_claimPayment_zk_fail_notOwnerOrMM() public { 75 | hoax(makeAddr("bob"), 100 wei); 76 | vm.expectRevert("Only Owner or MM can call this function"); 77 | yab_caller.claimPaymentZKSync{value: 100}(1, 0x1, 100, 1, 1); 78 | } 79 | 80 | function test_setStarknetClaimPaymentSelector() public { 81 | vm.startPrank(deployer); 82 | yab_caller.setStarknetClaimPaymentSelector(0x123); 83 | } 84 | 85 | function test_setStarknetClaimPaymentSelector_fail_notOwnerOrMM() public { 86 | hoax(makeAddr("alice"), 100 wei); 87 | vm.expectRevert(); //only owner 88 | yab_caller.setStarknetClaimPaymentSelector(0x123); 89 | } 90 | 91 | function test_set_and_get_StarknetClaimPaymentSelector() public { 92 | vm.startPrank(deployer); 93 | uint256 new_selector = 0x123; 94 | yab_caller.setStarknetClaimPaymentSelector(new_selector); 95 | assertEq(yab_caller.StarknetEscrowClaimPaymentSelector(), new_selector); 96 | vm.stopPrank(); 97 | } 98 | 99 | function test_setStarknetEscrowAddress() public { 100 | vm.startPrank(deployer); 101 | yab_caller.setStarknetEscrowAddress(0x123); 102 | } 103 | 104 | function test_setStarknetEscrowAddress_fail_notOwnerOrMM() public { 105 | hoax(makeAddr("alice"), 100 wei); 106 | vm.expectRevert(); //only owner 107 | yab_caller.setStarknetEscrowAddress(0x123); 108 | } 109 | 110 | function test_set_and_get_StarknetEscrowAddress() public { 111 | vm.startPrank(deployer); 112 | uint256 new_addr = 0x123; 113 | yab_caller.setStarknetEscrowAddress(new_addr); 114 | assertEq(yab_caller.StarknetEscrowAddress(), new_addr); 115 | vm.stopPrank(); 116 | } 117 | 118 | function test_setZKSyncEscrowAddress() public { 119 | vm.startPrank(deployer); 120 | yab_caller.setZKSyncEscrowAddress(makeAddr("escrow")); 121 | } 122 | 123 | function test_setZKSyncEscrowAddress_fail_notOwnerOrMM() public { 124 | hoax(makeAddr("alice"), 100 wei); 125 | vm.expectRevert(); //only owner 126 | yab_caller.setZKSyncEscrowAddress(makeAddr("alice")); 127 | } 128 | 129 | function test_set_and_get_ZKSyncEscrowAddress() public { 130 | vm.startPrank(deployer); 131 | address new_addr = makeAddr("new_addr"); 132 | yab_caller.setZKSyncEscrowAddress(new_addr); 133 | assertEq(yab_caller.ZKSyncEscrowAddress(), new_addr); 134 | vm.stopPrank(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /contracts/ethereum/test/Transfer_Claim_SN.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/PaymentRegistry.sol"; 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | contract TransferTest is Test { 9 | 10 | address public deployer = makeAddr('deployer'); 11 | address public marketMaker = makeAddr("marketMaker"); 12 | uint256 public snEscrowAddress = 0x0; 13 | 14 | PaymentRegistry public yab; 15 | ERC1967Proxy public proxy; 16 | PaymentRegistry public yab_caller; 17 | 18 | address STARKNET_MESSAGING_ADDRESS = 0xde29d060D45901Fb19ED6C6e959EB22d8626708e; 19 | uint256 SN_ESCROW_CLAIM_PAYMENT_SELECTOR = 0x15511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77; 20 | address ZKSYNC_DIAMOND_PROXY_ADDRESS = 0x2eD8eF54a16bBF721a318bd5a5C0F39Be70eaa65; 21 | 22 | function setUp() public { 23 | vm.startPrank(deployer); 24 | 25 | yab = new PaymentRegistry(); 26 | proxy = new ERC1967Proxy(address(yab), ""); 27 | yab_caller = PaymentRegistry(address(proxy)); 28 | yab_caller.initialize(STARKNET_MESSAGING_ADDRESS, snEscrowAddress, SN_ESCROW_CLAIM_PAYMENT_SELECTOR, marketMaker, ZKSYNC_DIAMOND_PROXY_ADDRESS); 29 | 30 | // Mock calls to Starknet Messaging contract 31 | vm.mockCall( 32 | STARKNET_MESSAGING_ADDRESS, 33 | abi.encodeWithSelector(IStarknetMessaging(STARKNET_MESSAGING_ADDRESS).sendMessageToL2.selector), 34 | abi.encode(0x0, 0x1) 35 | ); 36 | vm.stopPrank(); 37 | } 38 | 39 | function test_transfer_sn() public { 40 | hoax(marketMaker, 100 wei); 41 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.Starknet); 42 | assertEq(address(0x1).balance, 100); 43 | } 44 | 45 | function test_claimPayment_sn_fail_noOrderId() public { 46 | hoax(marketMaker, 100 wei); 47 | vm.expectRevert("Transfer not found."); //Won't match to a random transfer number 48 | yab_caller.claimPayment{value: 100}(1, 0x1, 100); 49 | } 50 | 51 | function test_claimPayment_sn_fail_wrongOrderId() public { 52 | hoax(marketMaker, 100 wei); 53 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.Starknet); 54 | hoax(marketMaker, 100 wei); 55 | vm.expectRevert("Transfer not found."); //Won't match to a wrong transfer number 56 | yab_caller.claimPayment(2, 0x1, 100); 57 | } 58 | 59 | function test_claimPayment_sn() public { 60 | hoax(marketMaker, 100 wei); 61 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.Starknet); 62 | hoax(marketMaker, 100 wei); 63 | yab_caller.claimPayment(1, 0x1, 100); 64 | assertEq(address(marketMaker).balance, 100); 65 | } 66 | 67 | function test_claimPayment_sn_maxInt() public { 68 | uint256 maxInt = type(uint256).max; 69 | 70 | vm.deal(marketMaker, maxInt); 71 | vm.startPrank(marketMaker); 72 | 73 | yab_caller.transfer{value: maxInt}(1, 0x1, PaymentRegistry.Chain.Starknet); 74 | yab_caller.claimPayment(1, 0x1, maxInt); 75 | vm.stopPrank(); 76 | } 77 | 78 | function test_claimPayment_sn_minInt() public { 79 | hoax(marketMaker, 1 wei); 80 | yab_caller.transfer{value: 1}(1, 0x1, PaymentRegistry.Chain.Starknet); 81 | hoax(marketMaker, 1 wei); 82 | yab_caller.claimPayment(1, 0x1, 1); 83 | } 84 | 85 | function test_claimPayment_fail_wrongChain() public { 86 | hoax(marketMaker, 1 wei); 87 | yab_caller.transfer{value: 1}(1, 0x1, PaymentRegistry.Chain.Starknet); 88 | hoax(marketMaker, 1 wei); 89 | vm.expectRevert("Transfer not found."); //Won't match to a transfer made on the other chain 90 | yab_caller.claimPaymentZKSync(1, 0x1, 1, 1 ,1); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /contracts/ethereum/test/Transfer_Claim_ZKSync.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.20; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/PaymentRegistry.sol"; 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | contract TransferTest is Test { 9 | 10 | address public deployer = makeAddr('deployer'); 11 | address public marketMaker = makeAddr("marketMaker"); 12 | uint256 public snEscrowAddress = 0x0; 13 | 14 | PaymentRegistry public yab; 15 | ERC1967Proxy public proxy; 16 | PaymentRegistry public yab_caller; 17 | 18 | address SN_MESSAGING_ADDRESS = 0xde29d060D45901Fb19ED6C6e959EB22d8626708e; 19 | uint256 SN_ESCROW_CLAIM_PAYMENT_SELECTOR = 0x15511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77; 20 | address ZKSYNC_DIAMOND_PROXY_ADDRESS = 0x2eD8eF54a16bBF721a318bd5a5C0F39Be70eaa65; 21 | 22 | function setUp() public { 23 | vm.startPrank(deployer); 24 | 25 | yab = new PaymentRegistry(); 26 | proxy = new ERC1967Proxy(address(yab), ""); 27 | yab_caller = PaymentRegistry(address(proxy)); 28 | yab_caller.initialize(SN_MESSAGING_ADDRESS, snEscrowAddress, SN_ESCROW_CLAIM_PAYMENT_SELECTOR, marketMaker, ZKSYNC_DIAMOND_PROXY_ADDRESS); 29 | 30 | //Mock calls to ZKSync Mailbox contract 31 | vm.mockCall( 32 | ZKSYNC_DIAMOND_PROXY_ADDRESS, 33 | abi.encodeWithSelector(0xeb672419, 0), //TODO add selector 34 | abi.encode(0x12345678901234567890123456789012) //TODO add return data 35 | ); 36 | 37 | vm.stopPrank(); 38 | } 39 | 40 | function test_transfer_zk() public { 41 | hoax(marketMaker, 100 wei); 42 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.ZKSync); 43 | assertEq(address(0x1).balance, 100); 44 | } 45 | 46 | function test_claimPayment_zk_fail_noOrderId() public { 47 | hoax(marketMaker, 100 wei); 48 | vm.expectRevert("Transfer not found."); //Won't match to a random transfer number 49 | yab_caller.claimPaymentZKSync(1, 0x1, 100, 1, 1); 50 | } 51 | 52 | function test_claimPayment_zk_fail_wrongOrderId() public { 53 | hoax(marketMaker, 100 wei); 54 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.ZKSync); 55 | hoax(marketMaker, 100 wei); 56 | vm.expectRevert("Transfer not found."); //Won't match to a wrong transfer number 57 | yab_caller.claimPaymentZKSync(2, 0x1, 100, 1, 1); 58 | } 59 | 60 | function test_claimPayment_zk() public { 61 | hoax(marketMaker, 100 wei); 62 | yab_caller.transfer{value: 100}(1, 0x1, PaymentRegistry.Chain.ZKSync); 63 | hoax(marketMaker, 100 wei); 64 | yab_caller.claimPaymentZKSync(1, 0x1, 100, 1, 1); 65 | assertEq(address(marketMaker).balance, 100); 66 | } 67 | 68 | function test_claimPayment_zk_maxInt() public { 69 | uint256 maxInt = type(uint256).max; 70 | 71 | vm.deal(marketMaker, maxInt); 72 | vm.startPrank(marketMaker); 73 | 74 | yab_caller.transfer{value: maxInt}(1, 0x1, PaymentRegistry.Chain.ZKSync); 75 | yab_caller.claimPaymentZKSync(1, 0x1, maxInt, 1, 1); 76 | vm.stopPrank(); 77 | } 78 | 79 | function test_claimPayment_zk_minInt() public { 80 | hoax(marketMaker, 1 wei); 81 | yab_caller.transfer{value: 1}(1, 0x1, PaymentRegistry.Chain.ZKSync); 82 | hoax(marketMaker, 1 wei); 83 | yab_caller.claimPaymentZKSync(1, 0x1, 1, 1, 1); 84 | } 85 | 86 | function test_claimPayment_fail_wrongChain() public { 87 | hoax(marketMaker, 1 wei); 88 | yab_caller.transfer{value: 1}(1, 0x1, PaymentRegistry.Chain.ZKSync); 89 | hoax(marketMaker, 1 wei); 90 | vm.expectRevert("Transfer not found."); //Won't match to a transfer made on the other chain 91 | yab_caller.claimPayment(1, 0x1, 1); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/ethereum/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | cd contracts/ethereum 5 | 6 | if [ -z "$PAYMENT_REGISTRY_PROXY_ADDRESS" ]; then 7 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 8 | echo "PAYMENT_REGISTRY_PROXY_ADDRESS Variable is empty. Aborting execution.\n" 9 | exit 1 10 | fi 11 | if [ -z "$ETHEREUM_PRIVATE_KEY" ]; then 12 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 13 | echo "ETHEREUM_PRIVATE_KEY Variable is empty. Aborting execution.\n" 14 | exit 1 15 | fi 16 | 17 | printf "${GREEN}\n=> [ETH] Upgrading PaymentRegistry ${COLOR_RESET}\n" 18 | 19 | RESULT_LOG=$(forge script ./script/Upgrade.s.sol --rpc-url $ETHEREUM_RPC --broadcast --verify) 20 | # echo "$RESULT_LOG" #uncomment this line for debugging in detail 21 | 22 | # Getting result addresses 23 | PROXY_ADDRESS=$(echo "$RESULT_LOG" | grep -oP '0: address \K[^\n]+' | awk '{print $0}') 24 | PAYMENT_REGISTRY_ADDRESS=$(echo "$RESULT_LOG" | grep -oP '1: address \K[^\n]+' | awk '{print $0}') 25 | 26 | if [ -z "$PAYMENT_REGISTRY_ADDRESS" ]; then 27 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 28 | echo "PAYMENT_REGISTRY_ADDRESS Variable is empty. Aborting execution.\n" 29 | exit 1 30 | fi 31 | 32 | printf "${GREEN}\n=> [ETH] Unchanged PaymentRegistry Proxy address: $PAYMENT_REGISTRY_PROXY_ADDRESS ${COLOR_RESET}\n" 33 | printf "${GREEN}\n=> [ETH] Newly Deployed PaymentRegistry contract address: $PAYMENT_REGISTRY_ADDRESS ${COLOR_RESET}\n" 34 | 35 | cd ../.. 36 | -------------------------------------------------------------------------------- /contracts/starknet/.env.example: -------------------------------------------------------------------------------- 1 | ## Starkli 2 | STARKNET_ACCOUNT= 3 | STARKNET_KEYSTORE= 4 | STARKNET_RPC= 5 | 6 | ## Required for Escrow Contract 7 | STARKNET_ESCROW_OWNER= #in lowercase hexa with the 0x prefix 8 | MM_STARKNET_WALLET_ADDRESS= #in lowercase hexa with the 0x prefix 9 | CLAIM_PAYMENT_NAME= #must match the exact name of the function to claim the payment from the starknet smart contract 10 | MM_ETHEREUM_WALLET_ADDRESS= #in lowercase hexa with the 0x prefix 11 | NATIVE_TOKEN_ETH_STARKNET= #in lowercase hexa with the 0x prefix 12 | -------------------------------------------------------------------------------- /contracts/starknet/.env.test: -------------------------------------------------------------------------------- 1 | STARKNET_ACCOUNT=/home/runner/.config/.starkli/account_katana.json 2 | STARKNET_ACCOUNT_ADDRESS=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 3 | STARKNET_PRIVATE_KEY=0x1800000000300000180000000000030000000000003006001800006600 4 | STARKNET_RPC=http://0.0.0.0:5050 5 | STARKNET_ESCROW_OWNER=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 6 | MM_STARKNET_WALLET_ADDRESS=0x5686a647a9cdd63ade617e0baf3b364856b813b508f03903eb58a7e622d5855 7 | CLAIM_PAYMENT_NAME=claim_payment 8 | MM_ETHEREUM_WALLET_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 9 | NATIVE_TOKEN_ETH_STARKNET=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 10 | -------------------------------------------------------------------------------- /contracts/starknet/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .snfoundry_cache 3 | .env 4 | .env.sepolia 5 | .env.goerli 6 | .env.local 7 | -------------------------------------------------------------------------------- /contracts/starknet/.tool-versions: -------------------------------------------------------------------------------- 1 | scarb 2.3.1 2 | starknet-foundry 0.12.0 -------------------------------------------------------------------------------- /contracts/starknet/Scarb.lock: -------------------------------------------------------------------------------- 1 | # Code generated by scarb DO NOT EDIT. 2 | version = 1 3 | 4 | [[package]] 5 | name = "openzeppelin" 6 | version = "0.8.0" 7 | source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.8.0#c23e8e96de60e6e3159b1ff8591a1187269c0eb7" 8 | 9 | [[package]] 10 | name = "snforge_std" 11 | version = "0.1.0" 12 | source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.12.0#0c3d2fe4ab31aa4484fa216417408ae65a149efe" 13 | 14 | [[package]] 15 | name = "yab" 16 | version = "0.1.0" 17 | dependencies = [ 18 | "openzeppelin", 19 | "snforge_std", 20 | ] 21 | -------------------------------------------------------------------------------- /contracts/starknet/Scarb.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yab" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | starknet = "2.3.1" 7 | snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.12.0" } 8 | openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.0"} 9 | 10 | [[target.starknet-contract]] 11 | sierra = true 12 | casm = true 13 | 14 | [tool.snforge] 15 | exit_first = true 16 | 17 | -------------------------------------------------------------------------------- /contracts/starknet/change_pause_state.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$STARKNET_ACCOUNT" ]; then 4 | echo "\n${RED}ERROR:${COLOR_RESET}" 5 | echo "STARKNET_ACCOUNT Variable is empty. Aborting execution.\n" 6 | exit 1 7 | fi 8 | if [ -z "$STARKNET_KEYSTORE" ]; then 9 | echo "\n${RED}ERROR:${COLOR_RESET}" 10 | echo "STARKNET_KEYSTORE Variable is empty. Aborting execution.\n" 11 | exit 1 12 | fi 13 | 14 | # Starkli implicitly utilizes these environment variables, so every time we use Starkli, 15 | # we avoid adding flags such as --account, --keystore, and --rpc. 16 | export STARKNET_ACCOUNT=$STARKNET_ACCOUNT 17 | export STARKNET_KEYSTORE=$STARKNET_KEYSTORE 18 | # export STARKNET_RPC=$STARKNET_RPC #todo: this must remain commented until we find a reliable and compatible rpc 19 | 20 | if [ -z "$ESCROW_CONTRACT_ADDRESS" ]; then 21 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 22 | echo "ESCROW_CONTRACT_ADDRESS Variable is empty. Aborting execution.\n" 23 | exit 1 24 | fi 25 | 26 | if [[ $1 == 'pause' ]]; then 27 | starkli invoke $ESCROW_CONTRACT_ADDRESS pause 28 | elif [[ "$1" == 'unpause' ]]; then 29 | starkli invoke $ESCROW_CONTRACT_ADDRESS unpause 30 | else 31 | echo "Error, parameter must be 'pause' or 'unpause'" 32 | fi 33 | -------------------------------------------------------------------------------- /contracts/starknet/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | export STARKNET_RPC=$STARKNET_RPC 5 | 6 | if [ -z "$STARKNET_ACCOUNT" ]; then 7 | printf "\n${RED}ERROR:${COLOR_RESET}" 8 | echo "STARKNET_ACCOUNT Variable is empty. Aborting execution.\n" 9 | exit 1 10 | fi 11 | if [ -z "$STARKNET_KEYSTORE" ] && [ -z "$STARKNET_PRIVATE_KEY" ]; then 12 | printf "\n${RED}ERROR:${COLOR_RESET}" 13 | echo "STARKNET_KEYSTORE and STARKNET_PRIVATE_KEY Variables are empty. Aborting execution.\n" 14 | exit 1 15 | fi 16 | if [ -z "$MM_STARKNET_WALLET_ADDRESS" ]; then 17 | printf "\n${RED}ERROR:${COLOR_RESET}" 18 | echo "MM_STARKNET_WALLET_ADDRESS Variable is empty. Aborting execution.\n" 19 | exit 1 20 | fi 21 | if [ -z "$NATIVE_TOKEN_ETH_STARKNET" ]; then 22 | printf "\n${RED}ERROR:${COLOR_RESET}" 23 | echo "NATIVE_TOKEN_ETH_STARKNET Variable is empty. Aborting execution.\n" 24 | exit 1 25 | fi 26 | if [ -z "$PAYMENT_REGISTRY_PROXY_ADDRESS" ]; then 27 | printf "\n${RED}ERROR:${COLOR_RESET}" 28 | echo "PAYMENT_REGISTRY_PROXY_ADDRESS Variable is empty. Aborting execution.\n" 29 | exit 1 30 | fi 31 | if [ -z "$MM_ETHEREUM_WALLET_ADDRESS" ]; then 32 | printf "\n${RED}ERROR:${COLOR_RESET}" 33 | echo "MM_ETHEREUM_WALLET_ADDRESS Variable is empty. Aborting execution.\n" 34 | exit 1 35 | fi 36 | 37 | 38 | printf "${GREEN}\n=> [SN] Declaring Escrow${COLOR_RESET}\n" 39 | ESCROW_CLASS_HASH=$(starkli declare \ 40 | --account $STARKNET_ACCOUNT \ 41 | $(if [ -n "$STARKNET_KEYSTORE" ]; then echo "--keystore $STARKNET_KEYSTORE"; fi) \ 42 | $(if [ -n "$STARKNET_PRIVATE_KEY" ]; then echo "--private-key $STARKNET_PRIVATE_KEY"; fi) \ 43 | --watch contracts/starknet/target/dev/yab_Escrow.contract_class.json) 44 | 45 | 46 | if [ -z "$ESCROW_CLASS_HASH" ]; then 47 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 48 | echo "ESCROW_CLASS_HASH Variable is empty. Aborting execution.\n" 49 | exit 1 50 | fi 51 | 52 | if [ -z "$STARKNET_ESCROW_OWNER" ]; then 53 | echo "" #\n 54 | printf "${ORANGE}WARNING:${COLOR_RESET} no STARKNET_ESCROW_OWNER defined in .env, declaring deployer as the owner of the contract\n" 55 | STARKNET_ESCROW_OWNER=$(cat "$STARKNET_ACCOUNT" | grep '"address"' | sed -E 's/.*"address": "([^"]+)".*/\1/') 56 | fi 57 | 58 | 59 | printf "${GREEN}\n=> [SN] Escrow Declared${COLOR_RESET}\n" 60 | 61 | printf "${CYAN}[SN] Escrow ClassHash: $ESCROW_CLASS_HASH${COLOR_RESET}\n" 62 | printf "${CYAN}[SN] Market Maker SN Wallet: $MM_STARKNET_WALLET_ADDRESS${COLOR_RESET}\n" 63 | printf "${CYAN}[SN] Ethereum ERC20 ContractAddress $NATIVE_TOKEN_ETH_STARKNET${COLOR_RESET}\n" 64 | printf "${PINK}[ETH] PaymentRegistry Proxy Address: $PAYMENT_REGISTRY_PROXY_ADDRESS${COLOR_RESET}\n" 65 | printf "${PINK}[ETH] Market Maker ETH Wallet: $MM_ETHEREUM_WALLET_ADDRESS${COLOR_RESET}\n" 66 | 67 | printf "${GREEN}\n=> [SN] Deploying Escrow${COLOR_RESET}\n" 68 | ESCROW_CONTRACT_ADDRESS=$(starkli deploy \ 69 | --account $STARKNET_ACCOUNT \ 70 | $(if [ -n "$STARKNET_KEYSTORE" ]; then echo "--keystore $STARKNET_KEYSTORE"; fi) \ 71 | $(if [ -n "$STARKNET_PRIVATE_KEY" ]; then echo "--private-key $STARKNET_PRIVATE_KEY"; fi) \ 72 | --watch $ESCROW_CLASS_HASH \ 73 | $STARKNET_ESCROW_OWNER \ 74 | $PAYMENT_REGISTRY_PROXY_ADDRESS \ 75 | $MM_ETHEREUM_WALLET_ADDRESS \ 76 | $MM_STARKNET_WALLET_ADDRESS \ 77 | $NATIVE_TOKEN_ETH_STARKNET) 78 | echo $ESCROW_CONTRACT_ADDRESS 79 | 80 | printf "${GREEN}\n=> [SN] Escrow Deployed${COLOR_RESET}\n" 81 | 82 | printf "${CYAN}[SN] Escrow Address: $ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" 83 | 84 | echo "\nIf you now wish to finish the configuration of this deploy, you will need to run the following commands:" 85 | echo "export PAYMENT_REGISTRY_PROXY_ADDRESS=$PAYMENT_REGISTRY_PROXY_ADDRESS" 86 | echo "export ESCROW_CONTRACT_ADDRESS=$ESCROW_CONTRACT_ADDRESS" 87 | echo "make ethereum-set-escrow" 88 | echo "make ethereum-set-claim-payment-selector" 89 | -------------------------------------------------------------------------------- /contracts/starknet/src/interfaces/IERC20.cairo: -------------------------------------------------------------------------------- 1 | use starknet::ContractAddress; 2 | 3 | #[starknet::interface] 4 | trait IERC20 { 5 | fn name(self: @TState) -> felt252; 6 | fn symbol(self: @TState) -> felt252; 7 | fn decimals(self: @TState) -> u8; 8 | fn totalSupply(self: @TState) -> u256; 9 | fn balanceOf(self: @TState, account: ContractAddress) -> u256; 10 | fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; 11 | fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; 12 | fn transferFrom( 13 | ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 14 | ) -> bool; 15 | fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; 16 | } 17 | -------------------------------------------------------------------------------- /contracts/starknet/src/interfaces/IEVMFactsRegistry.cairo: -------------------------------------------------------------------------------- 1 | #[starknet::interface] 2 | trait IEVMFactsRegistry { 3 | fn get_slot_value(self: @TState, account: felt252, block: u256, slot: u256) -> Option; 4 | } 5 | -------------------------------------------------------------------------------- /contracts/starknet/src/lib.cairo: -------------------------------------------------------------------------------- 1 | mod ERC20; 2 | mod escrow; 3 | 4 | mod interfaces { 5 | mod IERC20; 6 | mod IEVMFactsRegistry; 7 | } 8 | 9 | mod mocks { 10 | mod mock_EVMFactsRegistry; 11 | mod mock_Escrow_changed_functions; 12 | mod mock_pausableEscrow; 13 | 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | mod test_escrow_allowance; 19 | mod test_escrow_pause; 20 | mod test_escrow_upgrade; 21 | mod test_escrow_ownable; 22 | mod utils { 23 | mod constants; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/starknet/src/mocks/mock_EVMFactsRegistry.cairo: -------------------------------------------------------------------------------- 1 | #[starknet::contract] 2 | mod EVMFactsRegistry { 3 | use yab::interfaces::IEVMFactsRegistry::IEVMFactsRegistry; 4 | 5 | #[storage] 6 | struct Storage { 7 | slots: LegacyMap:: 8 | } 9 | 10 | #[constructor] 11 | fn constructor(ref self: ContractState) { 12 | self.slots.write(0, 12345); // mock recipient_address 13 | self.slots.write(1, 500); // mock amount 14 | } 15 | 16 | #[external(v0)] 17 | impl EVMFactsRegistry of IEVMFactsRegistry { 18 | fn get_slot_value( 19 | self: @ContractState, account: felt252, block: u256, slot: u256 20 | ) -> Option { 21 | Option::Some(self.slots.read(slot)) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/starknet/src/tests/test_escrow_ownable.cairo: -------------------------------------------------------------------------------- 1 | mod Escrow { 2 | use core::to_byte_array::FormatAsByteArray; 3 | use core::serde::Serde; 4 | use core::traits::Into; 5 | use starknet::{EthAddress, ContractAddress}; 6 | use integer::BoundedInt; 7 | 8 | use snforge_std::{declare, ContractClassTrait, L1Handler, L1HandlerTrait}; 9 | use snforge_std::{CheatTarget, start_prank, stop_prank, start_warp, stop_warp}; 10 | 11 | use yab::mocks::mock_Escrow_changed_functions::{IEscrow_mock_changed_functionsDispatcher, IEscrow_mock_changed_functionsDispatcherTrait}; 12 | use yab::mocks::mock_pausableEscrow::{IEscrow_mockPausableDispatcher, IEscrow_mockPausableDispatcherTrait}; 13 | use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; 14 | use yab::escrow::{IEscrowDispatcher, IEscrowDispatcherTrait, Order}; 15 | use yab::interfaces::IEVMFactsRegistry::{ 16 | IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait 17 | }; 18 | 19 | use yab::tests::utils::{ 20 | constants::EscrowConstants::{ 21 | USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT, ETH_USER 22 | }, 23 | }; 24 | 25 | use openzeppelin::{ 26 | upgrades::{ 27 | UpgradeableComponent, 28 | interface::{IUpgradeable, IUpgradeableDispatcher, IUpgradeableDispatcherTrait} 29 | }, 30 | }; 31 | 32 | fn setup() -> (IEscrowDispatcher, IERC20Dispatcher) { 33 | setup_general(BoundedInt::max(), BoundedInt::max()) 34 | } 35 | 36 | fn setup_approved(approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ 37 | setup_general(BoundedInt::max(), approved) 38 | } 39 | 40 | fn setup_balance(balance: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ 41 | setup_general(balance, BoundedInt::max()) 42 | } 43 | 44 | fn setup_general(balance: u256, approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ 45 | let eth_token = deploy_erc20('ETH', '$ETH', BoundedInt::max(), OWNER()); 46 | let escrow = deploy_escrow( 47 | OWNER(), 48 | ETH_TRANSFER_CONTRACT(), 49 | MM_ETHEREUM(), 50 | MM_STARKNET(), 51 | eth_token.contract_address 52 | ); 53 | 54 | start_prank(CheatTarget::One(eth_token.contract_address), OWNER()); 55 | eth_token.transfer(USER(), balance); 56 | stop_prank(CheatTarget::One(eth_token.contract_address)); 57 | 58 | start_prank(CheatTarget::One(eth_token.contract_address), USER()); 59 | eth_token.approve(escrow.contract_address, approved); 60 | stop_prank(CheatTarget::One(eth_token.contract_address)); 61 | 62 | (escrow, eth_token) 63 | } 64 | 65 | fn deploy_escrow( 66 | escrow_owner: ContractAddress, 67 | eth_transfer_contract: EthAddress, 68 | mm_ethereum_contract: EthAddress, 69 | mm_starknet_contract: ContractAddress, 70 | native_token_eth_starknet: ContractAddress 71 | ) -> IEscrowDispatcher { 72 | let escrow = declare('Escrow'); 73 | let mut calldata: Array = ArrayTrait::new(); 74 | calldata.append(escrow_owner.into()); 75 | calldata.append(eth_transfer_contract.into()); 76 | calldata.append(mm_ethereum_contract.into()); 77 | calldata.append(mm_starknet_contract.into()); 78 | calldata.append(native_token_eth_starknet.into()); 79 | let address = escrow.deploy(@calldata).unwrap(); 80 | return IEscrowDispatcher { contract_address: address }; 81 | } 82 | 83 | fn deploy_erc20( 84 | name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress 85 | ) -> IERC20Dispatcher { 86 | let erc20 = declare('ERC20'); 87 | let mut calldata = array![name, symbol]; 88 | Serde::serialize(@initial_supply, ref calldata); 89 | calldata.append(recipent.into()); 90 | let address = erc20.deploy(@calldata).unwrap(); 91 | return IERC20Dispatcher { contract_address: address }; 92 | } 93 | 94 | #[test] 95 | #[should_panic(expected: ('Caller is not the owner',))] 96 | fn test_fail_upgrade_escrow_caller_isnt_the_owner() { 97 | let (escrow, _) = setup(); 98 | let upgradeable = IUpgradeableDispatcher { contract_address: escrow.contract_address }; 99 | start_prank(CheatTarget::One(escrow.contract_address), MM_STARKNET()); 100 | upgradeable.upgrade(declare('Escrow_mock_changed_functions').class_hash); 101 | } 102 | 103 | #[test] 104 | #[should_panic(expected: ('Caller is not the owner',))] 105 | fn test_fail_set_eth_transfer_contract() { 106 | let (escrow, _) = setup(); 107 | escrow.set_eth_transfer_contract(MM_ETHEREUM()); 108 | } 109 | 110 | #[test] 111 | fn test_set_eth_transfer_contract() { 112 | let (escrow, _) = setup(); 113 | start_prank(CheatTarget::One(escrow.contract_address), OWNER()); 114 | escrow.set_eth_transfer_contract(MM_ETHEREUM()); 115 | } 116 | 117 | #[test] 118 | #[should_panic(expected: ('Caller is not the owner',))] 119 | fn test_fail_set_mm_ethereum_contract() { 120 | let (escrow, _) = setup(); 121 | escrow.set_mm_ethereum_contract(MM_ETHEREUM()); 122 | } 123 | 124 | #[test] 125 | fn test_set_mm_ethereum_contract() { 126 | let (escrow, _) = setup(); 127 | start_prank(CheatTarget::One(escrow.contract_address), OWNER()); 128 | escrow.set_mm_ethereum_contract(MM_ETHEREUM()); 129 | } 130 | 131 | #[test] 132 | #[should_panic(expected: ('Caller is not the owner',))] 133 | fn test_fail_set_mm_starknet_contract() { 134 | let (escrow, _) = setup(); 135 | escrow.set_mm_starknet_contract(USER()); 136 | } 137 | 138 | #[test] 139 | fn test_set_mm_starknet_contract() { 140 | let (escrow, _) = setup(); 141 | start_prank(CheatTarget::One(escrow.contract_address), OWNER()); 142 | escrow.set_mm_starknet_contract(USER()); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /contracts/starknet/src/tests/test_escrow_upgrade.cairo: -------------------------------------------------------------------------------- 1 | mod Escrow { 2 | use core::to_byte_array::FormatAsByteArray; 3 | use core::serde::Serde; 4 | use core::traits::Into; 5 | use starknet::{EthAddress, ContractAddress}; 6 | use integer::BoundedInt; 7 | 8 | use snforge_std::{declare, ContractClassTrait, L1Handler, L1HandlerTrait}; 9 | use snforge_std::{CheatTarget, start_prank, stop_prank, start_warp, stop_warp}; 10 | 11 | use yab::mocks::mock_Escrow_changed_functions::{IEscrow_mock_changed_functionsDispatcher, IEscrow_mock_changed_functionsDispatcherTrait}; 12 | use yab::mocks::mock_pausableEscrow::{IEscrow_mockPausableDispatcher, IEscrow_mockPausableDispatcherTrait}; 13 | use yab::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; 14 | use yab::escrow::{IEscrowDispatcher, IEscrowDispatcherTrait, Order}; 15 | use yab::interfaces::IEVMFactsRegistry::{ 16 | IEVMFactsRegistryDispatcher, IEVMFactsRegistryDispatcherTrait 17 | }; 18 | 19 | use yab::tests::utils::{ 20 | constants::EscrowConstants::{ 21 | USER, OWNER, MM_STARKNET, MM_ETHEREUM, ETH_TRANSFER_CONTRACT, ETH_USER 22 | }, 23 | }; 24 | 25 | use openzeppelin::{ 26 | upgrades::{ 27 | UpgradeableComponent, 28 | interface::{IUpgradeable, IUpgradeableDispatcher, IUpgradeableDispatcherTrait} 29 | }, 30 | }; 31 | 32 | fn setup() -> (IEscrowDispatcher, IERC20Dispatcher) { 33 | setup_general(BoundedInt::max(), BoundedInt::max()) 34 | } 35 | 36 | fn setup_approved(approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ 37 | setup_general(BoundedInt::max(), approved) 38 | } 39 | 40 | fn setup_balance(balance: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ 41 | setup_general(balance, BoundedInt::max()) 42 | } 43 | 44 | fn setup_general(balance: u256, approved: u256) -> (IEscrowDispatcher, IERC20Dispatcher){ 45 | let eth_token = deploy_erc20('ETH', '$ETH', BoundedInt::max(), OWNER()); 46 | let escrow = deploy_escrow( 47 | OWNER(), 48 | ETH_TRANSFER_CONTRACT(), 49 | MM_ETHEREUM(), 50 | MM_STARKNET(), 51 | eth_token.contract_address 52 | ); 53 | 54 | start_prank(CheatTarget::One(eth_token.contract_address), OWNER()); 55 | eth_token.transfer(USER(), balance); 56 | stop_prank(CheatTarget::One(eth_token.contract_address)); 57 | 58 | start_prank(CheatTarget::One(eth_token.contract_address), USER()); 59 | eth_token.approve(escrow.contract_address, approved); 60 | stop_prank(CheatTarget::One(eth_token.contract_address)); 61 | 62 | (escrow, eth_token) 63 | } 64 | 65 | fn deploy_escrow( 66 | escrow_owner: ContractAddress, 67 | eth_transfer_contract: EthAddress, 68 | mm_ethereum_contract: EthAddress, 69 | mm_starknet_contract: ContractAddress, 70 | native_token_eth_starknet: ContractAddress 71 | ) -> IEscrowDispatcher { 72 | let escrow = declare('Escrow'); 73 | let mut calldata: Array = ArrayTrait::new(); 74 | calldata.append(escrow_owner.into()); 75 | calldata.append(eth_transfer_contract.into()); 76 | calldata.append(mm_ethereum_contract.into()); 77 | calldata.append(mm_starknet_contract.into()); 78 | calldata.append(native_token_eth_starknet.into()); 79 | let address = escrow.deploy(@calldata).unwrap(); 80 | return IEscrowDispatcher { contract_address: address }; 81 | } 82 | 83 | fn deploy_erc20( 84 | name: felt252, symbol: felt252, initial_supply: u256, recipent: ContractAddress 85 | ) -> IERC20Dispatcher { 86 | let erc20 = declare('ERC20'); 87 | let mut calldata = array![name, symbol]; 88 | Serde::serialize(@initial_supply, ref calldata); 89 | calldata.append(recipent.into()); 90 | let address = erc20.deploy(@calldata).unwrap(); 91 | return IERC20Dispatcher { contract_address: address }; 92 | } 93 | 94 | #[test] 95 | fn test_upgrade_escrow() { 96 | let (escrow, _) = setup(); 97 | let upgradeable = IUpgradeableDispatcher { contract_address: escrow.contract_address }; 98 | let value = escrow.get_mm_starknet_contract(); 99 | start_prank(CheatTarget::One(escrow.contract_address), OWNER()); 100 | upgradeable.upgrade(declare('Escrow_mock_changed_functions').class_hash); 101 | let escrow_v2 = IEscrow_mock_changed_functionsDispatcher { contract_address: escrow.contract_address }; 102 | let value_v2 = escrow_v2.get_mm_starknet_contractv2(); //would fail if new function name didn't exist 103 | assert(value == value_v2, 'value should be the same'); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /contracts/starknet/src/tests/utils/constants.cairo: -------------------------------------------------------------------------------- 1 | mod EscrowConstants { 2 | use starknet::{EthAddress, ContractAddress, contract_address_const}; 3 | 4 | fn USER() -> ContractAddress { 5 | contract_address_const::<'USER'>() 6 | } 7 | 8 | fn OWNER() -> ContractAddress { 9 | contract_address_const::<'OWNER'>() 10 | } 11 | 12 | fn MM_STARKNET() -> ContractAddress { 13 | contract_address_const::<'HERODOTUS_FACTS_REGISTRY'>() 14 | } 15 | 16 | fn MM_ETHEREUM() -> EthAddress { 17 | 50.try_into().unwrap() 18 | } 19 | 20 | fn ETH_TRANSFER_CONTRACT() -> EthAddress { 21 | 69.try_into().unwrap() 22 | } 23 | 24 | fn ETH_USER() -> EthAddress { 25 | 99.try_into().unwrap() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/starknet/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | # Starkli implicitly utilizes these environment variables, so every time we use Starkli, 5 | # we avoid adding flags such as --account, --keystore, and --rpc. 6 | export STARKNET_ACCOUNT=$STARKNET_ACCOUNT 7 | export STARKNET_KEYSTORE=$STARKNET_KEYSTORE 8 | # export STARKNET_RPC=$STARKNET_RPC #todo: this must remain commented until we find a reliable and compatible rpc 9 | 10 | if [ -z "$ESCROW_CONTRACT_ADDRESS" ]; then 11 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 12 | echo "ESCROW_CONTRACT_ADDRESS Variable is empty. Aborting execution.\n" 13 | exit 1 14 | fi 15 | 16 | cd contracts/starknet 17 | 18 | printf "${GREEN}\n=> [SN] Declare Escrow${COLOR_RESET}\n" 19 | NEW_ESCROW_CLASS_HASH=$(starkli declare --watch target/dev/yab_Escrow.contract_class.json) 20 | 21 | if [ -z "$NEW_ESCROW_CLASS_HASH" ]; then 22 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 23 | echo "Failed to generate New Escrow Class Hash. Aborting execution.\n" 24 | exit 1 25 | fi 26 | 27 | printf "${CYAN}[SN] Escrow address: $ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" 28 | printf "${CYAN}[SN] New Escrow ClassHash: $NEW_ESCROW_CLASS_HASH${COLOR_RESET}\n" 29 | 30 | printf "${GREEN}\n=> [SN] Upgrade Escrow${COLOR_RESET}\n" 31 | starkli invoke --watch $ESCROW_CONTRACT_ADDRESS upgrade $NEW_ESCROW_CLASS_HASH 32 | 33 | cd ../.. 34 | -------------------------------------------------------------------------------- /contracts/starknet_wallet_setup.md: -------------------------------------------------------------------------------- 1 | # Setting up a Starknet Testnet Wallet 2 | 3 | **This guide will help you declare and deploy contracts on a testnet. Please 4 | note that you won't be able to use the commands in the Makefile unless you 5 | follow these instructions.** 6 | 7 | A smart wallet consists of two parts: a Signer and an Account Descriptor. The 8 | Signer is a smart contract capable of signing transactions (for which we need 9 | its private key). The Account Descriptor is a JSON file containing information 10 | about the smart wallet, such as its address and public key. 11 | 12 | Follow the steps below to set up a smart wallet using `starkli`: 13 | 14 | 1. **Connect to a Provider**: to interact with the network you need an RPC 15 | Provider. For our project we will be using Alchemy's free tier in Goerli 16 | Testnet. 17 | 18 | 1. Go to [Alchemy website](https://www.alchemy.com/) and create an account. 19 | 2. It will ask which network you want to develop on and choose Starknet. 20 | 3. Select the Free version of the service (we will only need access to send 21 | some transactions to deploy the contracts) 22 | 4. Once the account creation process is done, go to _My apps_ and create a 23 | new Application. Choose Starknet as a Chain and the network you want to use in Starknet. 24 | 5. Click on _View key_ on the new Starknet Application and copy the HTTPS 25 | url. 26 | 6. On your terminal run: 27 | 28 | ```bash 29 | export STARKNET_RPC="" 30 | ``` 31 | 32 | 2. **Create a Keystore**: A Keystore is an encrypted `json` file that stores the 33 | private keys. 34 | 35 | 1. **Create a hidden folder**: Use the following command: 36 | 37 | ```bash 38 | mkdir -p ~/.starkli-wallets 39 | ``` 40 | 41 | 2. **Generate a new Keystore file**: Run the following command to create a 42 | new private key stored in the file. It will **ask for a password** to 43 | encrypt the file: 44 | 45 | ```bash 46 | starkli signer keystore new ~/.starkli-wallets/keystore.json 47 | ``` 48 | 49 | The command will return the Public Key of your account, copy it to your 50 | clipboard to fund the account. 51 | 3. **Set STARKNET_KEYSTORE**: To set the environment variable just run: 52 | 53 | ```bash 54 | export STARKNET_KEYSTORE="~/.starkli-wallets/keystore.json" 55 | ``` 56 | 57 | 3. **Account Creation**: In Starknet every account is a smart contract, so to 58 | create one it will need to be deployed. 59 | 60 | 1. **Initiate the account with the Open Zeppelin Account contract**: 61 | 62 | ```bash 63 | starkli account init --keystore ~/.starkli-wallets/keystore.json ~/.starkli-wallets/account.json 64 | ``` 65 | The [current version of Starkli](https://book.starkli.rs/accounts) supports these account variants (by alphabetical order): 66 | 67 | | Vendor | Identifier | Link | 68 | |--------------|------------|-------------------------------------------------| 69 | | Argent | argent | https://www.argent.xyz/argent-x/ | 70 | | Braavos | braavos | https://braavos.app/ | 71 | | OpenZeppelin | oz | https://github.com/OpenZeppelin/cairo-contracts | 72 | 73 | 2. **Deploy the account by running**: 74 | 75 | ```bash 76 | starkli account deploy --keystore ~/.starkli-wallets/keystore.json ~/.starkli-wallets/account.json 77 | ``` 78 | 79 | For the deployment `starkli` will ask you to fund an account. To do so 80 | you will need to fund the address given by `starkli`. 81 | - In Goerli you can use [Goerli Starknet Faucet](https://faucet.goerli.starknet.io) 82 | 83 | 4. **Setting Up Environment Variables**: There are two primary environment 84 | variables vital for effective usage of Starkli’s CLI. These are the location 85 | of the keystore file for the Signer, and the location of the Account 86 | Descriptor file: 87 | 88 | ```bash 89 | export STARKNET_ACCOUNT=~/.starkli-wallets/account.json 90 | export STARKNET_KEYSTORE=~/.starkli-wallets/keystore.json 91 | ``` 92 | -------------------------------------------------------------------------------- /contracts/utils/colors.sh: -------------------------------------------------------------------------------- 1 | GREEN='\e[32m' 2 | CYAN='\033[36m' 3 | PINK='\033[1;35m' 4 | ORANGE='\033[1;33m' 5 | RED='\033[0;31m' 6 | COLOR_RESET='\033[0m' 7 | -------------------------------------------------------------------------------- /contracts/utils/display_info.sh: -------------------------------------------------------------------------------- 1 | printf "${GREEN}\n=> Newly deployed contracts information: ${COLOR_RESET}\n" 2 | printf "${PINK}[ETH] Deployed Proxy of PaymentRegistry address: $PAYMENT_REGISTRY_PROXY_ADDRESS ${COLOR_RESET}\n" 3 | 4 | if ! [ -z "$ESCROW_CONTRACT_ADDRESS" ]; then 5 | printf "${CYAN}[SN] Escrow Address: $ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" 6 | fi 7 | if ! [ -z "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" ]; then 8 | printf "${CYAN}[ZKSync] Escrow Address: $ZKSYNC_ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" 9 | fi 10 | -------------------------------------------------------------------------------- /contracts/zksync/.env.example: -------------------------------------------------------------------------------- 1 | WALLET_PRIVATE_KEY= 2 | MM_ZKSYNC_WALLET= 3 | -------------------------------------------------------------------------------- /contracts/zksync/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .vscode 9 | 10 | # hardhat artifacts 11 | artifacts 12 | cache 13 | 14 | # zksync artifacts 15 | artifacts-zk 16 | cache-zk 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | node_modules/ 51 | jspm_packages/ 52 | 53 | # TypeScript v1 declaration files 54 | typings/ 55 | 56 | # TypeScript cache 57 | *.tsbuildinfo 58 | 59 | # Optional npm cache directory 60 | .npm 61 | 62 | # Optional eslint cache 63 | .eslintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variables file 81 | .env 82 | .env.test 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | 87 | # Next.js build output 88 | .next 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox/ 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | # TernJS port file 113 | .tern-port 114 | -------------------------------------------------------------------------------- /contracts/zksync/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matter Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/zksync/README.md: -------------------------------------------------------------------------------- 1 | # zkSync Escrow 2 | 3 | This project was initialized with [zksync-cli](https://github.com/matter-labs/zksync-cli). 4 | 5 | ## Project Layout 6 | 7 | - `/contracts`: Contains source files, solidity smart contracts. 8 | - `/deploy`: Scripts for contract deployment and interaction. 9 | - `/test`: Test files. 10 | - `/artifacts-zk`: Autogenerated with required libraries. 11 | - `hardhat.config.ts`: Configuration settings. 12 | 13 | ## How to Use 14 | 15 | - `make deps`: Installs dependencies 16 | - `make zksync-build`: Compiles contracts. 17 | - `make zksync-deploy`: Deploys using script `/deploy/deploy.ts`. 18 | - `make zksync-connect`: Connects itself to saved PaymentRegistry address. 19 | - `make zksync-deploy-and-connect`: Deploys and connects itself to saved PaymentRegistry address. 20 | - `make ethereum-and-zksync-deploy`: Deploys both smart contracts and connects them to each other. 21 | 22 | ### Local Tests 23 | 24 | To run local tests you should first start a local dockerized node. 25 | Then: 26 | - `make zksync-test`: Tests the contracts. 27 | 28 | ### Network Support 29 | 30 | `hardhat.config.ts` comes with a list of networks to deploy and test contracts. Add more by adjusting the `networks` section in the `hardhat.config.ts`. To make a network the default, set the `defaultNetwork` to its name. You can also override the default using the `--network` option, like: `hardhat test --network dockerizedNode`. 31 | 32 | ### Local Tests 33 | 34 | Running `npm run test` by default runs the [zkSync In-memory Node](https://era.zksync.io/docs/tools/testing/era-test-node.html) provided by the [@matterlabs/hardhat-zksync-node](https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-node.html) tool. 35 | 36 | Important: zkSync In-memory Node currently supports only the L2 node. If contracts also need L1, use another testing environment like Dockerized Node. Refer to [test documentation](https://era.zksync.io/docs/tools/testing/) for details. 37 | 38 | 39 | 40 | ## License 41 | 42 | This project is under the [MIT](./LICENSE) license. -------------------------------------------------------------------------------- /contracts/zksync/contracts/escrow.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | 5 | import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | 8 | import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 9 | import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; 10 | // import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; 11 | // import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 12 | // TODO make upgradeable 13 | 14 | contract Escrow is Initializable, OwnableUpgradeable, PausableUpgradeable { //}, UUPSUpgradeable { 15 | 16 | struct Order { 17 | address recipient_address; 18 | uint256 amount; 19 | uint256 fee; 20 | } 21 | 22 | event SetOrder(uint256 order_id, address recipient_address, uint256 amount, uint256 fee); 23 | 24 | event ClaimPayment(uint256 order_id, address claimerAddress, uint256 amount); 25 | 26 | 27 | //storage 28 | uint256 private _current_order_id; 29 | mapping(uint256 => Order) private _orders; 30 | mapping(uint256 => bool) private _orders_pending; 31 | mapping(uint256 => address) private _orders_senders; 32 | mapping(uint256 => uint256) private _orders_timestamps; 33 | address public ethereum_payment_registry; 34 | address public mm_zksync_wallet; 35 | 36 | function initialize( 37 | address ethereum_payment_registry_, 38 | address mm_zksync_wallet_ 39 | ) public initializer { 40 | __Ownable_init(); 41 | // __UUPSUpgradeable_init(); 42 | 43 | _current_order_id = 0; 44 | ethereum_payment_registry = ethereum_payment_registry_; 45 | mm_zksync_wallet = mm_zksync_wallet_; 46 | } 47 | 48 | 49 | // FUNCTIONS : 50 | 51 | function get_order(uint256 order_id) public view returns (Order memory) { 52 | return _orders[order_id]; 53 | } 54 | 55 | //Function recieves in msg.value the total value, and in fee the user specifies what portion of that msg.value is fee for MM 56 | function set_order(address recipient_address, uint256 fee) public payable whenNotPaused returns (uint256) { 57 | require(msg.value > 0, 'some ETH must be sent'); 58 | require(msg.value > fee, 'ETH sent must be more than fee'); 59 | 60 | uint256 bridge_amount = msg.value - fee; //no underflow since previous check is made 61 | 62 | Order memory new_order = Order({recipient_address: recipient_address, amount: bridge_amount, fee: fee}); 63 | _orders[_current_order_id] = new_order; 64 | _orders_pending[_current_order_id] = true; 65 | _orders_senders[_current_order_id] = msg.sender; 66 | _orders_timestamps[_current_order_id] = block.timestamp; 67 | _current_order_id++; //this here to follow CEI pattern 68 | 69 | emit SetOrder(_current_order_id-1, recipient_address, bridge_amount, fee); 70 | 71 | return _current_order_id-1; 72 | } 73 | 74 | // l1 handler 75 | function claim_payment( 76 | uint256 order_id, 77 | address recipient_address, 78 | uint256 amount 79 | ) public whenNotPaused { 80 | require(msg.sender == ethereum_payment_registry, 'Only PAYMENT_REGISTRY can call'); 81 | require(_orders_pending[order_id], 'Order claimed or nonexistent'); 82 | 83 | Order memory current_order = _orders[order_id]; //TODO check if order is memory or calldata 84 | require(current_order.recipient_address == recipient_address, 'recipient_address not match L1'); 85 | require(current_order.amount == amount, 'amount not match L1'); 86 | 87 | _orders_pending[order_id] = false; 88 | uint256 payment_amount = current_order.amount + current_order.fee; // TODO check overflow 89 | 90 | (bool success,) = payable(address(uint160(mm_zksync_wallet))).call{value: payment_amount}(""); 91 | require(success, "Transfer failed."); 92 | 93 | emit ClaimPayment(order_id, mm_zksync_wallet, amount); 94 | } 95 | 96 | function is_order_pending(uint256 order_id) public view returns (bool) { 97 | return _orders_pending[order_id]; 98 | } 99 | 100 | function set_ethereum_payment_registry(address new_payment_registry_address) public whenNotPaused onlyOwner { 101 | ethereum_payment_registry = new_payment_registry_address; 102 | } 103 | 104 | function set_mm_zksync_wallet(address new_mm_zksync_wallet) public whenNotPaused onlyOwner { 105 | mm_zksync_wallet = new_mm_zksync_wallet; 106 | } 107 | 108 | function pause() public onlyOwner { 109 | _pause(); 110 | } 111 | 112 | function unpause() public onlyOwner { 113 | _unpause(); 114 | } 115 | 116 | //todo for upgradeable in zksync: 117 | // function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} 118 | 119 | } 120 | -------------------------------------------------------------------------------- /contracts/zksync/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . contracts/utils/colors.sh #for ANSI colors 3 | 4 | cd ./contracts/zksync/ 5 | 6 | if [ -z "$PAYMENT_REGISTRY_PROXY_ADDRESS" ]; then 7 | echo "\n${RED}ERROR:${COLOR_RESET}" 8 | echo "PAYMENT_REGISTRY_PROXY_ADDRESS Variable is empty. Aborting execution.\n" 9 | exit 1 10 | fi 11 | 12 | DEPLOY="deploy" 13 | if [ "$TEST" == true ]; then 14 | DEPLOY="deploy-devnet" 15 | fi 16 | 17 | export WALLET_PRIVATE_KEY=$WALLET_PRIVATE_KEY 18 | export PAYMENT_REGISTRY_PROXY_ADDRESS=$PAYMENT_REGISTRY_PROXY_ADDRESS 19 | export MM_ZKSYNC_WALLET=$MM_ZKSYNC_WALLET 20 | 21 | ZKSYNC_ESCROW_CONTRACT_ADDRESS=$(yarn $DEPLOY | grep "Contract address:" | egrep -i -o '0x[a-zA-Z0-9]{40}') 22 | 23 | if [ -z "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" ]; then 24 | printf "\n${RED}ERROR:${COLOR_RESET}\n" 25 | echo "ZKSYNC_ESCROW_CONTRACT_ADDRESS Variable is empty. Aborting execution.\n" 26 | exit 1 27 | fi 28 | 29 | printf "${CYAN}[ZKSync] Escrow Address: $ZKSYNC_ESCROW_CONTRACT_ADDRESS${COLOR_RESET}\n" 30 | printf "\nIf you now wish to finish the configuration of this deploy, you will need to run the following commands:\n" 31 | echo "export PAYMENT_REGISTRY_PROXY_ADDRESS=$PAYMENT_REGISTRY_PROXY_ADDRESS" 32 | echo "export ZKSYNC_ESCROW_CONTRACT_ADDRESS=$ZKSYNC_ESCROW_CONTRACT_ADDRESS" 33 | echo "make zksync-connect" 34 | 35 | cd ../.. 36 | 37 | -------------------------------------------------------------------------------- /contracts/zksync/deploy/deploy.ts: -------------------------------------------------------------------------------- 1 | import { deployContractWithProxy } from "./utils"; 2 | import { deployContract } from "./utils"; 3 | // import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 4 | import * as dotenv from 'dotenv'; 5 | import { utils } from "zksync-ethers"; 6 | 7 | 8 | // It will deploy a Escrow contract to selected network 9 | export default async function () { 10 | const escrowArtifactName = "Escrow"; 11 | const escrowConstructorArguments = []; 12 | const escrow = await deployContract(escrowArtifactName, escrowConstructorArguments); 13 | // const escrow = await deployContractWithProxy(escrowArtifactName, escrowConstructorArguments); 14 | 15 | dotenv.config(); 16 | 17 | const ethereum_payment_registry = process.env.PAYMENT_REGISTRY_PROXY_ADDRESS; 18 | const mm_zksync_wallet = process.env.MM_ZKSYNC_WALLET; 19 | 20 | if (!ethereum_payment_registry ){ 21 | throw new Error("Missing required environment variable: PAYMENT_REGISTRY_PROXY_ADDRESS"); 22 | } 23 | if (!mm_zksync_wallet) { 24 | throw new Error("Missing required environment variable: MM_ZKSYNC_WALLET"); 25 | } 26 | 27 | 28 | const PaymentRegistryL2Alias = utils.applyL1ToL2Alias(ethereum_payment_registry) 29 | 30 | const initResult = await escrow.initialize(PaymentRegistryL2Alias, mm_zksync_wallet); 31 | 32 | // console.log("Initialization result:", initResult); 33 | } 34 | -------------------------------------------------------------------------------- /contracts/zksync/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | 3 | import "@matterlabs/hardhat-zksync-node"; 4 | import "@matterlabs/hardhat-zksync-deploy"; 5 | import "@matterlabs/hardhat-zksync-solc"; 6 | import "@matterlabs/hardhat-zksync-verify"; 7 | import "@matterlabs/hardhat-zksync-upgradable"; 8 | import "@nomicfoundation/hardhat-chai-matchers"; 9 | 10 | const config: HardhatUserConfig = { 11 | defaultNetwork: "dockerizedNode", 12 | // defaultNetwork: "zkSyncSepoliaTestnet", 13 | networks: { 14 | zkSyncSepoliaTestnet: { 15 | url: "https://sepolia.era.zksync.dev", 16 | ethNetwork: "sepolia", 17 | zksync: true, 18 | verifyURL: "https://explorer.sepolia.era.zksync.dev/contract_verification", 19 | }, 20 | zkSyncMainnet: { 21 | url: "https://mainnet.era.zksync.io", 22 | ethNetwork: "mainnet", 23 | zksync: true, 24 | verifyURL: "https://zksync2-mainnet-explorer.zksync.io/contract_verification", 25 | }, 26 | zkSyncGoerliTestnet: { // deprecated network 27 | url: "https://testnet.era.zksync.dev", 28 | ethNetwork: "goerli", 29 | zksync: true, 30 | verifyURL: "https://zksync2-testnet-explorer.zksync.dev/contract_verification", 31 | }, 32 | dockerizedNode: { 33 | url: "http://localhost:3050", 34 | ethNetwork: "http://localhost:8545", 35 | zksync: true, 36 | }, 37 | inMemoryNode: { 38 | url: "http://127.0.0.1:8011", 39 | ethNetwork: "", // in-memory node doesn't support eth node; removing this line will cause an error 40 | zksync: true, 41 | }, 42 | hardhat: { 43 | zksync: true, 44 | }, 45 | }, 46 | zksolc: { 47 | version: "latest", 48 | settings: { 49 | // find all available options in the official documentation 50 | // https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-solc.html#configuration 51 | }, 52 | }, 53 | solidity: { 54 | version: "0.8.17", 55 | }, 56 | }; 57 | 58 | export default config; 59 | -------------------------------------------------------------------------------- /contracts/zksync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zksync-hardhat-escrow", 3 | "private": true, 4 | "packageManager": "yarn@1.22.21", 5 | "scripts": { 6 | "deploy": "hardhat deploy-zksync --network zkSyncSepoliaTestnet --script deploy.ts", 7 | "deploy-devnet": "hardhat deploy-zksync --network dockerizedNode --script deploy.ts", 8 | "compile": "hardhat compile", 9 | "clean": "hardhat clean", 10 | "test": "hardhat test --network dockerizedNode --parallel --show-stack-traces" 11 | }, 12 | "devDependencies": { 13 | "@matterlabs/hardhat-zksync-deploy": "^1.1.2", 14 | "@matterlabs/hardhat-zksync-node": "^1.0.1", 15 | "@matterlabs/hardhat-zksync-solc": "^1.0.6", 16 | "@matterlabs/hardhat-zksync-upgradable": "^1.2.4", 17 | "@matterlabs/hardhat-zksync-verify": "^1.2.2", 18 | "@matterlabs/zksync-contracts": "^0.6.1", 19 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", 20 | "@nomicfoundation/hardhat-ethers": "^3.0.5", 21 | "@nomiclabs/hardhat-etherscan": "^3.1.7", 22 | "@openzeppelin/contracts": "4.9.5", 23 | "@openzeppelin/contracts-upgradeable": "4.9.5", 24 | "@openzeppelin/upgrades-core": "^1.32.4", 25 | "@types/chai": "^4.3.4", 26 | "@types/mocha": "^10.0.1", 27 | "chai": "^4.3.7", 28 | "dotenv": "^16.0.3", 29 | "ethers": "^6.9.2", 30 | "hardhat": "^2.12.4", 31 | "mocha": "^10.2.0", 32 | "ts-node": "^10.9.1", 33 | "typescript": "^4.9.5", 34 | "zksync-ethers": "^6.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/zksync/test/claim_payment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . contracts/utils/colors.sh #for ANSI colors 4 | 5 | printf "${GREEN}\n=> [ETH] Making Claim Payment${COLOR_RESET}\n" 6 | 7 | MM_INITIAL_BALANCE_L1=$(cast balance --rpc-url $ETHEREUM_RPC --ether $MM_ETHEREUM_PUBLIC_ADDRESS) 8 | echo "Initial MM balance L1:" 9 | echo "$MM_INITIAL_BALANCE_L1" 10 | 11 | echo "Initial MM balance L2:" 12 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$MM_ZKSYNC_WALLET" | grep -E -o "\d+(\.\d+)? ETH" 13 | 14 | echo "Initial Escrow balance:" 15 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" | grep -E -o "\d+(\.\d+)? ETH" 16 | 17 | 18 | echo "Withdrawing $BRIDGE_AMOUNT_ETH ETH" 19 | echo "Withdrawing $BRIDGE_AMOUNT_WEI WEI" 20 | 21 | cast send --rpc-url $ETHEREUM_RPC --private-key $ETHEREUM_PRIVATE_KEY \ 22 | $PAYMENT_REGISTRY_PROXY_ADDRESS "claimPaymentZKSync(uint256, uint256, uint256, uint256, uint256)" \ 23 | "0" "$USER_ETHEREUM_PUBLIC_ADDRESS_UINT" "$BRIDGE_AMOUNT_WEI" "2000000000" "800"\ 24 | --value 5000000000000000000 25 | 26 | #ele pe eme 27 | #me revertea con info: None 28 | #no estoy seguro si existe un diamond proxy en la dada address. 29 | 30 | 31 | 32 | sleep 15 33 | 34 | 35 | MM_INITIAL_BALANCE_L1=$(cast balance --rpc-url $ETHEREUM_RPC --ether $MM_ETHEREUM_PUBLIC_ADDRESS) 36 | echo "After MM balance L1:" 37 | echo "$MM_INITIAL_BALANCE_L1" 38 | 39 | echo "After MM balance L2:" 40 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$MM_ZKSYNC_WALLET" | grep -E -o "\d+(\.\d+)? ETH" 41 | 42 | echo "After Escrow balance:" 43 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" | grep -E -o "\d+(\.\d+)? ETH" 44 | 45 | 46 | 47 | # starkli call $ESCROW_CONTRACT_ADDRESS get_order_pending u256:0 48 | 49 | # ESCROW_FINAL_BALANCE=$(starkli balance $ESCROW_CONTRACT_ADDRESS) 50 | # MM_FINAL_BALANCE=$(starkli balance $MM_SN_WALLET_ADDR) 51 | # echo "Final Escrow balance: $ESCROW_FINAL_BALANCE" 52 | # echo "Final MM balance: $MM_FINAL_BALANCE" 53 | -------------------------------------------------------------------------------- /contracts/zksync/test/main.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { deployAndInit } from './utils'; 3 | import { Contract, Fragment, Wallet } from 'ethers'; 4 | import { getWallet, deployContract, LOCAL_RICH_WALLETS } from '../deploy/utils'; 5 | 6 | let escrow: Contract; 7 | let deployer: Wallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey); 8 | let user_zk: Wallet = getWallet(LOCAL_RICH_WALLETS[1].privateKey); 9 | let user_zk2: Wallet = getWallet(LOCAL_RICH_WALLETS[2].privateKey); 10 | let user_eth: Wallet = getWallet(LOCAL_RICH_WALLETS[1].privateKey); 11 | let user_eth2: Wallet = getWallet(LOCAL_RICH_WALLETS[2].privateKey); 12 | 13 | 14 | const fee = 1; //TODO check, maybe make fuzz 15 | const value = 10; //TODO check, maybe make fuzz 16 | 17 | beforeEach( async () => { 18 | escrow = await deployAndInit(); 19 | }); 20 | 21 | // /* 22 | // working::: 23 | describe('Pause tests', function () { 24 | 25 | it("Should start unpaused", async function () { 26 | expect(await escrow.paused()).to.eq(false); 27 | }); 28 | 29 | it("Should pause", async function () { 30 | const setPauseTx = await escrow.pause(); 31 | await setPauseTx.wait(); 32 | 33 | expect(await escrow.paused()).to.equal(true); 34 | }); 35 | 36 | it("Should unpause", async function () { 37 | const setPauseTx = await escrow.pause(); 38 | await setPauseTx.wait(); 39 | 40 | const setUnpauseTx = await escrow.unpause(); 41 | await setUnpauseTx.wait(); 42 | 43 | expect(await escrow.paused()).to.eq(false); 44 | }); 45 | 46 | it("Should not allow when paused: set_mm_zksync_wallet", async () => { 47 | const setPauseTx = await escrow.pause(); 48 | await setPauseTx.wait(); 49 | await expect(escrow.set_mm_zksync_wallet(user_zk)).to.be.revertedWith("Pausable: paused"); 50 | }); 51 | 52 | it("Should not allow when paused: set_ethereum_payment_registry", async () => { 53 | const setPauseTx = await escrow.pause(); 54 | await setPauseTx.wait(); 55 | await expect(escrow.set_ethereum_payment_registry(user_eth)).to.be.revertedWith("Pausable: paused"); 56 | }); 57 | 58 | it("Should not allow when paused: set_order", async () => { 59 | const setPauseTx = await escrow.pause(); 60 | await setPauseTx.wait(); 61 | await expect(escrow.set_order(user_eth, fee, {value})).to.be.revertedWith("Pausable: paused"); 62 | }); 63 | 64 | }); 65 | // */ 66 | // // working :: 67 | describe('Set Order tests', function () { 68 | it("Should emit correct Event", async () => { 69 | let events = await escrow.queryFilter("*"); 70 | const events_length = events.length; 71 | 72 | const setOrderTx = await escrow.connect(user_zk).set_order(user_eth, fee, {value}); 73 | await setOrderTx.wait(); 74 | 75 | events = await escrow.queryFilter("*"); 76 | expect(events.length).to.equal(events_length + 1); 77 | expect(events[events.length - 1].fragment.name).to.equal("SetOrder"); 78 | }); 79 | 80 | it("Should get the order setted", async () => { 81 | const setOrderTx = await escrow.connect(user_zk).set_order(user_eth, fee, {value}); 82 | await setOrderTx.wait(); 83 | 84 | let events = await escrow.queryFilter("*"); 85 | const newOrderEvent = events[events.length - 1]; 86 | 87 | const orderId = newOrderEvent.args[0]; 88 | 89 | const newOrder = await escrow.get_order(orderId); 90 | 91 | expect(newOrder[0]).to.eq(user_eth.address); //recipient_address 92 | expect(Number(newOrder[1])).to.eq(value-fee); //amount 93 | expect(Number(newOrder[2])).to.eq(fee); //fee 94 | }) 95 | 96 | it("Should get the pending order", async () => { 97 | const setOrderTx = await escrow.connect(user_zk).set_order(user_eth, fee, {value}); 98 | await setOrderTx.wait(); 99 | 100 | expect(await escrow.is_order_pending(0)).to.equal(true); 101 | }) 102 | it("Should not get the pending order", async () => { 103 | expect(await escrow.is_order_pending(0)).to.equal(false); 104 | }) 105 | }) 106 | 107 | describe('Ownable tests', function () { 108 | it("Should not allow random user to pause", async () => { 109 | await expect(escrow.connect(user_zk).pause()).to.be.revertedWith("Ownable: caller is not the owner"); 110 | }); 111 | 112 | it("Should not allow random user to unpause", async () => { 113 | const setPauseTx = await escrow.pause(); 114 | await setPauseTx.wait(); 115 | await expect(escrow.connect(user_zk).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); 116 | }); 117 | 118 | it("Should not allow random user to set_mm_zksync_wallet", async () => { 119 | await expect(escrow.connect(user_zk).set_mm_zksync_wallet(user_zk)).to.be.revertedWith("Ownable: caller is not the owner"); 120 | }); 121 | 122 | it("Should allow owner to set_mm_zksync_wallet", async () => { 123 | const setTx = await escrow.set_mm_zksync_wallet(user_zk2); 124 | await setTx.wait(); 125 | 126 | expect(await escrow.mm_zksync_wallet()).to.equals(user_zk2.address); 127 | }); 128 | 129 | it("Should not allow random user to set_ethereum_payment_registry", async () => { 130 | await expect(escrow.connect(user_zk).set_ethereum_payment_registry(user_eth)).to.be.revertedWith("Ownable: caller is not the owner"); 131 | }); 132 | 133 | it("Should allow owner to set_ethereum_payment_registry", async () => { 134 | const setTx = await escrow.set_ethereum_payment_registry(user_eth2); 135 | await setTx.wait(); 136 | 137 | expect(await escrow.ethereum_payment_registry()).to.equals(user_eth2.address); 138 | }); 139 | 140 | }) 141 | -------------------------------------------------------------------------------- /contracts/zksync/test/set_order.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # cast call 0x97589bcE7727f5D0C8082440681DB6092b6Dda1a "getNames()(string)" --rpc-url http://localhost:8545 3 | # exit 4 | 5 | . contracts/utils/colors.sh #for ANSI colors 6 | 7 | FEE=10000000000000000 #in WEI 8 | VALUE=2 #in ETH 9 | VALUE_WEI=$(echo "scale=0; $VALUE * 10^18" | bc) 10 | BRIDGE_AMOUNT_WEI=$(echo "scale=0; $VALUE_WEI - $FEE" | bc) 11 | BRIDGE_AMOUNT_ETH=$(echo "scale=18; $BRIDGE_AMOUNT_WEI / 10^18" | bc) 12 | # BRIDGE_AMOUNT_WEI=$(printf "%.0f" "$BRIDGE_AMOUNT_WEI") 13 | 14 | 15 | printf "${GREEN}\n=> [SN] Making Set Order on Escrow${COLOR_RESET}\n" 16 | echo "\nUser ZKSync funds before setOrder:" 17 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$USER_ZKSYNC_PUBLIC_ADDRESS" | grep -E -o "\d+(\.\d+)? ETH" 18 | echo "\nEscrow ZKSync funds before setOrder:" 19 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" | grep -E -o "\d+(\.\d+)? ETH" 20 | 21 | 22 | npx zksync-cli contract write --private-key $USER_ZKSYNC_PRIVATE_ADDRESS --chain "dockerized-node" --contract "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" --method "set_order(address recipient_address, uint256 fee)" --args "$USER_ETHEREUM_PUBLIC_ADDRESS" "$FEE" --value "$VALUE" >> /dev/null 23 | 24 | 25 | echo "\nUser ZKSync funds after setOrder:" 26 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$USER_ZKSYNC_PUBLIC_ADDRESS" | grep -E -o "\d+(\.\d+)? ETH" 27 | echo "\nEscrow ZKSync funds after setOrder:" 28 | npx zksync-cli wallet balance --chain "dockerized-node" --address "$ZKSYNC_ESCROW_CONTRACT_ADDRESS" | grep -E -o "\d+(\.\d+)? ETH" 29 | -------------------------------------------------------------------------------- /contracts/zksync/test/transfer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . contracts/utils/colors.sh #for ANSI colors 4 | 5 | # export DESTINATION_ADDRESS=0xceee57f2b700c2f37d1476a7974965e149fce2d4 6 | # export DESTINATION_ADDRESS_UINT=1181367337507422765615536123397692015769584198356 7 | 8 | printf "${GREEN}\n=> [ETH] Making transfer to Destination account${COLOR_RESET}\n" 9 | 10 | MM_INITIAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $MM_ETHEREUM_PUBLIC_ADDRESS) 11 | DESTINATION_INITIAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $USER_ETHEREUM_PUBLIC_ADDRESS) 12 | echo "Initial MM balance: $MM_INITIAL_BALANCE" 13 | echo "Initial Destination balance: $DESTINATION_INITIAL_BALANCE" 14 | 15 | 16 | echo "Transferring $BRIDGE_AMOUNT_WEI WEI to $USER_ETHEREUM_PUBLIC_ADDRESS" 17 | 18 | cast send --rpc-url $ETHEREUM_RPC --private-key $MM_ETHEREUM_PRIVATE_KEY \ 19 | $PAYMENT_REGISTRY_PROXY_ADDRESS "transfer(uint256, uint256, uint8)" \ 20 | "0" "$USER_ETHEREUM_PUBLIC_ADDRESS_UINT" "1"\ 21 | --value $BRIDGE_AMOUNT_WEI >> /dev/null 22 | 23 | 24 | 25 | MM_FINAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $MM_ETHEREUM_PUBLIC_ADDRESS) 26 | DESTINATION_FINAL_BALANCE=$(cast balance --rpc-url $ETHEREUM_RPC --ether $USER_ETHEREUM_PUBLIC_ADDRESS) 27 | echo "Final MM balance: $MM_FINAL_BALANCE" 28 | echo "Final Destination balance: $DESTINATION_FINAL_BALANCE" 29 | -------------------------------------------------------------------------------- /contracts/zksync/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getWallet, deployContract, LOCAL_RICH_WALLETS } from '../deploy/utils'; 3 | import { Contract, Wallet } from 'ethers'; 4 | import * as hre from "hardhat"; 5 | import * as ethers from 'ethers'; 6 | 7 | 8 | export async function deployAndInit(): Promise { 9 | 10 | const deployer = getWallet(LOCAL_RICH_WALLETS[0].privateKey); 11 | 12 | const escrow = await deployContract("Escrow", [], { wallet: deployer }); 13 | 14 | //TODO deploy paymentregistry on L1 local 15 | const ethereum_payment_registry = "0x4337768cB3eC57Dd2cb843eFb929B773B13322de"; //process.env.PAYMENT_REGISTRY_PROXY_ADDRESS; 16 | const mm_ethereum_wallet = process.env.MM_ZKSYNC_WALLET; 17 | const mm_zksync_wallet = process.env.MM_ZKSYNC_WALLET; 18 | const native_token_eth_in_zksync = process.env.NATIVE_TOKEN_ETH_IN_ZKSYNC; 19 | if (!ethereum_payment_registry || !mm_ethereum_wallet || !mm_zksync_wallet || !native_token_eth_in_zksync) { 20 | console.log(ethereum_payment_registry,mm_ethereum_wallet,mm_zksync_wallet,native_token_eth_in_zksync); 21 | throw new Error("Missing required environment variables."); 22 | } 23 | 24 | escrow.connect(deployer); 25 | const initResult = await escrow.initialize(ethereum_payment_registry, mm_zksync_wallet); 26 | await initResult.wait(); 27 | return escrow 28 | } 29 | 30 | export async function deployPaymentRegistry(): Promise { 31 | const deployer = getWallet(LOCAL_RICH_WALLETS[0].privateKey); 32 | 33 | const paymentRegistry = await deployContract("PaymentRegistry", [], { wallet: deployer }); 34 | 35 | return await paymentRegistry.wait() 36 | } 37 | -------------------------------------------------------------------------------- /contracts/zksync/tests.md: -------------------------------------------------------------------------------- 1 | to run tests: 2 | 3 | first run a dockerized L1-L2 blockchains: 4 | ' 5 | git clone https://github.com/matter-labs/local-setup.git 6 | 7 | cd local-setup 8 | 9 | run docker 10 | 11 | ./start 12 | ' 13 | 14 | now from another terminal make zksync-tests 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Yet Another Bridge 🍭 2 | 3 | > [!CAUTION] 4 | > Use with caution and at your own risk as this repository is still experimental and NOT production ready. 5 | 6 |
7 | Yet Another Bridge 8 |

Yet Another Bridge is the cheapest, fastest and most secure bridge solution from Starknet to Ethereum

9 |
10 | 11 | Bridges are generally insecure and economically inefficient. They exhibit an 12 | asymmetry between users and bridge operators, where users can easily lose funds. 13 | We propose a bridge design that is simple, modular, and utilizes multi-storage 14 | proofs and the native messaging system between Ethereum and Layer 2 networks (L2s) 15 | as a fallback mechanism. 16 | 17 | In the following sections, you will find information about: 18 | 19 | - [Features](about_yab/features.md) 20 | - [How does it work](about_yab/how_does_it_work.md) 21 | - [FAQ](about_yab/FAQ.md) 22 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Yet Another Bridge](README.md) 4 | 5 | ## About YAB 6 | 7 | * [⭐️ Features](about_yab/features.md) 8 | 9 | * [💻 How does it work](about_yab/how_does_it_work.md) 10 | 11 | * [❓ FAQ](about_yab/FAQ.md) 12 | 13 | ## Contracts 14 | 15 | * [📜 Introduction](contracts/README.md) 16 | 17 | * [🏦 Escrow](contracts/escrow.md) 18 | 19 | * [🧾 Payment Registry](contracts/payment_registry.md) 20 | 21 | * [🔨 Deploy](contracts/deploy.md) 22 | 23 | * [📈 Upgrade Contract](contracts/upgrade.md) 24 | 25 | * [⛔️ Pause Contract](contracts/pause.md) 26 | 27 | ## Market Maker Bot 28 | 29 | * [🤖 Introduction](mm_bot/README.md) 30 | 31 | * [🏗️ Architecture](mm_bot/architecture.md) 32 | 33 | * [🚀 Deploy](mm_bot/deploy.md) 34 | 35 | ## User Guides 36 | 37 | 38 | ## Links 39 | 40 | * [Telegram Group](https://t.me/grindlabs) 41 | * [Twitter/X](https://twitter.com/yanotherbridge) 42 | -------------------------------------------------------------------------------- /docs/about_yab/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## What is YAB? 4 | 5 | Yet Another Bridge (YAB) is the cheapest, fastest and most secure bridge 6 | solution from Starknet to Ethereum. 7 | 8 | ## What makes YAB different from other bridge solutions? 9 | 10 | Bridges are generally insecure and economically inefficient. They exhibit an 11 | asymmetry between users and bridge operators, where users can easily lose funds. 12 | What we propose different from other solutions is a bridge design that is simple, 13 | modular, and utilizes multi-storage proofs and the native messaging system between 14 | Ethereum and Layer 2 networks (L2s) as a fallback mechanism. 15 | 16 | ## How much time does it take to bridge? 17 | 18 | From the user's perspective, the bridging is completed in less than 30 seconds, 19 | or as little time it takes for the market maker to observe the user's deposit 20 | and execute a transfer. 21 | 22 | From the market maker’s perspective, they will be able to receive back the money 23 | after paying the user and sending a message using the native messaging system. 24 | It takes less than 30 seconds, due to the time it takes to make a transaction 25 | in Ethereum and the L2’s sequencer receiving the message. 26 | 27 | Storage proofs are also implemented but they are not in use because they are useful to 28 | scale and bridge from multiple L2 chains. This normally takes between 5 and 15 29 | minutes. 30 | 31 | ## How much does it cost to bridge? 32 | 33 | The base cost of YAB is similar to an ERC20 transfer plus the cost of proving the state of the L1 and L2. Therefore, the 34 | bridge's final cost would be approximately $15. 35 | 36 | ## What chains are currently supported? 37 | 38 | Currently, Starknet is the only supported chain. 39 | 40 | We are working on integrating ZkSync in the near future, so stay tuned for further updates! 41 | 42 | ## How can I get in touch with the team? 43 | 44 | You can contact us in: 45 | 46 | - [Telegram Group](https://t.me/grindlabs) 47 | - Twitter/X 48 | - [Yet Another Bridge](https://twitter.com/yanotherbridge) 49 | - [Yet Another Company](https://twitter.com/yetanotherco) -------------------------------------------------------------------------------- /docs/about_yab/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | ## Cost Effective 4 | 5 | The cost of YAB is similar to an ERC20 transfer plus 6 | the cost of proving the state of the L1 and L2. 7 | 8 | Batch transfers and state proofs of the L1 and L2 are currently in the roadmap. 9 | 10 | ## Performance 11 | 12 | From the user's perspective, the bridging from Starknet to Ethereum is completed in less than 30 seconds, or as long as 13 | it takes the market maker to observe the user's deposit and execute a transfer. 14 | 15 | From the market maker's perspective, they will be able to withdraw 16 | the money after paying the user and generating the storage proof. 17 | 18 | ## Reduced Risks 19 | 20 | Since the capital is locked for a short period of time (i.e. until the proof is 21 | generated or the message arrives), the risks are minimized and the 22 | attack surface is smaller for the market maker. 23 | 24 | ## Decentralized Liquidity 25 | 26 | The final design handles multiple market makers, so if one market maker 27 | is compromised, the orders can be routed to another market maker instead. 28 | -------------------------------------------------------------------------------- /docs/about_yab/how_does_it_work.md: -------------------------------------------------------------------------------- 1 | # How does it work? 2 | 3 | How can we offer a system where the users don't have to trust a facilitator to exchange their assets from an L2 to 4 | Ethereum? 5 | 6 | We propose a simple protocol that follows these steps: 7 | 8 | 1. The user specifies a destination address on Ethereum and locks X tokens 9 | to be bridged into an L2 escrow smart contract. 10 | 11 | 2. The market maker is monitoring for a change of state in the escrow smart contract. 12 | 13 | 3. a. The market maker calls the transfer function of the PaymentRegistry Contract in Ethereum. 14 | 15 | b. The transfer function of the PaymentRegistry contract in Ethereum pays the X tokens to the user. 16 | 17 | 4. A storage proof is generated, containing evidence of a transfer from the 18 | market maker's Ethereum account to the user-specified address in Ethereum. 19 | 20 | 5. Ethereum PaymentRegistry storage information is then used as part of a storage proof. 21 | 22 | 6. L2 Escrow contract verifies the storage proof of the PaymentRegistry 23 | contract in Ethereum and pays the MM with the initial tokens locked by the user. 24 | 25 | ![YAB-diagram](../images/YAB-diagram.png) 26 | 27 | The same design can be expanded to be used to bridge tokens from an L2 to another L2 and it can include multi-proof 28 | storage proofs instead of using only one. 29 | 30 | We also have implemented a fallback mechanism using the native message mechanism 31 | between Ethereum and L2s in case the storage proof providers are offline. 32 | 33 | ## Fallback mechanism 34 | 35 | If the storage proof providers are not available, the market maker can prove 36 | to the Escrow contract that they fulfilled the user's intent through the rollup's 37 | native messaging system. Using this messaging system has the same trust 38 | assumptions as the L2s used in the transfer. 39 | 40 | ## Risks 41 | 42 | For the user, the risks include: 43 | 44 | 1. The existence of a bug in the code of the smart contract, 45 | 2. the existence of a bug in the circuits of the ZK/validity proof verification and the fact that the storage proof 46 | provider can go offline. 47 | 48 | The first risk is mitigated by having a very simple smart contract. 49 | 50 | While the second risk is mitigated by using multi-proof storage proofs and multiple ZK/validity 51 | proof implementations or TEEs. If the storage proof provider goes offline 52 | the fallback mechanism can be used. 53 | 54 | The risks for market makers are the same as for users, plus the risk of 55 | reorganization of the chain and the fact that the market maker receives the 56 | same tokens on the L2s rather than on Ethereum. 57 | 58 | Since the capital is locked for a short period of time (i.e. until the proof is generated or 59 | the message arrives), the risks are minimized and the attack surface is much smaller 60 | for the market maker. 61 | -------------------------------------------------------------------------------- /docs/contracts/README.md: -------------------------------------------------------------------------------- 1 | # Contracts 2 | 3 | YAB is conformed primarily by two Smart Contracts, one Smart Contract on L1 ETH blockchain 4 | (called [Payment Registry](../../contracts/solidity/src/PaymentRegistry.sol)), and one 5 | Smart Contract on L2 Starknet blockchain 6 | (called [Escrow](../../contracts/cairo/src/escrow.cairo)). Another vital entity for 7 | YAB's functionality is the Market Maker (MM for short). 8 | 9 | In the following sections, you will find information about: 10 | - [Escrow](escrow.md) 11 | - [Payment Registry](payment_registry.md) 12 | - [Deploy](deploy.md) 13 | - [Upgrade Contract](upgrade.md) 14 | - [Pause Contract](pause.md) 15 | -------------------------------------------------------------------------------- /docs/contracts/escrow.md: -------------------------------------------------------------------------------- 1 | # Escrow 2 | 3 | [Escrow](../../contracts/cairo/src/escrow.cairo) is a Smart Contract written in Cairo that resides in Ethereum's L2 Starknet. 4 | 5 | This contract is responsible for receiving Users' payments in L2, and liberating them 6 | to the MM when, and only when, the MM has proved the L1 payment. 7 | 8 | This contract has a storage of all orders. When a new order is made, by calling the 9 | `set_order` function, this contract reads the new order's details, verifies the Order 10 | is acceptable, and if so, it stores this data and accepts from the sender the 11 | appropriate amount of tokens. An Order's details are: the address where the User wants 12 | to receive the transaction on L1, the amount he wants to receive, and the amount he is 13 | willing to give the MM to concrete the bridge process. 14 | 15 | Once Escrow has accepted the new order, it will emit a `SetOrder` event, containing 16 | this information so that MMs can decide if they want to accept this offer. 17 | 18 | The user must wait until an MM picks its order, which should be almost instantaneous 19 | if the transfer fee is the suggested one. 20 | 21 | After an MM consolidates an order, Escrow will receive a `claim_payment` call from 22 | Payment Registry, containing the information about how MM has indeed bridged the funds 23 | to the User's L1 address, and where does MM want to receive it's L2 tokens. Escrow 24 | will then cross-check this information to its own records, and if everything is in 25 | check, Escrow will transfer the bridged amount of tokens, plus the fee, to MM's L2 26 | address. 27 | 28 | -------------------------------------------------------------------------------- /docs/contracts/pause.md: -------------------------------------------------------------------------------- 1 | ### Pause Escrow Contract 2 | 3 | Escrow also implements the interesting `Pauseable` module. This means the smart contract 4 | can be paused and unpaused by the smart contract Owner. When paused, all modifying 5 | functions are unavailable for everyone, including the Owner. 6 | 7 | For this, the Owner must execute the `pause` or `unpause` function from the smart 8 | contract, you can execute the following: 9 | 10 | ```bash 11 | make starknet-pause 12 | ``` 13 | 14 | ```bash 15 | make starknet-unpause 16 | ``` 17 | 18 | Alternatively, you can directly execute the function using the starkli tool. Note 19 | however, to run starkli this way, you must have exported the STARKNET_ACCOUNT and 20 | STARKNET_KEYSTORE variables: 21 | 22 | ```bash 23 | starkli invoke $ESCROW_CONTRACT_ADDRESS pause 24 | ``` 25 | 26 | ```bash 27 | starkli invoke $ESCROW_CONTRACT_ADDRESS unpause 28 | ``` 29 | 30 | You can also see if the contract is paused or unpaused by executing the following: 31 | 32 | ```bash 33 | starkli call $ESCROW_CONTRACT_ADDRESS is_paused 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/contracts/payment_registry.md: -------------------------------------------------------------------------------- 1 | # Payment Registry 2 | 3 | [Payment Registry](../../contracts/solidity/src/PaymentRegistry.sol) is a Smart 4 | Contract that resides in Ethereum's L1, responsible for receiving MM's transaction 5 | on L1, forwarding it to the User's address, and sending the information of this 6 | transaction to Escrow. 7 | 8 | So, when MM wants to complete an order it has read on Escrow, it will call the 9 | `transfer` function from Payment Registry, containing the relevant information 10 | (orderID, User's address on L1, and amount). Payment Registry will verify the information 11 | is acceptable, store it, and send the desired amount to User's L1 address. 12 | 13 | After this transfer is completed, MM must call `claimPayment` on Payment Registry to 14 | receive back the initial deposit made by the User, so that Payment Registry can verify 15 | MM has previously sent the order's amount to the User. If it has, this same function will 16 | call Escrow's `claim_payment`, informing Escrow that MM has indeed bridged funds for User, 17 | and that he wants to receive back his amount on L2. Then, as mentioned before, Escrow will 18 | release MM's funds to his desired L2 address. 19 | -------------------------------------------------------------------------------- /docs/contracts/upgrade.md: -------------------------------------------------------------------------------- 1 | # Upgrade 2 | 3 | ## Upgrading Payment Registry 4 | 5 | After deploying the `Payment Registry` contract, you can perform [upgrades](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable) to it. As 6 | mentioned previously, this is done via a [ERC1967 Proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Proxy). So, to upgrade 7 | Payment Registry, another smart contract must be deployed, and the address stored inside 8 | the Proxy must be changed. 9 | 10 | To do this you must: 11 | 12 | 1. Configure the `contracts/solidity/.env` file. 13 | 14 | ```env 15 | ETHEREUM_RPC = RPC provider URL 16 | 17 | ETHEREUM_PRIVATE_KEY = private key of your ETH wallet 18 | 19 | ETHERSCAN_API_KEY = API Key to use etherscan to read the Ethereum blockchain 20 | 21 | STARKNET_MESSAGING_ADDRESS = Starknet Messaging address 22 | ``` 23 | 24 | **NOTE:** This is a very similar configuration than the mentioned before, but 25 | MM_ETHEREUM_WALLET_ADDRESS is not necessary 26 | 27 | 2. Configure the address of the proxy to be upgraded: 28 | 29 | ```b 30 | export PAYMENT_REGISTRY_PROXY_ADDRESS = Address of your Payment Registry's Proxy 31 | ``` 32 | 33 | 3. Use the Makefile command to upgrade `Payment Registry` contract 34 | 35 | ```bash 36 | make ethereum-upgrade 37 | ``` 38 | 39 | **Note** 40 | - You must be the **owner** of the contract to upgrade it. 41 | - This command will: 42 | - Rebuild `Payment Registry.sol` 43 | - Deploy the new contract to the network 44 | - Utilize Foundry to upgrade the previous contract, changing the proxy's pointing 45 | address to the newly deployed contract 46 | 47 | ## Upgrade Escrow 48 | 49 | Our Escrow contract is also upgradeable, but it's method and process of upgrading is 50 | different from Payment Registry's upgrade. Starknet implemented the `replace_class` syscall, 51 | allowing a contract to update its source code by replacing its class hash once deployed. 52 | So, to upgrade Escrow, a new class hash must be declared, and the contract's class 53 | hash must be replaced. 54 | 55 | We will perform the upgrade using the `starkli` tool, so the same configuration used 56 | for deployment is necessary: 57 | 58 | 1. Configure `contracts/cairo/.env` file. 59 | 60 | ```env 61 | STARKNET_ACCOUNT = Path of your starknet account 62 | 63 | STARKNET_KEYSTORE = Path of your starknet keystore 64 | ``` 65 | 66 | 2. Configure the address of the contract to be upgraded: 67 | 68 | ```bash 69 | export ESCROW_CONTRACT_ADDRESS = Address of your Escrow smart contract 70 | ``` 71 | 72 | 3. Use the Makefile command to upgrade `Escrow` contract 73 | 74 | ```bash 75 | make starknet-upgrade 76 | ``` 77 | 78 | **Note** 79 | 80 | - You must be the **owner** of the contract to upgrade it. 81 | - This command will: 82 | - **rebuild** `Escrow.cairo` 83 | - **declare** it on Starknet 84 | - Call the external **upgrade()** function, 85 | from [OpenZeppellin's Upgradeable implementation](https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.8.0/src/upgrades/upgradeable.cairo), with the new class hash 86 | 87 | -------------------------------------------------------------------------------- /docs/images/YAB-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yetanotherco/yet-another-bridge/1217c0019ebeb8820286c4441d04167d4f150fd3/docs/images/YAB-diagram.png -------------------------------------------------------------------------------- /docs/images/YAB-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yetanotherco/yet-another-bridge/1217c0019ebeb8820286c4441d04167d4f150fd3/docs/images/YAB-header.jpg -------------------------------------------------------------------------------- /docs/mm_bot/README.md: -------------------------------------------------------------------------------- 1 | # Market Maker Bot 2 | 3 | Market Maker Bot is a bot that provides liquidity to the Yet Another 4 | Bridge (YAB). 5 | 6 | In the following sections, you will find information about: 7 | - [Architecture](architecture.md) 8 | - [Deploy](deploy.md) 9 | -------------------------------------------------------------------------------- /docs/mm_bot/architecture.md: -------------------------------------------------------------------------------- 1 | # MM Bot 2 | MM Bot is a process designed to supply liquidity to YAB Escrow orders. 3 | 4 | ## Logical View 5 | ### Functional Requirements 6 | - The bot must be able to read an order from the Escrow contract. 7 | - The bot must be able to perform a transfer in Ethereum to the recipient address through the Payment 8 | Registry contract. 9 | - The bot must be able to perform a repay in Ethereum to recover the funds in the L2 through the 10 | Payment Registry contract. 11 | - The bot must be able to store the orders in a database and update their status. 12 | - In case of an error, the bot must be able to store the error and retry the order. 13 | 14 | ### Simplified Class Diagram 15 | The following diagram shows the mm classes and how they interact with each other. 16 | 17 | ![mm_diagram_class.svg](images%2Fmm_diagram_class.svg) 18 | 19 | ### Full Class Diagram 20 | The following diagram is a detailed version of the previous diagram, 21 | showing the attributes and methods of each class. 22 | 23 | ![mm_diagram_class_full.svg](images%2Fmm_diagram_class_full.svg) 24 | 25 | ## Process View 26 | ### Non-Functional Requirements 27 | - The bot must be able to handle multiple orders concurrently. 28 | - The bot must be able to retrieve the status of the orders in case of interruption and complete them. 29 | - The bot must be highly available. 30 | - The bot must index the orders that belong to accepted blocks to ensure that orders are not lost. 31 | - The bot must be able to retry failed orders. 32 | - The bot must generate adequate logs for order tracking. 33 | 34 | ### Architecture 35 | The bot architecture is as follows: 36 | 37 | ![architecture.png](images/architecture.png) 38 | 39 | The bot is composed of the following components: 40 | - **Main Process**: The main process of the bot. It has the following subcomponents: 41 | - `Main Order Indexer`: The `Main Order Indexer` is responsible for indexing the orders from 42 | the pending blocks. 43 | - `Order Processor`: Responsible for processing the orders. 44 | - `Failed Orders Processor`: Responsible for retrying the failed orders. 45 | It runs every 5 minutes. 46 | - `Accepted Blocks Processor`: Responsible for indexing the orders that belong to accepted blocks. It runs every 5 minutes. 47 | - **Database**: The database is used to store the following data: 48 | - Orders 49 | - Errors 50 | - Block numbers 51 | 52 | An important aspect of the bot is that it must be able to handle multiple orders concurrently. 53 | For that reason, the bot uses the library 'asyncio' to handle orders concurrently. This approach, 54 | preferred over using threads, is particularly suitable for the bot's I/O-bound nature and the potential 55 | high volume of orders it could potentially need to manage. 56 | 57 | Another important requirement is that the bot must have a reliable network connection to communicate 58 | with Ethereum's and L2 networks' RPCs. 59 | 60 | ## Physical View 61 | There is a server to run the MM Bot Main Process as well as its 62 | database. 63 | 64 | ![physical_view.png](images/physical_view.png) 65 | 66 | ## Scenarios 67 | ### 1. Order Processing Flow 68 | The following diagram shows the complete process of an order. 69 | 70 | ![order_processing.svg](images%2Forder_processing.svg) 71 | 72 | And each has the following states: 73 | 74 | ![state_diagram.svg](images%2Fstate_diagram.svg) 75 | 76 | ### 2. Failed Orders Reprocessing 77 | When an order fails, the bot stores the error, and marks the order as failed. This way, the `Failed 78 | Orders Processor` is able to retry the failed orders. The following diagram shows the flow of a 79 | failed order through the bot. 80 | 81 | ![failed_orders.svg](images%2Ffailed_orders.svg) 82 | 83 | ### 3. Shutdown Recovery 84 | When the bot starts, it retrieves incomplete orders from the database and continues their processing. 85 | 86 | ### 4. Accepted Blocks Indexation 87 | The Main Order Indexer processes orders from pending blocks. The `Orders from 88 | Accepted Blocks Processor` will index the orders that belong to accepted blocks. This way, if the `Main Order 89 | Indexer` loses an order, it will be captured and processed by the `Orders from Accepted Blocks Processor`. 90 | 91 | ![accepted_blocks.svg](images%2Faccepted_blocks.svg) 92 | -------------------------------------------------------------------------------- /docs/mm_bot/deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy Guide 2 | 3 | ## Prerequisites 4 | - Python v3.10 or higher 5 | - pip 6 | - Postgres (Native or Docker) 7 | 8 | ## Setup 9 | ### Installation 10 | 11 | ```bash 12 | pip install -r requirements.txt 13 | ``` 14 | #### Virtual Environment 15 | If you want to use a virtual environment, you can use the following command: 16 | 17 | ```bash 18 | make create_python_venv 19 | ``` 20 | To run the virtual environment, you can use the following command: 21 | 22 | ```bash 23 | source venv/bin/activate 24 | ``` 25 | 26 | ### Environment Variables 27 | This API uses environment variables to configure the application. You can create a `.env` file in the root of the project to set the environment variables. 28 | 29 | To create your own `.env` file run the following command: 30 | 31 | ```bash 32 | make create_env 33 | ``` 34 | 35 | The following environment variables are used: 36 | 37 | ENVIRONMENT= 38 | ETHEREUM_RPC= 39 | STARKNET_RPC= 40 | ETH_FALLBACK_RPC_URL= 41 | SN_FALLBACK_RPC_URL= 42 | ETHEREUM_CONTRACT_ADDRESS= 43 | STARKNET_CONTRACT_ADDRESS= 44 | ETHEREUM_PRIVATE_KEY= 45 | STARKNET_WALLET_ADDRESS= 46 | STARKNET_PRIVATE_KEY= 47 | HERODOTUS_API_KEY= 48 | POSTGRES_HOST= 49 | POSTGRES_USER= 50 | POSTGRES_PASSWORD= 51 | POSTGRES_DATABASE= 52 | LOGGING_LEVEL= 53 | LOGGING_DIRECTORY= 54 | PAYMENT_CLAIMER= 55 | 56 | 57 | There is a example file called `.env.example` in the root of the project. 58 | 59 | ### Database Setup 60 | #### Create Database Container 61 | This Bot uses a Postgres database. You can either install Postgres natively or use Docker (recommended for development environment). 62 | If you use Docker, you can use the following command to start a Postgres container: 63 | ```bash 64 | make create_db container= user= password= database= 65 | ``` 66 | Where: 67 | - container: the name of the docker container. If not provided, the default value is 'postgres' 68 | - user: the user to create. If not provided, the default value is 'user' 69 | - password: the password for the user. If not provided, the default value is '123123123' 70 | - database: the name of the database to create. If not provided, the default value is 'mm-bot' 71 | 72 | This container will have a database called ``, by default it is `mm-bot`. 73 | 74 | #### Run Database Container 75 | If you want to run or re-run the database container, you can use the following command: 76 | ```bash 77 | make run_db container= 78 | ``` 79 | Where: 80 | - container: the name of the docker container. If not provided, the default value is 'postgres' 81 | 82 | #### Stop Database Container 83 | If you want to stop the database container, you can use the following command: 84 | ```bash 85 | make stop_db container= 86 | ``` 87 | Where: 88 | - container: the name of the docker container. If not provided, the default value is 'postgres' 89 | 90 | #### Database Population 91 | To create the tables, you can use the following command: 92 | ```bash 93 | TODO 94 | ``` 95 | You must run schema.sql into the database to create the tables. You can use pgAdmin or any other tool to run the script. 96 | 97 | ## Development 98 | To start the Bot, you can use the following command: 99 | 100 | ```bash 101 | python3 src/main.py 102 | ``` 103 | 104 | ## Test [TODO] 105 | To run the tests, you can use the following command: 106 | 107 | ```bash 108 | 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/mm_bot/diagrams/accepted_blocks.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | hide footbox 3 | actor User as U 4 | box Starknet #ebc7ff 5 | entity Escrow as E #purple 6 | end box 7 | box Ethereum #99e6ff 8 | entity "Payment Registry" as PR #blue 9 | end box 10 | box "Market Maker" 11 | control "Order Processor" as OP 12 | control "Order from Accepted Blocks Processor" as ABP 13 | database Database 14 | end box 15 | 16 | note right of U 17 | Order already exists 18 | in an accepted block 19 | end note 20 | ABP -[#purple]> E : Gets Orders from Accepted Blocks 21 | ABP -> Database : Stores missing orders 22 | ABP -> OP : Processes missing Order 23 | OP -> OP : Processes Order 24 | OP -[#blue]> PR : Transfers funds 25 | PR -[#blue]> U: Transfers funds 26 | OP -[#blue]> PR : Sends proof of payment 27 | PR -[#purple]> E : Sends proof of payment 28 | E -[#purple]> OP : Send funds 29 | OP -> Database : Stores Order as completed 30 | 31 | @enduml 32 | 33 | -------------------------------------------------------------------------------- /docs/mm_bot/diagrams/failed_orders.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | hide footbox 3 | actor User as U 4 | box Starknet #ebc7ff 5 | entity Escrow as E #purple 6 | end box 7 | box Ethereum #99e6ff 8 | entity "Payment Registry" as PR #blue 9 | end box 10 | box "Market Maker" 11 | control "Order Processor" as OP 12 | control "Failed Order Processor" as FOP 13 | database Database 14 | end box 15 | 16 | U -[#purple]> E : Sets Order 17 | OP -[#purple]> E : Gets Order 18 | OP ->x OP : Processes Order 19 | OP -> Database : Stores Order as failed due to an error 20 | FOP -> Database : Gets failed orders 21 | FOP -> OP : Processes failed order 22 | OP -[#blue]> PR : Transfers funds 23 | PR -[#blue]> U: Transfers funds 24 | OP -[#blue]> PR : Sends proof of payment 25 | PR -[#purple]> E : Sends proof of payment 26 | E -[#purple]> OP : Send funds 27 | OP -> Database : Stores Order as completed 28 | 29 | @enduml 30 | -------------------------------------------------------------------------------- /docs/mm_bot/diagrams/order_processing.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | hide footbox 3 | actor User as U 4 | box Starknet #ebc7ff 5 | entity Escrow as E #purple 6 | end box 7 | box Ethereum #99e6ff 8 | entity "Payment Registry" as PR #blue 9 | box "Market Maker" 10 | control "Order Processor" as OP 11 | database Database 12 | end box 13 | 14 | U -[#purple]> E : Sets Order 15 | OP -[#purple]> E : Gets Order 16 | OP -> Database : Stores Order as pending 17 | OP -> OP : Processes Order 18 | OP -[#blue]> PR : Transfers funds 19 | PR -[#blue]> U: Transfers funds 20 | OP -[#blue]> PR : Sends proof of payment 21 | PR -[#purple]> E : Sends proof of payment 22 | E -[#purple]> OP : Send funds 23 | OP -> Database : Stores Order as completed 24 | 25 | 26 | @enduml 27 | -------------------------------------------------------------------------------- /docs/mm_bot/diagrams/state_diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skin rose 4 | hide empty description 5 | 6 | State Pending 7 | State Processing 8 | State Transferring 9 | State Fulfilled 10 | State Proving 11 | State Proved 12 | State Completed #lightgreen 13 | State Dropped #pink 14 | 15 | note "User sets a new order" as N1 16 | [*] -d-> Pending 17 | note on link 18 | Creates a new order in the database 19 | end note 20 | 21 | Pending -d[#black]-> Processing 22 | note on link 23 | Starts processing order 24 | end note 25 | 26 | State CheckFee <> 27 | note right of CheckFee 28 | Is fee enough? 29 | end note 30 | State CheckAmount <> 31 | note left of CheckAmount 32 | amount <= 0.1 ETH? 33 | end note 34 | Processing -d[#black]-> CheckFee 35 | CheckFee -[#black]-> CheckAmount : yes 36 | CheckFee -r[#red]-> Dropped : no 37 | 38 | CheckAmount -[#black]-> Transferring 39 | note on link 40 | Transfers funds on L1 41 | end note 42 | CheckAmount -d[#red]-> Dropped : no 43 | 44 | Transferring -d[#black]-> Fulfilled 45 | note on link 46 | Waits for transfer confirmation 47 | end note 48 | 49 | Fulfilled -d[#black]-> Proving 50 | note on link 51 | Send proof of payment 52 | end note 53 | 54 | Proving -d[#black]-> Proved 55 | note on link 56 | Waits for proof confirmation 57 | end note 58 | 59 | Proved -d[#black]-> Completed 60 | note on link 61 | Order is completed 62 | end note 63 | 64 | Completed -d-> [*] 65 | Dropped --> [*] 66 | 67 | @enduml 68 | -------------------------------------------------------------------------------- /docs/mm_bot/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yetanotherco/yet-another-bridge/1217c0019ebeb8820286c4441d04167d4f150fd3/docs/mm_bot/images/architecture.png -------------------------------------------------------------------------------- /docs/mm_bot/images/physical_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yetanotherco/yet-another-bridge/1217c0019ebeb8820286c4441d04167d4f150fd3/docs/mm_bot/images/physical_view.png -------------------------------------------------------------------------------- /mm-bot/.env.example: -------------------------------------------------------------------------------- 1 | ENVIRONMENT= 2 | ETHEREUM_CHAIN_ID=<1=mainnet|5=goerli|11155111=sepolia> 3 | STARKNET_CHAIN_ID= 4 | ETHEREUM_RPC= 5 | STARKNET_RPC= 6 | ZKSYNC_RPC= 7 | ETHEREUM_FALLBACK_RPC= 8 | STARKNET_FALLBACK_RPC= 9 | ZKSYNC_FALLBACK_RPC= 10 | ETHEREUM_CONTRACT_ADDRESS= 11 | STARKNET_CONTRACT_ADDRESS= 12 | ZKSYNC_CONTRACT_ADDRESS= 13 | ETHEREUM_PRIVATE_KEY= 14 | STARKNET_WALLET_ADDRESS= 15 | STARKNET_PRIVATE_KEY= 16 | HERODOTUS_API_KEY= 17 | POSTGRES_HOST= 18 | POSTGRES_USER= 19 | POSTGRES_PASSWORD= 20 | POSTGRES_DATABASE= 21 | LOGGING_LEVEL= 22 | LOGGING_DIRECTORY= 23 | PAYMENT_CLAIMER= 24 | -------------------------------------------------------------------------------- /mm-bot/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | __pycache__/ 3 | .env 4 | venv 5 | .env.sepolia 6 | .env.goerli 7 | .env.local 8 | -------------------------------------------------------------------------------- /mm-bot/Makefile: -------------------------------------------------------------------------------- 1 | # Database variables 2 | container = mm-bot 3 | user = user 4 | password = 123123123 5 | database = mm-bot-db 6 | 7 | 8 | # Application Commands 9 | run: 10 | @echo "Running application..." 11 | @. venv/bin/activate && python3 src/main.py 12 | 13 | create_env: 14 | @echo "Creating environment variables files .env", 15 | @cp .env.example .env 16 | @echo "Environment variables files created successfully!" 17 | 18 | create_python_venv: 19 | @echo "Creating virtual environment..." 20 | @python3 -m venv venv 21 | @echo "Virtual environment created successfully!" 22 | 23 | # Database Commands 24 | start_db: 25 | @if [ "$(container)" = "postgres" ]; then echo "Using default container name: postgres."; fi 26 | docker start $(container) 27 | 28 | stop_db: 29 | @if [ "$(container)" = "postgres" ]; then echo "Using default container name: postgres."; fi 30 | docker stop $(container) 31 | 32 | create_db: 33 | @if [ "$(container)" = "mm-bot" ]; then echo "Using default container name: 'mm-bot'."; fi 34 | @if [ "$(user)" = "user" ]; then echo "Using default user name: 'user'."; fi 35 | @if [ "$(password)" = "123123123" ]; then echo "Using default password: '123123123'."; fi 36 | @if [ "$(database)" = "mm-bot-db" ]; then echo "Using default database name: 'mm-bot-db'."; fi 37 | 38 | docker run --name $(container) -e POSTGRES_PASSWORD=$(password) -e POSTGRES_USER=$(user) -p 5432:5432 -d postgres 39 | sleep 5 # Wait for the PostgreSQL container to start (you can adjust this as needed) 40 | docker exec -it $(container) psql -U $(user) -c 'CREATE DATABASE "$(database)" WITH ENCODING "UTF-8";' 41 | -------------------------------------------------------------------------------- /mm-bot/README.md: -------------------------------------------------------------------------------- 1 | # Market Maker Bot 2 | Market Maker Bot is a bot that provides liquidity to the Yet Another Bridge (YAB). 3 | 4 | # Prerequisites 5 | - Python v3.10 or higher 6 | - pip 7 | - Postgres (Native or Docker) 8 | 9 | # Setup 10 | ## Installation 11 | 12 | ```bash 13 | pip install -r requirements.txt 14 | ``` 15 | ### Virtual Environment 16 | If you want to use a virtual environment, you can use the following command: 17 | 18 | ```bash 19 | make create_python_venv 20 | ``` 21 | To run the virtual environment, you can use the following command: 22 | 23 | ```bash 24 | source venv/bin/activate 25 | ``` 26 | 27 | ## Environment Variables 28 | This API uses environment variables to configure the application. You can create a `.env` file in the root of the project to set the environment variables. 29 | 30 | To create your own `.env` file run the following command: 31 | 32 | ```bash 33 | make create_env 34 | ``` 35 | 36 | The following environment variables are used: 37 | 38 | ENVIRONMENT= 39 | ETHEREUM_RPC= 40 | STARKNET_RPC= 41 | ETHEREUM_FALLBACK_RPC= 42 | STARKNET_FALLBACK_RPC= 43 | ETHEREUM_CONTRACT_ADDRESS= 44 | STARKNET_CONTRACT_ADDRESS= 45 | ETHEREUM_PRIVATE_KEY= 46 | STARKNET_WALLET_ADDRESS= 47 | STARKNET_PRIVATE_KEY= 48 | HERODOTUS_API_KEY= 49 | POSTGRES_HOST= 50 | POSTGRES_USER= 51 | POSTGRES_PASSWORD= 52 | POSTGRES_DATABASE= 53 | LOGGING_LEVEL= 54 | LOGGING_DIRECTORY= 55 | PAYMENT_CLAIMER= 56 | 57 | 58 | There is a example file called `.env.example` in the root of the project. 59 | 60 | ## Database Setup 61 | ### Create Database Container 62 | This Bot uses a Postgres database. You can either install Postgres natively or use Docker (recommended for development environment). 63 | If you use Docker, you can use the following command to start a Postgres container: 64 | ```bash 65 | make create_db container= user= password= database= 66 | ``` 67 | Where: 68 | - container: the name of the docker container. If not provided, the default value is 'postgres' 69 | - user: the user to create. If not provided, the default value is 'user' 70 | - password: the password for the user. If not provided, the default value is '123123123' 71 | - database: the name of the database to create. If not provided, the default value is 'mm-bot' 72 | 73 | This container will have a database called ``, by default it is `mm-bot`. 74 | 75 | ### Run Database Container 76 | If you want to run or re-run the database container, you can use the following command: 77 | ```bash 78 | make run_db container= 79 | ``` 80 | Where: 81 | - container: the name of the docker container. If not provided, the default value is 'postgres' 82 | 83 | ### Stop Database Container 84 | If you want to stop the database container, you can use the following command: 85 | ```bash 86 | make stop_db container= 87 | ``` 88 | Where: 89 | - container: the name of the docker container. If not provided, the default value is 'postgres' 90 | 91 | ### Database Population 92 | To create the tables, you can use the following command: 93 | ```bash 94 | TODO 95 | ``` 96 | You must run schema.sql into the database to create the tables. You can use pgAdmin or any other tool to run the script. 97 | 98 | # Development 99 | To start the Bot, you can use the following command: 100 | 101 | ```bash 102 | python3 src/main.py 103 | ``` 104 | 105 | # Test [TODO] 106 | To run the tests, you can use the following command: 107 | 108 | ```bash 109 | 110 | ``` 111 | -------------------------------------------------------------------------------- /mm-bot/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==1.0.0 2 | Requests==2.31.0 3 | starknet_py==0.19.0-alpha 4 | web3==6.5.0 5 | SQLAlchemy==2.0.23 6 | psycopg2-binary==2.9.9 7 | schedule==1.2.1 8 | zksync2==1.1.0 9 | -------------------------------------------------------------------------------- /mm-bot/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS orders 2 | ( 3 | order_id INT NOT NULL, 4 | origin_network VARCHAR(32) NOT NULL, 5 | 6 | recipient_address VARCHAR(42) NOT NULL, -- 0x + 40 bytes 7 | amount NUMERIC(78, 0) NOT NULL, -- uint 256 8 | fee NUMERIC(78, 0) NOT NULL, -- uint 256 9 | 10 | status VARCHAR(32) NOT NULL DEFAULT 'PENDING', 11 | failed BOOLEAN NOT NULL DEFAULT FALSE, 12 | 13 | set_order_tx_hash BYTEA NOT NULL, -- 66 chars 14 | transfer_tx_hash BYTEA NULL, -- 32 bytes 15 | claim_tx_hash BYTEA NULL, -- 32 bytes 16 | 17 | herodotus_task_id VARCHAR(64) NULL, 18 | herodotus_block BIGINT NULL, -- uint 64 19 | herodotus_slot BYTEA NULL, -- 32 bytes 20 | 21 | created_at TIMESTAMP NOT NULL DEFAULT clock_timestamp(), 22 | transferred_at TIMESTAMP NULL, 23 | completed_at TIMESTAMP NULL, 24 | 25 | PRIMARY KEY (order_id, origin_network) 26 | ); 27 | 28 | CREATE TABLE IF NOT EXISTS block 29 | ( 30 | id SERIAL PRIMARY KEY, 31 | network VARCHAR(32) NOT NULL, 32 | latest_block BIGINT NOT NULL DEFAULT 0, 33 | created_at TIMESTAMP NOT NULL DEFAULT clock_timestamp() 34 | ); 35 | 36 | INSERT INTO block (latest_block) VALUES (0) ON CONFLICT DO NOTHING; 37 | 38 | CREATE TABLE IF NOT EXISTS error 39 | ( 40 | id SERIAL PRIMARY KEY, 41 | order_id INT NOT NULL, 42 | origin_network VARCHAR(32) NOT NULL, 43 | message TEXT NOT NULL, 44 | created_at TIMESTAMP NOT NULL DEFAULT clock_timestamp(), 45 | 46 | FOREIGN KEY (order_id, origin_network) REFERENCES orders (order_id, origin_network) ON DELETE CASCADE 47 | ); 48 | -------------------------------------------------------------------------------- /mm-bot/resources/zksync.sql: -------------------------------------------------------------------------------- 1 | -- This a script to migrate the original implementation to the implementation with origin_network field 2 | 3 | ALTER TABLE orders ADD COLUMN origin_network VARCHAR(32) NOT NULL DEFAULT 'STARKNET'; 4 | 5 | -- Modify the orders primary key to include the new column 6 | ALTER TABLE orders DROP CONSTRAINT orders_pkey CASCADE; -- Cascade is needed to drop the foreign key constraint 7 | ALTER TABLE orders ADD PRIMARY KEY (order_id, origin_network); 8 | 9 | -- Set new error foreign key 10 | ALTER TABLE error ADD COLUMN origin_network VARCHAR(32) NOT NULL DEFAULT 'STARKNET'; 11 | ALTER TABLE error ADD CONSTRAINT errors_order_id_fkey FOREIGN KEY (order_id, origin_network) REFERENCES orders(order_id, origin_network); 12 | 13 | -- Add set_order_tx_hash column to orders table 14 | ALTER TABLE orders ADD COLUMN set_order_tx_hash BYTEA; 15 | 16 | ALTER TABLE orders alter starknet_tx_hash drop not null; 17 | 18 | -- Rename tx_hash to transfer_tx_hash 19 | ALTER TABLE orders RENAME COLUMN tx_hash TO transfer_tx_hash; 20 | 21 | -- Rename eth_claim_tx_hash to claim_tx_hash 22 | ALTER TABLE orders RENAME COLUMN eth_claim_tx_hash TO claim_tx_hash; 23 | 24 | -- Add network column to block table with default value 'STARKNET' 25 | ALTER TABLE block ADD COLUMN network VARCHAR(32) NOT NULL DEFAULT 'STARKNET'; 26 | -------------------------------------------------------------------------------- /mm-bot/src/config/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | ENVIRONMENT = os.getenv('ENVIRONMENT') 7 | 8 | ETHEREUM_CHAIN_ID = os.getenv('ETHEREUM_CHAIN_ID') 9 | STARKNET_CHAIN_ID = os.getenv('STARKNET_CHAIN_ID') 10 | 11 | ETHEREUM_RPC = os.getenv('ETHEREUM_RPC') 12 | STARKNET_RPC = os.getenv('STARKNET_RPC') 13 | ZKSYNC_RPC = os.getenv('ZKSYNC_RPC') 14 | 15 | ETHEREUM_FALLBACK_RPC = os.getenv('ETHEREUM_FALLBACK_RPC') 16 | STARKNET_FALLBACK_RPC = os.getenv('STARKNET_FALLBACK_RPC') 17 | ZKSYNC_FALLBACK_RPC = os.getenv('ZKSYNC_FALLBACK_RPC') 18 | 19 | ETHEREUM_CONTRACT_ADDRESS = os.getenv('ETHEREUM_CONTRACT_ADDRESS') 20 | STARKNET_CONTRACT_ADDRESS = os.getenv('STARKNET_CONTRACT_ADDRESS') 21 | ZKSYNC_CONTRACT_ADDRESS = os.getenv('ZKSYNC_CONTRACT_ADDRESS') 22 | 23 | ETHEREUM_PRIVATE_KEY = os.getenv('ETHEREUM_PRIVATE_KEY') 24 | STARKNET_WALLET_ADDRESS = os.getenv('STARKNET_WALLET_ADDRESS') 25 | STARKNET_PRIVATE_KEY = os.getenv('STARKNET_PRIVATE_KEY') 26 | 27 | HERODOTUS_API_KEY = os.getenv('HERODOTUS_API_KEY') 28 | HERODOTUS_ORIGIN_CHAIN = '5' 29 | HERODOTUS_DESTINATION_CHAIN = 'SN_GOERLI' 30 | 31 | MAX_RETRIES = 30 32 | RETRIES_DELAY = 60 # 60 seconds, 30 tries within 30 minutes 33 | 34 | POSTGRES_HOST = os.getenv('POSTGRES_HOST') 35 | POSTGRES_USER = os.getenv('POSTGRES_USER') 36 | POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD') 37 | POSTGRES_DATABASE = os.getenv('POSTGRES_DATABASE') 38 | 39 | LOGGING_LEVEL = os.getenv('LOGGING_LEVEL') 40 | LOGGING_DIRECTORY = os.getenv('LOGGING_DIRECTORY') 41 | 42 | PAYMENT_CLAIMER = os.getenv('PAYMENT_CLAIMER') 43 | -------------------------------------------------------------------------------- /mm-bot/src/config/database_config.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker, declarative_base 3 | 4 | from config import constants 5 | 6 | engine = create_engine( 7 | f"postgresql://{constants.POSTGRES_USER}:{constants.POSTGRES_PASSWORD}@{constants.POSTGRES_HOST}:5432/{constants.POSTGRES_DATABASE}") 8 | 9 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 10 | 11 | Base = declarative_base() 12 | 13 | 14 | def get_db(): 15 | return SessionLocal() 16 | -------------------------------------------------------------------------------- /mm-bot/src/config/logging_config.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from logging.handlers import TimedRotatingFileHandler 4 | import sys 5 | from config import constants 6 | 7 | 8 | def setup_logger(): 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.DEBUG) 11 | 12 | # Formatter for log messages 13 | log_format = "%(asctime)s %(levelname)6s - [%(threadName)15s] [%(taskName)15s] : %(message)s" 14 | formatter = logging.Formatter(log_format, datefmt="%Y-%m-%dT%H:%M:%S") 15 | 16 | # Add handlers based on the environment 17 | if in_production(): 18 | handler = get_production_handler(formatter) 19 | else: 20 | handler = get_development_handler(formatter) 21 | handler.addFilter(AsyncioFilter()) 22 | logger.addHandler(handler) 23 | 24 | 25 | def in_production(): 26 | return constants.ENVIRONMENT == 'prod' 27 | 28 | 29 | # Console Handler for development 30 | def get_development_handler(formatter): 31 | console_handler = logging.StreamHandler(sys.stdout) 32 | console_handler.setLevel(constants.LOGGING_LEVEL) 33 | console_handler.setFormatter(formatter) 34 | return console_handler 35 | 36 | 37 | # File Handler for production with daily rotation 38 | def get_production_handler(formatter): 39 | file_handler = TimedRotatingFileHandler( 40 | filename=f"{constants.LOGGING_DIRECTORY}/mm-bot.log", 41 | when="midnight", 42 | interval=1, 43 | backupCount=7, # Keep up to 7 old log files 44 | encoding="utf-8" 45 | ) 46 | file_handler.setLevel(constants.LOGGING_LEVEL) 47 | file_handler.setFormatter(formatter) 48 | return file_handler 49 | 50 | 51 | class AsyncioFilter(logging.Filter): 52 | """ 53 | This is a filter which injects contextual information into the log. 54 | """ 55 | def filter(self, record): 56 | try: 57 | record.taskName = asyncio.current_task().get_name() 58 | except Exception: 59 | record.taskName = "Main" 60 | return True 61 | -------------------------------------------------------------------------------- /mm-bot/src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | import schedule 5 | 6 | from config import constants 7 | from config.database_config import get_db 8 | from config.logging_config import setup_logger 9 | from models.network import Network 10 | from persistence.block_dao import BlockDao 11 | from persistence.error_dao import ErrorDao 12 | from persistence.order_dao import OrderDao 13 | from services.executors.order_executor import OrderExecutor 14 | from services.fee_calculators.starknet_fee_calculator import StarknetFeeCalculator 15 | from services.fee_calculators.zksync_fee_calculator import ZksyncFeeCalculator 16 | from services.indexers.starknet_order_indexer import StarknetOrderIndexer 17 | from services.indexers.zksync_order_indexer import ZksyncOrderIndexer 18 | from services.order_service import OrderService 19 | from services.payment_claimer.ethereum_2_zksync_payment_claimer import Ethereum2ZksyncPaymentClaimer 20 | from services.payment_claimer.ethereum_payment_claimer import EthereumPaymentClaimer 21 | from services.payment_claimer.herodotus_payment_claimer import HerodotusPaymentClaimer 22 | from services.payment_claimer.payment_claimer import PaymentClaimer 23 | from services.processors.accepted_blocks_orders_processor import AcceptedBlocksOrdersProcessor 24 | from services.processors.failed_orders_processor import FailedOrdersProcessor 25 | from services.processors.orders_processor import OrdersProcessor 26 | from services.senders.ethereum_sender import EthereumSender 27 | 28 | setup_logger() 29 | logger = logging.getLogger(__name__) 30 | SLEEP_TIME = 5 31 | PROCESS_FAILED_ORDERS_MINUTES_TIMER = 5 32 | PROCESS_ACCEPTED_BLOCKS_MINUTES_TIMER = 5 33 | MAX_ETH_TRANSFER_WEI = 100000000000000000 # TODO move to env variable 34 | 35 | 36 | def using_herodotus(): 37 | return constants.PAYMENT_CLAIMER == "herodotus" 38 | 39 | 40 | async def run(): 41 | logger.info(f"[+] Listening events") 42 | # Initialize DAOs 43 | order_dao = OrderDao(get_db()) 44 | error_dao = ErrorDao(get_db()) 45 | block_dao = BlockDao(get_db()) 46 | 47 | # Initialize services 48 | order_service = OrderService(order_dao, error_dao) 49 | 50 | # Initialize concurrency primitives 51 | eth_lock = asyncio.Lock() 52 | herodotus_semaphore = asyncio.Semaphore(100) 53 | 54 | # Initialize fee calculator 55 | starknet_fee_calculator = StarknetFeeCalculator() 56 | zksync_fee_calculator = ZksyncFeeCalculator() 57 | 58 | # Initialize sender and payment claimer 59 | ethereum_sender = EthereumSender(order_service) 60 | starknet_payment_claimer: PaymentClaimer = HerodotusPaymentClaimer() if using_herodotus() \ 61 | else EthereumPaymentClaimer(starknet_fee_calculator) 62 | zksync_payment_claimer: PaymentClaimer = Ethereum2ZksyncPaymentClaimer(zksync_fee_calculator) 63 | 64 | # Initialize starknet indexer and processor 65 | starknet_order_indexer = StarknetOrderIndexer(order_service) 66 | starknet_order_executor = OrderExecutor(order_service, ethereum_sender, starknet_payment_claimer, 67 | starknet_fee_calculator, eth_lock, herodotus_semaphore, 68 | MAX_ETH_TRANSFER_WEI) 69 | starknet_orders_processor = OrdersProcessor(starknet_order_indexer, starknet_order_executor) 70 | 71 | # Initialize ZkSync indexer and processor 72 | zksync_order_indexer = ZksyncOrderIndexer(order_service) 73 | zksync_order_executor = OrderExecutor(order_service, ethereum_sender, zksync_payment_claimer, 74 | zksync_fee_calculator, eth_lock, herodotus_semaphore, 75 | MAX_ETH_TRANSFER_WEI) 76 | zksync_order_processor = OrdersProcessor(zksync_order_indexer, zksync_order_executor) 77 | 78 | # Initialize failed orders processor for starknet 79 | failed_orders_processor = FailedOrdersProcessor(starknet_order_executor, order_service) 80 | 81 | # Initialize accepted blocks orders processor for starknet 82 | accepted_blocks_orders_processor = AcceptedBlocksOrdersProcessor(starknet_order_indexer, starknet_order_executor, 83 | block_dao) 84 | 85 | (schedule.every(PROCESS_FAILED_ORDERS_MINUTES_TIMER).minutes 86 | .do(failed_orders_processor.process_orders_job)) 87 | (schedule.every(PROCESS_ACCEPTED_BLOCKS_MINUTES_TIMER).minutes 88 | .do(accepted_blocks_orders_processor.process_orders_job)) 89 | 90 | try: 91 | # Get all orders that are not completed from the db 92 | orders = order_service.get_incomplete_orders() 93 | for order in orders: 94 | if order.origin_network is Network.STARKNET: 95 | starknet_order_executor.execute(order) 96 | elif order.origin_network is Network.ZKSYNC: 97 | zksync_order_executor.execute(order) 98 | except Exception as e: 99 | logger.error(f"[-] Error: {e}") 100 | 101 | schedule.run_all() 102 | 103 | while True: 104 | try: 105 | # Process new orders 106 | tasks = [asyncio.create_task(starknet_orders_processor.process_orders(), name="Starknet_Processor"), 107 | asyncio.create_task(zksync_order_processor.process_orders(), name="ZkSync_Processor")] 108 | await asyncio.gather(*tasks) 109 | 110 | schedule.run_pending() 111 | except Exception as e: 112 | logger.error(f"[-] Error: {e}") 113 | 114 | await asyncio.sleep(SLEEP_TIME) 115 | 116 | 117 | if __name__ == '__main__': 118 | asyncio.run(run()) 119 | -------------------------------------------------------------------------------- /mm-bot/src/migrate_zksync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from config.database_config import get_db 4 | from models.order import Order 5 | from persistence.order_dao import OrderDao 6 | 7 | 8 | async def run(): 9 | db = get_db() 10 | order_dao = OrderDao(db) 11 | orders = order_dao.get_orders(Order.order_id > 0) 12 | for order in orders: 13 | aux_hash = order.set_order_tx_hash.decode() 14 | aux = bytes.fromhex(aux_hash.replace("0x", "").zfill(64)) 15 | order.set_order_tx_hash = aux 16 | order_dao.update_order(order) 17 | 18 | asyncio.run(run()) 19 | -------------------------------------------------------------------------------- /mm-bot/src/models/block.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, Integer, DateTime, Enum 4 | 5 | from config.database_config import Base 6 | from models.network import Network 7 | 8 | 9 | class Block(Base): 10 | __tablename__ = "block" 11 | id: int = Column(Integer, primary_key=True, nullable=False) 12 | network: Network = Column(Enum(Network), nullable=False) 13 | latest_block: int = Column(Integer, nullable=False) 14 | created_at: datetime = Column(DateTime, nullable=False, server_default="clock_timestamp()") 15 | 16 | def __str__(self): 17 | return f"latest_block:{self.latest_block}" 18 | -------------------------------------------------------------------------------- /mm-bot/src/models/error.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, Integer, ForeignKeyConstraint, String, DateTime, Enum 4 | from sqlalchemy.orm import relationship, Mapped 5 | 6 | from config.database_config import Base 7 | from models.network import Network 8 | from models.order import Order 9 | 10 | 11 | class Error(Base): 12 | __tablename__ = "error" 13 | id: int = Column(Integer, primary_key=True, nullable=False) 14 | order_id: int = Column(Integer, nullable=False) 15 | origin_network: Network = Column(Enum(Network), nullable=False) 16 | order: Mapped[Order] = relationship("Order") 17 | message: str = Column(String, nullable=False) 18 | created_at: datetime = Column(DateTime, nullable=False, server_default="clock_timestamp()") 19 | 20 | # Set order_id and origin_network as composite foreign key 21 | __table_args__ = ( 22 | ForeignKeyConstraint( 23 | ["order_id", "origin_network"], 24 | ["orders.order_id", "orders.origin_network"] 25 | ), 26 | ) 27 | -------------------------------------------------------------------------------- /mm-bot/src/models/network.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Network(Enum): 5 | STARKNET = 0 6 | ZKSYNC = 1 7 | -------------------------------------------------------------------------------- /mm-bot/src/models/order.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from datetime import datetime 3 | 4 | from hexbytes import HexBytes 5 | from sqlalchemy import Column, Integer, String, DateTime, Enum, Numeric, LargeBinary 6 | 7 | from config.database_config import Base 8 | from models.network import Network 9 | from models.order_status import OrderStatus 10 | from models.set_order_event import SetOrderEvent 11 | 12 | 13 | class Order(Base): 14 | __tablename__ = "orders" 15 | order_id: int = Column(Integer, primary_key=True, nullable=False) 16 | origin_network: Network = Column(Enum(Network), primary_key=True, nullable=False) 17 | 18 | recipient_address: str = Column(String(42), nullable=False) 19 | amount: decimal = Column(Numeric(78, 0), nullable=False) 20 | fee: decimal = Column(Numeric(78, 0), nullable=False) 21 | 22 | status: OrderStatus = Column(Enum(OrderStatus), nullable=False, default=OrderStatus.PENDING) 23 | failed: bool = Column(Integer, nullable=False, default=False) 24 | 25 | set_order_tx_hash: HexBytes = Column(LargeBinary, nullable=False) 26 | transfer_tx_hash: HexBytes = Column(LargeBinary, nullable=True) 27 | claim_tx_hash: HexBytes = Column(LargeBinary, nullable=True) 28 | 29 | herodotus_task_id: str = Column(String, nullable=True) 30 | herodotus_block: int = Column(Integer, nullable=True) 31 | herodotus_slot: HexBytes = Column(LargeBinary, nullable=True) 32 | 33 | created_at: datetime = Column(DateTime, nullable=False, server_default="clock_timestamp()") 34 | transferred_at: datetime = Column(DateTime, nullable=True) 35 | completed_at: datetime = Column(DateTime, nullable=True) 36 | 37 | def __str__(self): 38 | return f"[{self.origin_network.name} ~ {self.order_id}], recipient: {self.recipient_address}, amount: {self.amount}, fee: {self.fee}, status: {self.status.value}" 39 | 40 | def __repr__(self): 41 | return str(self) 42 | 43 | def get_int_amount(self) -> int: 44 | return int(self.amount) 45 | 46 | def get_int_fee(self) -> int: 47 | return int(self.fee) 48 | 49 | def summary(self) -> str: 50 | """ 51 | Returns a string with the origin network and order id 52 | """ 53 | return f"{self.origin_network.name} ~ {self.order_id}" 54 | 55 | @staticmethod 56 | def from_set_order_event(set_order_event: SetOrderEvent): 57 | return Order( 58 | order_id=set_order_event.order_id, 59 | origin_network=set_order_event.origin_network, 60 | recipient_address=set_order_event.recipient_address, 61 | amount=set_order_event.amount, 62 | fee=set_order_event.fee, 63 | set_order_tx_hash=set_order_event.set_order_tx_hash, 64 | status=OrderStatus.COMPLETED if set_order_event.is_used else OrderStatus.PENDING 65 | ) 66 | -------------------------------------------------------------------------------- /mm-bot/src/models/order_status.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OrderStatus(Enum): 5 | PENDING = "PENDING" 6 | PROCESSING = "PROCESSING" 7 | TRANSFERRING = "TRANSFERRING" 8 | FULFILLED = "FULFILLED" 9 | PROVING = "PROVING" 10 | PROVED = "PROVED" 11 | COMPLETED = "COMPLETED" 12 | DROPPED = "DROPPED" 13 | -------------------------------------------------------------------------------- /mm-bot/src/models/set_order_event.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from models.network import Network 4 | from services import ethereum 5 | 6 | 7 | class SetOrderEvent: 8 | 9 | def __init__(self, 10 | order_id, 11 | origin_network, 12 | set_order_tx_hash, 13 | recipient_address, 14 | amount, 15 | fee, 16 | block_number, 17 | is_used=False): 18 | self.order_id = order_id 19 | self.origin_network = origin_network 20 | self.set_order_tx_hash = set_order_tx_hash 21 | self.recipient_address = recipient_address 22 | self.amount = amount 23 | self.fee = fee 24 | self.block_number = block_number 25 | self.is_used = is_used 26 | 27 | @staticmethod 28 | async def from_starknet(event): 29 | """ 30 | event = { 31 | "tx_hash": "0x 32 | "block_number": 0, 33 | "data": [0, 0, 0, 0, 0, 0, 0] 34 | } 35 | """ 36 | order_id = SetOrderEvent.parse_u256_from_double_u128(event.data[0], event.data[1]) 37 | # event.tx_hash is a string. We need to store it as bytes. 38 | # Complete the hash with zeroes because fromhex needs pair number of elements 39 | set_order_tx_hash = bytes.fromhex(event.tx_hash.replace("0x", "").zfill(64)) 40 | recipient_address = hex(event.data[2]) 41 | amount = SetOrderEvent.parse_u256_from_double_u128(event.data[3], event.data[4]) 42 | fee = SetOrderEvent.parse_u256_from_double_u128(event.data[5], event.data[6]) 43 | is_used = await asyncio.to_thread(ethereum.get_is_used_order, order_id, recipient_address, amount, Network.STARKNET.value) 44 | return SetOrderEvent( 45 | order_id=order_id, 46 | origin_network=Network.STARKNET, 47 | set_order_tx_hash=set_order_tx_hash, 48 | recipient_address=recipient_address, 49 | amount=amount, 50 | fee=fee, 51 | block_number=event.block_number, 52 | is_used=is_used 53 | ) 54 | 55 | @staticmethod 56 | async def from_zksync(log): 57 | """ 58 | log = { 59 | "transactionHash": "0x", 60 | "blockNumber": 0, 61 | "args": { 62 | "order_id": 0, 63 | "recipient_address": "0x", 64 | "amount": 0, 65 | "fee": 0 66 | } 67 | } 68 | transactionHash: HexBytes 69 | recipient_address: str 70 | """ 71 | order_id = log.args.order_id 72 | set_order_tx_hash = log.transactionHash 73 | recipient_address = log.args.recipient_address 74 | amount = log.args.amount 75 | fee = log.args.fee 76 | is_used = await asyncio.to_thread(ethereum.get_is_used_order, order_id, recipient_address, amount, Network.ZKSYNC.value) # TODO change to new contract signature 77 | return SetOrderEvent( 78 | order_id=order_id, 79 | origin_network=Network.ZKSYNC, 80 | set_order_tx_hash=set_order_tx_hash, 81 | recipient_address=recipient_address, 82 | amount=amount, 83 | fee=fee, 84 | block_number=log.blockNumber, 85 | is_used=is_used 86 | ) 87 | 88 | @staticmethod 89 | def parse_u256_from_double_u128(low, high) -> int: 90 | return high << 128 | low 91 | 92 | def __str__(self): 93 | return f"Order: {self.order_id} - Origin: {self.origin_network} - Amount: {self.amount} - Fee: {self.fee} - " \ 94 | f"Recipient: {self.recipient_address} - Is used: {self.is_used} - Block: {self.block_number}" 95 | 96 | def __repr__(self): 97 | return self.__str__() 98 | -------------------------------------------------------------------------------- /mm-bot/src/persistence/block_dao.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from models.block import Block 4 | from models.network import Network 5 | 6 | 7 | class BlockDao: 8 | def __init__(self, db: Session): 9 | self.db = db 10 | 11 | def get_latest_block(self, network: Network) -> int: 12 | return (self.db.query(Block) 13 | .filter(Block.network == network) 14 | .order_by(Block.id.desc()) 15 | .first() 16 | .latest_block) 17 | 18 | def update_latest_block(self, latest_block: int, network: Network): 19 | if self.get_latest_block(network) == latest_block: 20 | return 21 | block = Block(latest_block=latest_block, network=network) 22 | self.db.add(block) 23 | self.db.commit() 24 | return block 25 | -------------------------------------------------------------------------------- /mm-bot/src/persistence/error_dao.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from models.error import Error 4 | 5 | 6 | class ErrorDao: 7 | def __init__(self, db: Session): 8 | self.db = db 9 | 10 | def create_error(self, error: Error) -> Error: 11 | self.db.add(error) 12 | self.db.commit() 13 | return error 14 | -------------------------------------------------------------------------------- /mm-bot/src/persistence/order_dao.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from sqlalchemy import and_ 4 | from sqlalchemy.orm import Session 5 | 6 | from models.order import Order 7 | from models.order_status import OrderStatus 8 | 9 | 10 | class OrderDao: 11 | def __init__(self, db: Session): 12 | self.db = db 13 | 14 | def create_order(self, order: Order) -> Order: 15 | if self.get_order(order.order_id, order.origin_network): 16 | raise Exception(f"Order [{order.origin_network} ~ {order.order_id}] already exists") 17 | self.db.add(order) 18 | self.db.commit() 19 | return order 20 | 21 | def get_order(self, order_id, origin_network) -> Order | None: 22 | return (self.db.query(Order) 23 | .filter(and_(Order.order_id == order_id, 24 | Order.origin_network == origin_network)) 25 | .first()) 26 | 27 | def get_orders(self, criteria) -> list[Type[Order]]: 28 | return (self.db.query(Order) 29 | .filter(criteria) 30 | .all()) 31 | 32 | def get_incomplete_orders(self) -> list[Type[Order]]: 33 | """ 34 | An order is incomplete if it's not completed or not DROPPED and not failed 35 | """ 36 | criteria = and_(Order.status != OrderStatus.COMPLETED, 37 | Order.status != OrderStatus.DROPPED, 38 | Order.failed == False) 39 | return self.get_orders(criteria) 40 | 41 | def get_failed_orders(self) -> list[Type[Order]]: 42 | criteria = Order.failed 43 | return self.get_orders(criteria) 44 | 45 | def already_exists(self, order_id, origin_network) -> bool: 46 | return self.get_order(order_id, origin_network) is not None 47 | 48 | def update_order(self, order: Order) -> Order: 49 | self.db.commit() 50 | return order 51 | -------------------------------------------------------------------------------- /mm-bot/src/services/decorators/use_fallback.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | def use_fallback(rpc_nodes, logger, error_message="Failed"): 5 | def decorator(func): 6 | @wraps(func) 7 | def wrapper(*args, **kwargs): 8 | exceptions = [] 9 | for rpc_node in rpc_nodes: 10 | try: 11 | return func(*args, **kwargs, rpc_node=rpc_node) 12 | except Exception as exception: 13 | logger.warning(f"[-] {error_message}: {exception}") 14 | exceptions.append(exception) 15 | logger.error(f"[-] {error_message} from all nodes") 16 | raise Exception(f"{error_message} from all nodes: [{', '.join(str(e) for e in exceptions)}]") 17 | 18 | return wrapper 19 | 20 | return decorator 21 | 22 | 23 | def use_async_fallback(rpc_nodes, logger, error_message="Failed"): 24 | def decorator(func): 25 | @wraps(func) 26 | async def wrapper(*args, **kwargs): 27 | exceptions = [] 28 | for rpc_node in rpc_nodes: 29 | try: 30 | return await func(*args, **kwargs, rpc_node=rpc_node) 31 | except Exception as exception: 32 | logger.warning(f"[-] {error_message}: {exception}") 33 | exceptions.append(exception) 34 | logger.error(f"[-] {error_message} from all nodes") 35 | raise Exception(f"{error_message} from all nodes: [{', '.join(str(e) for e in exceptions)}]") 36 | 37 | return wrapper 38 | 39 | return decorator 40 | 41 | -------------------------------------------------------------------------------- /mm-bot/src/services/executors/order_executor.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from models.order import Order 5 | from models.order_status import OrderStatus 6 | from services.fee_calculators.fee_calculator import FeeCalculator 7 | from services.order_service import OrderService 8 | from services.payment_claimer.payment_claimer import PaymentClaimer 9 | from services.senders.ethereum_sender import EthereumSender 10 | 11 | 12 | class OrderExecutor: 13 | 14 | def __init__(self, order_service: OrderService, 15 | ethereum_sender: EthereumSender, 16 | payment_claimer: PaymentClaimer, 17 | fee_calculator: FeeCalculator, 18 | eth_lock: asyncio.Lock, herodotus_semaphore: asyncio.Semaphore, 19 | max_eth_transfer_wei: int): 20 | self.logger = logging.getLogger(__name__) 21 | self.order_service: OrderService = order_service 22 | self.sender: EthereumSender = ethereum_sender 23 | self.payment_claimer: PaymentClaimer = payment_claimer 24 | self.fee_calculator: FeeCalculator = fee_calculator 25 | self.eth_lock: asyncio.Lock = eth_lock 26 | self.herodotus_semaphore: asyncio.Semaphore = herodotus_semaphore 27 | self.MAX_ETH_TRANSFER_WEI: int = max_eth_transfer_wei 28 | 29 | def execute(self, order: Order): 30 | """ 31 | Execute the order 32 | """ 33 | asyncio.create_task(self.process_order(order), name=order.summary()) 34 | 35 | async def process_order(self, order: Order): 36 | """ 37 | Process the order 38 | """ 39 | try: 40 | self.logger.info(f"[+] Processing order: {order}") 41 | # 1. Check if the order fee is enough 42 | if order.status is OrderStatus.PENDING: 43 | estimated_fee = await self.fee_calculator.estimate_overall_fee(order) 44 | if order.get_int_fee() < estimated_fee: 45 | self.logger.error(f"[-] Order fee is too low: {order.get_int_fee()} < {estimated_fee}") 46 | self.order_service.set_order_dropped(order) 47 | return 48 | self.order_service.set_order_processing(order) 49 | 50 | # 1.5. Check if order amount is too high 51 | if order.amount > self.MAX_ETH_TRANSFER_WEI: 52 | self.logger.error(f"[-] Order amount is too high: {order.amount}") 53 | self.order_service.set_order_dropped(order) 54 | return 55 | 56 | # 2. Transfer eth on ethereum 57 | # (bridging is complete for the user) 58 | if order.status in [OrderStatus.PROCESSING, OrderStatus.TRANSFERRING]: 59 | async with self.eth_lock: 60 | if order.status is OrderStatus.PROCESSING: 61 | await self.sender.transfer(order) 62 | 63 | # 2.5. Wait for transfer 64 | if order.status is OrderStatus.TRANSFERRING: 65 | await self.sender.wait_transfer(order) 66 | 67 | if order.status in [OrderStatus.FULFILLED, OrderStatus.PROVING]: 68 | # async with self.herodotus_semaphore if using_herodotus() else eth_lock: 69 | # TODO implement using_herodotus 70 | async with self.eth_lock: 71 | # 3. Call payment claimer to prove 72 | if order.status is OrderStatus.FULFILLED: 73 | await self.payment_claimer.send_payment_claim(order, self.order_service) 74 | 75 | # 4. Poll herodotus to check task status 76 | if order.status is OrderStatus.PROVING: 77 | await self.payment_claimer.wait_for_payment_claim(order, self.order_service) 78 | 79 | # 5. Claim payment eth from starknet 80 | # (bridging is complete for the mm) 81 | if order.status is OrderStatus.PROVED: 82 | await self.payment_claimer.close_payment_claim(order, self.order_service) 83 | 84 | if order.status is OrderStatus.COMPLETED: 85 | self.logger.info(f"[+] Order {order.order_id} completed") 86 | except Exception as e: 87 | self.logger.error(f"[-] Error: {e}") 88 | self.order_service.set_order_failed(order, str(e)) 89 | -------------------------------------------------------------------------------- /mm-bot/src/services/fee_calculators/fee_calculator.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from abc import ABC, abstractmethod 4 | from web3 import Web3 5 | 6 | from models.order import Order 7 | from services.ethereum import create_transfer, estimate_transaction_fee, get_gas_price 8 | 9 | 10 | class FeeCalculator(ABC): 11 | 12 | def __init__(self): 13 | self.logger = logging.getLogger(__name__) 14 | 15 | async def estimate_overall_fee(self, order: Order) -> int: 16 | """ 17 | Operational cost per order done by the market maker. 18 | This includes: 19 | calling the transfer (from PaymentRegistry) + 20 | claimPayment (from PaymentRegistry) + 21 | msg fee paid to Starknet (when calling claim_payment) 22 | """ 23 | transfer_fee = await asyncio.to_thread(self.estimate_transfer_fee, order) 24 | message_fee = await self.estimate_message_fee(order) 25 | claim_payment_fee = self.estimate_claim_payment_fee() 26 | overall_fee = transfer_fee + message_fee + claim_payment_fee 27 | self.logger.info(f"Overall fee: {overall_fee} [received: {order.get_int_fee()}]") 28 | return overall_fee 29 | 30 | def estimate_transfer_fee(self, order: Order) -> int: 31 | dst_addr_bytes = int(order.recipient_address, 0) 32 | deposit_id = Web3.to_int(order.order_id) 33 | amount = Web3.to_int(order.get_int_amount()) 34 | chain_id = order.origin_network.value 35 | 36 | unsent_tx, signed_tx = create_transfer(deposit_id, dst_addr_bytes, amount, chain_id) 37 | 38 | return estimate_transaction_fee(unsent_tx) 39 | 40 | def estimate_claim_payment_fee(self) -> int: 41 | """ 42 | Due to the deposit does not exist on ethereum at this point, 43 | we cannot estimate the gas fee of the claim payment transaction 44 | So we will use fixed values for the gas 45 | """ 46 | eth_claim_payment_gas = 86139 # TODO this is a fixed value, if the contract changes, this should be updated 47 | return eth_claim_payment_gas * get_gas_price() 48 | 49 | @abstractmethod 50 | async def estimate_message_fee(self, order: Order) -> int: 51 | pass 52 | 53 | -------------------------------------------------------------------------------- /mm-bot/src/services/fee_calculators/starknet_fee_calculator.py: -------------------------------------------------------------------------------- 1 | from starknet_py.hash.selector import get_selector_from_name 2 | 3 | from config import constants 4 | from models.order import Order 5 | from services import starknet 6 | from services.fee_calculators.fee_calculator import FeeCalculator 7 | 8 | 9 | class StarknetFeeCalculator(FeeCalculator): 10 | 11 | async def estimate_message_fee(self, order: Order) -> int: 12 | """ 13 | Estimate the message fee for the claim payment transaction 14 | on Starknet. It is used as value in the transaction. 15 | """ 16 | from_address = constants.ETHEREUM_CONTRACT_ADDRESS 17 | to_address = constants.STARKNET_CONTRACT_ADDRESS 18 | entry_point_selector = hex(get_selector_from_name("claim_payment")) 19 | payload = [ 20 | hex(order.order_id), 21 | "0x0", 22 | order.recipient_address, 23 | hex(order.get_int_amount()), 24 | "0x0" 25 | ] 26 | return await starknet.estimate_message_fee(from_address, to_address, entry_point_selector, payload) 27 | -------------------------------------------------------------------------------- /mm-bot/src/services/fee_calculators/zksync_fee_calculator.py: -------------------------------------------------------------------------------- 1 | from zksync2.core.utils import DEPOSIT_GAS_PER_PUBDATA_LIMIT 2 | 3 | from models.order import Order 4 | from services.fee_calculators.fee_calculator import FeeCalculator 5 | 6 | 7 | class ZksyncFeeCalculator(FeeCalculator): 8 | 9 | async def estimate_overall_fee(self, order: Order) -> int: 10 | return 0 # TODO: implement 11 | 12 | async def estimate_message_fee(self, order: Order) -> int: 13 | """ 14 | This fee is used as value in claim payment tx to ethereum 15 | """ 16 | return 10_000_000_000_000_000 # 0.01 ETH TODO implement estimation 17 | 18 | async def estimate_gas_limit(self, order: Order) -> int: 19 | return 300_000 20 | 21 | def estimate_gas_per_pub_data_byte_limit(self, order: Order) -> int: 22 | return DEPOSIT_GAS_PER_PUBDATA_LIMIT # This is a constant from zksync2.core.utils 23 | -------------------------------------------------------------------------------- /mm-bot/src/services/herodotus.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import requests 4 | import time 5 | from config import constants 6 | 7 | reqs = [] 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | async def herodotus_prove(block, order_id, slot) -> str: 12 | headers = { 13 | "Content-Type": "application/json", 14 | } 15 | 16 | request_data = { 17 | "destinationChainId": constants.HERODOTUS_DESTINATION_CHAIN, 18 | "fee": "0", 19 | "data": { 20 | constants.HERODOTUS_ORIGIN_CHAIN: { 21 | "block:{}".format(block): { 22 | "accounts": { 23 | constants.ETHEREUM_CONTRACT_ADDRESS: { 24 | "slots": [ 25 | slot.hex(), 26 | hex(int(slot.hex(), 16) + 1), 27 | ] 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | retries = 0 36 | while retries <= constants.MAX_RETRIES: 37 | try: 38 | response = requests.post( 39 | 'https://api.herodotus.cloud/submit-batch-query?apiKey={}'.format(constants.HERODOTUS_API_KEY), 40 | headers=headers, 41 | json=request_data, 42 | ) 43 | response.raise_for_status() 44 | 45 | return response.json()['internalId'] 46 | except requests.exceptions.RequestException as err: 47 | logger.error(err) 48 | retries += 1 49 | if retries == constants.MAX_RETRIES: 50 | raise err 51 | await asyncio.sleep(constants.RETRIES_DELAY) 52 | 53 | 54 | def herodotus_status(task_id) -> str: 55 | try: 56 | response = requests.get( 57 | 'https://api.herodotus.cloud/batch-query-status?apiKey={}&batchQueryId={}'.format( 58 | constants.HERODOTUS_API_KEY, task_id), 59 | ) 60 | response.raise_for_status() 61 | 62 | return response.json()['queryStatus'] 63 | except requests.exceptions.RequestException as err: 64 | logger.error(err) 65 | raise err 66 | 67 | 68 | async def herodotus_poll_status(task_id) -> bool: 69 | # instead of returning a bool we can store it in a mapping 70 | retries = 0 71 | start_time = time.time() 72 | while retries <= constants.MAX_RETRIES: 73 | try: 74 | status = herodotus_status(task_id) 75 | logger.info(f"[+] Herodotus status: {status}") 76 | if status == 'DONE': 77 | end_time = time.time() 78 | request_time = end_time - start_time 79 | logger.info(f"[+] Herodotus request time: {request_time}") 80 | reqs.append(request_time) 81 | logger.info(f"[+] Herodotus average request time (total): {sum(reqs) / len(reqs)}") 82 | return True 83 | retries += 1 84 | except requests.exceptions.RequestException as err: 85 | logger.error(err) 86 | retries += 1 87 | if retries == constants.MAX_RETRIES: 88 | raise err 89 | await asyncio.sleep(constants.RETRIES_DELAY) 90 | return False 91 | -------------------------------------------------------------------------------- /mm-bot/src/services/indexers/order_indexer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | 4 | from models.order import Order 5 | from models.set_order_event import SetOrderEvent 6 | from services.order_service import OrderService 7 | 8 | 9 | class OrderIndexer(ABC): 10 | 11 | def __init__(self, order_service: OrderService): 12 | self.logger = logging.getLogger(__name__) 13 | self.order_service = order_service 14 | 15 | @abstractmethod 16 | async def get_orders(self, from_block, to_block) -> list[Order]: 17 | """ 18 | Get orders from the escrow 19 | """ 20 | pass 21 | 22 | @abstractmethod 23 | async def get_new_orders(self) -> list[Order]: 24 | """ 25 | Get new orders from the escrow 26 | Depending on the indexer implementation, it will use correct 27 | parameters to set from_block and to_block 28 | """ 29 | pass 30 | 31 | def save_orders(self, set_order_events: list[SetOrderEvent]) -> list[Order]: 32 | """ 33 | Save orders to the database if not already saved 34 | This is a common implementation for all indexers 35 | """ 36 | orders: list[Order] = [] 37 | for set_order_event in set_order_events: 38 | if self.order_service.already_exists(set_order_event.order_id, set_order_event.origin_network): 39 | self.logger.debug(f"[+] Order already processed: [{set_order_event.origin_network} ~ {set_order_event.order_id}]") 40 | continue 41 | 42 | try: 43 | order = Order.from_set_order_event(set_order_event) 44 | order = self.order_service.create_order(order) 45 | orders.append(order) 46 | self.logger.debug(f"[+] New order: {order}") 47 | except Exception as e: 48 | self.logger.error(f"[-] Error: {e}") 49 | continue 50 | return orders 51 | 52 | -------------------------------------------------------------------------------- /mm-bot/src/services/indexers/starknet_order_indexer.py: -------------------------------------------------------------------------------- 1 | from models.order import Order 2 | from services import starknet 3 | from services.indexers.order_indexer import OrderIndexer 4 | from services.starknet import SetOrderEvent 5 | 6 | 7 | class StarknetOrderIndexer(OrderIndexer): 8 | 9 | async def get_orders(self, from_block, to_block) -> list[Order]: 10 | """ 11 | Get orders from the escrow 12 | """ 13 | set_order_events: list[SetOrderEvent] = await starknet.get_order_events(from_block, to_block) 14 | return self.save_orders(set_order_events) 15 | 16 | async def get_new_orders(self) -> list[Order]: 17 | """ 18 | Get new orders from the escrow 19 | On Starknet, we use pending as from_block and to_block 20 | """ 21 | return await self.get_orders("pending", "pending") 22 | -------------------------------------------------------------------------------- /mm-bot/src/services/indexers/zksync_order_indexer.py: -------------------------------------------------------------------------------- 1 | from models.order import Order 2 | from models.set_order_event import SetOrderEvent 3 | from services import zksync 4 | from services.indexers.order_indexer import OrderIndexer 5 | 6 | 7 | class ZksyncOrderIndexer(OrderIndexer): 8 | 9 | async def get_orders(self, from_block, to_block) -> list[Order]: 10 | """ 11 | Get orders from the escrow 12 | """ 13 | set_order_events: list[SetOrderEvent] = await zksync.get_set_order_events(from_block, to_block) 14 | return self.save_orders(set_order_events) 15 | 16 | async def get_new_orders(self) -> list[Order]: 17 | """ 18 | Get new orders from the escrow 19 | On ZKSync we index new orders from the last 10 blocks 20 | """ 21 | block_number = await zksync.get_latest_block() 22 | from_block = max(block_number - 10, 1) 23 | to_block = block_number 24 | return await self.get_orders(from_block, to_block) 25 | -------------------------------------------------------------------------------- /mm-bot/src/services/mm_full_node_client.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, List, Union, cast 3 | 4 | from marshmallow import fields, post_load 5 | from starknet_py.net.client_models import Hash, Tag, EventsChunk, Event 6 | from starknet_py.net.full_node_client import FullNodeClient, _get_raw_block_identifier, _to_rpc_felt 7 | from starknet_py.net.schemas.rpc import EventSchema, EventsChunkSchema 8 | 9 | EXCLUDE = "exclude" 10 | 11 | 12 | @dataclass 13 | class MmEvent(Event): 14 | tx_hash: str 15 | block_number: int 16 | 17 | 18 | class MmEventSchema(EventSchema): 19 | tx_hash = fields.String(data_key="transaction_hash", required=True) 20 | block_number = fields.Integer(data_key="block_number", load_default=None) 21 | 22 | @post_load 23 | def make_dataclass(self, data, **kwargs) -> Event: 24 | return MmEvent(**data) 25 | 26 | 27 | @dataclass 28 | class MmEventsChunk(EventsChunk): 29 | events: List[MmEvent] 30 | continuation_token: Optional[str] 31 | 32 | 33 | class MmEventsChunkSchema(EventsChunkSchema): 34 | events = fields.List( 35 | fields.Nested(MmEventSchema(unknown=EXCLUDE)), 36 | data_key="events", 37 | required=True, 38 | ) 39 | continuation_token = fields.String(data_key="continuation_token", load_default=None) 40 | 41 | @post_load 42 | def make_dataclass(self, data, **kwargs): 43 | return EventsChunk(**data) 44 | 45 | 46 | class MmFullNodeClient(FullNodeClient): 47 | 48 | async def get_events(self, address: Optional[Hash] = None, keys: Optional[List[List[Hash]]] = None, *, 49 | from_block_number: Optional[Union[int, Tag]] = None, 50 | from_block_hash: Optional[Union[Hash, Tag]] = None, 51 | to_block_number: Optional[Union[int, Tag]] = None, 52 | to_block_hash: Optional[Union[Hash, Tag]] = None, follow_continuation_token: bool = False, 53 | continuation_token: Optional[str] = None, chunk_size: int = 1) -> MmEventsChunk: 54 | if chunk_size <= 0: 55 | raise ValueError("Argument chunk_size must be greater than 0.") 56 | 57 | if keys is None: 58 | keys = [] 59 | if address is not None: 60 | address = _to_rpc_felt(address) 61 | if from_block_number is None and from_block_hash is None: 62 | from_block_number = 0 63 | 64 | from_block = _get_raw_block_identifier(from_block_hash, from_block_number) 65 | to_block = _get_raw_block_identifier(to_block_hash, to_block_number) 66 | keys = [[_to_rpc_felt(key) for key in inner_list] for inner_list in keys] 67 | 68 | events_list = [] 69 | while True: 70 | events, continuation_token = await self._get_events_chunk( 71 | from_block=from_block, 72 | to_block=to_block, 73 | address=address, 74 | keys=keys, 75 | chunk_size=chunk_size, 76 | continuation_token=continuation_token, 77 | ) 78 | events_list.extend(events) 79 | if not follow_continuation_token or continuation_token is None: 80 | break 81 | 82 | events_response = cast( 83 | MmEventsChunk, 84 | MmEventsChunkSchema().load( 85 | {"events": events_list, "continuation_token": continuation_token} 86 | ), 87 | ) 88 | 89 | return events_response 90 | -------------------------------------------------------------------------------- /mm-bot/src/services/order_service.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from models.error import Error 4 | from models.order import Order 5 | from models.order_status import OrderStatus 6 | from persistence.error_dao import ErrorDao 7 | from persistence.order_dao import OrderDao 8 | 9 | 10 | class OrderService: 11 | def __init__(self, order_dao: OrderDao, error_dao: ErrorDao): 12 | self.order_dao = order_dao 13 | self.error_dao = error_dao 14 | 15 | def create_order(self, order: Order) -> Order: 16 | """ 17 | Create an order in the database 18 | 19 | :param order: the order to create 20 | """ 21 | return self.order_dao.create_order(order) 22 | 23 | def get_order(self, order_id, origin_network) -> Order | None: 24 | """ 25 | Get an order from the database with the given order_id 26 | 27 | :param order_id: the order_id of the order to get 28 | :param origin_network: the origin_network of the order to get 29 | """ 30 | return self.order_dao.get_order(order_id, origin_network) 31 | 32 | def get_incomplete_orders(self): 33 | """ 34 | Get all orders that are not completed from the database 35 | An order is incomplete if it's not completed and not failed 36 | """ 37 | return self.order_dao.get_incomplete_orders() 38 | 39 | def get_failed_orders(self): 40 | """ 41 | Get all orders that are failed from the database 42 | """ 43 | return self.order_dao.get_failed_orders() 44 | 45 | def already_exists(self, order_id, origin_network) -> bool: 46 | """ 47 | Check if an order with the given order_id already exists in the database 48 | 49 | :param order_id: the order_id of the order to check 50 | :param origin_network: the origin_network of the order to check 51 | """ 52 | return self.order_dao.already_exists(order_id, origin_network) 53 | 54 | def set_order_processing(self, order: Order) -> Order: 55 | """ 56 | Set the order status to "PROCESSING" 57 | 58 | :param order: the order to update 59 | """ 60 | order.status = OrderStatus.PROCESSING.name 61 | return self.order_dao.update_order(order) 62 | 63 | def set_order_transferring(self, order: Order, tx_hash) -> Order: 64 | """ 65 | Set the order status to "TRANSFERRING" 66 | 67 | :param order: the order to update 68 | :param tx_hash: the tx_hash of the ethereum transfer transaction 69 | """ 70 | order.transfer_tx_hash = tx_hash 71 | order.status = OrderStatus.TRANSFERRING.name 72 | return self.order_dao.update_order(order) 73 | 74 | def set_order_fulfilled(self, order: Order) -> Order: 75 | """ 76 | Set the order status to "FULFILLED" 77 | 78 | :param order: the order to update 79 | """ 80 | order.status = OrderStatus.FULFILLED.name 81 | order.transferred_at = datetime.now() 82 | return self.order_dao.update_order(order) 83 | 84 | def set_order_proving_herodotus(self, order: Order, task_id, block, slot) -> Order: 85 | """ 86 | Set the order status to "PROVING" 87 | 88 | :param order: the order to update 89 | :param task_id: the task_id of the herodotus task TODO define better 90 | :param block: the block of the herodotus task TODO define better 91 | :param slot: the slot of the herodotus task TODO define better 92 | """ 93 | order.herodotus_task_id = task_id 94 | order.herodotus_block = block 95 | order.herodotus_slot = slot 96 | order.status = OrderStatus.PROVING.name 97 | return self.order_dao.update_order(order) 98 | 99 | def set_order_proving_ethereum(self, order: Order, tx_hash) -> Order: 100 | """ 101 | Set the order status to "PROVING" 102 | 103 | :param order: the order to update 104 | :param tx_hash: the tx_hash of the ethereum claim_payment transaction 105 | """ 106 | order.claim_tx_hash = tx_hash 107 | order.status = OrderStatus.PROVING.name 108 | return self.order_dao.update_order(order) 109 | 110 | def set_order_proved(self, order: Order) -> Order: 111 | """ 112 | Set the order status to "PROVED" 113 | 114 | :param order: the order to update 115 | """ 116 | order.status = OrderStatus.PROVED.name 117 | return self.order_dao.update_order(order) 118 | 119 | def set_order_completed(self, order: Order) -> Order: 120 | """ 121 | Set the order status to "COMPLETED" 122 | """ 123 | order.status = OrderStatus.COMPLETED.name 124 | order.completed_at = datetime.now() 125 | return self.order_dao.update_order(order) 126 | 127 | def set_order_dropped(self, order: Order) -> Order: 128 | """ 129 | Set the order status to "DROPPED" 130 | """ 131 | order.status = OrderStatus.DROPPED.name 132 | return self.order_dao.update_order(order) 133 | 134 | def set_order_failed(self, order: Order, error_message: str) -> Order: 135 | """ 136 | Set the order failed to True and create an error in the database. 137 | 138 | The order status is not changed, so the order can be retried 139 | 140 | :param order: the order to update 141 | :param error_message: the error message to store in the database 142 | """ 143 | order = self.set_failed(order, True) 144 | error = Error(order_id=order.order_id, origin_network=order.origin_network, message=error_message) 145 | self.error_dao.create_error(error) 146 | return order 147 | 148 | def reset_failed_order(self, order: Order) -> Order: 149 | """ 150 | Reset the failed order to False, so it can be retried 151 | 152 | :param order: the order to update 153 | """ 154 | return self.set_failed(order, False) 155 | 156 | def set_failed(self, order: Order, failed: bool) -> Order: 157 | """ 158 | Set the order failed to the given value 159 | 160 | :param order: the order to update 161 | :param failed: the value to set failed to 162 | """ 163 | order.failed = failed 164 | return self.order_dao.update_order(order) 165 | -------------------------------------------------------------------------------- /mm-bot/src/services/payment_claimer/ethereum_2_zksync_payment_claimer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from config import constants 4 | from services import ethereum 5 | from services.fee_calculators.zksync_fee_calculator import ZksyncFeeCalculator 6 | from services.payment_claimer.payment_claimer import PaymentClaimer 7 | 8 | 9 | class Ethereum2ZksyncPaymentClaimer(PaymentClaimer): 10 | 11 | def __init__(self, fee_calculator: ZksyncFeeCalculator): 12 | super().__init__() 13 | self.fee_calculator: ZksyncFeeCalculator = fee_calculator 14 | 15 | async def send_payment_claim(self, order, order_service): 16 | """ 17 | Makes the payment claim on ethereum 18 | Sends a 'claimPaymentZKSync' transaction to ethereum smart contract 19 | """ 20 | self.logger.info(f"[+] Sending payment claim tx to ethereum") 21 | order_id, recipient_address, amount = order.order_id, order.recipient_address, order.get_int_amount() 22 | value = await self.fee_calculator.estimate_message_fee(order) 23 | gas_limit = await self.fee_calculator.estimate_gas_limit(order) 24 | gas_per_pub_data_byte_limit = self.fee_calculator.estimate_gas_per_pub_data_byte_limit(order) 25 | tx_hash = await asyncio.to_thread(ethereum.claim_payment_zksync, 26 | order_id, recipient_address, amount, value, gas_limit, gas_per_pub_data_byte_limit) 27 | order_service.set_order_proving_ethereum(order, tx_hash) 28 | self.logger.info(f"[+] Payment claim tx hash: {tx_hash.hex()}") 29 | 30 | async def wait_for_payment_claim(self, order, order_service): 31 | """ 32 | Waits for the payment claim transaction to be confirmed on ethereum 33 | """ 34 | await asyncio.to_thread(ethereum.wait_for_transaction_receipt, order.claim_tx_hash) 35 | order_service.set_order_proved(order) 36 | self.logger.info(f"[+] Claim payment tx confirmed") 37 | 38 | async def close_payment_claim(self, order, order_service): 39 | """ 40 | Closes the payment claim setting the order as completed 41 | # TODO it should be checking the status in the escrow instead of the payment registry 42 | # TODO this is why this code is repeated in the other payment claimers 43 | """ 44 | retries = 0 45 | while retries < constants.MAX_RETRIES: 46 | if await asyncio.to_thread(ethereum.get_is_used_order, 47 | order.order_id, order.recipient_address, order.get_int_amount(), order.origin_network.value): 48 | break 49 | retries += 1 50 | await asyncio.sleep(10) 51 | self.logger.info(f"[+] Claim payment complete") 52 | order_service.set_order_completed(order) 53 | -------------------------------------------------------------------------------- /mm-bot/src/services/payment_claimer/ethereum_payment_claimer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from config import constants 4 | from models.order import Order 5 | from services import ethereum 6 | from services.fee_calculators.starknet_fee_calculator import StarknetFeeCalculator 7 | from services.order_service import OrderService 8 | from services.payment_claimer.payment_claimer import PaymentClaimer 9 | 10 | 11 | class EthereumPaymentClaimer(PaymentClaimer): 12 | 13 | def __init__(self, fee_calculator: StarknetFeeCalculator): 14 | super().__init__() 15 | self.fee_calculator: StarknetFeeCalculator = fee_calculator 16 | 17 | async def send_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 18 | """ 19 | Makes the payment claim on ethereum 20 | Sends a 'claimPayment' transaction to ethereum smart contract 21 | """ 22 | self.logger.info(f"[+] Sending payment claim tx to ethereum") 23 | order_id, recipient_address, amount = order.order_id, order.recipient_address, order.get_int_amount() 24 | value = await self.fee_calculator.estimate_message_fee(order) 25 | tx_hash = await asyncio.to_thread(ethereum.claim_payment, 26 | order_id, recipient_address, amount, value) 27 | order_service.set_order_proving_ethereum(order, tx_hash) 28 | self.logger.info(f"[+] Payment claim tx hash: {tx_hash.hex()}") 29 | 30 | async def wait_for_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 31 | """ 32 | Waits for the payment claim transaction to be confirmed on ethereum 33 | """ 34 | await asyncio.to_thread(ethereum.wait_for_transaction_receipt, order.claim_tx_hash) 35 | order_service.set_order_proved(order) 36 | self.logger.info(f"[+] Claim payment tx confirmed") 37 | 38 | async def close_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 39 | """ 40 | Closes the payment claim setting the order as completed 41 | """ 42 | retries = 0 43 | while retries < constants.MAX_RETRIES: 44 | if await asyncio.to_thread(ethereum.get_is_used_order, 45 | order.order_id, order.recipient_address, order.get_int_amount(), order.origin_network.value): 46 | break 47 | retries += 1 48 | await asyncio.sleep(10) 49 | self.logger.info(f"[+] Claim payment complete") 50 | order_service.set_order_completed(order) 51 | -------------------------------------------------------------------------------- /mm-bot/src/services/payment_claimer/herodotus_payment_claimer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from web3 import Web3 4 | 5 | from models.order import Order 6 | from services import ethereum, herodotus, starknet 7 | from services.order_service import OrderService 8 | from services.payment_claimer.payment_claimer import PaymentClaimer 9 | 10 | 11 | class HerodotusPaymentClaimer(PaymentClaimer): 12 | 13 | async def send_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 14 | """ 15 | Initialize the proof on herodotus 16 | """ 17 | block = ethereum.get_latest_block() 18 | index = Web3.solidity_keccak(['uint256', 'uint256', 'uint256'], 19 | [order.order_id, int(order.recipient_address, 0), order.get_int_amount()]) 20 | slot = Web3.solidity_keccak(['uint256', 'uint256'], [int(index.hex(), 0), 0]) 21 | self.logger.debug(f"[+] Index: {index.hex()}") 22 | self.logger.debug(f"[+] Slot: {slot.hex()}") 23 | self.logger.debug(f"[+] Proving block {block}") 24 | task_id = await herodotus.herodotus_prove(block, order.order_id, slot) 25 | order_service.set_order_proving_herodotus(order, task_id, block, slot) 26 | self.logger.info(f"[+] Block being proved with task id: {task_id}") 27 | 28 | async def wait_for_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 29 | """ 30 | Wait for the proof to be done on herodotus 31 | """ 32 | self.logger.info(f"[+] Polling herodotus for task status") 33 | # avoid weird case where herodotus insta says done 34 | await asyncio.sleep(10) 35 | completed = await herodotus.herodotus_poll_status(order.herodotus_task_id) 36 | if completed: 37 | order_service.set_order_proved(order) 38 | self.logger.info(f"[+] Task completed") 39 | 40 | async def close_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 41 | """ 42 | Makes the claim_payment on starknet 43 | """ 44 | self.logger.info(f"[+] Claiming payment eth from starknet") 45 | await starknet.claim_payment(order.order_id, order.herodotus_block, order.herodotus_slot) 46 | order_service.set_order_completed(order) 47 | self.logger.info(f"[+] Claim payment complete") 48 | -------------------------------------------------------------------------------- /mm-bot/src/services/payment_claimer/payment_claimer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | 4 | from models.order import Order 5 | from services.order_service import OrderService 6 | 7 | 8 | class PaymentClaimer(ABC): 9 | 10 | def __init__(self): # TODO add order_service as a parameter 11 | self.logger = logging.getLogger(__name__) 12 | 13 | """ 14 | """ 15 | @abstractmethod 16 | async def send_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 17 | pass 18 | 19 | """ 20 | """ 21 | @abstractmethod 22 | async def wait_for_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 23 | pass 24 | 25 | """ 26 | """ 27 | @abstractmethod 28 | async def close_payment_claim(self, order: Order, order_service: OrderService): # TODO remove order_service 29 | pass 30 | -------------------------------------------------------------------------------- /mm-bot/src/services/processors/accepted_blocks_orders_processor.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from models.network import Network 5 | from persistence.block_dao import BlockDao 6 | from services import starknet 7 | from services.executors.order_executor import OrderExecutor 8 | from services.indexers.order_indexer import OrderIndexer 9 | 10 | 11 | class AcceptedBlocksOrdersProcessor: 12 | 13 | def __init__(self, order_indexer: OrderIndexer, 14 | order_executor: OrderExecutor, 15 | block_dao: BlockDao): 16 | self.logger = logging.getLogger(__name__) 17 | self.order_indexer: OrderIndexer = order_indexer 18 | self.order_executor: OrderExecutor = order_executor 19 | self.block_dao: BlockDao = block_dao 20 | 21 | async def process_orders(self, ): 22 | """ 23 | 24 | """ 25 | try: 26 | latest_block = self.block_dao.get_latest_block(Network.STARKNET) 27 | new_latest_block = await starknet.get_latest_block() 28 | orders = await self.order_indexer.get_orders(latest_block, new_latest_block) 29 | for order in orders: 30 | self.order_executor.execute(order) 31 | self.block_dao.update_latest_block(new_latest_block, Network.STARKNET) 32 | except Exception as e: 33 | self.logger.error(f"[-] Error: {e}") 34 | 35 | def process_orders_job(self): 36 | """ 37 | Process orders job for the scheduler 38 | """ 39 | asyncio.create_task(self.process_orders(), 40 | name="Accepted Blocks") 41 | -------------------------------------------------------------------------------- /mm-bot/src/services/processors/failed_orders_processor.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from models.network import Network 5 | from services.executors.order_executor import OrderExecutor 6 | from services.order_service import OrderService 7 | 8 | 9 | class FailedOrdersProcessor: 10 | 11 | def __init__(self, order_executor: OrderExecutor, 12 | order_service: OrderService): 13 | self.logger = logging.getLogger(__name__) 14 | self.order_executor: OrderExecutor = order_executor 15 | self.order_service: OrderService = order_service 16 | 17 | async def process_orders(self): 18 | """ 19 | Process failed orders stored in the database 20 | """ 21 | try: 22 | orders = self.order_service.get_failed_orders() 23 | for order in orders: 24 | if order.origin_network is Network.STARKNET: 25 | self.order_service.reset_failed_order(order) 26 | self.order_executor.execute(order) 27 | # TODO add support for ZkSync 28 | except Exception as e: 29 | self.logger.error(f"[-] Error: {e}") 30 | 31 | def process_orders_job(self): 32 | """ 33 | Process orders job for the scheduler 34 | """ 35 | asyncio.create_task(self.process_orders(), 36 | name="Failed Orders") 37 | -------------------------------------------------------------------------------- /mm-bot/src/services/processors/orders_processor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from services.executors.order_executor import OrderExecutor 4 | from services.indexers.order_indexer import OrderIndexer 5 | 6 | 7 | class OrdersProcessor: 8 | def __init__(self, order_indexer: OrderIndexer, 9 | order_executor: OrderExecutor): 10 | """ 11 | Each processor will have: 12 | - an indexer to get new orders 13 | - an executor to process orders 14 | """ 15 | self.logger = logging.getLogger(__name__) 16 | self.order_indexer: OrderIndexer = order_indexer 17 | self.order_executor: OrderExecutor = order_executor 18 | 19 | async def process_orders(self): 20 | """ 21 | 22 | """ 23 | orders = await self.order_indexer.get_new_orders() 24 | for order in orders: 25 | self.order_executor.execute(order) 26 | -------------------------------------------------------------------------------- /mm-bot/src/services/senders/ethereum_sender.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from models.order import Order 5 | from services import ethereum 6 | from services.order_service import OrderService 7 | 8 | 9 | class EthereumSender: 10 | 11 | def __init__(self, order_service: OrderService): 12 | self.logger = logging.getLogger(__name__) 13 | self.order_service: OrderService = order_service 14 | 15 | async def transfer(self, order: Order): 16 | """ 17 | 18 | """ 19 | self.logger.info(f"[+] Transferring eth on ethereum") 20 | tx_hash = await asyncio.to_thread(ethereum.transfer, order.order_id, order.recipient_address, 21 | order.get_int_amount(), order.origin_network) 22 | self.order_service.set_order_transferring(order, tx_hash) 23 | self.logger.info(f"[+] Transfer tx hash: {tx_hash.hex()}") 24 | 25 | async def wait_transfer(self, order: Order): 26 | """ 27 | 28 | """ 29 | await asyncio.to_thread(ethereum.wait_for_transaction_receipt, order.transfer_tx_hash) 30 | self.order_service.set_order_fulfilled(order) 31 | self.logger.info(f"[+] Transfer complete") 32 | -------------------------------------------------------------------------------- /mm-bot/src/services/starknet.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Literal 4 | 5 | from starknet_py.common import int_from_bytes 6 | from starknet_py.hash.selector import get_selector_from_name 7 | from starknet_py.net.account.account import Account 8 | from starknet_py.net.client_models import Call 9 | from starknet_py.net.models.chains import StarknetChainId 10 | from starknet_py.net.signer.stark_curve_signer import KeyPair 11 | 12 | from config import constants 13 | from models.set_order_event import SetOrderEvent 14 | from services.decorators.use_fallback import use_async_fallback 15 | from services.mm_full_node_client import MmFullNodeClient 16 | 17 | STARKNET_CHAIN_ID = int_from_bytes(constants.STARKNET_CHAIN_ID.encode("utf-8")) 18 | SET_ORDER_EVENT_KEY = 0x2c75a60b5bdad73ebbf539cc807fccd09875c3cbf3f44041f852cdb96d8acd3 19 | 20 | 21 | class StarknetRpcNode: 22 | def __init__(self, rpc_url, private_key, wallet_address, contract_address, chain_id): 23 | self.full_node_client = MmFullNodeClient(node_url=rpc_url) 24 | key_pair = KeyPair.from_private_key(key=private_key) 25 | self.account = Account( 26 | client=self.full_node_client, 27 | address=wallet_address, 28 | key_pair=key_pair, 29 | chain=chain_id, # ignore this warning TODO change to StarknetChainId when starknet_py adds sepolia 30 | ) 31 | self.contract_address = contract_address 32 | 33 | 34 | main_rpc_node = StarknetRpcNode(constants.STARKNET_RPC, 35 | constants.STARKNET_PRIVATE_KEY, 36 | constants.STARKNET_WALLET_ADDRESS, 37 | constants.STARKNET_CONTRACT_ADDRESS, 38 | STARKNET_CHAIN_ID) 39 | fallback_rpc_node = StarknetRpcNode(constants.STARKNET_FALLBACK_RPC, 40 | constants.STARKNET_PRIVATE_KEY, 41 | constants.STARKNET_WALLET_ADDRESS, 42 | constants.STARKNET_CONTRACT_ADDRESS, 43 | STARKNET_CHAIN_ID) 44 | rpc_nodes = [main_rpc_node, fallback_rpc_node] 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | 49 | @use_async_fallback(rpc_nodes, logger, "Failed to get events") 50 | async def get_starknet_events(from_block_number: Literal["pending", "latest"] | int | None = "pending", 51 | to_block_number: Literal["pending", "latest"] | int | None = "pending", 52 | continuation_token=None, rpc_node=main_rpc_node): 53 | events_response = await rpc_node.full_node_client.get_events( 54 | address=constants.STARKNET_CONTRACT_ADDRESS, 55 | chunk_size=1000, 56 | keys=[[SET_ORDER_EVENT_KEY]], 57 | from_block_number=from_block_number, 58 | to_block_number=to_block_number, 59 | continuation_token=continuation_token 60 | ) 61 | return events_response 62 | 63 | 64 | @use_async_fallback(rpc_nodes, logger, "Failed to set order") 65 | async def get_is_used_order(order_id, rpc_node=main_rpc_node) -> bool: 66 | call = Call( 67 | to_addr=constants.STARKNET_CONTRACT_ADDRESS, 68 | selector=get_selector_from_name("get_order_used"), 69 | calldata=[order_id, 0], 70 | ) 71 | status = await rpc_node.account.client.call_contract(call) 72 | return status[0] 73 | 74 | 75 | async def get_order_events(from_block_number, to_block_number) -> list[SetOrderEvent]: 76 | continuation_token = None 77 | events = [] 78 | order_events = [] 79 | tasks = [] 80 | while True: 81 | events_response = await get_starknet_events(from_block_number, to_block_number, continuation_token) 82 | events.extend(events_response.events) 83 | continuation_token = events_response.continuation_token 84 | if continuation_token is None: 85 | break 86 | 87 | for event in events: 88 | tasks.append(asyncio.create_task(SetOrderEvent.from_starknet(event))) 89 | 90 | for task in tasks: 91 | order = await task 92 | order_events.append(order) 93 | return order_events 94 | 95 | 96 | @use_async_fallback(rpc_nodes, logger, "Failed to get latest block number") 97 | async def get_latest_block(rpc_node=main_rpc_node) -> int: 98 | return await rpc_node.full_node_client.get_block_number() 99 | 100 | 101 | async def claim_payment(order_id, block, slot) -> bool: 102 | slot = slot.hex() 103 | slot_high = int(slot.replace("0x", "")[0:32], 16) 104 | slot_low = int(slot.replace("0x", "")[32:64], 16) 105 | call = Call( 106 | to_addr=int(constants.STARKNET_CONTRACT_ADDRESS, 0), 107 | selector=get_selector_from_name("claim_payment"), 108 | calldata=[order_id, 0, block, 0, slot_low, slot_high] 109 | ) 110 | try: 111 | transaction = await sign_invoke_transaction(call, max_fee=10000000000000) # TODO manage fee better 112 | result = await send_transaction(transaction) 113 | await wait_for_tx(result.transaction_hash) 114 | 115 | logger.info(f"[+] Claim payment from starknet: {hex(result.transaction_hash)}") 116 | return True 117 | except Exception as e: 118 | logger.error(f"[-] Failed to claim payment from starknet: {e}") 119 | return False 120 | 121 | 122 | @use_async_fallback(rpc_nodes, logger, "Failed to sign invoke transaction") 123 | async def sign_invoke_transaction(call: Call, max_fee: int, rpc_node=main_rpc_node): 124 | return await rpc_node.account.sign_invoke_v1_transaction(call, max_fee=max_fee) # TODO migrate to V3 125 | 126 | 127 | @use_async_fallback(rpc_nodes, logger, "Failed to estimate message fee") 128 | async def estimate_message_fee(from_address, to_address, entry_point_selector, payload, rpc_node=main_rpc_node): 129 | fee = await rpc_node.full_node_client.estimate_message_fee(from_address, to_address, entry_point_selector, payload) 130 | return int(fee.overall_fee / 100) 131 | 132 | 133 | @use_async_fallback(rpc_nodes, logger, "Failed to send transaction") 134 | async def send_transaction(transaction, rpc_node=main_rpc_node): 135 | return await rpc_node.account.client.send_transaction(transaction) 136 | 137 | 138 | @use_async_fallback(rpc_nodes, logger, "Failed to wait for tx") 139 | async def wait_for_tx(transaction_hash, rpc_node=main_rpc_node): 140 | await rpc_node.account.client.wait_for_tx(transaction_hash) 141 | 142 | -------------------------------------------------------------------------------- /mm-bot/src/services/zksync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import os 5 | from typing import cast 6 | 7 | from web3 import AsyncWeb3 8 | from web3.eth.async_eth import AsyncContract 9 | from web3.types import EventData 10 | 11 | from config import constants 12 | from models.set_order_event import SetOrderEvent 13 | from services.decorators.use_fallback import use_async_fallback 14 | 15 | # Just for keep consistency with the ethereum and starknet 16 | # services it won't be a class. It will be a set of functions 17 | 18 | escrow_abi_file = json.load(open(os.getcwd() + '/abi/Escrow.json'))['abi'] 19 | 20 | 21 | class EthereumAsyncRpcNode: 22 | """ 23 | This implementation use async calls to the Ethereum RPC 24 | Using async methods it's not necessary to create a thread for avoid blocking 25 | TODO migrate Ethereum service to async calls 26 | https://stackoverflow.com/questions/68954638/how-to-use-asynchttpprovider-in-web3py 27 | """ 28 | 29 | def __init__(self, rpc_url, private_key, contract_address, abi): 30 | self.w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(rpc_url)) 31 | # self.account = self.w3.eth.account.from_key(private_key) # TODO use private key when necessary 32 | self.contract: AsyncContract = self.w3.eth.contract(address=contract_address, abi=abi) 33 | 34 | 35 | main_rpc_node = EthereumAsyncRpcNode(constants.ZKSYNC_RPC, 36 | None, 37 | constants.ZKSYNC_CONTRACT_ADDRESS, 38 | escrow_abi_file) 39 | fallback_rpc_node = EthereumAsyncRpcNode(constants.ZKSYNC_FALLBACK_RPC, 40 | None, 41 | constants.ZKSYNC_CONTRACT_ADDRESS, 42 | escrow_abi_file) 43 | rpc_nodes = [main_rpc_node, fallback_rpc_node] 44 | 45 | logger = logging.getLogger(__name__) 46 | 47 | 48 | # TODO if we migrate to classes it can be inherited from Ethereum service 49 | @use_async_fallback(rpc_nodes, logger, "Failed to get latest block") 50 | async def get_latest_block(rpc_node=main_rpc_node) -> int: 51 | return await rpc_node.w3.eth.block_number 52 | 53 | 54 | @use_async_fallback(rpc_nodes, logger, "Failed to get set_order events from ZkSync") 55 | async def get_set_order_logs(from_block_number: int, to_block_number: int, rpc_node=main_rpc_node) -> list[EventData]: 56 | logs = await rpc_node.contract.events.SetOrder().get_logs( 57 | fromBlock=hex(from_block_number), 58 | toBlock=hex(to_block_number) 59 | ) 60 | return logs 61 | 62 | 63 | async def get_set_order_events(from_block, to_block) -> list[SetOrderEvent]: 64 | """ 65 | Get set_orders events from the escrow 66 | """ 67 | set_order_logs: list[EventData] = await get_set_order_logs(from_block, to_block) 68 | # Create a list of tasks to parallelize the creation of the SetOrderEvent list 69 | tasks = [asyncio.create_task(SetOrderEvent.from_zksync(log)) for log in set_order_logs] 70 | set_order_events = await asyncio.gather(*tasks) 71 | return cast(list[SetOrderEvent], set_order_events) 72 | --------------------------------------------------------------------------------