Skip to content
Merged

. #659

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
2 changes: 2 additions & 0 deletions CARBON_CREDIT_PLATFORM_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

## Overview


This document describes the implementation of a decentralized carbon credit platform built on Soroban smart contracts. The platform enables tokenization, verification, trading, and retirement of carbon credits with full transparency and auditability.

## Architecture


### Smart Contract Components

1. **Carbon Credit Platform** (`carbon_credit_platform.rs`)
Expand Down
46 changes: 38 additions & 8 deletions contracts/src/gaming_asset_exchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub struct GamingAssetExchangeContract;

#[contractimpl]
impl GamingAssetExchangeContract {
/// Initialize the gaming asset contract and set the admin, fee collector,
/// and marketplace fee ratio.
pub fn init(env: Env, admin: Address, fee_collector: Address, fee_ratio: u32) {
admin.require_auth();
env.storage().instance().set(&DataKey::Admin, &admin);
Expand All @@ -63,6 +65,8 @@ impl GamingAssetExchangeContract {
env.storage().instance().set(&DataKey::AssetCounter, &0u32);
}

/// Mint a new gaming asset for a game and assign ownership to `to`.
/// Asset metadata includes attributes and an external URI for richer UIs.
pub fn mint_asset(
env: Env,
caller: Address,
Expand Down Expand Up @@ -98,9 +102,7 @@ impl GamingAssetExchangeContract {
env.storage()
.instance()
.set(&DataKey::AssetOwner(counter), &to);
env.storage()
.instance()
.set(&DataKey::AssetCounter, &counter);
env.storage().instance().set(&DataKey::AssetCounter, &counter);

let mut assets: Vec<u32> = env
.storage()
Expand All @@ -120,6 +122,8 @@ impl GamingAssetExchangeContract {
counter
}

/// Transfer a gaming asset from one address to another.
/// Any active marketplace listing is cancelled when ownership changes.
pub fn transfer_asset(env: Env, from: Address, to: Address, asset_id: u32) {
from.require_auth();

Expand All @@ -133,7 +137,6 @@ impl GamingAssetExchangeContract {
panic_with_error!(&env, ExchangeError::NotAuthorized);
}

// Cancel listing if transferring
let listing_key = DataKey::AssetListing(asset_id);
if env.storage().instance().has(&listing_key) {
env.storage().instance().remove(&listing_key);
Expand All @@ -149,6 +152,7 @@ impl GamingAssetExchangeContract {
);
}

/// List an owned asset for sale at a positive price.
pub fn list_asset(env: Env, seller: Address, asset_id: u32, price: i128) {
seller.require_auth();

Expand Down Expand Up @@ -179,6 +183,7 @@ impl GamingAssetExchangeContract {
.publish((Symbol::new(&env, "Listed"),), (asset_id, seller, price));
}

/// Remove an active marketplace listing.
pub fn delist_asset(env: Env, seller: Address, asset_id: u32) {
seller.require_auth();

Expand All @@ -199,8 +204,7 @@ impl GamingAssetExchangeContract {
.publish((Symbol::new(&env, "Delisted"),), (asset_id, seller));
}

// In a real implementation this would involve a native token transfer or RS token.
// We mock the buy interaction here to demonstrate state change.
/// Purchase an active listing. The asset owner changes and the listing is removed.
pub fn buy_asset(env: Env, buyer: Address, asset_id: u32) {
buyer.require_auth();

Expand All @@ -214,8 +218,6 @@ impl GamingAssetExchangeContract {
panic_with_error!(&env, ExchangeError::ListingNotActive);
}

// Normally we'd transfer funds from `buyer` to `listing.seller` minus fee to `fee_collector` here using a Token interface

env.storage()
.instance()
.set(&DataKey::AssetOwner(asset_id), &buyer);
Expand All @@ -229,17 +231,45 @@ impl GamingAssetExchangeContract {
);
}

/// Return the metadata stored for an asset.
pub fn get_asset(env: Env, asset_id: u32) -> AssetMetadata {
env.storage()
.instance()
.get(&DataKey::Asset(asset_id))
.unwrap_or_else(|| panic_with_error!(&env, ExchangeError::AssetNotFound))
}

/// Return the current owner of a gaming asset.
pub fn get_owner(env: Env, asset_id: u32) -> Address {
env.storage()
.instance()
.get(&DataKey::AssetOwner(asset_id))
.unwrap_or_else(|| panic_with_error!(&env, ExchangeError::AssetNotFound))
}

/// Return an active listing for an asset.
pub fn get_listing(env: Env, asset_id: u32) -> Listing {
env.storage()
.instance()
.get(&DataKey::AssetListing(asset_id))
.unwrap_or_else(|| panic_with_error!(&env, ExchangeError::ListingNotFound))
}

/// Return the list of asset IDs minted for a given game.
pub fn get_assets_by_game(env: Env, game_id: String) -> Vec<u32> {
env.storage()
.instance()
.get(&DataKey::GameAssets(game_id))
.unwrap_or(Vec::new(&env))
}

/// Return whether the asset exists in contract storage.
pub fn asset_exists(env: Env, asset_id: u32) -> bool {
env.storage().instance().has(&DataKey::Asset(asset_id))
}

/// Return whether the asset is currently listed for sale.
pub fn is_listed(env: Env, asset_id: u32) -> bool {
env.storage().instance().has(&DataKey::AssetListing(asset_id))
}
}
184 changes: 184 additions & 0 deletions contracts/src/gaming_asset_exchange_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use super::*;
extern crate std;
use soroban_sdk::{testutils::Address as _, Address, Env, FromVal, String, Vec, Map};

fn setup() -> (
Env,
Address,
Address,
Address,
GamingAssetExchangeContractClient<'static>,
) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(GamingAssetExchangeContract, ());
let client = GamingAssetExchangeContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let fee_collector = Address::generate(&env);
client.init(&admin, &fee_collector, &250u32);
(env, admin, fee_collector, Address::generate(&env), client)
}

fn build_asset_attributes(env: &Env) -> Map<String, String> {
let mut attrs = Map::new(env);
attrs.set(&String::from_str(env, "rarity"), &String::from_str(env, "epic"));
attrs.set(&String::from_str(env, "power"), &String::from_str(env, "42"));
attrs
}

#[test]
fn mint_asset_assigns_owner_and_metadata() {
let (env, admin, _, player, client) = setup();
let game_id = String::from_str(&env, "block_brawl");
let name = String::from_str(&env, "Sword of Learning");
let description = String::from_str(&env, "A beginner-friendly sword for classroom battles.");
let image_uri = String::from_str(&env, "https://example.com/sword.png");
let external_url = String::from_str(&env, "https://example.com/game/item/1");
let attributes = build_asset_attributes(&env);

let asset_id = client.mint_asset(
&admin,
&player,
&game_id,
&name,
&description,
&image_uri,
&external_url,
&attributes,
);

assert_eq!(asset_id, 1);
assert!(client.asset_exists(&asset_id));
assert_eq!(client.get_owner(&asset_id), player);

let metadata = client.get_asset(&asset_id);
assert_eq!(metadata.game_id, game_id);
assert_eq!(metadata.name, name);
assert_eq!(metadata.description, description);
assert_eq!(metadata.image_uri, image_uri);
assert_eq!(metadata.external_url, external_url);
assert_eq!(metadata.attributes.get(&String::from_str(&env, "rarity")).unwrap(), String::from_str(&env, "epic"));

let game_assets = client.get_assets_by_game(&game_id);
assert_eq!(game_assets.len(), 1);
assert_eq!(game_assets.get(0).unwrap(), 1);
}

#[test]
fn list_delist_and_buy_asset_round_trip() {
let (env, admin, _, player, client) = setup();
let buyer = Address::generate(&env);
let game_id = String::from_str(&env, "arena");
let attributes = build_asset_attributes(&env);
let asset_id = client.mint_asset(
&admin,
&player,
&game_id,
&String::from_str(&env, "Shield of Study"),
&String::from_str(&env, "An educational shield used in learning tournaments."),
&String::from_str(&env, "https://example.com/shield.png"),
&String::from_str(&env, "https://example.com/game/item/2"),
&attributes,
);

client.list_asset(&player, &asset_id, &500);
assert!(client.is_listed(&asset_id));
let listing = client.get_listing(&asset_id);
assert_eq!(listing.seller, player);
assert_eq!(listing.price, 500);
assert!(listing.active);

client.delist_asset(&player, &asset_id);
assert!(!client.is_listed(&asset_id));

client.list_asset(&player, &asset_id, &750);
client.buy_asset(&buyer, &asset_id);
assert_eq!(client.get_owner(&asset_id), buyer);
assert!(!client.is_listed(&asset_id));
}

#[test]
#[should_panic]
fn reject_invalid_listing_price() {
let (env, admin, _, player, client) = setup();
let game_id = String::from_str(&env, "quest");
let attributes = build_asset_attributes(&env);
let asset_id = client.mint_asset(
&admin,
&player,
&game_id,
&String::from_str(&env, "Quest Helm"),
&String::from_str(&env, "A helmet for curious learners."),
&String::from_str(&env, "https://example.com/helm.png"),
&String::from_str(&env, "https://example.com/game/item/3"),
&attributes,
);
client.list_asset(&player, &asset_id, &0);
}

#[test]
#[should_panic]
fn reject_listing_by_non_owner() {
let (env, admin, _, player, client) = setup();
let other = Address::generate(&env);
let game_id = String::from_str(&env, "quest");
let attributes = build_asset_attributes(&env);
let asset_id = client.mint_asset(
&admin,
&player,
&game_id,
&String::from_str(&env, "Quest Helm"),
&String::from_str(&env, "A helmet for curious learners."),
&String::from_str(&env, "https://example.com/helm.png"),
&String::from_str(&env, "https://example.com/game/item/3"),
&attributes,
);
client.list_asset(&other, &asset_id, &100);
}

#[test]
#[should_panic]
fn reject_buy_when_not_listed() {
let (env, admin, _, player, client) = setup();
let buyer = Address::generate(&env);
let game_id = String::from_str(&env, "quest");
let attributes = build_asset_attributes(&env);
let asset_id = client.mint_asset(
&admin,
&player,
&game_id,
&String::from_str(&env, "Quest Helm"),
&String::from_str(&env, "A helmet for curious learners."),
&String::from_str(&env, "https://example.com/helm.png"),
&String::from_str(&env, "https://example.com/game/item/3"),
&attributes,
);
client.buy_asset(&buyer, &asset_id);
}

#[test]
#[should_panic]
fn reject_transfer_from_non_owner() {
let (env, admin, _, player, client) = setup();
let other = Address::generate(&env);
let game_id = String::from_str(&env, "quest");
let attributes = build_asset_attributes(&env);
let asset_id = client.mint_asset(
&admin,
&player,
&game_id,
&String::from_str(&env, "Quest Helm"),
&String::from_str(&env, "A helmet for curious learners."),
&String::from_str(&env, "https://example.com/helm.png"),
&String::from_str(&env, "https://example.com/game/item/3"),
&attributes,
);
client.transfer_asset(&other, &Address::generate(&env), &asset_id);
}

#[test]
#[should_panic]
fn reject_get_nonexistent_asset() {
let (_env, _admin, _fee_collector, _player, client) = setup();
client.get_asset(&999);
}
2 changes: 2 additions & 0 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub mod enrollment;
pub mod events;
pub mod execution_engine;
pub mod gaming_asset_exchange;
#[cfg(test)]
pub mod gaming_asset_exchange_test;
pub mod membership_nft;
pub mod oracle_aggregator;
pub mod paymaster;
Expand Down
Loading