Fundraising and positions
During the fundraising window, the engine accepts investments through the invest(addr, amount, token_id) function. As we know from the last section, only "operator" addresses are allowed to invest.
Calling the invest function
stellar contract invoke \
--id <YOUR_CONTRACT_ID> \
-s operator \
--network testnet \
-- \
invest \
--addr "G....."
--amount "5000000000"
--token_id 1234
The addr parameter must hold the operator identity address.
Each token_id represents a position which basically holds (among others):
- How much are you going to earn
- How much are you going to pay in commissions
- How much are you going to receive monthly
Equillar adapts to the integrator as much as possible. That's because the token id is not generated on-chain but off-chain by the integrator. As an example, think about a token id that does not represent a single investment but a collective group of investors. Equillar simply relates said token to a specific address and will send the corresponding payments to said address. The integrator will then distribute the payment among the various investors accordingly.
Fundraising constraints
An investment is accepted only if:
- fundraising has not ended yet
- the fundraising goal has not already been reached
- the amount is at least
min_per_investment - the investing address has enough balance
- The token Id does not exists yet
Investment allocation
The gross investment amount is split into two buckets:
- project amount
- commission amount
The commission is dynamic and depends on investment size and the configured rate logic. Larger investments increase the denominator tier and reduce the effective commission rate. The integrator configures the rate at which commissions are reduced at the time of deployment using the following parameters
- cmr_upper_divisor
- cmr_lower_divisor
- cmr_reductor
In the following sections we will see how these 3 parameters are used.
Worked example
The following example uses the same values covered by the contract tests:
- investment amount:
500 - annual interest rate:
500basis points =5% - return months:
4 - cmr_upper_divisor:
60 - cmr_lower_dividor:
10 - cmr_reductor:
400
Step 1: split the investment into buckets
The commission uses the engine's decreasing commission model.
At a high level, the contract:
- computes a commission denominator tier
- converts the configured interest rate into an effective commission rate
- applies that effective rate to the investment amount
At the contract level, the denominator follows this rule:
- it starts at
cmr_lower_dividor:10 - it increases by 1 for every additional
cmr_reductor (400)normalized token units - it is capped at
cmr_upper_divisor:60
In simplified form:
min_per_investment to 399 extra units -> denominator 10
400 to 799 extra units -> denominator 11
800 to 1199 extra units -> denominator 12
...
This is the ratio that makes the commission model decrease as investment size grows: every additional 400 normalized token units increase the denominator by 1.
So the effective commission rate becomes:
commission_rate = i_rate / (11 * 10_000)
commission_rate = 500 / (11 * 10_000)
commission_rate = 500 / 110_000
commission_rate = 0,0045 = 0.45%
Then the commission amount is:
commission = 500 * 0.45% = 2.27
The resulting allocation is:
- project amount:
497.72 - commission amount:
2.27
The intuition is:
500
- 2.27 to commission (0.45%)
= 497.72 to project
As the investment size increases, the protocol increases the denominator tier and the effective commission rate goes down. That is what makes the commission model decreasing rather than flat.
Step 3: compute accumulated interests
The position interests are computed from the "deposited" amount minus the commission:
accumulated_interests = 497.72 * 5% = 24.88
Step 4: compute total obligations
The total amount the position is entitled to receive over its full lifecycle is:
total = 497.72 + 24.88 = 522.61
Step 5: compute the regular payment amount
At this point, the final shape of the position depends on the selected return model.
ReverseLoan
In ReverseLoan, the regular payment is:
regular_payment = total / return_months
So:
regular_payment = 522.61 / 12 = 43.55
Coupon
In Coupon, the regular payment is:
regular_payment = accumulated_interests / return_months
So:
regular_payment = 24.88 / 12 = 2.073
Investment invoke function result
Below is the result returned by invoking the invest function:
{
"commission":"22727272",
"completed":false,
"deposited":"4977272728",
"paid":"0",
"payments_transferred":0,
"regular_payment":"20738636",
"returns":"248863636",
"token_id":1234,
"total":"5226136364"
}
If you unscale the i128 values (7 decimals, in this example), they match the figures from our worked example:
- commission:
22727272→2_2727272→ 2.27 - deposited:
4977272727→497_7272727→ 497.72 - regular_payment:
20738636→2_0738636→ 2.07 (Coupon) - returns:
248863636→24_8863636→ 24.88 - total:
5226136363→522_6136363→ 522.61
Events and balance after a new investment
When a new deposit is accepted and a new position is created, the contract updates the balance through recalculate_from_position, which increases comission and project and adds the position's total to payment_obligations.
Two events are emitted as part of this flow:
1. ContractBalanceUpdated — the updated balance snapshot after accounting for the new position:
ContractBalanceUpdated {
reserve: contract_balance.reserve,
project: contract_balance.project,
comission: contract_balance.comission,
comission_withdrawal: contract_balance.comission_withdrawal,
payments: contract_balance.payments,
project_withdrawals: contract_balance.project_withdrawals,
payment_obligations: contract_balance.payment_obligations,
collateral_received: contract_balance.collateral_received,
collateral_liquidated: contract_balance.collateral_liquidated,
collateral_returned: contract_balance.collateral_returned,
refunded_to_investor: contract_balance.refunded_to_investor,
}
2. InvestmentReceived — details of the investment itself:
InvestmentReceived {
addr, // the address of the contract
deposited, // the amount deposited by the investor
capital_gains, // the gains the investor will receive
}
Additionally, if the deposit causes the fundraising goal to be reached, a third event is emitted:
3. GoalReached:
GoalReached {
addr, // the address of the contract
total_received, // the total amount raised so far
goal, // the fundraising goal configured at deployment
}
Reading these events with stellar-cli
You can query only these events by filtering on their respective topics and restricting the type to contract:
stellar events \
--id <CONTRACT_ID>
--network testnet \
--start-ledger <LEDGER_NUMBER> \
--type contract \
--topic "InvestmentReceived,*"
stellar events \
--id <CONTRACT_ID>
--network testnet \
--start-ledger <LEDGER_NUMBER> \
--type contract \
--topic "GoalReached,*"