diff --git a/cairo/ethereum/cancun/state.cairo b/cairo/ethereum/cancun/state.cairo index 4295defa6..52f29072f 100644 --- a/cairo/ethereum/cancun/state.cairo +++ b/cairo/ethereum/cancun/state.cairo @@ -2,7 +2,8 @@ from starkware.cairo.common.cairo_builtins import PoseidonBuiltin from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.registers import get_fp_and_pc from starkware.cairo.common.math import assert_not_zero - +from starkware.cairo.common.uint256 import Uint256 +from src.utils.uint256 import uint256_add, uint256_sub from ethereum.cancun.fork_types import ( Address, Account, @@ -32,7 +33,7 @@ from ethereum.cancun.trie import ( ) from ethereum_types.bytes import Bytes, Bytes32 from ethereum_types.numeric import U256, U256Struct, Bool, bool, Uint -from ethereum.utils.numeric import is_zero +from ethereum.utils.numeric import is_zero, U256_le, U256_sub, U256_add from src.utils.dict import hashdict_read, hashdict_write, hashdict_get, dict_new_empty @@ -184,6 +185,27 @@ func set_account{poseidon_ptr: PoseidonBuiltin*, state: State}( return (); } +func move_ether{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, state: State}( + sender_address: Address, recipient_address: Address, amount: U256 +) { + alloc_locals; + let sender_account = get_account(sender_address); + let sender_balance = sender_account.value.balance; + + let is_sender_balance_sufficient = U256_le(amount, sender_balance); + with_attr error_message("Sender has insufficient balance") { + assert is_sender_balance_sufficient.value = 1; + } + + let new_sender_account_balance = U256_sub(sender_balance, amount); + set_account_balance(sender_address, new_sender_account_balance); + + let recipient_account = get_account(recipient_address); + let new_recipient_account_balance = U256_add(recipient_account.value.balance, amount); + set_account_balance(recipient_address, new_recipient_account_balance); + return (); +} + func get_storage{poseidon_ptr: PoseidonBuiltin*, state: State}( address: Address, key: Bytes32 ) -> U256 { diff --git a/cairo/ethereum/utils/numeric.cairo b/cairo/ethereum/utils/numeric.cairo index 3f4403a8f..86c5a35fd 100644 --- a/cairo/ethereum/utils/numeric.cairo +++ b/cairo/ethereum/utils/numeric.cairo @@ -5,7 +5,8 @@ from ethereum_types.bytes import Bytes32, Bytes32Struct, Bytes20 from starkware.cairo.common.cairo_builtins import BitwiseBuiltin from starkware.cairo.common.math import split_felt -from starkware.cairo.common.uint256 import word_reverse_endian +from starkware.cairo.common.uint256 import word_reverse_endian, Uint256, uint256_le +from src.utils.uint256 import uint256_add, uint256_sub func min{range_check_ptr}(a: felt, b: felt) -> felt { alloc_locals; @@ -165,3 +166,34 @@ func U256_from_be_bytes20{range_check_ptr, bitwise_ptr: BitwiseBuiltin*}(bytes20 tempvar res = U256(new U256Struct(low=low, high=high)); return res; } + +// @dev Panics if overflow +func U256_add{range_check_ptr}(a: U256, b: U256) -> U256 { + alloc_locals; + let (res, carry) = uint256_add([a.value], [b.value]); + + with_attr error_message("OverflowError") { + assert carry = 0; + } + + tempvar result = U256(new U256Struct(res.low, res.high)); + return result; +} + +// @dev Panics if underflow with OverflowError +func U256_sub{range_check_ptr}(a: U256, b: U256) -> U256 { + alloc_locals; + let is_within_bounds = U256_le(b, a); + with_attr error_message("OverflowError") { + assert is_within_bounds.value = 1; + } + let (result) = uint256_sub([a.value], [b.value]); + tempvar res = U256(new U256Struct(result.low, result.high)); + return res; +} + +func U256_le{range_check_ptr}(a: U256, b: U256) -> bool { + let (result) = uint256_le([a.value], [b.value]); + tempvar res = bool(result); + return res; +} diff --git a/cairo/tests/ethereum/cancun/test_state.py b/cairo/tests/ethereum/cancun/test_state.py index 15916b934..793e1a081 100644 --- a/cairo/tests/ethereum/cancun/test_state.py +++ b/cairo/tests/ethereum/cancun/test_state.py @@ -6,7 +6,7 @@ from hypothesis import strategies as st from hypothesis.strategies import composite -from ethereum.cancun.fork_types import Account +from ethereum.cancun.fork_types import Account, Address from ethereum.cancun.state import ( account_exists, account_exists_and_is_empty, @@ -23,6 +23,7 @@ is_account_alive, is_account_empty, mark_account_created, + move_ether, set_account, set_account_balance, set_code, @@ -31,6 +32,7 @@ touch_account, ) from tests.utils.args_gen import State, TransientStorage +from tests.utils.errors import strict_raises from tests.utils.strategies import address, bytes32, code, state, transient_storage @@ -118,6 +120,24 @@ def test_set_account(self, cairo_run, data, account: Optional[Account]): set_account(state, address, account) assert state_cairo == state + @given( + data=state_and_address_and_optional_key(), recipient_address=address, amount=... + ) + def test_move_ether( + self, cairo_run, data, recipient_address: Address, amount: U256 + ): + state, sender_address = data + try: + state_cairo = cairo_run( + "move_ether", state, sender_address, recipient_address, amount + ) + except Exception as cairo_error: + with strict_raises(type(cairo_error)): + move_ether(state, sender_address, recipient_address, amount) + return + move_ether(state, sender_address, recipient_address, amount) + assert state_cairo == state + @given(data=state_and_address_and_optional_key()) def test_destroy_account(self, cairo_run, data): state, address = data diff --git a/cairo/tests/ethereum/utils/test_numeric.py b/cairo/tests/ethereum/utils/test_numeric.py index 98d8c1fd1..56af7e77b 100644 --- a/cairo/tests/ethereum/utils/test_numeric.py +++ b/cairo/tests/ethereum/utils/test_numeric.py @@ -8,6 +8,7 @@ from ethereum.cancun.fork_types import Address from ethereum.cancun.vm.gas import BLOB_GASPRICE_UPDATE_FRACTION, MIN_BLOB_GASPRICE from ethereum.utils.numeric import ceil32, taylor_exponential +from tests.utils.errors import cairo_error from tests.utils.strategies import felt, uint128 pytestmark = pytest.mark.python_vm @@ -81,3 +82,30 @@ def test_U256__eq__(self, cairo_run, a: U256, b: U256): @given(address=...) def test_U256_from_be_bytes20(self, cairo_run, address: Address): assert U256.from_be_bytes(address) == cairo_run("U256_from_be_bytes20", address) + + @given(a=..., b=...) + def test_U256_le(self, cairo_run, a: U256, b: U256): + assert (a <= b) == cairo_run("U256_le", a, b) + + @given(a=..., b=...) + def test_U256_add(self, cairo_run, a: U256, b: U256): + try: + a + b + except Exception as e: + with cairo_error(str(e.__class__.__name__)): + cairo_result = cairo_run("U256_add", a, b) + return + cairo_result = cairo_run("U256_add", a, b) + assert cairo_result == a + b + + @given(a=..., b=...) + def test_U256_sub(self, cairo_run, a: U256, b: U256): + try: + a - b + except Exception as e: + with cairo_error(str(e.__class__.__name__)): + cairo_result = cairo_run("U256_sub", a, b) + return + + cairo_result = cairo_run("U256_sub", a, b) + assert cairo_result == a - b