A Soroban smart contract workspace for a Stellar-based learning platform. ChainLearn enables on-chain learning progress tracking, quiz-based token rewards, and verifiable credential NFTs.
The workspace contains three interconnected contracts and a shared utilities package:
The reward token for the platform. Implements the SEP-41 fungible token standard with additional reward logic:
- Standard interface:
initialize,mint,transfer,balance,total_supply,approve,allowance - Reward system:
claim_reward(learner, quiz_id, score)mints tokens proportional to quiz score - Anti-fraud: Each quiz reward can only be claimed once per learner
- Reward formula:
score * BASE_REWARD_PER_POINT(100 tokens per point)
Non-transferable credential NFTs that certify course completion:
- Minting:
mint_credential(to, course_id, score, metadata_uri)-- score-gated at 50+ - Verification:
verify_credential(credential_id)returns full credential info - Lookup:
get_credentials_for(learner)lists all credentials for a learner - Revocation: Admin can revoke credentials if needed
- One per course: Each learner can only receive one credential per course
Tracks learner enrollment, module completion, and quiz scores:
- Enrollment:
enroll(learner, course_id) - Module tracking:
complete_module(learner, course_id, module_id) - Quiz scores:
submit_quiz_score(learner, course_id, quiz_id, score) - Progress view:
get_progress(learner, course_id)returnsProgressInfo - Eligibility: Automatic credential eligibility calculation
- Weighted progress: 70% module completion + 30% quiz performance
Common types and constants used across all contracts:
MIN_CREDENTIAL_SCORE(50): Minimum score to mint a credentialMAX_QUIZ_SCORE(100): Maximum possible quiz scoreTOKEN_DECIMALS(7): Token decimal placesBASE_REWARD_PER_POINT(100): Tokens minted per quiz point
chainlearn-contracts/
├── contracts/
│ ├── learn-token/ # SEP-41 fungible token
│ ├── credential-nft/ # NFT credentials
│ └── progress-tracker/ # Learning progress
├── packages/
│ └── shared/ # Shared types and constants
├── tests/
│ ├── integration/ # End-to-end flow tests
│ └── unit/ # Contract unit tests
├── scripts/
│ ├── deploy.sh # Deployment script
│ └── initialize.sh # Post-deploy initialization
├── Cargo.toml # Workspace root
└── README.md
- Rust 1.70+ with
wasm32-unknown-unknowntarget - Soroban CLI v21+
- Stellar account with XLM for deployment fees
Install the WASM target:
rustup target add wasm32-unknown-unknownInstall Soroban CLI:
cargo install --locked soroban-cli --version 21.0.0Build all contracts for release:
cargo build --release --target wasm32-unknown-unknownBuild in development mode (faster, includes debug info):
cargo build --target wasm32-unknown-unknownRun all tests:
cargo testRun tests for a specific contract:
cargo test -p learn-token
cargo test -p credential-nft
cargo test -p progress-trackerRun with output:
cargo test -- --nocapture- Set your secret key:
export STELLAR_SECRET_KEY="S..."- Deploy contracts:
./scripts/deploy.sh testnet- Initialize contracts:
./scripts/initialize.sh testnet./scripts/deploy.sh mainnet
./scripts/initialize.sh mainnetsoroban contract invoke \
--id <PROGRESS_TRACKER_ID> \
--source "$STELLAR_SECRET_KEY" \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase "Test SDF Network ; September 2015" \
-- \
create_course \
--course_id "rust_101" \
--total_modules 3 \
--total_quizzes 2 \
--module_ids '["mod_basics","mod_ownership","mod_traits"]'- Enroll in a course
- Complete modules one by one
- Submit quiz scores after each module section
- Claim token rewards for each quiz
- Receive credential NFT upon full completion
# Enroll
soroban contract invoke --id <PROGRESS_ID> -- enroll \
--learner <ADDRESS> --course_id "rust_101"
# Complete a module
soroban contract invoke --id <PROGRESS_ID> -- complete_module \
--learner <ADDRESS> --course_id "rust_101" --module_id "mod_basics"
# Submit quiz score
soroban contract invoke --id <PROGRESS_ID> -- submit_quiz_score \
--learner <ADDRESS> --course_id "rust_101" --quiz_id "quiz_1" --score 85
# Claim token reward
soroban contract invoke --id <TOKEN_ID> -- claim_reward \
--learner <ADDRESS> --quiz_id "quiz_1" --score 85
# Mint credential (after completing all modules and quizzes)
soroban contract invoke --id <CREDENTIAL_ID> -- mint_credential \
--to <ADDRESS> --course_id "rust_101" --score 80 \
--metadata_uri "ipfs://Qm..."struct ProgressInfo {
enrolled_at: u64, // Timestamp of enrollment
modules_completed: Vec<Symbol>, // Completed module IDs
quiz_scores: Vec<QuizResult>, // Quiz submission results
overall_progress: u32, // Progress percentage (0-100)
eligible_for_credential: bool, // Qualifies for credential
}struct CredentialInfo {
learner: Address, // Credential holder
course_id: Symbol, // Course identifier
score: u32, // Final score (0-100)
issued_at: u64, // Issuance timestamp
revoked: bool, // Revocation status
metadata_uri: Symbol, // Off-chain metadata URI
}struct QuizResult {
quiz_id: Symbol, // Quiz identifier
course_id: Symbol, // Parent course
score: u32, // Score achieved
submitted_at: u64, // Submission timestamp
}Overall progress is calculated as a weighted average:
- Module completion: 70% weight (proportion of modules completed)
- Quiz performance: 30% weight (average quiz score / 100)
Formula: progress = (completed_modules / total_modules * 70) + (avg_quiz_score / 100 * 30)
A learner is eligible for a credential when:
- All course modules are completed
- All quizzes are submitted
- Average quiz score >= 50 (MIN_CREDENTIAL_SCORE)
- Auth: All state-changing functions require authorization from the relevant party
- Double-claim prevention: Token rewards and quiz submissions are tracked to prevent duplicates
- Score gating: Credentials require a minimum passing score
- Admin controls: Only the admin can create courses, mint tokens, and revoke credentials
- Non-transferable credentials: Credential NFTs are soulbound (not transferable)
MIT