Introduction
A few days ago I faced an interesting problem: how to manage multiple investments per user in the Equillar contract? The traditional solution would be something like Map<Address, Vec<Investment>>, but then I wondered: what if each project was an NFT and each investment a minted token id?
Let me tell you how I implemented it using the OpenZeppelin's stellar-tokens.
The Original Problem
Imagine an investment contract where:
- Users can invest multiple times
- Each investment has independent monthly payments
- Users should be able to sell/transfer their investments
With the traditional approach, transferring a specific investment would require complicated custom logic. With NFTs, it comes almost for free.
The Solution: NFTs as Investment Certificates
Every time a contract is deployed, the constructor:
- Loads the project investment conditions (rate, goal, return months etc ...).
- Stores the NFT metadata (token_uri, name and symbol).
- Foreach investment a new token id is minted. This ID token relates the address that have invested with its corresponding data (earnings, payment start date, etc.)
Let's explore it step by step:
Basic Setup
First, we add the dependency:
[dependencies]
stellar-tokens = "0.6.0"
And make our contract implements the OpenZeppelin's NonFungibleToken trait:
use stellar_tokens::non_fungible::{Base, NonFungibleToken};
#[contractimpl(contracttrait)]
impl NonFungibleToken for InvestmentContract {
type ContractType = Base;
}
That's it. You now have working NFT.
Constructor: Initializing the Collection
In the constructor, you need to configure the NFT collection metadata:
pub fn __constructor(
env: Env,
owner_addr: Address,
project_address: Address,
token_addr: Address,
uri: String,
name: String,
symbol: String,
investment_params: InvestmentContractParams
) -> Result<(), Error> {
owner_addr.require_auth();
// validations ...
InvestmentReturnType::from_number(investment_params.return_type).ok_or(Error::UnsupportedReturnType)?;
ownable::set_owner(&env, &owner_addr);
let contract_data = ContractData::from_investment_contract_params(&investment_params, token_addr, project_address);
// Add NFT metadata
Base::set_metadata(&env, uri, name, symbol);
Storage::update_contract_data(&env, &contract_data);
Ok(())
}
What do these parameters do?
-
uri: Base URL for metadata (e.g.,"https://project.com") -
name: Collection name (e.g.,"InvestmentNFT") -
symbol: Short symbol (e.g.,"INVST")
Minting: A new Token ID for each investment
Here's the magic. When someone invests, we mint a new token and link it to the investment data:
pub fn invest(env: Env, addr: Address, amount: i128) -> Result<Investment, Error> {
addr.require_auth();
// 1. Validations
let contract_data = Storage::get_contract_data(&env);
validation::validate_investment(amount, &contract_data, ...)?;
// 2. Transfer tokens to contract
let tk = get_token(&env, &contract_data);
tk.transfer(&addr, &env.current_contract_address(), &amount)?;
// 3. Mint the NFT (sequential: 0, 1, 2, ...)
let token_id = Base::sequential_mint(&env, &addr);
// 4. Create and store investment data using token_id as key
let investment = Investment::new(&env, &contract_data, &amount, decimals, token_id);
Storage::update_investment(&env, token_id, &investment);
Ok(investment)
}
The important part:
-
Base::sequential_mint(&env, &addr)mints a new token to the investor's address and returns thetoken_id - We use that
token_idas the key to store the investment data - The NFT and investment are linked forever
Storage: Two Connected Worlds
We have two independent but related storages:
NFT Storage (stellar-tokens):
NFTStorageKey::Owner(token_id) → Address // Who owns the NFT
Investment Storage (ours):
DataKey::Investment(token_id) → Investment // That investment's data
When transferring the NFT, only the first storage changes. The investment data stays there, but now "belongs" to the new NFT owner.
Claiming Payments
When a user wants to claim their payments, we use the NFT to verify ownership:
pub fn claim(env: Env, token_id: u32) -> Result<Investment, Error> {
// 1. Verify caller owns the NFT
let addr: Address = Self::owner_of(&env, token_id);
addr.require_auth();
// 2. Get investment data
let mut investment = Storage::get_investment(&env, token_id)
.ok_or(Error::AddressHasNotInvested)?;
// 3. Process pending payments
let amount = investment.process_payments(&env, &contract_data);
// 4. Transfer tokens
let tk = get_token(&env, &contract_data);
tk.try_transfer(&env.current_contract_address(), &addr, &amount)?;
// 5. Update state
Storage::update_investment_with_claim(&env, token_id, &investment);
Ok(investment)
}
Self::owner_of(&env, token_id)tells us who currently owns the NFT. If Alice sold her NFT #42 to Bob,owner_of(42)now returns Bob's address.
Free Secondary Market
This is the best part: any Stellar NFT-compatible marketplace can list these tokens. The flow would be:
- Alice invests 1000 USDC → receives NFT #42
- Alice lists NFT #42 on a marketplace for 1100 USDC
- Bob buys the NFT
- Bob can now claim the remaining monthly payments
You don't need extra code. When transferring the NFT, the payment rights go with it.
Methods You Get Automatically
By implementing NonFungibleToken, you get for free:
-
balance(account)— how many investment NFTs a user has -
owner_of(token_id)— who is the current owner -
transfer(from, to, token_id)— transfer the NFT -
approve(approver, approved, token_id)— give permission to a third party (approved) -
token_uri(token_id)— metadata URL
All work out-of-the-box. You can call them from other contracts or from a frontend.
Advantages of This Approach
Advantages:
- ✅ Native transferability of investments
- ✅ Compatibility with explorers and wallets
- ✅ Less custom code
- ✅ Multiple investments per user without complications
- ✅ Each investment is independent
Tradeoffs:
- ⚠️ You need to manage an additional
token_id - ⚠️ Minting has a cost (small, but exists)
Conclusion
Using NFTs to manage investments turned out to be more elegant than I expected. The code is simpler, transferability is native, and ecosystem integration comes for free.
Is it the right approach for everything? No, but it's useful because it allows easily assigning more than one investment per user in the same project and transferring them easily if the user wants to.
If you're building something similar, check out OpenZeppelin's stellar-tokens. The library does the heavy lifting for you.
Top comments (0)