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
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"migrate:status": "prisma migrate status",
"migrate:validate-env": "node scripts/validate-migration-env.js",
"migrate:bootstrap": "bash scripts/bootstrap-db.sh",
"clean": "rm -rf logs coverage && find . -name '*.db-journal' -delete && find . -name '*.sqlite-journal' -delete"
"clean": "node -e \"const fs=require('fs'),path=require('path');function walk(dir){if(!fs.existsSync(dir))return;for(const f of fs.readdirSync(dir)){const fp=path.join(dir,f);if(fs.statSync(fp).isDirectory())walk(fp);else if(['.db-journal','.db-wal','.db-shm'].some(e=>f.endsWith(e))){fs.unlinkSync(fp);console.log('Removed:',fp)}}};walk('.');['logs','coverage'].forEach(d=>{if(fs.existsSync(d)){fs.rmSync(d,{recursive:true,force:true});console.log('Removed:',d)}});console.log('Clean complete');\""
},
"dependencies": {
Expand Down
4 changes: 4 additions & 0 deletions backend/src/api/controllers/sep12.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ export class Sep12Controller {
return res.status(400).json({ error: 'upload_id and account are required' });
}

if (req.user && req.user.publicKey !== account) {
return res.status(403).json({ error: 'Forbidden: session account does not match request account' });
}

const record = uploadStore.get(upload_id);

if (!record || record.status === 'EXPIRED') {
Expand Down
83 changes: 83 additions & 0 deletions contracts/revenue_distributor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,27 @@ mod tests {
}

#[test]
fn test_zero_balance_distribute() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let treasury = Address::generate(&env);
let gov_stakers = Address::generate(&env);

let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(token_admin.clone());
let token_client = token::Client::new(&env, &token_id.address());

let distributor_id = env.register_contract(None, RevenueDistributor);
let distributor_client = RevenueDistributorClient::new(&env, &distributor_id);
distributor_client.initialize(&admin, &treasury, &gov_stakers, &6000);

// Distributor has no balance — distribute should be a no-op
distributor_client.distribute(&token_id.address());

assert_eq!(token_client.balance(&gov_stakers), 0);
assert_eq!(token_client.balance(&treasury), 0);
fn test_zero_balance_distribute_is_noop() {
let env = Env::default();
env.mock_all_auths();
Expand All @@ -183,6 +204,26 @@ mod tests {
fn test_full_gov_share() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let treasury = Address::generate(&env);
let gov_stakers = Address::generate(&env);

let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(token_admin.clone());
let token_client = token::Client::new(&env, &token_id.address());

let distributor_id = env.register_contract(None, RevenueDistributor);
let distributor_client = RevenueDistributorClient::new(&env, &distributor_id);
// 100% to gov_stakers, 0% to treasury
distributor_client.initialize(&admin, &treasury, &gov_stakers, &10000);

token::StellarAssetClient::new(&env, &token_id.address()).mint(&distributor_id, &1000);
distributor_client.distribute(&token_id.address());

assert_eq!(token_client.balance(&gov_stakers), 1000);
assert_eq!(token_client.balance(&treasury), 0);
assert_eq!(token_client.balance(&distributor_id), 0);
let admin = Address::generate(&env);
let treasury = Address::generate(&env);
let gov_stakers = Address::generate(&env);
Expand All @@ -205,6 +246,33 @@ mod tests {
fn test_zero_gov_share() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let treasury = Address::generate(&env);
let gov_stakers = Address::generate(&env);

let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(token_admin.clone());
let token_client = token::Client::new(&env, &token_id.address());

let distributor_id = env.register_contract(None, RevenueDistributor);
let distributor_client = RevenueDistributorClient::new(&env, &distributor_id);
// 0% to gov_stakers, 100% to treasury
distributor_client.initialize(&admin, &treasury, &gov_stakers, &0);

token::StellarAssetClient::new(&env, &token_id.address()).mint(&distributor_id, &1000);
distributor_client.distribute(&token_id.address());

assert_eq!(token_client.balance(&gov_stakers), 0);
assert_eq!(token_client.balance(&treasury), 1000);
assert_eq!(token_client.balance(&distributor_id), 0);
}

#[test]
fn test_weight_precision_with_odd_amounts() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let treasury = Address::generate(&env);
let gov_stakers = Address::generate(&env);
Expand Down Expand Up @@ -245,6 +313,21 @@ mod tests {
let treasury = Address::generate(&env);
let gov_stakers = Address::generate(&env);

let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(token_admin.clone());
let token_client = token::Client::new(&env, &token_id.address());

let distributor_id = env.register_contract(None, RevenueDistributor);
let distributor_client = RevenueDistributorClient::new(&env, &distributor_id);
// 30% gov, 70% treasury
distributor_client.initialize(&admin, &treasury, &gov_stakers, &3000);

token::StellarAssetClient::new(&env, &token_id.address()).mint(&distributor_id, &1000);
distributor_client.distribute(&token_id.address());

assert_eq!(token_client.balance(&gov_stakers), 300);
assert_eq!(token_client.balance(&treasury), 700);
assert_eq!(token_client.balance(&distributor_id), 0);
let distributor_id = env.register_contract(None, RevenueDistributor);
let client = RevenueDistributorClient::new(&env, &distributor_id);
client.initialize(&admin, &treasury, &gov_stakers, &10001);
Expand Down
13 changes: 10 additions & 3 deletions contracts/yield/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ impl YieldDistribution {
.get(&DataKey::TotalStaked)
.unwrap_or(0);

// Update state before external token transfer (reentrancy guard pattern).
// If nobody is staking yet, rewards accumulate but can't be distributed —
// they will be claimable once the first stake occurs (reward_per_token
// stays 0 until then, so the deposited tokens sit idle).
let reward_token: Address = env.storage().instance().get(&DataKey::RewardToken).unwrap();

// CEI: update state before external token transfer
Expand All @@ -116,13 +120,16 @@ impl YieldDistribution {
.set(&DataKey::RewardPerTokenStored, &rpt);
}

// Transfer reward tokens into the contract after state is updated
let reward_token: Address = env.storage().instance().get(&DataKey::RewardToken).unwrap();
// External interaction last
token::Client::new(&env, &reward_token).transfer(
&from,
&env.current_contract_address(),
&amount,
);

// Topic: event name only; from + amount in data.
env.events()
.publish((symbol_short!("dep_rwd"),), (from, amount));
}
Expand All @@ -145,6 +152,7 @@ impl YieldDistribution {
// Settle any pending rewards before changing the stake
Self::_update_reward(&env, &user);

// Update state before external token transfer (reentrancy guard pattern)
let stake_token: Address = env.storage().instance().get(&DataKey::StakeToken).unwrap();

// CEI: update state before external token transfer
Expand All @@ -162,13 +170,15 @@ impl YieldDistribution {
.instance()
.set(&DataKey::TotalStaked, &total.checked_add(amount).expect("total staked overflow"));

let stake_token: Address = env.storage().instance().get(&DataKey::StakeToken).unwrap();
// External interaction last
token::Client::new(&env, &stake_token).transfer(
&user,
&env.current_contract_address(),
&amount,
);

// Topic: event name only; user + amount in data.
env.events()
.publish((symbol_short!("staked"),), (user, amount));
}
Expand Down Expand Up @@ -251,11 +261,8 @@ impl YieldDistribution {
);

env.events()
<<<<<<< HEAD
.publish(symbol_short!("claimed"), (user, reward));
=======
.publish((symbol_short!("claimed"),), (user, reward));
>>>>>>> upstream/main
}

reward
Expand Down