Deploying the contract
An Equillar contract instance represents one financing project with its own immutable economic setup.
Clone the equillar contract
Clone the equillar contract using git:
git clone https://github.com/icolomina/equillar-soroban.git
Then you can go into the project folder and execute the tests:
cd contracts/investment-income-based
cargo test
Dependency issue: ed25519-dalek version conflict
If you see an error like this while building or testing:
error[E0277]: the trait bound ChaCha20Rng: ed25519_dalek::rand_core::CryptoRng is not satisfied
Fix it by running:
cargo update -p ed25519-dalek@3.0.0 --precise 2.2.0
Creating an stellar identity
Before deploying, you need a local identity to act as the source account for the upload and deploy commands. stellar keys generate creates a new keypair and stores it locally under the given alias (admin in this case), while stellar keys fund requests test funds (via Friendbot) to activate that account on the network — this only works on testnet; on mainnet the account must be funded manually.
stellar keys generate admin
stellar keys fund admin
Build the contract
For building the contract and generating the wasm file, proceed as follows from the "contracts/investment-income-based" folder:
stellar contract build
After the command finishes, you will find the wasm file under the folder "target/wasm32v1-none/release"
Upload the contract
Now, before deploying an instance of the contract, we need to install it in the network:
stellar contract upload \
--wasm target/wasm32v1-none/release/<your_wasm_filename>.wasm \
-s admin \
--network testnet
After the command finishes, you will get the contract wasm hash that you will use later to deploy an instance of the contract.
The
-s adminflag (short for--source-account) tells the CLI to sign and submit this transaction using theadminidentity created above.
Deploy the contract
Contract deployment parameters
The equillar contract __construct method must receive the following parameters:
| Parameter | Type | Description |
|---|---|---|
owner_addr | Address | Address of the contract's owner/administrator. The admin controls every capital flow in the contract except deposit inflows, which must be signed by addresses holding the operator role. The role system is covered in detail in the following section. |
token_addr | Address | Address of the token (SEP-41 / CAP-46) used to handle all financial flows (deposits, payouts, returns) within the contract. |
price_oracle | Address | Address of the price oracle contract used to value the collateral token against the contract's token, enabling calculation of the collateral coverage level. Covered in detail in the collateral section below. |
investment_params | InvestmentContractParams | Struct holding the economic and time-related parameters (see next section). |
Note about the token addr: Equillar is designed around stablecoins as the settlement token (
token_addr). Returns are meant to reflect the performance of the underlying financed project — not the price behavior of the token used to move capital. Using a volatile asset instead would introduce a source of risk entirely unrelated to the investment itself, undermining the predictability that structured financing products like are meant to provide. Since stablecoins are typically regulated and pegged to a fiat currency, they keep that additional volatility out of the equation.
Investment Params
| Field | Type | Description |
|---|---|---|
i_rate | u32 | Interest/return rate offered to investors. It is represented in basis points or as a scaled integer (e.g. 500 = 5.00%) |
fundraising_days | u64 | Duration in days of the fundraising phase. |
claim_block_days | u64 | Number of days during which claims are blocked/unavailable, typically counted after the fundraising period closes. |
goal | i128 | Fundraising goal, expressed using the number of decimals defined by the token. For example, for a token with 7 decimals, to express 1 unit you must send 1 * 10^7 = 10000000. |
return_type | u32 | Payment scheme used for investor returns: 1 = ReverseLoan, 2 = Coupon. ReverseLoan: capital plus gains are distributed together across the configured months. Coupon: gains are paid out first across the configured months, with the capital returned in the final payment (bond-like). See the Payment Distribution to Investors section for further details. |
return_months | u32 | Number of months over which the return is distributed to investors. |
min_per_investment | i128 | Minimum amount allowed per individual investment, expressed using the number of decimals defined by the token. For example, for a token with 7 decimals, to express 1 unit you must send 1 * 10^7 = 10000000. |
cmr_upper_divisor | u32 | Maximum divisor that can be applied to reduce the commission |
cmr_lower_divisor | u32 | Minimum divisor applicable for calculating the commission |
cmr_reductor | i128 | Number of token units invested, by which the divisor increases by one |
Note on
cmr_parameters: These parameters will be explained in greater detail in Section Fundraising and positions.
Note on
claim_block_days: The appropriate length of this window is up to the integrator and typically depends on the nature of the financed project. Project type matters — financing a truck fleet (operational quickly) differs from financing a solar plant (longer build-out, more exposure to technical/bureaucratic delays). Company profile matters too — fast-growing, cash-intensive companies (e.g. startups) generally warrant longer lock periods, reflecting their higher risk profile, compared to more established businesses.
Deployment example with stellar-cli
From the CLI version that supports constructors (protocol ≥ 22), stellar contract deploy accepts constructor arguments after --, the same way stellar contract invoke does. Simple types (Address, String, u32, u64) are passed as plain text; custom structs are passed as a JSON object whose keys match the struct's field names.
stellar contract deploy \
--wasm-hash <hash>\
-s admin \
--network testnet \
-- \
--admin_addr GBOWNERXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
--token_addr CATOKENXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
--price_oracle COAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
--investment_params '{
"i_rate": 500,
"claim_block_days": 180,
"fundraising_days": 60,
"goal": "10000000000000",
"return_type": 2,
"return_months": 12,
"min_per_investment": "1000000000",
"cmr_upper_divisor" : 60,
"cmr_lower_divisor" : 10,
"cmr_reductor" : "4000000000"
}'
Pay special attention to the goal, min_per_investment and cmr_reductor parameters: they are scaled according to the token's decimals (7 decimals in this example).
goal:1000000_0000000→ one million tokens.min_per_investment:100_0000000→ a minimum investment of one hundred tokens.
Both fields are i128, so they must be passed as strings in the JSON payload (not as plain numbers), e.g. "goal": "10000000000000".
For getting the public address (G....) of the admin_addr parameter you can derive it from the admin identity we've created before uploading the contract.
stellar keys public-key admin
Balance and Events
Balance
Equillar tracks the contract's financial state through a single ContractBalance structure, which is updated after every relevant operation (investments, payments, withdrawals, collateral movements, refunds, emergency payouts, etc.). Understanding what each field represents makes it easier to follow how the balance evolves throughout the rest of this document.
| Field | Meaning |
|---|---|
reserve | Funds contributed by the company and currently available to pay investors. Consumed by regular and emergency payments. |
project | Funds raised from investors that belong to the project/company, net of commissions. Consumed by company withdrawals and refunds. |
comission | Total commission accrued from investments. Reduced when a refund reverses a position. |
comission_withdrawal | Total commission amount already withdrawn by the integrator. |
payments | Total amount paid out to investors so far, through regular payment rounds or emergency payouts. |
project_withdrawals | Total amount withdrawn by the company from the project balance. |
payment_obligations | Total amount still owed to investors across all active positions. |
collateral_received | Total collateral amount deposited into the contract. |
collateral_liquidated | Total collateral amount liquidated to cover payment obligations. |
collateral_returned | Total collateral amount returned back to the company. |
refunded_to_investor | Total amount refunded to investors due to fundraising cancellations. |
The balance can be queried at any time through the read-only function get_contract_balance(env: Env, caller: Address). This function requires the caller to hold one of the following roles: operator, company, manager, or admin.
stellar contract invoke \
--id <YOUR_CONTRACT_ID> \
-s admin \
--network testnet \
-- \
get_contract_balance \
--addr "G...."
The following subsections walk through each stage of the contract's lifecycle, showing which event is emitted and how the balance changes at each step.
Events after deployment
Right after a successful deployment, the contract emits a ContractDeployed event:
ContractDeployed {
addr, // the address of the newly deployed contract
ts_fundraising_ends, // timestamp when the fundraising period closes
ts_payments_start, // timestamp when the payment schedule begins
}
This event lets integrators track new deployments and their key dates without having to poll the contract state directly.
At this point, the balance is fully zero-initialized — no investments, payments, collateral, or withdrawals have taken place yet:
reserve = 0
project = 0
comission = 0
comission_withdrawal = 0
payments = 0
project_withdrawals = 0
payment_obligations = 0
collateral_received = 0
collateral_liquidated = 0
collateral_returned = 0
refunded_to_investor = 0
Reading events with stellar-cli
Contract events can be queried directly using the stellar events command, filtering by contract ID and, optionally, by topic:
stellar events \
--id \
--network testnet \
--start-ledger <LEDGER_NUMBER> \
--type contract
Events emitted with #[contractevent] automatically publish the event's name as part of their topics, along with any field explicitly marked with #[topic]. This means a query can be narrowed down to only ContractDeployed events using --topic:
stellar events \
--id <CONTRACT_ID> \
--network testnet \
--start-ledger <LEDGER_NUMBER> \
--topic "ContractDeployed,*" \
--type contract