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

    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 используется в расчетах дробная степень и логарифм.
    Поделиться публикацией

    Похожие публикации

    Комментарии 4
      0
      А почему бы не заменить все m100.mul(500) на предрассчитанные константы, раз так уж взялись за оптимизацию газа?

      И такие штуки развернуть y/m100/m100/m100/6
        0

        Такие вещи должен оптимизировать транслятор.

          +1
          Именно. Но по сути — с сокращениями код был бы просто менее понятен. Так то конечно он будет еще оптимизирован. Я просто хотел показать, что вопреки мнению многих — использование логарифмов и дробных степеней вполне приемлемо.
            0
            Ну сейчас не оптимизирует

            contract Ballot {
                uint256 m100=100000000;
            
                function q() public  {
                    uint256 a = 10**30 / m100 /m100 ;
                }
                
                function q2() public  {
                    uint256 a = 10**14 ;
                }
            }
            

            q() — 625 газа
            q2() — 141 газ

            Про читаемость согласен

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

        Самое читаемое