Skip to content

Commit

Permalink
Merge pull request #18 from argentlabs/feature/contract_guardian
Browse files Browse the repository at this point in the history
Contract guardian
  • Loading branch information
juniset authored Nov 26, 2021
2 parents 84183a2 + 2f528c7 commit 2adeb80
Show file tree
Hide file tree
Showing 6 changed files with 485 additions and 53 deletions.
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# Argent Account on Starknet
# Argent Account on StarkNet

Preliminary work for an Argent Account on Starknet.
*Warning: StarkNet is still in alpha, so is this project. In particular the `ArgentAccount.cairo` contract has not been audited yet and should not be used to store significant value.*

## High-Level Specification

The account is a 2-of-2 custom multisig where the `signer` key is typically stored on the user's phone and the `guardian` key is managed by an off-chain service to enable fraud monitoring (e.g. trusted contacts, daily limits, etc). The user can always opt-out of the guardian service and manage the `guardian` key himself.
The account is a 2-of-2 custom multisig where the `signer` key is typically stored on the user's phone and the `guardian` is an external contract that can validate the signatures of one or more keys.
The `guardian` acts both as a co-validator for typical operations of the wallet, and as the trusted actor that can recover the wallet in case the `signer` key is lost or compromised.
These two features may have different key requirements (e.g. a single key for fraud monitoring, and a n-of-m setup for 'social' recovery) as encapsulated by the logic of the `guardian` contract.

Normal operations of the wallet (`execute`, `change_signer` and `change_guardian`) require both signatures to be executed.
By default the `guardian` has a single key managed by an off-chain service to enable fraud monitoring (e.g. trusted contacts, daily limits, etc) and recovery. The user can always opt-out of the guardian service and select a `guardian` contract with different key requirements.

Each party alone can trigger the `escape` mode on the wallet if the other party is not cooperating or lost. An escape takes 7 days before being active, after which the non-cooperating party can be replaced. The wallet is asymmetric in favor of the `signer` who can override an escape triggered by the `guardian`.
Normal operations of the wallet (`execute`, `change_signer`, `change_guardian`, `cancel_escape`) require the approval of both parties to be executed.

A triggered escape can always be cancelled with both signatures.
Each party alone can trigger the `escape` mode (a.k.a. recovery) on the wallet if the other party is not cooperating or lost. An escape takes 7 days before being active, after which the non-cooperating party can be replaced.
The wallet is always asymmetric in favor of one of the party depending on the `weight` of the `guardian`. The favoured party can always override an escape triggered by the other party.

A triggered escape can always be cancelled with the approval of both parties.

We assume that the `signer` key is backed up such that the probability of the `signer` key being lost should be close to zero.

Expand Down
84 changes: 70 additions & 14 deletions contracts/ArgentAccount.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
from starkware.cairo.common.signature import verify_ecdsa_signature
from starkware.cairo.common.registers import get_fp_and_pc
from starkware.cairo.common.alloc import alloc
from starkware.cairo.common.memcpy import memcpy
from starkware.cairo.common.math import assert_not_zero, assert_le, assert_nn
from starkware.starknet.common.syscalls import call_contract, get_tx_signature, get_contract_address, get_caller_address
from starkware.cairo.common.hash_state import (
hash_init, hash_finalize, hash_update, hash_update_single
)

####################
# INTERFACE
####################

@contract_interface
namespace IGuardian:
func is_valid_signature(hash: felt, sig_len: felt, sig: felt*):
end
func weight() -> (weight: felt):
end
end
####################
# CONSTANTS
####################
Expand Down Expand Up @@ -104,6 +118,11 @@ func execute{
# compute message hash
let (message_hash) = get_message_hash(to, selector, calldata_len, calldata, nonce)

# rebind pointers
local syscall_ptr: felt* = syscall_ptr
local range_check_ptr = range_check_ptr
local pedersen_ptr: HashBuiltin* = pedersen_ptr

if to == self:
tempvar signer_condition = (selector - ESCAPE_GUARDIAN_SELECTOR) * (selector - TRIGGER_ESCAPE_GUARDIAN_SELECTOR)
tempvar guardian_condition = (selector - ESCAPE_SIGNER_SELECTOR) * (selector - TRIGGER_ESCAPE_SIGNER_SELECTOR)
Expand All @@ -113,8 +132,10 @@ func execute{
jmp do_execute
end
if guardian_condition == 0:
# add flag to indicate an escape
let (extended_sig) = add_escape_flag(sig, sig_len)
# validate guardian signature
validate_guardian_signature(message_hash, sig, sig_len)
validate_guardian_signature(message_hash, extended_sig, sig_len + 1)
jmp do_execute
end
end
Expand All @@ -130,7 +151,6 @@ func execute{
calldata_size=calldata_len,
calldata=calldata
)

return (response=response.retdata_size)
end

Expand All @@ -142,7 +162,6 @@ func change_signer{
} (
new_signer: felt
):

# only called via execute
assert_only_self()

Expand Down Expand Up @@ -175,11 +194,27 @@ func trigger_escape_guardian{
pedersen_ptr: HashBuiltin*,
range_check_ptr
} ():

alloc_locals

# only called via execute
assert_only_self()
# no escape when the guardian is not set
assert_guardian_set()
let (guardian) = assert_guardian_set()

# no escape if there is an escape by the guardian and guardian has weight > 1
let (current_escape) = _escape.read()
if current_escape.caller == guardian:
let (weight) = IGuardian.weight(contract_address=guardian)
# assert weight <= 1
assert_nn(1 - weight)
tempvar syscall_ptr: felt* = syscall_ptr
tempvar range_check_ptr = range_check_ptr
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
else:
tempvar syscall_ptr: felt* = syscall_ptr
tempvar range_check_ptr = range_check_ptr
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
end

# store new escape
let (block_timestamp) = _block_timestamp.read()
Expand All @@ -202,10 +237,19 @@ func trigger_escape_signer{
# no escape when the guardian is not set
let (guardian) = assert_guardian_set()

# no escape when there is an ongoing escape by the signer
# no escape if there is an escape by the signer and guardian has weight <= 1
let (current_escape) = _escape.read()
if current_escape.active_at != 0:
assert current_escape.caller = guardian
if ((current_escape.caller - guardian) * current_escape.caller) != 0:
let (weight) = IGuardian.weight(contract_address=guardian)
# assert weight > 1
assert_nn(weight - 2)
tempvar syscall_ptr: felt* = syscall_ptr
tempvar range_check_ptr = range_check_ptr
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
else:
tempvar syscall_ptr: felt* = syscall_ptr
tempvar range_check_ptr = range_check_ptr
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
end
# store new escape
Expand Down Expand Up @@ -425,27 +469,39 @@ end
func validate_guardian_signature{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
ecdsa_ptr : SignatureBuiltin*,
ecdsa_ptr: SignatureBuiltin*,
range_check_ptr
} (
message: felt,
signatures: felt*,
signatures_len: felt
) -> ():
alloc_locals
let (guardian) = _guardian.read()
if guardian == 0:
return()
else:
assert_nn(signatures_len - 2)
verify_ecdsa_signature(
message=message,
public_key=guardian,
signature_r=signatures[0],
signature_s=signatures[1])
IGuardian.is_valid_signature(contract_address=guardian, hash=message, sig_len=signatures_len, sig=signatures)
return()
end
end

func add_escape_flag{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} (
sig: felt*,
sig_len: felt
) -> (extended: felt*):
alloc_locals
let (local extended : felt*) = alloc()
memcpy(extended, sig, sig_len)
assert [extended+sig_len] = 'escape'
return(extended=extended)
end

func get_message_hash{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
Expand Down
94 changes: 94 additions & 0 deletions contracts/guardians/SCSKGuardian.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
%lang starknet
%builtins pedersen range_check ecdsa

from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
from starkware.cairo.common.signature import verify_ecdsa_signature
from starkware.cairo.common.hash import hash2
from starkware.cairo.common.math import assert_not_zero, assert_nn
from starkware.starknet.common.syscalls import get_tx_signature

######################################
# Single Common Stark Key Guardian
######################################

@storage_var
func _signing_key() -> (res: felt):
end
@constructor
func constructor{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} (
signing_key: felt
):
assert_not_zero(signing_key)
_signing_key.write(signing_key)
return ()
end

@external
func set_signing_key{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
ecdsa_ptr: SignatureBuiltin*,
range_check_ptr
} (
new_signing_key: felt
) -> ():

# get the signature
let (sig_len : felt, sig : felt*) = get_tx_signature()
# Verify the signature length.
assert_nn(sig_len - 2)
# Compute the hash of the message.
let (hash) = hash2{hash_ptr=pedersen_ptr}(new_signing_key, 0)
# get the existing signing key
let (signing_key) = _signing_key.read()
# verify the signature
verify_ecdsa_signature(
message=hash,
public_key=signing_key,
signature_r=sig[0],
signature_s=sig[1])
# set the new key
_signing_key.write(new_signing_key)
return()
end

@view
func is_valid_signature{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
ecdsa_ptr: SignatureBuiltin*,
range_check_ptr
} (
hash: felt,
sig_len: felt,
sig: felt*
) -> ():
assert_nn(sig_len - 2)
let (signing_key) = _signing_key.read()
verify_ecdsa_signature(
message=hash,
public_key=signing_key,
signature_r=sig[0],
signature_s=sig[1])
return()
end

@view
func weight() -> (weight: felt):
return (weight=1)
end
@view
func get_signing_key{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} () -> (signing_key: felt):
let (signing_key) = _signing_key.read()
return (signing_key=signing_key)
end
107 changes: 107 additions & 0 deletions contracts/guardians/USDKGuardian.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
%lang starknet
%builtins pedersen range_check ecdsa

from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
from starkware.cairo.common.signature import verify_ecdsa_signature
from starkware.cairo.common.math import assert_not_zero
from starkware.starknet.common.syscalls import get_caller_address

##############################################
# User selected different single keys Guardian
##############################################

@storage_var
func _signing_key(account : felt) -> (res: felt):
end
@storage_var
func _escape_key(account : felt) -> (res: felt):
end
@view
func is_valid_signature{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
ecdsa_ptr: SignatureBuiltin*,
range_check_ptr
} (
hash: felt,
sig_len: felt,
sig: felt*
) -> ():

let (account) = get_caller_address()
if sig_len == 3:
assert [sig + 2] = 'escape'
let (key) = _escape_key.read(account)
verify_ecdsa_signature(
message=hash,
public_key=key,
signature_r=sig[0],
signature_s=sig[1])
return()
end

if sig_len == 2:
let (key) = _signing_key.read(account)
verify_ecdsa_signature(
message=hash,
public_key=key,
signature_r=sig[0],
signature_s=sig[1])
return()
end
assert_not_zero(0)
return()
end

@view
func weight() -> (weight: felt):
return (weight=1)
end
@external
func set_signing_key{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} (
key: felt
) -> ():
let (account) = get_caller_address()
_signing_key.write(account, key)
return()
end

@external
func set_escape_key{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} (
key: felt
) -> ():
let (account) = get_caller_address()
_escape_key.write(account, key)
return()
end

@view
func get_signing_key{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} (account: felt) -> (signing_key: felt):
let (signing_key) = _signing_key.read(account)
return (signing_key=signing_key)
end

@view
func get_escape_key{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
} (account: felt) -> (escape_key: felt):
let (escape_key) = _escape_key.read(account)
return (escape_key=escape_key)
end
Loading

0 comments on commit 2adeb80

Please sign in to comment.