Skip to content

Commit 15d0d3e

Browse files
broodyclaude
andcommitted
fix(starterpack): skip self-referral to prevent fee leakage
When the Controller passes the buyer's own address as the referrer, the referral fee was deducted from the payment_receiver's share but stayed on the buyer's account (self-transfer). This caused the game owner to receive less than expected. Skip referral fee when ref_addr == payer so the owner gets the full base_price. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c0355bc commit 15d0d3e

File tree

2 files changed

+93
-25
lines changed

2 files changed

+93
-25
lines changed

packages/starterpack/src/components/issuable.cairo

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -76,35 +76,40 @@ pub mod IssuableComponent {
7676
if base_price != 0 {
7777
let token_dispatcher = IERC20Dispatcher { contract_address: payment_token };
7878

79-
// Calculate referral fee if referrer exists (included in base price)
79+
// Calculate referral fee if referrer exists and is not the payer
8080
let referral_fee_amount = if let Option::Some(ref_addr) = referrer {
81-
let ref_fee = base_price
82-
* starterpack.referral_percentage.into()
83-
/ FEE_DENOMINATOR.into();
84-
85-
// Transfer referral fee
86-
if ref_fee > 0 {
87-
token_dispatcher.transfer_from(payer, ref_addr, ref_fee);
88-
89-
// Track referral reward for individual referrer
90-
let mut referral_reward = store.get_referral_reward(ref_addr);
91-
if referral_reward.total_referrals == 0 {
92-
referral_reward = ReferralRewardTrait::new(ref_addr);
93-
}
94-
referral_reward.add_referral(ref_fee);
95-
store.set_referral_reward(@referral_reward);
96-
97-
// Track group reward if referrer_group exists
98-
if let Option::Some(group_id) = referrer_group {
99-
let mut group_reward = store.get_group_reward(group_id);
100-
if group_reward.total_referrals == 0 {
101-
group_reward = GroupRewardTrait::new(group_id);
81+
if ref_addr == payer {
82+
// Skip self-referral
83+
0
84+
} else {
85+
let ref_fee = base_price
86+
* starterpack.referral_percentage.into()
87+
/ FEE_DENOMINATOR.into();
88+
89+
// Transfer referral fee
90+
if ref_fee > 0 {
91+
token_dispatcher.transfer_from(payer, ref_addr, ref_fee);
92+
93+
// Track referral reward for individual referrer
94+
let mut referral_reward = store.get_referral_reward(ref_addr);
95+
if referral_reward.total_referrals == 0 {
96+
referral_reward = ReferralRewardTrait::new(ref_addr);
97+
}
98+
referral_reward.add_referral(ref_fee);
99+
store.set_referral_reward(@referral_reward);
100+
101+
// Track group reward if referrer_group exists
102+
if let Option::Some(group_id) = referrer_group {
103+
let mut group_reward = store.get_group_reward(group_id);
104+
if group_reward.total_referrals == 0 {
105+
group_reward = GroupRewardTrait::new(group_id);
106+
}
107+
group_reward.add_referral(ref_fee);
108+
store.set_group_reward(@group_reward);
102109
}
103-
group_reward.add_referral(ref_fee);
104-
store.set_group_reward(@group_reward);
105110
}
111+
ref_fee
106112
}
107-
ref_fee
108113
} else {
109114
0
110115
};

packages/starterpack/src/tests/test_fees.cairo

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,66 @@ fn test_sp_free() {
237237
// [Assert] No payment made
238238
assert_eq!(systems.erc20.balance_of(context.spender), spender_initial, "No payment for free");
239239
}
240+
241+
#[test]
242+
fn test_sp_fees_self_referral_ignored() {
243+
// [Setup]
244+
let (_world, systems, context) = spawn();
245+
246+
// [Initialize]
247+
testing::set_contract_address(OWNER());
248+
testing::set_block_timestamp(1);
249+
250+
// [Register]
251+
testing::set_contract_address(context.creator);
252+
let metadata = METADATA();
253+
let starterpack_id = systems
254+
.starterpack
255+
.register(
256+
implementation: systems.starterpack_impl,
257+
referral_percentage: REFERRAL_PERCENTAGE,
258+
reissuable: false,
259+
price: PRICE,
260+
payment_token: systems.erc20.contract_address,
261+
payment_receiver: Option::None,
262+
metadata: metadata,
263+
);
264+
265+
// [Record] Initial balances
266+
let spender_initial = systems.erc20.balance_of(context.spender);
267+
let creator_initial = systems.erc20.balance_of(context.creator);
268+
let receiver_initial = systems.erc20.balance_of(context.receiver);
269+
270+
// [Issue] With self as referrer (spender == referrer)
271+
testing::set_contract_address(context.spender);
272+
let protocol_fee_amount = PRICE * PROTOCOL_FEE.into() / FEE_DENOMINATOR.into();
273+
let total_cost = PRICE + protocol_fee_amount;
274+
systems.erc20.approve(systems.starterpack.contract_address, total_cost);
275+
systems
276+
.starterpack
277+
.issue(
278+
recipient: PLAYER(),
279+
starterpack_id: starterpack_id,
280+
quantity: 1,
281+
referrer: Option::Some(context.spender), // self-referral
282+
referrer_group: Option::None,
283+
);
284+
285+
// [Assert] Self-referral is ignored, behaves like no referrer
286+
// Spender paid: base price + protocol fee
287+
assert_eq!(
288+
systems.erc20.balance_of(context.spender), spender_initial - total_cost, "Spender balance",
289+
);
290+
291+
// Creator received: full base price (no referral deducted)
292+
assert_eq!(
293+
systems.erc20.balance_of(context.creator), creator_initial + PRICE, "Creator balance",
294+
);
295+
296+
// Protocol receiver got: protocol fee only
297+
assert_eq!(
298+
systems.erc20.balance_of(context.receiver),
299+
receiver_initial + protocol_fee_amount,
300+
"Protocol receiver balance",
301+
);
302+
}

0 commit comments

Comments
 (0)