Skip to content

Commit 2adeb80

Browse files
authored
Merge pull request #18 from argentlabs/feature/contract_guardian
Contract guardian
2 parents 84183a2 + 2f528c7 commit 2adeb80

File tree

6 files changed

+485
-53
lines changed

6 files changed

+485
-53
lines changed

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
# Argent Account on Starknet
1+
# Argent Account on StarkNet
22

3-
Preliminary work for an Argent Account on Starknet.
3+
*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.*
44

55
## High-Level Specification
66

7-
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.
7+
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.
8+
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.
9+
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.
810

9-
Normal operations of the wallet (`execute`, `change_signer` and `change_guardian`) require both signatures to be executed.
11+
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.
1012

11-
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`.
13+
Normal operations of the wallet (`execute`, `change_signer`, `change_guardian`, `cancel_escape`) require the approval of both parties to be executed.
1214

13-
A triggered escape can always be cancelled with both signatures.
15+
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.
16+
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.
17+
18+
A triggered escape can always be cancelled with the approval of both parties.
1419

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

contracts/ArgentAccount.cairo

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,26 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
55
from starkware.cairo.common.signature import verify_ecdsa_signature
66
from starkware.cairo.common.registers import get_fp_and_pc
77
from starkware.cairo.common.alloc import alloc
8+
from starkware.cairo.common.memcpy import memcpy
89
from starkware.cairo.common.math import assert_not_zero, assert_le, assert_nn
910
from starkware.starknet.common.syscalls import call_contract, get_tx_signature, get_contract_address, get_caller_address
1011
from starkware.cairo.common.hash_state import (
1112
hash_init, hash_finalize, hash_update, hash_update_single
1213
)
1314

15+
####################
16+
# INTERFACE
17+
####################
18+
19+
@contract_interface
20+
namespace IGuardian:
21+
func is_valid_signature(hash: felt, sig_len: felt, sig: felt*):
22+
end
23+
24+
func weight() -> (weight: felt):
25+
end
26+
end
27+
1428
####################
1529
# CONSTANTS
1630
####################
@@ -104,6 +118,11 @@ func execute{
104118
# compute message hash
105119
let (message_hash) = get_message_hash(to, selector, calldata_len, calldata, nonce)
106120
121+
# rebind pointers
122+
local syscall_ptr: felt* = syscall_ptr
123+
local range_check_ptr = range_check_ptr
124+
local pedersen_ptr: HashBuiltin* = pedersen_ptr
125+
107126
if to == self:
108127
tempvar signer_condition = (selector - ESCAPE_GUARDIAN_SELECTOR) * (selector - TRIGGER_ESCAPE_GUARDIAN_SELECTOR)
109128
tempvar guardian_condition = (selector - ESCAPE_SIGNER_SELECTOR) * (selector - TRIGGER_ESCAPE_SIGNER_SELECTOR)
@@ -113,8 +132,10 @@ func execute{
113132
jmp do_execute
114133
end
115134
if guardian_condition == 0:
135+
# add flag to indicate an escape
136+
let (extended_sig) = add_escape_flag(sig, sig_len)
116137
# validate guardian signature
117-
validate_guardian_signature(message_hash, sig, sig_len)
138+
validate_guardian_signature(message_hash, extended_sig, sig_len + 1)
118139
jmp do_execute
119140
end
120141
end
@@ -130,7 +151,6 @@ func execute{
130151
calldata_size=calldata_len,
131152
calldata=calldata
132153
)
133-
134154
return (response=response.retdata_size)
135155
end
136156
@@ -142,7 +162,6 @@ func change_signer{
142162
} (
143163
new_signer: felt
144164
):
145-
146165
# only called via execute
147166
assert_only_self()
148167
@@ -175,11 +194,27 @@ func trigger_escape_guardian{
175194
pedersen_ptr: HashBuiltin*,
176195
range_check_ptr
177196
} ():
178-
197+
alloc_locals
198+
179199
# only called via execute
180200
assert_only_self()
181201
# no escape when the guardian is not set
182-
assert_guardian_set()
202+
let (guardian) = assert_guardian_set()
203+
204+
# no escape if there is an escape by the guardian and guardian has weight > 1
205+
let (current_escape) = _escape.read()
206+
if current_escape.caller == guardian:
207+
let (weight) = IGuardian.weight(contract_address=guardian)
208+
# assert weight <= 1
209+
assert_nn(1 - weight)
210+
tempvar syscall_ptr: felt* = syscall_ptr
211+
tempvar range_check_ptr = range_check_ptr
212+
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
213+
else:
214+
tempvar syscall_ptr: felt* = syscall_ptr
215+
tempvar range_check_ptr = range_check_ptr
216+
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
217+
end
183218
184219
# store new escape
185220
let (block_timestamp) = _block_timestamp.read()
@@ -202,10 +237,19 @@ func trigger_escape_signer{
202237
# no escape when the guardian is not set
203238
let (guardian) = assert_guardian_set()
204239
205-
# no escape when there is an ongoing escape by the signer
240+
# no escape if there is an escape by the signer and guardian has weight <= 1
206241
let (current_escape) = _escape.read()
207-
if current_escape.active_at != 0:
208-
assert current_escape.caller = guardian
242+
if ((current_escape.caller - guardian) * current_escape.caller) != 0:
243+
let (weight) = IGuardian.weight(contract_address=guardian)
244+
# assert weight > 1
245+
assert_nn(weight - 2)
246+
tempvar syscall_ptr: felt* = syscall_ptr
247+
tempvar range_check_ptr = range_check_ptr
248+
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
249+
else:
250+
tempvar syscall_ptr: felt* = syscall_ptr
251+
tempvar range_check_ptr = range_check_ptr
252+
tempvar pedersen_ptr: HashBuiltin* = pedersen_ptr
209253
end
210254
211255
# store new escape
@@ -425,27 +469,39 @@ end
425469
func validate_guardian_signature{
426470
syscall_ptr: felt*,
427471
pedersen_ptr: HashBuiltin*,
428-
ecdsa_ptr : SignatureBuiltin*,
472+
ecdsa_ptr: SignatureBuiltin*,
429473
range_check_ptr
430474
} (
431475
message: felt,
432476
signatures: felt*,
433477
signatures_len: felt
434478
) -> ():
479+
alloc_locals
435480
let (guardian) = _guardian.read()
436481
if guardian == 0:
437482
return()
438483
else:
439484
assert_nn(signatures_len - 2)
440-
verify_ecdsa_signature(
441-
message=message,
442-
public_key=guardian,
443-
signature_r=signatures[0],
444-
signature_s=signatures[1])
485+
IGuardian.is_valid_signature(contract_address=guardian, hash=message, sig_len=signatures_len, sig=signatures)
445486
return()
446487
end
447488
end
448489
490+
func add_escape_flag{
491+
syscall_ptr: felt*,
492+
pedersen_ptr: HashBuiltin*,
493+
range_check_ptr
494+
} (
495+
sig: felt*,
496+
sig_len: felt
497+
) -> (extended: felt*):
498+
alloc_locals
499+
let (local extended : felt*) = alloc()
500+
memcpy(extended, sig, sig_len)
501+
assert [extended+sig_len] = 'escape'
502+
return(extended=extended)
503+
end
504+
449505
func get_message_hash{
450506
syscall_ptr: felt*,
451507
pedersen_ptr: HashBuiltin*,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
%lang starknet
2+
%builtins pedersen range_check ecdsa
3+
4+
from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
5+
from starkware.cairo.common.signature import verify_ecdsa_signature
6+
from starkware.cairo.common.hash import hash2
7+
from starkware.cairo.common.math import assert_not_zero, assert_nn
8+
from starkware.starknet.common.syscalls import get_tx_signature
9+
10+
######################################
11+
# Single Common Stark Key Guardian
12+
######################################
13+
14+
@storage_var
15+
func _signing_key() -> (res: felt):
16+
end
17+
18+
@constructor
19+
func constructor{
20+
syscall_ptr: felt*,
21+
pedersen_ptr: HashBuiltin*,
22+
range_check_ptr
23+
} (
24+
signing_key: felt
25+
):
26+
assert_not_zero(signing_key)
27+
_signing_key.write(signing_key)
28+
return ()
29+
end
30+
31+
@external
32+
func set_signing_key{
33+
syscall_ptr: felt*,
34+
pedersen_ptr: HashBuiltin*,
35+
ecdsa_ptr: SignatureBuiltin*,
36+
range_check_ptr
37+
} (
38+
new_signing_key: felt
39+
) -> ():
40+
41+
# get the signature
42+
let (sig_len : felt, sig : felt*) = get_tx_signature()
43+
# Verify the signature length.
44+
assert_nn(sig_len - 2)
45+
# Compute the hash of the message.
46+
let (hash) = hash2{hash_ptr=pedersen_ptr}(new_signing_key, 0)
47+
# get the existing signing key
48+
let (signing_key) = _signing_key.read()
49+
# verify the signature
50+
verify_ecdsa_signature(
51+
message=hash,
52+
public_key=signing_key,
53+
signature_r=sig[0],
54+
signature_s=sig[1])
55+
# set the new key
56+
_signing_key.write(new_signing_key)
57+
return()
58+
end
59+
60+
@view
61+
func is_valid_signature{
62+
syscall_ptr: felt*,
63+
pedersen_ptr: HashBuiltin*,
64+
ecdsa_ptr: SignatureBuiltin*,
65+
range_check_ptr
66+
} (
67+
hash: felt,
68+
sig_len: felt,
69+
sig: felt*
70+
) -> ():
71+
assert_nn(sig_len - 2)
72+
let (signing_key) = _signing_key.read()
73+
verify_ecdsa_signature(
74+
message=hash,
75+
public_key=signing_key,
76+
signature_r=sig[0],
77+
signature_s=sig[1])
78+
return()
79+
end
80+
81+
@view
82+
func weight() -> (weight: felt):
83+
return (weight=1)
84+
end
85+
86+
@view
87+
func get_signing_key{
88+
syscall_ptr: felt*,
89+
pedersen_ptr: HashBuiltin*,
90+
range_check_ptr
91+
} () -> (signing_key: felt):
92+
let (signing_key) = _signing_key.read()
93+
return (signing_key=signing_key)
94+
end
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
%lang starknet
2+
%builtins pedersen range_check ecdsa
3+
4+
from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
5+
from starkware.cairo.common.signature import verify_ecdsa_signature
6+
from starkware.cairo.common.math import assert_not_zero
7+
from starkware.starknet.common.syscalls import get_caller_address
8+
9+
##############################################
10+
# User selected different single keys Guardian
11+
##############################################
12+
13+
@storage_var
14+
func _signing_key(account : felt) -> (res: felt):
15+
end
16+
17+
@storage_var
18+
func _escape_key(account : felt) -> (res: felt):
19+
end
20+
21+
@view
22+
func is_valid_signature{
23+
syscall_ptr: felt*,
24+
pedersen_ptr: HashBuiltin*,
25+
ecdsa_ptr: SignatureBuiltin*,
26+
range_check_ptr
27+
} (
28+
hash: felt,
29+
sig_len: felt,
30+
sig: felt*
31+
) -> ():
32+
33+
let (account) = get_caller_address()
34+
if sig_len == 3:
35+
assert [sig + 2] = 'escape'
36+
let (key) = _escape_key.read(account)
37+
verify_ecdsa_signature(
38+
message=hash,
39+
public_key=key,
40+
signature_r=sig[0],
41+
signature_s=sig[1])
42+
return()
43+
end
44+
45+
if sig_len == 2:
46+
let (key) = _signing_key.read(account)
47+
verify_ecdsa_signature(
48+
message=hash,
49+
public_key=key,
50+
signature_r=sig[0],
51+
signature_s=sig[1])
52+
return()
53+
end
54+
assert_not_zero(0)
55+
return()
56+
end
57+
58+
@view
59+
func weight() -> (weight: felt):
60+
return (weight=1)
61+
end
62+
63+
@external
64+
func set_signing_key{
65+
syscall_ptr: felt*,
66+
pedersen_ptr: HashBuiltin*,
67+
range_check_ptr
68+
} (
69+
key: felt
70+
) -> ():
71+
let (account) = get_caller_address()
72+
_signing_key.write(account, key)
73+
return()
74+
end
75+
76+
@external
77+
func set_escape_key{
78+
syscall_ptr: felt*,
79+
pedersen_ptr: HashBuiltin*,
80+
range_check_ptr
81+
} (
82+
key: felt
83+
) -> ():
84+
let (account) = get_caller_address()
85+
_escape_key.write(account, key)
86+
return()
87+
end
88+
89+
@view
90+
func get_signing_key{
91+
syscall_ptr: felt*,
92+
pedersen_ptr: HashBuiltin*,
93+
range_check_ptr
94+
} (account: felt) -> (signing_key: felt):
95+
let (signing_key) = _signing_key.read(account)
96+
return (signing_key=signing_key)
97+
end
98+
99+
@view
100+
func get_escape_key{
101+
syscall_ptr: felt*,
102+
pedersen_ptr: HashBuiltin*,
103+
range_check_ptr
104+
} (account: felt) -> (escape_key: felt):
105+
let (escape_key) = _escape_key.read(account)
106+
return (escape_key=escape_key)
107+
end

0 commit comments

Comments
 (0)