Смарт-контракт Ethereum для расчета бонусов используя дробные степени

в 3:17, , рубрики: Ethereum, smart-contract, solidity, Программирование, Учебный процесс в IT, финансы в IT
image

Начну с того, что всем известно. ICO повсюду, все предлагают разные проекты, все продают токены. И у всех есть какая-то модель продаж со скидками, бонусами, сроками и т.п.
Как ни странно хоть область и достаточно экспериментальная, модель бонусов достаточно примитивна. Просто есть разные объемы или временные интервалы и проценты в них. Да и зачем усложнять? А я все таки попробую. Эксперимента ради. Новое всегда привлекает.

Кроме вопроса безопасности, вторым по важности в случае написания смарт-контракта является вопрос оптимизации. Каждая транзакция съедает газ, а значит и эфир, который расходуется при обработке транзакций. Если код сложный, требует много вычислений — он будет очень дорог для инвесторов в ICO.

А теперь представьте что мы хотим сделать гладкую кривую бонусов в зависимости от объема инвестированных средств. И не просто линейную, а степенную.
Bonus = (Volume of Invest ^ 0.07)-1

Вот как это выглядит на графике:

image

Средний инвестор по текущей статистике инвестирует около 5 ETH, так что допустим что в случае объема меньше 1 ETH бонус начисляться не будет.

Вычислять придется используя только тип uint, так как float пока достаточно экспериментален и опять же достаточно дорогой. И использовать мы можем в таком случае только умножение, деление, сложение и вычитание.

Таким образом нам нужно разложить функцию возведения в степень в ряд.

a^x=e^xlnx=1+xlna+(xlna)^2/2!+(xlna)^3/3!+⋯ −∞<x<∞

В свою очередь логарифм от X надо посчитать один раз, взяв несколько ключевых точек, чтобы аппроксимировать его набором линейных функция на разных интервалах.
Дальше взять например 4 первых члена ряда.

Дальше оценить ошибку взяв нормальный калькулятор и затраченный газ за вызов функции.
В итоге вы получите что-то типа этого:

Для примера вычислил значения для приближения ln(1.07).

pragma solidity ^0.4.15;

library SafeMath {
    function mul(uint256 a, uint256 b) internal constant returns (uint256) {
        uint256 c = a * b;
        assert(a == 0 || c / a == b);
        return c;
    }
    function div(uint256 a, uint256 b) internal constant returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
    }
    function sub(uint256 a, uint256 b) internal constant returns (uint256) {
        assert(b <= a);
        return a - b;
    }
    function add(uint256 a, uint256 b) internal constant returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
}

contract Simple {
    
    using SafeMath for uint256;
    
    uint256 m100=100000000;
    uint256 ln3= 109861228; 
    uint256 ln10=230258512;
    uint256 ln20=299573231;
    uint256 ln30=340119743;
    uint256 ln50=391202307;
    uint256 ln100=460517025;
    uint256 ln200=529831743;
    uint256 ln500=621460819;
    uint256 ln1000=690775527;
    
    function test2(uint256 a){
        test1(a);
    }

    function test1(uint256 a) constant returns (uint256 result) {
        uint256 lnbase;
        uint256 a0;
        
        if(a > m100.mul(1000)){
            lnbase = ln1000;
            a0 = a.div(1000);
        }else if(a > m100.mul(500)){
            lnbase=ln500;
            a0 = a.div(500);
        }else if(a > m100.mul(200)){
            lnbase = ln200;
            a0 = a.div(200);
        }else if(a > m100.mul(100)){
            lnbase = ln100;
            a0 = a.div(100);
        }else if(a > m100.mul(50)){
            lnbase = ln50;
            a0 = a.div(50);
        }else if(a > m100.mul(30)){
            lnbase = ln30;
            a0 = a.div(30);
        }else if(a > m100.mul(20)){
            lnbase = ln20;
            a0 = a.div(20);
        }else if(a > m100.mul(10)){
            lnbase = ln10;
            a0 = a.div(10);
        }else if(a > m100.mul(3)){
            lnbase = ln3;
            a0 = a.div(3);
        }else if(a > m100){
            lnbase = 0;
            a0 = a;
        }else{
            return a;
        }
       
        
        uint256 x=a0.sub(m100).mul(m100).div(a0.add(m100));
        uint256 y=x.add(x.mul(x).mul(x)/m100/m100/3).mul(2);
        y=lnbase.add(y);
        y=y.mul(7)/100;
        x=a.add(a.mul(y)/m100);
        x = x.add(a.mul(y).mul(y)/m100/m100/2);
        y = a.mul(y).mul(y).mul(y);
        x = x.add(y/m100/m100/m100/6);
        return (x);
    }
}

Оценка:

9 ETH умножаем на 10^8 и передаем в функцию:
900000000 дает 1048494787, т.е. 10.485 ETH (бонус около 16%)
на калькуляторе 9^1.07 = 10,496378550818314120261545161046 ETH
ошибка: 0.001, т.е. 0.1%

С учетом затрачиваемых транзакций на перевод эта ошибка вполне сопоставима с коммисией в эфире. Если нужна большая точность, можно конечно добавить 5 член ряда.

Затраты газа на вызов этой функции:
execution cost 9598 gas (Cost only applies when called by a contract)

В случае 1001 ETH получаем:
162102425800 в сравнении с 1001^1.07=1623,54549 (т.е. бонус около 62%) это дает точность 1,0015, т.е. 0.15%
execution cost 6406 gas (Cost only applies when called by a contract)

Как вы можете видеть затраты газа на функцию небольшие (6-9к) учитывая что на данный момент все ICO рекомендуют использовать 200к газа.
И при этом точности в пределах коммисии за среднюю транзакцию.

Если вам кажется, что это какой-то конь в вакууме, то скажу что в системе бонусов проекта SINTEZ Platform sintez.global используется в расчетах дробная степень и логарифм.

Автор: Vinchi

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js