diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17f41d417..2571b4422 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 # Python setup - name: Set up Python 3.10.14 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.14 - name: Load cached Poetry installation @@ -40,8 +40,8 @@ jobs: with: virtualenvs-create: true virtualenvs-in-project: true + virtualenvs-path: .venv installer-parallel: true - - run: poetry config installer.modern-installation false - name: Load cached venv id: cached-poetry-dependencies uses: actions/cache@v4 @@ -51,7 +51,8 @@ jobs: - name: Install dependencies if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: make setup - + - uses: asdf-vm/actions/install@v3 + - run: asdf install # Build artifacts - name: Compile all the cairo files run: make build @@ -70,7 +71,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.10.14 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.14 - name: Load cached Poetry installation @@ -85,8 +86,8 @@ jobs: with: virtualenvs-create: true virtualenvs-in-project: true + virtualenvs-path: .venv installer-parallel: true - - run: poetry config installer.modern-installation false - name: Load cached venv id: cached-poetry-dependencies uses: actions/cache@v4 @@ -123,7 +124,7 @@ jobs: KATANA_VERSION=$(grep -oP '^KATANA_VERSION = \K.*' Makefile) echo "katana_version=$KATANA_VERSION" >> "$GITHUB_OUTPUT" - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.14 - name: Load cached Poetry installation @@ -138,8 +139,8 @@ jobs: with: virtualenvs-create: true virtualenvs-in-project: true + virtualenvs-path: .venv installer-parallel: true - - run: poetry config installer.modern-installation false - name: Load cached venv id: cached-poetry-dependencies uses: actions/cache@v4 @@ -153,8 +154,7 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - - name: Install asdf & tools # For multiple versions of scarb - reads from .tool-versions and installs them - uses: asdf-vm/actions/install@v3 + - uses: asdf-vm/actions/install@v3 - name: Load cached katana id: cached-katana uses: actions/cache@v4 @@ -204,7 +204,7 @@ jobs: - name: Move ERC20 run: mv build/v0/ERC20.json build/common - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.14 - name: run tests @@ -228,7 +228,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.10.14 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.14 - name: Load cached Poetry installation @@ -243,8 +243,8 @@ jobs: with: virtualenvs-create: true virtualenvs-in-project: true + virtualenvs-path: .venv installer-parallel: true - - run: poetry config installer.modern-installation false - name: Load cached venv id: cached-poetry-dependencies uses: actions/cache@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5c88db80..92e33dab5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.14 - name: Load cached Poetry installation diff --git a/.github/workflows/trunk-check.yaml b/.github/workflows/trunk-check.yaml index 2e0ec335f..0adad3aa0 100644 --- a/.github/workflows/trunk-check.yaml +++ b/.github/workflows/trunk-check.yaml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Python 3.10.14 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.14 cache: pip diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index ac32e75b2..b5f0a28ca 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -75,6 +75,7 @@ lint: - resources* - tests/ef_tests/test_data - .katana/messaging_config.json + - deployments - linters: [solidity] paths: - solidity_contracts/src/UniswapV2/**/*.sol diff --git a/Makefile b/Makefile index 971b3e64f..3fbdb5432 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ fetch-ssj-artifacts: setup: fetch-ssj-artifacts poetry install -test: build-sol build-cairo1 deploy +test: deploy poetry run pytest tests/src -m "not NoCI" --log-cli-level=INFO -n logical --seed 42 poetry run pytest tests/end_to_end --seed 42 @@ -46,7 +46,7 @@ test-unit: build-sol poetry run pytest tests/src -m "not NoCI" -n logical --seed 42 # run make run-nodes in other terminal -test-end-to-end: build-sol build-cairo1 deploy +test-end-to-end: deploy poetry run pytest tests/end_to_end --seed 42 deploy: build build-sol @@ -70,47 +70,11 @@ build-sol: git submodule update --init --recursive forge build --names --force -# Builds Cairo 1.0 contracts by iterating over subdirectories, -# compiling contracts, and copying the resulting .sierra.json (old versions) or .contract_class.json -# files to the ROOT_DIR/build/fixtures directory with appropriate file extensions. -build-cairo1: - @mkdir -p build/ssj - @for d in cairo1_contracts/*/ ; do \ - if [ "$$d" != "cairo1_contracts/build/" ]; then \ - echo "Building $$d"; \ - cd $$d; \ - scarb build; \ - for f in target/dev/*.sierra.json target/dev/*.contract_class.json target/dev/*.casm.json target/dev/*.compiled_contract_class.json; do \ - if [ -e "$$f" ]; then \ - case "$$f" in \ - *.sierra.json) \ - CONTRACT_NAME="$$(basename $$f | sed -E 's/^.*_([^_.]*)\.sierra\.json$$/\1/')"; \ - cp "$$f" "$(ROOT_DIR)/build/ssj/contracts_$$CONTRACT_NAME.contract_class.json"; \ - ;; \ - *.contract_class.json) \ - CONTRACT_NAME="$$(basename $$f | sed -E 's/^.*_([^_.]*)\.contract_class\.json$$/\1/')"; \ - cp "$$f" "$(ROOT_DIR)/build/ssj/contracts_$$CONTRACT_NAME.contract_class.json"; \ - ;; \ - *.casm.json) \ - CONTRACT_NAME="$$(basename $$f | sed -E 's/^.*_([^_.]*)\.casm\.json$$/\1/')"; \ - cp "$$f" "$(ROOT_DIR)/build/ssj/contracts_$$CONTRACT_NAME.compiled_contract_class.json"; \ - ;; \ - *.compiled_contract_class.json) \ - CONTRACT_NAME="$$(basename $$f | sed -E 's/^.*_([^_.]*)\.compiled_contract_class\.json$$/\1/')"; \ - cp "$$f" "$(ROOT_DIR)/build/ssj/contracts_$$CONTRACT_NAME.compiled_contract_class.json"; \ - ;; \ - esac; \ - fi; \ - done; \ - cd -; \ - fi; \ - done - install-katana: cargo install --git https://github.com/dojoengine/dojo --locked --tag "${KATANA_VERSION}" katana run-katana: - katana --chain-id test --validate-max-steps 6000000 --invoke-max-steps 14000000 --eth-gas-price 0 --strk-gas-price 0 --disable-fee + katana --chain-id test --validate-max-steps 6000000 --invoke-max-steps 14000000 --eth-gas-price 0 --strk-gas-price 0 --disable-fee --seed 0 run-anvil: anvil --block-base-fee-per-gas 10 @@ -118,4 +82,4 @@ run-anvil: run-nodes: @echo "Starting Anvil and Katana in messaging mode" @anvil --block-base-fee-per-gas 10 & - @katana --chain-id test --validate-max-steps 6000000 --invoke-max-steps 14000000 --eth-gas-price 0 --strk-gas-price 0 --disable-fee --messaging .katana/messaging_config.json + @katana --chain-id test --validate-max-steps 6000000 --invoke-max-steps 14000000 --eth-gas-price 0 --strk-gas-price 0 --disable-fee --messaging .katana/messaging_config.json --seed 0 diff --git a/cairo1_contracts/token/src/lib.cairo b/cairo1_contracts/token/src/lib.cairo index 22a9f7028..9b8f06e6a 100644 --- a/cairo1_contracts/token/src/lib.cairo +++ b/cairo1_contracts/token/src/lib.cairo @@ -1,35 +1 @@ -#[starknet::contract] -mod StarknetToken { - use openzeppelin::token::erc20::ERC20Component; - use starknet::ContractAddress; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - - #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - impl ERC20InternalImpl = ERC20Component::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - erc20: ERC20Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { - let name = "MyToken"; - let symbol = "MTK"; - - self.erc20.initializer(name, symbol); - self.erc20._mint(recipient, initial_supply); - } -} +mod starknet_token; diff --git a/cairo1_contracts/token/src/starknet_token.cairo b/cairo1_contracts/token/src/starknet_token.cairo new file mode 100644 index 000000000..22a9f7028 --- /dev/null +++ b/cairo1_contracts/token/src/starknet_token.cairo @@ -0,0 +1,35 @@ +#[starknet::contract] +mod StarknetToken { + use openzeppelin::token::erc20::ERC20Component; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { + let name = "MyToken"; + let symbol = "MTK"; + + self.erc20.initializer(name, symbol); + self.erc20._mint(recipient, initial_supply); + } +} diff --git a/cairo1_contracts/utils/.gitignore b/cairo1_contracts/utils/.gitignore new file mode 100644 index 000000000..eb5a316cb --- /dev/null +++ b/cairo1_contracts/utils/.gitignore @@ -0,0 +1 @@ +target diff --git a/cairo1_contracts/utils/.tool-versions b/cairo1_contracts/utils/.tool-versions new file mode 100644 index 000000000..179f2a8c2 --- /dev/null +++ b/cairo1_contracts/utils/.tool-versions @@ -0,0 +1 @@ +scarb 2.6.5 diff --git a/cairo1_contracts/utils/Scarb.lock b/cairo1_contracts/utils/Scarb.lock new file mode 100644 index 000000000..abda56939 --- /dev/null +++ b/cairo1_contracts/utils/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "utils" +version = "0.1.0" diff --git a/cairo1_contracts/utils/Scarb.toml b/cairo1_contracts/utils/Scarb.toml new file mode 100644 index 000000000..084e2cea0 --- /dev/null +++ b/cairo1_contracts/utils/Scarb.toml @@ -0,0 +1,12 @@ +[package] +name = "utils" +version = "0.1.0" +edition = "2023_11" + +[dependencies] +starknet = "2.6.4" + +[[target.starknet-contract]] +casm = true +sierra = true +casm-add-pythonic-hints = true diff --git a/cairo1_contracts/utils/src/balance_sender.cairo b/cairo1_contracts/utils/src/balance_sender.cairo new file mode 100644 index 000000000..0afe76564 --- /dev/null +++ b/cairo1_contracts/utils/src/balance_sender.cairo @@ -0,0 +1,39 @@ +use core::starknet::{get_caller_address, ContractAddress}; + + +#[starknet::interface] +pub trait IERC20 { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::contract] +pub mod BalanceSender { + use core::starknet::{get_caller_address, ContractAddress, ClassHash, get_contract_address, SyscallResult}; + use super::{IERC20Dispatcher, IERC20DispatcherTrait}; + use core::starknet::syscalls::{replace_class_syscall}; + + #[storage] + struct Storage {} + + #[external(v0)] + fn send_balance(self: @ContractState, token_address: ContractAddress, recipient: ContractAddress) -> bool { + let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + let balance = erc20_dispatcher.balance_of(get_contract_address()); + erc20_dispatcher.transfer(recipient, balance) + } + + #[external(v0)] + fn replace_class(ref self: ContractState, new_class: ClassHash) -> SyscallResult<()>{ + replace_class_syscall(new_class) + } +} diff --git a/cairo1_contracts/utils/src/lib.cairo b/cairo1_contracts/utils/src/lib.cairo new file mode 100644 index 000000000..bdd1426d0 --- /dev/null +++ b/cairo1_contracts/utils/src/lib.cairo @@ -0,0 +1,3 @@ +mod universal_library_caller; + +mod balance_sender; diff --git a/cairo1_contracts/utils/src/universal_library_caller.cairo b/cairo1_contracts/utils/src/universal_library_caller.cairo new file mode 100644 index 000000000..b404a3e23 --- /dev/null +++ b/cairo1_contracts/utils/src/universal_library_caller.cairo @@ -0,0 +1,28 @@ +use starknet::{ + SyscallResult, storage_access::StorageAddress, class_hash::ClassHash, +}; + + +#[starknet::interface] +pub trait IUniversalLibraryCaller { + fn library_call(self: @TContractState, class_hash: ClassHash, function_selector: felt252, calldata: Span) -> SyscallResult>; +} + +#[starknet::contract] +pub mod UniversalLibraryCaller { + use starknet::syscalls::library_call_syscall; + use starknet::{ + SyscallResult, storage_access::StorageAddress, class_hash::ClassHash, + }; + + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl UniversalLibraryCallerImpl of super::IUniversalLibraryCaller { + fn library_call(self: @ContractState, class_hash: ClassHash, function_selector: felt252, calldata: Span) -> SyscallResult> { + library_call_syscall(class_hash, function_selector, calldata) + } + } +} diff --git a/deployments/starknet-sepolia/declarations.json b/deployments/starknet-sepolia/declarations.json index 77d9b7a69..c4c401168 100644 --- a/deployments/starknet-sepolia/declarations.json +++ b/deployments/starknet-sepolia/declarations.json @@ -1,9 +1,17 @@ { - "kakarot": "0x6aa7b2a71ce0e28301a6547b8eb38c5f4130d0091b5aa8a12c8f380efcbc4c4", - "account_contract": "0x56d311021950bf65ee500426e007b9e3ced0db97f9c1e0d29a9e03d79a9bf6c", - "uninitialized_account": "0x1d8b8047e26b484d3f6262d1967217d980d0f2dfc69afa5661492bd5bfe2954", - "EVM": "0x78e943202d567c81ec9b523e5121c15914210f915dd7bce69f09ceb5ae91934", - "OpenzeppelinAccount": "0x452189b6cd1ef66a7daef29cbffb77ce809cac95449687aabb169117c04e2f9", - "Cairo1Helpers": "0xff0ec0846982c93e48ed5130dba8efe5905c099d0ffe45c1fd777a97a2b71a", - "replace_class": "0x5cd1a33bc766f50965fe2343e6aec12a12c562b3bb653085b88dc6751b71682" + "account_contract": "0x6cb1275516c11f6c1f9d2758bd212d2c40d8136ebd353c316779b754a216d83", + "uninitialized_account_fixture": "0x2957ff0877441dddcd140e6af50a3d45712f4f7205a36a846110a70297036be", + "uninitialized_account": "0x45f7d0803659c3f58b5b6ba46f349178253dadabbfc6ab47fa1ba4bab4699f8", + "EVM": "0x1ce258b332ad964d0d0a472b7795615a84f25196b733a319e101b948f3064a8", + "OpenzeppelinAccount": "0x6153ccf69fd20f832c794df36e19135f0070d0576144f0b47f75a226e4be530", + "Cairo1Helpers": "0x28ece3751ecf5bdf2d791eb64a65bfb6a8816432b698870dba2f38a36101d58", + "Cairo1HelpersFixture": "0x4e7811d9bbba41193bd3c77d05c16f7aaa55dd1d601686b50f6fa0e3766a712", + "replace_class": "0xa187318c5e79b010cf45975f589f0a8d441fadde5b1e7ccad46501568437b5", + "Counter": "0x4fc47610d8c9ce0bcfc2f9e03658f0fbcd3e0a9c351a1aa59d465a33533a7c8", + "MockPragmaOracle": "0x675f00328ff84f127d71b179b3f3a3a06ce8432054770cddd5729c8d62866da", + "StarknetToken": "0x27dd8ce628866f1544202ae06ec57b3c9b1f775d5f7c2797de7aa1586ecf693", + "ERC20": "0x3c5ee4bc12f4247cd8071150c3f5d9bee71f40b0ef7aeae59f468d898f60933", + "kakarot": "0x3f9e4ac97c943181453ce74f1fd1c163c154c40d9cbbbe5c2453512ee1a86e6", + "UniversalLibraryCaller": "0x5e84816dcbfd11581d8d5160af5754a4adc71ab35a0c0aaa053773f61838627", + "BalanceSender": "0x2cc118f56b9d3ad311900db5254f3dca75fbf24de3b68ee670a0fb3691ac5b3" } diff --git a/deployments/starknet-sepolia/deployments.json b/deployments/starknet-sepolia/deployments.json index 4f8a8d55a..9dcc82f1f 100644 --- a/deployments/starknet-sepolia/deployments.json +++ b/deployments/starknet-sepolia/deployments.json @@ -1,7 +1,27 @@ { "kakarot": { - "address": "0x464f7e37179d2f93ea208795bdb2d0912e8257f6fb5f67ae2559251523aee19", - "tx": "0x209a134f8c8f3a9b9e98c2e4789476d4432aceeb252ecfb3dba069cec5ec974", + "address": "0x6f625bb0bd82401b268c1ba9fa0973bb9cdf732c6a6f21fe14dfd4c82a28e89", + "tx": "0x36c51e168146d9c104a4323a2dddf873a9a54d52b02f5cbc83d8cb957426f60", "artifact": "build/kakarot.json" + }, + "EVM": { + "address": "0x41c4025537b9677034f3b58f6e722e19c40a7b77a82e3851fdc6b7adf5ad414", + "tx": "0xa0b92fd60470ac35b5a64236bacbe86d6b5aaea30997aced328123cba74024", + "artifact": "build/fixtures/EVM.json" + }, + "Counter": { + "address": "0x2d6741b182475b7cfc62ec1000fbcba553ea08f2e603fa2840d0288cd2d1e3c", + "tx": "0xd1781094a55ac09c3177e6799f9d484c5477baa6d120923c942da5c2fdbfea", + "artifact": "build/fixtures/Counter.json" + }, + "MockPragmaOracle": { + "address": "0x17e64c92b06da9a331da9fd333a683a33019ae2a393254caf332d4158edc74d", + "tx": "0x3d6b91602c1e290bc65c6f85751f5ea156cf982d01c6bf1ea694d7398a9d5a5", + "artifact": "build/ssj/contracts_MockPragmaOracle" + }, + "UniversalLibraryCaller": { + "address": "0x01e12ea32baf68b1e11c1ce32595d3a61a22ccdcbc67f94c77268b6ce99fa6d4", + "tx": "0x3d6b91602c1e290bc65c6f85751f5ea156cf982d01c6bf1ea694d7398a9d5a5", + "artifact": "cairo1_contracts/utils/target/dev/library_call_UniversalLibraryCaller" } } diff --git a/kakarot_scripts/constants.py b/kakarot_scripts/constants.py index 3547a6703..b365d38b0 100644 --- a/kakarot_scripts/constants.py +++ b/kakarot_scripts/constants.py @@ -4,12 +4,15 @@ from enum import Enum, IntEnum from math import ceil, log from pathlib import Path +from typing import Dict, List import requests from dotenv import load_dotenv from eth_keys import keys +from starknet_py.net.account.account import Account from starknet_py.net.full_node_client import FullNodeClient from starknet_py.net.models.chains import StarknetChainId +from starknet_py.net.signer.stark_curve_signer import KeyPair from web3 import Web3 logging.basicConfig() @@ -41,9 +44,9 @@ class NetworkType(Enum): "sepolia": { "name": "starknet-sepolia", "explorer_url": "https://sepolia.starkscan.co/", - "rpc_url": "https://starknet-sepolia.public.blastapi.io/rpc/v0_6", + "rpc_url": os.getenv("STARKNET_SEPOLIA_RPC_URL"), "l1_rpc_url": f"https://sepolia.infura.io/v3/{os.getenv('INFURA_KEY')}", - "type": NetworkType.PROD, + "type": NetworkType.STAGING, "chain_id": StarknetChainId.SEPOLIA, "check_interval": 5, "max_wait": 30, @@ -65,6 +68,48 @@ class NetworkType(Enum): "type": NetworkType.DEV, "check_interval": 0.01, "max_wait": 3, + "relayers": [ + { + "address": 0xB3FF441A68610B30FD5E2ABBF3A1548EB6BA6F3559F2862BF2DC757E5828CA, + "private_key": 0x2BBF4F9FD0BBB2E60B0316C1FE0B76CF7A4D0198BD493CED9B8DF2A3A24D68A, + }, + { + "address": 0xE29882A1FCBA1E7E10CAD46212257FEA5C752A4F9B1B1EC683C503A2CF5C8A, + "private_key": 0x14D6672DCB4B77CA36A887E9A11CD9D637D5012468175829E9C6E770C61642, + }, + { + "address": 0x29873C310FBEFDE666DC32A1554FEA6BB45EECC84F680F8A2B0A8FBB8CB89AF, + "private_key": 0xC5B2FCAB997346F3EA1C00B002ECF6F382C5F9C9659A3894EB783C5320F912, + }, + { + "address": 0x2D71E9C974539BB3FFB4B115E66A23D0F62A641EA66C4016E903454C8753BBC, + "private_key": 0x33003003001800009900180300D206308B0070DB00121318D17B5E6262150B, + }, + { + "address": 0x3EBB4767AAE1262F8EB28D9368DB5388CFE367F50552A8244123506F0B0BCCA, + "private_key": 0x3E3979C1ED728490308054FE357A9F49CF67F80F9721F44CC57235129E090F4, + }, + { + "address": 0x541DA8F7F3AB8247329D22B3987D1FFB181BC8DC7F9611A6ECCEC3B0749A585, + "private_key": 0x736ADBBCDAC7CC600F89051DB1ABBC16B9996B46F6B58A9752A11C1028A8EC8, + }, + { + "address": 0x56C155B624FDF6BFC94F7B37CF1DBEBB5E186EF2E4AB2762367CD07C8F892A1, + "private_key": 0x6BF3604BCB41FED6C42BCCA5436EEB65083A982FF65DB0DC123F65358008B51, + }, + { + "address": 0x6162896D1D7AB204C7CCAC6DD5F8E9E7C25ECD5AE4FCB4AD32E57786BB46E03, + "private_key": 0x1800000000300000180000000000030000000000003006001800006600, + }, + { + "address": 0x66EFB28AC62686966AE85095FF3A772E014E7FBF56D4C5F6FAC5606D4DDE23A, + "private_key": 0x283D1E73776CD4AC1AC5F0B879F561BDED25ECEB2CC589C674AF0CEC41DF441, + }, + { + "address": 0x6B86E40118F29EBE393A75469B4D926C7A44C2E2681B6D319520B7C1156D114, + "private_key": 0x1C9053C053EDF324AEC366A34C6901B1095B07AF69495BFFEC7D7FE21EFFB1B, + }, + ], }, "madara": { "name": "madara", @@ -123,18 +168,6 @@ class NetworkType(Enum): else: NETWORK = NETWORKS["katana"] -prefix = NETWORK["name"].upper().replace("-", "_") -NETWORK["account_address"] = os.environ.get(f"{prefix}_ACCOUNT_ADDRESS") -if NETWORK["account_address"] is None: - logger.warning( - f"⚠️ {prefix}_ACCOUNT_ADDRESS not set, defaulting to ACCOUNT_ADDRESS" - ) - NETWORK["account_address"] = os.getenv("ACCOUNT_ADDRESS") -NETWORK["private_key"] = os.environ.get(f"{prefix}_PRIVATE_KEY") -if NETWORK["private_key"] is None: - logger.warning(f"⚠️ {prefix}_PRIVATE_KEY not set, defaulting to PRIVATE_KEY") - NETWORK["private_key"] = os.getenv("PRIVATE_KEY") - RPC_CLIENT = FullNodeClient(node_url=NETWORK["rpc_url"]) L1_RPC_PROVIDER = Web3(Web3.HTTPProvider(NETWORK["l1_rpc_url"])) WEB3 = Web3() @@ -155,7 +188,9 @@ class NetworkType(Enum): if WEB3.is_connected(): chain_id = WEB3.eth.chain_id else: - chain_id = starknet_chain_id + chain_id = starknet_chain_id % ( + 2**53 if NETWORK["name"] != "starknet-sepolia" else 2**32 + ) except ( requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, @@ -164,8 +199,13 @@ class NetworkType(Enum): logger.info( f"⚠️ Could not get chain Id from {NETWORK['rpc_url']}: {e}, defaulting to KKRT" ) - chain_id = int.from_bytes(b"KKRT", "big") starknet_chain_id = int.from_bytes(b"KKRT", "big") + chain_id = starknet_chain_id % ( + # TODO: remove once Kakarot is redeployed on sepolia + 2**53 + if NETWORK["name"] != "starknet-sepolia" + else 2**32 + ) class ChainId(IntEnum): @@ -181,56 +221,59 @@ class ChainId(IntEnum): or "0x20eB005C0b9c906691F885eca5895338E15c36De", 16, ) -SOURCE_DIR = Path("src") -SOURCE_DIR_FIXTURES = Path("tests/fixtures") -CONTRACTS = {p.stem: p for p in list(SOURCE_DIR.glob("**/*.cairo"))} -CONTRACTS_FIXTURES = {p.stem: p for p in list(SOURCE_DIR_FIXTURES.glob("**/*.cairo"))} +CAIRO_ZERO_DIR = Path("src") +CAIRO_DIR = Path("cairo1_contracts") +TESTS_DIR = Path("tests") + +CONTRACTS = { + p.stem: p + for p in ( + list(CAIRO_ZERO_DIR.glob("**/*.cairo")) + + list(TESTS_DIR.glob("**/*.cairo")) + + list(CAIRO_DIR.glob("**/*.cairo")) + ) +} BUILD_DIR = Path("build") -BUILD_DIR_FIXTURES = BUILD_DIR / "fixtures" BUILD_DIR.mkdir(exist_ok=True, parents=True) -BUILD_DIR_FIXTURES.mkdir(exist_ok=True, parents=True) BUILD_DIR_SSJ = BUILD_DIR / "ssj" DATA_DIR = Path("kakarot_scripts") / "data" -class ArtifactType(Enum): - cairo0 = 0 - cairo1 = 1 - - DEPLOYMENTS_DIR = Path("deployments") / NETWORK["name"] DEPLOYMENTS_DIR.mkdir(exist_ok=True, parents=True) COMPILED_CONTRACTS = [ - {"contract_name": "kakarot", "is_account_contract": False}, {"contract_name": "account_contract", "is_account_contract": True}, - {"contract_name": "uninitialized_account_fixture", "is_account_contract": False}, - {"contract_name": "uninitialized_account", "is_account_contract": False}, + {"contract_name": "BalanceSender", "is_account_contract": False}, + {"contract_name": "Counter", "is_account_contract": False}, + {"contract_name": "ERC20", "is_account_contract": False}, {"contract_name": "EVM", "is_account_contract": False}, + {"contract_name": "kakarot", "is_account_contract": False}, + {"contract_name": "MockPragmaOracle", "is_account_contract": False}, {"contract_name": "OpenzeppelinAccount", "is_account_contract": True}, - {"contract_name": "ERC20", "is_account_contract": False}, {"contract_name": "replace_class", "is_account_contract": False}, - {"contract_name": "Counter", "is_account_contract": False}, + {"contract_name": "StarknetToken", "is_account_contract": False}, + {"contract_name": "uninitialized_account_fixture", "is_account_contract": False}, + {"contract_name": "uninitialized_account", "is_account_contract": False}, + {"contract_name": "UniversalLibraryCaller", "is_account_contract": False}, ] DECLARED_CONTRACTS = [ - {"contract_name": "kakarot", "cairo_version": ArtifactType.cairo0}, - {"contract_name": "account_contract", "cairo_version": ArtifactType.cairo0}, - { - "contract_name": "uninitialized_account_fixture", - "cairo_version": ArtifactType.cairo0, - }, - {"contract_name": "uninitialized_account", "cairo_version": ArtifactType.cairo0}, - {"contract_name": "EVM", "cairo_version": ArtifactType.cairo0}, - {"contract_name": "OpenzeppelinAccount", "cairo_version": ArtifactType.cairo0}, - {"contract_name": "Cairo1Helpers", "cairo_version": ArtifactType.cairo1}, - {"contract_name": "Cairo1HelpersFixture", "cairo_version": ArtifactType.cairo1}, - {"contract_name": "replace_class", "cairo_version": ArtifactType.cairo0}, - {"contract_name": "Counter", "cairo_version": ArtifactType.cairo0}, - {"contract_name": "MockPragmaOracle", "cairo_version": ArtifactType.cairo1}, - {"contract_name": "StarknetToken", "cairo_version": ArtifactType.cairo1}, - {"contract_name": "ERC20", "cairo_version": ArtifactType.cairo0}, + "account_contract", + "Cairo1Helpers", + "Cairo1HelpersFixture", + "Counter", + "ERC20", + "EVM", + "kakarot", + "MockPragmaOracle", + "OpenzeppelinAccount", + "replace_class", + "StarknetToken", + "uninitialized_account_fixture", + "uninitialized_account", + "UniversalLibraryCaller", ] # PRE-EIP155 TX @@ -255,6 +298,55 @@ class ArtifactType(Enum): ).public_key.to_checksum_address() ) +prefix = NETWORK["name"].upper().replace("-", "_") +NETWORK["account_address"] = os.environ.get(f"{prefix}_ACCOUNT_ADDRESS") +if NETWORK["account_address"] is None: + logger.warning( + f"⚠️ {prefix}_ACCOUNT_ADDRESS not set, defaulting to ACCOUNT_ADDRESS" + ) + NETWORK["account_address"] = os.getenv("ACCOUNT_ADDRESS") +NETWORK["private_key"] = os.environ.get(f"{prefix}_PRIVATE_KEY") +if NETWORK["private_key"] is None: + logger.warning(f"⚠️ {prefix}_PRIVATE_KEY not set, defaulting to PRIVATE_KEY") + NETWORK["private_key"] = os.getenv("PRIVATE_KEY") + + +class RelayerPool: + def __init__(self, relayers: List[Dict[str, int]]): + self.relayer_accounts = [ + Account( + address=relayer["address"], + client=RPC_CLIENT, + chain=ChainId.starknet_chain_id, + key_pair=KeyPair.from_private_key(relayer["private_key"]), + ) + for relayer in relayers + ] + self._index = 0 + + def __next__(self) -> Account: + relayer = self.relayer_accounts[self._index] + self._index = (self._index + 1) % len(self.relayer_accounts) + return relayer + + +NETWORK["relayers"] = RelayerPool( + NETWORK.get( + "relayers", + ( + [ + { + "address": int(NETWORK["account_address"], 16), + "private_key": int(NETWORK["private_key"], 16), + } + ] + if NETWORK["account_address"] is not None + and NETWORK["private_key"] is not None + else [] + ), + ) +) + if NETWORK.get("chain_id"): logger.info( f"ℹ️ Connected to CHAIN_ID {NETWORK['chain_id'].value.to_bytes(ceil(log(NETWORK['chain_id'].value, 256)), 'big')}" diff --git a/kakarot_scripts/deploy_kakarot.py b/kakarot_scripts/deploy_kakarot.py index 3fddb4f41..fc6257e0e 100644 --- a/kakarot_scripts/deploy_kakarot.py +++ b/kakarot_scripts/deploy_kakarot.py @@ -44,10 +44,7 @@ async def main(): account = await get_starknet_account() logger.info(f"ℹ️ Using account 0x{account.address:064x} as deployer") - class_hash = { - contract["contract_name"]: await declare(contract) - for contract in DECLARED_CONTRACTS - } + class_hash = {contract: await declare(contract) for contract in DECLARED_CONTRACTS} dump_declarations(class_hash) # %% Deployments @@ -88,7 +85,7 @@ async def main(): ) freshly_deployed = True - if NETWORK["type"] is NetworkType.STAGING: + if NETWORK["type"] is NetworkType.STAGING or NETWORK["type"] is NetworkType.DEV: starknet_deployments["EVM"] = await upgrade( "EVM", account.address, # owner @@ -101,21 +98,8 @@ async def main(): ) starknet_deployments["Counter"] = await upgrade("Counter") starknet_deployments["MockPragmaOracle"] = await upgrade("MockPragmaOracle") - - if NETWORK["type"] is NetworkType.DEV: - starknet_deployments["EVM"] = await deploy_starknet( - "EVM", - account.address, # owner - ETH_TOKEN_ADDRESS, # native_token_address_ - class_hash["account_contract"], # account_contract_class_hash_ - class_hash["uninitialized_account"], # uninitialized_account_class_hash_ - class_hash["Cairo1Helpers"], - COINBASE, - BLOCK_GAS_LIMIT, - ) - starknet_deployments["Counter"] = await deploy_starknet("Counter") - starknet_deployments["MockPragmaOracle"] = await deploy_starknet( - "MockPragmaOracle" + starknet_deployments["UniversalLibraryCaller"] = await upgrade( + "UniversalLibraryCaller" ) dump_deployments(starknet_deployments) @@ -124,11 +108,7 @@ async def main(): logger.info(f"ℹ️ Found default EVM address {EVM_ADDRESS}") from kakarot_scripts.utils.kakarot import get_eoa - amount = ( - 0.02 - if NETWORK["type"] is not (NetworkType.DEV or NetworkType.STAGING) - else 100 - ) + amount = 100 if NETWORK["type"] is NetworkType.DEV else 0.01 await get_eoa(amount=amount) # Set the base fee if freshly deployed @@ -152,6 +132,22 @@ async def main(): evm_deployments["CreateX"] = await deploy_with_presigned_tx( CREATEX_DEPLOYER, CREATEX_SIGNED_TX, amount=0.3, name="CreateX" ) + + if NETWORK["type"] is NetworkType.STAGING or NETWORK["type"] is NetworkType.DEV: + bridge = await deploy_evm("CairoPrecompiles", "EthStarknetBridge") + evm_deployments["Bridge"] = { + "address": int(bridge.address, 16), + "starknet_address": bridge.starknet_address, + } + await invoke( + "kakarot", + "set_authorized_cairo_precompile_caller", + int(bridge.address, 16), + 1, + ) + await invoke("kakarot", "set_coinbase", int(bridge.address, 16)) + await invoke("kakarot", "set_base_fee", 1) + dump_evm_deployments(evm_deployments) diff --git a/kakarot_scripts/utils/kakarot.py b/kakarot_scripts/utils/kakarot.py index 17794733d..6ec971750 100644 --- a/kakarot_scripts/utils/kakarot.py +++ b/kakarot_scripts/utils/kakarot.py @@ -43,12 +43,11 @@ from kakarot_scripts.utils.starknet import get_balance from kakarot_scripts.utils.starknet import get_contract as _get_starknet_contract from kakarot_scripts.utils.starknet import get_deployments as _get_starknet_deployments -from kakarot_scripts.utils.starknet import get_starknet_account from kakarot_scripts.utils.starknet import invoke as _invoke_starknet from kakarot_scripts.utils.starknet import wait_for_transaction +from kakarot_scripts.utils.uint256 import int_to_uint256 from tests.utils.constants import TRANSACTION_GAS_LIMIT from tests.utils.helpers import pack_calldata, rlp_encode_signed_data -from tests.utils.uint256 import int_to_uint256 logging.basicConfig() logger = logging.getLogger(__name__) @@ -59,6 +58,10 @@ class EvmTransactionError(Exception): pass +class StarknetTransactionError(Exception): + pass + + @functools.lru_cache() def get_solidity_artifacts( contract_app: str, @@ -316,7 +319,16 @@ def dump_deployments(deployments): def get_deployments(): try: - return json.load(open(DEPLOYMENTS_DIR / "kakarot_deployments.json", "r")) + return { + name: { + **value, + "address": int(value["address"], 16), + "starknet_address": int(value["starknet_address"], 16), + } + for name, value in json.load( + open(DEPLOYMENTS_DIR / "kakarot_deployments.json", "r") + ).items() + } except FileNotFoundError: return {} @@ -432,6 +444,7 @@ async def _wrapper(self, *args, **kwargs): data=calldata, caller_eoa=caller_eoa_ if caller_eoa_ else None, max_fee=max_fee, + gas_price=gas_price, ) if success == 0: logger.error(f"❌ {self.address}.{fun} failed") @@ -450,13 +463,12 @@ async def _wrapper(self, *args, **kwargs): async def _contract_exists(address: int) -> bool: try: await RPC_CLIENT.get_class_hash_at(address) - logger.info(f"ℹ️ Contract at address {hex(address)} already exists") return True except ClientError: return False -async def get_eoa(private_key=None, amount=10) -> Account: +async def get_eoa(private_key=None, amount=0) -> Account: private_key = private_key or keys.PrivateKey(bytes.fromhex(EVM_PRIVATE_KEY[2:])) starknet_address = await deploy_and_fund_evm_address( private_key.public_key.to_checksum_address(), amount @@ -541,6 +553,7 @@ async def eth_send_transaction( value: Union[int, str] = 0, caller_eoa: Optional[Account] = None, max_fee: Optional[int] = None, + gas_price=DEFAULT_GAS_PRICE, ): """Execute the data at the EVM contract to on Kakarot.""" evm_account = caller_eoa or await get_eoa() @@ -558,12 +571,11 @@ async def eth_send_transaction( ).nonce payload = { - "type": 0x2, + "type": 0x1, "chainId": NETWORK["chain_id"], "nonce": nonce, "gas": gas, - "maxPriorityFeePerGas": 1, - "maxFeePerGas": DEFAULT_GAS_PRICE, + "gasPrice": gas_price, "to": to_checksum_address(to) if to else None, "value": value, "data": data, @@ -603,7 +615,7 @@ async def send_starknet_transaction( packed_encoded_unsigned_tx: List[int], max_fee: Optional[int] = None, ): - relayer = await get_starknet_account() + relayer = next(NETWORK["relayers"]) current_timestamp = (await RPC_CLIENT.get_block("latest")).timestamp outside_execution = { "caller": int.from_bytes(b"ANY_CALLER", "big"), @@ -645,6 +657,8 @@ async def send_starknet_transaction( if event.from_address == evm_account.address and event.keys[0] == starknet_keccak(b"transaction_executed") ] + if receipt.execution_status.name == "REVERTED": + raise StarknetTransactionError(f"Starknet tx reverted: {receipt.revert_reason}") if len(transaction_events) != 1: raise ValueError("Cannot locate the single event giving the actual tx status") ( diff --git a/kakarot_scripts/utils/starknet.py b/kakarot_scripts/utils/starknet.py index 4de3adc14..35da2abe0 100644 --- a/kakarot_scripts/utils/starknet.py +++ b/kakarot_scripts/utils/starknet.py @@ -2,7 +2,9 @@ import json import logging import random +import re import subprocess +from collections import namedtuple from copy import deepcopy from datetime import datetime from functools import cache @@ -26,6 +28,7 @@ from starknet_py.hash.transaction import TransactionHashPrefix, compute_transaction_hash from starknet_py.hash.utils import message_signature from starknet_py.net.account.account import Account +from starknet_py.net.client_errors import ClientError from starknet_py.net.client_models import Call, DeclareTransactionResponse from starknet_py.net.full_node_client import _create_broadcasted_txn from starknet_py.net.models.transaction import DeclareV1 @@ -35,16 +38,14 @@ from kakarot_scripts.constants import ( BUILD_DIR, - BUILD_DIR_FIXTURES, BUILD_DIR_SSJ, + CAIRO_DIR, + CAIRO_ZERO_DIR, CONTRACTS, - CONTRACTS_FIXTURES, DEPLOYMENTS_DIR, ETH_TOKEN_ADDRESS, NETWORK, RPC_CLIENT, - SOURCE_DIR, - ArtifactType, ChainId, NetworkType, ) @@ -58,12 +59,7 @@ # to have at least 0.1 ETH _max_fee = int(1e17) - -def int_to_uint256(value): - value = int(value) - low = value & ((1 << 128) - 1) - high = value >> 128 - return {"low": low, "high": high} +Artifact = namedtuple("Artifact", ["sierra", "casm"]) @alru_cache @@ -85,7 +81,7 @@ async def get_starknet_account( key_pair = KeyPair.from_private_key(int(private_key, 16)) public_key = None - for selector in ["get_public_key", "getPublicKey", "getSigner"]: + for selector in ["get_public_key", "getPublicKey", "getSigner", "get_owner"]: try: call = Call( to_addr=address, @@ -105,6 +101,7 @@ async def get_starknet_account( or "Invalid message selector." in message or "StarknetErrorCode.ENTRY_POINT_NOT_FOUND_IN_CONTRACT" in message or ("code 40" in message and "not found in contract" in message) + or "{'error': 'Invalid message selector'}" in message ): continue else: @@ -133,21 +130,19 @@ async def get_starknet_account( async def get_eth_contract(provider=None) -> Contract: return Contract( ETH_TOKEN_ADDRESS, - get_abi("ERC20", cairo_version=ArtifactType.cairo0), + get_abi("ERC20"), provider or await get_starknet_account(), - cairo_version=ArtifactType.cairo0, + cairo_version=0, ) @cache -def get_contract( - contract_name, address=None, provider=None, cairo_version=None -) -> Contract: +def get_contract(contract_name, address=None, provider=None) -> Contract: return Contract( address or get_deployments()[contract_name]["address"], - get_abi(contract_name, cairo_version), + get_abi(contract_name), provider or RPC_CLIENT, - cairo_version=get_artifact_version(contract_name).value, + cairo_version=get_cairo_version(contract_name), ) @@ -158,7 +153,7 @@ async def fund_address( Fund a given starknet address with {amount} ETH. """ address = int(address, 16) if isinstance(address, str) else address - amount = amount * 1e18 + amount = int(amount * 1e18) if NETWORK["name"] == "starknet-devnet": response = requests.post( "http://127.0.0.1:5050/mint", @@ -176,9 +171,7 @@ async def fund_address( raise ValueError( f"Cannot send {amount / 1e18} ETH from default account with current balance {balance / 1e18} ETH" ) - prepared = eth_contract.functions["transfer"].prepare_invoke_v1( - address, int_to_uint256(amount) - ) + prepared = eth_contract.functions["transfer"].prepare_invoke_v1(address, amount) tx = await prepared.invoke(max_fee=_max_fee) status = await wait_for_transaction(tx.hash) @@ -249,86 +242,82 @@ def get_deployments(): @cache -def get_artifact(contract_name, cairo_version=None): - if cairo_version is None: - cairo_version = get_artifact_version(contract_name) - if cairo_version == ArtifactType.cairo1: - return (BUILD_DIR_SSJ / f"contracts_{contract_name}", ArtifactType.cairo1) - - return ( - ( - BUILD_DIR / f"{contract_name}.json" - if not is_fixture_contract(contract_name) - else BUILD_DIR_FIXTURES / f"{contract_name}.json" - ), - ArtifactType.cairo0, +def get_artifact(contract_name): + artifacts = list(CAIRO_DIR.glob(f"**/*{contract_name}.*.json")) or list( + BUILD_DIR_SSJ.glob(f"**/*{contract_name}.*.json") ) + if artifacts: + sierra, casm = ( + artifacts + if "sierra.json" in artifacts[0].name + or ".contract_class.json" in artifacts[0].name + else artifacts[::-1] + ) + return Artifact(sierra=sierra, casm=casm) + + artifacts = list(BUILD_DIR.glob(f"**/*{contract_name}*.json")) + if not artifacts: + raise FileNotFoundError(f"No artifact found for {contract_name}") + return Artifact(sierra=None, casm=artifacts[0]) @cache -def get_abi(contract_name, cairo_version=None): - artifact, cairo_version = get_artifact(contract_name, cairo_version) - if cairo_version == ArtifactType.cairo1: - artifact = artifact.with_suffix(".contract_class.json") +def get_abi(contract_name): + artifact = get_artifact(contract_name) + return json.loads( + (artifact.sierra if artifact.sierra else artifact.casm).read_text() + )["abi"] + - return json.loads(artifact.read_text())["abi"] +@cache +def get_cairo_version(contract_name): + return get_artifact(contract_name).sierra is not None +@cache def get_tx_url(tx_hash: int) -> str: return f"{NETWORK['explorer_url']}/tx/0x{tx_hash:064x}" -def is_fixture_contract(contract_name): - return CONTRACTS_FIXTURES.get(contract_name) is not None - - -def get_artifact_version(contract_name): - cairo_0 = contract_name in set(CONTRACTS_FIXTURES).union(set(CONTRACTS)) - cairo_1 = any( - contract_name in str(artifact) for artifact in BUILD_DIR_SSJ.glob("*.json") - ) - if cairo_0 and cairo_1: - raise ValueError(f"Contract {contract_name} is ambiguous") - if cairo_0: - return ArtifactType.cairo0 - if cairo_1: - return ArtifactType.cairo1 - raise ValueError(f"Cannot find artifact for contract {contract_name}") - - def compile_contract(contract): logger.info(f"⏳ Compiling {contract['contract_name']}") start = datetime.now() - is_fixture = is_fixture_contract(contract["contract_name"]) - artifact, cairo_version = get_artifact(contract["contract_name"]) - - if cairo_version == ArtifactType.cairo1: - raise NotImplementedError("SSJ compilation not implemented yet") - - output = subprocess.run( - [ - "starknet-compile-deprecated", - ( - CONTRACTS[contract["contract_name"]] - if not is_fixture - else CONTRACTS_FIXTURES[contract["contract_name"]] - ), - "--output", - artifact, - "--cairo_path", - str(SOURCE_DIR), - *(["--no_debug_info"] if NETWORK["type"] is not NetworkType.DEV else []), - *(["--account_contract"] if contract["is_account_contract"] else []), - *( - ["--disable_hint_validation"] - if NETWORK["type"] is NetworkType.DEV - else [] - ), - ], - capture_output=True, + contract_path = CONTRACTS.get(contract["contract_name"]) or CONTRACTS.get( + re.sub("(?!^)([A-Z]+)", r"_\1", contract["contract_name"]).lower() ) + + if contract_path.is_relative_to(CAIRO_DIR): + output = subprocess.run( + "scarb build", shell=True, cwd=contract_path.parent, capture_output=True + ) + else: + output = subprocess.run( + [ + "starknet-compile-deprecated", + contract_path, + "--output", + BUILD_DIR / f"{contract['contract_name']}.json", + "--cairo_path", + str(CAIRO_ZERO_DIR), + *( + ["--no_debug_info"] + if NETWORK["type"] is not NetworkType.DEV + else [] + ), + *(["--account_contract"] if contract["is_account_contract"] else []), + *( + ["--disable_hint_validation"] + if NETWORK["type"] is NetworkType.DEV + else [] + ), + ], + capture_output=True, + ) + if output.returncode != 0: - raise RuntimeError(output.stderr) + raise RuntimeError( + f"❌ {contract['contract_name']} raised:\n{output.stderr}.\nOutput:\n{output.stdout}" + ) elapsed = datetime.now() - start logger.info( @@ -374,18 +363,14 @@ async def deploy_starknet_account(class_hash=None, private_key=None, amount=1): } -async def declare(contract): - logger.info(f"ℹ️ Declaring {contract['contract_name']}") - artifact, cairo_version = get_artifact(**contract) +async def declare(contract_name): + logger.info(f"ℹ️ Declaring {contract_name}") + artifact = get_artifact(contract_name) account = await get_starknet_account() - if cairo_version == ArtifactType.cairo1: - casm_compiled_contract = artifact.with_suffix( - ".compiled_contract_class.json" - ).read_text() - sierra_compiled_contract = artifact.with_suffix( - ".contract_class.json" - ).read_text() + if artifact.sierra is not None: + casm_compiled_contract = artifact.casm.read_text() + sierra_compiled_contract = artifact.sierra.read_text() casm_class = create_casm_class(casm_compiled_contract) class_hash = compute_casm_class_hash(casm_class) @@ -408,7 +393,7 @@ async def declare(contract): resp = await account.client.declare(transaction=declare_v2_transaction) else: contract_class = create_compiled_contract( - compiled_contract=artifact.read_text() + compiled_contract=artifact.casm.read_text() ) class_hash = compute_class_hash(contract_class=deepcopy(contract_class)) try: @@ -453,24 +438,14 @@ async def declare(contract): status = await wait_for_transaction(resp.transaction_hash) - logger.info( - f"{status} {contract['contract_name']} class hash: {hex(resp.class_hash)}" - ) + logger.info(f"{status} {contract_name} class hash: {hex(resp.class_hash)}") return deployed_class_hash async def deploy(contract_name, *args): logger.info(f"ℹ️ Deploying {contract_name}") - artifact, cairo_version = get_artifact(contract_name) + abi = get_abi(contract_name) declarations = get_declarations() - if cairo_version == ArtifactType.cairo0: - compiled_contract = Path(artifact).read_text() - abi = json.loads(compiled_contract)["abi"] - else: - sierra_compiled_contract = artifact.with_suffix( - ".contract_class.json" - ).read_text() - abi = json.loads(sierra_compiled_contract)["abi"] account = await get_starknet_account() deploy_result = await Contract.deploy_contract_v1( @@ -479,7 +454,7 @@ async def deploy(contract_name, *args): abi=abi, constructor_args=list(args), max_fee=_max_fee, - cairo_version=cairo_version.value, + cairo_version=get_cairo_version(contract_name), ) status = await wait_for_transaction(deploy_result.hash) logger.info( @@ -499,10 +474,15 @@ async def upgrade(contract_name, *args): logger.info(f"ℹ️ {contract_name} already deployed, checking version.") class_hash = get_declarations() + try: + deployed_class_hash = await RPC_CLIENT.get_class_hash_at( + deployments[contract_name]["address"] + ) + except ClientError as e: + if "Contract not found" in str(e): + logger.info(f"ℹ️ deploying {contract_name}.") + return await deploy(contract_name, *args) - deployed_class_hash = await RPC_CLIENT.get_class_hash_at( - deployments[contract_name]["address"] - ) if deployed_class_hash != class_hash[contract_name]: logger.info(f"ℹ️ redeploying {contract_name}.") return await deploy(contract_name, *args) diff --git a/tests/utils/uint256.py b/kakarot_scripts/utils/uint256.py similarity index 100% rename from tests/utils/uint256.py rename to kakarot_scripts/utils/uint256.py diff --git a/solidity_contracts/src/CairoPrecompiles/EthStarknetBridge.sol b/solidity_contracts/src/CairoPrecompiles/EthStarknetBridge.sol new file mode 100644 index 000000000..bead05a28 --- /dev/null +++ b/solidity_contracts/src/CairoPrecompiles/EthStarknetBridge.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.0 <0.9.0; + +import {CairoLib} from "kakarot-lib/CairoLib.sol"; + +using CairoLib for uint256; + +contract EthStarknetBridge { + /// @dev The cairo contract to call + uint256 constant starknetEth = 0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7; + uint256 constant TRANSFER_SELECTOR = uint256(keccak256("transfer")) % 2 ** 250; + + // State variable to store the owner of the contract + address immutable owner; + + // Constructor sets the owner of the contract + constructor() { + owner = msg.sender; + } + + // Modifier to restrict access to owner only + modifier onlyOwner() { + require(msg.sender == owner, "Not the contract owner"); + _; + } + + /// @notice Withdraws ETH from the contract to a Starknet address + function withdraw(uint256 toStarknetAddress) external onlyOwner { + uint256 balance = address(this).balance; + transfer(toStarknetAddress, balance); + } + + /// @notice Calls the Eth Cairo contract + /// @param toStarknetAddress The Starknet address to send ETH to + /// @param amount The amount of ETH to send + function transfer(uint256 toStarknetAddress, uint256 amount) public { + // Split amount in [low, high] + uint128 amountLow = uint128(amount); + uint128 amountHigh = uint128(amount >> 128); + + uint256[] memory transferCallData = new uint256[](3); + transferCallData[0] = toStarknetAddress; + transferCallData[1] = uint256(amountLow); + transferCallData[2] = uint256(amountHigh); + + starknetEth.delegatecallCairo(TRANSFER_SELECTOR, transferCallData); + } +} diff --git a/src/kakarot/accounts/account_contract.cairo b/src/kakarot/accounts/account_contract.cairo index 2e580821d..792e3c400 100644 --- a/src/kakarot/accounts/account_contract.cairo +++ b/src/kakarot/accounts/account_contract.cairo @@ -5,7 +5,7 @@ from openzeppelin.access.ownable.library import Ownable, Ownable_owner from starkware.cairo.common.alloc import alloc from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin, SignatureBuiltin -from starkware.cairo.common.math import assert_le +from starkware.cairo.common.math import assert_le, assert_not_zero from starkware.cairo.common.math_cmp import is_nn from starkware.cairo.common.uint256 import Uint256 from starkware.starknet.common.syscalls import ( @@ -86,7 +86,9 @@ func execute_from_outside{ // Starknet validation if (outside_execution.caller != 'ANY_CALLER') { - assert caller = outside_execution.caller; + with_attr error_message("Execute from outside: invalid caller") { + assert caller = outside_execution.caller; + } } let (block_timestamp) = get_block_timestamp(); let too_early = is_nn(outside_execution.execute_after - block_timestamp); @@ -102,7 +104,7 @@ func execute_from_outside{ } let (tx_info) = get_tx_info(); let version = tx_info.version; - with_attr error_message("Deprecated tx version: {version}") { + with_attr error_message("Deprecated tx version: 0") { assert_le(1, version); } @@ -114,8 +116,17 @@ func execute_from_outside{ // Unpack the tx data let packed_tx_data_len = [call_array].data_len; + with_attr error_message("Execute from outside: packed_tx_data_len is zero or out of range") { + assert_not_zero(packed_tx_data_len); + assert [range_check_ptr] = packed_tx_data_len; + let range_check_ptr = range_check_ptr + 1; + } let packed_tx_data = calldata + [call_array].data_offset; let tx_data_len = [packed_tx_data]; + with_attr error_message("Execute from outside: tx_data_len is out of range") { + assert [range_check_ptr] = tx_data_len; + let range_check_ptr = range_check_ptr + 1; + } let (tx_data) = Helpers.load_packed_bytes( packed_tx_data_len - 1, packed_tx_data + 1, tx_data_len ); diff --git a/src/kakarot/accounts/library.cairo b/src/kakarot/accounts/library.cairo index 5713fb66f..a58cd19d1 100644 --- a/src/kakarot/accounts/library.cairo +++ b/src/kakarot/accounts/library.cairo @@ -5,11 +5,10 @@ from starkware.cairo.common.alloc import alloc from starkware.cairo.common.bool import FALSE, TRUE from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin -from starkware.cairo.common.math import split_int, split_felt +from starkware.cairo.common.math import split_int from starkware.cairo.common.memcpy import memcpy -from starkware.cairo.common.uint256 import Uint256, uint256_not, uint256_le +from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.common.math_cmp import is_nn, is_le_felt -from starkware.cairo.common.math import assert_le_felt from starkware.starknet.common.syscalls import ( StorageRead, StorageWrite, @@ -29,7 +28,6 @@ from kakarot.accounts.model import CallArray from kakarot.errors import Errors from kakarot.constants import Constants from utils.eth_transaction import EthTransaction -from utils.uint256 import uint256_add from utils.bytes import bytes_to_bytes8_little_endian from utils.signature import Signature from utils.utils import Helpers @@ -163,7 +161,7 @@ namespace AccountContract { let s = Uint256(signature[2], signature[3]); let v = signature[4]; - let tx_type = EthTransaction.get_tx_type(tx_data); + let tx_type = EthTransaction.get_tx_type(tx_data_len, tx_data); local y_parity: felt; local pre_eip155_tx: felt; if (tx_type == 0) { @@ -205,94 +203,17 @@ namespace AccountContract { helpers_class=helpers_class, ); - let tx = EthTransaction.decode(tx_data_len, tx_data); - - // Whitelisting pre-eip155 or validate chain_id for post eip155 + // Whitelisting pre-eip155 + let (is_authorized) = Account_authorized_message_hashes.read(msg_hash); if (pre_eip155_tx != FALSE) { - let (is_authorized) = Account_authorized_message_hashes.read(msg_hash); with_attr error_message("Unauthorized pre-eip155 transaction") { assert is_authorized = TRUE; } - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - } else { - with_attr error_message("Invalid chain id") { - assert tx.chain_id = chain_id; - } - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - } - let syscall_ptr = cast([ap - 3], felt*); - let pedersen_ptr = cast([ap - 2], HashBuiltin*); - let range_check_ptr = [ap - 1]; - - // Validate nonce - let (account_nonce) = Account_nonce.read(); - with_attr error_message("Invalid nonce") { - assert tx.signer_nonce = account_nonce; - } - - // Validate gas and value - let (kakarot_address) = Ownable_owner.read(); - let (native_token_address) = IKakarot.get_native_token(kakarot_address); - let (contract_address) = get_contract_address(); - let (balance) = IERC20.balanceOf(native_token_address, contract_address); - - with_attr error_message("Gas limit too high") { - assert_le_felt(tx.gas_limit, 2 ** 64 - 1); - } - - with_attr error_message("Max fee per gas too high") { - assert [range_check_ptr] = tx.max_fee_per_gas; - let range_check_ptr = range_check_ptr + 1; - } - - let max_gas_fee = tx.gas_limit * tx.max_fee_per_gas; - let (max_fee_high, max_fee_low) = split_felt(max_gas_fee); - let (tx_cost, carry) = uint256_add(tx.amount, Uint256(low=max_fee_low, high=max_fee_high)); - assert carry = 0; - let (is_balance_enough) = uint256_le(tx_cost, balance); - with_attr error_message("Not enough ETH to pay msg.value + max gas fees") { - assert is_balance_enough = TRUE; - } - - let (block_gas_limit) = IKakarot.get_block_gas_limit(kakarot_address); - let tx_gas_fits_in_block = is_nn(block_gas_limit - tx.gas_limit); - with_attr error_message("Transaction gas_limit > Block gas_limit") { - assert tx_gas_fits_in_block = TRUE; } - let (block_base_fee) = IKakarot.get_base_fee(kakarot_address); - let enough_fee = is_nn(tx.max_fee_per_gas - block_base_fee); - with_attr error_message("Max fee per gas too low") { - assert enough_fee = TRUE; - } - - with_attr error_message("Max priority fee greater than max fee per gas") { - assert_le_felt(tx.max_priority_fee_per_gas, tx.max_fee_per_gas); - } - - let possible_priority_fee = tx.max_fee_per_gas - block_base_fee; - let priority_fee_is_max_priority_fee = is_nn( - possible_priority_fee - tx.max_priority_fee_per_gas - ); - let priority_fee_per_gas = priority_fee_is_max_priority_fee * tx.max_priority_fee_per_gas + - (1 - priority_fee_is_max_priority_fee) * possible_priority_fee; - let effective_gas_price = priority_fee_per_gas + block_base_fee; - // Send tx to Kakarot - let (return_data_len, return_data, success, gas_used) = IKakarot.eth_send_transaction( - contract_address=kakarot_address, - to=tx.destination, - gas_limit=tx.gas_limit, - gas_price=effective_gas_price, - value=tx.amount, - data_len=tx.payload_len, - data=tx.payload, - access_list_len=tx.access_list_len, - access_list=tx.access_list, + let (return_data_len, return_data, success, gas_used) = IKakarot.eth_send_raw_unsigned_tx( + contract_address=kakarot_address, tx_data_len=tx_data_len, tx_data=tx_data ); // See Argent account diff --git a/src/kakarot/eth_rpc.cairo b/src/kakarot/eth_rpc.cairo index b982e194a..9f54a1b01 100644 --- a/src/kakarot/eth_rpc.cairo +++ b/src/kakarot/eth_rpc.cairo @@ -1,9 +1,12 @@ %lang starknet +from openzeppelin.access.ownable.library import Ownable_owner +from starkware.cairo.common.bool import FALSE, TRUE from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin -from starkware.cairo.common.math_cmp import is_not_zero +from starkware.cairo.common.math import assert_le, assert_nn, split_felt +from starkware.cairo.common.math_cmp import is_not_zero, is_nn from starkware.cairo.common.registers import get_fp_and_pc -from starkware.cairo.common.uint256 import Uint256 +from starkware.cairo.common.uint256 import Uint256, uint256_add, uint256_le from starkware.starknet.common.syscalls import get_caller_address, get_tx_info from backend.starknet import Starknet @@ -12,6 +15,7 @@ from kakarot.interfaces.interfaces import IAccount, IERC20 from kakarot.library import Kakarot from kakarot.model import model from kakarot.storages import Kakarot_native_token_address +from utils.eth_transaction import EthTransaction from utils.maths import unsigned_div_rem from utils.utils import Helpers @@ -180,7 +184,6 @@ func eth_estimate_gas{ // @return return_data An array of returned felts // @return success An boolean, TRUE if the transaction succeeded, FALSE otherwise // @return gas_used The amount of gas used by the transaction -@external func eth_send_transaction{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* }( @@ -221,3 +224,100 @@ func eth_send_transaction{ return result; } + +// @notice The eth_send_raw_unsigned_tx. Modified version of eth_sendRawTransaction function described in the spec. +// See https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction +// @dev This function takes the transaction data unsigned. Signature validation should be done before calling this function. +// @param tx_data_len The length of the unsigned transaction data +// @param tx_data The unsigned transaction data +// @return return_data_len The length of the return_data +// @return return_data An array of returned felts +// @return success An boolean, TRUE if the transaction succeeded, FALSE otherwise +// @return gas_used The amount of gas used by the transaction +@external +func eth_send_raw_unsigned_tx{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* +}(tx_data_len: felt, tx_data: felt*) -> ( + return_data_len: felt, return_data: felt*, success: felt, gas_used: felt +) { + alloc_locals; + let tx = EthTransaction.decode(tx_data_len, tx_data); + + // Validate chain_id for post eip155 + let (chain_id) = Kakarot.eth_chain_id(); + if (tx.chain_id.is_some != FALSE) { + with_attr error_message("Invalid chain id") { + assert tx.chain_id.value = chain_id; + } + } + + // Get the caller address + let (caller_address) = get_caller_address(); + + // Validate nonce + let (account_nonce) = IAccount.get_nonce(contract_address=caller_address); + with_attr error_message("Invalid nonce") { + assert tx.signer_nonce = account_nonce; + } + + // Validate gas + with_attr error_message("Gas limit too high") { + assert [range_check_ptr] = tx.gas_limit; + let range_check_ptr = range_check_ptr + 1; + assert_le(tx.gas_limit, 2 ** 64 - 1); + } + + with_attr error_message("Max fee per gas too high") { + assert [range_check_ptr] = tx.max_fee_per_gas; + let range_check_ptr = range_check_ptr + 1; + } + + let (block_gas_limit) = Kakarot.get_block_gas_limit(); + with_attr error_message("Transaction gas_limit > Block gas_limit") { + assert_nn(block_gas_limit - tx.gas_limit); + } + + let (block_base_fee) = Kakarot.get_base_fee(); + with_attr error_message("Max fee per gas too low") { + assert_nn(tx.max_fee_per_gas - block_base_fee); + } + + with_attr error_message("Max priority fee greater than max fee per gas") { + assert [range_check_ptr] = tx.max_priority_fee_per_gas; + let range_check_ptr = range_check_ptr + 1; + assert_le(tx.max_priority_fee_per_gas, tx.max_fee_per_gas); + } + + let (evm_address) = IAccount.get_evm_address(caller_address); + let (balance) = eth_get_balance(evm_address); + let max_gas_fee = tx.gas_limit * tx.max_fee_per_gas; + let (max_fee_high, max_fee_low) = split_felt(max_gas_fee); + let (tx_cost, carry) = uint256_add(tx.amount, Uint256(low=max_fee_low, high=max_fee_high)); + assert carry = 0; + let (is_balance_enough) = uint256_le(tx_cost, balance); + with_attr error_message("Not enough ETH to pay msg.value + max gas fees") { + assert is_balance_enough = TRUE; + } + + let possible_priority_fee = tx.max_fee_per_gas - block_base_fee; + let priority_fee_is_max_priority_fee = is_nn( + possible_priority_fee - tx.max_priority_fee_per_gas + ); + let priority_fee_per_gas = priority_fee_is_max_priority_fee * tx.max_priority_fee_per_gas + ( + 1 - priority_fee_is_max_priority_fee + ) * possible_priority_fee; + let effective_gas_price = priority_fee_per_gas + block_base_fee; + + let (return_data_len, return_data, success, gas_used) = eth_send_transaction( + to=tx.destination, + gas_limit=tx.gas_limit, + gas_price=effective_gas_price, + value=tx.amount, + data_len=tx.payload_len, + data=tx.payload, + access_list_len=tx.access_list_len, + access_list=tx.access_list, + ); + + return (return_data_len, return_data, success, gas_used); +} diff --git a/src/kakarot/evm.cairo b/src/kakarot/evm.cairo index fcedc448c..1cc11c787 100644 --- a/src/kakarot/evm.cairo +++ b/src/kakarot/evm.cairo @@ -62,6 +62,7 @@ namespace EVM { is_create=evm.message.is_create, depth=evm.message.depth, env=evm.message.env, + cairo_precompile_called=evm.message.cairo_precompile_called, ); tempvar evm = new model.EVM( @@ -279,6 +280,7 @@ namespace EVM { is_create=self.message.is_create, depth=self.message.depth, env=self.message.env, + cairo_precompile_called=self.message.cairo_precompile_called, ); if (is_valid_jumpdest == FALSE) { diff --git a/src/kakarot/instructions/system_operations.cairo b/src/kakarot/instructions/system_operations.cairo index aed64adb1..13d9dcfca 100644 --- a/src/kakarot/instructions/system_operations.cairo +++ b/src/kakarot/instructions/system_operations.cairo @@ -176,6 +176,7 @@ namespace SystemOperations { is_create=TRUE, depth=evm.message.depth + 1, env=evm.message.env, + cairo_precompile_called=evm.message.cairo_precompile_called, ); let child_evm = EVM.init(message, gas_limit); let stack = Stack.init(); @@ -900,6 +901,7 @@ namespace CallHelper { is_create=FALSE, depth=evm.message.depth + 1, env=evm.message.env, + cairo_precompile_called=evm.message.cairo_precompile_called, ); let child_evm = EVM.init(message, gas); @@ -945,12 +947,29 @@ namespace CallHelper { code_account, evm.message.valid_jumpdests_start, evm.message.valid_jumpdests ); State.update_account(code_account); - + tempvar message = new model.Message( + bytecode=evm.message.parent.evm.message.bytecode, + bytecode_len=evm.message.parent.evm.message.bytecode_len, + valid_jumpdests_start=evm.message.parent.evm.message.valid_jumpdests_start, + valid_jumpdests=evm.message.parent.evm.message.valid_jumpdests, + calldata=evm.message.parent.evm.message.calldata, + calldata_len=evm.message.parent.evm.message.calldata_len, + value=evm.message.parent.evm.message.value, + caller=evm.message.parent.evm.message.caller, + parent=evm.message.parent.evm.message.parent, + address=evm.message.parent.evm.message.address, + code_address=evm.message.parent.evm.message.code_address, + read_only=evm.message.parent.evm.message.read_only, + is_create=evm.message.parent.evm.message.is_create, + depth=evm.message.parent.evm.message.depth, + env=evm.message.parent.evm.message.env, + cairo_precompile_called=evm.message.cairo_precompile_called, + ); if (evm.reverted == Errors.EXCEPTIONAL_HALT) { // If the call has halted exceptionnaly, the return_data is empty // and nothing is copied to memory, and the gas is not returned; tempvar evm = new model.EVM( - message=evm.message.parent.evm.message, + message=message, return_data_len=0, return_data=evm.return_data, program_counter=evm.message.parent.evm.program_counter + 1, @@ -969,7 +988,7 @@ namespace CallHelper { Memory.store_n(actual_output_size, evm.return_data, ret_offset.low); tempvar evm = new model.EVM( - message=evm.message.parent.evm.message, + message=message, return_data_len=evm.return_data_len, return_data=evm.return_data, program_counter=evm.message.parent.evm.program_counter + 1, @@ -1146,6 +1165,24 @@ namespace CreateHelper { }(evm: model.EVM*) -> model.EVM* { alloc_locals; + tempvar message = new model.Message( + bytecode=evm.message.parent.evm.message.bytecode, + bytecode_len=evm.message.parent.evm.message.bytecode_len, + valid_jumpdests_start=evm.message.parent.evm.message.valid_jumpdests_start, + valid_jumpdests=evm.message.parent.evm.message.valid_jumpdests, + calldata=evm.message.parent.evm.message.calldata, + calldata_len=evm.message.parent.evm.message.calldata_len, + value=evm.message.parent.evm.message.value, + caller=evm.message.parent.evm.message.caller, + parent=evm.message.parent.evm.message.parent, + address=evm.message.parent.evm.message.address, + code_address=evm.message.parent.evm.message.code_address, + read_only=evm.message.parent.evm.message.read_only, + is_create=evm.message.parent.evm.message.is_create, + depth=evm.message.parent.evm.message.depth, + env=evm.message.parent.evm.message.env, + cairo_precompile_called=evm.message.cairo_precompile_called, + ); // Reverted during execution - either REVERT or exceptional if (evm.reverted != FALSE) { let is_exceptional_revert = is_not_zero(Errors.REVERT - evm.reverted); @@ -1161,7 +1198,7 @@ namespace CreateHelper { tempvar state = evm.message.parent.state; tempvar evm = new model.EVM( - message=evm.message.parent.evm.message, + message=message, return_data_len=return_data_len, return_data=evm.return_data, program_counter=evm.message.parent.evm.program_counter + 1, @@ -1196,7 +1233,7 @@ namespace CreateHelper { tempvar state = evm.message.parent.state; tempvar evm = new model.EVM( - message=evm.message.parent.evm.message, + message=message, return_data_len=0, return_data=evm.return_data, program_counter=evm.message.parent.evm.program_counter + 1, diff --git a/src/kakarot/interfaces/interfaces.cairo b/src/kakarot/interfaces/interfaces.cairo index 3ba5e2f6f..e0c19abf0 100644 --- a/src/kakarot/interfaces/interfaces.cairo +++ b/src/kakarot/interfaces/interfaces.cairo @@ -189,6 +189,11 @@ namespace IKakarot { func eth_chain_id() -> (chain_id: felt) { } + + func eth_send_raw_unsigned_tx(tx_data_len: felt, tx_data: felt*) -> ( + return_data_len: felt, return_data: felt*, success: felt, gas_used: felt + ) { + } } @contract_interface diff --git a/src/kakarot/interpreter.cairo b/src/kakarot/interpreter.cairo index fa77b6c93..c15d7e918 100644 --- a/src/kakarot/interpreter.cairo +++ b/src/kakarot/interpreter.cairo @@ -89,6 +89,37 @@ namespace Interpreter { let evm_reverted = is_not_zero(evm.reverted); let success = (1 - precompile_reverted) * (1 - evm_reverted); let evm = EVM.stop(evm, output_len, output, 1 - success); + let is_cairo_precompile_called = PrecompilesHelpers.is_kakarot_precompile( + evm.message.code_address.evm + ); + tempvar message = new model.Message( + bytecode=evm.message.bytecode, + bytecode_len=evm.message.bytecode_len, + valid_jumpdests_start=evm.message.valid_jumpdests_start, + valid_jumpdests=evm.message.valid_jumpdests, + calldata=evm.message.calldata, + calldata_len=evm.message.calldata_len, + value=evm.message.value, + caller=evm.message.caller, + parent=evm.message.parent, + address=evm.message.address, + code_address=evm.message.code_address, + read_only=evm.message.read_only, + is_create=evm.message.is_create, + depth=evm.message.depth, + env=evm.message.env, + cairo_precompile_called=is_cairo_precompile_called, + ); + tempvar evm = new model.EVM( + message=message, + return_data_len=evm.return_data_len, + return_data=evm.return_data, + program_counter=evm.program_counter, + stopped=evm.stopped, + gas_left=evm.gas_left, + gas_refund=evm.gas_refund, + reverted=evm.reverted, + ); return evm; } else { let (return_data: felt*) = alloc(); @@ -862,6 +893,7 @@ namespace Interpreter { is_create=is_deploy_tx, depth=0, env=env, + cairo_precompile_called=FALSE, ); let stack = Stack.init(); @@ -897,7 +929,7 @@ namespace Interpreter { // Charge the gas fee to the user without setting up a transfer. // Transfers with the exact amounts will be performed post-execution. - // Note: balance > effective_fee was verified in AccountContract.execute_from_outside() + // Note: balance > effective_fee was verified in eth_send_raw_unsigned_tx() let max_fee = gas_limit * env.gas_price; let (fee_high, fee_low) = split_felt(max_fee); let max_fee_u256 = Uint256(low=fee_low, high=fee_high); @@ -954,6 +986,10 @@ namespace Interpreter { // Only the gas fee paid will be committed. State.finalize{state=state}(); if (evm.reverted != 0) { + with_attr error_message( + "EVM tx reverted, reverting SN tx because of previous calls to cairo precompiles") { + assert evm.message.cairo_precompile_called = FALSE; + } tempvar state = State.init(); } else { tempvar state = state; diff --git a/src/kakarot/kakarot.cairo b/src/kakarot/kakarot.cairo index 1496e7350..6d73141fd 100644 --- a/src/kakarot/kakarot.cairo +++ b/src/kakarot/kakarot.cairo @@ -25,6 +25,7 @@ from kakarot.eth_rpc import ( eth_call, eth_estimate_gas, eth_send_transaction, + eth_send_raw_unsigned_tx, ) // Constructor diff --git a/src/kakarot/library.cairo b/src/kakarot/library.cairo index c0a233ab6..07d8c68fd 100644 --- a/src/kakarot/library.cairo +++ b/src/kakarot/library.cairo @@ -125,7 +125,7 @@ namespace Kakarot { chain_id: felt ) { let (tx_info) = get_tx_info(); - let (_, chain_id) = unsigned_div_rem(tx_info.chain_id, 2 ** 32); + let (_, chain_id) = unsigned_div_rem(tx_info.chain_id, 2 ** 53); return (chain_id=chain_id); } @@ -218,7 +218,7 @@ namespace Kakarot { block_gas_limit: felt ) { let (block_gas_limit) = Kakarot_block_gas_limit.read(); - return (block_gas_limit,); + return (block_gas_limit=block_gas_limit); } // @notice Deploy a new externally owned account. diff --git a/src/kakarot/model.cairo b/src/kakarot/model.cairo index 308b5f730..ad8729201 100644 --- a/src/kakarot/model.cairo +++ b/src/kakarot/model.cairo @@ -138,6 +138,7 @@ namespace model { is_create: felt, depth: felt, env: Environment*, + cairo_precompile_called: felt, } // @dev Stores all data relevant to the current execution context. @@ -217,6 +218,6 @@ namespace model { payload: felt*, access_list_len: felt, access_list: felt*, - chain_id: felt, + chain_id: Option, } } diff --git a/src/utils/eth_transaction.cairo b/src/utils/eth_transaction.cairo index 289bfd263..a272b1688 100644 --- a/src/utils/eth_transaction.cairo +++ b/src/utils/eth_transaction.cairo @@ -4,6 +4,7 @@ from starkware.cairo.common.alloc import alloc from starkware.cairo.common.bool import TRUE, FALSE from starkware.cairo.common.cairo_builtins import BitwiseBuiltin, HashBuiltin from starkware.cairo.common.math_cmp import is_not_zero, is_nn +from starkware.cairo.common.math import assert_not_zero from starkware.cairo.common.memcpy import memcpy from starkware.cairo.common.uint256 import Uint256 @@ -56,6 +57,7 @@ namespace EthTransaction { // pre eip-155 txs have 6 fields, post eip-155 txs have 9 fields if (items_len == 6) { + tempvar is_some = 0; tempvar chain_id = 0; } else { assert items_len = 9; @@ -63,7 +65,11 @@ namespace EthTransaction { assert items[7].is_list = FALSE; assert items[8].is_list = FALSE; let chain_id = Helpers.bytes_to_felt(items[6].data_len, items[6].data); + + tempvar is_some = 1; + tempvar chain_id = chain_id; } + let is_some = [ap - 2]; let chain_id = [ap - 1]; tempvar tx = new model.EthTransaction( @@ -77,7 +83,7 @@ namespace EthTransaction { payload=payload, access_list_len=0, access_list=cast(0, felt*), - chain_id=chain_id, + chain_id=model.Option(is_some=is_some, value=chain_id), ); return tx; } @@ -134,7 +140,7 @@ namespace EthTransaction { payload=payload, access_list_len=access_list_len, access_list=access_list, - chain_id=chain_id, + chain_id=model.Option(is_some=1, value=chain_id), ); return tx; } @@ -192,7 +198,7 @@ namespace EthTransaction { payload=payload, access_list_len=access_list_len, access_list=access_list, - chain_id=chain_id, + chain_id=model.Option(is_some=1, value=chain_id), ); return tx; } @@ -201,8 +207,13 @@ namespace EthTransaction { // @dev This function checks if a raw transaction is a legacy Ethereum transaction by checking the transaction type // according to EIP-2718. If the transaction type is greater than or equal to 0xc0, it's a legacy transaction. // See https://eips.ethereum.org/EIPS/eip-2718#transactiontype-only-goes-up-to-0x7f + // @param tx_data_len The len of the raw transaction data // @param tx_data The raw transaction data - func get_tx_type{range_check_ptr}(tx_data: felt*) -> felt { + func get_tx_type{range_check_ptr}(tx_data_len: felt, tx_data: felt*) -> felt { + with_attr error_message("tx_data_len is zero") { + assert_not_zero(tx_data_len); + } + let type = [tx_data]; let is_legacy = is_nn(type - 0xc0); if (is_legacy != FALSE) { @@ -217,7 +228,7 @@ namespace EthTransaction { func decode{bitwise_ptr: BitwiseBuiltin*, range_check_ptr}( tx_data_len: felt, tx_data: felt* ) -> model.EthTransaction* { - let tx_type = get_tx_type(tx_data); + let tx_type = get_tx_type(tx_data_len, tx_data); let is_supported = is_nn(2 - tx_type); with_attr error_message("Kakarot: transaction type not supported") { assert is_supported = TRUE; diff --git a/src/utils/rlp.cairo b/src/utils/rlp.cairo index e41944049..5f1762693 100644 --- a/src/utils/rlp.cairo +++ b/src/utils/rlp.cairo @@ -1,11 +1,15 @@ -from starkware.cairo.common.bool import FALSE +from starkware.cairo.common.bool import FALSE, TRUE from starkware.cairo.common.math_cmp import is_nn +from starkware.cairo.common.math import assert_not_zero, assert_nn from starkware.cairo.common.alloc import alloc from utils.utils import Helpers // The namespace handling all RLP computation namespace RLP { + const TYPE_STRING = 0; + const TYPE_LIST = 1; + // The type returned when data is RLP decoded // An RLP Item is either a byte array or a list of other RLP Items. // The data is either a pointer to the first byte in the array or a pointer to the first Item in the list. @@ -18,11 +22,14 @@ namespace RLP { is_list: felt, } - // Decodes the type of an RLP item. - // @dev type=0 means the item is a byte array, type=1 means the item is a list. - // @returns (type, data_offset, data_size) where data_offset is the offset of the data in the - // original array and data_size is the size of the data. - func decode_type{range_check_ptr}(data_len: felt, data: felt*) -> (felt, felt, felt) { + // @notive Decode the type of an RLP item. + // @dev Unsafe function, does not check if the data is long enough (can be exploited by a malicious prover). + // Always check afterwards that outputs are compatible with the associated data_len. + // @param data The RLP encoded data. + // @return rlp_type The type of the RLP data (string or list). + // @return offset The offset of the data in the RLP encoded data. + // @return len The length of the data. + func decode_type{range_check_ptr}(data: felt*) -> (rlp_type: felt, offset: felt, len: felt) { alloc_locals; let prefix = [data]; @@ -30,30 +37,30 @@ namespace RLP { // Char let is_le_127 = is_nn(0x7f - prefix); if (is_le_127 != FALSE) { - return (0, 0, 1); + return (TYPE_STRING, 0, 1); } let is_le_183 = is_nn(0xb7 - prefix); // a max 55 bytes long string if (is_le_183 != FALSE) { - return (0, 1, prefix - 0x80); + return (TYPE_STRING, 1, prefix - 0x80); } let is_le_191 = is_nn(0xbf - prefix); // string longer than 55 bytes if (is_le_191 != FALSE) { local len_bytes_count = prefix - 0xb7; let string_len = Helpers.bytes_to_felt(len_bytes_count, data + 1); - return (0, 1 + len_bytes_count, string_len); + return (TYPE_STRING, 1 + len_bytes_count, string_len); } let is_le_247 = is_nn(0xf7 - prefix); // list 0-55 bytes long if (is_le_247 != FALSE) { local list_len = prefix - 0xc0; - return (1, 1, list_len); + return (TYPE_LIST, 1, list_len); } local len_bytes_count = prefix - 0xf7; let list_len = Helpers.bytes_to_felt(len_bytes_count, data + 1); - return (1, 1 + len_bytes_count, list_len); + return (TYPE_LIST, 1 + len_bytes_count, list_len); } // @notice Decodes a Recursive Length Prefix (RLP) encoded data. @@ -75,23 +82,23 @@ namespace RLP { return 0; } - let (rlp_type, offset, len) = decode_type(data_len=data_len, data=data); + let (rlp_type, offset, len) = decode_type(data); + local remaining_data_len = data_len - len - offset; + with_attr error_message("RLP data too short for declared length") { + assert_nn(remaining_data_len); + } - if (rlp_type == 1) { - // Case list + if (rlp_type == TYPE_LIST) { let (sub_items: Item*) = alloc(); let sub_items_len = decode_raw(items=sub_items, data_len=len, data=data + offset); - assert [items] = Item(data_len=sub_items_len, data=cast(sub_items, felt*), is_list=1); + assert [items] = Item(sub_items_len, cast(sub_items, felt*), TRUE); tempvar range_check_ptr = range_check_ptr; } else { - // Case string or empty list. If the list or string is empty, - // the data_len is 0 so not passing an empty data segment is fine. - assert [items] = Item(data_len=len, data=data + offset, is_list=rlp_type); + assert [items] = Item(data_len=len, data=data + offset, is_list=FALSE); tempvar range_check_ptr = range_check_ptr; } tempvar items = items + Item.SIZE; - let remaining_data_len = data_len - len - offset; let items_len = decode_raw( items=items, data_len=remaining_data_len, data=data + offset + len ); @@ -100,7 +107,7 @@ namespace RLP { func decode{range_check_ptr}(items: Item*, data_len: felt, data: felt*) { alloc_locals; - let (rlp_type, offset, len) = decode_type(data_len=data_len, data=data); + let (rlp_type, offset, len) = decode_type(data); local extra_bytes = data_len - offset - len; with_attr error_message("RLP string ends with {extra_bytes} superfluous bytes") { assert extra_bytes = 0; diff --git a/tests/end_to_end/CairoPrecompiles/Pragma/test_pragma_precompile.py b/tests/end_to_end/CairoPrecompiles/Pragma/test_pragma_precompile.py index 34eab4f38..b865b9db7 100644 --- a/tests/end_to_end/CairoPrecompiles/Pragma/test_pragma_precompile.py +++ b/tests/end_to_end/CairoPrecompiles/Pragma/test_pragma_precompile.py @@ -5,7 +5,7 @@ from kakarot_scripts.utils.kakarot import deploy from kakarot_scripts.utils.starknet import get_contract, get_deployments, invoke -from tests.utils.errors import evm_error +from tests.utils.errors import cairo_error ENTRY_TYPE_INDEX = {"SpotEntry": 0, "FutureEntry": 1, "GenericEntry": 2} @@ -150,5 +150,7 @@ async def test_should_fail_unauthorized_caller(self, pragma_caller, data_type): ) solidity_input = serialize_data_type(data_type) - with evm_error("CairoLib: call_contract failed"): + with cairo_error( + "EVM tx reverted, reverting SN tx because of previous calls to cairo precompiles" + ): await pragma_caller.getDataMedianSpot(solidity_input) diff --git a/tests/end_to_end/CairoPrecompiles/test_cairo_precompiles.py b/tests/end_to_end/CairoPrecompiles/test_cairo_precompiles.py index 1fac904b9..e9a73299b 100644 --- a/tests/end_to_end/CairoPrecompiles/test_cairo_precompiles.py +++ b/tests/end_to_end/CairoPrecompiles/test_cairo_precompiles.py @@ -3,7 +3,7 @@ from kakarot_scripts.utils.kakarot import deploy, get_eoa from kakarot_scripts.utils.starknet import get_contract, invoke, wait_for_transaction -from tests.utils.errors import evm_error +from tests.utils.errors import cairo_error @pytest_asyncio.fixture() @@ -69,7 +69,9 @@ async def test_should_fail_precompile_caller_not_whitelisted( cairo_counter_caller = await deploy( "CairoPrecompiles", "CairoCounterCaller", cairo_counter.address ) - with evm_error("CairoLib: call_contract failed"): + with cairo_error( + "EVM tx reverted, reverting SN tx because of previous calls to cairo precompiles" + ): await cairo_counter_caller.incrementCairoCounter() async def test_last_caller_address_should_be_eoa(self, cairo_counter_caller): diff --git a/tests/end_to_end/CairoPrecompiles/test_dual_vm_token.py b/tests/end_to_end/CairoPrecompiles/test_dual_vm_token.py index 9eb47af21..e395c12e4 100644 --- a/tests/end_to_end/CairoPrecompiles/test_dual_vm_token.py +++ b/tests/end_to_end/CairoPrecompiles/test_dual_vm_token.py @@ -5,6 +5,7 @@ from kakarot_scripts.utils.starknet import deploy as deploy_starknet from kakarot_scripts.utils.starknet import get_contract as get_contract_starknet from kakarot_scripts.utils.starknet import invoke +from tests.utils.errors import cairo_error @pytest_asyncio.fixture() @@ -115,3 +116,13 @@ async def test_should_transfer_from( assert balance_owner_before - amount == balance_owner_after assert balance_other_before + amount == balance_other_after + + async def test_should_revert_tx_cairo_precompiles( + self, starknet_token, dual_vm_token, owner, other + ): + with cairo_error( + "EVM tx reverted, reverting SN tx because of previous calls to cairo precompiles" + ): + await dual_vm_token.transfer( + other.address, 1, gas_limit=45_000 + ) # fails with out of gas diff --git a/tests/end_to_end/Solmate/conftest.py b/tests/end_to_end/Solmate/conftest.py index 0e27a2a27..10d70306a 100644 --- a/tests/end_to_end/Solmate/conftest.py +++ b/tests/end_to_end/Solmate/conftest.py @@ -3,9 +3,9 @@ @pytest_asyncio.fixture(scope="module") async def from_wallet(new_eoa): - return await new_eoa() + return await new_eoa(0.1) @pytest_asyncio.fixture(scope="module") async def to_wallet(new_eoa): - return await new_eoa() + return await new_eoa(0.1) diff --git a/tests/end_to_end/Solmate/test_erc20.py b/tests/end_to_end/Solmate/test_erc20.py index 540a0010c..fb9cd56a9 100644 --- a/tests/end_to_end/Solmate/test_erc20.py +++ b/tests/end_to_end/Solmate/test_erc20.py @@ -229,36 +229,9 @@ async def test_should_permit(self, erc_20, owner, other): } ] assert await erc_20.allowance(owner.address, other.address) == TEST_SUPPLY - assert await erc_20.nonces(owner.address) == 1 + assert await erc_20.nonces(owner.address) == nonce + 1 - async def test_permit_should_fail_with_bad_nonce(self, erc_20, owner, other): - bad_nonce = 1 - deadline = 2**256 - 1 - digest = get_approval_digest( - "Kakarot Token", - erc_20.address, - { - "owner": owner.address, - "spender": other.address, - "value": MAX_INT, - }, - bad_nonce, - deadline, - ) - v, r, s = ec_sign(digest, owner.private_key) - with evm_error("INVALID_SIGNER"): - await erc_20.permit( - owner.address, - other.address, - TEST_SUPPLY, - deadline, - v, - r, - s, - caller_eoa=owner.starknet_contract, - ) - - async def test_permit_should_fail_with_bad_deadline( + async def test_should_fail_with_bad_deadline( self, erc_20, block_timestamp, owner, other ): nonce = await erc_20.nonces(owner.address) @@ -289,7 +262,7 @@ async def test_permit_should_fail_with_bad_deadline( caller_eoa=owner.starknet_contract, ) - async def test_permit_should_fail_on_replay(self, erc_20, owner, other): + async def test_should_fail_on_replay(self, erc_20, owner, other): nonce = await erc_20.nonces(owner.address) deadline = 2**256 - 1 digest = get_approval_digest( diff --git a/tests/end_to_end/conftest.py b/tests/end_to_end/conftest.py index ecbb9e8b8..b9f3df2e7 100644 --- a/tests/end_to_end/conftest.py +++ b/tests/end_to_end/conftest.py @@ -9,8 +9,11 @@ from starknet_py.net.account.account import Account from kakarot_scripts.constants import RPC_CLIENT, NetworkType +from kakarot_scripts.utils.kakarot import eth_balance_of +from kakarot_scripts.utils.kakarot import get_contract as get_solidity_contract from kakarot_scripts.utils.kakarot import get_eoa from kakarot_scripts.utils.starknet import ( + call, get_contract, get_eth_contract, get_starknet_account, @@ -48,24 +51,46 @@ def max_fee(): return int(5e17) -@pytest.fixture(scope="session") -def new_eoa(max_fee) -> Wallet: +@pytest_asyncio.fixture(scope="session") +async def new_eoa(deployer) -> Wallet: """ Return a factory to create a new EOA with enough ETH to pass ~100 tx by default. """ - async def _factory(amount=None): + deployed = [] + + async def _factory(amount=0): private_key: PrivateKey = generate_random_private_key() - return Wallet( + wallet = Wallet( address=private_key.public_key.to_checksum_address(), private_key=private_key, - starknet_contract=await get_eoa( - private_key, amount=amount or (100 * max_fee / 1e18) - ), + starknet_contract=await get_eoa(private_key, amount=amount), + ) + deployed.append(wallet) + return wallet + + yield _factory + + bridge_address = (await call("kakarot", "get_coinbase")).coinbase + bridge = await get_solidity_contract( + "CairoPrecompiles", "EthStarknetBridge", address=bridge_address + ) + gas_price = (await call("kakarot", "get_base_fee")).base_fee + gas_limit = 40_000 + tx_cost = gas_limit * gas_price + for wallet in deployed: + balance = await eth_balance_of(wallet.address) + if balance < tx_cost: + continue + + await bridge.transfer( + deployer.address, + balance - tx_cost, + caller_eoa=wallet.starknet_contract, + gas_limit=gas_limit, + gas_price=gas_price, ) - - return _factory @pytest_asyncio.fixture(scope="session") @@ -73,7 +98,7 @@ async def owner(new_eoa): """ Return the main caller of all tests. """ - return await new_eoa() + return await new_eoa(0.1) @pytest_asyncio.fixture(scope="module") @@ -81,7 +106,7 @@ async def other(new_eoa): """ Just another EOA. """ - return await new_eoa() + return await new_eoa(0.1) @pytest_asyncio.fixture(scope="session") diff --git a/tests/end_to_end/test_kakarot.py b/tests/end_to_end/test_kakarot.py index c6c099e30..d592d69ba 100644 --- a/tests/end_to_end/test_kakarot.py +++ b/tests/end_to_end/test_kakarot.py @@ -394,11 +394,11 @@ async def test_should_raise_when_tx_view_entrypoint(self, kakarot, entrypoint): class TestEthRPCEntrypoints: async def test_should_return_native_balance_of(self, new_eoa): - eoa = await new_eoa() + eoa = await new_eoa(0x1234 / 1e18) balance = ( await call("kakarot", "eth_get_balance", int(eoa.address, 16)) ).balance - assert balance == 50000000000000000000 + assert balance == 0x1234 async def test_should_return_transaction_count(self, new_eoa): eoa = await new_eoa() diff --git a/tests/src/kakarot/accounts/test_account_contract.cairo b/tests/src/kakarot/accounts/test_account_contract.cairo index 0f7b1420f..8995fb4d8 100644 --- a/tests/src/kakarot/accounts/test_account_contract.cairo +++ b/tests/src/kakarot/accounts/test_account_contract.cairo @@ -17,7 +17,9 @@ from kakarot.accounts.account_contract import ( set_authorized_pre_eip155_tx, execute_starknet_call, set_code_hash, + execute_from_outside, ) +from kakarot.accounts.model import OutsideExecution, CallArray func test__initialize{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* @@ -124,7 +126,7 @@ func test__execute_from_outside{ tempvar chain_id: felt; %{ - ids.tx_data_len = len(program_input["tx_data"]) + ids.tx_data_len = program_input.get("tx_data_len", len(program_input["tx_data"])) segments.write_arg(ids.tx_data, program_input["tx_data"]) ids.signature_len = len(program_input["signature"]) segments.write_arg(ids.signature, program_input["signature"]) @@ -139,6 +141,50 @@ func test__execute_from_outside{ return (return_data_len, return_data); } +func test__execute_from_outside_entrypoint{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, bitwise_ptr: BitwiseBuiltin*, range_check_ptr +}() -> (felt, felt*) { + // Given + + let (outside_execution: OutsideExecution*) = alloc(); + tempvar call_array_len: felt; + let (call_array: CallArray*) = alloc(); + tempvar calldata_len: felt; + let (calldata) = alloc(); + tempvar signature_len: felt; + let (signature) = alloc(); + + %{ + from itertools import chain + + segments.write_arg(ids.outside_execution.address_, program_input["outside_execution"].values()) + ids.call_array_len = len(program_input["call_array"]) + segments.write_arg( + ids.call_array.address_, + list(chain.from_iterable([ + call_.values() for call_ in program_input["call_array"] + ])), + ) + ids.calldata_len = len(program_input["calldata"]) + segments.write_arg(ids.calldata, program_input["calldata"]) + ids.signature_len = len(program_input["signature"]) + segments.write_arg(ids.signature, program_input["signature"]) + %} + + // When + let (return_data_len, return_data) = execute_from_outside( + [outside_execution], + call_array_len, + call_array, + calldata_len, + calldata, + signature_len, + signature, + ); + + return (return_data_len, return_data); +} + func test__write_jumpdests{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { // Given tempvar jumpdests_len: felt; diff --git a/tests/src/kakarot/accounts/test_account_contract.py b/tests/src/kakarot/accounts/test_account_contract.py index e87ac0b89..d487e1225 100644 --- a/tests/src/kakarot/accounts/test_account_contract.py +++ b/tests/src/kakarot/accounts/test_account_contract.py @@ -6,7 +6,7 @@ import rlp from eth_account.account import Account from eth_utils import keccak -from hypothesis import assume, given, settings +from hypothesis import given, settings from hypothesis.strategies import binary, composite, integers, lists, permutations from starkware.cairo.lang.cairo_constants import DEFAULT_PRIME from starkware.starknet.public.abi import ( @@ -15,12 +15,12 @@ ) from kakarot_scripts.constants import ARACHNID_PROXY_DEPLOYER, ARACHNID_PROXY_SIGNED_TX -from tests.utils.constants import CHAIN_ID, TRANSACTION_GAS_LIMIT, TRANSACTIONS +from kakarot_scripts.utils.uint256 import int_to_uint256 +from tests.utils.constants import CHAIN_ID, TRANSACTIONS from tests.utils.errors import cairo_error from tests.utils.helpers import generate_random_private_key, rlp_encode_signed_data from tests.utils.hints import patch_hint from tests.utils.syscall_handler import SyscallHandler -from tests.utils.uint256 import int_to_uint256 CHAIN_ID_OFFSET = 35 V_OFFSET = 27 @@ -382,272 +382,11 @@ def test_should_raise_invalid_signature_for_invalid_chain_id_when_tx_type0_not_p chain_id=CHAIN_ID + 1, ) - def test_should_raise_invalid_chain_id_tx_type_different_from_0( - self, cairo_run - ): - transaction = { - "type": 2, - "gas": 100_000, - "maxFeePerGas": 2_000_000_000, - "maxPriorityFeePerGas": 2_000_000_000, - "data": "0x616263646566", - "nonce": 34, - "to": "", - "value": 0x00, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - cairo_error(message="Invalid chain id"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=CHAIN_ID + 1, - ) - - @SyscallHandler.patch("Account_nonce", 1) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_should_raise_invalid_nonce(self, cairo_run, transaction): - # explicitly set the nonce in transaction to be different from the patch - transaction = {**transaction, "nonce": 0} - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - cairo_error(message="Invalid nonce"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 0]) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_raise_not_enough_ETH_balance(self, cairo_run, transaction): - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)), - cairo_error(message="Not enough ETH to pay msg.value + max gas fees"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) + @SyscallHandler.patch("IKakarot.get_native_token", lambda _, __: [0xDEAD]) + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: int_to_uint256(10**128)) @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch("IKakarot.get_block_gas_limit", lambda addr, data: [0]) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_raise_transaction_gas_limit_too_high(self, cairo_run, transaction): - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)), - cairo_error(message="Transaction gas_limit > Block gas_limit"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch( - "IKakarot.get_base_fee", lambda addr, data: [TRANSACTION_GAS_LIMIT * 10**10] - ) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_raise_max_fee_per_gas_too_low(self, cairo_run, transaction): - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)), - cairo_error(message="Max fee per gas too low"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @given(gas_limit=integers(min_value=2**64, max_value=DEFAULT_PRIME - 1)) - def test_raise_gas_limit_too_high(self, cairo_run, gas_limit): - transaction = { - "type": 2, - "gas": gas_limit, - "maxFeePerGas": 2_000_000_000, - "maxPriorityFeePerGas": 3_000_000_000, - "data": "0x616263646566", - "nonce": 34, - "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", - "value": 0x5AF3107A4000, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction["nonce"]), - cairo_error(message="Gas limit too high"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction["chainId"], - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @given(maxFeePerGas=integers(min_value=2**128, max_value=DEFAULT_PRIME - 1)) - def test_raise_max_fee_per_gas_too_high(self, cairo_run, maxFeePerGas): - transaction = { - "type": 2, - "gas": 100_000, - "maxFeePerGas": maxFeePerGas, - "maxPriorityFeePerGas": 3_000_000_000, - "data": "0x616263646566", - "nonce": 34, - "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", - "value": 0x5AF3107A4000, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction["nonce"]), - cairo_error(message="Max fee per gas too high"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction["chainId"], - ) - - @composite - def max_priority_fee_too_high(draw): - maxFeePerGas = draw(integers(min_value=0, max_value=2**128 - 1)) - maxPriorityFeePerGas = draw(integers(min_value=0, max_value=2**128 - 1)) - assume(maxFeePerGas < maxPriorityFeePerGas) - return (maxFeePerGas, maxPriorityFeePerGas) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) - @given(max_priority_fee_too_high()) - def test_raise_max_priority_fee_too_high( - self, cairo_run, max_priority_fee_too_high - ): - transaction = { - "type": 2, - "gas": 100_000, - "maxFeePerGas": max_priority_fee_too_high[0], - "maxPriorityFeePerGas": max_priority_fee_too_high[1], - "data": "0x616263646566", - "nonce": 34, - "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", - "value": 0x5AF3107A4000, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction["nonce"]), - cairo_error(message="Max priority fee greater than max fee per gas"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction["chainId"], - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) - @SyscallHandler.patch( - "IKakarot.eth_send_transaction", - lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + "IKakarot.eth_send_raw_unsigned_tx", + lambda _, __: [1, 0x68656C6C6F, 1, 1], # hello ) def test_pass_authorized_pre_eip155_transaction(self, cairo_run): rlp_decoded = rlp.decode(ARACHNID_PROXY_SIGNED_TX) @@ -688,17 +427,10 @@ def test_pass_authorized_pre_eip155_transaction(self, cairo_run): assert output_len == 1 assert output[0] == 0x68656C6C6F - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: int_to_uint256(10**128)) @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) - @SyscallHandler.patch( - "IKakarot.eth_send_transaction", - lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + "IKakarot.eth_send_raw_unsigned_tx", + lambda _, __: [1, 0x68656C6C6F, 1, 1], # hello ) @pytest.mark.parametrize("transaction", TRANSACTIONS) def test_pass_all_transactions_types(self, cairo_run, seed, transaction): @@ -732,17 +464,10 @@ def test_pass_all_transactions_types(self, cairo_run, seed, transaction): assert output_len == 1 assert output[0] == 0x68656C6C6F - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: int_to_uint256(10**128)) @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) - @SyscallHandler.patch( - "IKakarot.eth_send_transaction", - lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + "IKakarot.eth_send_raw_unsigned_tx", + lambda _, __: [1, 0x68656C6C6F, 1, 1], # hello ) def test_should_pass_all_data_len(self, cairo_run, bytecode): transaction = { @@ -779,3 +504,169 @@ def test_should_pass_all_data_len(self, cairo_run, bytecode): assert output_len == 1 assert output[0] == 0x68656C6C6F + + class TestExecuteFromOutsideEntrypoint: + + def test_should_raise_when_caller_is_not_any_caller_nor_actual_caller( + self, cairo_run + ): + with cairo_error(message="Execute from outside: invalid caller"): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address + 1, + "nonce": 0, + "execute_after": 0, + "execute_before": 0, + }, + call_array=[], + calldata=[], + signature=[], + ) + + def test_should_raise_when_call_is_too_early(self, cairo_run): + with cairo_error(message="Execute from outside: too early"): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": SyscallHandler.block_timestamp + 1, + "execute_before": 0, + }, + call_array=[], + calldata=[], + signature=[], + ) + + def test_should_raise_when_call_is_too_late(self, cairo_run): + with cairo_error(message="Execute from outside: too late"): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": 0, + "execute_before": SyscallHandler.block_timestamp - 1, + }, + call_array=[], + calldata=[], + signature=[], + ) + + def test_should_raise_when_call_array_is_empty(self, cairo_run): + with cairo_error(message="Execute from outside: multicall not supported"): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": 0, + "execute_before": SyscallHandler.block_timestamp + 1, + }, + call_array=[], + calldata=[], + signature=[], + ) + + def test_should_raise_when_call_array_has_more_than_one_call(self, cairo_run): + with cairo_error(message="Execute from outside: multicall not supported"): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": 0, + "execute_before": SyscallHandler.block_timestamp + 1, + }, + call_array=[{}, {}], + calldata=[], + signature=[], + ) + + @patch.dict(SyscallHandler.tx_info, {"version": 0}) + def test_should_raise_when_tx_version_is_zero(self, cairo_run): + with cairo_error(message="Deprecated tx version: 0"): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": 0, + "execute_before": SyscallHandler.block_timestamp + 1, + }, + call_array=[ + {"to": 0, "selector": 0, "data_offset": 0, "data_len": 0}, + ], + calldata=[], + signature=[], + ) + + @SyscallHandler.patch("Account_bytecode_len", 1) + def test_should_raise_when_eoa_has_code(self, cairo_run): + with cairo_error(message="EOAs cannot have code"): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": 0, + "execute_before": SyscallHandler.block_timestamp + 1, + }, + call_array=[ + {"to": 0, "selector": 0, "data_offset": 0, "data_len": 0}, + ], + calldata=[], + signature=[], + ) + + @given(data_len=integers(min_value=2**128, max_value=DEFAULT_PRIME)) + def test_should_raise_when_packed_data_len_is_zero_or_out_of_range( + self, cairo_run, data_len + ): + with cairo_error( + message="Execute from outside: packed_tx_data_len is zero or out of range" + ): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": 0, + "execute_before": SyscallHandler.block_timestamp + 1, + }, + call_array=[ + { + "to": 0, + "selector": 0, + "data_offset": 0, + "data_len": data_len % DEFAULT_PRIME, + }, + ], + calldata=[], + signature=[], + ) + + def test_should_raise_when_tx_data_len_is_out_of_range(self, cairo_run): + with cairo_error( + message="Execute from outside: tx_data_len is out of range" + ): + cairo_run( + "test__execute_from_outside_entrypoint", + outside_execution={ + "caller": SyscallHandler.caller_address, + "nonce": 0, + "execute_after": 0, + "execute_before": SyscallHandler.block_timestamp + 1, + }, + call_array=[ + { + "to": 0, + "selector": 0, + "data_offset": 0, + "data_len": 1, + }, + ], + calldata=[2**128], + signature=[], + ) diff --git a/tests/src/kakarot/instructions/test_block_information.py b/tests/src/kakarot/instructions/test_block_information.py index 56e38a4ad..f46869099 100644 --- a/tests/src/kakarot/instructions/test_block_information.py +++ b/tests/src/kakarot/instructions/test_block_information.py @@ -1,10 +1,9 @@ -from collections import OrderedDict from unittest.mock import patch import pytest from kakarot_scripts.constants import COINBASE -from tests.utils.constants import BIG_CHAIN_ID, BLOCK_GAS_LIMIT, CHAIN_ID, Opcodes +from tests.utils.constants import BLOCK_GAS_LIMIT, CHAIN_ID, Opcodes from tests.utils.syscall_handler import SyscallHandler @@ -29,23 +28,3 @@ class TestBlockInformation: def test__exec_block_information(self, cairo_run, opcode, expected_result): output = cairo_run("test__exec_block_information", opcode=opcode) assert output == hex(expected_result) - - @patch.object( - SyscallHandler, - "tx_info", - OrderedDict( - { - "version": 1, - "account_contract_address": 0xABDE1, - "max_fee": int(1e17), - "signature_len": None, - "signature": [], - "transaction_hash": 0xABDE1, - "chain_id": BIG_CHAIN_ID, - "nonce": 1, - } - ), - ) - def test__exec_chain_id__should_return_mod_64(self, cairo_run): - output = cairo_run("test__exec_block_information", opcode=Opcodes.CHAINID) - assert output == hex(BIG_CHAIN_ID % 2**32) diff --git a/tests/src/kakarot/instructions/test_environmental_information.py b/tests/src/kakarot/instructions/test_environmental_information.py index f990ac5a1..d8014db5e 100644 --- a/tests/src/kakarot/instructions/test_environmental_information.py +++ b/tests/src/kakarot/instructions/test_environmental_information.py @@ -3,8 +3,8 @@ import pytest from Crypto.Hash import keccak +from kakarot_scripts.utils.uint256 import int_to_uint256 from tests.utils.syscall_handler import SyscallHandler -from tests.utils.uint256 import int_to_uint256 EXISTING_ACCOUNT = 0xABDE1 EXISTING_ACCOUNT_SN_ADDR = 0x1234 diff --git a/tests/src/kakarot/instructions/test_stop_and_math_operations.py b/tests/src/kakarot/instructions/test_stop_and_math_operations.py index a03871da9..c052b5bcd 100644 --- a/tests/src/kakarot/instructions/test_stop_and_math_operations.py +++ b/tests/src/kakarot/instructions/test_stop_and_math_operations.py @@ -1,7 +1,7 @@ import pytest +from kakarot_scripts.utils.uint256 import int_to_uint256 from tests.utils.constants import Opcodes -from tests.utils.uint256 import int_to_uint256 class TestStopMathOperations: diff --git a/tests/src/kakarot/test_account.cairo b/tests/src/kakarot/test_account.cairo index 98d83d5df..79e753100 100644 --- a/tests/src/kakarot/test_account.cairo +++ b/tests/src/kakarot/test_account.cairo @@ -24,7 +24,7 @@ func test__init__should_return_account_with_default_dict_as_storage{ local nonce: felt; local balance_low: felt; %{ - from tests.utils.uint256 import int_to_uint256 + from kakarot_scripts.utils.uint256 import int_to_uint256 ids.evm_address = program_input["evm_address"] ids.code_len = len(program_input["code"]) @@ -68,7 +68,7 @@ func test__copy__should_return_new_account_with_same_attributes{ local nonce: felt; local balance_low: felt; %{ - from tests.utils.uint256 import int_to_uint256 + from kakarot_scripts.utils.uint256 import int_to_uint256 ids.evm_address = program_input["evm_address"] ids.code_len = len(program_input["code"]) @@ -196,7 +196,7 @@ func test__has_code_or_nonce{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, ran let (code_hash_ptr) = alloc(); local nonce: felt; %{ - from tests.utils.uint256 import int_to_uint256 + from kakarot_scripts.utils.uint256 import int_to_uint256 ids.code_len = len(program_input["code"]) segments.write_arg(ids.code, program_input["code"]) diff --git a/tests/src/kakarot/test_account.py b/tests/src/kakarot/test_account.py index b9ebdcf67..046c39d0a 100644 --- a/tests/src/kakarot/test_account.py +++ b/tests/src/kakarot/test_account.py @@ -3,8 +3,8 @@ from hypothesis import given from hypothesis.strategies import binary +from kakarot_scripts.utils.uint256 import int_to_uint256 from tests.utils.syscall_handler import SyscallHandler -from tests.utils.uint256 import int_to_uint256 class TestAccount: diff --git a/tests/src/kakarot/test_gas.cairo b/tests/src/kakarot/test_gas.cairo index 6c57612ec..80b68d8fc 100644 --- a/tests/src/kakarot/test_gas.cairo +++ b/tests/src/kakarot/test_gas.cairo @@ -58,7 +58,7 @@ func test__memory_expansion_cost_saturated{range_check_ptr}() -> felt { let (offset) = alloc(); let (size) = alloc(); %{ - from tests.utils.uint256 import int_to_uint256 + from kakarot_scripts.utils.uint256 import int_to_uint256 ids.words_len = program_input["words_len"] segments.write_arg(ids.offset, int_to_uint256(program_input["offset"])) segments.write_arg(ids.size, int_to_uint256(program_input["size"])) diff --git a/tests/src/kakarot/test_kakarot.cairo b/tests/src/kakarot/test_kakarot.cairo index 4c6750c34..fef315d67 100644 --- a/tests/src/kakarot/test_kakarot.cairo +++ b/tests/src/kakarot/test_kakarot.cairo @@ -6,6 +6,7 @@ from starkware.cairo.common.uint256 import Uint256 from kakarot.library import Kakarot from kakarot.kakarot import ( + eth_send_raw_unsigned_tx, register_account, set_native_token, set_base_fee, @@ -24,8 +25,6 @@ from kakarot.account import Account func eth_call{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* }() -> (model.EVM*, model.State*, felt, felt) { - // Given - tempvar origin; tempvar to: model.Option; tempvar gas_limit; @@ -38,8 +37,7 @@ func eth_call{ let (access_list) = alloc(); %{ - from tests.utils.uint256 import int_to_uint256 - + from kakarot_scripts.utils.uint256 import int_to_uint256 ids.origin = program_input.get("origin", 0) ids.to.is_some = int(bool(program_input.get("to") is not None)) @@ -70,6 +68,24 @@ func eth_call{ return (evm, state, gas_used, required_gas); } +func test__eth_send_raw_unsigned_tx{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* +}() -> (felt, felt*, felt, felt) { + tempvar tx_data_len: felt; + let (tx_data) = alloc(); + + %{ + segments.write_arg(ids.tx_data, program_input["tx_data"]) + ids.tx_data_len = len(program_input["tx_data"]) + %} + + let (return_data_len, return_data, success, gas_used) = eth_send_raw_unsigned_tx( + tx_data_len=tx_data_len, tx_data=tx_data + ); + + return (return_data_len, return_data, success, gas_used); +} + func compute_starknet_address{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( ) -> felt { tempvar evm_address; @@ -194,3 +210,8 @@ func test__set_cairo1_helpers_class_hash{ set_cairo1_helpers_class_hash(value); return (); } + +func test__eth_chain_id{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> felt { + let (chain_id) = Kakarot.eth_chain_id(); + return chain_id; +} diff --git a/tests/src/kakarot/test_kakarot.py b/tests/src/kakarot/test_kakarot.py index 3c53901ae..fe01e6aec 100644 --- a/tests/src/kakarot/test_kakarot.py +++ b/tests/src/kakarot/test_kakarot.py @@ -6,15 +6,18 @@ from eth_abi import decode, encode from eth_utils import keccak from eth_utils.address import to_checksum_address +from hypothesis import given +from hypothesis.strategies import composite, integers +from starkware.cairo.lang.cairo_constants import DEFAULT_PRIME from starkware.starknet.public.abi import get_storage_var_address from web3._utils.abi import map_abi_data from web3._utils.normalizers import BASE_RETURN_NORMALIZERS from web3.exceptions import NoABIFunctionsFound from kakarot_scripts.ef_tests.fetch import EF_TESTS_PARSED_DIR -from tests.utils.constants import TRANSACTION_GAS_LIMIT +from tests.utils.constants import CHAIN_ID, TRANSACTION_GAS_LIMIT, TRANSACTIONS from tests.utils.errors import cairo_error -from tests.utils.helpers import felt_to_signed_int +from tests.utils.helpers import felt_to_signed_int, rlp_encode_signed_data from tests.utils.syscall_handler import SyscallHandler, parse_state CONTRACT_ADDRESS = 1234 @@ -405,6 +408,187 @@ def test_failing_contract(self, cairo_run): ) assert not evm["reverted"] + class TestEthChainIdEntrypoint: + @given(chain_id=integers(min_value=0, max_value=2**64 - 1)) + def test_should_return_chain_id_modulo_53(self, cairo_run, chain_id): + with patch.dict(SyscallHandler.tx_info, {"chain_id": chain_id}): + res = cairo_run("test__eth_chain_id") + assert res == chain_id % 2**53 + + class TestEthSendRawTransactionEntrypoint: + def test_should_raise_invalid_chain_id_tx_type_different_from_0( + self, cairo_run + ): + transaction = { + "type": 2, + "gas": 100_000, + "maxFeePerGas": 2_000_000_000, + "maxPriorityFeePerGas": 2_000_000_000, + "data": "0x616263646566", + "nonce": 34, + "to": "", + "value": 0x00, + "accessList": [], + "chainId": 9999, + } + tx_data = list(rlp_encode_signed_data(transaction)) + + with cairo_error(message="Invalid chain id"): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @SyscallHandler.patch("IAccount.get_nonce", lambda addr, data: [1]) + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_should_raise_invalid_nonce(self, cairo_run, tx): + # explicitly set the nonce in transaction to be different from the patch + tx = {**tx, "nonce": 0} + tx_data = list(rlp_encode_signed_data(tx)) + with cairo_error(message="Invalid nonce"): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @given(gas_limit=integers(min_value=2**64, max_value=DEFAULT_PRIME - 1)) + def test_raise_gas_limit_too_high(self, cairo_run, gas_limit): + tx = { + "type": 2, + "gas": gas_limit, + "maxFeePerGas": 2_000_000_000, + "maxPriorityFeePerGas": 3_000_000_000, + "data": "0x616263646566", + "nonce": 34, + "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", + "value": 0x5AF3107A4000, + "accessList": [], + "chainId": CHAIN_ID, + } + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Gas limit too high"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @given(maxFeePerGas=integers(min_value=2**128, max_value=DEFAULT_PRIME - 1)) + def test_raise_max_fee_per_gas_too_high(self, cairo_run, maxFeePerGas): + tx = { + "type": 2, + "gas": 100_000, + "maxFeePerGas": maxFeePerGas, + "maxPriorityFeePerGas": 3_000_000_000, + "data": "0x616263646566", + "nonce": 34, + "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", + "value": 0x5AF3107A4000, + "accessList": [], + "chainId": CHAIN_ID, + } + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Max fee per gas too high"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_raise_transaction_gas_limit_too_high(self, cairo_run, tx): + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Transaction gas_limit > Block gas_limit"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @SyscallHandler.patch("Kakarot_block_gas_limit", TRANSACTION_GAS_LIMIT) + @SyscallHandler.patch("Kakarot_base_fee", TRANSACTION_GAS_LIMIT * 10**10) + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_raise_max_fee_per_gas_too_low(self, cairo_run, tx): + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Max fee per gas too low"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @composite + def max_priority_fee_too_high(draw): + max_fee_per_gas = draw(integers(min_value=0, max_value=2**128 - 2)) + max_priority_fee_per_gas = draw( + integers(min_value=max_fee_per_gas + 1, max_value=DEFAULT_PRIME - 1) + ) + return (max_fee_per_gas, max_priority_fee_per_gas) + + @SyscallHandler.patch("Kakarot_block_gas_limit", TRANSACTION_GAS_LIMIT) + @given(max_priority_fee_too_high()) + def test_raise_max_priority_fee_too_high( + self, cairo_run, max_priority_fee_too_high + ): + tx = { + "type": 2, + "gas": 100_000, + "maxFeePerGas": max_priority_fee_too_high[0], + "maxPriorityFeePerGas": max_priority_fee_too_high[1], + "data": "0x616263646566", + "nonce": 34, + "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", + "value": 0x5AF3107A4000, + "accessList": [], + "chainId": CHAIN_ID, + } + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Max priority fee greater than max fee per gas"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: [0, 0]) + @SyscallHandler.patch("Kakarot_block_gas_limit", TRANSACTION_GAS_LIMIT) + @SyscallHandler.patch("IAccount.get_evm_address", lambda _, __: [0xABDE1]) + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_raise_not_enough_ETH_balance(self, cairo_run, tx): + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Not enough ETH to pay msg.value + max gas fees"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + class TestLoopProfiling: @pytest.mark.slow @pytest.mark.NoCI diff --git a/tests/src/kakarot/test_state.cairo b/tests/src/kakarot/test_state.cairo index 9b0ee80b9..97848cb73 100644 --- a/tests/src/kakarot/test_state.cairo +++ b/tests/src/kakarot/test_state.cairo @@ -113,7 +113,7 @@ func test__is_account_alive__account_alive_in_state{ let (code) = alloc(); let (code_hash_ptr) = alloc(); %{ - from tests.utils.uint256 import int_to_uint256 + from kakarot_scripts.utils.uint256 import int_to_uint256 ids.nonce = program_input["nonce"] ids.balance_low = program_input["balance_low"] diff --git a/tests/src/utils/test_bytes.py b/tests/src/utils/test_bytes.py index 2ea3b5e0a..32b8f866b 100644 --- a/tests/src/utils/test_bytes.py +++ b/tests/src/utils/test_bytes.py @@ -4,9 +4,9 @@ from hypothesis import given from hypothesis.strategies import integers +from kakarot_scripts.utils.uint256 import int_to_uint256 from tests.utils.errors import cairo_error from tests.utils.hints import patch_hint -from tests.utils.uint256 import int_to_uint256 PRIME = 0x800000000000011000000000000000000000000000000000000000000000001 diff --git a/tests/src/utils/test_eth_transaction.cairo b/tests/src/utils/test_eth_transaction.cairo index d41c22309..640b7c3cd 100644 --- a/tests/src/utils/test_eth_transaction.cairo +++ b/tests/src/utils/test_eth_transaction.cairo @@ -53,11 +53,15 @@ func test__parse_access_list{range_check_ptr}(output_ptr: felt*) { func test__get_tx_type{range_check_ptr}() -> felt { alloc_locals; // Given + tempvar data_len: felt; let (data) = alloc(); - %{ segments.write_arg(ids.data, program_input["data"]) %} + %{ + ids.data_len = program_input.get("data_len", len(program_input["data"])) + segments.write_arg(ids.data, program_input["data"]) + %} // When - let tx_type = EthTransaction.get_tx_type(data); + let tx_type = EthTransaction.get_tx_type(data_len, data); return tx_type; } diff --git a/tests/src/utils/test_eth_transaction.py b/tests/src/utils/test_eth_transaction.py index e0c3d01b1..1e6569bf7 100644 --- a/tests/src/utils/test_eth_transaction.py +++ b/tests/src/utils/test_eth_transaction.py @@ -49,7 +49,7 @@ async def test_should_decode_all_transactions_types( assert expected_to == decoded_tx["destination"] assert transaction["value"] == int(decoded_tx["amount"], 16) # pre-eip155 txs have an internal chain_id set to 0 in the decoded tx - assert transaction.get("chainId", 0) == decoded_tx["chain_id"] + assert transaction.get("chainId") == decoded_tx["chain_id"] assert expected_data == decoded_tx["payload"] assert expected_access_list == decoded_tx["access_list"] @@ -91,3 +91,7 @@ def test_should_return_tx_type(self, cairo_run, transaction): encoded_unsigned_tx = rlp_encode_signed_data(transaction) tx_type = cairo_run("test__get_tx_type", data=list(encoded_unsigned_tx)) assert tx_type == transaction.get("type", 0) + + def test_should_raise_when_data_len_is_zero(self, cairo_run): + with cairo_error("tx_data_len is zero"): + cairo_run("test__get_tx_type", data_len=0, data=[1, 2, 3]) diff --git a/tests/src/utils/test_rlp.cairo b/tests/src/utils/test_rlp.cairo index da12318f4..933ad18b7 100644 --- a/tests/src/utils/test_rlp.cairo +++ b/tests/src/utils/test_rlp.cairo @@ -5,24 +5,24 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin from starkware.cairo.common.alloc import alloc from starkware.cairo.common.memcpy import memcpy -func test__decode{range_check_ptr}() -> RLP.Item* { +func test__decode_raw{range_check_ptr}() -> RLP.Item* { alloc_locals; // Given tempvar data_len: felt; let (data) = alloc(); %{ - ids.data_len = len(program_input["data"]) + ids.data_len = program_input.get("data_len", len(program_input["data"])) segments.write_arg(ids.data, program_input["data"]) %} // When let (local items: RLP.Item*) = alloc(); - RLP.decode(items, data_len, data); + RLP.decode_raw(items, data_len, data); return items; } -func test__decode_type{range_check_ptr}() -> (felt, felt, felt) { +func test__decode{range_check_ptr}() -> RLP.Item* { alloc_locals; // Given tempvar data_len: felt; @@ -33,7 +33,20 @@ func test__decode_type{range_check_ptr}() -> (felt, felt, felt) { %} // When - let (type, offset, len) = RLP.decode_type(data_len, data); + let (local items: RLP.Item*) = alloc(); + RLP.decode(items, data_len, data); + + return items; +} + +func test__decode_type{range_check_ptr}() -> (felt, felt, felt) { + alloc_locals; + // Given + let (data) = alloc(); + %{ segments.write_arg(ids.data, program_input["data"]) %} + + // When + let (type, offset, len) = RLP.decode_type(data); // Then return (type, offset, len); diff --git a/tests/src/utils/test_rlp.py b/tests/src/utils/test_rlp.py index 35b78b821..e45fb0740 100644 --- a/tests/src/utils/test_rlp.py +++ b/tests/src/utils/test_rlp.py @@ -26,11 +26,23 @@ def test_should_match_prefix_reference_implementation(self, cairo_run, data): assert output == [expected_type, expected_offset, expected_len] + class TestDecodeRaw: + def test_should_raise_when_parsed_len_greater_than_data(self, cairo_run): + with cairo_error("RLP data too short for declared length"): + cairo_run("test__decode_raw", data=[0xB8, 0x01]) + + @given(data=lists(binary(min_size=2), min_size=2) | binary(min_size=2)) + def test_should_raise_when_malicious_prover_fills_data(self, cairo_run, data): + with cairo_error("RLP data too short for declared length"): + cairo_run( + "test__decode_raw", + data_len=len(encode(data)) - 1, + data=list(encode(data)), + ) + class TestDecode: @given(data=recursive(binary(), lists)) - async def test_should_match_decode_reference_implementation( - self, cairo_run, data - ): + def test_should_match_decode_reference_implementation(self, cairo_run, data): encoded_data = encode(data) items = cairo_run("test__decode", data=list(encoded_data)) @@ -40,7 +52,7 @@ async def test_should_match_decode_reference_implementation( data=recursive(binary(), lists), extra_data=binary(min_size=1, max_size=255), ) - async def test_raise_when_data_contains_extra_bytes( + def test_raise_when_data_contains_extra_bytes( self, cairo_run, data, extra_data ): encoded_data = encode(data) diff --git a/tests/src/utils/test_uint256.py b/tests/src/utils/test_uint256.py index 30313dac2..dbdfdfa37 100644 --- a/tests/src/utils/test_uint256.py +++ b/tests/src/utils/test_uint256.py @@ -2,7 +2,7 @@ from hypothesis import given, settings from hypothesis.strategies import integers -from tests.utils.uint256 import int_to_uint256, uint256_to_int +from kakarot_scripts.utils.uint256 import int_to_uint256, uint256_to_int class TestUint256: diff --git a/tests/utils/errors.py b/tests/utils/errors.py index b277a3e80..5eff6b086 100644 --- a/tests/utils/errors.py +++ b/tests/utils/errors.py @@ -2,6 +2,7 @@ from contextlib import contextmanager import pytest +from starknet_py.net.client_errors import ClientError from web3 import Web3 @@ -33,7 +34,10 @@ def cairo_error(message=None): yield e if message is None: return - error = re.search(r"Error message: (.*)", str(e.value)) + if type(e.value) == ClientError: + error = re.search(r"Error message: (.*)", str(e.value.data["revert_error"])) + else: + error = re.search(r"Error message: (.*)", str(e.value)) error = error.group(1) if error else str(e.value) assert message == error, f"Expected {message}, got {error}" finally: diff --git a/tests/utils/helpers.cairo b/tests/utils/helpers.cairo index 53f686d19..847061924 100644 --- a/tests/utils/helpers.cairo +++ b/tests/utils/helpers.cairo @@ -57,6 +57,7 @@ namespace TestHelpers { is_create=FALSE, depth=0, env=env, + cairo_precompile_called=FALSE, ); let evm: model.EVM* = EVM.init(message, 1000000); return evm; diff --git a/tests/utils/helpers.py b/tests/utils/helpers.py index 3df45f6d8..d0fea36a9 100644 --- a/tests/utils/helpers.py +++ b/tests/utils/helpers.py @@ -13,7 +13,7 @@ from starkware.starknet.public.abi import get_storage_var_address from kakarot_scripts.constants import NETWORK -from tests.utils.uint256 import int_to_uint256 +from kakarot_scripts.utils.uint256 import int_to_uint256 PERMIT_TYPEHASH = keccak( text="Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" diff --git a/tests/utils/serde.py b/tests/utils/serde.py index b8460c8e7..4d6208fef 100644 --- a/tests/utils/serde.py +++ b/tests/utils/serde.py @@ -164,7 +164,9 @@ def serialize_eth_transaction(self, ptr): if raw["access_list"] is not None else [] ), - "chain_id": raw["chain_id"], + "chain_id": ( + raw["chain_id"]["value"] if raw["chain_id"]["is_some"] == 1 else None + ), } def serialize_message(self, ptr): diff --git a/tests/utils/syscall_handler.py b/tests/utils/syscall_handler.py index 9cd505be7..fbc09f16f 100644 --- a/tests/utils/syscall_handler.py +++ b/tests/utils/syscall_handler.py @@ -16,12 +16,12 @@ get_storage_var_address, ) +from kakarot_scripts.utils.uint256 import int_to_uint256, uint256_to_int from tests.utils.constants import ( ACCOUNT_CLASS_IMPLEMENTATION, CAIRO1_HELPERS_CLASS_HASH, CHAIN_ID, ) -from tests.utils.uint256 import int_to_uint256, uint256_to_int def cairo_keccak(class_hash, calldata):