B9lab Logo
Tezos Developer Portal
Developer PortalDeveloper Portal

Smart Contract Implementation II

Implementing the sell function


Reading Time: 25 min

Now let us talk about the sell() function.

The sell() function

The sell() function determines the sell price after the MFG is reached. sell calculates the amount of tez for the sent number of tokens when a user wants to sell the offering's tokens.

Calculating the sell price post-MFG
Calculating the sell price post-MFG

The sell price is lower than the buy price.

info icon

If you are wondering why the sell price is lower than the buy price, make sure to go through the basics in the Continuous Organizations Whitepaper.

Entrypoint

Let us have a look at the sell entrypoint:

    @sp.entry_point
    def sell(self, params):
        sp.if self.data.phase == 0:
            self.sell_initial(params.amount)
        sp.if self.data.phase == 1:
            self.sell_slope(params.amount)

This seems less complex than the buy entrypoint. The reason is that we cannot change the phase by selling:

    def sell_initial(self, amount):
        # check if the address owns tokens
        sp.if self.data.ledger.contains(sp.sender):
        # check if the address owns enough tokens
            sp.if self.data.ledger[sp.sender] >= sp.as_nat(amount):
                # calculate the amount of tez to send
                # see https://github.com/C-ORG/whitepaper#-investments---sell
                pay_amount = sp.local(
                    "pay_amount", 
                    sp.utils.mutez_to_nat(self.data.price) * sp.as_nat(amount)
                )
                # burn the amount of tokens sold
                self.burn_intern(amount)
                # send pay_amount tez to the sender of the transaction
                sp.send(sp.sender, sp.utils.nat_to_mutez(pay_amount.value))
                self.modify_sell_slope(sp.utils.nat_to_mutez(pay_amount.value))

In the initial phase, one can sell a token and get 100% of the buy price, which remains the same until the MFG is reached. We do not use the sell_slope in the initial phase, because the price is fixed but we keep it updated.

In the post-MFG phase, we use the sell_slope to calculate the amount of tez to be sent:

    def sell_slope(self, amount):
        # check if the address owns tokens
        sp.if self.data.ledger.contains(sp.sender):
        # check if the address owns enough tokens
            sp.if self.data.ledger[sp.sender] >= sp.as_nat(amount):
                # calculate the amount of tez to send
                # see https://github.com/C-ORG/whitepaper#-investments---sell
                pay_amount = sp.local(
                    "pay_amount", 
                    sp.as_nat(
                        self.data.total_tokens * sp.as_nat(amount) * self.data.sell_slope - 
                        sp.as_nat(amount * amount) * self.data.sell_slope / 2 
                        ) + 
                        self.data.sell_slope * sp.as_nat(amount) * 
                        self.data.burned_tokens * self.data.burned_tokens /
                        sp.as_nat(2 * (self.data.total_tokens - self.data.burned_tokens) )
                )
                # burn the amount of tokens sold
                self.burn_intern(amount)
                # send pay_amount tez to the sender of the transaction
                sp.send(sp.sender, sp.utils.nat_to_mutez(pay_amount.value))
                self.modify_sell_slope(sp.utils.nat_to_mutez(pay_amount.value))

The calculation of pay_amount looks a bit messy because it depends on total_tokens, self.data.sell_slope, params.amount (the amount of tez sent with the transaction), and self.data.burned_tokens:

Formula to calculate the pay amount
Formula to calculate the pay amount

In the formula, our pay_amount represents M, x is the amount of tez sent with the transaction, x' is the number of burned tokens, and a represents the total number of tokens issued.

As you can see, the sell slope increases if tokens are burned but the buy slope is always constant. This means that the entrypoint burn allows for the organization to increase the sell price.

You can see that the contract burns the number of tokens sold.

Calculating the sell slope

Compared to the buy price calculation, this time it is a bit more complex because we do not have a linear price function for the sell price. The sell slope changes over time. The sell slope depends on the reserve, which is represented by the contract balance:

Formula to calculate the sell price
Formula to calculate the sell price

a represents the total number of tokens issued.

    # s calculus after each transaction
    def modify_sell_slope(self, send_back):
        sp.if self.data.total_tokens != 0:
           self.data.sell_slope = 2 * sp.utils.mutez_to_nat(sp.balance - send_back) / (self.data.total_tokens * self.data.total_tokens)

The value of sp.balance is the balance of the contract after the transaction is received but before the contract sends any tez back to the user. So the helper function needs to know the number of tez sent back.

Burning tokens

Because burning is a part of the selling process, we define an internal function to burn tokens:

    # internal burn function will be called by the entrypoints burn and sell
    def burn_intern(self, amount):
        # check if the address owns tokens
        sp.if self.data.ledger.contains(sp.sender):
            # check if the address owns enough tokens
            sp.if self.data.ledger[sp.sender] >= sp.as_nat(amount):
                # "burn"
                self.data.ledger[sp.sender] = sp.as_nat(self.data.ledger[sp.sender] - sp.as_nat(amount))
                self.data.burned_tokens += sp.as_nat(amount)

In addition, the contract offers a burn entrypoint, in case a token holder wants to burn its tokens:

    @sp.entry_point
    def burn(self, params):
        self.burn_intern(params.amount)
        self.modify_sell_slope()

In the above code, an important aspect to notice is that we do not modify the total number of tokens issued if we burn a token. Thus, the buy price is not affected.

Anyway, there is another mechanism by which users/investors can gain profits from buying tokens: closing.

The closing phase

tip icon

If you want to again take a look at the closing phase, the introduction section of this chapter includes a section on Stages in a Rolling SAFE.

Price calculation and exit fee
Price calculation and exit fee

If the closing phase is triggered, the contract buys back each token for the price of the last buy price and pays the token owners their part of the exit fee:

    @sp.entry_point
    def close(self):
        # check if MPT is reached
        sp.verify(sp.now - self.data.MPT >= 0)

        # check that the initial phase is over but not closed
        sp.verify(self.data.phase == 1)

        # verify this is called by the org
        sp.verify(sp.sender == self.data.organization)

        # check the correct amount of tez is sent
        closing_sell_price= sp.local(
            "closing_sell_price",
            self.data.b * self.data.total_tokens
            )

        closing_sell_amount= sp.local(
            "closing_sell_amount",
            closing_sell_price.value * sp.as_nat(self.data.total_tokens - self.data.burned_tokens)
            )

        sp.if sp.balance < sp.utils.nat_to_mutez(closing_sell_amount.value):
            sp.failwith("Please send more tez for the closing")

        sp.for account in self.data.ledger.items():
            sp.send(account.key, sp.utils.nat_to_mutez(account.value * closing_sell_price.value))
        
        self.data.phase = 2

So, the sum of the payments is the exit fee.

Only the contract owner can call close. For the close transaction to be successful, the contract owner needs to send the exit fee. Note that the minimum period of time (MPT) must be reached if the organization wants to trigger the closing.

Notice that if the data in the ledger increases in size, closing costs more beacuse of:

        sp.for account in self.data.ledger.items():
            sp.send(account.key, sp.utils.nat_to_mutez(account.value * closing_sell_price.value))

We do not expect this to be a problem because one needs to pay tez to increase the size of the ledger. Alternatively we can create another entrypoint which can be called by the investors to get their tez.

Discuss on Slack
Rate this Page
Would you like to add a message?
Submit
Thank you for your Feedback!