Skip to content

Commit 8abbbe3

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 8abbbe3

File tree

2 files changed

+69
-1
lines changed

2 files changed

+69
-1
lines changed

packages/starterpack/src/components/issuable.cairo

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,12 @@ 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+
if ref_addr == payer {
82+
// Skip self-referral
83+
0
84+
} else {
8185
let ref_fee = base_price
8286
* starterpack.referral_percentage.into()
8387
/ FEE_DENOMINATOR.into();
@@ -105,6 +109,7 @@ pub mod IssuableComponent {
105109
}
106110
}
107111
ref_fee
112+
}
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)