Skip to content

Commit

Permalink
Eth account support (#361)
Browse files Browse the repository at this point in the history
* Create separate execute function

* Add is_valid_eth_signature to account library

* Add eth_execute to account library

* Create eth account mock and test

* Add missing dependencies

* Create TestEthSigner

* Update used private key

* Update implicit parameters

* Update execute parameters

* Update all implicit arguments

* Update signature values and hash

* Update variable name

* Update documentation

* Fix merge error

* Improve format

* Update tests/utils.py

Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com>

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* Rename test and fix documentation

* Add documentation

* Update src/openzeppelin/account/library.cairo

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update src/openzeppelin/account/library.cairo

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update src/openzeppelin/account/library.cairo

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update src/openzeppelin/account/library.cairo

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update tests/utils.py

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update tests/utils.py

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update tests/mocks/eth_account.cairo

Co-authored-by: Martín Triay <martriay@gmail.com>

* Create eth account preset

* Create signers module

* use assert_revert to test nonce

* Add test for valid signature

* use internal hash

* Update validity test

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update tests/signers.py

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update src/openzeppelin/account/library.cairo

Co-authored-by: Martín Triay <martriay@gmail.com>

* Fix after merge

* Improve tests

* Update account library

* Update Account.md

* update format

* Update src/openzeppelin/account/library.cairo

Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com>

* Update tests/access/test_Ownable.py

Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com>

* Update src/openzeppelin/account/library.cairo

Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com>

* Update eth test

* Update Account.md

* Update test

* Update tests/signers.py

Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com>

* Fix typo

* Update signers

* Update test

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update tests/signers.py

Co-authored-by: Martín Triay <martriay@gmail.com>

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

* update test

* Update documenation for Account

* Update docs/Account.md

* Update docs/Account.md

Co-authored-by: Martín Triay <martriay@gmail.com>

Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com>
Co-authored-by: Martín Triay <martriay@gmail.com>
  • Loading branch information
3 people committed Jun 29, 2022
1 parent f7d319f commit 2cd6027
Show file tree
Hide file tree
Showing 22 changed files with 640 additions and 96 deletions.
97 changes: 89 additions & 8 deletions docs/Account.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A more detailed writeup on the topic can be found on [Perama's blogpost](https:/
* [Keys, signatures and signers](#keys-signatures-and-signers)
* [Signer](#signer)
* [MockSigner utility](#mocksigner-utility)
* [MockEthSigner utility](#mockethsigner-utility)
* [Account entrypoint](#account-entrypoint)
* [Call and AccountCallArray format](#call-and-accountcallarray-format)
* [Call](#call)
Expand All @@ -24,6 +25,12 @@ A more detailed writeup on the topic can be found on [Perama's blogpost](https:/
* [`set_public_key`](#set_public_key)
* [`is_valid_signature`](#is_valid_signature)
* [`__execute__`](#__execute__)
* [`is_valid_eth_signature`](#is_valid_eth_signature)
* [`eth_execute`](#eth_execute)
* [`_unsafe_execute`](#_unsafe_execute)
* [Presets](#presets)
* [Account](#account)
* [Eth Account](#eth-account)
* [Account differentiation with ERC165](#account-differentiation-with-erc165)
* [Extending the Account contract](#extending-the-account-contract)
* [L1 escape hatch mechanism](#l1-escape-hatch-mechanism)
Expand Down Expand Up @@ -159,6 +166,13 @@ If utilizing multicall, send multiple transactions with the `send_transactions`
)
```

### MockEthSigner utility

The `MockEthSigner` class in [utils.py](../tests/utils.py) is used to perform transactions on a given Account with a secp256k1 curve key pair, crafting the transaction and managing nonces. It differs from the `MockSigner` implementation by:

* not using the public key but its derived address instead (the last 20 bytes of the keccak256 hash of the public key and adding `0x` to the beginning)
* signing the message with a secp256k1 curve address

## Account entrypoint

`__execute__` acts as a single entrypoint for all user interaction with any contract, including managing the account contract itself. That's why if you want to change the public key controlling the Account, you would send a transaction targeting the very Account contract:
Expand Down Expand Up @@ -361,11 +375,10 @@ is_valid: felt

This is the only external entrypoint to interact with the Account contract. It:

1. Takes the input and builds a `Call` for each iterated message. See [Multicall transactions](#multicall-transactions) for more information
2. Validates the transaction signature matches the message (including the nonce)
3. Increments the nonce
4. Calls the target contract with the intended function selector and calldata parameters
5. Forwards the contract call response data as return value
1. Validates the transaction signature matches the message (including the nonce)
2. Increments the nonce
3. Calls the target contract with the intended function selector and calldata parameters
4. Forwards the contract call response data as return value

Parameters:

Expand All @@ -386,6 +399,69 @@ response_len: felt
response: felt*
```

### `is_valid_eth_signature`

Returns `TRUE` if a given signature in the secp256k1 curve is valid, otherwise it reverts. In the future it will return `FALSE` if a given signature is invalid (for more info please check [this issue](https://github.com/OpenZeppelin/cairo-contracts/issues/327)).

Parameters:

```cairo
signature_len: felt
signature: felt*
```

Returns:

```cairo
is_valid: felt
```

> returns `TRUE` if a given signature is valid. Otherwise, reverts. In the future it will return `FALSE` if a given signature is invalid (for more info please check [this issue](https://github.com/OpenZeppelin/cairo-contracts/issues/327)).
### `eth_execute`

This follows the same idea as the vanilla version of `execute` with the sole difference that signature verification is on the secp256k1 curve.

Parameters:

```cairo
call_array_len: felt
call_array: AccountCallArray*
calldata_len: felt
calldata: felt*
nonce: felt
```

> Note that the current signature scheme expects a 7-element array like `[sig_v, uint256_sig_r_low, uint256_sig_r_high, uint256_sig_s_low, uint256_sig_s_high, uint256_hash_low, uint256_hash_high]` given that the parameters of the verification are bigger than a felt.
Returns:

```cairo
response_len: felt
response: felt*
```

### `_unsafe_execute`

It's an internal method that performs the following tasks:

1. Increments the nonce.
2. Takes the input and builds a `Call` for each iterated message. See [Multicall transactions](#multicall-transactions) for more information.
3. Calls the target contract with the intended function selector and calldata parameters
4. Forwards the contract call response data as return value

## Presets

The following contract presets are ready to deploy and can be used as-is for quick prototyping and testing. Each preset differs on the signature type being used by the Account.

### Account

The [`Account`](../src/openzeppelin/account/Account.cairo) preset uses StarkNet keys to validate transactions.

### Eth Account

The [`EthAccount`](../src/openzeppelin/account/EthAccount.cairo) preset supports Ethereum addresses, validating transactions with secp256k1 keys.

## Account differentiation with ERC165

Certain contracts like ERC721 require a means to differentiate between account contracts and non-account contracts. For a contract to declare itself as an account, it should implement [ERC165](https://eips.ethereum.org/EIPS/eip-165) as proposed in [#100](https://github.com/OpenZeppelin/cairo-contracts/discussions/100). To be in compliance with ERC165 specifications, the idea is to calculate the XOR of `IAccount`'s EVM selectors (not StarkNet selectors). The resulting magic value of `IAccount` is 0x50b70dcb.
Expand All @@ -394,13 +470,18 @@ Our ERC165 integration on StarkNet is inspired by OpenZeppelin's Solidity implem

## Extending the Account contract

Account contracts can be extended by following the [extensibility pattern](../docs/Extensibility.md#the-pattern). The basic idea behind integrating the pattern is to import the requisite methods from the Account library and incorporate the extended logic thereafter.
Account contracts can be extended by following the [extensibility pattern](../docs/Extensibility.md#the-pattern).

To implement custom account contracts, a pair of `validate` and `execute` functions should be exposed. This is why the Account library comes with different flavors of such pairs, like the vanilla `is_valid_signature` and `execute`, or the Ethereum flavored `is_valid_eth_signature` and `eth_execute` pair.

Account contract developers are encouraged to implement the [standard Account interface](https://github.com/OpenZeppelin/cairo-contracts/discussions/41) and incorporate the custom logic thereafter.

To implement alternative `execute` functions, make sure to check their corresponding `validate` function before calling the `_unsafe_execute` building block, as each of the current presets is doing. Do not expose `_unsafe_execute` directly.

Currently, there's only a single library/preset Account scheme, but we're looking for feedback and new presets to emerge. Some new validation schemes to look out for in the future:
Some other validation schemes to look out for in the future:

* multisig
* guardian logic like in [Argent's account](https://github.com/argentlabs/argent-contracts-starknet/blob/de5654555309fa76160ba3d7393d32d2b12e7349/contracts/ArgentAccount.cairo)
* [Ethereum signatures](https://github.com/OpenZeppelin/cairo-contracts/issues/161)

## L1 escape hatch mechanism

Expand Down
5 changes: 3 additions & 2 deletions src/openzeppelin/account/Account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin, BitwiseBuiltin

from openzeppelin.account.library import Account, AccountCallArray

Expand Down Expand Up @@ -95,7 +95,8 @@ func __execute__{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
ecdsa_ptr: SignatureBuiltin*
ecdsa_ptr: SignatureBuiltin*,
bitwise_ptr: BitwiseBuiltin*
}(
call_array_len: felt,
call_array: AccountCallArray*,
Expand Down
113 changes: 113 additions & 0 deletions src/openzeppelin/account/EthAccount.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# SPDX-License-Identifier: MIT
# OpenZeppelin Contracts for Cairo v0.1.0 (account/EthAccount.cairo)

%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin, BitwiseBuiltin
from openzeppelin.account.library import Account, AccountCallArray

from openzeppelin.introspection.ERC165 import ERC165

#
# Constructor
#

@constructor
func constructor{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(eth_address: felt):
Account.initializer(eth_address)
return ()
end

#
# Getters
#

@view
func get_eth_address{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}() -> (res: felt):
let (res) = Account.get_public_key()
return (res=res)
end

@view
func get_nonce{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}() -> (res: felt):
let (res) = Account.get_nonce()
return (res=res)
end

@view
func supportsInterface{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} (interfaceId: felt) -> (success: felt):
let (success) = ERC165.supports_interface(interfaceId)
return (success)
end

#
# Setters
#

@external
func set_eth_address{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr
}(new_eth_address: felt):
Account.set_public_key(new_eth_address)
return ()
end

#
# Business logic
#

@view
func is_valid_signature{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
ecdsa_ptr: SignatureBuiltin*,
bitwise_ptr: BitwiseBuiltin*
}(
hash: felt,
signature_len: felt,
signature: felt*
) -> (is_valid: felt):
let (is_valid) = Account.is_valid_eth_signature(hash, signature_len, signature)
return (is_valid=is_valid)
end

@external
func __execute__{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
bitwise_ptr: BitwiseBuiltin*
}(
call_array_len: felt,
call_array: AccountCallArray*,
calldata_len: felt,
calldata: felt*,
nonce: felt
) -> (response_len: felt, response: felt*):
let (response_len, response) = Account.eth_execute(
call_array_len,
call_array,
calldata_len,
calldata,
nonce
)
return (response_len=response_len, response=response)
end
Loading

0 comments on commit 2cd6027

Please sign in to comment.