Skip to main content

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:

  1. project amount
  2. 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: 500 basis 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:

  1. computes a commission denominator tier
  2. converts the configured interest rate into an effective commission rate
  3. 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: 227272722_2727272 → 2.27
  • deposited: 4977272727497_7272727 → 497.72
  • regular_payment: 207386362_0738636 → 2.07 (Coupon)
  • returns: 24886363624_8863636 → 24.88
  • total: 5226136363522_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,*"