Skip to content

Commit 37ddf10

Browse files
Merge pull request #1077 from jonathanayubausara-a11y/fix/dex-competition-reward-escrow
fix(dex): escrow trading competition rewards before they can be claimed
2 parents ac3b136 + 1fcb437 commit 37ddf10

2 files changed

Lines changed: 98 additions & 6 deletions

File tree

contracts/dex/src/lib.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ pub mod dex {
198198
competition_scores: Mapping<(u64, AccountId), u128>,
199199
competition_participants: Mapping<u64, Vec<AccountId>>,
200200
competition_claimed: Mapping<(u64, AccountId), bool>,
201+
/// Native value actually escrowed for each trading competition's reward
202+
/// pool (competition_id -> amount). Claims may only draw from this
203+
/// balance, never from nothing (Issue #1022).
204+
competition_reward_escrow: Mapping<u64, u128>,
201205
admin_timelock_delay: u64,
202206
pending_admin_actions: Mapping<u64, PendingAdminAction>,
203207
pending_admin_action_counter: u64,
@@ -249,6 +253,7 @@ pub mod dex {
249253
competition_scores: Mapping::default(),
250254
competition_participants: Mapping::default(),
251255
competition_claimed: Mapping::default(),
256+
competition_reward_escrow: Mapping::default(),
252257
admin_timelock_delay: 0,
253258
pending_admin_actions: Mapping::default(),
254259
pending_admin_action_counter: 0,
@@ -997,8 +1002,17 @@ pub mod dex {
9971002
Ok(())
9981003
}
9991004

1000-
/// Create a new trading competition. Admin only. Sets start/end timestamps, reward pool, and participant cap.
1001-
#[ink(message)]
1005+
/// Creates a new trading competition. Only the admin may call.
1006+
///
1007+
/// # Reward escrow (Issue #1022)
1008+
///
1009+
/// The competition reward must be **funded up front**: the caller must
1010+
/// attach native value at least equal to `reward_amount`. The attached
1011+
/// value is escrowed in `competition_reward_escrow` and the governance
1012+
/// token supply is minted once, by exactly the escrowed amount, so
1013+
/// `claim_competition_reward` can never credit more than the value
1014+
/// actually held for the competition.
1015+
#[ink(message, payable)]
10021016
pub fn create_trading_competition(
10031017
&mut self,
10041018
pair_id: Option<u64>,
@@ -1016,6 +1030,9 @@ pub mod dex {
10161030
if title.is_empty() || start_block >= end_block || reward_amount == 0 {
10171031
return Err(Error::InvalidRequest);
10181032
}
1033+
if self.env().transferred_value() < reward_amount {
1034+
return Err(Error::InvalidRequest);
1035+
}
10191036
self.trade_competition_counter = self.trade_competition_counter.saturating_add(1);
10201037
let competition_id = self.trade_competition_counter;
10211038
let competition = TradingCompetition {
@@ -1034,6 +1051,15 @@ pub mod dex {
10341051
.insert(competition_id, &competition);
10351052
self.competition_participants
10361053
.insert(competition_id, &Vec::<AccountId>::new());
1054+
1055+
// Escrow the full announced reward and mint the governance supply
1056+
// once, backed by the attached value. Claims later redistribute
1057+
// this escrow without touching `total_supply` again.
1058+
self.competition_reward_escrow
1059+
.insert(competition_id, &reward_amount);
1060+
self.governance_config.total_supply =
1061+
self.governance_config.total_supply.saturating_add(reward_amount);
1062+
10371063
self.env().emit_event(TradingCompetitionCreated {
10381064
competition_id,
10391065
pair_id,
@@ -1057,7 +1083,10 @@ pub mod dex {
10571083
.unwrap_or(0)
10581084
}
10591085

1060-
/// Claim reward tokens for a completed competition. Requires the competition to be finished.
1086+
/// Claim reward tokens for a completed competition. Requires the
1087+
/// competition to be finished and its reward pool to have been funded
1088+
/// (escrowed) at creation. Claims are paid pro rata from the escrow;
1089+
/// an unfunded competition yields `RewardUnavailable` (Issue #1022).
10611090
#[ink(message)]
10621091
pub fn claim_competition_reward(&mut self, competition_id: u64) -> Result<u128, Error> {
10631092
let competition = self
@@ -1110,13 +1139,27 @@ pub mod dex {
11101139
return Err(Error::InvalidRequest);
11111140
}
11121141

1142+
// The reward pool must actually hold value: claims may only draw
1143+
// from the escrow funded at creation (Issue #1022). The pro-rata
1144+
// shares of all participants never exceed the escrowed amount, so
1145+
// the cap is purely defensive against partially-funded legacy
1146+
// competitions.
1147+
let escrowed = self.competition_reward_escrow.get(competition_id).unwrap_or(0);
1148+
if escrowed == 0 {
1149+
return Err(Error::RewardUnavailable);
1150+
}
1151+
let reward = reward.min(escrowed);
1152+
1153+
self.competition_reward_escrow
1154+
.insert(competition_id, &escrowed.saturating_sub(reward));
11131155
self.competition_claimed
11141156
.insert((competition_id, caller), &true);
11151157
let balance = self.governance_balances.get(caller).unwrap_or(0);
11161158
self.governance_balances
11171159
.insert(caller, &balance.saturating_add(reward));
1118-
self.governance_config.total_supply =
1119-
self.governance_config.total_supply.saturating_add(reward);
1160+
// `total_supply` is not increased here: it was minted once when
1161+
// the competition was funded, so supply grows only by the escrowed
1162+
// amount across the whole competition lifecycle.
11201163
self.env().emit_event(CompetitionRewardClaimed {
11211164
competition_id,
11221165
trader: caller,
@@ -1551,6 +1594,13 @@ pub mod dex {
15511594
self.governance_balances.get(trader).unwrap_or(0)
15521595
}
15531596

1597+
/// Return the amount of value still escrowed for a competition's reward
1598+
/// pool (0 if it was never funded or has been fully claimed).
1599+
#[ink(message)]
1600+
pub fn get_competition_reward_escrow(&self, competition_id: u64) -> u128 {
1601+
self.competition_reward_escrow.get(competition_id).unwrap_or(0)
1602+
}
1603+
15541604
/// Return the configurable settings for a competition.
15551605
#[ink(message)]
15561606
pub fn get_competition_settings(&self, competition_id: u64) -> Option<(u32, u128)> {

contracts/dex/src/tests.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,8 +776,12 @@ mod tests {
776776
let accounts = test::default_accounts::<DefaultEnvironment>();
777777

778778
test::set_block_number::<DefaultEnvironment>(100);
779+
let supply_before = dex.governance_config.total_supply;
779780

780-
// Admin creates a competition on pair 1 running blocks 100..110.
781+
// Admin creates a competition on pair 1 running blocks 100..110. The
782+
// reward must be funded up front (Issue #1022): the attached value is
783+
// escrowed and the governance supply is minted once, by that amount.
784+
test::set_value_transferred::<DefaultEnvironment>(10_000);
781785
let competition_id = dex
782786
.create_trading_competition(
783787
Some(pair_id),
@@ -791,6 +795,13 @@ mod tests {
791795
)
792796
.expect("competition creation should work");
793797

798+
assert_eq!(dex.get_competition_reward_escrow(competition_id), 10_000);
799+
assert_eq!(
800+
dex.governance_config.total_supply,
801+
supply_before + 10_000,
802+
"supply is minted once, by exactly the escrowed amount"
803+
);
804+
794805
let competition = dex.get_trading_competition(competition_id).unwrap();
795806
assert!(competition.active);
796807
assert_eq!(
@@ -832,12 +843,26 @@ mod tests {
832843
assert!(expected > 0);
833844
test::set_caller::<DefaultEnvironment>(accounts.bob);
834845
let balance_before = dex.get_governance_balance(accounts.bob);
846+
// Swap emissions mint governance tokens, so capture the supply after
847+
// the trades; the claim itself must not mint anything further.
848+
let supply_before_claim = dex.governance_config.total_supply;
835849
let reward = dex.claim_competition_reward(competition_id).unwrap();
836850
assert_eq!(reward, expected);
837851
assert_eq!(
838852
dex.get_governance_balance(accounts.bob),
839853
balance_before + reward
840854
);
855+
// Claiming pays from the escrow: supply is unchanged at claim time and
856+
// the escrow shrinks by exactly the claimed amount (Issue #1022).
857+
assert_eq!(
858+
dex.governance_config.total_supply,
859+
supply_before_claim,
860+
"total supply must not increase at claim time"
861+
);
862+
assert_eq!(
863+
dex.get_competition_reward_escrow(competition_id),
864+
10_000 - reward
865+
);
841866

842867
// Double claims are rejected...
843868
assert_eq!(
@@ -918,6 +943,23 @@ mod tests {
918943
Err(Error::InvalidRequest)
919944
);
920945

946+
// The reward must be funded up front: creating a competition without
947+
// attaching the reward value is rejected (Issue #1022).
948+
assert_eq!(
949+
dex.create_trading_competition(
950+
None,
951+
String::from("Unfunded Race"),
952+
5_000,
953+
100,
954+
110,
955+
1,
956+
10,
957+
String::from("PCG")
958+
),
959+
Err(Error::InvalidRequest)
960+
);
961+
962+
test::set_value_transferred::<DefaultEnvironment>(5_000);
921963
let competition_id = dex
922964
.create_trading_competition(
923965
None,

0 commit comments

Comments
 (0)