diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae1a29a5..9d6b9d91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,9 +3,9 @@ name: SX-Starknet Workflow env: STARKNET_SIERRA_COMPILE_PATH: ./cairo/bin/starknet-sierra-compile OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES - ADDRESS: "0x347be35996a21f6bf0623e75dbce52baba918ad5ae8d83b6f416045ab22961a" - PUBLIC_KEY: "0x674efe292c3c1125108916d6128bd6d1db4528db07322a84177551067aa2bef" - PK: "0xbdd640fb06671ad11c80317fa3b1799d" + ADDRESS: '0x347be35996a21f6bf0623e75dbce52baba918ad5ae8d83b6f416045ab22961a' + PUBLIC_KEY: '0x674efe292c3c1125108916d6128bd6d1db4528db07322a84177551067aa2bef' + PK: '0xbdd640fb06671ad11c80317fa3b1799d' on: push: @@ -116,4 +116,6 @@ jobs: run: yarn hardhat starknet-build - name: run Hardhat tests - run: yarn test:l1-execution; yarn test:eth-sig-auth; yarn test:stark-sig-auth; yarn test:eth-tx-auth + run: + yarn test:l1-execution; yarn test:eth-sig-auth; yarn test:stark-sig-auth; yarn test:eth-tx-auth; + yarn test:eth-sig-sk-auth; yarn test:stark-sig-sk-auth; yarn test:eth-tx-sk-auth; yarn test:stark-tx-sk-auth diff --git a/package.json b/package.json index 38d6eaf2..b535bf91 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,10 @@ "test:stark-sig-auth": "bash './scripts/stark-sig-auth-test.sh'", "test:eth-sig-auth": "bash './scripts/eth-sig-auth-test.sh'", "test:eth-tx-auth": "bash './scripts/eth-tx-auth-test.sh'", + "test:eth-sig-sk-auth": "bash './scripts/eth-sig-sk-auth-test.sh'", + "test:eth-tx-sk-auth": "bash './scripts/eth-tx-sk-auth-test.sh'", + "test:stark-sig-sk-auth": "bash './scripts/stark-sig-sk-auth-test.sh'", + "test:stark-tx-sk-auth": "bash './scripts/stark-tx-sk-auth-test.sh'", "test:l1-execution": "bash './scripts/l1-avatar-execution-test.sh'" }, "devDependencies": { diff --git a/scripts/eth-sig-sk-auth-test.sh b/scripts/eth-sig-sk-auth-test.sh new file mode 100644 index 00000000..aea1abc7 --- /dev/null +++ b/scripts/eth-sig-sk-auth-test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +kill -9 $(lsof -t -i:5050) +yarn chain:l2 & +sleep 10 && +yarn hardhat test tests/eth-sig-sk-auth.test.ts +if [ $? -eq 0 ] +then + kill -9 $(lsof -t -i:5050) + exit 0 +else + kill -9 $(lsof -t -i:5050) + echo "Tests failed" + exit 1 +fi \ No newline at end of file diff --git a/scripts/eth-tx-sk-auth-test.sh b/scripts/eth-tx-sk-auth-test.sh new file mode 100644 index 00000000..4b21491e --- /dev/null +++ b/scripts/eth-tx-sk-auth-test.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +kill -9 $(lsof -t -i:8545) +kill -9 $(lsof -t -i:5050) +yarn chain & +sleep 10 && +yarn hardhat test tests/eth-tx-sk-auth.test.ts --network 'ethereumLocal' --starknet-network 'starknetLocal' +if [ $? -eq 0 ] +then + kill -9 $(lsof -t -i:8545) + kill -9 $(lsof -t -i:5050) + exit 0 +else + kill -9 $(lsof -t -i:8545) + kill -9 $(lsof -t -i:5050) + echo "Tests failed" + exit 1 +fi \ No newline at end of file diff --git a/scripts/stark-sig-sk-auth-test.sh b/scripts/stark-sig-sk-auth-test.sh new file mode 100644 index 00000000..06355683 --- /dev/null +++ b/scripts/stark-sig-sk-auth-test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +kill -9 $(lsof -t -i:5050) +yarn chain:l2 & +sleep 10 && +yarn hardhat test tests/stark-sig-sk-auth.test.ts +if [ $? -eq 0 ] +then + kill -9 $(lsof -t -i:5050) + exit 0 +else + kill -9 $(lsof -t -i:5050) + echo "Tests failed" + exit 1 +fi \ No newline at end of file diff --git a/scripts/stark-tx-sk-auth-test.sh b/scripts/stark-tx-sk-auth-test.sh new file mode 100644 index 00000000..6cd7c527 --- /dev/null +++ b/scripts/stark-tx-sk-auth-test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +kill -9 $(lsof -t -i:5050) +yarn chain:l2 & +sleep 10 && +yarn hardhat test tests/stark-tx-sk-auth.test.ts +if [ $? -eq 0 ] +then + kill -9 $(lsof -t -i:5050) + exit 0 +else + kill -9 $(lsof -t -i:5050) + echo "Tests failed" + exit 1 +fi \ No newline at end of file diff --git a/starknet/Scarb.lock b/starknet/Scarb.lock new file mode 100644 index 00000000..e77b5ac9 --- /dev/null +++ b/starknet/Scarb.lock @@ -0,0 +1,14 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "openzeppelin" +version = "0.7.0-rc.0" +source = "git+https://github.com/snapshot-labs/openzeppelin-cairo-contracts.git?branch=feat%2Ferc20votes-%23631-frozen#e9d17922f938cc62c7dabaf13d700e7290b7c274" + +[[package]] +name = "sx" +version = "0.1.0" +dependencies = [ + "openzeppelin", +] diff --git a/starknet/src/.DS_Store b/starknet/src/.DS_Store new file mode 100644 index 00000000..51227d71 Binary files /dev/null and b/starknet/src/.DS_Store differ diff --git a/starknet/src/authenticators/eth_sig_session_key.cairo b/starknet/src/authenticators/eth_sig_session_key.cairo new file mode 100644 index 00000000..5a3b9d4c --- /dev/null +++ b/starknet/src/authenticators/eth_sig_session_key.cairo @@ -0,0 +1,211 @@ +use starknet::{ContractAddress, EthAddress}; +use sx::types::{Strategy, IndexedStrategy, Choice}; + +#[starknet::interface] +trait IEthSigSessionKeyAuthenticator { + fn authenticate_propose( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ); + + fn authenticate_vote( + ref self: TContractState, + signature: Array, + space: ContractAddress, + voter: EthAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ); + + fn authenticate_update_proposal( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ); + + fn register_with_owner_sig( + ref self: TContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + salt: u256, + ); + + fn revoke_with_owner_sig( + ref self: TContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + salt: u256, + ); + + fn revoke_with_session_key_sig( + ref self: TContractState, + signature: Array, + owner: EthAddress, + session_public_key: felt252, + salt: felt252 + ); +} + +#[starknet::contract] +mod EthSigSessionKeyAuthenticator { + use super::IEthSigSessionKeyAuthenticator; + use starknet::{ContractAddress, EthAddress}; + use sx::interfaces::{ISpaceDispatcher, ISpaceDispatcherTrait}; + use sx::types::{Strategy, IndexedStrategy, Choice, UserAddress}; + use sx::utils::{EIP712, SessionKey, LegacyHashEthAddress, LegacyHashUsedSalts, ByteReverse}; + + #[storage] + struct Storage {} + + #[external(v0)] + impl EthSigSessionKeyAuthenticator of IEthSigSessionKeyAuthenticator { + fn authenticate_propose( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_propose( + ref state, + signature, + space, + UserAddress::Ethereum(author), + metadata_uri, + execution_strategy, + user_proposal_validation_params, + salt, + session_public_key + ); + } + + fn authenticate_vote( + ref self: ContractState, + signature: Array, + space: ContractAddress, + voter: EthAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_vote( + ref state, + signature, + space, + UserAddress::Ethereum(voter), + proposal_id, + choice, + user_voting_strategies, + metadata_uri, + session_public_key + ); + } + + fn authenticate_update_proposal( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_update_proposal( + ref state, + signature, + space, + UserAddress::Ethereum(author), + proposal_id, + execution_strategy, + metadata_uri, + salt, + session_public_key + ); + } + + + fn register_with_owner_sig( + ref self: ContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + salt: u256, + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::register_with_owner_eth_sig( + ref state, r, s, v, owner, session_public_key, session_duration, salt + ); + } + + fn revoke_with_owner_sig( + ref self: ContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + salt: u256, + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_owner_eth_sig( + ref state, r, s, v, owner, session_public_key, salt + ); + } + + fn revoke_with_session_key_sig( + ref self: ContractState, + signature: Array, + owner: EthAddress, + session_public_key: felt252, + salt: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_session_key_sig( + ref state, signature, UserAddress::Ethereum(owner), session_public_key, salt + ); + } + } + + #[constructor] + fn constructor(ref self: ContractState, name: felt252, version: felt252,) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::eth_sig_initializer(ref state, name, version); + } +} diff --git a/starknet/src/authenticators/eth_tx_session_key.cairo b/starknet/src/authenticators/eth_tx_session_key.cairo new file mode 100644 index 00000000..0e0d5d60 --- /dev/null +++ b/starknet/src/authenticators/eth_tx_session_key.cairo @@ -0,0 +1,213 @@ +use starknet::{ContractAddress, EthAddress}; +use sx::types::{Strategy, IndexedStrategy, Choice}; + +#[starknet::interface] +trait IEthTxSessionKeyAuthenticator { + fn authenticate_propose( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ); + + fn authenticate_vote( + ref self: TContractState, + signature: Array, + space: ContractAddress, + voter: EthAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ); + + fn authenticate_update_proposal( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ); + + fn register_with_owner_tx( + ref self: TContractState, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + ); + + fn revoke_with_owner_tx( + ref self: TContractState, owner: EthAddress, session_public_key: felt252 + ); + + fn revoke_with_session_key_sig( + ref self: TContractState, + signature: Array, + owner: EthAddress, + salt: felt252, + session_public_key: felt252 + ); +} + + +#[starknet::contract] +mod EthTxSessionKeyAuthenticator { + use core::traits::AddEq; + use super::IEthTxSessionKeyAuthenticator; + use starknet::{ContractAddress, EthAddress}; + use sx::interfaces::{ISpaceDispatcher, ISpaceDispatcherTrait}; + use sx::types::{Strategy, IndexedStrategy, Choice, UserAddress}; + use sx::utils::{SessionKey, LegacyHashFelt252EthAddress, LegacyHashUsedSalts}; + use sx::utils::constants::{ + REGISTER_SESSION_WITH_OWNER_TX_SELECTOR, REVOKE_SESSION_WITH_OWNER_TX_SELECTOR + }; + #[storage] + struct Storage { + _used_salts: LegacyMap::<(EthAddress, u256), bool>, + _starknet_commit_address: EthAddress, + _commits: LegacyMap::<(felt252, EthAddress), bool> + } + + #[external(v0)] + impl EthTxSessionKeyAuthenticator of IEthTxSessionKeyAuthenticator { + fn authenticate_propose( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_propose( + ref state, + signature, + space, + UserAddress::Ethereum(author), + metadata_uri, + execution_strategy, + user_proposal_validation_params, + salt, + session_public_key + ); + } + + fn authenticate_vote( + ref self: ContractState, + signature: Array, + space: ContractAddress, + voter: EthAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_vote( + ref state, + signature, + space, + UserAddress::Ethereum(voter), + proposal_id, + choice, + user_voting_strategies, + metadata_uri, + session_public_key + ); + } + + fn authenticate_update_proposal( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: EthAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_update_proposal( + ref state, + signature, + space, + UserAddress::Ethereum(author), + proposal_id, + execution_strategy, + metadata_uri, + salt, + session_public_key + ); + } + + fn register_with_owner_tx( + ref self: ContractState, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::register_with_owner_eth_tx( + ref state, owner, session_public_key, session_duration + ); + } + + fn revoke_with_owner_tx( + ref self: ContractState, owner: EthAddress, session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_owner_eth_tx( + ref state, owner, session_public_key + ); + } + + fn revoke_with_session_key_sig( + ref self: ContractState, + signature: Array, + owner: EthAddress, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_session_key_sig( + ref state, signature, UserAddress::Ethereum(owner), salt, session_public_key + ); + } + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: felt252, + version: felt252, + starknet_commit_address: EthAddress + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::eth_tx_initializer( + ref state, name, version, starknet_commit_address + ); + } + + #[l1_handler] + fn commit( + ref self: ContractState, from_address: felt252, sender_address: felt252, hash: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::commit(ref state, from_address, sender_address, hash); + } +} diff --git a/starknet/src/authenticators/stark_sig_session_key.cairo b/starknet/src/authenticators/stark_sig_session_key.cairo new file mode 100644 index 00000000..25ecca00 --- /dev/null +++ b/starknet/src/authenticators/stark_sig_session_key.cairo @@ -0,0 +1,202 @@ +use starknet::{ContractAddress, EthAddress}; +use sx::types::{Strategy, IndexedStrategy, Choice}; + +#[starknet::interface] +trait IStarkSigSessionKeyAuthenticator { + fn authenticate_propose( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ); + + fn authenticate_vote( + ref self: TContractState, + signature: Array, + space: ContractAddress, + voter: ContractAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ); + + fn authenticate_update_proposal( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ); + fn register_with_owner_sig( + ref self: TContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32, + salt: felt252, + ); + + fn revoke_with_owner_sig( + ref self: TContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252, + ); + + fn revoke_with_session_key_sig( + ref self: TContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252 + ); +} + +#[starknet::contract] +mod StarkSigSessionKeyAuthenticator { + use super::IStarkSigSessionKeyAuthenticator; + use starknet::ContractAddress; + use sx::types::{Strategy, IndexedStrategy, Choice, UserAddress}; + use sx::utils::{SessionKey, StarkEIP712}; + + #[storage] + struct Storage {} + + #[external(v0)] + impl StarkSigSessionKeyAuthenticator of IStarkSigSessionKeyAuthenticator { + fn authenticate_propose( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_propose( + ref state, + signature, + space, + UserAddress::Starknet(author), + metadata_uri, + execution_strategy, + user_proposal_validation_params, + salt, + session_public_key + ); + } + + fn authenticate_vote( + ref self: ContractState, + signature: Array, + space: ContractAddress, + voter: ContractAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_vote( + ref state, + signature, + space, + UserAddress::Starknet(voter), + proposal_id, + choice, + user_voting_strategies, + metadata_uri, + session_public_key + ); + } + + fn authenticate_update_proposal( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_update_proposal( + ref state, + signature, + space, + UserAddress::Starknet(author), + proposal_id, + execution_strategy, + metadata_uri, + salt, + session_public_key + ); + } + + fn register_with_owner_sig( + ref self: ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32, + salt: felt252, + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::register_with_owner_stark_sig( + ref state, signature, owner, session_public_key, session_duration, salt + ); + } + + fn revoke_with_owner_sig( + ref self: ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252, + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_owner_stark_sig( + ref state, signature, owner, session_public_key, salt + ); + } + + fn revoke_with_session_key_sig( + ref self: ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_session_key_sig( + ref state, signature, UserAddress::Starknet(owner), session_public_key, salt + ); + } + } + + #[constructor] + fn constructor(ref self: ContractState, name: felt252, version: felt252) { + let mut state = StarkEIP712::unsafe_new_contract_state(); + StarkEIP712::InternalImpl::initializer(ref state, name, version); + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::eth_sig_initializer(ref state, name, version); + } +} diff --git a/starknet/src/authenticators/stark_tx_session_key.cairo b/starknet/src/authenticators/stark_tx_session_key.cairo new file mode 100644 index 00000000..c5a6206e --- /dev/null +++ b/starknet/src/authenticators/stark_tx_session_key.cairo @@ -0,0 +1,189 @@ +use starknet::ContractAddress; +use sx::types::{Strategy, IndexedStrategy, Choice}; + +#[starknet::interface] +trait IStarkTxSessionKeyAuthenticator { + fn authenticate_propose( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ); + + fn authenticate_vote( + ref self: TContractState, + signature: Array, + space: ContractAddress, + voter: ContractAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ); + + fn authenticate_update_proposal( + ref self: TContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ); + + fn register_with_owner_tx( + ref self: TContractState, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32 + ); + + fn revoke_with_owner_tx( + ref self: TContractState, owner: ContractAddress, session_public_key: felt252 + ); + + fn revoke_with_session_key_sig( + ref self: TContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252 + ); +} + +#[starknet::contract] +mod StarkTxSessionKeyAuthenticator { + use super::IStarkTxSessionKeyAuthenticator; + use starknet::ContractAddress; + use sx::types::{Strategy, IndexedStrategy, Choice, UserAddress}; + use sx::utils::SessionKey; + + #[storage] + struct Storage {} + + #[external(v0)] + impl StarkTxSessionKeyAuthenticator of IStarkTxSessionKeyAuthenticator { + fn authenticate_propose( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_propose( + ref state, + signature, + space, + UserAddress::Starknet(author), + metadata_uri, + execution_strategy, + user_proposal_validation_params, + salt, + session_public_key + ); + } + + fn authenticate_vote( + ref self: ContractState, + signature: Array, + space: ContractAddress, + voter: ContractAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_vote( + ref state, + signature, + space, + UserAddress::Starknet(voter), + proposal_id, + choice, + user_voting_strategies, + metadata_uri, + session_public_key + ); + } + + fn authenticate_update_proposal( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: ContractAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::authenticate_update_proposal( + ref state, + signature, + space, + UserAddress::Starknet(author), + proposal_id, + execution_strategy, + metadata_uri, + salt, + session_public_key + ); + } + + fn register_with_owner_tx( + ref self: ContractState, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::register_with_owner_stark_tx( + ref state, owner, session_public_key, session_duration + ); + } + + fn revoke_with_owner_tx( + ref self: ContractState, owner: ContractAddress, session_public_key: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_owner_stark_tx( + ref state, owner, session_public_key + ); + } + + fn revoke_with_session_key_sig( + ref self: ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252 + ) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::revoke_with_session_key_sig( + ref state, signature, UserAddress::Starknet(owner), session_public_key, salt + ); + } + } + + #[constructor] + fn constructor(ref self: ContractState, name: felt252, version: felt252) { + let mut state = SessionKey::unsafe_new_contract_state(); + SessionKey::InternalImpl::eth_sig_initializer(ref state, name, version); + } +} diff --git a/starknet/src/lib.cairo b/starknet/src/lib.cairo index e5686a55..c7eb092d 100644 --- a/starknet/src/lib.cairo +++ b/starknet/src/lib.cairo @@ -10,6 +10,18 @@ mod authenticators { mod stark_tx; use stark_tx::StarkTxAuthenticator; + + mod eth_sig_session_key; + use eth_sig_session_key::EthSigSessionKeyAuthenticator; + + mod eth_tx_session_key; + use eth_tx_session_key::IEthTxSessionKeyAuthenticator; + + mod stark_sig_session_key; + use stark_sig_session_key::StarkSigSessionKeyAuthenticator; + + mod stark_tx_session_key; + use stark_tx_session_key::StarkTxSessionKeyAuthenticator; } mod execution_strategies { @@ -86,7 +98,7 @@ mod types { use finalization_status::FinalizationStatus; mod user_address; - use user_address::{UserAddress, UserAddressTrait}; + use user_address::{UserAddress, UserAddressTrait, UserAddressIntoFelt}; mod indexed_strategy; use indexed_strategy::{IndexedStrategy, IndexedStrategyImpl, IndexedStrategyTrait}; @@ -131,7 +143,8 @@ mod utils { mod legacy_hash; use legacy_hash::{ LegacyHashEthAddress, LegacyHashFelt252EthAddress, LegacyHashUsedSalts, LegacyHashChoice, - LegacyHashUserAddress, LegacyHashVotePower, LegacyHashVoteRegistry, LegacyHashSpanFelt252 + LegacyHashUserAddress, LegacyHashVotePower, LegacyHashVoteRegistry, LegacyHashSpanFelt252, + LegacyHashUserAddressU256 }; mod math; @@ -144,6 +157,9 @@ mod utils { mod reinitializable; use reinitializable::Reinitializable; + mod session_key; + use session_key::SessionKey; + mod simple_majority; mod simple_quorum; diff --git a/starknet/src/tests/.DS_Store b/starknet/src/tests/.DS_Store new file mode 100644 index 00000000..7cc77677 Binary files /dev/null and b/starknet/src/tests/.DS_Store differ diff --git a/starknet/src/types/user_address.cairo b/starknet/src/types/user_address.cairo index b286a2a8..361129a0 100644 --- a/starknet/src/types/user_address.cairo +++ b/starknet/src/types/user_address.cairo @@ -78,6 +78,18 @@ impl UserAddressZeroable of Zeroable { } } +impl UserAddressIntoFelt of Into { + fn into(self: UserAddress) -> felt252 { + match self { + UserAddress::Starknet(address) => address.into(), + UserAddress::Ethereum(address) => address.into(), + UserAddress::Custom(address) => { + panic_with_felt252('Undefined') + } + } + } +} + #[cfg(test)] mod tests { use super::{UserAddress, UserAddressZeroable}; diff --git a/starknet/src/utils/constants.cairo b/starknet/src/utils/constants.cairo index ebafb5f3..8e598575 100644 --- a/starknet/src/utils/constants.cairo +++ b/starknet/src/utils/constants.cairo @@ -34,6 +34,18 @@ const VOTE_TYPEHASH_LOW: u128 = 0x298f2993575d81a3b6ec2d52ae694cb7; const UPDATE_PROPOSAL_TYPEHASH_HIGH: u128 = 0x2df41899fffb50338812b0c6bb5db608; const UPDATE_PROPOSAL_TYPEHASH_LOW: u128 = 0x9503bccdcdb9a1ed596eea5bb5087f84; +// keccak256( +// "SessionKeyAuth(uint256 chainId,uint256 authenticator,address owner,uint256 sessionPublicKey,uint256 sessionDuration,uint256 salt)" +// ) +const SESSION_KEY_AUTH_TYPEHASH_HIGH: u128 = 0xbf6f5331c3a2c744889ff780cad3d0f2; +const SESSION_KEY_AUTH_TYPEHASH_LOW: u128 = 0x550df4038acab427712b5b0b38e43c3d; + +// keccak256( +// "SessionKeyRevoke(uint256 chainId,uint256 authenticator,address owner,uint256 sessionPublicKey,uint256 salt)" +// ) +const SESSION_KEY_REVOKE_TYPEHASH_HIGH: u128 = 0xf607352669f95230a0602015a636355d; +const SESSION_KEY_REVOKE_TYPEHASH_LOW: u128 = 0x1ef75bc12feec22425baac28de317513; + // keccak256("Strategy(uint256 address,uint256[] params)") const STRATEGY_TYPEHASH_HIGH: u128 = 0xa6cb034787a88e7219605b9db792cb9a; const STRATEGY_TYPEHASH_LOW: u128 = 0x312314462975078b4bdad10feee486d9; @@ -62,6 +74,14 @@ const VOTE_TYPEHASH: felt252 = 0x1d9763f87aaaeb271287d4b9c84053d3f201ad61efc2c32 const UPDATE_PROPOSAL_TYPEHASH: felt252 = 0x34f1b3fe98891caddfc18d9b8d3bee36be34145a6e9f7a7bb76a45038dda780; +// H('SessionKeyAuth(owner:felt252,sessionPublicKey:felt252,sessionDuration:felt252,salt:felt252)') +const SESSION_KEY_AUTH_TYPEHASH: felt252 = + 0x3AE06AD61C8456C0833FD6862CD5D5F3CE96C8B9EB80B4B7FB2D0FF15C840F6; + +// H('SessionKeyRevoke(owner:felt252,sessionPublicKey:felt252,salt:felt252)') +const SESSION_KEY_REVOKE_TYPEHASH: felt252 = + 0x11FA5E8349D04FAA798D9F772F97D151A10FAF60B5CC9022CECA1D0A6BB06A; + // StarknetKeccak('Strategy(address:felt252,params:felt*)') const STRATEGY_TYPEHASH: felt252 = 0x39154ec0efadcd0deffdfc2044cf45dd986d260e59c26d69564b50a18f40f6b; @@ -78,3 +98,9 @@ const U256_TYPEHASH: felt252 = 0x1094260a770342332e6a73e9256b901d484a43892531620 const ERC165_ACCOUNT_INTERFACE_ID: felt252 = 0x2ceccef7f994940b3962a6c67e0ba4fcd37df7d131417c604f91e03caecc1cd; // SNIP-6 compliant account ID, functions are snake case + + +// ------ Pseudo selectors for Tx based Session key authentication ------ +const REGISTER_SESSION_WITH_OWNER_TX_SELECTOR: felt252 = 'register_session'; + +const REVOKE_SESSION_WITH_OWNER_TX_SELECTOR: felt252 = 'revoke_session'; diff --git a/starknet/src/utils/eip712.cairo b/starknet/src/utils/eip712.cairo index 94da6eff..a9843eac 100644 --- a/starknet/src/utils/eip712.cairo +++ b/starknet/src/utils/eip712.cairo @@ -1,5 +1,6 @@ #[starknet::contract] mod EIP712 { + use integer::U32IntoU256; use starknet::{EthAddress, ContractAddress, secp256_trait}; use starknet::secp256k1::Secp256k1Point; use sx::types::{Strategy, IndexedStrategy, Choice}; @@ -7,7 +8,9 @@ mod EIP712 { use sx::utils::constants::{ DOMAIN_HASH_LOW, DOMAIN_HASH_HIGH, ETHEREUM_PREFIX, PROPOSE_TYPEHASH_LOW, PROPOSE_TYPEHASH_HIGH, VOTE_TYPEHASH_LOW, VOTE_TYPEHASH_HIGH, UPDATE_PROPOSAL_TYPEHASH_LOW, - UPDATE_PROPOSAL_TYPEHASH_HIGH, INDEXED_STRATEGY_TYPEHASH_LOW, + UPDATE_PROPOSAL_TYPEHASH_HIGH, SESSION_KEY_AUTH_TYPEHASH_HIGH, + SESSION_KEY_AUTH_TYPEHASH_LOW, SESSION_KEY_REVOKE_TYPEHASH_HIGH, + SESSION_KEY_REVOKE_TYPEHASH_LOW, INDEXED_STRATEGY_TYPEHASH_LOW, INDEXED_STRATEGY_TYPEHASH_HIGH, }; @@ -87,6 +90,38 @@ mod EIP712 { ); } + fn verify_session_key_auth_sig( + self: @ContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + salt: u256 + ) { + let digest: u256 = self + .get_session_key_auth_digest(owner, session_public_key, session_duration, salt); + secp256_trait::verify_eth_signature::( + digest, secp256_trait::signature_from_vrs(v, r, s), owner + ); + } + + fn verify_session_key_revoke_sig( + self: @ContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + salt: u256 + ) { + let digest: u256 = self.get_session_key_revoke_digest(owner, session_public_key, salt); + secp256_trait::verify_eth_signature::( + digest, secp256_trait::signature_from_vrs(v, r, s), owner + ); + } + /// Returns the digest of the propose calldata. fn get_propose_digest( self: @ContractState, @@ -162,6 +197,43 @@ mod EIP712 { self.hash_typed_data(message_hash) } + fn get_session_key_auth_digest( + self: @ContractState, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + salt: u256 + ) -> u256 { + let encoded_data = array![ + u256 { low: SESSION_KEY_AUTH_TYPEHASH_LOW, high: SESSION_KEY_AUTH_TYPEHASH_HIGH }, + Felt252IntoU256::into(starknet::get_tx_info().unbox().chain_id), + starknet::get_contract_address().into(), + owner.into(), + Felt252IntoU256::into(session_public_key), + U32IntoU256::into(session_duration), + salt + ]; + let message_hash = keccak::keccak_u256s_be_inputs(encoded_data.span()).byte_reverse(); + self.hash_typed_data(message_hash) + } + + fn get_session_key_revoke_digest( + self: @ContractState, owner: EthAddress, session_public_key: felt252, salt: u256 + ) -> u256 { + let encoded_data = array![ + u256 { + low: SESSION_KEY_REVOKE_TYPEHASH_LOW, high: SESSION_KEY_REVOKE_TYPEHASH_HIGH + }, + Felt252IntoU256::into(starknet::get_tx_info().unbox().chain_id), + starknet::get_contract_address().into(), + owner.into(), + Felt252IntoU256::into(session_public_key), + salt + ]; + let message_hash = keccak::keccak_u256s_be_inputs(encoded_data.span()).byte_reverse(); + self.hash_typed_data(message_hash) + } + /// Hashes typed data according to the EIP-712 specification. fn hash_typed_data(self: @ContractState, message_hash: u256) -> u256 { let encoded_data = InternalImpl::add_prefix_array( @@ -199,7 +271,6 @@ mod EIP712 { out } - /// Adds a 16 bit prefix to a 128 bit input, returning the result and a carry. fn add_prefix_u128(input: u128, prefix: u128) -> (u128, u128) { let with_prefix = u256 { low: input, high: prefix }; diff --git a/starknet/src/utils/legacy_hash.cairo b/starknet/src/utils/legacy_hash.cairo index 9e5dbd5f..6d077e8e 100644 --- a/starknet/src/utils/legacy_hash.cairo +++ b/starknet/src/utils/legacy_hash.cairo @@ -50,6 +50,14 @@ impl LegacyHashUserAddress of LegacyHash { } } +impl LegacyHashUserAddressU256 of LegacyHash<(UserAddress, u256)> { + fn hash(state: felt252, value: (UserAddress, u256)) -> felt252 { + let (addr, u256) = value; + let state = LegacyHash::hash(state, addr); + LegacyHash::hash(state, u256) + } +} + impl LegacyHashUsedSalts of LegacyHash<(EthAddress, u256)> { fn hash(state: felt252, value: (EthAddress, u256)) -> felt252 { let (addr, salt) = value; diff --git a/starknet/src/utils/session_key.cairo b/starknet/src/utils/session_key.cairo new file mode 100644 index 00000000..45479c6b --- /dev/null +++ b/starknet/src/utils/session_key.cairo @@ -0,0 +1,649 @@ +#[starknet::contract] +mod SessionKey { + // use core::debug::PrintTrait; + use starknet::{info, ContractAddress, EthAddress}; + use sx::types::{ + Strategy, IndexedStrategy, Choice, UserAddress, UserAddressTrait, UserAddressIntoFelt + }; + use sx::interfaces::{ISpaceDispatcher, ISpaceDispatcherTrait}; + use sx::utils::{ + EIP712, StarkEIP712, StructHash, LegacyHashEthAddress, LegacyHashUserAddressU256, + LegacyHashFelt252EthAddress, + }; + use sx::utils::constants::{ + STARKNET_MESSAGE, DOMAIN_TYPEHASH, PROPOSE_TYPEHASH, VOTE_TYPEHASH, + UPDATE_PROPOSAL_TYPEHASH, SESSION_KEY_REVOKE_TYPEHASH, ERC165_ACCOUNT_INTERFACE_ID, + REGISTER_SESSION_WITH_OWNER_TX_SELECTOR, REVOKE_SESSION_WITH_OWNER_TX_SELECTOR + }; + + #[derive(Clone, Drop, Option, PartialEq, Serde, starknet::Store)] + struct Session { + // We use a general address type so we can handle EVM, Starknet, and other address types. + owner: UserAddress, + end_timestamp: u32, + } + + #[storage] + struct Storage { + _domain_hash: felt252, + _used_salts: LegacyMap::<(UserAddress, u256), bool>, + _sessions: LegacyMap::, + _starknet_commit_address: EthAddress, + _commits: LegacyMap::<(felt252, EthAddress), bool> + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + SessionKeyRegistered: SessionKeyRegistered, + SessionKeyRevoked: SessionKeyRevoked + } + + #[derive(Drop, PartialEq, starknet::Event)] + struct SessionKeyRegistered { + session_public_key: felt252, + session: Session, + } + + #[derive(Drop, PartialEq, starknet::Event)] + struct SessionKeyRevoked { + session_public_key: felt252, + session: Session, + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn authenticate_propose( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: UserAddress, + metadata_uri: Array, + execution_strategy: Strategy, + user_proposal_validation_params: Array, + salt: felt252, + session_public_key: felt252 + ) { + self.assert_session_key_owner(session_public_key, author); + + assert(!self._used_salts.read((author, salt.into())), 'Salt Already Used'); + + self + .verify_propose_sig( + signature.span(), + space, + author, + metadata_uri.span(), + @execution_strategy, + user_proposal_validation_params.span(), + salt, + session_public_key + ); + + self._used_salts.write((author, salt.into()), true); + + ISpaceDispatcher { contract_address: space } + .propose( + author, metadata_uri, execution_strategy, user_proposal_validation_params, + ); + } + + fn authenticate_vote( + ref self: ContractState, + signature: Array, + space: ContractAddress, + voter: UserAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Array, + metadata_uri: Array, + session_public_key: felt252 + ) { + // No need to check salts here, as double voting is prevented by the space itself. + + self.assert_session_key_owner(session_public_key, voter); + + self + .verify_vote_sig( + signature.span(), + space, + voter, + proposal_id, + choice, + user_voting_strategies.span(), + metadata_uri.span(), + session_public_key + ); + + ISpaceDispatcher { contract_address: space } + .vote(voter, proposal_id, choice, user_voting_strategies, metadata_uri); + } + + fn authenticate_update_proposal( + ref self: ContractState, + signature: Array, + space: ContractAddress, + author: UserAddress, + proposal_id: u256, + execution_strategy: Strategy, + metadata_uri: Array, + salt: felt252, + session_public_key: felt252 + ) { + assert(!self._used_salts.read((author, salt.into())), 'Salt Already Used'); + + self.assert_session_key_owner(session_public_key, author); + + self + .verify_update_proposal_sig( + signature.span(), + space, + author, + proposal_id, + @execution_strategy, + metadata_uri.span(), + salt, + session_public_key + ); + + self._used_salts.write((author, salt.into()), true); + + ISpaceDispatcher { contract_address: space } + .update_proposal(author, proposal_id, execution_strategy, metadata_uri); + } + + fn register_with_owner_eth_sig( + ref self: ContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + salt: u256, + ) { + assert( + !self._used_salts.read((UserAddress::Ethereum(owner), salt)), 'Salt Already Used' + ); + + let state = EIP712::unsafe_new_contract_state(); + EIP712::InternalImpl::verify_session_key_auth_sig( + @state, r, s, v, owner, session_public_key, session_duration, salt + ); + + self._used_salts.write((UserAddress::Ethereum(owner), salt), true); + + self.register(UserAddress::Ethereum(owner), session_public_key, session_duration); + } + + fn revoke_with_owner_eth_sig( + ref self: ContractState, + r: u256, + s: u256, + v: u32, + owner: EthAddress, + session_public_key: felt252, + salt: u256, + ) { + self.assert_session_key_owner(session_public_key, UserAddress::Ethereum(owner)); + assert( + !self._used_salts.read((UserAddress::Ethereum(owner), salt)), 'Salt Already Used' + ); + + let state = EIP712::unsafe_new_contract_state(); + EIP712::InternalImpl::verify_session_key_revoke_sig( + @state, r, s, v, owner, session_public_key, salt + ); + + self._used_salts.write((UserAddress::Ethereum(owner), salt), true); + + self.revoke(session_public_key); + } + + fn register_with_owner_eth_tx( + ref self: ContractState, + owner: EthAddress, + session_public_key: felt252, + session_duration: u32, + ) { + let mut payload = array![]; + starknet::get_contract_address().serialize(ref payload); + REGISTER_SESSION_WITH_OWNER_TX_SELECTOR.serialize(ref payload); + owner.serialize(ref payload); + session_public_key.serialize(ref payload); + session_duration.serialize(ref payload); + let payload_hash = poseidon::poseidon_hash_span(payload.span()); + + self.consume_commit(payload_hash, owner); + + self.register(UserAddress::Ethereum(owner), session_public_key, session_duration); + } + + fn revoke_with_owner_eth_tx( + ref self: ContractState, owner: EthAddress, session_public_key: felt252 + ) { + let mut payload = array![]; + starknet::get_contract_address().serialize(ref payload); + REVOKE_SESSION_WITH_OWNER_TX_SELECTOR.serialize(ref payload); + owner.serialize(ref payload); + session_public_key.serialize(ref payload); + let payload_hash = poseidon::poseidon_hash_span(payload.span()); + + self.consume_commit(payload_hash, owner); + + self.revoke(session_public_key); + } + + fn register_with_owner_stark_sig( + ref self: ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32, + salt: felt252, + ) { + assert( + !self._used_salts.read((UserAddress::Starknet(owner), salt.into())), + 'Salt Already Used' + ); + + let state = StarkEIP712::unsafe_new_contract_state(); + StarkEIP712::InternalImpl::verify_session_key_auth_sig( + @state, signature, owner, session_public_key, session_duration, salt + ); + + self._used_salts.write((UserAddress::Starknet(owner), salt.into()), true); + + self.register(UserAddress::Starknet(owner), session_public_key, session_duration); + } + + fn revoke_with_owner_stark_sig( + ref self: ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252, + ) { + self.assert_session_key_owner(session_public_key, UserAddress::Starknet(owner)); + assert( + !self._used_salts.read((UserAddress::Starknet(owner), salt.into())), + 'Salt Already Used' + ); + + let state = StarkEIP712::unsafe_new_contract_state(); + StarkEIP712::InternalImpl::verify_session_key_revoke_sig( + @state, signature, owner, session_public_key, salt + ); + + self._used_salts.write((UserAddress::Starknet(owner), salt.into()), true); + + self.revoke(session_public_key); + } + + fn register_with_owner_stark_tx( + ref self: ContractState, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32, + ) { + assert(info::get_caller_address() == owner, 'Invalid Caller'); + + self.register(UserAddress::Starknet(owner), session_public_key, session_duration); + } + + fn revoke_with_owner_stark_tx( + ref self: ContractState, owner: ContractAddress, session_public_key: felt252 + ) { + assert(info::get_caller_address() == owner, 'Invalid Caller'); + + self.revoke(session_public_key); + } + + fn revoke_with_session_key_sig( + ref self: ContractState, + signature: Array, + owner: UserAddress, + session_public_key: felt252, + salt: felt252, + ) { + self.assert_session_key_owner(session_public_key, owner); + assert(!self._used_salts.read((owner, salt.into())), 'Salt Already Used'); + + self.verify_session_key_revoke_sig(signature.span(), owner, session_public_key, salt); + + self._used_salts.write((owner, salt.into()), true); + + self.revoke(session_public_key); + } + + + // Reverts if a session key is invalid or the owner is not the address specified. + fn assert_session_key_owner( + self: @ContractState, session_public_key: felt252, owner: UserAddress + ) { + let session = self._sessions.read(session_public_key); + self.assert_valid(@session); + // If the session key has been revoked, the owner will be the zero address. + assert(session.owner == owner, 'Invalid owner'); + } + + /// Returns the owner of the session key if it is valid. + fn get_owner_if_valid(self: @ContractState, session_public_key: felt252) -> UserAddress { + let session = self._sessions.read(session_public_key); + self.assert_valid(@session); + session.owner + } + + /// Reverts if the session is invalid. + /// This occurs if the session does not exist (end timestamp is 0) or has expired. + fn assert_valid(self: @ContractState, session: @Session) { + let current_timestamp: u32 = info::get_block_timestamp().try_into().unwrap(); + assert(current_timestamp < *session.end_timestamp, 'Session key expired'); + } + + fn register( + ref self: ContractState, + owner: UserAddress, + session_public_key: felt252, + session_duration: u32 + ) { + let current_timestamp = info::get_block_timestamp().try_into().unwrap(); + let end_timestamp = current_timestamp + session_duration; // Will revert on overflow + let session = Session { owner, end_timestamp }; + + self._sessions.write(session_public_key, session.clone()); + + self + .emit( + Event::SessionKeyRegistered( + SessionKeyRegistered { session_public_key, session } + ) + ); + } + + fn revoke(ref self: ContractState, session_public_key: felt252) { + let session = self._sessions.read(session_public_key); + self.assert_valid(@session); + + // Writing the session state to zero. + self + ._sessions + .write( + session_public_key, + Session { + owner: UserAddress::Starknet(starknet::contract_address_const::<0>()), + end_timestamp: 0 + } + ); + + self.emit(Event::SessionKeyRevoked(SessionKeyRevoked { session_public_key, session })); + } + + fn eth_sig_initializer(ref self: ContractState, name: felt252, version: felt252) { + self._domain_hash.write(InternalImpl::get_domain_hash(name, version)); + } + + fn eth_tx_initializer( + ref self: ContractState, + name: felt252, + version: felt252, + starknet_commit_address: EthAddress + ) { + self._domain_hash.write(InternalImpl::get_domain_hash(name, version)); + self._starknet_commit_address.write(starknet_commit_address); + } + + /// Verifies the signature of the propose calldata. + fn verify_propose_sig( + self: @ContractState, + signature: Span, + space: ContractAddress, + author: UserAddress, + metadata_uri: Span, + execution_strategy: @Strategy, + user_proposal_validation_params: Span, + salt: felt252, + session_public_key: felt252 + ) { + let digest: felt252 = self + .get_propose_digest( + space, + author, + metadata_uri, + execution_strategy, + user_proposal_validation_params, + salt + ); + + assert( + InternalImpl::is_valid_stark_signature(digest, session_public_key, signature), + 'Invalid Signature' + ); + } + + /// Verifies the signature of the vote calldata. + fn verify_vote_sig( + self: @ContractState, + signature: Span, + space: ContractAddress, + voter: UserAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Span, + metadata_uri: Span, + session_public_key: felt252 + ) { + let digest: felt252 = self + .get_vote_digest( + space, voter, proposal_id, choice, user_voting_strategies, metadata_uri + ); + assert( + InternalImpl::is_valid_stark_signature(digest, session_public_key, signature), + 'Invalid Signature' + ); + } + + /// Verifies the signature of the update proposal calldata. + fn verify_update_proposal_sig( + self: @ContractState, + signature: Span, + space: ContractAddress, + author: UserAddress, + proposal_id: u256, + execution_strategy: @Strategy, + metadata_uri: Span, + salt: felt252, + session_public_key: felt252 + ) { + let digest: felt252 = self + .get_update_proposal_digest( + space, author, proposal_id, execution_strategy, metadata_uri, salt + ); + assert( + InternalImpl::is_valid_stark_signature(digest, session_public_key, signature), + 'Invalid Signature' + ); + } + + /// Verifies the signature of a session key revokation. + fn verify_session_key_revoke_sig( + self: @ContractState, + signature: Span, + owner: UserAddress, + session_public_key: felt252, + salt: felt252, + ) { + let digest: felt252 = self + .get_session_key_revoke_digest(owner, session_public_key, salt); + assert( + InternalImpl::is_valid_stark_signature(digest, session_public_key, signature), + 'Invalid Signature' + ); + } + + /// Returns the digest of the propose calldata. + fn get_propose_digest( + self: @ContractState, + space: ContractAddress, + author: UserAddress, + metadata_uri: Span, + execution_strategy: @Strategy, + user_proposal_validation_params: Span, + salt: felt252, + ) -> felt252 { + let mut encoded_data = array![]; + PROPOSE_TYPEHASH.serialize(ref encoded_data); + space.serialize(ref encoded_data); + UserAddressIntoFelt::into(author).serialize(ref encoded_data); + metadata_uri.struct_hash().serialize(ref encoded_data); + execution_strategy.struct_hash().serialize(ref encoded_data); + user_proposal_validation_params.struct_hash().serialize(ref encoded_data); + salt.serialize(ref encoded_data); + self.hash_typed_data(encoded_data.span().struct_hash()) + } + + /// Returns the digest of the vote calldata. + fn get_vote_digest( + self: @ContractState, + space: ContractAddress, + voter: UserAddress, + proposal_id: u256, + choice: Choice, + user_voting_strategies: Span, + metadata_uri: Span, + ) -> felt252 { + let mut encoded_data = array![]; + VOTE_TYPEHASH.serialize(ref encoded_data); + space.serialize(ref encoded_data); + UserAddressIntoFelt::into(voter).serialize(ref encoded_data); + proposal_id.struct_hash().serialize(ref encoded_data); + choice.serialize(ref encoded_data); + user_voting_strategies.struct_hash().serialize(ref encoded_data); + metadata_uri.struct_hash().serialize(ref encoded_data); + self.hash_typed_data(encoded_data.span().struct_hash()) + } + + + /// Returns the digest of the update proposal calldata. + fn get_update_proposal_digest( + self: @ContractState, + space: ContractAddress, + author: UserAddress, + proposal_id: u256, + execution_strategy: @Strategy, + metadata_uri: Span, + salt: felt252 + ) -> felt252 { + let mut encoded_data = array![]; + UPDATE_PROPOSAL_TYPEHASH.serialize(ref encoded_data); + space.serialize(ref encoded_data); + UserAddressIntoFelt::into(author).serialize(ref encoded_data); + proposal_id.struct_hash().serialize(ref encoded_data); + execution_strategy.struct_hash().serialize(ref encoded_data); + metadata_uri.struct_hash().serialize(ref encoded_data); + salt.serialize(ref encoded_data); + self.hash_typed_data(encoded_data.span().struct_hash()) + } + + fn get_session_key_revoke_digest( + self: @ContractState, owner: UserAddress, session_public_key: felt252, salt: felt252 + ) -> felt252 { + let mut encoded_data = array![]; + SESSION_KEY_REVOKE_TYPEHASH.serialize(ref encoded_data); + UserAddressIntoFelt::into(owner).serialize(ref encoded_data); + session_public_key.serialize(ref encoded_data); + salt.serialize(ref encoded_data); + // encoded_data.clone().print(); + self.hash_typed_data(encoded_data.span().struct_hash()) + } + + /// Returns the domain hash of the contract. + fn get_domain_hash(name: felt252, version: felt252) -> felt252 { + let mut encoded_data = array![]; + DOMAIN_TYPEHASH.serialize(ref encoded_data); + name.serialize(ref encoded_data); + version.serialize(ref encoded_data); + starknet::get_tx_info().unbox().chain_id.serialize(ref encoded_data); + starknet::get_contract_address().serialize(ref encoded_data); + encoded_data.span().struct_hash() + } + + /// Hashes typed data according to the starknet equiavalent to the EIP-712 specification. + fn hash_typed_data(self: @ContractState, message_hash: felt252) -> felt252 { + let mut encoded_data = array![]; + STARKNET_MESSAGE.serialize(ref encoded_data); + self._domain_hash.read().serialize(ref encoded_data); + 0x1.serialize(ref encoded_data); + message_hash.serialize(ref encoded_data); + // encoded_data.clone().print(); + encoded_data.span().struct_hash() + } + + /// OpenZeppelin Implementation + /// NOTE: Did not import as our OZ dependency is not the latest version. + fn is_valid_stark_signature( + msg_hash: felt252, public_key: felt252, signature: Span + ) -> bool { + let valid_length = signature.len() == 2; + + if valid_length { + ecdsa::check_ecdsa_signature( + msg_hash, public_key, *signature.at(0_u32), *signature.at(1_u32) + ) + } else { + false + } + } + + fn consume_commit(ref self: ContractState, hash: felt252, sender_address: EthAddress) { + assert(self._commits.read((hash, sender_address)), 'Commit not found'); + // Delete the commit to prevent replay attacks. + self._commits.write((hash, sender_address), false); + } + + fn commit( + ref self: ContractState, from_address: felt252, sender_address: felt252, hash: felt252 + ) { + assert( + from_address == self._starknet_commit_address.read().into(), + 'Invalid commit address' + ); + let sender_address = sender_address.try_into().unwrap(); + assert(self._commits.read((hash, sender_address)) == false, 'Commit already exists'); + self._commits.write((hash, sender_address), true); + } + } +} + +#[cfg(test)] +mod tests { + use super::SessionKey; + use debug::PrintTrait; + + use starknet::{info, ContractAddress, EthAddress}; + use sx::types::{Strategy, IndexedStrategy, Choice, UserAddress, UserAddressTrait}; + use sx::interfaces::{ISpaceDispatcher, ISpaceDispatcherTrait}; + use sx::utils::{ + EIP712, StarkEIP712, StructHash, LegacyHashEthAddress, LegacyHashUserAddressU256, + LegacyHashFelt252EthAddress + }; + use sx::utils::constants::{ + STARKNET_MESSAGE, DOMAIN_TYPEHASH, PROPOSE_TYPEHASH, VOTE_TYPEHASH, + UPDATE_PROPOSAL_TYPEHASH, ERC165_ACCOUNT_INTERFACE_ID, + REGISTER_SESSION_WITH_OWNER_TX_SELECTOR, REVOKE_SESSION_WITH_OWNER_TX_SELECTOR + }; + + #[test] + #[available_gas(10000000)] + fn testSessionKey() { + let state = SessionKey::unsafe_new_contract_state(); + let mut payload = array![]; + starknet::get_contract_address().serialize(ref payload); + REGISTER_SESSION_WITH_OWNER_TX_SELECTOR.serialize(ref payload); + 0x1234.serialize(ref payload); + 0x5678.serialize(ref payload); + 0x9999.serialize(ref payload); + payload.print(); + } +} + diff --git a/starknet/src/utils/stark_eip712.cairo b/starknet/src/utils/stark_eip712.cairo index 6fc92a2b..94d5b7e9 100644 --- a/starknet/src/utils/stark_eip712.cairo +++ b/starknet/src/utils/stark_eip712.cairo @@ -2,13 +2,15 @@ /// See here for more info: https://community.starknet.io/t/snip-off-chain-signatures-a-la-eip712/98029 #[starknet::contract] mod StarkEIP712 { + use core::starknet::account::AccountContract; use starknet::ContractAddress; use openzeppelin::account::interface::{AccountABIDispatcher, AccountABIDispatcherTrait}; use sx::types::{Strategy, IndexedStrategy, Choice}; use sx::utils::StructHash; use sx::utils::constants::{ STARKNET_MESSAGE, DOMAIN_TYPEHASH, PROPOSE_TYPEHASH, VOTE_TYPEHASH, - UPDATE_PROPOSAL_TYPEHASH, ERC165_ACCOUNT_INTERFACE_ID + UPDATE_PROPOSAL_TYPEHASH, SESSION_KEY_AUTH_TYPEHASH, SESSION_KEY_REVOKE_TYPEHASH, + ERC165_ACCOUNT_INTERFACE_ID }; #[storage] @@ -82,6 +84,31 @@ mod StarkEIP712 { InternalImpl::verify_signature(digest, signature, author); } + fn verify_session_key_auth_sig( + self: @ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32, + salt: felt252 + ) { + let digest: felt252 = self + .get_session_key_auth_digest(owner, session_public_key, session_duration, salt); + InternalImpl::verify_signature(digest, signature, owner); + } + + fn verify_session_key_revoke_sig( + self: @ContractState, + signature: Array, + owner: ContractAddress, + session_public_key: felt252, + salt: felt252 + ) { + let digest: felt252 = self + .get_session_key_revoke_digest(owner, session_public_key, salt); + InternalImpl::verify_signature(digest, signature, owner); + } + /// Returns the digest of the propose calldata. fn get_propose_digest( self: @ContractState, @@ -146,6 +173,32 @@ mod StarkEIP712 { self.hash_typed_data(encoded_data.span().struct_hash(), author) } + fn get_session_key_auth_digest( + self: @ContractState, + owner: ContractAddress, + session_public_key: felt252, + session_duration: u32, + salt: felt252 + ) -> felt252 { + let mut encoded_data = array![]; + SESSION_KEY_AUTH_TYPEHASH.serialize(ref encoded_data); + owner.serialize(ref encoded_data); + session_public_key.serialize(ref encoded_data); + session_duration.serialize(ref encoded_data); + salt.serialize(ref encoded_data); + self.hash_typed_data(encoded_data.span().struct_hash(), owner) + } + + fn get_session_key_revoke_digest( + self: @ContractState, owner: ContractAddress, session_public_key: felt252, salt: felt252 + ) -> felt252 { + let mut encoded_data = array![]; + SESSION_KEY_REVOKE_TYPEHASH.serialize(ref encoded_data); + owner.serialize(ref encoded_data); + session_public_key.serialize(ref encoded_data); + salt.serialize(ref encoded_data); + self.hash_typed_data(encoded_data.span().struct_hash(), owner) + } /// Returns the domain hash of the contract. fn get_domain_hash(name: felt252, version: felt252) -> felt252 { diff --git a/tests/eth-sig-sk-auth.test.ts b/tests/eth-sig-sk-auth.test.ts new file mode 100644 index 00000000..da46af5e --- /dev/null +++ b/tests/eth-sig-sk-auth.test.ts @@ -0,0 +1,767 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import { starknet, ethers } from 'hardhat'; +import { CallData, typedData, cairo, shortString, Account, Provider } from 'starknet'; +import { + sessionKeyAuthTypes, + SessionKeyAuth, + sessionKeyRevokeTypes, + SessionKeyRevoke, +} from './eth-sig-types'; +import { + proposeTypes, + Propose, + updateProposalTypes, + UpdateProposal, + voteTypes, + Vote, + sessionKeyRevokeTypes as sessionKeyRevokeTypesStark, + SessionKeyRevoke as SessionKeyRevokeStark, +} from './stark-sig-types'; +import { getRSVFromSig } from './utils'; + +dotenv.config(); + +const account_address = process.env.ADDRESS || ''; +const account_pk = process.env.PK || ''; +const account_public_key = process.env.PUBLIC_KEY || ''; +const network = process.env.STARKNET_NETWORK_URL || ''; + +describe('Ethereum Signature Session Key Authenticator', function () { + this.timeout(1000000); + + // Ethereum EIP712 Domain is empty. + const ethDomain = {}; + + let ethSigner: ethers.HDNodeWallet; + // Account used to submit transactions + let manaAccount: starknet.starknetAccount; + // SNIP-6 compliant account (the defaults deployed on the devnet are not SNIP-6 compliant and therefore cannot be used for signatures) + let sessionAccountWithSigner: Account; + let ethSigSessionKeyAuthenticator: starknet.StarknetContract; + let vanillaVotingStrategy: starknet.StarknetContract; + let vanillaProposalValidationStrategy: starknet.StarknetContract; + let space: starknet.StarknetContract; + + let starkDomain: any; + + before(async function () { + ethSigner = ethers.Wallet.createRandom(); + manaAccount = await starknet.OpenZeppelinAccount.getAccountFromAddress( + account_address, + account_pk, + ); + + const accountFactory = await starknet.getContractFactory('openzeppelin_Account'); + const ethSigSessionKeyAuthenticatorFactory = await starknet.getContractFactory( + 'sx_EthSigSessionKeyAuthenticator', + ); + const vanillaVotingStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaVotingStrategy', + ); + const vanillaProposalValidationStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaProposalValidationStrategy', + ); + const spaceFactory = await starknet.getContractFactory('sx_Space'); + + try { + // If the contracts are already declared, this will be skipped + await manaAccount.declare(accountFactory); + await manaAccount.declare(ethSigSessionKeyAuthenticatorFactory); + await manaAccount.declare(vanillaVotingStrategyFactory); + await manaAccount.declare(vanillaProposalValidationStrategyFactory); + await manaAccount.declare(spaceFactory); + } catch {} + + ethSigSessionKeyAuthenticator = await manaAccount.deploy(ethSigSessionKeyAuthenticatorFactory, { + name: shortString.encodeShortString('sx-sn'), + version: shortString.encodeShortString('0.1.0'), + }); + + starkDomain = { + name: 'sx-sn', + version: '0.1.0', + chainId: '0x534e5f474f45524c49', // devnet id + verifyingContract: ethSigSessionKeyAuthenticator.address, + }; + + const accountObj = await manaAccount.deploy(accountFactory, { + _public_key: account_public_key, + }); + + sessionAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: network } }), + '0x1', + account_pk, + ); + + vanillaVotingStrategy = await manaAccount.deploy(vanillaVotingStrategyFactory); + vanillaProposalValidationStrategy = await manaAccount.deploy( + vanillaProposalValidationStrategyFactory, + ); + space = await manaAccount.deploy(spaceFactory); + + // Initializing the space + const initializeCalldata = CallData.compile({ + _owner: 1, + _max_voting_duration: 200, + _min_voting_duration: 200, + _voting_delay: 100, + _proposal_validation_strategy: { + address: vanillaProposalValidationStrategy.address, + params: [], + }, + _proposal_validation_strategy_metadata_uri: [], + _voting_strategies: [{ address: vanillaVotingStrategy.address, params: [] }], + _voting_strategies_metadata_uri: [[]], + _authenticators: [ethSigSessionKeyAuthenticator.address], + _metadata_uri: [], + _dao_uri: [], + }); + + await manaAccount.invoke(space, 'initialize', initializeCalldata, { rawInput: true }); + + // Dumping the Starknet state so it can be loaded at the same point for each test + await starknet.devnet.dump('dump.pkl'); + }, 10000000); + + it('can register a session then authenticate a proposal, a vote, and a proposal update with that session', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + chainId: '0x534e5f474f45524c49', + authenticator: ethSigSessionKeyAuthenticator.address, + owner: ethSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + let sig = await ethSigner._signTypedData(ethDomain, sessionKeyAuthTypes, sessionKeyAuthMsg); + let splitSig = getRSVFromSig(sig); + const sessionKeyAuthCalldata = CallData.compile({ + r: cairo.uint256(splitSig.r), + s: cairo.uint256(splitSig.s), + v: splitSig.v, + owner: sessionKeyAuthMsg.owner, + sessionPublicKey: sessionKeyAuthMsg.sessionPublicKey, + sessionDuration: sessionKeyAuthMsg.sessionDuration, + salt: cairo.uint256(sessionKeyAuthMsg.salt), + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + // Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // UPDATE PROPOSAL + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + const updateProposalSig = (await sessionAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + const updateProposalCalldata = CallData.compile({ + signature: [updateProposalSig.r, updateProposalSig.s], + ...updateProposalMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_update_proposal', + updateProposalCalldata, + { + rawInput: true, + }, + ); + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + // VOTE + const voteMsg: Vote = { + space: space.address, + voter: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + const voteSig = (await sessionAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + const voteCalldata = CallData.compile({ + signature: [voteSig.r, voteSig.s], + ...voteMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke(ethSigSessionKeyAuthenticator, 'authenticate_vote', voteCalldata, { + rawInput: true, + }); + }, 1000000); + + it('will revert if incorrect signatures are used', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + chainId: '0x534e5f474f45524c49', + authenticator: ethSigSessionKeyAuthenticator.address, + owner: ethSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + let sig = await ethSigner._signTypedData(ethDomain, sessionKeyAuthTypes, sessionKeyAuthMsg); + let splitSig = getRSVFromSig(sig); + const sessionKeyAuthCalldata = CallData.compile({ + r: cairo.uint256(splitSig.r), + s: cairo.uint256(splitSig.s), + v: splitSig.v, + owner: sessionKeyAuthMsg.owner, + sessionPublicKey: sessionKeyAuthMsg.sessionPublicKey, + sessionDuration: sessionKeyAuthMsg.sessionDuration, + salt: cairo.uint256(sessionKeyAuthMsg.salt), + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Account #1 on Starknet devnet with seed 42 + const invalidAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: network } }), + '0x7aac39162d91acf2c4f0d539f4b81e23832619ac0c3df9fce22e4a8d505632a', + '0x23b8c1e9392456de3eb13b9046685257', + ); + + // Attempt to propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + + const invalidProposeSig = (await invalidAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const invalidProposeCalldata = CallData.compile({ + signature: [invalidProposeSig.r, invalidProposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + + try { + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_propose', + invalidProposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Actually creating a proposal + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // Attempt to update proposal + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + + const invalidUpdateProposalSig = (await invalidAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + + const invalidUpdateProposalCalldata = CallData.compile({ + signature: [invalidUpdateProposalSig.r, invalidUpdateProposalSig.s], + ...updateProposalMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + + try { + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_update_proposal', + invalidUpdateProposalCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + + // Attempt to Vote + const voteMsg: Vote = { + space: space.address, + voter: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + + const invalidVoteSig = (await invalidAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + + const invalidVoteCalldata = CallData.compile({ + signature: [invalidVoteSig.r, invalidVoteSig.s], + ...voteMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + + try { + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_vote', + invalidVoteCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Attempt to revoke session with owner sig + const revokeSessionMsg: SessionKeyRevoke = { + chainId: '0x534e5f474f45524c49', + authenticator: ethSigSessionKeyAuthenticator.address, + owner: ethSigner.address, + sessionPublicKey: account_public_key, + salt: '0x3', + }; + + let invalidEthSigner = ethers.Wallet.createRandom(); + let invalidSig = await invalidEthSigner._signTypedData( + ethDomain, + sessionKeyRevokeTypes, + revokeSessionMsg, + ); + let invalidSplitSig = getRSVFromSig(invalidSig); + const invalidRevokeSessionCalldata = CallData.compile({ + r: cairo.uint256(invalidSplitSig.r), + s: cairo.uint256(invalidSplitSig.s), + v: invalidSplitSig.v, + owner: revokeSessionMsg.owner, + sessionPublicKey: revokeSessionMsg.sessionPublicKey, + salt: cairo.uint256(revokeSessionMsg.salt), + }); + + try { + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'revoke_with_owner_sig', + invalidRevokeSessionCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid signature')); + } + }, 1000000); + + it('can revoke a session with an owner sig', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + chainId: '0x534e5f474f45524c49', + authenticator: ethSigSessionKeyAuthenticator.address, + owner: ethSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + let sig = await ethSigner._signTypedData(ethDomain, sessionKeyAuthTypes, sessionKeyAuthMsg); + let splitSig = getRSVFromSig(sig); + const sessionKeyAuthCalldata = CallData.compile({ + r: cairo.uint256(splitSig.r), + s: cairo.uint256(splitSig.s), + v: splitSig.v, + owner: sessionKeyAuthMsg.owner, + sessionPublicKey: sessionKeyAuthMsg.sessionPublicKey, + sessionDuration: sessionKeyAuthMsg.sessionDuration, + salt: cairo.uint256(sessionKeyAuthMsg.salt), + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + // Revoke Session with owner sig + const revokeSessionMsg: SessionKeyRevoke = { + chainId: '0x534e5f474f45524c49', + authenticator: ethSigSessionKeyAuthenticator.address, + owner: ethSigner.address, + sessionPublicKey: account_public_key, + salt: '0x1', + }; + sig = await ethSigner._signTypedData(ethDomain, sessionKeyRevokeTypes, revokeSessionMsg); + splitSig = getRSVFromSig(sig); + const revokeSessionCalldata = CallData.compile({ + r: cairo.uint256(splitSig.r), + s: cairo.uint256(splitSig.s), + v: splitSig.v, + owner: revokeSessionMsg.owner, + sessionPublicKey: revokeSessionMsg.sessionPublicKey, + salt: cairo.uint256(revokeSessionMsg.salt), + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'revoke_with_owner_sig', + revokeSessionCalldata, + { + rawInput: true, + }, + ); + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); + + it('can revoke a session with an session key sig', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + chainId: '0x534e5f474f45524c49', + authenticator: ethSigSessionKeyAuthenticator.address, + owner: ethSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + let sig = await ethSigner._signTypedData(ethDomain, sessionKeyAuthTypes, sessionKeyAuthMsg); + let splitSig = getRSVFromSig(sig); + const sessionKeyAuthCalldata = CallData.compile({ + r: cairo.uint256(splitSig.r), + s: cairo.uint256(splitSig.s), + v: splitSig.v, + owner: sessionKeyAuthMsg.owner, + sessionPublicKey: sessionKeyAuthMsg.sessionPublicKey, + sessionDuration: sessionKeyAuthMsg.sessionDuration, + salt: cairo.uint256(sessionKeyAuthMsg.salt), + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + // Revoke Session with session key sig + const revokeSessionMsg: SessionKeyRevokeStark = { + owner: ethSigner.address, + sessionPublicKey: sessionKeyAuthMsg.sessionPublicKey, + salt: '0x1', + }; + const revokeSessionKeySig = (await sessionAccountWithSigner.signMessage({ + types: sessionKeyRevokeTypesStark, + primaryType: 'SessionKeyRevoke', + domain: starkDomain, + message: revokeSessionMsg as any, + } as typedData.TypedData)) as any; + const revokeSessionCalldata = CallData.compile({ + signature: [revokeSessionKeySig.r, revokeSessionKeySig.s], + owner: revokeSessionMsg.owner, + sessionPublicKey: revokeSessionMsg.sessionPublicKey, + salt: revokeSessionMsg.salt, + }); + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'revoke_with_session_key_sig', + revokeSessionCalldata, + { + rawInput: true, + }, + ); + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); + + it('The session cannot be used if it has expired', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + chainId: '0x534e5f474f45524c49', + authenticator: ethSigSessionKeyAuthenticator.address, + owner: ethSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x1', + salt: '0x0', + }; + + let sig = await ethSigner._signTypedData(ethDomain, sessionKeyAuthTypes, sessionKeyAuthMsg); + let splitSig = getRSVFromSig(sig); + + const sessionKeyAuthCalldata = CallData.compile({ + r: cairo.uint256(splitSig.r), + s: cairo.uint256(splitSig.s), + v: splitSig.v, + owner: sessionKeyAuthMsg.owner, + sessionPublicKey: sessionKeyAuthMsg.sessionPublicKey, + sessionDuration: sessionKeyAuthMsg.sessionDuration, + salt: cairo.uint256(sessionKeyAuthMsg.salt), + }); + + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Increase time to expire the session + await starknet.devnet.increaseTime(100); + + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + ethSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); +}); diff --git a/tests/eth-sig-types.ts b/tests/eth-sig-types.ts index fca27201..6335569d 100644 --- a/tests/eth-sig-types.ts +++ b/tests/eth-sig-types.ts @@ -51,6 +51,27 @@ export const updateProposalTypes = { Strategy: sharedTypes.Strategy, }; +export const sessionKeyAuthTypes = { + SessionKeyAuth: [ + { name: 'chainId', type: 'uint256' }, + { name: 'authenticator', type: 'uint256' }, + { name: 'owner', type: 'address' }, + { name: 'sessionPublicKey', type: 'uint256' }, + { name: 'sessionDuration', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + ], +}; + +export const sessionKeyRevokeTypes = { + SessionKeyRevoke: [ + { name: 'chainId', type: 'uint256' }, + { name: 'authenticator', type: 'uint256' }, + { name: 'owner', type: 'address' }, + { name: 'sessionPublicKey', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + ], +}; + export interface Propose { chainId: string; authenticator: string; @@ -84,6 +105,23 @@ export interface UpdateProposal { salt: string; } +export interface SessionKeyAuth { + chainId: string; + authenticator: string; + owner: string; + sessionPublicKey: string; + sessionDuration: string; + salt: string; +} + +export interface SessionKeyRevoke { + chainId: string; + authenticator: string; + owner: string; + sessionPublicKey: string; + salt: string; +} + export interface Strategy { address: string; params: string[]; diff --git a/tests/eth-tx-sk-auth.test.ts b/tests/eth-tx-sk-auth.test.ts new file mode 100644 index 00000000..9d145094 --- /dev/null +++ b/tests/eth-tx-sk-auth.test.ts @@ -0,0 +1,691 @@ +import { expect } from 'chai'; +import dotenv from 'dotenv'; +import { poseidonHashMany } from 'micro-starknet'; +import { starknet, ethers, network } from 'hardhat'; +import { HttpNetworkConfig } from 'hardhat/types'; +import { CallData, cairo, shortString, selector, Account, typedData, Provider } from 'starknet'; +import { + Propose, + proposeTypes, + UpdateProposal, + updateProposalTypes, + Vote, + voteTypes, +} from './stark-sig-types'; + +dotenv.config(); + +const eth_network: string = (network.config as HttpNetworkConfig).url; +const stark_network = process.env.STARKNET_NETWORK_URL || ''; +const account_address = process.env.ADDRESS || ''; +const account_pk = process.env.PK || ''; +const account_public_key = process.env.PUBLIC_KEY || ''; + +describe('Ethereum Transaction Authenticator', function () { + this.timeout(1000000); + + let ethSigner: ethers.Wallet; + let invalidSigner: ethers.Wallet; + let mockStarknetMessaging: ethers.Contract; + let starknetCommit: ethers.Contract; + // Account used to submit transactions + let manaAccount: starknet.starknetAccount; + // SNIP-6 compliant account (the defaults deployed on the devnet are not SNIP-6 compliant and therefore cannot be used for signatures) + let sessionAccountWithSigner: Account; + let ethTxSessionKeyAuthenticator: starknet.StarknetContract; + let vanillaVotingStrategy: starknet.StarknetContract; + let vanillaProposalValidationStrategy: starknet.StarknetContract; + let space: starknet.StarknetContract; + + let starkDomain: any; + + before(async function () { + const commit = `0x${poseidonHashMany([0x1].map((v) => BigInt(v))).toString(16)}`; + + const signers = await ethers.getSigners(); + ethSigner = signers[0]; + invalidSigner = signers[1]; + + manaAccount = await starknet.OpenZeppelinAccount.getAccountFromAddress( + account_address, + account_pk, + ); + + sessionAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: stark_network } }), + '0x1', + account_pk, + ); + + // Deploy Mock Starknet Core contract to L1 + const MockStarknetMessaging = await ethers.getContractFactory( + 'MockStarknetMessaging', + ethSigner, + ); + const messageCancellationDelay = 5 * 60; // seconds + mockStarknetMessaging = await MockStarknetMessaging.deploy(messageCancellationDelay); + + // Deploy Starknet Commit contract to L1 + const starknetCommitFactory = await ethers.getContractFactory('StarknetCommitMockMessaging'); + starknetCommit = await starknetCommitFactory.deploy(mockStarknetMessaging.address); + + const ethTxSessionKeyAuthenticatorFactory = await starknet.getContractFactory( + 'sx_EthTxSessionKeyAuthenticator', + ); + const vanillaVotingStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaVotingStrategy', + ); + const vanillaProposalValidationStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaProposalValidationStrategy', + ); + const spaceFactory = await starknet.getContractFactory('sx_Space'); + + try { + // If the contracts are already declared, this will be skipped + await manaAccount.declare(ethTxSessionKeyAuthenticatorFactory); + await manaAccount.declare(vanillaVotingStrategyFactory); + await manaAccount.declare(vanillaProposalValidationStrategyFactory); + await manaAccount.declare(spaceFactory); + } catch {} + + ethTxSessionKeyAuthenticator = await manaAccount.deploy(ethTxSessionKeyAuthenticatorFactory, { + name: shortString.encodeShortString('sx-sn'), + version: shortString.encodeShortString('0.1.0'), + starknet_commit_address: starknetCommit.address, + }); + + starkDomain = { + name: 'sx-sn', + version: '0.1.0', + chainId: '0x534e5f474f45524c49', // devnet id + verifyingContract: ethTxSessionKeyAuthenticator.address, + }; + + vanillaVotingStrategy = await manaAccount.deploy(vanillaVotingStrategyFactory); + vanillaProposalValidationStrategy = await manaAccount.deploy( + vanillaProposalValidationStrategyFactory, + ); + space = await manaAccount.deploy(spaceFactory); + + // Initializing the space + const initializeCalldata = CallData.compile({ + _owner: 1, + _max_voting_duration: 200, + _min_voting_duration: 200, + _voting_delay: 100, + _proposal_validation_strategy: { + address: vanillaProposalValidationStrategy.address, + params: [], + }, + _proposal_validation_strategy_metadata_uri: [], + _voting_strategies: [{ address: vanillaVotingStrategy.address, params: [] }], + _voting_strategies_metadata_uri: [[]], + _authenticators: [ethTxSessionKeyAuthenticator.address], + _metadata_uri: [], + _dao_uri: [], + }); + + await manaAccount.invoke(space, 'initialize', initializeCalldata, { rawInput: true }); + + // Dumping the Starknet state so it can be loaded at the same point for each test + await starknet.devnet.dump('dump.pkl'); + }, 10000000); + + it('can register a session then authenticate a proposal, a vote, and a proposal update with that session', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + const registerSessionCommitPreImage = CallData.compile({ + authenticator: ethTxSessionKeyAuthenticator.address, + selector: shortString.encodeShortString('register_session'), + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }); + + // Commit hash of payload to the Starknet Commit L1 contract + const commit = `0x${poseidonHashMany( + registerSessionCommitPreImage.map((v) => BigInt(v)), + ).toString(16)}`; + + await starknetCommit.commit(ethTxSessionKeyAuthenticator.address, commit, { + value: 18485000000000, + }); + + // Checking that the L1 -> L2 message has been propagated + expect((await starknet.devnet.flush()).consumed_messages.from_l1).to.have.a.lengthOf(1); + + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'register_with_owner_tx', + CallData.compile({ + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }), + { rawInput: true }, + ); + + // Propose + const proposeMsg: Propose = { + space: space.address, + author: ethSigner.address, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // UPDATE PROPOSAL + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: ethSigner.address, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + const updateProposalSig = (await sessionAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + const updateProposalCalldata = CallData.compile({ + signature: [updateProposalSig.r, updateProposalSig.s], + ...updateProposalMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'authenticate_update_proposal', + updateProposalCalldata, + { + rawInput: true, + }, + ); + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + // VOTE + const voteMsg: Vote = { + space: space.address, + voter: ethSigner.address, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + const voteSig = (await sessionAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + const voteCalldata = CallData.compile({ + signature: [voteSig.r, voteSig.s], + ...voteMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke(ethTxSessionKeyAuthenticator, 'authenticate_vote', voteCalldata, { + rawInput: true, + }); + }, 1000000); + + it('should revert if an invalid hash of an action was committed', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + const registerSessionCommitPreImage = CallData.compile({ + authenticator: ethTxSessionKeyAuthenticator.address, + selector: shortString.encodeShortString('register_session'), + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }); + + // Commit hash of payload to the Starknet Commit L1 contract + const commit = `0x${poseidonHashMany( + registerSessionCommitPreImage.map((v) => BigInt(v)), + ).toString(16)}`; + + await starknetCommit.commit(ethTxSessionKeyAuthenticator.address, commit, { + value: 18485000000000, + }); + + // Checking that the L1 -> L2 message has been propagated + expect((await starknet.devnet.flush()).consumed_messages.from_l1).to.have.a.lengthOf(1); + + // Invalid owner + try { + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'register_with_owner_tx', + CallData.compile({ + owner: invalidSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }), + { rawInput: true }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Commit not found')); + } + }, 1000000); + + it('can revoke a session with an owner tx', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + // Register a session + const registerSessionCommitPreImage = CallData.compile({ + authenticator: ethTxSessionKeyAuthenticator.address, + selector: shortString.encodeShortString('register_session'), + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }); + + // Commit hash of payload to the Starknet Commit L1 contract + const commit = `0x${poseidonHashMany( + registerSessionCommitPreImage.map((v) => BigInt(v)), + ).toString(16)}`; + + await starknetCommit.commit(ethTxSessionKeyAuthenticator.address, commit, { + value: 18485000000000, + }); + + // Checking that the L1 -> L2 message has been propagated + expect((await starknet.devnet.flush()).consumed_messages.from_l1).to.have.a.lengthOf(1); + + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'register_with_owner_tx', + CallData.compile({ + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }), + { rawInput: true }, + ); + + const revokeSessionCommitPreImage = CallData.compile({ + authenticator: ethTxSessionKeyAuthenticator.address, + selector: shortString.encodeShortString('revoke_session'), + owner: ethSigner.address, + sessionPublicKey: account_public_key, + }); + + // Commit hash of payload to the Starknet Commit L1 contract + const revokeCommit = `0x${poseidonHashMany( + revokeSessionCommitPreImage.map((v) => BigInt(v)), + ).toString(16)}`; + + await starknetCommit.commit(ethTxSessionKeyAuthenticator.address, revokeCommit, { + value: 18485000000000, + }); + + // Checking that the L1 -> L2 message has been propagated + expect((await starknet.devnet.flush()).consumed_messages.from_l1).to.have.a.lengthOf(1); + + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'revoke_with_owner_tx', + CallData.compile({ + owner: ethSigner.address, + sessionPublicKey: account_public_key, + }), + { rawInput: true }, + ); + }, 1000000); + + it('can revoke a session with an session key sig', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + // Register a session + const registerSessionCommitPreImage = CallData.compile({ + authenticator: ethTxSessionKeyAuthenticator.address, + selector: shortString.encodeShortString('register_session'), + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }); + + // Commit hash of payload to the Starknet Commit L1 contract + const commit = `0x${poseidonHashMany( + registerSessionCommitPreImage.map((v) => BigInt(v)), + ).toString(16)}`; + + await starknetCommit.commit(ethTxSessionKeyAuthenticator.address, commit, { + value: 18485000000000, + }); + + // Checking that the L1 -> L2 message has been propagated + expect((await starknet.devnet.flush()).consumed_messages.from_l1).to.have.a.lengthOf(1); + + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'register_with_owner_tx', + CallData.compile({ + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }), + { rawInput: true }, + ); + }, 1000000); + + it('will revert if incorrect signatures are used', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + // Register a session + const registerSessionCommitPreImage = CallData.compile({ + authenticator: ethTxSessionKeyAuthenticator.address, + selector: shortString.encodeShortString('register_session'), + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }); + + // Commit hash of payload to the Starknet Commit L1 contract + const commit = `0x${poseidonHashMany( + registerSessionCommitPreImage.map((v) => BigInt(v)), + ).toString(16)}`; + + await starknetCommit.commit(ethTxSessionKeyAuthenticator.address, commit, { + value: 18485000000000, + }); + + // Checking that the L1 -> L2 message has been propagated + expect((await starknet.devnet.flush()).consumed_messages.from_l1).to.have.a.lengthOf(1); + + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'register_with_owner_tx', + CallData.compile({ + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x123', + }), + { rawInput: true }, + ); + + // Account #1 on Starknet devnet with seed 42 + const invalidAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: stark_network } }), + '0x7aac39162d91acf2c4f0d539f4b81e23832619ac0c3df9fce22e4a8d505632a', + '0x23b8c1e9392456de3eb13b9046685257', + ); + + // Attempt to propose + const proposeMsg: Propose = { + space: space.address, + author: ethSigner.address, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + + const invalidProposeSig = (await invalidAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const invalidProposeCalldata = CallData.compile({ + signature: [invalidProposeSig.r, invalidProposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + + try { + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'authenticate_propose', + invalidProposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Actually creating a proposal + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // Attempt to update proposal + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: ethSigner.address, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + + const invalidUpdateProposalSig = (await invalidAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + + const invalidUpdateProposalCalldata = CallData.compile({ + signature: [invalidUpdateProposalSig.r, invalidUpdateProposalSig.s], + ...updateProposalMsg, + session_public_key: account_public_key, + }); + + try { + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'authenticate_update_proposal', + invalidUpdateProposalCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + + // Attempt to Vote + const voteMsg: Vote = { + space: space.address, + voter: ethSigner.address, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + + const invalidVoteSig = (await invalidAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + + const invalidVoteCalldata = CallData.compile({ + signature: [invalidVoteSig.r, invalidVoteSig.s], + ...voteMsg, + session_public_key: account_public_key, + }); + + try { + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'authenticate_vote', + invalidVoteCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + }, 1000000); + + it('The session cannot be used if it has expired', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + const registerSessionCommitPreImage = CallData.compile({ + authenticator: ethTxSessionKeyAuthenticator.address, + selector: shortString.encodeShortString('register_session'), + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x1', + }); + + // Commit hash of payload to the Starknet Commit L1 contract + const commit = `0x${poseidonHashMany( + registerSessionCommitPreImage.map((v) => BigInt(v)), + ).toString(16)}`; + + await starknetCommit.commit(ethTxSessionKeyAuthenticator.address, commit, { + value: 18485000000000, + }); + + // Checking that the L1 -> L2 message has been propagated + expect((await starknet.devnet.flush()).consumed_messages.from_l1).to.have.a.lengthOf(1); + + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'register_with_owner_tx', + CallData.compile({ + owner: ethSigner.address, + sessionPublicKey: account_public_key, + session_duration: '0x1', + }), + { rawInput: true }, + ); + + // Increase time to expire the session + await starknet.devnet.increaseTime(100); + + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: ethSigner.address, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + ethTxSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); +}); diff --git a/tests/stark-sig-sk-auth.test.ts b/tests/stark-sig-sk-auth.test.ts new file mode 100644 index 00000000..9e0cb705 --- /dev/null +++ b/tests/stark-sig-sk-auth.test.ts @@ -0,0 +1,733 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import { starknet, ethers } from 'hardhat'; +import { CallData, typedData, cairo, shortString, Account, Provider } from 'starknet'; +import { + sessionKeyAuthTypes, + SessionKeyAuth, + sessionKeyRevokeTypes, + SessionKeyRevoke, +} from './stark-sig-types'; +import { + proposeTypes, + Propose, + updateProposalTypes, + UpdateProposal, + voteTypes, + Vote, + sessionKeyRevokeTypes as sessionKeyRevokeTypesStark, + SessionKeyRevoke as SessionKeyRevokeStark, +} from './stark-sig-types'; +import { getRSVFromSig } from './utils'; + +dotenv.config(); + +const account_address = process.env.ADDRESS || ''; +const account_pk = process.env.PK || ''; +const account_public_key = process.env.PUBLIC_KEY || ''; +const network = process.env.STARKNET_NETWORK_URL || ''; + +describe('Ethereum Signature Session Key Authenticator', function () { + this.timeout(1000000); + + // Account used to submit transactions + let manaAccount: starknet.starknetAccount; + // SNIP-6 compliant account (the defaults deployed on the devnet are not SNIP-6 compliant and therefore cannot be used for signatures) + let sessionOwnerAccountWithSigner: Account; + let sessionAccountWithSigner: Account; + let starkSigSessionKeyAuthenticator: starknet.StarknetContract; + let vanillaVotingStrategy: starknet.StarknetContract; + let vanillaProposalValidationStrategy: starknet.StarknetContract; + let space: starknet.StarknetContract; + + let starkDomain: any; + + before(async function () { + manaAccount = await starknet.OpenZeppelinAccount.getAccountFromAddress( + account_address, + account_pk, + ); + + const accountFactory = await starknet.getContractFactory('openzeppelin_Account'); + const starkSigSessionKeyAuthenticatorFactory = await starknet.getContractFactory( + 'sx_StarkSigSessionKeyAuthenticator', + ); + const vanillaVotingStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaVotingStrategy', + ); + const vanillaProposalValidationStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaProposalValidationStrategy', + ); + const spaceFactory = await starknet.getContractFactory('sx_Space'); + + try { + // If the contracts are already declared, this will be skipped + await manaAccount.declare(accountFactory); + await manaAccount.declare(starkSigSessionKeyAuthenticatorFactory); + await manaAccount.declare(vanillaVotingStrategyFactory); + await manaAccount.declare(vanillaProposalValidationStrategyFactory); + await manaAccount.declare(spaceFactory); + } catch {} + + starkSigSessionKeyAuthenticator = await manaAccount.deploy( + starkSigSessionKeyAuthenticatorFactory, + { + name: shortString.encodeShortString('sx-sn'), + version: shortString.encodeShortString('0.1.0'), + }, + ); + + starkDomain = { + name: 'sx-sn', + version: '0.1.0', + chainId: '0x534e5f474f45524c49', // devnet id + verifyingContract: starkSigSessionKeyAuthenticator.address, + }; + + // Dummy account wrapper to sign messages + sessionAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: network } }), + '0x1', + account_pk, + ); + + const accountObj = await manaAccount.deploy(accountFactory, { + _public_key: account_public_key, + }); + + sessionOwnerAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: network } }), + accountObj.address, + account_pk, + ); + + vanillaVotingStrategy = await manaAccount.deploy(vanillaVotingStrategyFactory); + vanillaProposalValidationStrategy = await manaAccount.deploy( + vanillaProposalValidationStrategyFactory, + ); + space = await manaAccount.deploy(spaceFactory); + + // Initializing the space + const initializeCalldata = CallData.compile({ + _owner: 1, + _max_voting_duration: 200, + _min_voting_duration: 200, + _voting_delay: 100, + _proposal_validation_strategy: { + address: vanillaProposalValidationStrategy.address, + params: [], + }, + _proposal_validation_strategy_metadata_uri: [], + _voting_strategies: [{ address: vanillaVotingStrategy.address, params: [] }], + _voting_strategies_metadata_uri: [[]], + _authenticators: [starkSigSessionKeyAuthenticator.address], + _metadata_uri: [], + _dao_uri: [], + }); + + await manaAccount.invoke(space, 'initialize', initializeCalldata, { rawInput: true }); + + // Dumping the Starknet state so it can be loaded at the same point for each test + await starknet.devnet.dump('dump.pkl'); + }, 10000000); + + it('can register a session then authenticate a proposal, a vote, and a proposal update with that session', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + const sessionKeyAuthSig = (await sessionOwnerAccountWithSigner.signMessage({ + types: sessionKeyAuthTypes, + primaryType: 'SessionKeyAuth', + domain: starkDomain, + message: sessionKeyAuthMsg as any, + } as typedData.TypedData)) as any; + + const sessionKeyAuthCalldata = CallData.compile({ + signature: [sessionKeyAuthSig.r, sessionKeyAuthSig.s], + ...sessionKeyAuthMsg, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + // Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // UPDATE PROPOSAL + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + const updateProposalSig = (await sessionAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + const updateProposalCalldata = CallData.compile({ + signature: [updateProposalSig.r, updateProposalSig.s], + ...updateProposalMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_update_proposal', + updateProposalCalldata, + { + rawInput: true, + }, + ); + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + // VOTE + const voteMsg: Vote = { + space: space.address, + voter: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + const voteSig = (await sessionAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + const voteCalldata = CallData.compile({ + signature: [voteSig.r, voteSig.s], + ...voteMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke(starkSigSessionKeyAuthenticator, 'authenticate_vote', voteCalldata, { + rawInput: true, + }); + }, 1000000); + + it('will revert if incorrect signatures are used', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + + // Account #1 on Starknet devnet with seed 42 + const invalidAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: network } }), + '0x7aac39162d91acf2c4f0d539f4b81e23832619ac0c3df9fce22e4a8d505632a', + '0x23b8c1e9392456de3eb13b9046685257', + ); + + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + + const sessionKeyAuthSig = (await sessionOwnerAccountWithSigner.signMessage({ + types: sessionKeyAuthTypes, + primaryType: 'SessionKeyAuth', + domain: starkDomain, + message: sessionKeyAuthMsg as any, + } as typedData.TypedData)) as any; + + const invalidSessionKeyAuthCalldata = CallData.compile({ + signature: ['0x1', sessionKeyAuthSig.s], + ...sessionKeyAuthMsg, + }); + + try { + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'register_with_owner_sig', + invalidSessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + const sessionKeyAuthCalldata = CallData.compile({ + signature: [sessionKeyAuthSig.r, sessionKeyAuthSig.s], + ...sessionKeyAuthMsg, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Attempt to propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + + const invalidProposeSig = (await invalidAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const invalidProposeCalldata = CallData.compile({ + signature: [invalidProposeSig.r, invalidProposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + + try { + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_propose', + invalidProposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Actually creating a proposal + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // Attempt to update proposal + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + + const invalidUpdateProposalSig = (await invalidAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + + const invalidUpdateProposalCalldata = CallData.compile({ + signature: [invalidUpdateProposalSig.r, invalidUpdateProposalSig.s], + ...updateProposalMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + + try { + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_update_proposal', + invalidUpdateProposalCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + + // Attempt to Vote + const voteMsg: Vote = { + space: space.address, + voter: sessionKeyAuthMsg.owner, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + + const invalidVoteSig = (await invalidAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + + const invalidVoteCalldata = CallData.compile({ + signature: [invalidVoteSig.r, invalidVoteSig.s], + ...voteMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + + try { + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_vote', + invalidVoteCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Attempt to revoke session with owner sig + const sessionKeyRevokeMsg: SessionKeyRevoke = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: account_public_key, + salt: '0x3', + }; + const invalidSessionKeyRevokeSig = (await invalidAccountWithSigner.signMessage({ + types: sessionKeyRevokeTypes, + primaryType: 'SessionKeyRevoke', + domain: starkDomain, + message: sessionKeyRevokeMsg as any, + } as typedData.TypedData)) as any; + + const invalidSessionKeyRevokeCalldata = CallData.compile({ + signature: [invalidSessionKeyRevokeSig.r, invalidSessionKeyRevokeSig.s], + ...sessionKeyRevokeMsg, + }); + + try { + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'revoke_with_owner_sig', + invalidSessionKeyRevokeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + }, 1000000); + + it('can revoke a session with an owner sig', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + const sessionKeyAuthSig = (await sessionOwnerAccountWithSigner.signMessage({ + types: sessionKeyAuthTypes, + primaryType: 'SessionKeyAuth', + domain: starkDomain, + message: sessionKeyAuthMsg as any, + } as typedData.TypedData)) as any; + + const sessionKeyAuthCalldata = CallData.compile({ + signature: [sessionKeyAuthSig.r, sessionKeyAuthSig.s], + ...sessionKeyAuthMsg, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Revoke Session + const sessionKeyRevokeMsg: SessionKeyRevoke = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: account_public_key, + salt: '0x3', + }; + const sessionKeyRevokeSig = (await sessionOwnerAccountWithSigner.signMessage({ + types: sessionKeyRevokeTypes, + primaryType: 'SessionKeyRevoke', + domain: starkDomain, + message: sessionKeyRevokeMsg as any, + } as typedData.TypedData)) as any; + + const sessionKeyRevokeCalldata = CallData.compile({ + signature: [sessionKeyRevokeSig.r, sessionKeyRevokeSig.s], + ...sessionKeyRevokeMsg, + }); + + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'revoke_with_owner_sig', + sessionKeyRevokeCalldata, + { + rawInput: true, + }, + ); + }, 1000000); + + it('can revoke a session with an session key sig', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + salt: '0x0', + }; + const sessionKeyAuthSig = (await sessionOwnerAccountWithSigner.signMessage({ + types: sessionKeyAuthTypes, + primaryType: 'SessionKeyAuth', + domain: starkDomain, + message: sessionKeyAuthMsg as any, + } as typedData.TypedData)) as any; + + const sessionKeyAuthCalldata = CallData.compile({ + signature: [sessionKeyAuthSig.r, sessionKeyAuthSig.s], + ...sessionKeyAuthMsg, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Revoke Session with session key sig + const revokeSessionMsg: SessionKeyRevokeStark = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: sessionKeyAuthMsg.sessionPublicKey, + salt: '0x1', + }; + const revokeSessionKeySig = (await sessionAccountWithSigner.signMessage({ + types: sessionKeyRevokeTypesStark, + primaryType: 'SessionKeyRevoke', + domain: starkDomain, + message: revokeSessionMsg as any, + } as typedData.TypedData)) as any; + const revokeSessionCalldata = CallData.compile({ + signature: [revokeSessionKeySig.r, revokeSessionKeySig.s], + owner: revokeSessionMsg.owner, + sessionPublicKey: revokeSessionMsg.sessionPublicKey, + salt: revokeSessionMsg.salt, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'revoke_with_session_key_sig', + revokeSessionCalldata, + { + rawInput: true, + }, + ); + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); + + it('The session cannot be used if it has expired', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthMsg: SessionKeyAuth = { + owner: sessionOwnerAccountWithSigner.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x1', + salt: '0x0', + }; + const sessionKeyAuthSig = (await sessionOwnerAccountWithSigner.signMessage({ + types: sessionKeyAuthTypes, + primaryType: 'SessionKeyAuth', + domain: starkDomain, + message: sessionKeyAuthMsg as any, + } as typedData.TypedData)) as any; + + const sessionKeyAuthCalldata = CallData.compile({ + signature: [sessionKeyAuthSig.r, sessionKeyAuthSig.s], + ...sessionKeyAuthMsg, + }); + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'register_with_owner_sig', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Increase time to expire the session + await starknet.devnet.increaseTime(100); + + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionKeyAuthMsg.owner, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: sessionKeyAuthMsg.sessionPublicKey, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + starkSigSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); +}); diff --git a/tests/stark-sig-types.ts b/tests/stark-sig-types.ts index 5a4b1ea4..694e3d41 100644 --- a/tests/stark-sig-types.ts +++ b/tests/stark-sig-types.ts @@ -63,6 +63,25 @@ export const updateProposalTypes = { u256: sharedTypes.u256, }; +export const sessionKeyRevokeTypes = { + StarkNetDomain: domainTypes.StarkNetDomain, + SessionKeyRevoke: [ + { name: 'owner', type: 'felt252' }, + { name: 'sessionPublicKey', type: 'felt252' }, + { name: 'salt', type: 'felt252' }, + ], +}; + +export const sessionKeyAuthTypes = { + StarkNetDomain: domainTypes.StarkNetDomain, + SessionKeyAuth: [ + { name: 'owner', type: 'felt252' }, + { name: 'sessionPublicKey', type: 'felt252' }, + { name: 'sessionDuration', type: 'felt252' }, + { name: 'salt', type: 'felt252' }, + ], +}; + export interface Strategy { address: string; params: string[]; @@ -105,6 +124,19 @@ export interface UpdateProposal { salt: string; } +export interface SessionKeyAuth { + owner: string; + sessionPublicKey: string; + sessionDuration: string; + salt: string; +} + +export interface SessionKeyRevoke { + owner: string; + sessionPublicKey: string; + salt: string; +} + export interface StarknetSigProposeCalldata extends Propose { signature: string[]; accountType: string; diff --git a/tests/stark-tx-sk-auth.test.ts b/tests/stark-tx-sk-auth.test.ts new file mode 100644 index 00000000..de33ec6c --- /dev/null +++ b/tests/stark-tx-sk-auth.test.ts @@ -0,0 +1,628 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import { starknet, ethers } from 'hardhat'; +import { CallData, typedData, cairo, shortString, Account, Provider } from 'starknet'; +import { + sessionKeyAuthTypes, + SessionKeyAuth, + sessionKeyRevokeTypes, + SessionKeyRevoke, +} from './stark-sig-types'; +import { + proposeTypes, + Propose, + updateProposalTypes, + UpdateProposal, + voteTypes, + Vote, + sessionKeyRevokeTypes as sessionKeyRevokeTypesStark, + SessionKeyRevoke as SessionKeyRevokeStark, +} from './stark-sig-types'; +import { getRSVFromSig } from './utils'; + +dotenv.config(); + +const account_address = process.env.ADDRESS || ''; +const account_pk = process.env.PK || ''; +const account_public_key = process.env.PUBLIC_KEY || ''; +const network = process.env.STARKNET_NETWORK_URL || ''; + +describe('Ethereum Signature Session Key Authenticator', function () { + this.timeout(1000000); + + // Account used to submit transactions + let manaAccount: starknet.starknetAccount; + let sessionOwnerAccount: starknet.starknetAccount; + // SNIP-6 compliant account (the defaults deployed on the devnet are not SNIP-6 compliant and therefore cannot be used for signatures) + let sessionAccountWithSigner: Account; + let starkTxSessionKeyAuthenticator: starknet.StarknetContract; + let vanillaVotingStrategy: starknet.StarknetContract; + let vanillaProposalValidationStrategy: starknet.StarknetContract; + let space: starknet.StarknetContract; + + let starkDomain: any; + + before(async function () { + manaAccount = await starknet.OpenZeppelinAccount.getAccountFromAddress( + account_address, + account_pk, + ); + + // Account #2 on Starknet devnet with seed 42 + sessionOwnerAccount = await starknet.OpenZeppelinAccount.getAccountFromAddress( + '0x9c4ba7b103329632f6bf5035f1e440b341e7477c4231a47b15545b19d23f76', + '0xbd9c66b3ad3c2d6d1a3d1fa7bc8960a9', + ); + + const accountFactory = await starknet.getContractFactory('openzeppelin_Account'); + const starkTxSessionKeyAuthenticatorFactory = await starknet.getContractFactory( + 'sx_StarkTxSessionKeyAuthenticator', + ); + const vanillaVotingStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaVotingStrategy', + ); + const vanillaProposalValidationStrategyFactory = await starknet.getContractFactory( + 'sx_VanillaProposalValidationStrategy', + ); + const spaceFactory = await starknet.getContractFactory('sx_Space'); + + try { + // If the contracts are already declared, this will be skipped + await manaAccount.declare(accountFactory); + await manaAccount.declare(starkTxSessionKeyAuthenticatorFactory); + await manaAccount.declare(vanillaVotingStrategyFactory); + await manaAccount.declare(vanillaProposalValidationStrategyFactory); + await manaAccount.declare(spaceFactory); + } catch {} + + starkTxSessionKeyAuthenticator = await manaAccount.deploy( + starkTxSessionKeyAuthenticatorFactory, + { + name: shortString.encodeShortString('sx-sn'), + version: shortString.encodeShortString('0.1.0'), + }, + ); + + starkDomain = { + name: 'sx-sn', + version: '0.1.0', + chainId: '0x534e5f474f45524c49', // devnet id + verifyingContract: starkTxSessionKeyAuthenticator.address, + }; + + // Dummy account wrapper to sign messages + sessionAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: network } }), + '0x1', + account_pk, + ); + + vanillaVotingStrategy = await manaAccount.deploy(vanillaVotingStrategyFactory); + vanillaProposalValidationStrategy = await manaAccount.deploy( + vanillaProposalValidationStrategyFactory, + ); + space = await manaAccount.deploy(spaceFactory); + + // Initializing the space + const initializeCalldata = CallData.compile({ + _owner: 1, + _max_voting_duration: 200, + _min_voting_duration: 200, + _voting_delay: 100, + _proposal_validation_strategy: { + address: vanillaProposalValidationStrategy.address, + params: [], + }, + _proposal_validation_strategy_metadata_uri: [], + _voting_strategies: [{ address: vanillaVotingStrategy.address, params: [] }], + _voting_strategies_metadata_uri: [[]], + _authenticators: [starkTxSessionKeyAuthenticator.address], + _metadata_uri: [], + _dao_uri: [], + }); + + await manaAccount.invoke(space, 'initialize', initializeCalldata, { rawInput: true }); + + // Dumping the Starknet state so it can be loaded at the same point for each test + await starknet.devnet.dump('dump.pkl'); + }, 10000000); + + it('can register a session then authenticate a proposal, a vote, and a proposal update with that session', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthCalldata = CallData.compile({ + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + }); + await sessionOwnerAccount.invoke( + starkTxSessionKeyAuthenticator, + 'register_with_owner_tx', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + // Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionOwnerAccount.address, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // UPDATE PROPOSAL + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: sessionOwnerAccount.address, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + const updateProposalSig = (await sessionAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + const updateProposalCalldata = CallData.compile({ + signature: [updateProposalSig.r, updateProposalSig.s], + ...updateProposalMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_update_proposal', + updateProposalCalldata, + { + rawInput: true, + }, + ); + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + // VOTE + const voteMsg: Vote = { + space: space.address, + voter: sessionOwnerAccount.address, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + const voteSig = (await sessionAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + const voteCalldata = CallData.compile({ + signature: [voteSig.r, voteSig.s], + ...voteMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke(starkTxSessionKeyAuthenticator, 'authenticate_vote', voteCalldata, { + rawInput: true, + }); + }, 1000000); + + it('will revert if incorrect signatures are used', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthCalldata = CallData.compile({ + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + }); + await sessionOwnerAccount.invoke( + starkTxSessionKeyAuthenticator, + 'register_with_owner_tx', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Account #1 on Starknet devnet with seed 42 + const invalidAccountWithSigner = new Account( + new Provider({ sequencer: { baseUrl: network } }), + '0x7aac39162d91acf2c4f0d539f4b81e23832619ac0c3df9fce22e4a8d505632a', + '0x23b8c1e9392456de3eb13b9046685257', + ); + + // Attempt to propose + const proposeMsg: Propose = { + space: space.address, + author: sessionOwnerAccount.address, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + + const invalidProposeSig = (await invalidAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const invalidProposeCalldata = CallData.compile({ + signature: [invalidProposeSig.r, invalidProposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + + try { + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_propose', + invalidProposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Actually creating a proposal + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + // Attempt to update proposal + const updateProposalMsg: UpdateProposal = { + space: space.address, + author: sessionOwnerAccount.address, + proposalId: { low: '0x1', high: '0x0' }, + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x5', '0x6', '0x7', '0x8'], + }, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + salt: '0x2', + }; + + const invalidUpdateProposalSig = (await invalidAccountWithSigner.signMessage({ + types: updateProposalTypes, + primaryType: 'UpdateProposal', + domain: starkDomain, + message: updateProposalMsg as any, + } as typedData.TypedData)) as any; + + const invalidUpdateProposalCalldata = CallData.compile({ + signature: [invalidUpdateProposalSig.r, invalidUpdateProposalSig.s], + ...updateProposalMsg, + session_public_key: account_public_key, + }); + + try { + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_update_proposal', + invalidUpdateProposalCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + + // Increase time so voting period begins + await starknet.devnet.increaseTime(100); + + // Attempt to Vote + const voteMsg: Vote = { + space: space.address, + voter: sessionOwnerAccount.address, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x1', + userVotingStrategies: [{ index: '0x0', params: ['0x1', '0x2', '0x3', '0x4'] }], + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + }; + + const invalidVoteSig = (await invalidAccountWithSigner.signMessage({ + types: voteTypes, + primaryType: 'Vote', + domain: starkDomain, + message: voteMsg as any, + } as typedData.TypedData)) as any; + + const invalidVoteCalldata = CallData.compile({ + signature: [invalidVoteSig.r, invalidVoteSig.s], + ...voteMsg, + session_public_key: account_public_key, + }); + + try { + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_vote', + invalidVoteCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Signature')); + } + }, 1000000); + + it('can revoke a session with an owner tx', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthCalldata = CallData.compile({ + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + }); + await sessionOwnerAccount.invoke( + starkTxSessionKeyAuthenticator, + 'register_with_owner_tx', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Revoke Session + const revokeSessionCalldata = CallData.compile({ + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + }); + await sessionOwnerAccount.invoke( + starkTxSessionKeyAuthenticator, + 'revoke_with_owner_tx', + revokeSessionCalldata, + { + rawInput: true, + }, + ); + }, 1000000); + + it('can revoke a session with an session key sig', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthCalldata = CallData.compile({ + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + }); + await sessionOwnerAccount.invoke( + starkTxSessionKeyAuthenticator, + 'register_with_owner_tx', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Revoke Session with session key sig + const revokeSessionMsg: SessionKeyRevokeStark = { + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + salt: '0x1', + }; + const revokeSessionKeySig = (await sessionAccountWithSigner.signMessage({ + types: sessionKeyRevokeTypesStark, + primaryType: 'SessionKeyRevoke', + domain: starkDomain, + message: revokeSessionMsg as any, + } as typedData.TypedData)) as any; + const revokeSessionCalldata = CallData.compile({ + signature: [revokeSessionKeySig.r, revokeSessionKeySig.s], + owner: revokeSessionMsg.owner, + sessionPublicKey: revokeSessionMsg.sessionPublicKey, + salt: revokeSessionMsg.salt, + }); + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'revoke_with_session_key_sig', + revokeSessionCalldata, + { + rawInput: true, + }, + ); + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionOwnerAccount.address, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); + + it('The session cannot be used if it has expired', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Register Session + const sessionKeyAuthCalldata = CallData.compile({ + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x1', + }); + await sessionOwnerAccount.invoke( + starkTxSessionKeyAuthenticator, + 'register_with_owner_tx', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + + // Increase time to expire the session + await starknet.devnet.increaseTime(100); + + // Try to Propose + const proposeMsg: Propose = { + space: space.address, + author: sessionOwnerAccount.address, + metadataUri: ['0x1', '0x2', '0x3', '0x4'], + executionStrategy: { + address: '0x0000000000000000000000000000000000005678', + params: ['0x0'], + }, + userProposalValidationParams: [ + '0xffffffffffffffffffffffffffffffffffffffffff', + '0x1234', + '0x5678', + '0x9abc', + ], + salt: '0x1', + }; + const proposeSig = (await sessionAccountWithSigner.signMessage({ + types: proposeTypes, + primaryType: 'Propose', + domain: starkDomain, + message: proposeMsg as any, + } as typedData.TypedData)) as any; + const proposeCalldata = CallData.compile({ + signature: [proposeSig.r, proposeSig.s], + ...proposeMsg, + session_public_key: account_public_key, + }); + // Proposing should fail because the session is revoked + try { + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'authenticate_propose', + proposeCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Session key expired')); + } + }, 1000000); + + it('The register/revoke tx must be submitted by the session owner', async () => { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + // Try to Register Session through mana account + const sessionKeyAuthCalldata = CallData.compile({ + owner: sessionOwnerAccount.address, + sessionPublicKey: account_public_key, + sessionDuration: '0x123', + }); + try { + await manaAccount.invoke( + starkTxSessionKeyAuthenticator, + 'register_with_owner_tx', + sessionKeyAuthCalldata, + { + rawInput: true, + }, + ); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.contain(shortString.encodeShortString('Invalid Caller')); + } + }, 1000000); +});