В статье я расскажу про то, что такое Assurance Contract, также известные как механизм предоставления общественных и групповых благ, и вместе напишем свою собственную реализацию краудфандинга на основе Автономных Агентов на платформе Obyte.

Assurance Contract – это механизм организованного сбора средств для создания нового общественного блага. Типичным примером использования таких контрактов может быть следующая ситуация: участники дачного кооператива решили построить дорогу для удобного подъезда к своим дачам. Для этого необходимо собрать сумму N и передать её подрядчику для постройки дороги. Возникает много вопросов: где найти человека, которому бы доверяли все и поручить сбор, как заставить всех потенциальных пользователей дороги вложиться в её постройку, что будет, если собранных денег не хватит, и др.

Assurance contracts очень схожи по смыслу с краудфандингом, только вкладчики не подвержены риску, как в случае с инвесторами, которые вкладывают деньги в рискованные предприятия. Такие контракты, как и краудфандинг, могут также иметь два исхода — успешный и неуспешный, в зависимости от достижения таргета по собранной сумме. Результатом успешного сбора будет являться получение вкладчиками, но иногда не только ими, нового общественного блага. В случае неуспеха деньги просто возвращаются вкладчикам.

Итак, для начала повторим основные условия. Суть краудфандинга – сбор средств «толпой» на заранее определённую цель, с заданной финальной суммой (не обязательно фиксированной, может быть динамической) и датой окончания (тоже не всегда, но как правило). При достижении данной суммы раунд финансирования считается успешным и сбор прекращается, все средства направляются на реализацию цели. В случае неуспеха, то есть по достижению дедлайна и при недостаточной сумме собранных средств, все средства возвращаются обратно донорам.

Какие проблемы решает криптовалютный краудфандинг? Во-первых, избавляет вкладчиков от необходимости доверять и проверять того, кому они отправляют свои деньги. Код автономного агента для сбора средств открыт и каждый вкладчик может удостовериться, что он может забрать свой вклад в любой момент, пока не достигнута финальная сумма, а так же, что все средства будут направлены на заранее выбранный адрес для создания блага. Во-вторых, прозрачный учёт всех собранных средств помогает распределять будущие блага от реализации цели пропорционально сделанному вкладу.

Ещё одна проблема, которую можно решить с помощью Assurance Contract — проблема координации. Сегодня мы будем дополнять нашу прошлую статью про игру «Атака 51%», и как раз будем решать именно эту проблему. Без использования АА участники игры будут подвержены риску «нехватки» собранной суммы из-за недостаточной координации. Тогда те, кто уже успел сделать вложение, просто потеряют свои деньги из-за других участников, не проявивших интерес, или просто не успевших это сделать (помним, что у команд-соперников есть ровно 24 часа на оспаривание текущего лидера). Собирая деньги через assurance contract, игроки в любой момент будут иметь возможность забрать свои деньги до момента, пока не соберётся необходимая сумма.

Не применительно в нашем случае, но ещё одним полезным свойством таких контрактов, является уменьшение влияние «эффекта безбилетника» или «free rider problem». Так как благо общественное, то есть либо неотчуждаемое, либо сложное в контролировании доступа к нему, проблема безбилетника является основной при организации общественных благ. Люди не желают вкладываться в то, чем другие смогут пользоваться бесплатно, либо же сам вкладчик, придерживаясь абсолютно рационального экономического поведения и выбирая стратегию «невкладывания», в случае успешного сбора, скорее всего сможет пользоваться созданным благом.

Я также расскажу про вариант оптимизированного assurance contract, который повышает шансы достижения успеха по сбору средств, т.к. рациональным поведением участников уже будет выбор стратегии вкладывания, а не игнорирования.

Играем в игру коллективно


Для нашего примера мы возьмём уже известную по прошлой статье игру «Атака 51%».
В её текущей реализации участники игры отправляют деньги на адрес АА игры. Но алгоритм можно улучшить и уменьшить шансы потери денег участниками в случае проигрыша их команды.
Для этого любой участник команды может организовать краудфандинг с динамическим таргетом, равным как минимум 51% от всего собранного пула игры. При увеличении пула финальный таргет краудфандинга тоже будет смещаться. И только в случае успеха деньги будут отправлены на адрес АА игры и команда сразу же станет текущим лидером. Если же такое событие так никогда и не наступит, то участники не потеряют свои деньги, впустую увеличивая пул игры, а просто заберут деньги с АА краудфандинга.

Оригиналы обоих автономных агентов всегда доступны в онлайн редакторе кода Oscript в виде шаблонов, достаточно выбрать их из выпадающего меню: «51% attack game» и «Fundraising proxy».

Прежде чем начать писать АА на языке Oscript, настоятельно рекомендую прочитать Getting Started Guide (eng) в нашей документации, чтобы быстро ознакомиться с основными принципами написания АА.

Пишем код


Сначала распишем алгоритм: донор отправляет байты на адрес АА, получая взамен краудфандинговые токены в пропорции 1 к 1. В любой момент он может обменять эти токены обратно на байты. После получения байт, АА проверяет, что мы достигли цели по сбору средств, и если это так, то отправляет все байты на адрес АА игры и получает взамен игровые токены (токены команды). Донор теперь может обменять имеющиеся у него краудфандинговые токены на игровые токены, которые, в будущем, в случае победы команды, уже можно обменять на байты через АА игры, получив до x2 байт от первоначально вложенных.

Итак, приступим.

Блок обработки старта нового периода фандрайзинга. При получении поля «start» во входящем data-сообщении на адрес нашего АА, мы выпустим наш краудфандинговый токен и запишем его в state, также вернём его в качестве ответа вызывающему.

{ // start a new fundraising period
    if: `{trigger.data.start AND !$asset}`,
    messages: [
        {
            app: 'asset',
            payload: {
                is_private: false,
                is_transferrable: true,
                auto_destroy: false,
                fixed_denominations: false,
                issued_by_definer_only: true,
                cosigned_by_definer: false,
                spender_attested: false
            }
        },
        {
            app: 'state',
            state: `{
                var[response_unit || '_status'] = 'open';
                var['asset'] = response_unit;
                response['asset'] = response_unit;
            }`
        }
    ]
},

Обработка депозитов от доноров. Сперва отсеем «мусорные» транзакции с очень маленькими суммами, которых не хватит даже на комиссию. Далее проверка базовых условий, что игра не завершена и сбор возможен. Полный листинг блока:

{ // contribute
    if: `{trigger.output[[asset=base]] >= 1e5 AND $asset}`,
    init: `{
        if (var[$destination_aa]['finished'])
            bounce('game over');
        $amount = trigger.output[[asset=base]] - 2000; // to account for fees we need to respond now and to refund bytes or pay shares later
        $total_raised = var['total_raised'] + $amount;
        $missing_amount = ceil((balance[$destination_aa][base] + $total_raised)*0.51) - var[$destination_aa]['team_' || $team || '_amount'];
        $bDone = ($total_raised > $missing_amount);
    }`,
    messages: [
        {
            app: 'payment',
            payload: {
                asset: "{$asset}",
                outputs: [{address: "{trigger.address}", amount: "{$amount}"}]
            }
        },
        {
            if: `{$bDone}`,
            app: 'payment',
            payload: {
                asset: "base",
                outputs: [{address: "{$destination_aa}", amount: "{$total_raised}"}]
            }
        },
        {
            if: `{$bDone}`,
            app: 'data',
            payload: {
                team: "{$team}"
            }
        },
        {
            app: 'state',
            state: `{
                if ($bDone)
                    var[$asset || '_status'] = 'raised';
                else
                    var['total_raised'] = $total_raised;
            }`
        }
    ]
},

Сразу после начальных проверок идёт основная логика нашего АА:

$total_raised = var['total_raised'] + $amount; — здесь мы суммируем только что полученный amount к общей сумме средств на агенте.

$missing_amount = ceil((balance[$destination_aa][base] + $total_raised)*0.51) - var[$destination_aa]['team_' || $team || '_amount']; — проверяем, что новой суммы собранных средств достаточно для победы в игре. Обратите внимание на обращение к балансу и состоянию переменных другого АА: balance[$destination_aa][base] и var[$destination_aa]['team_' || $team || '_amount'].

Всё это мы делаем в блоке init, который вызывается каждый раз перед обработкой сообщений транзакции. Ответом на входящую транзакцию, очевидно, будет отправка токенов АА в пропорции один к одному относительно полученных байт (первый блок массива messages). Второй и третий блоки будут выполнены только если локальная переменная $bDone выставлена в true (она выставляется в блоке init). В них мы отправим все средства с адреса этого АА на адрес АА игры, получив взамен игровые токены. В последнем блоке сообщений мы просто обновляем состояние, выставляя нужные статусы и собранную сумму.

Обработка полученных токенов от АА игры, они уже лежат на балансе нашего АА, нам необходимо только поменять состояние агента:

{ // received team asset
    if: `{trigger.output[[asset=var[$destination_aa]['team_' || $team || '_asset']]] AND $asset}`,
    messages: [
        {
            app: 'state',
            state: `{
                var[$asset || '_status'] = 'done';
                var['asset'] = false;
                var['total_raised'] = false;
            }`
        }
    ]
},

Рефанд вложений игроками. Мы разрешаем в любой момент отказаться от участия и забрать свои байты назад. Для этого игрок присылает токены этого АА, а мы отправляем ему ответной транзакцией столько же байт:

{ // refund
    if: `{$asset AND trigger.output[[asset=$asset]] > 0}`,
    init: `{
        $amount = trigger.output[[asset=$asset]];
    }`,
    messages: [
        {
            app: 'payment',
            payload: {
                asset: "base",
                outputs: [{address: "{trigger.address}", amount: "{$amount}"}]
            }
        },
        {
            app: 'state',
            state: `{
                var['total_raised'] -= $amount;
            }`
        }
    ]
},

Обмен токенов нашего АА на токены игры. Мы просто отправляем игровые токены, которые лежат на балансе нашего АА (в случае прохождения блока if) в количестве, равном количеству полученных «наших» токенов.

{ // pay the obtained team asset in exchange for the issued asset
    if: `{
        $in_asset = trigger.output[[asset!=base]].asset;
        var[$in_asset || '_status'] == 'done'
    }`,
    messages: [
        {
            app: 'payment',
            payload: {
                asset: "{var[$destination_aa]['team_' || $team || '_asset']}",
                outputs: [{address: "{trigger.address}", amount: "{trigger.output[[asset=$in_asset]]}"}]
            }
        },
    ]
}

Код агента готов, вот полный его листинг:

Полный код агента
{
    /*
    This is a fundraising proxy AA.

    It allows to raise money up to a specific target.  If the target is reached, the money is forwarded to another AA, otherwise the money is refunded.

    This specific example raises money for challenging the current candidate winner in 51% attack game.  The target is a moving target as other teams may be adding contributions at the same time.

    Contributors get shares of the proxy in exchange for Bytes.  They can exchange the shares back to the same amount of Bytes any time before the target is reached.  As soon as the target is reached, the raised funds are forwarded to the game and the proxy receives the shares of the team in exchange.  Then, the contributors can exchange the shares of the proxy for the shares of the team.
    */

    init: `{
        $asset = var['asset'];
        $destination_aa = 'WWHEN5NDHBI2UF4CLJ7LQ7VAW2QELMD7';
        $team = 'VF5UVKDSOXPMITMDGYXEIGUJSQBRAMMN';
    }`,
    messages: {
        cases: [
            { // start a new fundraising period
                if: `{trigger.data.start AND !$asset}`,
                messages: [
                    {
                        app: 'asset',
                        payload: {
                            is_private: false,
                            is_transferrable: true,
                            auto_destroy: false,
                            fixed_denominations: false,
                            issued_by_definer_only: true,
                            cosigned_by_definer: false,
                            spender_attested: false
                        }
                    },
                    {
                        app: 'state',
                        state: `{
                            var[response_unit || '_status'] = 'open';
                            var['asset'] = response_unit;
                            response['asset'] = response_unit;
                        }`
                    }
                ]
            },
            { // contribute
                if: `{trigger.output[[asset=base]] >= 1e5 AND $asset}`,
                init: `{
                    if (var[$destination_aa]['finished'])
                        bounce('game over');
                    $amount = trigger.output[[asset=base]] - 2000; // to account for fees we need to respond now and to refund bytes or pay shares later
                    $total_raised = var['total_raised'] + $amount;
                    $missing_amount = ceil((balance[$destination_aa][base] + $total_raised)*0.51) - var[$destination_aa]['team_' || $team || '_amount'];
                    $bDone = ($total_raised > $missing_amount);
                }`,
                messages: [
                    {
                        app: 'payment',
                        payload: {
                            asset: "{$asset}",
                            outputs: [{address: "{trigger.address}", amount: "{$amount}"}]
                        }
                    },
                    {
                        if: `{$bDone}`,
                        app: 'payment',
                        payload: {
                            asset: "base",
                            outputs: [{address: "{$destination_aa}", amount: "{$total_raised}"}]
                        }
                    },
                    {
                        if: `{$bDone}`,
                        app: 'data',
                        payload: {
                            team: "{$team}"
                        }
                    },
                    {
                        app: 'state',
                        state: `{
                            if ($bDone)
                                var[$asset || '_status'] = 'raised';
                            else
                                var['total_raised'] = $total_raised;
                        }`
                    }
                ]
            },
            { // received team asset
                if: `{trigger.output[[asset=var[$destination_aa]['team_' || $team || '_asset']]] AND $asset}`,
                messages: [
                    {
                        app: 'state',
                        state: `{
                            var[$asset || '_status'] = 'done';
                            var['asset'] = false;
                            var['total_raised'] = false;
                        }`
                    }
                ]
            },
            { // refund
                if: `{$asset AND trigger.output[[asset=$asset]] > 0}`,
                init: `{
                    $amount = trigger.output[[asset=$asset]];
                }`,
                messages: [
                    {
                        app: 'payment',
                        payload: {
                            asset: "base",
                            outputs: [{address: "{trigger.address}", amount: "{$amount}"}]
                        }
                    },
                    {
                        app: 'state',
                        state: `{
                            var['total_raised'] -= $amount;
                        }`
                    }
                ]
            },
            { // pay the obtained team asset in exchange for the issued asset
                if: `{
                    $in_asset = trigger.output[[asset!=base]].asset;
                    var[$in_asset || '_status'] == 'done'
                }`,
                messages: [
                    {
                        app: 'payment',
                        payload: {
                            asset: "{var[$destination_aa]['team_' || $team || '_asset']}",
                            outputs: [{address: "{trigger.address}", amount: "{trigger.output[[asset=$in_asset]]}"}]
                        }
                    },
                ]
            }
        ]
    }
}


Мы реализовали не самый тривиальный алгоритм, т.к. таргет у нас динамический, что не свойственно типичным assurance contract. В них чаще всего целевая сумма фиксирована, так же, как и время дедлайна. Больших изменений в коде это не потребует, поэтому оставим это в качестве упражнения для читателя.

Оптимальная стратегия — «вкладываться»!


А теперь вернёмся к проблема безбилетника. В случае с игрой, у нас такой проблемы нет, т.к. выигрыш могут получить только владельцы краудфандинговых токенов. Но решение задачи безбилетника поможет и в случае с игрой. Мы можем «простимулировать» людей на вкладывание. Для этого организатор сбора средств, он же создатель АА, может заложить небольшую компенсацию, которую он будет выплачивать из своего кармана в случае, если цель сбора достигнута не будет. В случае же успешного сбора, организатор заберёт себе небольшой процент. То есть, мы вводим некое третье лицо, которое с целью получения выгоды будет покрывать риск неуспешного сбора.

Применяя такой «оптимизированный краудфандинг», ранее рациональное поведение «невкладывания» (в общем случае для любых общественных благ быть безбилетником всегда является экономически самой выгодной стратегией) может стать уже менее рациональным, чем вкладывание. Особенно актуальным это может быть в случаях создания групповых благ, в которых участвует определённый круг лиц. Это не избавляет полностью от проблемы безбилетника, но делает участие рациональным поведением.

Код, реализующий «оптимизированный assurance contract» также оставим в качестве интересного упражнения для желающих.

Комментарии (0)