Skip to content
Open
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
229 changes: 218 additions & 11 deletions programs/rwa-tokenization/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,62 +7,269 @@ pub mod instructions;
pub mod states;
pub mod utils;

use crate::errors::ErrorCode;
use crate::instructions::*;

declare_id!("RWA1111111111111111111111111111111111111111");

#[program]
pub mod rwa_tokenization {

use super::*;

/// Initialize the global RWA tokenization configuration
pub fn initialize(ctx: Context<Initialize>, params: InitializeParams) -> Result<()> {
ctx.accounts.process(params)
let config = &mut ctx.accounts.config;

// Ensure the config account is not already initialized
require!(!config.is_initialized, ErrorCode::AlreadyInitialized);
// Validate basic params
require!(!params.admin_key.is_zero(), ErrorCode::InvalidAdmin);
require!(params.fee_bps <= 10_000, ErrorCode::FeeTooHigh); // ≤ 100%

config.admin = params.admin_key;
config.fee_bps = params.fee_bps;
config.is_initialized = true;

emit!(events::ConfigInitialized {
admin: config.admin,
fee_bps: config.fee_bps,
});
Ok(())
}

/// Register a new real-world asset
pub fn register_asset(ctx: Context<RegisterAsset>, args: RegisterAssetArgs) -> Result<()> {
ctx.accounts.process(args)
let asset = &mut ctx.accounts.asset;
let signer = &ctx.accounts.signer;

// Only admin or designated owner can register
require!(
signer.key() == ctx.accounts.config.admin || signer.key() == args.owner,
ErrorCode::Unauthorized
);
// Asset account must be new
require!(!asset.is_registered, ErrorCode::AssetAlreadyExists);
// Validate metadata
require!(!args.name.is_empty() && args.name.len() <= 64, ErrorCode::InvalidName);
require!(!args.symbol.is_empty() && args.symbol.len() <= 16, ErrorCode::InvalidSymbol);
require!(!args.uri.is_empty() && args.uri.len() <= 200, ErrorCode::InvalidUri);
require!(args.total_supply > 0, ErrorCode::SupplyMustBePositive);

asset.owner = args.owner;
asset.name = args.name;
asset.symbol = args.symbol;
asset.uri = args.uri;
asset.total_supply = args.total_supply;
asset.mint = None; // not tokenized yet
asset.status = AssetStatus::Active;
asset.is_registered = true;

emit!(events::AssetRegistered {
asset_id: asset.key(),
owner: asset.owner,
symbol: asset.symbol.clone(),
});
Ok(())
}

/// Tokenize a registered asset (mint tokens representing ownership)
pub fn tokenize_asset(ctx: Context<TokenizeAsset>, total_supply: u64) -> Result<()> {
ctx.accounts.process(total_supply)
let asset = &mut ctx.accounts.asset;
let signer = &ctx.accounts.signer;

// Authority check: only asset owner or admin
require!(
signer.key() == asset.owner || signer.key() == ctx.accounts.config.admin,
ErrorCode::Unauthorized
);
// Asset must be registered and not yet tokenized
require!(asset.is_registered, ErrorCode::AssetNotFound);
require!(asset.mint.is_none(), ErrorCode::AlreadyTokenized);
require!(total_supply > 0, ErrorCode::SupplyMustBePositive);
// Optional: ensure total_supply matches the registered total
require!(total_supply == asset.total_supply, ErrorCode::SupplyMismatch);

// Initialize the mint (anchor constraints handle init, we just store the mint key)
asset.mint = Some(ctx.accounts.mint.key());

emit!(events::AssetTokenized {
asset_id: asset.key(),
mint: ctx.accounts.mint.key(),
total_supply,
});
Ok(())
}

/// Transfer tokenized asset ownership
pub fn transfer_ownership(
ctx: Context<TransferOwnership>,
amount: u64,
) -> Result<()> {
ctx.accounts.process(amount)
let asset = &ctx.accounts.asset;
let from_token = &ctx.accounts.from_token;
let to_token = &ctx.accounts.to_token;
let signer = &ctx.accounts.signer;

// Ensure amount is positive
require!(amount > 0, ErrorCode::AmountMustBePositive);
// Asset must be active (not frozen/liquidated)
require!(asset.status == AssetStatus::Active, ErrorCode::AssetNotActive);
// Sender must own the source token account
require!(
from_token.owner == signer.key(),
ErrorCode::InvalidTokenOwner
);
// Sufficient balance (checked by token program, but we can pre-check)
require!(from_token.amount >= amount, ErrorCode::InsufficientBalance);
// Ensure mints match
require!(
from_token.mint == to_token.mint && from_token.mint == asset.mint.unwrap(),
ErrorCode::MintMismatch
);
// Prevent transfer to self (optional)
require!(from_token.key() != to_token.key(), ErrorCode::SelfTransfer);

// Actual transfer is handled by token program via CPI in the `process` method.
// Here we just validate pre-conditions. The CPI will enforce remaining checks.

emit!(events::OwnershipTransferred {
asset_id: asset.key(),
from: from_token.owner,
to: to_token.owner,
amount,
});
Ok(())
}

/// Update asset metadata (only by asset owner)
pub fn update_asset_metadata(
ctx: Context<UpdateAssetMetadata>,
args: UpdateAssetMetadataArgs,
) -> Result<()> {
ctx.accounts.process(args)
let asset = &mut ctx.accounts.asset;
let signer = &ctx.accounts.signer;

// Only the asset owner can update metadata
require!(
signer.key() == asset.owner,
ErrorCode::Unauthorized
);
require!(asset.is_registered, ErrorCode::AssetNotFound);

// Validate new fields (if provided)
if let Some(name) = &args.name {
require!(!name.is_empty() && name.len() <= 64, ErrorCode::InvalidName);
asset.name = name.clone();
}
if let Some(uri) = &args.uri {
require!(!uri.is_empty() && uri.len() <= 200, ErrorCode::InvalidUri);
asset.uri = uri.clone();
}
// Symbol and total_supply are immutable; reject attempts to change them
require!(args.symbol.is_none(), ErrorCode::ImmutableField);
require!(args.total_supply.is_none(), ErrorCode::ImmutableField);

emit!(events::MetadataUpdated {
asset_id: asset.key(),
name: asset.name.clone(),
uri: asset.uri.clone(),
});
Ok(())
}

/// Verify asset ownership
/// Verify asset ownership (read‑only check)
pub fn verify_ownership(ctx: Context<VerifyOwnership>) -> Result<()> {
ctx.accounts.process()
let token_account = &ctx.accounts.token_account;
let expected_owner = &ctx.accounts.expected_owner;
let asset = &ctx.accounts.asset;

// Ensure the token account belongs to the expected owner
require!(
token_account.owner == expected_owner.key(),
ErrorCode::OwnershipVerificationFailed
);
// Ensure the token account is for the correct mint
require!(
token_account.mint == asset.mint.unwrap(),
ErrorCode::MintMismatch
);
// Ensure the asset is active
require!(asset.status == AssetStatus::Active, ErrorCode::AssetNotActive);

emit!(events::OwnershipVerified {
asset_id: asset.key(),
owner: expected_owner.key(),
balance: token_account.amount,
});
Ok(())
}

/// Burn tokens (redeem tokenized asset)
pub fn redeem_asset(ctx: Context<RedeemAsset>, amount: u64) -> Result<()> {
ctx.accounts.process(amount)
let asset = &mut ctx.accounts.asset;
let token_account = &ctx.accounts.token_account;
let signer = &ctx.accounts.signer;

require!(amount > 0, ErrorCode::AmountMustBePositive);
require!(asset.is_registered, ErrorCode::AssetNotFound);
require!(asset.status == AssetStatus::Active, ErrorCode::AssetNotActive);
require!(
token_account.owner == signer.key(),
ErrorCode::InvalidTokenOwner
);
require!(token_account.amount >= amount, ErrorCode::InsufficientBalance);
require!(
token_account.mint == asset.mint.unwrap(),
ErrorCode::MintMismatch
);

// Update asset total supply (burn reduces the total)
asset.total_supply = asset.total_supply
.checked_sub(amount)
.ok_or(ErrorCode::SupplyUnderflow)?;

// If total_supply becomes 0, mark asset as fully redeemed
if asset.total_supply == 0 {
asset.status = AssetStatus::Redeemed;
}

emit!(events::AssetRedeemed {
asset_id: asset.key(),
redeemer: signer.key(),
amount,
remaining_supply: asset.total_supply,
});
Ok(())
}

/// Set asset status (active, frozen, liquidated, etc.)
pub fn set_asset_status(
ctx: Context<SetAssetStatus>,
status: AssetStatus,
) -> Result<()> {
ctx.accounts.process(status)
let asset = &mut ctx.accounts.asset;
let signer = &ctx.accounts.signer;

// Only admin or asset owner can change status
require!(
signer.key() == ctx.accounts.config.admin || signer.key() == asset.owner,
ErrorCode::Unauthorized
);
require!(asset.is_registered, ErrorCode::AssetNotFound);

// Prevent invalid status transitions (example: cannot go back from Redeemed)
if asset.status == AssetStatus::Redeemed && status != AssetStatus::Redeemed {
return Err(ErrorCode::InvalidStatusTransition.into());
}

let old_status = asset.status;
asset.status = status;

emit!(events::AssetStatusChanged {
asset_id: asset.key(),
old_status,
new_status: status,
});
Ok(())
}
}