Skip to content

Commit

Permalink
chore: executable as standalone component
Browse files Browse the repository at this point in the history
  • Loading branch information
Darlington02 committed Aug 15, 2024
1 parent 0923398 commit 14628c4
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 129 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ jobs:
- uses: software-mansion/setup-scarb@v1
- uses: foundry-rs/setup-snfoundry@v3
with:
starknet-foundry-version: 0.20.1
starknet-foundry-version: 0.27.0
- name: Run cairo tests
run: snforge test
160 changes: 44 additions & 116 deletions src/account/account.cairo
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
////////////////////////////////
// Account Component
////////////////////////////////
// *************************************************************************
// ACCOUNT COMPONENT
// *************************************************************************
#[starknet::component]
mod AccountComponent {
// *************************************************************************
// IMPORTS
// *************************************************************************
use core::num::traits::zero::Zero;
use starknet::{
get_tx_info, get_caller_address, get_contract_address, get_block_timestamp, ContractAddress,
Expand All @@ -13,59 +16,49 @@ mod AccountComponent {
IAccount, IAccountDispatcherTrait, IAccountDispatcher, TBA_INTERFACE_ID
};

// *************************************************************************
// STORAGE
// *************************************************************************
#[storage]
struct Storage {
account_token_contract: ContractAddress, // contract address of NFT
account_token_id: u256, // token ID of NFT
account_unlock_timestamp: u64, // time to unlock account when locked
state: u256
}

// *************************************************************************
// EVENTS
// *************************************************************************
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
AccountCreated: AccountCreated,
AccountLocked: AccountLocked,
TransactionExecuted: TransactionExecuted
TBACreated: TBACreated
}

/// @notice Emitted exactly once when the account is initialized
/// @param owner The owner address
#[derive(Drop, starknet::Event)]
struct AccountCreated {
struct TBACreated {
#[key]
owner: ContractAddress,
}

/// @notice Emitted when the account executes a transaction
/// @param hash The transaction hash
/// @param response The data returned by the methods called
#[derive(Drop, starknet::Event)]
struct TransactionExecuted {
#[key]
hash: felt252,
response: Span<Span<felt252>>
}

/// @notice Emitted when the account is locked
/// @param account tokenbound account who's lock function was triggered
/// @param locked_at timestamp at which the lock function was triggered
/// @param duration time duration for which the account remains locked
#[derive(Drop, starknet::Event)]
struct AccountLocked {
#[key]
account: ContractAddress,
locked_at: u64,
duration: u64,
account_address: ContractAddress,
parent_account: ContractAddress,
token_contract: ContractAddress,
token_id: u256
}

// *************************************************************************
// ERRORS
// *************************************************************************
mod Errors {
const LOCKED_ACCOUNT: felt252 = 'Account: account is locked!';
const INV_TX_VERSION: felt252 = 'Account: invalid tx version';
const UNAUTHORIZED: felt252 = 'Account: unauthorized';
const INV_SIG_LEN: felt252 = 'Account: invalid sig length';
const INV_SIGNATURE: felt252 = 'Account: invalid signature';
}

// *************************************************************************
// EXTERNAL FUNCTIONS
// *************************************************************************
#[embeddable_as(AccountImpl)]
impl Account<
TContractState, +HasComponent<TContractState>, +Drop<TContractState>
Expand Down Expand Up @@ -113,27 +106,6 @@ mod AccountComponent {
self._validate_transaction()
}

/// @notice executes a transaction
/// @param calls an array of transactions to be executed
fn __execute__(
ref self: ComponentState<TContractState>, mut calls: Array<Call>
) -> Array<Span<felt252>> {
let caller = get_caller_address();
assert(self._is_valid_signer(caller), Errors::UNAUTHORIZED);

let (lock_status, _) = self._is_locked();
assert(!lock_status, Errors::LOCKED_ACCOUNT);

let tx_info = get_tx_info().unbox();
assert(tx_info.version != 0, Errors::INV_TX_VERSION);

let retdata = self._execute_calls(calls);
let hash = tx_info.transaction_hash;
let response = retdata.span();
self.emit(TransactionExecuted { hash, response });
retdata
}

/// @notice gets the NFT owner
/// @param token_contract the contract address of the NFT
/// @param token_id the token ID of the NFT
Expand All @@ -144,33 +116,13 @@ mod AccountComponent {
}

/// @notice returns the contract address and token ID of the associated NFT
fn token(self: @ComponentState<TContractState>) -> (ContractAddress, u256) {
fn token(self: @ComponentState<TContractState>) -> (ContractAddress, u256, felt252) {
self._get_token()
}

// @notice protection mechanism for selling token bound accounts. can't execute when account
// is locked @param duration for which to lock account
fn lock(ref self: ComponentState<TContractState>, duration: u64) {
let caller = get_caller_address();
assert(self._is_valid_signer(caller), Errors::UNAUTHORIZED);

let (lock_status, _) = self._is_locked();
assert(!lock_status, Errors::LOCKED_ACCOUNT);

let current_timestamp = get_block_timestamp();
let unlock_time = current_timestamp + duration;
self.account_unlock_timestamp.write(unlock_time);
self
.emit(
AccountLocked {
account: get_contract_address(), locked_at: current_timestamp, duration
}
);
}

// @notice returns account lock status and time left until account unlocks
fn is_locked(self: @ComponentState<TContractState>) -> (bool, u64) {
return self._is_locked();
/// @notice returns the current state of the contract
fn state(self: @ComponentState<TContractState>) -> u256 {
self.state.read()
}

// @notice check that account supports TBA interface
Expand All @@ -186,6 +138,9 @@ mod AccountComponent {
}
}

// *************************************************************************
// PRIVATE FUNCTIONS
// *************************************************************************
#[generate_trait]
impl InternalImpl<
TContractState, +HasComponent<TContractState>, +Drop<TContractState>
Expand All @@ -201,7 +156,13 @@ mod AccountComponent {
// initialize account
self.account_token_contract.write(token_contract);
self.account_token_id.write(token_id);
self.emit(AccountCreated { owner });
self.emit(
TBACreated {
account_address: get_contract_address(),
parent_account: owner,
token_contract,
token_id
});
}

/// @notice internal function for getting NFT owner
Expand All @@ -227,23 +188,12 @@ mod AccountComponent {
}

/// @notice internal transaction for returning the contract address and token ID of the NFT
fn _get_token(self: @ComponentState<TContractState>) -> (ContractAddress, u256) {
fn _get_token(self: @ComponentState<TContractState>) -> (ContractAddress, u256, felt252) {
let contract = self.account_token_contract.read();
let tokenId = self.account_token_id.read();
(contract, tokenId)
}

// @notice protection mechanism for TBA trading. Returns the lock-status (true or false),
// and the remaning time till account unlocks.
fn _is_locked(self: @ComponentState<TContractState>) -> (bool, u64) {
let unlock_timestamp = self.account_unlock_timestamp.read();
let current_time = get_block_timestamp();
if (current_time < unlock_timestamp) {
let time_until_unlocks = unlock_timestamp - current_time;
return (true, time_until_unlocks);
} else {
return (false, 0_u64);
}
let token_id = self.account_token_id.read();
let tx_info = get_tx_info().unbox();
let chain_id = tx_info.chain_id;
(contract, token_id, chain_id)
}

// @notice internal function for validating signer
Expand Down Expand Up @@ -287,27 +237,5 @@ mod AccountComponent {
);
starknet::VALIDATED
}

/// @notice internal function for executing transactions
/// @param calls An array of transactions to be executed
fn _execute_calls(
ref self: ComponentState<TContractState>, mut calls: Array<Call>
) -> Array<Span<felt252>> {
let mut result: Array<Span<felt252>> = ArrayTrait::new();
let mut calls = calls;

loop {
match calls.pop_front() {
Option::Some(call) => {
match call_contract_syscall(call.to, call.selector, call.calldata) {
Result::Ok(mut retdata) => { result.append(retdata); },
Result::Err(_) => { panic(array!['multicall_failed']); }
}
},
Option::None(_) => { break (); }
};
};
result
}
}
}
1 change: 1 addition & 0 deletions src/accountV3.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod accountV3;
3 changes: 1 addition & 2 deletions src/presets/account.cairo → src/accountV3/accountV3.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ mod Account {
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(self.account._is_valid_signer(caller), AccountComponent::Errors::UNAUTHORIZED);
let (lock_status, _) = self.account._is_locked();
assert(!lock_status, AccountComponent::Errors::LOCKED_ACCOUNT);
// TODO: check account is locked
self.upgradeable._upgrade(new_class_hash);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/components/executable.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod executable;
104 changes: 104 additions & 0 deletions src/components/executable/executable.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// *************************************************************************
// EXECUTABLE COMPONENT
// *************************************************************************
#[starknet::component]
mod UpgradeableComponent {
use starknet::SyscallResultTrait;

use token_bound_accounts::interfaces::IExecutable;

// *************************************************************************
// STORAGE
// *************************************************************************
#[storage]
struct Storage {}

// *************************************************************************
// EVENT
// *************************************************************************
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
TransactionExecuted: TransactionExecuted
}

/// @notice Emitted when the account executes a transaction
/// @param hash The transaction hash
/// @param response The data returned by the methods called
#[derive(Drop, starknet::Event)]
struct TransactionExecuted {
#[key]
hash: felt252,
#[key]
account_address: ContractAddress,
response: Span<Span<felt252>>
}

// *************************************************************************
// ERRORS
// *************************************************************************
mod Errors {
const UNAUTHORIZED: felt252 = 'Account: unauthorized';
}

// *************************************************************************
// EXTERNAL FUNCTIONS
// *************************************************************************
#[embeddable_as(AccountExecutable)]
impl Account<
TContractState, +HasComponent<TContractState>, +Drop<TContractState>
> of IExecutable<ComponentState<TContractState>> {
/// @notice executes a transaction
/// @param calls an array of transactions to be executed
fn _execute(
ref self: ComponentState<TContractState>, mut calls: Array<Call>
) -> Array<Span<felt252>> {
let caller = get_caller_address();
assert(self._is_valid_signer(caller), Errors::UNAUTHORIZED);

let tx_info = get_tx_info().unbox();
assert(tx_info.version != 0, Errors::INV_TX_VERSION);

let retdata = self._execute_calls(calls);
let hash = tx_info.transaction_hash;
let response = retdata.span();
self.emit(
TransactionExecuted {
hash,
account_address: get_contract_address(),
response
});
retdata
}
}

// *************************************************************************
// PRIVATE FUNCTIONS
// *************************************************************************
#[generate_trait]
impl InternalImpl<
TContractState, +HasComponent<TContractState>, +Drop<TContractState>
> of InternalTrait<TContractState> {
/// @notice internal function for executing transactions
/// @param calls An array of transactions to be executed
fn _execute_calls(
ref self: ComponentState<TContractState>, mut calls: Array<Call>
) -> Array<Span<felt252>> {
let mut result: Array<Span<felt252>> = ArrayTrait::new();
let mut calls = calls;

loop {
match calls.pop_front() {
Option::Some(call) => {
match call_contract_syscall(call.to, call.selector, call.calldata) {
Result::Ok(mut retdata) => { result.append(retdata); },
Result::Err(_) => { panic(array!['multicall_failed']); }
}
},
Option::None(_) => { break (); }
};
};
result
}
}
}
2 changes: 0 additions & 2 deletions src/components/upgradeable/upgradeable.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ mod UpgradeableComponent {
use starknet::SyscallResultTrait;
use core::zeroable::Zeroable;

use token_bound_accounts::interfaces::IUpgradeable;

#[storage]
struct Storage {}

Expand Down
6 changes: 2 additions & 4 deletions src/interfaces/IAccount.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ trait IAccount<TContractState> {
fn __validate_deploy__(
ref self: TContractState, token_contract: ContractAddress, token_id: u256
) -> felt252;
fn __execute__(ref self: TContractState, calls: Array<Call>) -> Array<Span<felt252>>;
fn token(self: @TContractState) -> (ContractAddress, u256);
fn token(self: @TContractState) -> (ContractAddress, u256, felt252);
fn owner(self: @TContractState) -> ContractAddress;
fn lock(ref self: TContractState, duration: u64);
fn is_locked(self: @TContractState) -> (bool, u64);
fn state(self: @TContractState) -> u256;
fn supports_interface(self: @TContractState, interface_id: felt252) -> bool;
}
Loading

0 comments on commit 14628c4

Please sign in to comment.