diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 3a5423cb0..1dbe62bf0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -87,7 +87,7 @@ lint: - solidity_contracts/src/WETH/**/*.sol - solidity_contracts/src/UniswapV2Router/**/*.sol - solidity_contracts/src/Solmate/**/*.sol - - solidity_contracts/src/starknet/**/*.sol + - solidity_contracts/src/Starknet/**/*.sol actions: disabled: - trunk-announce diff --git a/kakarot_scripts/constants.py b/kakarot_scripts/constants.py index f9b6dbf58..a2bf3ed34 100644 --- a/kakarot_scripts/constants.py +++ b/kakarot_scripts/constants.py @@ -217,6 +217,8 @@ class ChainId(IntEnum): NETWORK["chain_id"] = ChainId.chain_id ETH_TOKEN_ADDRESS = 0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7 +STRK_TOKEN_ADDRESS = 0x04718F5A0FC34CC1AF16A1CDEE98FFB20C31F5CD61D6AB07201858F4287C938D + COINBASE = int( os.getenv("KAKAROT_COINBASE_RECIPIENT") or "0x20eB005C0b9c906691F885eca5895338E15c36De", # Defaults to faucet on appchain sepolia @@ -357,8 +359,8 @@ def __next__(self) -> Account: NETWORK["relayers"] = RelayerPool(NETWORK.get("relayers", [default_relayer])) - +kakarot_chain_ascii = bytes.fromhex(f"{ChainId.chain_id.value:014x}").lstrip(b"\x00") logger.info( f"ℹ️ Connected to Starknet chain id {bytes.fromhex(f'{ChainId.starknet_chain_id.value:x}')} " - f"and Kakarot chain id {bytes.fromhex(f'{ChainId.chain_id.value:014x}')}" + f"and Kakarot chain id {kakarot_chain_ascii}" ) diff --git a/kakarot_scripts/deploy_kakarot.py b/kakarot_scripts/deploy_kakarot.py index fa70170b8..f9a240971 100644 --- a/kakarot_scripts/deploy_kakarot.py +++ b/kakarot_scripts/deploy_kakarot.py @@ -1,6 +1,7 @@ # %% Imports import logging +from eth_utils.address import to_checksum_address from uvloop import run from kakarot_scripts.constants import ( @@ -18,6 +19,7 @@ MULTICALL3_SIGNED_TX, NETWORK, RPC_CLIENT, + STRK_TOKEN_ADDRESS, NetworkType, ) from kakarot_scripts.utils.kakarot import deploy as deploy_evm @@ -136,6 +138,7 @@ async def main(): remove_lazy_account(account.address) # %% EVM Deployments + starknet_deployments = get_starknet_deployments() evm_deployments = get_evm_deployments() # %% Pre-EIP155 deployments, done only once @@ -159,6 +162,7 @@ async def main(): max_fee=int(0.2e18), ) + # %% Tokens deployments if not EVM_ADDRESS: logger.info("ℹ️ No EVM address provided, skipping EVM deployments") return @@ -169,20 +173,56 @@ async def main(): EVM_ADDRESS, amount=100 if NETWORK["type"] is NetworkType.DEV else 0.01 ) - coinbase = (await call("kakarot", "get_coinbase")).coinbase - if evm_deployments.get("Bridge", {}).get("address") != coinbase: - bridge = await deploy_evm("CairoPrecompiles", "EthStarknetBridge") - evm_deployments["Bridge"] = { - "address": int(bridge.address, 16), - "starknet_address": bridge.starknet_address, + for contract_app, contract_name, deployed_name, *deployment_args in [ + ("WETH", "WETH9", "WETH9"), + ( + "CairoPrecompiles", + "DualVmToken", + "KakarotETH", + starknet_deployments["kakarot"], + ETH_TOKEN_ADDRESS, + ), + ( + "CairoPrecompiles", + "DualVmToken", + "KakarotSTRK", + starknet_deployments["kakarot"], + STRK_TOKEN_ADDRESS, + ), + ]: + deployment = evm_deployments.get(deployed_name) + if deployment is not None: + token_starknet_address = ( + await call("kakarot", "get_starknet_address", deployment["address"]) + ).starknet_address + if deployment["starknet_address"] == token_starknet_address: + logger.info(f"✅ {deployed_name} already deployed, skipping") + continue + + token = await deploy_evm(contract_app, contract_name, *deployment_args) + evm_deployments[deployed_name] = { + "address": int(token.address, 16), + "starknet_address": token.starknet_address, } await invoke( "kakarot", "set_authorized_cairo_precompile_caller", - int(bridge.address, 16), + int(token.address, 16), 1, ) - await invoke("kakarot", "set_coinbase", int(bridge.address, 16)) + + coinbase = (await call("kakarot", "get_coinbase")).coinbase + if evm_deployments.get("Coinbase", {}).get("address") != coinbase: + contract = await deploy_evm( + "Kakarot", + "Coinbase", + to_checksum_address(f'{evm_deployments["KakarotETH"]["address"]:040x}'), + ) + evm_deployments["Coinbase"] = { + "address": int(contract.address, 16), + "starknet_address": contract.starknet_address, + } + await invoke("kakarot", "set_coinbase", int(contract.address, 16)) coinbase = (await call("kakarot", "get_coinbase")).coinbase if coinbase == 0: @@ -190,20 +230,6 @@ async def main(): else: logger.info(f"✅ Coinbase set to: 0x{coinbase:040x}") - weth_starknet_address = ( - await call( - "kakarot", - "get_starknet_address", - evm_deployments.get("WETH", {}).get("address", 0), - ) - ).starknet_address - if evm_deployments.get("WETH", {}).get("starknet_address") != weth_starknet_address: - weth = await deploy_evm("WETH", "WETH9") - evm_deployments["WETH"] = { - "address": int(weth.address, 16), - "starknet_address": weth.starknet_address, - } - dump_evm_deployments(evm_deployments) balance_after = await get_balance(account.address) logger.info( diff --git a/kakarot_scripts/utils/kakarot.py b/kakarot_scripts/utils/kakarot.py index 45c3c606d..9fc937207 100644 --- a/kakarot_scripts/utils/kakarot.py +++ b/kakarot_scripts/utils/kakarot.py @@ -94,7 +94,9 @@ def get_solidity_artifacts( ) if len(target_solidity_file_path) != 1: raise ValueError( - f"Cannot locate a unique {contract_name} in {contract_app}" + f"Cannot locate a unique {contract_name} in {contract_app}:\n" + f"Search path: {str(src_path / contract_app)}/**/{contract_name}.sol\n" + f"Found: {target_solidity_file_path}" ) target_compilation_output = [ diff --git a/solidity_contracts/src/CairoPrecompiles/EthStarknetBridge.sol b/solidity_contracts/src/CairoPrecompiles/EthStarknetBridge.sol deleted file mode 100644 index bead05a28..000000000 --- a/solidity_contracts/src/CairoPrecompiles/EthStarknetBridge.sol +++ /dev/null @@ -1,48 +0,0 @@ -// 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/solidity_contracts/src/Kakarot/Coinbase.sol b/solidity_contracts/src/Kakarot/Coinbase.sol new file mode 100644 index 000000000..1f1be892f --- /dev/null +++ b/solidity_contracts/src/Kakarot/Coinbase.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import {DualVmToken} from "../CairoPrecompiles/DualVmToken.sol"; + +contract Coinbase { + /// @dev The EVM address of the DualVmToken for Kakarot ETH. + DualVmToken public immutable kakarotEth; + + /// @dev State variable to store the owner of the contract + address public owner; + + /// Constructor sets the owner of the contract + constructor(address _kakarotEth) { + owner = msg.sender; + kakarotEth = DualVmToken(_kakarotEth); + } + + /// Modifier to restrict access to owner only + /// @dev Assert that msd.sender is the owner + modifier onlyOwner() { + require(msg.sender == owner, "Not the contract owner"); + _; + } + + /// @notice Withdraws ETH from the contract to a Starknet address + /// @dev DualVmToken.balanceOf(this) is the same as address(this).balance + /// @param toStarknetAddress The Starknet address to withdraw to + function withdraw(uint256 toStarknetAddress) external onlyOwner { + uint256 balance = address(this).balance; + kakarotEth.transfer(toStarknetAddress, balance); + } + + /// @notice Transfers ownership of the contract to a new address + /// @param newOwner The address to transfer ownership to + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "New owner cannot be the zero address"); + owner = newOwner; + } +} diff --git a/solidity_contracts/src/Starknet/IStarknetMessaging.sol b/solidity_contracts/src/Starknet/IStarknetMessaging.sol new file mode 100644 index 000000000..facb03ec4 --- /dev/null +++ b/solidity_contracts/src/Starknet/IStarknetMessaging.sol @@ -0,0 +1,76 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./IStarknetMessagingEvents.sol"; + +interface IStarknetMessaging is IStarknetMessagingEvents { + /** + Returns the max fee (in Wei) that StarkNet will accept per single message. + */ + function getMaxL1MsgFee() external pure returns (uint256); + + /** + Sends a message to an L2 contract. + This function is payable, the paid amount is the message fee. + + Returns the hash of the message and the nonce of the message. + */ + function sendMessageToL2( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload + ) external payable returns (bytes32, uint256); + + /** + Consumes a message that was sent from an L2 contract. + + Returns the hash of the message. + */ + function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) + external + returns (bytes32); + + /** + Starts the cancellation of an L1 to L2 message. + A message can be canceled messageCancellationDelay() seconds after this function is called. + + Note: This function may only be called for a message that is currently pending and the caller + must be the sender of the that message. + */ + function startL1ToL2MessageCancellation( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external returns (bytes32); + + /** + Cancels an L1 to L2 message, this function should be called at least + messageCancellationDelay() seconds after the call to startL1ToL2MessageCancellation(). + A message may only be cancelled by its sender. + If the message is missing, the call will revert. + + Note that the message fee is not refunded. + */ + function cancelL1ToL2Message( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external returns (bytes32); +} diff --git a/solidity_contracts/src/Starknet/IStarknetMessagingEvents.sol b/solidity_contracts/src/Starknet/IStarknetMessagingEvents.sol new file mode 100644 index 000000000..11937727b --- /dev/null +++ b/solidity_contracts/src/Starknet/IStarknetMessagingEvents.sol @@ -0,0 +1,66 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +interface IStarknetMessagingEvents { + // This event needs to be compatible with the one defined in Output.sol. + event LogMessageToL1(uint256 indexed fromAddress, address indexed toAddress, uint256[] payload); + + // An event that is raised when a message is sent from L1 to L2. + event LogMessageToL2( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce, + uint256 fee + ); + + // An event that is raised when a message from L2 to L1 is consumed. + event ConsumedMessageToL1( + uint256 indexed fromAddress, + address indexed toAddress, + uint256[] payload + ); + + // An event that is raised when a message from L1 to L2 is consumed. + event ConsumedMessageToL2( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); + + // An event that is raised when a message from L1 to L2 Cancellation is started. + event MessageToL2CancellationStarted( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); + + // An event that is raised when a message from L1 to L2 is canceled. + event MessageToL2Canceled( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); +} diff --git a/solidity_contracts/src/Starknet/NamedStorage.sol b/solidity_contracts/src/Starknet/NamedStorage.sol new file mode 100644 index 000000000..5279f380f --- /dev/null +++ b/solidity_contracts/src/Starknet/NamedStorage.sol @@ -0,0 +1,120 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +/* + Library to provide basic storage, in storage location out of the low linear address space. + + New types of storage variables should be added here upon need. +*/ +library NamedStorage { + function bytes32ToUint256Mapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => uint256) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function bytes32ToAddressMapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function uintToAddressMapping(string memory tag_) + internal + pure + returns (mapping(uint256 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function addressToBoolMapping(string memory tag_) + internal + pure + returns (mapping(address => bool) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function getUintValue(string memory tag_) internal view returns (uint256 retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setUintValue(string memory tag_, uint256 value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setUintValueOnce(string memory tag_, uint256 value) internal { + require(getUintValue(tag_) == 0, "ALREADY_SET"); + setUintValue(tag_, value); + } + + function getAddressValue(string memory tag_) internal view returns (address retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setAddressValue(string memory tag_, address value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setAddressValueOnce(string memory tag_, address value) internal { + require(getAddressValue(tag_) == address(0x0), "ALREADY_SET"); + setAddressValue(tag_, value); + } + + function getBoolValue(string memory tag_) internal view returns (bool retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setBoolValue(string memory tag_, bool value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } +} diff --git a/solidity_contracts/src/Starknet/StarknetMessaging.sol b/solidity_contracts/src/Starknet/StarknetMessaging.sol new file mode 100644 index 000000000..69cb691e6 --- /dev/null +++ b/solidity_contracts/src/Starknet/StarknetMessaging.sol @@ -0,0 +1,201 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./IStarknetMessaging.sol"; +import "./NamedStorage.sol"; + +/** + Implements sending messages to L2 by adding them to a pipe and consuming messages from L2 by + removing them from a different pipe. A deriving contract can handle the former pipe and add items + to the latter pipe while interacting with L2. + Adapted from https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/solidity/StarknetMessaging.sol +*/ +contract StarknetMessaging is IStarknetMessaging { + /* + Random slot storage elements and accessors. + */ + string constant L1L2_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L1TOL2_MAPPPING_V2"; + string constant L2L1_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L2TOL1_MAPPPING"; + + string constant L1L2_MESSAGE_NONCE_TAG = "STARKNET_1.0_MSGING_L1TOL2_NONCE"; + + string constant L1L2_MESSAGE_CANCELLATION_MAP_TAG = ( + "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_MAPPPING" + ); + + string constant L1L2_MESSAGE_CANCELLATION_DELAY_TAG = ( + "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_DELAY" + ); + + uint256 constant MAX_L1_MSG_FEE = 1 ether; + + function getMaxL1MsgFee() public pure override returns (uint256) { + return MAX_L1_MSG_FEE; + } + + /** + Returns the msg_fee + 1 for the message with the given 'msgHash', + or 0 if no message with such a hash is pending. + */ + function l1ToL2Messages(bytes32 msgHash) external view returns (uint256) { + return l1ToL2Messages()[msgHash]; + } + + function l2ToL1Messages(bytes32 msgHash) external view returns (uint256) { + return l2ToL1Messages()[msgHash]; + } + + function l1ToL2Messages() internal pure returns (mapping(bytes32 => uint256) storage) { + return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_MAP_TAG); + } + + function l2ToL1Messages() internal pure returns (mapping(bytes32 => uint256) storage) { + return NamedStorage.bytes32ToUint256Mapping(L2L1_MESSAGE_MAP_TAG); + } + + function l1ToL2MessageNonce() public view returns (uint256) { + return NamedStorage.getUintValue(L1L2_MESSAGE_NONCE_TAG); + } + + function messageCancellationDelay() public view returns (uint256) { + return NamedStorage.getUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG); + } + + function messageCancellationDelay(uint256 delayInSeconds) internal { + NamedStorage.setUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG, delayInSeconds); + } + + /** + Returns the timestamp at the time cancelL1ToL2Message was called with a message + matching 'msgHash'. + + The function returns 0 if cancelL1ToL2Message was never called. + */ + function l1ToL2MessageCancellations(bytes32 msgHash) external view returns (uint256) { + return l1ToL2MessageCancellations()[msgHash]; + } + + function l1ToL2MessageCancellations() + internal + pure + returns (mapping(bytes32 => uint256) storage) + { + return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_CANCELLATION_MAP_TAG); + } + + /** + Returns the hash of an L1 -> L2 message from msg.sender. + */ + function getL1ToL2MsgHash( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) internal view returns (bytes32) { + return + keccak256( + abi.encodePacked( + uint256(uint160(msg.sender)), + toAddress, + nonce, + selector, + payload.length, + payload + ) + ); + } + + /** + Sends a message to an L2 contract. + */ + function sendMessageToL2( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload + ) external payable override returns (bytes32, uint256) { + require(msg.value > 0, "L1_MSG_FEE_MUST_BE_GREATER_THAN_0"); + require(msg.value <= getMaxL1MsgFee(), "MAX_L1_MSG_FEE_EXCEEDED"); + uint256 nonce = l1ToL2MessageNonce(); + NamedStorage.setUintValue(L1L2_MESSAGE_NONCE_TAG, nonce + 1); + emit LogMessageToL2(msg.sender, toAddress, selector, payload, nonce, msg.value); + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + // Note that the inclusion of the unique nonce in the message hash implies that + // l1ToL2Messages()[msgHash] was not accessed before. + l1ToL2Messages()[msgHash] = msg.value + 1; + return (msgHash, nonce); + } + + /** + Consumes a message that was sent from an L2 contract. + + Returns the hash of the message. + */ + function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) + external + override + returns (bytes32) + { + bytes32 msgHash = keccak256( + abi.encodePacked(fromAddress, uint256(uint160(msg.sender)), payload.length, payload) + ); + + require(l2ToL1Messages()[msgHash] > 0, "INVALID_MESSAGE_TO_CONSUME"); + emit ConsumedMessageToL1(fromAddress, msg.sender, payload); + l2ToL1Messages()[msgHash] -= 1; + return msgHash; + } + + function startL1ToL2MessageCancellation( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external override returns (bytes32) { + emit MessageToL2CancellationStarted(msg.sender, toAddress, selector, payload, nonce); + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; + require(msgFeePlusOne > 0, "NO_MESSAGE_TO_CANCEL"); + l1ToL2MessageCancellations()[msgHash] = block.timestamp; + return msgHash; + } + + function cancelL1ToL2Message( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external override returns (bytes32) { + emit MessageToL2Canceled(msg.sender, toAddress, selector, payload, nonce); + // Note that the message hash depends on msg.sender, which prevents one contract from + // cancelling another contract's message. + // Trying to do so will result in NO_MESSAGE_TO_CANCEL. + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; + require(msgFeePlusOne != 0, "NO_MESSAGE_TO_CANCEL"); + + uint256 requestTime = l1ToL2MessageCancellations()[msgHash]; + require(requestTime != 0, "MESSAGE_CANCELLATION_NOT_REQUESTED"); + + uint256 cancelAllowedTime = requestTime + messageCancellationDelay(); + require(cancelAllowedTime >= requestTime, "CANCEL_ALLOWED_TIME_OVERFLOW"); + require(block.timestamp >= cancelAllowedTime, "MESSAGE_CANCELLATION_NOT_ALLOWED_YET"); + + l1ToL2Messages()[msgHash] = 0; + return (msgHash); + } +} diff --git a/solidity_contracts/src/Starknet/StarknetMessagingLocal.sol b/solidity_contracts/src/Starknet/StarknetMessagingLocal.sol new file mode 100644 index 000000000..1e5718f92 --- /dev/null +++ b/solidity_contracts/src/Starknet/StarknetMessagingLocal.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "../starknet/StarknetMessaging.sol"; + +/** + @notice Interface related to local messaging for Starknet. + @author Glihm https://github.com/glihm/starknet-messaging-dev +*/ +interface IStarknetMessagingLocal { + function addMessageHashesFromL2( + uint256[] calldata msgHashes + ) + external + payable; +} + +/** + @title A superset of StarknetMessaging to support + local development by adding a way to directly register + a message hash ready to be consumed, without waiting the block + to be verified. + + @dev The idea is that, to not wait on the block to be proven, + this messaging contract can receive directly a hash of a message + to be considered as `received`. This message can then be consumed normally. + + DISCLAIMER: + The purpose of this contract is for local development only. +*/ +contract StarknetMessagingLocal is StarknetMessaging, IStarknetMessagingLocal { + + /** + @notice Hashes were added. + */ + event MessageHashesAddedFromL2( + uint256[] hashes + ); + + /** + @notice Adds the hashes of messages from L2. + + @param msgHashes Hashes to register as consumable. + */ + function addMessageHashesFromL2( + uint256[] calldata msgHashes + ) + external + payable + { + for (uint256 i = 0; i < msgHashes.length; i++) { + bytes32 hash = bytes32(msgHashes[i]); + l2ToL1Messages()[hash] += 1; + } + + emit MessageHashesAddedFromL2(msgHashes); + } + + function getMsgCountByHash( + bytes32 msgHash + ) public view returns (uint256){ + return l2ToL1Messages()[msgHash]; + } + +} diff --git a/tests/end_to_end/Kakarot/test_coinbase.py b/tests/end_to_end/Kakarot/test_coinbase.py new file mode 100644 index 000000000..3936a3582 --- /dev/null +++ b/tests/end_to_end/Kakarot/test_coinbase.py @@ -0,0 +1,61 @@ +import pytest +import pytest_asyncio + +from kakarot_scripts.utils.kakarot import deploy, eth_balance_of, fund_address +from kakarot_scripts.utils.starknet import invoke +from tests.utils.errors import evm_error + + +@pytest_asyncio.fixture(scope="package") +async def kakarot_eth(kakarot, eth): + token = await deploy( + "CairoPrecompiles", "DualVmToken", kakarot.address, eth.address + ) + await invoke( + "kakarot", "set_authorized_cairo_precompile_caller", int(token.address, 16), 1 + ) + return token + + +@pytest_asyncio.fixture(scope="package") +async def coinbase(owner, kakarot_eth): + return await deploy( + "Kakarot", "Coinbase", kakarot_eth.address, caller_eoa=owner.starknet_contract + ) + + +@pytest.mark.asyncio(scope="package") +class TestCoinbase: + class TestWithdrawal: + async def test_should_withdraw_all_eth(self, coinbase, owner): + await fund_address(coinbase.address, 0.001) + balance_coinbase_prev = await eth_balance_of(coinbase.address) + balance_owner_prev = await eth_balance_of(owner.address) + tx = await coinbase.withdraw(owner.starknet_contract.address) + balance_coinbase = await eth_balance_of(coinbase.address) + balance_owner = await eth_balance_of(owner.address) + assert balance_coinbase_prev > 0 + assert balance_coinbase == 0 + assert balance_owner - balance_owner_prev + tx["gas_used"] == 0.001e18 + + async def test_should_revert_when_not_owner(self, coinbase, other): + with evm_error("Not the contract owner"): + await coinbase.withdraw(0xDEAD, caller_eoa=other.starknet_contract) + + class TestTransferOwnership: + async def test_should_transfer_ownership(self, coinbase, owner, other): + await coinbase.transferOwnership(other.address) + assert await coinbase.owner() == other.address + await coinbase.transferOwnership( + owner.address, caller_eoa=other.starknet_contract + ) + + async def test_should_revert_when_new_owner_is_zero_address(self, coinbase): + with evm_error("New owner cannot be the zero address"): + await coinbase.transferOwnership(f"0x{0:040x}") + + async def test_should_revert_when_not_owner(self, coinbase, other): + with evm_error("Not the contract owner"): + await coinbase.transferOwnership( + other.address, caller_eoa=other.starknet_contract + ) diff --git a/tests/end_to_end/L1L2Messaging/test_messaging.py b/tests/end_to_end/L1L2Messaging/test_messaging.py index 36d02b2c6..b7f4866be 100644 --- a/tests/end_to_end/L1L2Messaging/test_messaging.py +++ b/tests/end_to_end/L1L2Messaging/test_messaging.py @@ -24,10 +24,10 @@ def sn_messaging_local(): if l1_addresses.get("StarknetMessagingLocal"): address = l1_addresses["StarknetMessagingLocal"]["address"] if l1_contract_exists(address): - return get_l1_contract("starknet", "StarknetMessagingLocal", address) + return get_l1_contract("Starknet", "StarknetMessagingLocal", address) contract = deploy_on_l1( - "starknet", + "Starknet", "StarknetMessagingLocal", ) l1_addresses.update({"StarknetMessagingLocal": {"address": contract.address}}) diff --git a/tests/end_to_end/conftest.py b/tests/end_to_end/conftest.py index e848427f7..ec9b7200f 100644 --- a/tests/end_to_end/conftest.py +++ b/tests/end_to_end/conftest.py @@ -12,7 +12,7 @@ from kakarot_scripts.utils.kakarot import deploy as deploy_kakarot 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.kakarot import get_deployments, get_eoa from kakarot_scripts.utils.starknet import ( call, get_contract, @@ -73,9 +73,10 @@ async def _factory(amount=0): yield _factory - bridge_address = (await call("kakarot", "get_coinbase")).coinbase - bridge = await get_solidity_contract( - "CairoPrecompiles", "EthStarknetBridge", address=bridge_address + kakarot_eth = await get_solidity_contract( + "CairoPrecompiles", + "DualVmToken", + address=get_deployments()["KakarotETH"]["address"], ) gas_price = (await call("kakarot", "get_base_fee")).base_fee gas_limit = 40_000 @@ -85,7 +86,7 @@ async def _factory(amount=0): if balance < tx_cost: continue - await bridge.transfer( + await kakarot_eth.functions["transfer(uint256,uint256)"]( deployer.address, balance - tx_cost, caller_eoa=wallet.starknet_contract, diff --git a/uv.lock b/uv.lock index 7325e67db..3888523bc 100644 --- a/uv.lock +++ b/uv.lock @@ -1336,7 +1336,6 @@ dev = [ { name = "pandas" }, { name = "py-ecc" }, { name = "py-evm" }, - { name = "pycryptodome" }, { name = "pyperclip" }, { name = "pysha3" }, { name = "pytest" }, @@ -1385,7 +1384,6 @@ dev = [ { name = "pandas", specifier = ">=1.5.1" }, { name = "py-ecc", specifier = ">=7.0.0" }, { name = "py-evm", git = "https://github.com/ethereum/py-evm?rev=f2219d6df83ede8f9574eab48cbe4bc33197066f" }, - { name = "pycryptodome", specifier = ">=3.16.0" }, { name = "pyperclip", specifier = ">=1.8.2" }, { name = "pysha3", specifier = ">=1.0.2" }, { name = "pytest", specifier = "==8.1.1" }, @@ -2766,6 +2764,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/32/4a7a205b14c11225609b75b28402c196e4396ac754dab6a81971b811781c/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", size = 12085794 }, { url = "https://files.pythonhosted.org/packages/c6/29/044048c5e911373827c0e1d3051321b9183b2a4f8d4e2f11c08fcff83f13/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", size = 12945797 }, { url = "https://files.pythonhosted.org/packages/aa/ce/c0b912f2f31aeb1b756a6ba56bcd84dd1f8a148470526a48515a3f4d48cd/scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", size = 10985467 }, + { url = "https://files.pythonhosted.org/packages/a4/50/8891028437858cc510e13578fe7046574a60c2aaaa92b02d64aac5b1b412/scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", size = 12025584 }, + { url = "https://files.pythonhosted.org/packages/d2/79/17feef8a1c14149436083bec0e61d7befb4812e272d5b20f9d79ea3e9ab1/scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", size = 10959795 }, + { url = "https://files.pythonhosted.org/packages/b1/c8/f08313f9e2e656bd0905930ae8bf99a573ea21c34666a813b749c338202f/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", size = 12077302 }, + { url = "https://files.pythonhosted.org/packages/a7/48/fbfb4dc72bed0fe31fe045fb30e924909ad03f717c36694351612973b1a9/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", size = 13002811 }, + { url = "https://files.pythonhosted.org/packages/a5/e7/0c869f9e60d225a77af90d2aefa7a4a4c0e745b149325d1450f0f0ce5399/scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", size = 10951354 }, ] [[package]]