Gnosis Conditional Token Framework

Gnosis Conditional Token Framework implements a codebase for tokenizing potential outcomes in prediction markets. Such markets are also often called information markets, idea futures, decision markets, or virtual stock markets. Essentially it's all the same thing. What they have in common is that in all of these markets, users place bets or vote on the outcome of various events. The main value of prediction markets lies in shaping the value of the outcomes that users vote on. Cost helps analyze and even predict anything: from small sporting events to management decision-making options.

We will consider CTF within the framework of a prediction market implemented on the blockchain, so you need to understand that the main logic will be executed on smart contracts.

You can look at the diagram below to understand in a simplified way what the prediction market is.

The prediction market on the diagram has an event “GPT-5 released in 2024?”. The user votes for one of the outcomes “Yes” or “No”. Essentially making a bet. Under the hood he will buy some quantity share token for the base token. The Share token determines each outcome. If his bet wins, he will receive a reward – essentially a win.

Since the prediction market is an isolated system implemented using smart contracts on the blockchain, data on the results of the event will be provided by a special oracle that is trusted by the protocol, and so will we.

Separately, it is worth saying that the user, having received possession of a share token, can at any time, while the event is not calculated and there are no results on it, sell and exchange the share token back to his underlying asset. It turns out that this is the basis for trading. I am not a trader myself and I am not encouraging you to do so.

Basic combinatorics

Essentially, CTF is used to form combinations of outcomes. For the “GPT-5 released in 2024?” event that we discussed above, there are only two defined outcomes. We will give the outcomes code names “Y” and “N”.

However, the prediction market can be large, and there can also be many events in it. Let's look at another prediction “Will GPT limit requests to 10 per hour until August?”. This prediction also has two outcomes. Let's give them code names “B” and “A”.

These two predictions are directly related to each other, and we can look at all possible outcomes within the framework of already two events.

Also, do not forget about the reverse order of combinations.

We consider that the user takes part in voting based on two predictions at once. As a result, we have eight different options for user behavior, taking into account the order of his actions (first the first prediction, then the second and vice versa). In the classic approach to building a prediction market, these are actually eight variations.

Now here we bring to the fore CTF, which allows us to simplify the number of combinations to 4 due to the fact that everything happens on one contract and reciprocal conditions are processed deep under the hood, or rather, opposite combinations are combined.

Within the CTF, each of the four combinations is called position. That is, this means that each position can be a user’s prediction for several events at once. Moreover, this approach allows you to create forecasts that not only correlate, but also depend on each other. For example, “How much will Bitcoin cost if Trump wins the election?”

CTF Use Cases

Everything we talked about above leads us to the idea that the conditional token framework (CTF) can only be used for the prediction market. And indeed this is so. The user expresses his opinion by voting with an asset, and this allows him to collect the most accurate forecast for a group of users. This forecast is highly likely to be close to the truth.

However, what is hidden behind the voting process is not immediately noticeable. This is the purchase of a share token at a specific market price. Moreover, prediction markets not only offer purchases; owners of a share token can sell them at any time. The sale will also be at market price. Did you catch the connection? It remains to add a couple of tools, for example, limit orders, and we already have something like an exchange based on the prediction market.

The protocol itself believes that CTF is not limited to prediction markets alone and believes that it can be applied in areas such as:

  • gameFI. In a gaming context, tokens can be used to create rewards or achievements that are contingent on meeting certain conditions in the game. For example, players can receive tokens for completing difficult tasks or for reaching certain levels in the game.

  • payment systems. For example, tokens can be used to implement purchase agreements where payments are only made when predefined conditions are met, such as delivery of a product or completion of a service.

  • options. For example, a token can be created to exercise an option that is activated if the price of an asset reaches a certain level.

  • and so on, we take any subject area, add a “condition” to it, for example, a token implements a condition, then something works in the subject area.

However, I am skeptical about other use cases, it seems to me that they are far-fetched, and the most successful application is prediction markets with the ability to buy or sell a share token. If you don't believe me, then readwhat gnosis itself writes about this and draw your own conclusion.

AMM for prediction markets

What does AMM have to do with it, you ask? After all, the prediction market is not a dex. However, in order to be able to buy and sell share identifying a token with your forecast, bet or position, you must consider the value of the share token.

Let's imagine that on the prediction market there is an event “Will Portugal win the World Cup this year?” This event has 100 share tokens “for” and 100 share tokens “against” with a fixed price. I don’t believe in a positive outcome of this event, I vote against it and buy back 70 corresponding share tokens. Anyone can also think the same way as me, and at some point a situation may arise that there will not be enough tokens for the vote of a new user. To avoid this situation use Automated Market Maker. Exactly the same technology for automatically generating the value of an asset is used for decentralized exchangers (dex). The cost calculation is based on the ratio of the number of asset reserves in the pool (on the smart contract).

Essentially, the market maker uses an algorithm and mathematics to ensure that the reserves for the two assets in the pool do not run out. Therefore, when one asset becomes smaller than the second, the price of the first rises. This is also true for the second asset.

Gnosis offers two market makers:

CPMM

Uses the same algorithm as Uniswap and Balancer pools. Uses exactly the same formula:

x * y = k

Where:

  • x – the amount of one asset in the liquidity pool,

  • y – the amount of another token in the liquidity pool

  • k – a constant value representing the product of the quantity of two assets x And y in the pool. The value of k remains unchanged for each transaction, which is the main invariant of this model.

The advantage of this market maker is that its mathematics is quite simple. There is no point in programming complex logarithms.

LMSR

Overall, initially, LMSR is a pricing mechanism designed for use in prediction markets. It uses a logarithmic function to determine the probability of an outcome. The more users vote for a certain outcome, the higher the probability (and therefore the cost) of that outcome.

C(q) = b \cdot \ln\left( \sum_{i} e^{q_i / b} \right)

Where:

  • C(q) is a function of cost.

  • q[i] – number of bets on the i-th outcome of the event

  • b – a liquidity parameter that regulates how quickly prices change when the volume of bets changes.

It is more scientific and the most researched compared to CPMM. Allows you to better control the risks and volatility of assets due to the fact that the maximum losses are limited by the liquidity parameter b.

Framework

After everything we discussed above, the conclusion suggests itself that the framework is complex and includes two large modules:

  • Conditional tokens. It is an ERC-1155 based condition token. Includes logic responsible for calculating the user's position based on events.

  • AMM for conditional tokens. Essentially liquidity, which is responsible for trading position tokens and pricing.

These two large modules are in different repositories. You can add a separate repository to them, which contains utilswhich are used for AMM.

C util-contracts interesting story. This repository is used in smart contracts of market maker factories. For example in FixedProductMarketMakerFactory.sol.

import { ConstructedCloneFactory } from "@gnosis.pm/util-contracts/contracts/ConstructedCloneFactory.sol";

However, if we go to the util-contracts repository itself, we will not find a smart contract in it ConstructedCloneFactory.sol. Apparently something went wrong and he did not survive the alpha version, but it is possible to find him. Can be pulled from npm versions. According to this one link.

Technical analysis

Below we will analyze the code of each module of the framework separately. This section is intended for people familiar with development.

Conditional tokens

Conditional tokens allow you to organize the process of working with share tokens so that users can make predictions. Allows you to combine forecasts by identifying the user’s opinion.

Logics conditional tokens implemented in this repositories.

Essentially, the main smart contract that we need is ConditionalTokens.sol. It is the entry point from which to begin studying. The smart contract is inherited from ERC1155.sol and uses a library to store auxiliary functions CTHelpers.sol.

❗️ To move on, it is necessary to discuss terminology. Now it will be a little difficult, but it is very important for further understanding of the article.

Condition and outcomes

First of all, about a forecast, also known as a prediction, an event, a statement, a question. We will call everything listed in the code – conditionits possible outcomes are outcomes.

Essentially condition – this is a question for the oracle that he will have to answer. Such condition requires its own identifier (conditionId), which is the result of hashing three parameters:

  • oracle. The address of the oracle that will calculate the result of the event.

  • questionId. Identifier for condition outside. The data type is assumed to be bytes32 and is left to the discretion of the forecaster via a smart contract call. It could be a counter, where each new prediction adds one to the counter, or it could be a more complex scheme with hashing of text and other data. Will be used in oracle.

  • outcomeSlotCount. Number of outcomes to predict.

To obtain the forecast ID, use the function getConditionId().

Index set

The outcome information (true or false) will be encoded using an array of bits called Index set. For example, there are three outcomes: A, B, C. Then the truth of A and B can be encoded in bits, starting with the least significant bit 0xCBA -> 0x011where one means that the outcome is true, 0 means false.

CollectionId

Index Set allows you to encode the outcome of an event to show what the user is voting for. Each such binary encoding can have a decimal representation. For example, 0x011 -> 3. So that the smart contract can clearly determine that this index set relates to a specific event will be generated collectionId based on:

Essentially index set allows you to describe all possible combinations for which the user can vote. Each of these combinations will have its own identifier called collectionId.

To obtain the identifier of the collection of outcomes, use the function getCollectionId().

Position

The last term that remains to be discussed is position. This is nothing more than a collection of user-selected outcomes (which are encoded using an index set) and a position collateral asset. A collateral asset is the base token of the prediction market for which a share token of a certain outcome is purchased.

By analogy, a position also has an identifier. Called positionId. The ERC-1155 token id will correspond to positionId. Since it is an ERC-1155 standard, there are a number of functions (safeTransferFrom(), setApprovalForAll()), which will allow you to transfer the share token and, along with it, the right to own the position to other accounts.

To obtain the position identifier, use the function getPositionId().

Prepare

In order for the smart contract ConditionalTokens.sol I found out about a new condition, I need to tell him about it. This process is called “event preparation” and involves calling a function prepareCondition().

function prepareCondition(address oracle, bytes32 questionId, uint outcomeSlotCount) external {
    // Ограничение по количеству исходов
    require(outcomeSlotCount <= 256, "too many outcome slots");
    require(outcomeSlotCount > 1, "there should be more than one outcome slot");

    // Генерация идентификатора для прогноза
    bytes32 conditionId = CTHelpers.getConditionId(oracle, questionId, outcomeSlotCount);

    // Проверяем, что такой прогноз еще не был создан
    require(payoutNumerators[conditionId].length == 0, "condition already prepared");

    // Создание массива слотов для исходов с привязкой к идентификатору прогноза
    payoutNumerators[conditionId] = new uint[](outcomeSlotCount);

    emit ConditionPreparation(conditionId, oracle, questionId, outcomeSlotCount);
}

Here you need to especially look at what kind of storage system is used for conditions. Two mappings are responsible for this:

mapping(bytes32 => uint[]) public payoutNumerators;
mapping(bytes32 => uint) public payoutDenominator;

The first mapping payoutNumerators the oracle will record the result of each outcome. The result for each outcome will indicate 0 – the outcome is false, 1 – the outcome is correct. For three outcomes, it may well be that two out of three are true. Then the oracle will return the result for each outcome in the following form [0.5, 0.5, 0].

Solidity cannot work with fractional numbers. Therefore, a classic approach is used, as with ERC-20, where the concept of decimal is introduced. This is the number you need to divide your balance by to get the result as a whole and a fraction. In our case, the second mapping is used instead of decimal payoutDenominatorwhich stores the number to which numerator is from payoutNumerators will share.

Reporting

Once an event is added to smart contracts, after the time has elapsed, it can be executed by the oracle. In order for an event to be executed only by a specific oracle at the stage prepare included in conditionId includes the oracle address. At the stage when the oracle will call the function reportPayouts(), conditionId will be generated again, only in place of the oracle address will be used msg.sender.

bytes32 conditionId = CTHelpers.getConditionId(msg.sender, questionId, outcomeSlotCount);

If msg.sender will be a different address from the originally set oracle address, then conditionId will also be generated incorrectly.

Full function code reportPayouts() below.Splitting

// payouts - список результатов для каждого исхода
function reportPayouts(bytes32 questionId, uint[] calldata payouts) external {
    // Количество результатов должно совпадать с количеством исходов
    uint outcomeSlotCount = payouts.length;
    require(outcomeSlotCount > 1, "there should be more than one outcome slot");
    // Генерируем идентификатор для прогноза, который должен совпадать с целевым идентификатором прогноза
    bytes32 conditionId = CTHelpers.getConditionId(msg.sender, questionId, outcomeSlotCount);
    require(payoutNumerators[conditionId].length == outcomeSlotCount, "condition not prepared or found");
    require(payoutDenominator[conditionId] == 0, "payout denominator already set");

    // Сохраняем результаты от оракула и рассчитываем denominator для значений
    uint den = 0;
    for (uint i = 0; i < outcomeSlotCount; i++) {
        uint num = payouts[i];
        den = den.add(num);

        require(payoutNumerators[conditionId][i] == 0, "payout numerator already set");
        payoutNumerators[conditionId][i] = num;
    }
    require(den > 0, "payout is all zeroes");
    payoutDenominator[conditionId] = den;
    emit ConditionResolution(conditionId, msg.sender, questionId, outcomeSlotCount, payoutNumerators[conditionId]);
}

Splitting

Somewhere between the preparation of an event and the moment when the oracle calculates this event, there will be a process where the user can vote for the selected outcome.

This process is called splittingbecause it implies dividing your asset into several positions and receiving a share of the share token in each position. The process includes two types of separation:

  1. Splitting collateral. This is when a user buys share tokens, votes by transferring the underlying asset, and places a bet on one or more outcomes.

  2. Splitting position. This is when the user divides his position into several components of this position.

With the underlying asset, everything is quite simple: you deposit the specified amount into the smart contract, and it is indicated for the selected positions.

With the division of position everything is a little more interesting. You can only split positions that are composite positions and contain multiple outcomes.

However, there are cases where separation is not possible. For example, you cannot divide a position that combines two events into two independent positions in each of the events. Or it is impossible to divide the underlying asset in such a way as to cover all possible outcomes and get a benefit in any case. More examples of invalid splits can be found in documentation.

In order to do splitting you need to call the function splitPosition().

function splitPosition(
    IERC20 collateralToken,
    bytes32 parentCollectionId,
    bytes32 conditionId,
    uint[] calldata partition,
    uint amount
) external {
    // Проверяем, что передан массив позиций для разделения
    require(partition.length > 1, "got empty or singleton partition");
    uint outcomeSlotCount = payoutNumerators[conditionId].length;
    // Проверяем, что событие под таким идентификатором существует
    require(outcomeSlotCount > 0, "condition not prepared yet");

    // Готовим маску по количеству исходов. для 4 исходов будет 0x1111, для пяти 0x11111
    uint fullIndexSet = (1 << outcomeSlotCount) - 1;
    // Будет отвечать за результат по позициям, которые разделяет пользователь
    uint freeIndexSet = fullIndexSet;
    uint[] memory positionIds = new uint[](partition.length);
    uint[] memory amounts = new uint[](partition.length);
    for (uint i = 0; i < partition.length; i++) {
        uint indexSet = partition[i];
        // Проверяет, что indexSet в диапазоне возможных комбинаций по событию
        require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set");
        require((indexSet & freeIndexSet) == indexSet, "partition not disjoint");
        freeIndexSet ^= indexSet;
        // Получение нового идентификатора позиции для index set
        positionIds[i] = CTHelpers.getPositionId(collateralToken, CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet));
        amounts[i] = amount;
    }

    if (freeIndexSet == 0) {
        // Разделение базового актива
        if (parentCollectionId == bytes32(0)) {
            // Переводим базовый актив от пользователя на смарт-контракт
            require(collateralToken.transferFrom(msg.sender, address(this), amount), "could not receive collateral tokens");
        } else {
            // Сжигаем share токены текущей позиции
            _burn(
                msg.sender,
                CTHelpers.getPositionId(collateralToken, parentCollectionId),
                amount
            );
        }
    } else {
        // Разделение позиции
        // Подразумевает сжигание текущей позиции
        _burn(
            msg.sender,
            CTHelpers.getPositionId(collateralToken,
                CTHelpers.getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)),
            amount
        );
    }

    // Минтинг share токенов для новых позиций
    _batchMint(
        msg.sender,
        positionIds,
        amounts,
        ""
    );

    emit PositionSplit(msg.sender, collateralToken, parentCollectionId, conditionId, partition, amount);
}

Merging

When a user votes, which is equivalent to the term “split a position” or “split the underlying asset”, he gave some amount of the underlying asset to the smart contract.

In order to exit voted positions (withdraw the underlying asset back), there is a process called merging (merger). It also involves merging not only to the underlying asset, but also to a certain intermediate position.

We can safely say that mergePositions() – this is the complete inverse function to splitPosition().

To initiate this process you need to call the function mergePositions().

function mergePositions(
    IERC20 collateralToken,
    bytes32 parentCollectionId,
    bytes32 conditionId,
    uint[] calldata partition,
    uint amount
) external {
    // Проверяем, что передан массив позиций для слияния
    require(partition.length > 1, "got empty or singleton partition");
    uint outcomeSlotCount = payoutNumerators[conditionId].length;
    // Проверяем, что событие под таким идентификатором существует
    require(outcomeSlotCount > 0, "condition not prepared yet");

    // Готовим маску по количеству исходов. для 4 исходов будет 0x1111, для пяти 0x11111
    uint fullIndexSet = (1 << outcomeSlotCount) - 1;
    uint freeIndexSet = fullIndexSet;
    uint[] memory positionIds = new uint[](partition.length);
    uint[] memory amounts = new uint[](partition.length);
    for (uint i = 0; i < partition.length; i++) {
        uint indexSet = partition[i];
        // Проверяет, что indexSet в диапазоне возможных комбинаций по событию
        require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set");
        require((indexSet & freeIndexSet) == indexSet, "partition not disjoint");
        freeIndexSet ^= indexSet;
        // Получение нового идентификатора позиции для index set
        positionIds[i] = CTHelpers.getPositionId(collateralToken, CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet));
        amounts[i] = amount;
    }
    // Сжигаем share токен согласно переданным позициям
    _batchBurn(
        msg.sender,
        positionIds,
        amounts
    );

    if (freeIndexSet == 0) {
        // Слияние до базового актива
        if (parentCollectionId == bytes32(0)) {
            // Отсылаем базовый актив пользователю
            require(collateralToken.transfer(msg.sender, amount), "could not send collateral tokens");
        } else {
            _mint(
                msg.sender,
                CTHelpers.getPositionId(collateralToken, parentCollectionId),
                amount,
                ""
            );
        }
    } else {
        // Слияние до промежуточной позиции
        _mint(
            msg.sender,
            CTHelpers.getPositionId(collateralToken,
                CTHelpers.getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)),
            amount,
            ""
        );
    }

    emit PositionsMerge(msg.sender, collateralToken, parentCollectionId, conditionId, partition, amount);
}

Redeem position

The last thing to consider here is receiving a reward after the vote has passed and the oracle has calculated the event. Redeem (redemption) of a position will be possible only if the outcome of the event is recognized as true.

In order to collect (redeem) your reward you need to call the function redeemPosition().

// Параметр indexSets называется не partition. Потому что не требуется передавать кодированное разделение позиции, можно передать список всех позиций
function redeemPositions(IERC20 collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint[] calldata indexSets) external {
    // Проверяем, что событие есть и оно рассчитано
    // Деноминатор будет проставлен оракулом, когда он вызовет функцию reportPayouts()
    uint den = payoutDenominator[conditionId];
    require(den > 0, "result for condition not received yet");
    uint outcomeSlotCount = payoutNumerators[conditionId].length;
    require(outcomeSlotCount > 0, "condition not prepared yet");

    uint totalPayout = 0;

    // Готовим маску по количеству исходов. для 4 исходов будет 0x1111, для пяти 0x11111
    uint fullIndexSet = (1 << outcomeSlotCount) - 1;
    // Цикл по всем переданным позициям
    for (uint i = 0; i < indexSets.length; i++) {
        uint indexSet = indexSets[i];
        // Проверяет, что indexSet в диапазоне возможных комбинаций по событию
        require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set");
        // Получаем идентификатор позиции
        uint positionId = CTHelpers.getPositionId(collateralToken,
            CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet));

        uint payoutNumerator = 0;
        for (uint j = 0; j < outcomeSlotCount; j++) {
            if (indexSet & (1 << j) != 0) {
                payoutNumerator = payoutNumerator.add(payoutNumerators[conditionId][j]);
            }
        }

        uint payoutStake = balanceOf(msg.sender, positionId);
        if (payoutStake > 0) {
            // Рассчитываем количество вознаграждения
            totalPayout = totalPayout.add(payoutStake.mul(payoutNumerator).div(den));
            // Сжигаем share токены по позиции
            _burn(msg.sender, positionId, payoutStake);
        }
    }

    // Выплачиваем вознаграждение или минтим share токены, если указана родительская коллекция
    if (totalPayout > 0) {
        if (parentCollectionId == bytes32(0)) {
            require(collateralToken.transfer(msg.sender, totalPayout), "could not transfer payout to message sender");
        } else {
            _mint(msg.sender, CTHelpers.getPositionId(collateralToken, parentCollectionId), totalPayout, "");
        }
    }
    emit PayoutRedemption(msg.sender, collateralToken, parentCollectionId, conditionId, indexSets, totalPayout);
}

Market maker

The protocol implements both types of market maker on smart contracts. Both CPMM and LMSR. In this section we will look at the code for just one of them – CPMM. It is easier to understand from a mathematical point of view.

The name of the smart contract that implements CPMM is FixedProductMarketMaker.sol. Each market maker has its own factory. For FixedProductMarketMaker, it is logical that the factory is called FixedProductMarketMakerFactory.sol.

It all works approximately as follows.

In order to create a new event, the market of which will work according to the CPMM rules, you need to call the function createFixedProductMarketMaker()which by expanding the factory ConstructedCloneFactory.sol will create a smart contract for a new market and deploy it.

The market maker will be the intermediate recipient of the ERC-1155 token. The token will be sent to the user who called the transaction. To be able to receive an ERC-1155 token, FixedProductMarketMakerFactory.sol has two functions: onERC1155Received() And onERC1155BatchReceived().

It turns out that the market maker is a layer between the user and conditionalTokens.

Liquidity

Since this is an AMM, the first thing to start with is that it is a pool that requires liquidity. That is, here is the whole story accompanying this: liquidity providers, LP tokens and the ability to set the value of an asset to the first supplier. In our case, it is not the cost that is specified, but the ratio of the probabilities of the occurrence of outcomes.

The supplier's liquidity will be distributed across all condition tokens in a normalized manner, according to the ratio of tokens in each outcome. This means that when a provider contributes liquidity, some amount of a condition token is issued for all possible outcomes.

To calculate liquidity distribution, condition token balances are used and the smart contract receives them in a private function getPoolBalances().

Technically, in order to add liquidity, you need to use the function addFunding(). Let's take a closer look at it.

function addFunding(uint addedFunds, uint[] calldata distributionHint)
    external
{
    // Проверяем, что указана сумма ликвидности, которая будет добавлена
    require(addedFunds > 0, "funding must be non-zero");

    uint[] memory sendBackAmounts = new uint[](positionIds.length);

    uint poolShareSupply = totalSupply();
    uint mintAmount;
    if(poolShareSupply > 0) {
        // Если ликвидность добавляется не в первый раз, то distributionHint должен быть пустым
        require(distributionHint.length == 0, "cannot use distribution hint after initial funding");
        uint[] memory poolBalances = getPoolBalances();
        uint poolWeight = 0;
        // Высчитывается пул с максимальным балансом, Необходимо для распределения ликвидности по токенам условия
        for(uint i = 0; i < poolBalances.length; i++) {
            uint balance = poolBalances[i];
            if(poolWeight < balance)
                poolWeight = balance;
        }

        for(uint i = 0; i < poolBalances.length; i++) {
            uint remaining = addedFunds.mul(poolBalances[i]) / poolWeight;
            // Рассчитываем сумму condition токенов, которые получит поставщик ликвидности
            sendBackAmounts[i] = addedFunds.sub(remaining);
        }

        // Рассчитываем количество LP токена, которое можно получить
        mintAmount = addedFunds.mul(poolShareSupply) / poolWeight;
    } else {
        // Если добавляется ликвидность в первый раз, то distributionHint будет задано правило распределения ликвидности по пулам
        if(distributionHint.length > 0) {
            require(distributionHint.length == positionIds.length, "hint length off");
            uint maxHint = 0;
            for(uint i = 0; i < distributionHint.length; i++) {
                uint hint = distributionHint[i];
                if(maxHint < hint)
                    maxHint = hint;
            }

            for(uint i = 0; i < distributionHint.length; i++) {
                uint remaining = addedFunds.mul(distributionHint[i]) / maxHint;
                require(remaining > 0, "must hint a valid distribution");
                // Рассчитываем сумму condition токенов, которые получит поставщик ликвидности
                sendBackAmounts[i] = addedFunds.sub(remaining);
            }
        }

        mintAmount = addedFunds;
    }

    // Перевод активов от пользователя на смарт-контракт
    require(collateralToken.transferFrom(msg.sender, address(this), addedFunds), "funding transfer failed");
    require(collateralToken.approve(address(conditionalTokens), addedFunds), "approval for splits failed");

    // Создание позиции для поставщика ликвидности
    splitPositionThroughAllConditions(addedFunds);

    // Создание LP токена
    _mint(msg.sender, mintAmount);

    // Перевод токенов условия вызывающему транзакцию
    conditionalTokens.safeBatchTransferFrom(address(this), msg.sender, positionIds, sendBackAmounts, "");

    for (uint i = 0; i < sendBackAmounts.length; i++) {
        sendBackAmounts[i] = addedFunds.sub(sendBackAmounts[i]);
    }

    emit FPMMFundingAdded(msg.sender, sendBackAmounts, mintAmount);
}

To withdraw liquidity you need to call the function removeFunding(). Lp tokens will be burned and the provider will receive their underlying asset back.

Why is liquidity needed? The more liquidity in the pool, the more resistant the market is to large trades and price manipulation. The ability to add liquidity to users allows you to earn on commissions.

Buy and sell

function buy(uint investmentAmount, uint outcomeIndex, uint minOutcomeTokensToBuy) external {
    // Рассчитываем сумму покупки токенов исхода (share токенов по конкретному исходу)
    uint outcomeTokensToBuy = calcBuyAmount(investmentAmount, outcomeIndex);
    // Проверяем допустимо минимальную сумму на которую мы согласны
    require(outcomeTokensToBuy >= minOutcomeTokensToBuy, "minimum buy amount not reached");

    // Переводим базовый актив на смарт-контракт маркет мейкера
    require(collateralToken.transferFrom(msg.sender, address(this), investmentAmount), "cost transfer failed");

    // Рассчитываем комиссию в базовом активе
    uint feeAmount = investmentAmount.mul(fee) / ONE;
    feePoolWeight = feePoolWeight.add(feeAmount);
    uint investmentAmountMinusFees = investmentAmount.sub(feeAmount);
    require(collateralToken.approve(address(conditionalTokens), investmentAmountMinusFees), "approval for splits failed");

    // Сплитим на все позиции
    splitPositionThroughAllConditions(investmentAmountMinusFees);
    // Переводим токены позиций сендеру транзакции
    conditionalTokens.safeTransferFrom(address(this), msg.sender, positionIds[outcomeIndex], outcomeTokensToBuy, "");

    emit FPMMBuy(msg.sender, investmentAmount, feeAmount, outcomeIndex, outcomeTokensToBuy);
}

Here the question may arise, what kind of splitting is for all positions (splitPositionThroughAllConditions). It's very simple. The bet is placed on all possible outcomes.

function splitPositionThroughAllConditions(uint amount)
    private
{
    for(uint i = conditionIds.length - 1; int(i) >= 0; i--) {
        uint[] memory partition = generateBasicPartition(outcomeSlotCounts[i]);
        for(uint j = 0; j < collectionIds[i].length; j++) {
            conditionalTokens.splitPosition(collateralToken, collectionIds[i][j], conditionIds[i], partition, amount);
        }
    }
}

There is a similar challenge for selling conditional tokens. The function is responsible for this sell(). Only for the place of splitting for all positions the function is called mergePositionsThroughAllConditions().

function sell(uint returnAmount, uint outcomeIndex, uint maxOutcomeTokensToSell) external {
    // Рассчитываем сумму токенов исходов которая будет обмениваться на базовый актив
    uint outcomeTokensToSell = calcSellAmount(returnAmount, outcomeIndex);
    require(outcomeTokensToSell <= maxOutcomeTokensToSell, "maximum sell amount exceeded");

    // Переводим токены исходов на смарт-контракт маркет мейкера
    conditionalTokens.safeTransferFrom(msg.sender, address(this), positionIds[outcomeIndex], outcomeTokensToSell, "");

    // Рассчитываем комиссию
    uint feeAmount = returnAmount.mul(fee) / (ONE.sub(fee));
    feePoolWeight = feePoolWeight.add(feeAmount);
    uint returnAmountPlusFees = returnAmount.add(feeAmount);
    // Мерджим позиции до базовго актива
    mergePositionsThroughAllConditions(returnAmountPlusFees);

    // Отправляем базовый активы сендеру транзакции
    require(collateralToken.transfer(msg.sender, returnAmount), "return transfer failed");

    emit FPMMSell(msg.sender, returnAmount, feeAmount, outcomeIndex, outcomeTokensToSell);
}

Conclusion

It's time to say who needs all this. The protocol gives examples of three projects that use CTF:

I don’t know anything about these protocols, but I know another example – this is the prediction market Polymarket. He successfully implemented CTF and built his own logic on top of limit orders. This is a fairly illustrative case, because polymarket is one of the most popular markets and has a fairly large number of users.

To summarize, CTF is a powerful tool for implementing a combination of event outcomes and various types of manipulation with them. However, it is not quite easy to understand. Requires immersion in combinatorics, working with bits of information, and so on. At the same time, it is almost the only solution on the basis of which it is possible to build a prediction market. Gnosis is still open to new uses for its framework, but it's worth noting that not many use cases have been conceived since its inception.

Thank you for reading this article to the end! I will be glad to receive any feedback!

Links

  1. Gnosis introduction

  2. Conditional Tokens Contracts repository

  3. Conditional Tokens – Automated Market Makers repository

By the way, in our telegram channel we publish the latest news from the world of web3 and our thoughts on them.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *