Skip to content

Commit

Permalink
Merge pull request horuslabsio#29 from Darlington02/main
Browse files Browse the repository at this point in the history
chore: add github workflows, update account contract
  • Loading branch information
Darlington02 authored Jan 16, 2024
2 parents 2996b63 + ef1ed67 commit 7975a3d
Show file tree
Hide file tree
Showing 22 changed files with 434 additions and 225 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/build_contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Build

on: [push, pull_request]

permissions: read-all

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: software-mansion/setup-scarb@v1
- name: Check cairo format
run: scarb fmt --check
- name: Build cairo programs
run: scarb build
16 changes: 16 additions & 0 deletions .github/workflows/test_contracts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Test

on: [push, pull_request]
permissions: read-all

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: software-mansion/setup-scarb@v1
- uses: foundry-rs/setup-snfoundry@v2
with:
starknet-foundry-version: 0.14.0
- name: Run cairo tests
run: snforge test
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
scarb 2.4.3
starknet-foundry 0.14.0
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Starknet Africa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# SRC-6551 Reference Implementation on Starknet
# ERC-6551 Reference Implementation on Starknet

This repository contains the reference implementation of ERC-6551.
This repository contains the reference implementation of ERC-6551 on Starknet.

**NB:** This project is under active development and may undergo changes until the SRC-6551 SNIP is finalized.
**NB:** This project is under active development and may undergo changes until SNIP-72 is finalized.

## The SRC-6551 Standard
This proposal defines a system which assigns contract accounts to Non-fungible tokens (SRC-721).
## The Tokenbound Standard
This proposal defines a system which assigns contract accounts to Non-fungible tokens (ERC-721s).

These accounts are referred to as token bound accounts and they allow NFTs to own assets and interact with applications, without requiring changes to existing smart contracts or infrastructure.

Expand Down
4 changes: 2 additions & 2 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ version = 1

[[package]]
name = "snforge_std"
version = "0.1.0"
source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.11.0#5465c41541c44a7804d16318fab45a2f0ccec9e7"
version = "0.14.0"
source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.14.0#e8cbecee4e31ed428c76d5173eaa90c8df796fe3"

[[package]]
name = "token_bound_accounts"
Expand Down
4 changes: 2 additions & 2 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ sierra = true
casm = true

[dependencies]
starknet = "2.3.1"
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.11.0" }
starknet = "2.4.3"
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.14.0" }

[tool.snforge]
# exit_first = true
2 changes: 1 addition & 1 deletion src/account.cairo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
mod account;
mod account;
126 changes: 76 additions & 50 deletions src/account/account.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
////////////////////////////////
#[starknet::contract]
mod Account {
use starknet::{get_tx_info, get_caller_address, get_contract_address, get_block_timestamp, ContractAddress, account::Call, call_contract_syscall, replace_class_syscall, ClassHash, SyscallResultTrait};
use starknet::{
get_tx_info, get_caller_address, get_contract_address, get_block_timestamp, ContractAddress,
account::Call, call_contract_syscall, replace_class_syscall, ClassHash, SyscallResultTrait
};
use ecdsa::check_ecdsa_signature;
use array::{SpanTrait, ArrayTrait};
use box::BoxTrait;
Expand All @@ -13,10 +16,11 @@ mod Account {
use token_bound_accounts::interfaces::IAccount::IAccount;

// SRC5 interface for token bound accounts
const TBA_INTERFACE_ID: felt252 = 0x539036932a2ab9c4734fbfd9872a1f7791a3f577e45477336ae0fd0a00c9ff;
const TBA_INTERFACE_ID: felt252 =
0x539036932a2ab9c4734fbfd9872a1f7791a3f577e45477336ae0fd0a00c9ff;

#[storage]
struct Storage{
struct Storage {
_token_contract: ContractAddress, // contract address of NFT
_token_id: u256, // token ID of NFT
_unlock_timestamp: u64, // time to unlock account when locked
Expand All @@ -26,8 +30,9 @@ mod Account {
#[derive(Drop, starknet::Event)]
enum Event {
AccountCreated: AccountCreated,
TransactionExecuted: TransactionExecuted,
Upgraded: Upgraded
AccountUpgraded: AccountUpgraded,
AccountLocked: AccountLocked,
TransactionExecuted: TransactionExecuted
}

/// @notice Emitted exactly once when the account is initialized
Expand All @@ -49,16 +54,26 @@ mod Account {
}

/// @notice Emitted when the account upgrades to a new implementation
/// @param tokenContract the contract address of the NFT
/// @param tokenId the token ID of the NFT
/// @param account tokenbound account to be upgraded
/// @param implementation the upgraded account class hash
#[derive(Drop, starknet::Event)]
struct Upgraded {
tokenContract: ContractAddress,
tokenId: u256,
struct AccountUpgraded {
account: ContractAddress,
implementation: ClassHash
}

/// @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,
}

#[constructor]
fn constructor(ref self: ContractState, token_contract: ContractAddress, token_id: u256) {
self._token_contract.write(token_contract);
Expand All @@ -69,19 +84,19 @@ mod Account {
}

#[external(v0)]
impl IAccountImpl of IAccount<ContractState>{
impl IAccountImpl of IAccount<ContractState> {
/// @notice used for signature validation
/// @param hash The message hash
/// @param signature The signature to be validated
fn is_valid_signature(self: @ContractState, hash: felt252, signature: Span<felt252>) -> felt252 {
fn is_valid_signature(
self: @ContractState, hash: felt252, signature: Span<felt252>
) -> felt252 {
self._is_valid_signature(hash, signature)
}
}

fn __validate_deploy__(
self: @ContractState,
class_hash:felt252,
contract_address_salt:felt252,
) -> felt252{
self: @ContractState, class_hash: felt252, contract_address_salt: felt252,
) -> felt252 {
self._validate_transaction()
}

Expand All @@ -91,13 +106,13 @@ mod Account {

/// @notice validate an account transaction
/// @param calls an array of transactions to be executed
fn __validate__( ref self: ContractState, mut calls:Array<Call>) -> felt252{
fn __validate__(ref self: ContractState, mut calls: Array<Call>) -> felt252 {
self._validate_transaction()
}

/// @notice executes a transaction
/// @param calls an array of transactions to be executed
fn __execute__(ref self: ContractState, mut calls:Array<Call>) -> Array<Span<felt252>> {
fn __execute__(ref self: ContractState, mut calls: Array<Call>) -> Array<Span<felt252>> {
self._assert_only_owner();
let (lock_status, _) = self._is_locked();
assert(!lock_status, 'Account: account is locked!');
Expand All @@ -115,14 +130,16 @@ mod Account {
/// @notice gets the token bound NFT owner
/// @param token_contract the contract address of the NFT
/// @param token_id the token ID of the NFT
fn owner(self: @ContractState, token_contract:ContractAddress, token_id:u256) -> ContractAddress {
self._get_owner(token_contract, token_id)
fn owner(
self: @ContractState, token_contract: ContractAddress, token_id: u256
) -> ContractAddress {
self._get_owner(token_contract, token_id)
}

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

/// @notice ugprades an account implementation
/// @param implementation the new class_hash
Expand All @@ -132,12 +149,8 @@ mod Account {
assert(!lock_status, 'Account: account is locked!');
assert(!implementation.is_zero(), 'Invalid class hash');
replace_class_syscall(implementation).unwrap_syscall();
self.emit(Upgraded{
tokenContract: self._token_contract.read(),
tokenId: self._token_id.read(),
implementation,
});
}
self.emit(AccountUpgraded { account: get_contract_address(), implementation, });
}

// @notice protection mechanism for selling token bound accounts. can't execute when account is locked
// @param duration for which to lock account
Expand All @@ -148,7 +161,13 @@ mod Account {
let current_timestamp = get_block_timestamp();
let unlock_time = current_timestamp + duration;
self._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: @ContractState) -> (bool, u64) {
Expand All @@ -158,18 +177,18 @@ mod Account {
// @notice check that account supports TBA interface
// @param interface_id interface to be checked against
fn supports_interface(self: @ContractState, interface_id: felt252) -> bool {
if(interface_id == TBA_INTERFACE_ID) {
if (interface_id == TBA_INTERFACE_ID) {
return true;
} else {
return false;
}
}
}
}

#[generate_trait]
impl internalImpl of InternalTrait{
impl internalImpl of InternalTrait {
/// @notice check that caller is the token bound account
fn _assert_only_owner(ref self: ContractState){
fn _assert_only_owner(ref self: ContractState) {
let caller = get_caller_address();
let owner = self._get_owner(self._token_contract.read(), self._token_id.read());
assert(caller == owner, 'Account: unathorized');
Expand All @@ -179,11 +198,15 @@ mod Account {
/// @param token_contract contract address of NFT
// @param token_id token ID of NFT
// NB: This function aims for compatibility with all contracts (snake or camel case) but do not work as expected on mainnet as low level calls do not return err at the moment. Should work for contracts which implements CamelCase but not snake_case until starknet v0.15.
fn _get_owner(self: @ContractState, token_contract: ContractAddress, token_id: u256) -> ContractAddress {
fn _get_owner(
self: @ContractState, token_contract: ContractAddress, token_id: u256
) -> ContractAddress {
let mut calldata: Array<felt252> = ArrayTrait::new();
Serde::serialize(@token_id, ref calldata);
let mut res = call_contract_syscall(token_contract, selector!("ownerOf"), calldata.span());
if(res.is_err()) {
let mut res = call_contract_syscall(
token_contract, selector!("ownerOf"), calldata.span()
);
if (res.is_err()) {
res = call_contract_syscall(token_contract, selector!("owner_of"), calldata.span());
}
let mut address = res.unwrap();
Expand All @@ -201,37 +224,42 @@ mod Account {
fn _is_locked(self: @ContractState) -> (bool, u64) {
let unlock_timestamp = self._unlock_timestamp.read();
let current_time = get_block_timestamp();
if(current_time < unlock_timestamp) {
if (current_time < unlock_timestamp) {
let time_until_unlocks = unlock_timestamp - current_time;
return (true, time_until_unlocks);
} else {
return (false, 0_u64);
}
}
}

/// @notice internal function for tx validation
fn _validate_transaction(self: @ContractState) -> felt252 {
let tx_info = get_tx_info().unbox();
let tx_hash = tx_info.transaction_hash;
let signature = tx_info.signature;
assert(self._is_valid_signature(tx_hash, signature) == starknet::VALIDATED, 'Account: invalid signature');
assert(
self._is_valid_signature(tx_hash, signature) == starknet::VALIDATED,
'Account: invalid signature'
);
starknet::VALIDATED
}

/// @notice internal function for signature validation
fn _is_valid_signature(self: @ContractState, hash:felt252, signature: Span<felt252>) -> felt252 {
fn _is_valid_signature(
self: @ContractState, hash: felt252, signature: Span<felt252>
) -> felt252 {
let signature_length = signature.len();
assert(signature_length == 2_u32, 'Account: invalid sig length');

let caller = get_caller_address();
let owner = self._get_owner(self._token_contract.read(), self._token_id.read());
if(caller == owner) {
if (caller == owner) {
return starknet::VALIDATED;
} else {
return 0;
}
}

/// @notice internal function for executing transactions
/// @param calls An array of transactions to be executed
fn _execute_calls(ref self: ContractState, mut calls: Span<Call>) -> Array<Span<felt252>> {
Expand All @@ -241,21 +269,19 @@ mod Account {
loop {
match calls.pop_front() {
Option::Some(call) => {
match call_contract_syscall(*call.to, *call.selector, call.calldata.span()) {
Result::Ok(mut retdata) => {
result.append(retdata);
},
match call_contract_syscall(
*call.to, *call.selector, call.calldata.span()
) {
Result::Ok(mut retdata) => { result.append(retdata); },
Result::Err(revert_reason) => {
panic_with_felt252('multicall_failed');
}
}
},
Option::None(_) => {
break();
}
Option::None(_) => { break (); }
};
};
result
}
}
}
}
2 changes: 1 addition & 1 deletion src/interfaces.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mod IAccount;
mod IERC721;
mod IRegistry;
mod IRegistry;
Loading

0 comments on commit 7975a3d

Please sign in to comment.