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
65 changes: 64 additions & 1 deletion contracts/chainmove-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum ContractError {
PoolAlreadyExists = 2,
PoolNotFound = 3,
InvestorPositionNotFound = 4,
Oversubscribed = 5,
PoolInactive = 6,
}

#[contracttype]
Expand All @@ -21,6 +23,7 @@ pub struct Pool {
pub id: u64,
pub owner: Address,
pub asset_label: String,
pub total_units: u64,
pub target_amount: i128,
pub total_invested: i128,
pub total_repaid: i128,
Expand Down Expand Up @@ -50,11 +53,12 @@ impl ChainMovePoolContract {
owner: Address,
pool_id: u64,
asset_label: String,
total_units: u64,
target_amount: i128,
) -> Result<Pool, ContractError> {
owner.require_auth();

if pool_id == 0 || target_amount <= 0 || asset_label.is_empty() {
if pool_id == 0 || total_units == 0 || target_amount <= 0 || asset_label.is_empty() {
return Err(ContractError::InvalidInput);
}

Expand All @@ -67,6 +71,7 @@ impl ChainMovePoolContract {
id: pool_id,
owner,
asset_label,
total_units,
target_amount,
total_invested: 0,
total_repaid: 0,
Expand Down Expand Up @@ -97,6 +102,14 @@ impl ChainMovePoolContract {
.get(&pool_key)
.ok_or(ContractError::PoolNotFound)?;

if !pool.active {
return Err(ContractError::PoolInactive);
}

if pool.total_invested + amount > pool.target_amount {
return Err(ContractError::Oversubscribed);
}

pool.total_invested += amount;
env.storage().persistent().set(&pool_key, &pool);

Expand Down Expand Up @@ -154,6 +167,56 @@ impl ChainMovePoolContract {
Ok(position)
}

/// Returns investor share in basis points (bps): invested * 10_000 / target_amount.
pub fn get_investor_share(
env: Env,
investor: Address,
pool_id: u64,
) -> Result<u64, ContractError> {
if pool_id == 0 {
return Err(ContractError::InvalidInput);
}

let pool: Pool = env
.storage()
.persistent()
.get(&DataKey::Pool(pool_id))
.ok_or(ContractError::PoolNotFound)?;

let position: InvestorPosition = env
.storage()
.persistent()
.get(&DataKey::InvestorPosition(pool_id, investor))
.ok_or(ContractError::InvestorPositionNotFound)?;

if pool.target_amount == 0 {
return Err(ContractError::InvalidInput);
}

let share_bps = (position.invested * 10_000 / pool.target_amount) as u64;
Ok(share_bps)
}

/// Marks a pool inactive so no further investments are accepted.
pub fn close_pool(env: Env, owner: Address, pool_id: u64) -> Result<Pool, ContractError> {
owner.require_auth();

if pool_id == 0 {
return Err(ContractError::InvalidInput);
}

let key = DataKey::Pool(pool_id);
let mut pool: Pool = env
.storage()
.persistent()
.get(&key)
.ok_or(ContractError::PoolNotFound)?;

pool.active = false;
env.storage().persistent().set(&key, &pool);
Ok(pool)
}

pub fn read_pool(env: Env, pool_id: u64) -> Result<Pool, ContractError> {
if pool_id == 0 {
return Err(ContractError::InvalidInput);
Expand Down
100 changes: 97 additions & 3 deletions contracts/chainmove-pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ fn setup_pool(env: &Env, client: &ChainMovePoolContractClient<'_>) -> (Address,
let asset_label = String::from_str(env, "testnet-van-01");

let pool = client
.try_create_pool(&owner, &1, &asset_label, &10_000)
.try_create_pool(&owner, &1, &asset_label, &100, &10_000)
.unwrap()
.unwrap();

assert_eq!(pool.id, 1);
assert_eq!(pool.owner, owner);
assert_eq!(pool.asset_label, asset_label);
assert_eq!(pool.total_units, 100);
assert_eq!(pool.target_amount, 10_000);
assert_eq!(pool.total_invested, 0);
assert_eq!(pool.total_repaid, 0);
Expand All @@ -40,6 +41,7 @@ fn creates_pool_and_reads_pool_data() {

assert_eq!(pool.id, 1);
assert_eq!(pool.owner, owner);
assert_eq!(pool.total_units, 100);
assert_eq!(pool.total_invested, 0);
assert_eq!(pool.total_repaid, 0);
}
Expand Down Expand Up @@ -102,17 +104,109 @@ fn rejects_invalid_input() {
let investor = Address::generate(&env);
let asset_label = String::from_str(&env, "testnet-van-01");

let invalid_pool = client.try_create_pool(&owner, &0, &asset_label, &10_000);
// pool_id = 0 is invalid
let invalid_pool = client.try_create_pool(&owner, &0, &asset_label, &100, &10_000);
assert!(invalid_pool.is_err());

// total_units = 0 is invalid
let invalid_units = client.try_create_pool(&owner, &1, &asset_label, &0, &10_000);
assert!(invalid_units.is_err());

client
.try_create_pool(&owner, &1, &asset_label, &10_000)
.try_create_pool(&owner, &1, &asset_label, &100, &10_000)
.unwrap()
.unwrap();

// amount = 0 is invalid
let invalid_investment = client.try_record_investment(&investor, &1, &0);
assert!(invalid_investment.is_err());

let missing_position_repayment = client.try_record_repayment(&owner, &1, &investor, &100);
assert!(missing_position_repayment.is_err());
}

#[test]
fn rejects_oversubscription() {
let env = Env::default();
env.mock_all_auths();
let client = create_client(&env);
let (_, investor) = setup_pool(&env, &client);

// invest 8_000 of 10_000 target — succeeds
client
.try_record_investment(&investor, &1, &8_000)
.unwrap()
.unwrap();

// attempt to invest 3_000 more would exceed target_amount — must fail
let oversubscribed = client.try_record_investment(&investor, &1, &3_000);
assert!(oversubscribed.is_err());

// investing exactly the remaining 2_000 — succeeds
client
.try_record_investment(&investor, &1, &2_000)
.unwrap()
.unwrap();

let pool = client.try_read_pool(&1).unwrap().unwrap();
assert_eq!(pool.total_invested, 10_000);
}

#[test]
fn updates_duplicate_investor_position() {
let env = Env::default();
env.mock_all_auths();
let client = create_client(&env);
let (_, investor) = setup_pool(&env, &client);

// first investment
client
.try_record_investment(&investor, &1, &1_000)
.unwrap()
.unwrap();

// second investment by same investor — should accumulate
let position = client
.try_record_investment(&investor, &1, &500)
.unwrap()
.unwrap();

assert_eq!(position.invested, 1_500);

let pool = client.try_read_pool(&1).unwrap().unwrap();
assert_eq!(pool.total_invested, 1_500);
}

#[test]
fn calculates_investor_share_in_bps() {
let env = Env::default();
env.mock_all_auths();
let client = create_client(&env);
let (_, investor) = setup_pool(&env, &client);

// invest 2_500 out of 10_000 target => 25% => 2500 bps
client
.try_record_investment(&investor, &1, &2_500)
.unwrap()
.unwrap();

let share_bps = client
.try_get_investor_share(&investor, &1)
.unwrap()
.unwrap();

assert_eq!(share_bps, 2_500);
}

#[test]
fn rejects_investment_into_closed_pool() {
let env = Env::default();
env.mock_all_auths();
let client = create_client(&env);
let (owner, investor) = setup_pool(&env, &client);

client.try_close_pool(&owner, &1).unwrap().unwrap();

let result = client.try_record_investment(&investor, &1, &1_000);
assert!(result.is_err());
}
Loading