Skip to content

Commit

Permalink
feat: deploy account from zero (#1065)
Browse files Browse the repository at this point in the history
<!--- Please provide a general summary of your changes in the title
above -->

<!-- Give an estimate of the time you spent on this PR in terms of work
days.
Did you spend 0.5 days on this PR or rather 2 days?  -->

Time spent on this PR: 0.3d

## Pull request type

<!-- Please try to limit your pull request to one type,
submit multiple pull requests if needed. -->

Please check the type of change your PR introduces:

- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying,
or link to a relevant issue. -->

Resolves #1064 Resolves #1046

## What is the new behavior?

<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Deploys accounts from zero to allow anyone to deploy an account for
Kakarot.
-
-

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg"
height="34" align="absmiddle"
alt="Reviewable"/>](https://reviewable.io/reviews/kkrt-labs/kakarot/1065)
<!-- Reviewable:end -->
  • Loading branch information
enitrat authored Apr 5, 2024
1 parent 1904d37 commit 268f386
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 118 deletions.
14 changes: 3 additions & 11 deletions src/backend/starknet.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
%lang starknet

from starkware.cairo.common.alloc import alloc
from starkware.cairo.common.bool import FALSE
from starkware.cairo.common.bool import FALSE, TRUE
from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.cairo.common.dict_access import DictAccess
from starkware.cairo.common.uint256 import Uint256
Expand All @@ -21,8 +21,7 @@ from starkware.starknet.common.syscalls import (
from kakarot.account import Account
from kakarot.precompiles.precompiles import Precompiles
from kakarot.constants import Constants
from kakarot.events import evm_contract_deployed
from kakarot.interfaces.interfaces import IERC20, IAccount, IUninitializedAccount
from kakarot.interfaces.interfaces import IERC20, IAccount

from kakarot.model import model
from kakarot.state import State
Expand Down Expand Up @@ -82,15 +81,8 @@ namespace Starknet {
contract_address_salt=evm_address,
constructor_calldata_size=2,
constructor_calldata=constructor_calldata,
deploy_from_zero=0,
deploy_from_zero=TRUE,
);

// Properly initialize the account once created
let (account_class_hash) = Kakarot_account_contract_class_hash.read();
IUninitializedAccount.initialize(starknet_address, account_class_hash);

evm_contract_deployed.emit(evm_address, starknet_address);
Kakarot_evm_to_starknet_address.write(evm_address, starknet_address);
return (account_address=starknet_address);
}

Expand Down
11 changes: 6 additions & 5 deletions src/kakarot/account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -441,28 +441,29 @@ namespace Account {
evm_address: felt
) -> felt {
alloc_locals;
let (_deployer_address: felt) = get_contract_address();
let (kakarot_address: felt) = get_contract_address();
let deployer_address = 0; // deploy_from_zero == TRUE
let (
_uninitialized_account_class_hash: felt
uninitialized_account_class_hash: felt
) = Kakarot_uninitialized_account_class_hash.read();
let (constructor_calldata: felt*) = alloc();
assert constructor_calldata[0] = _deployer_address;
assert constructor_calldata[0] = kakarot_address;
assert constructor_calldata[1] = evm_address;
let (hash_state_ptr) = hash_init();
let (hash_state_ptr) = hash_update_single{hash_ptr=pedersen_ptr}(
hash_state_ptr=hash_state_ptr, item=Constants.CONTRACT_ADDRESS_PREFIX
);
// hash deployer
let (hash_state_ptr) = hash_update_single{hash_ptr=pedersen_ptr}(
hash_state_ptr=hash_state_ptr, item=_deployer_address
hash_state_ptr=hash_state_ptr, item=deployer_address
);
// hash salt
let (hash_state_ptr) = hash_update_single{hash_ptr=pedersen_ptr}(
hash_state_ptr=hash_state_ptr, item=evm_address
);
// hash class hash
let (hash_state_ptr) = hash_update_single{hash_ptr=pedersen_ptr}(
hash_state_ptr=hash_state_ptr, item=_uninitialized_account_class_hash
hash_state_ptr=hash_state_ptr, item=uninitialized_account_class_hash
);
// hash constructor arguments
let (hash_state_ptr) = hash_update_with_hashchain{hash_ptr=pedersen_ptr}(
Expand Down
4 changes: 2 additions & 2 deletions src/kakarot/accounts/account_contract.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func constructor{
@external
func initialize{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin*
}(implementation_class: felt) {
return AccountContract.initialize(implementation_class);
}(kakarot_address: felt, evm_address: felt, implementation_class: felt) {
return AccountContract.initialize(kakarot_address, evm_address, implementation_class);
}

// @notice replaces the class of the account.
Expand Down
15 changes: 4 additions & 11 deletions src/kakarot/accounts/library.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,10 @@ func Account_nonce() -> (nonce: felt) {
func Account_implementation() -> (address: felt) {
}

// //////////////// DO NOT MODIFY //////////////////
// We are intentionally causing a storage_slot collision here,
// by defining these variables in both `uninitialized_account` and `account_contract`.
@storage_var
func Account_evm_address() -> (evm_address: felt) {
}

@storage_var
func Account_kakarot_address() -> (kakarot_address: felt) {
}
// /////////////////////////////////////////////////

@event
func transaction_executed(response_len: felt, response: felt*, success: felt, gas_used: felt) {
}
Expand All @@ -85,12 +77,10 @@ namespace AccountContract {
pedersen_ptr: HashBuiltin*,
range_check_ptr,
bitwise_ptr: BitwiseBuiltin*,
}(implementation_class: felt) {
}(kakarot_address, evm_address, implementation_class) {
alloc_locals;
let (is_initialized) = Account_is_initialized.read();
assert is_initialized = 0;
let (kakarot_address) = Account_kakarot_address.read();
let (evm_address) = Account_evm_address.read();
Account_is_initialized.write(1);
Ownable.initializer(kakarot_address);
Account_evm_address.write(evm_address);
Expand All @@ -100,6 +90,9 @@ namespace AccountContract {
let (native_token_address) = IKakarot.get_native_token(kakarot_address);
let infinite = Uint256(Constants.UINT128_MAX, Constants.UINT128_MAX);
IERC20.approve(native_token_address, kakarot_address, infinite);

// Register the account in the Kakarot mapping
IKakarot.register_account(kakarot_address, evm_address);
return ();
}

Expand Down
60 changes: 23 additions & 37 deletions src/kakarot/accounts/uninitialized_account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,40 @@

%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin
from starkware.cairo.common.alloc import alloc
from starkware.starknet.common.syscalls import get_caller_address, replace_class, library_call

// We are intentionally causing a storage_slot collision here,
// by defining these variables in both `uninitialized_account` and `account_contract`.
// We are defining them here instead of in the account library, so as to not depend
// on content of the account library in uninitialized_account and ensure a fixed class hash.
@storage_var
func Account_evm_address() -> (evm_address: felt) {
}
from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.starknet.common.syscalls import replace_class

@storage_var
func Account_kakarot_address() -> (kakarot_address: felt) {
@contract_interface
namespace IKakarot {
func get_account_contract_class_hash() -> (account_contract_class_hash: felt) {
}
}

const INITIALIZE_SELECTOR = 0x79dc0da7c54b95f10aa182ad0a46400db63156920adb65eca2654c0945a463; // sn_keccak('initialize')
@contract_interface
namespace IAccount {
func initialize(kakarot_address: felt, evm_address: felt, implementation_class: felt) {
}
}

// @title Uninitialized Contract Account. Used to get a deterministic address for an account, no
// matter the actual implementation class used.

// @notice Deploy and initialize the account with the Kakarot and EVM addresses it was deployed with.
// @param kakarot_address The address of the main Kakarot contract.
// @param evm_address The address of the EVM contract.
@constructor
func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
kakarot_address: felt, evm_address: felt
) {
Account_kakarot_address.write(kakarot_address);
Account_evm_address.write(evm_address);
return ();
}
let (implementation_class) = IKakarot.get_account_contract_class_hash(kakarot_address);

// @notice Initializes the account with the Kakarot and EVM addresses it was deployed with.
// @param implementation_class The address of the main Kakarot contract.
@external
func initialize{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin*
}(implementation_class: felt) {
let (caller) = get_caller_address();
let (kakarot_address) = Account_kakarot_address.read();
with_attr error_message("Only Kakarot") {
assert kakarot_address = caller;
}
replace_class(implementation_class);
let (calldata) = alloc();
assert [calldata] = implementation_class;
library_call(
class_hash=implementation_class,
function_selector=INITIALIZE_SELECTOR,
calldata_size=1,
calldata=calldata,
IAccount.library_call_initialize(
implementation_class,
kakarot_address=kakarot_address,
evm_address=evm_address,
implementation_class=implementation_class,
);

replace_class(implementation_class);
return ();
}
12 changes: 6 additions & 6 deletions src/kakarot/interfaces/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ namespace IERC20 {
}
}

@contract_interface
namespace IUninitializedAccount {
func initialize(new_class: felt) {
}
}

@contract_interface
namespace IAccount {
func initialize(implementation: felt, calldata_len: felt, calldata: felt*) {
Expand Down Expand Up @@ -113,6 +107,12 @@ namespace IKakarot {
func compute_starknet_address(evm_address: felt) -> (contract_address: felt) {
}

func get_account_contract_class_hash() -> (account_contract_class_hash: felt) {
}

func register_account(evm_address: felt) {
}

func get_starknet_address(evm_address: felt) -> (starknet_address: felt) {
}

Expand Down
19 changes: 19 additions & 0 deletions src/kakarot/kakarot.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ func compute_starknet_address{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, ra
return (contract_address=starknet_address);
}

// @notice Return the account implementation class hash
// @return account_contract_class_hash The account implementation class hash
@view
func get_account_contract_class_hash{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr
}() -> (account_contract_class_hash: felt) {
return Kakarot.get_account_contract_class_hash();
}

// @notice Returns the registered starknet address for a given EVM address.
// @dev Returns 0 if no contract is deployed for this EVM address.
// @param evm_address The EVM address to transform to a starknet address
Expand All @@ -176,6 +185,16 @@ func deploy_externally_owned_account{
return Kakarot.deploy_externally_owned_account(evm_address);
}

// @notice Register the calling Starknet address for the given EVM address
// @dev Only the corresponding computed Starknet address can make this call to ensure that registered accounts are actually deployed.
// @param evm_address The EVM address of the account.
@external
func register_account{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
evm_address: felt
) {
return Kakarot.register_account(evm_address);
}

// @notice The eth_call function as described in the spec,
// see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call
// This is a view only function, meaning that it doesn't make any state change.
Expand Down
41 changes: 39 additions & 2 deletions src/kakarot/library.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from openzeppelin.access.ownable.library import Ownable
from starkware.cairo.common.bool import FALSE, TRUE
from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin
from starkware.starknet.common.syscalls import get_caller_address, get_tx_info
from starkware.starknet.common.syscalls import get_caller_address
from starkware.cairo.common.math_cmp import is_not_zero
from starkware.cairo.common.uint256 import Uint256

Expand All @@ -20,7 +20,9 @@ from kakarot.storages import (
Kakarot_coinbase,
Kakarot_prev_randao,
Kakarot_block_gas_limit,
Kakarot_evm_to_starknet_address,
)
from kakarot.events import evm_contract_deployed
from kakarot.interpreter import Interpreter
from kakarot.instructions.system_operations import CreateHelper
from kakarot.interfaces.interfaces import IAccount, IERC20
Expand Down Expand Up @@ -220,10 +222,45 @@ namespace Kakarot {
}(evm_contract_address: felt) -> (starknet_contract_address: felt) {
alloc_locals;
let (starknet_contract_address) = Starknet.deploy(evm_contract_address);

return (starknet_contract_address=starknet_contract_address);
}

// @notice Return the class hash of the account implementation
// @return account_contract_class_hash The class hash of the account implementation
func get_account_contract_class_hash{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr
}() -> (account_contract_class_hash: felt) {
let (account_contract_class_hash) = Kakarot_account_contract_class_hash.read();
return (account_contract_class_hash,);
}

// @notice Register the calling Starknet address for the given EVM address
// @dev Only the corresponding computed Starknet address can make this call to ensure that registered accounts are actually deployed.
// @param evm_address The EVM address of the account.
func register_account{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
evm_address: felt
) {
alloc_locals;

let (existing_address) = Kakarot_evm_to_starknet_address.read(evm_address);
with_attr error_message("Kakarot: account already registered") {
assert existing_address = 0;
}

let (local caller_address: felt) = get_caller_address();
let starknet_address = Account.compute_starknet_address(evm_address);
local starknet_address = starknet_address;

with_attr error_message(
"Kakarot: Caller should be {starknet_address}, got {caller_address}") {
assert starknet_address = caller_address;
}

evm_contract_deployed.emit(evm_address, starknet_address);
Kakarot_evm_to_starknet_address.write(evm_address, starknet_address);
return ();
}

// @notice Get the EVM address from the transaction
// @dev When to=None, it's a deploy tx so we first compute the target address
// @param to The transaction to parameter
Expand Down
20 changes: 18 additions & 2 deletions tests/end_to_end/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,25 @@ async def _factory(evm_address: Union[int, str]):
tx = await kakarot.functions["deploy_externally_owned_account"].invoke_v1(
evm_address, max_fee=max_fee
)
await wait_for_transaction(
tx.hash,
await wait_for_transaction(tx.hash)
return tx

return _factory


@pytest.fixture(scope="session")
def register_account(kakarot: Contract, max_fee: int):
"""
Isolate the starknet-py logic and make the test agnostic of the backend.
"""

async def _factory(evm_address: Union[int, str]):
if isinstance(evm_address, str):
evm_address = int(evm_address, 16)
tx = await kakarot.functions["register_account"].invoke_v1(
evm_address, max_fee=max_fee
)
await wait_for_transaction(tx.hash)
return tx

return _factory
Expand Down
Loading

0 comments on commit 268f386

Please sign in to comment.