Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions soroban-contract/contracts/tba_account/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#![no_std]
use soroban_sdk::{
contract, contractimpl, contracttype, Address, BytesN, Env, Vec, Symbol, Val,
auth::Context,
};

#[contract]
pub struct TbaAccount;

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DataKey {
TokenContract, // Address of the NFT contract
TokenId, // Specific NFT token ID (u128)
ImplementationHash, // Hash used for deployment (u256)
Salt, // Deployment salt (u256)
Initialized, // Init flag
Nonce, // Transaction nonce counter
}

// Helper functions for storage
fn get_token_contract(env: &Env) -> Address {
env.storage()
.instance()
.get(&DataKey::TokenContract)
.expect("Contract not initialized")
}

fn set_token_contract(env: &Env, token_contract: &Address) {
env.storage().instance().set(&DataKey::TokenContract, token_contract);
}

fn get_token_id(env: &Env) -> u128 {
env.storage()
.instance()
.get(&DataKey::TokenId)
.expect("Contract not initialized")
}

fn set_token_id(env: &Env, token_id: &u128) {
env.storage().instance().set(&DataKey::TokenId, token_id);
}

fn get_implementation_hash(env: &Env) -> BytesN<32> {
env.storage()
.instance()
.get(&DataKey::ImplementationHash)
.expect("Contract not initialized")
}

fn set_implementation_hash(env: &Env, implementation_hash: &BytesN<32>) {
env.storage()
.instance()
.set(&DataKey::ImplementationHash, implementation_hash);
}

fn get_salt(env: &Env) -> BytesN<32> {
env.storage()
.instance()
.get(&DataKey::Salt)
.expect("Contract not initialized")
}

fn set_salt(env: &Env, salt: &BytesN<32>) {
env.storage().instance().set(&DataKey::Salt, salt);
}

fn is_initialized(env: &Env) -> bool {
env.storage()
.instance()
.get(&DataKey::Initialized)
.unwrap_or(false)
}

fn set_initialized(env: &Env, initialized: &bool) {
env.storage().instance().set(&DataKey::Initialized, initialized);
}

fn get_nonce(env: &Env) -> u64 {
env.storage()
.instance()
.get(&DataKey::Nonce)
.unwrap_or(0u64)
}

fn increment_nonce(env: &Env) -> u64 {
let current_nonce = get_nonce(env);
let new_nonce = current_nonce + 1;
env.storage().instance().set(&DataKey::Nonce, &new_nonce);
new_nonce
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TransactionExecutedEvent {
pub to: Address,
pub func: Symbol,
pub nonce: u64,
}

// Helper function to get NFT owner by calling the NFT contract
fn get_nft_owner(env: &Env, nft_contract: &Address, token_id: u128) -> Address {
// Call the NFT contract's owner_of function
// Using invoke_contract to call the owner_of function on the NFT contract
env.invoke_contract::<Address>(
nft_contract,
&soroban_sdk::symbol_short!("owner_of"),
soroban_sdk::vec![&env, Val::from_payload(token_id as u64)],
)
}

#[contractimpl]
impl TbaAccount {
/// Initialize the TBA account with NFT ownership details
/// This should be called once after deployment by the Registry
pub fn initialize(
env: Env,
token_contract: Address,
token_id: u128,
implementation_hash: BytesN<32>,
salt: BytesN<32>,
) {
// Prevent re-initialization
if is_initialized(&env) {
panic!("Contract already initialized");
}

// Store all parameters
set_token_contract(&env, &token_contract);
set_token_id(&env, &token_id);
set_implementation_hash(&env, &implementation_hash);
set_salt(&env, &salt);
set_initialized(&env, &true);
}

/// Get the NFT contract address
pub fn token_contract(env: Env) -> Address {
get_token_contract(&env)
}

/// Get the token ID
pub fn token_id(env: Env) -> u128 {
get_token_id(&env)
}

/// Get the current owner of the NFT (by querying the NFT contract)
pub fn owner(env: Env) -> Address {
let token_contract = get_token_contract(&env);
let token_id = get_token_id(&env);
get_nft_owner(&env, &token_contract, token_id)
}

/// Get token details as a tuple: (chain_id, token_contract, token_id)
/// This matches the ERC-6551 pattern for compatibility
/// Note: chain_id is set to 0 as Soroban doesn't expose chain_id in the same way
pub fn token(env: Env) -> (u32, Address, u128) {
// Soroban doesn't have chain_id exposed, using 0 as placeholder
// In production, this could be set during initialization
let chain_id = 0u32;
let token_contract = get_token_contract(&env);
let token_id = get_token_id(&env);
(chain_id, token_contract, token_id)
}

/// Get the current nonce
pub fn nonce(env: Env) -> u64 {
get_nonce(&env)
}

/// Execute a transaction to another contract
/// Only the current NFT owner can execute transactions
/// This function increments the nonce and emits an event
pub fn execute(
env: Env,
to: Address,
func: Symbol,
args: Vec<Val>,
) -> Vec<Val> {
// Verify contract is initialized
if !is_initialized(&env) {
panic!("Contract not initialized");
}

// Get the NFT owner and verify authorization
let token_contract = get_token_contract(&env);
let token_id = get_token_id(&env);
let owner = get_nft_owner(&env, &token_contract, token_id);

// Require authorization from the NFT owner
owner.require_auth();

// Increment nonce
let nonce = increment_nonce(&env);

// Emit transaction executed event
let event = TransactionExecutedEvent {
to: to.clone(),
func: func.clone(),
nonce,
};
env.events().publish(
(Symbol::new(&env, "executed"), Symbol::new(&env, "TransactionExecuted")),
event,
);

// Invoke the target contract
env.invoke_contract::<Vec<Val>>(&to, &func, args)
}

/// CustomAccountInterface implementation: Check authorization
/// Only the current NFT owner can authorize transactions
pub fn __check_auth(
env: Env,
signature_payload: BytesN<32>,
signatures: Vec<BytesN<64>>,
auth_context: Vec<Context>,
) {
// Get the NFT contract and token ID
let token_contract = get_token_contract(&env);
let token_id = get_token_id(&env);

// Get the current owner of the NFT
let owner = get_nft_owner(&env, &token_contract, token_id);

// Verify that the owner has authorized this transaction
// The require_auth_for_args will check if the owner has signed
owner.require_auth_for_args(
soroban_sdk::vec![
&env,
Val::from(signature_payload),
Val::from(signatures),
Val::from(auth_context),
],
);
}
}

#[cfg(test)]
mod test;

126 changes: 126 additions & 0 deletions soroban-contract/contracts/tba_account/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#![cfg(test)]

use super::*;
use soroban_sdk::{
testutils::Address as _,
vec, Address, BytesN, Env, Symbol,
};

// Mock NFT contract for testing
#[contract]
pub struct MockNftContract;

#[contractimpl]
impl MockNftContract {
pub fn owner_of(_env: Env, _token_id: u128) -> Address {
// Simple mock: return a generated address
// In real tests, you'd maintain a mapping of token_id -> owner
// For now, we'll use a generated address for testing
// This is a placeholder - actual tests would need proper NFT contract integration
Address::generate(&_env)
}
}

// Helper to create test environment
fn create_test_env() -> Env {
let env = Env::default();
env
}

#[test]
fn test_initialize() {
let env = create_test_env();
let contract_id = env.register(TbaAccount, ());
let client = TbaAccountClient::new(&env, &contract_id);

let nft_contract = Address::generate(&env);
let token_id = 1u128;
let impl_hash = BytesN::from_array(&env, &[1u8; 32]);
let salt = BytesN::from_array(&env, &[2u8; 32]);

// Initialize should succeed
client.initialize(&nft_contract, &token_id, &impl_hash, &salt);

// Verify initialization
assert_eq!(client.token_contract(), nft_contract);
assert_eq!(client.token_id(), token_id);
}

#[test]
#[should_panic(expected = "Contract already initialized")]
fn test_initialize_twice_panics() {
let env = create_test_env();
let contract_id = env.register(TbaAccount, ());
let client = TbaAccountClient::new(&env, &contract_id);

let nft_contract = Address::generate(&env);
let token_id = 1u128;
let impl_hash = BytesN::from_array(&env, &[1u8; 32]);
let salt = BytesN::from_array(&env, &[2u8; 32]);

// First initialization
client.initialize(&nft_contract, &token_id, &impl_hash, &salt);

// Second initialization should panic
client.initialize(&nft_contract, &token_id, &impl_hash, &salt);
}

#[test]
fn test_getter_functions() {
let env = create_test_env();
let contract_id = env.register(TbaAccount, ());
let client = TbaAccountClient::new(&env, &contract_id);

let nft_contract = Address::generate(&env);
let token_id = 42u128;
let impl_hash = BytesN::from_array(&env, &[1u8; 32]);
let salt = BytesN::from_array(&env, &[2u8; 32]);

client.initialize(&nft_contract, &token_id, &impl_hash, &salt);

// Test getters
assert_eq!(client.token_contract(), nft_contract);
assert_eq!(client.token_id(), token_id);

let (chain_id, contract_addr, id) = client.token();
assert_eq!(chain_id, 0u32); // We set it to 0
assert_eq!(contract_addr, nft_contract);
assert_eq!(id, token_id);
}

#[test]
fn test_nonce_initial_value() {
let env = create_test_env();
let contract_id = env.register(TbaAccount, ());
let client = TbaAccountClient::new(&env, &contract_id);

let nft_contract = Address::generate(&env);
let token_id = 1u128;
let impl_hash = BytesN::from_array(&env, &[1u8; 32]);
let salt = BytesN::from_array(&env, &[2u8; 32]);

client.initialize(&nft_contract, &token_id, &impl_hash, &salt);

// Nonce should start at 0
assert_eq!(client.nonce(), 0u64);
}

#[test]
#[should_panic(expected = "Contract not initialized")]
fn test_execute_before_initialization_panics() {
let env = create_test_env();
let contract_id = env.register(TbaAccount, ());
let client = TbaAccountClient::new(&env, &contract_id);

let target = Address::generate(&env);
let func = Symbol::new(&env, "test_func");
let args = vec![&env];

// Execute should panic if not initialized
client.execute(&target, &func, &args);
}

// Note: Testing execute with actual authorization requires more complex setup
// with proper NFT contract integration and auth simulation.
// These tests verify the basic structure and initialization.

Loading
Loading