Skip to content

Commit 66eb6fc

Browse files
authored
Merge branch 'main' into main
2 parents db97ed8 + 91c7cf7 commit 66eb6fc

5 files changed

Lines changed: 412 additions & 97 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contracts/rewards/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ doctest = false
1111
[dependencies]
1212
soroban-sdk = { workspace = true }
1313

14+
1415
[dev-dependencies]
16+
1517
soroban-sdk = { workspace = true, features = ["testutils"] }
18+
quest = { path = "../quest" }

contracts/rewards/src/lib.rs

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
#![no_std]
2-
use soroban_sdk::{contract, contracterror, contractimpl, contracttype, token, Address, Env};
2+
use soroban_sdk::{
3+
contract, contractclient, contracterror, contractimpl, contracttype, token, Address, Env,
4+
String,
5+
};
6+
7+
#[contracttype]
8+
#[derive(Clone, Debug, PartialEq)]
9+
pub struct QuestInfo {
10+
pub id: u32,
11+
pub owner: Address,
12+
pub name: String,
13+
pub description: String,
14+
pub token_addr: Address,
15+
pub created_at: u64,
16+
}
17+
18+
#[contractclient(name = "QuestClient")]
19+
pub trait QuestContractTrait {
20+
fn get_quest(env: Env, quest_id: u32) -> Result<QuestInfo, soroban_sdk::Val>;
21+
}
322

423
// Rewards contract: holds token pools per quest and distributes rewards.
524
//
625
// Flow:
726
// 1. Quest owner calls fund_quest() to deposit tokens into the pool
827
// 2. When owner verifies a milestone completion, frontend calls distribute_reward()
928
// 3. Tokens transfer from the contract's pool to the enrollee
10-
//
11-
// Auth model: whoever funds a quest becomes its authority.
12-
// Only the authority can distribute from that quest's pool.
1329

1430
#[contracttype]
1531
#[derive(Clone)]
1632
pub enum DataKey {
1733
TokenAddr,
34+
QuestContractAddr,
1835
// Who funded / controls a quest's pool
1936
QuestAuthority(u32),
2037
// Token balance allocated to a quest
@@ -45,14 +62,22 @@ pub struct RewardsContract;
4562

4663
#[contractimpl]
4764
impl RewardsContract {
48-
/// Initialize with the token contract address (SAC for the reward token).
49-
pub fn initialize(env: Env, token_addr: Address) -> Result<(), Error> {
65+
/// Initialize with the token contract address (SAC for the reward token)
66+
/// and the quest contract address for ownership verification.
67+
pub fn initialize(
68+
env: Env,
69+
token_addr: Address,
70+
quest_contract_addr: Address,
71+
) -> Result<(), Error> {
5072
if env.storage().instance().has(&DataKey::TokenAddr) {
5173
return Err(Error::AlreadyInitialized);
5274
}
5375
env.storage()
5476
.instance()
5577
.set(&DataKey::TokenAddr, &token_addr);
78+
env.storage()
79+
.instance()
80+
.set(&DataKey::QuestContractAddr, &quest_contract_addr);
5681
env.storage()
5782
.instance()
5883
.set(&DataKey::TotalDistributed, &0_i128);
@@ -69,11 +94,30 @@ impl RewardsContract {
6994
return Err(Error::InvalidAmount);
7095
}
7196

97+
// Security Fix: Verify that the funder is the quest owner
98+
let quest_contract_addr = env
99+
.storage()
100+
.instance()
101+
.get::<DataKey, Address>(&DataKey::QuestContractAddr)
102+
.ok_or(Error::NotInitialized)?;
103+
104+
// Using QuestClient trait-based client to avoid WASM requirement in CI
105+
let quest_client = QuestClient::new(&env, &quest_contract_addr);
106+
let quest_info = quest_client.get_quest(&quest_id);
107+
108+
if quest_info.owner != funder {
109+
return Err(Error::Unauthorized);
110+
}
111+
72112
let token_addr = Self::get_token(&env)?;
73113

74114
// If quest already has an authority, only they can add more funds
75115
let auth_key = DataKey::QuestAuthority(quest_id);
76-
if let Some(existing) = env.storage().persistent().get::<_, Address>(&auth_key) {
116+
if let Some(existing) = env
117+
.storage()
118+
.persistent()
119+
.get::<DataKey, Address>(&auth_key)
120+
{
77121
if existing != funder {
78122
return Err(Error::Unauthorized);
79123
}
@@ -121,7 +165,7 @@ impl RewardsContract {
121165
let stored: Address = env
122166
.storage()
123167
.persistent()
124-
.get(&auth_key)
168+
.get::<DataKey, Address>(&auth_key)
125169
.ok_or(Error::QuestNotFunded)?;
126170
if stored != authority {
127171
return Err(Error::Unauthorized);
@@ -197,7 +241,7 @@ impl RewardsContract {
197241
pub fn get_token(env: &Env) -> Result<Address, Error> {
198242
env.storage()
199243
.instance()
200-
.get(&DataKey::TokenAddr)
244+
.get::<DataKey, Address>(&DataKey::TokenAddr)
201245
.ok_or(Error::NotInitialized)
202246
}
203247
}

0 commit comments

Comments
 (0)