Solidity 中的数学(第 4 部分:复利)

news2024/11/15 14:22:21

本文是关于在 Solidity 中进行数学运算的系列文章中的第四篇。这次的主题是:复利

 

介绍

在我们之前的文章中,我们讨论了百分比以及它们是如何在 Solidity 中计算的。在金融数学中,百分比通常与贷款和存款支付的利息有关。在每个时间段结束时,比如一个月或一年,本金的一定百分比将支付给贷方或存款持有人。这种模式称为单利,每期支付的百分比称为定期利率。

在计算机程序中,通常使用利率而不是利率。例如,对于 3% 的利率,该比率为 0.03。因此,一个时期的利息支付金额可以计算为利息率乘以本金金额,并且从上一篇文章中我们已经知道如何在 Solidity 中有效且准确地执行此操作。

单利模式很简单,但如果利息不是立即支付给贷方或存款持有人,而是加到本金上,事情就会变得更加复杂。在这种情况下,过去期间累积的利息会影响未来收取的利息金额。

在本文中,我们将讨论如何在 Solidity 中实现该模式,该模式的名称为:复利

定期复利

我们已经知道如何计算单利。计算复利的直接解决方案是在每个时间段结束时计算单利,然后将计算出的利息与本金相加。在高级语言中,例如 JavaScript,它看起来像这样:

principal += ratio * principal; // Do after each time period

我们使用mulDiv上一篇文章中的函数,并假设它ratio是一个定点数,点后有 18 位小数。

上面的代码在大多数情况下都可以工作,但它的+=操作可能会溢出,所以为了使代码安全,我们需要这样修改它:

principal = add (principal, mulDiv (ratio, principal, 10^18));

这种变体可能适合生产,但难以阅读。在本文中,为简单起见,我们将使用普通算术运算,就好像 Solidity 支持分数并且算术运算不会溢出一样。在实际代码中,这些操作应该被适当的函数代替。

一旦我们知道如何计算单个期间的复利,问题就是:

我们如何在每个时间段结束时触发复利?

剧透:我们不应该

与传统应用程序不同,智能合约不能有任何后台活动。合约的字节码仅在交易调用合约时执行,无论是直接调用还是通过另一个智能合约调用。可以依靠第三方服务,如Provable(以前称为 Oraclize)定期调用特定的智能合约,或者可以从经济上激励普通人这样做。

这种方法可行,但有许多缺点。首先,有人必须为汽油付费,所以它不是免费的。其次,必须在每期末计算复利,即使在接下来的时间段内没有人会使用更新后的本金。第三,时间越短,复合越频繁,消耗的gas越多。第四,对于短时间,这种方法是不准确的,因为交易挖掘时间不可预测,并且在网络负载高的时候可能会非常大。

因此,如果在每个周期结束时复利对 Solidity 来说不是一个好主意,那么

我们什么时候应该复利?

剧透:“惰性”复合

与其在每个时间段结束时复利,更好的方法是仅在有人需要获取本金或债务或存款时才复利,并在此时对自复利以来结束的所有时间段执行复利最后一次:

uint currentPeriod = block.timestamp / periodLength;
for (uint period = lastPeriod; period < currentPeriod; period++)
  principal += ratio * principal;
lastPeriod = currentPeriod;

此代码将所有尚未复利的利息添加到本金中,并且每次有人想要访问时都必须执行principal。这种方法被称为“惰性”复利,实际计算会推迟到有人真正需要他们的结果时才进行。

然而,上面显示的“惰性”复合的实现有一个重要问题。实际的 gas 消耗量线性取决于自上次执行复利计算以来经过了多少时间间隔。如果时间段很短,或者上一次复利是很久以前进行的,那么在所有经过的时间段内复利所需的气体量可能会超过区块气体限制,从而有效地使进一步的复利变得不可能。所以问题是:

如何更高效地进行“惰性”复利?

剧透:间隔加倍

首先我们注意到,单个时间段的组合利息可以这样重写:

principal *= 1 + ratio;

对于两个时间间隔,这将是:

principal *= (1 + ratio) * (1 + ratio);

然后我们注意到,(1+ r )²=1+(2 r + r ²),所以双倍时间间隔的有效利率为 2 r + r ²,其中r是单倍时间间隔的利率。如果我们想要复利的时间间隔数是偶数,我们可以通过将时间间隔持续时间加倍来减半时间间隔数。当时间间隔数为奇数时,我们可以只进行一次复利,从而使剩余的时间间隔数为偶数。这是代码:

function compound (uint principal, uint ratio, uint n)
public pure returns (uint) {
  while (n > 0) {
    if (n % 2 == 1) {
      principal += principal * ratio;
      n -= 1;
    } else {
      ratio = 2 * ratio + ratio * ratio;
      n /= 2;
    }
  }
  return principal;
}

上面的代码具有对数复杂度,并且在principalratio较大时运行良好,因此principal * ratioproduct 具有足够的有效小数位以获得不错的精度。但是,如果principalratio很小,上面的代码可能会产生不准确的结果。现在的问题是:

如何提高惰性复利的精度?

剧透:通过平方求幂

在上面显示的代码中,以下单独的代码丢失了精度:

principal += principal * ratio;

这是因为我们假设principal是整数,所以赋值必须舍入计算值。舍入可能会执行多次,舍入误差会累加。

为了解决这个问题,我们可能会注意到,对于 n 个时间间隔,利息可能会像这样复合:

principal *= (1 + ratio) ** n;

如果 Solidity 支持分数,这段代码就可以工作,但只要不支持,我们就需要自己实现求幂。我们使用与上一节中相同的对数复杂度方法,因此代码非常相似:

function pow (uint x, uint n)
public pure returns (uint r) {
  r = 1.0;
  while (n > 0) {
    if (n % 2 == 1) {
      r *= x;
      n -= 1;
    } else {
      x *= x;
      n /= 2;
    }
  }
}
function compound (uint principal, uint ratio, uint n)
public pure returns (uint) {
  return principal * pow (1 + ratio, n);
}

请注意该表达式:r = 1.0. 在这里要记住,我们在这里使用分数,就好像 Solidity 确实支持它们,但实际上并不支持它们。人们将不得不用实现分数数学的函数替换所有算术运算。例如,下面是使用ABDK Math 64.64库实现 64.64 位定点数算术运算的真实代码:

function pow (int128 x, uint n)
public pure returns (int128 r) {
  r = ABDKMath64x64.fromUInt (1);
  while (n > 0) {
    if (n % 2 == 1) {
      r = ABDKMath64x64.mul (r, x);
      n -= 1;
    } else {
      x = ABDKMath64x64.mul (x, x);
      n /= 2;
    }
  }
}
function compound (uint principal, uint ratio, uint n)
public pure returns (uint) {
  return ABDKMath64x64.mulu (
    pow (
      ABDKMath64x64.add (
        ABDKMath64x64.fromUInt (1), 
        ABDKMath64x64.divu (
          ratio,
          10**18)),
      n),
    principal);
}

实际上,这个库已经有pow功能,可以用来代替我们的实现。

上面的代码非常精确和直接,但它仅适用于离散时间间隔。如果我们需要对任意时间间隔进行复利怎么办?这种模式被称为

连续复利

连续复利的想法是计算任意而不是固定时间段的利息。实现此目的的一种方法是使用小数周期。我们已经知道如何计算n期的复利:

principal *= (1 + ratio) ** n; 

假设时间段为一年,我们要计算 1 个月的复利,即一年的 1/12。那么公式应该是:

principal *= (1 + ratio) ** (1 / 12);

不幸的是,Solidity 和pow上面介绍的函数都不支持小数指数。我们可以通过整数幂和根,或者通过固定底数的对数和指数来实现它们,但是

有没有更简单的方法来进行连续复利?

剧透:是的:不要这样做

现实世界中的时间是连续的,或者至少看起来是这样。以太坊中的时间是离散的。它以秒为单位,用整数表示。因此,周期为 1 秒的周期性复利相当于连续复利,因为没有人可能在一个周期的中间观察到本金值。

每秒复利的想法乍一看可能很奇怪,但在以太坊上它的效果出奇地好。3% 的年利率实际上相当于每秒利率 0.000000093668115524%,或 0.000000000936681155 每秒利率,以 18 位小数表示。这里我们假设 1 年有 31556952 秒。

使用上述函数计算 1 年(31556952 个周期)的复利,该比率给出 2.99999999895% 的年利率,因此精确度接近 10 位有效数字。对于大多数应用来说已经足够了。使用 128.128 位定点数而不是 64.64 位甚至浮点数可以实现更高的精度。

在我们的实验中,复合周期性每秒利息 1 年消耗了大约 90K gas。对于大多数应用程序来说,这可能是负担得起的,但通常是相当高的。在我们的下一篇文章中,我们将介绍提供大致相同精度的更便宜的方法。

结论

由于缺乏本机分数支持,复杂的分数计算,例如复合定期利率所需的那些,在 Solidity 中可能具有挑战性。

然而,复利仍然可以通过平方算法和模拟定点数使用求幂来有效计算。

建议的方法足够强大,可以在 1 年(甚至更长)的时间跨度上复合每秒的利率。然而,这种方法非常耗气。

在下一篇文章中,我们将介绍更好的方法

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/173962.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习入门基础CNN系列——批归一化(Batch Normalization)和丢弃法(dropout)

想要入门深度学习的小伙伴们&#xff0c;可以了解下本博主的其它基础内容&#xff1a; &#x1f3e0;我的个人主页 &#x1f680;深度学习入门基础CNN系列——卷积计算 &#x1f31f;深度学习入门基础CNN系列——填充&#xff08;padding&#xff09;与步幅&#xff08;stride&…

CSS 搜索框

CSS 搜索框 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>搜索框</title><style type"text/css">* {margin: 0;padding: 0;}.search-container {margin: 50px;display: flex;width: 500px;height:…

rtthread pwm

1、配置PWM设备驱动相关宏定义 1.1 配置PWM和TIM设备驱动 在RT-Thread Settings 配置界面中&#xff0c;在设备驱动程序目录下勾选 HWTIMER 和 PWM设备驱动程序&#xff0c;如下图所示&#xff1a; 配置完后在 rtconfig.h 中可以查看刚刚配置的 RT_USING_HWTIMER 和 RT_U…

Ribbon策略改变实现步骤(基于Eureka服务注册中心)

前言 Ribbon作为服务调用的作用&#xff0c;自带的默认负载均衡机制是轮询机制也就是轮流访问机制。当然有时候并不是业务上都需要这种机制&#xff0c;这时候就需要改变。 机制类型 RoundRobinRule 轮询 RandomRule 随机 RetryRule 先进行轮询策略获取服务&#xff0c;如…

本地怎么画最简单的监控图

当我们需要监测程序是否正常运行&#xff0c;或者需要观测程序运行是否平稳&#xff0c;或者需要知道特定场景下指标的状态时&#xff0c;那我们都需要将监控可视化。今天我简单记录下&#xff0c;本地是如何监控可视化~ 目录简单调试阶段——仅使用prometheus1.安装promethues…

【Redis】GEO(地理坐标)数据结构

【Redis】GEO(地理坐标)数据结构 文章目录【Redis】GEO(地理坐标)数据结构1. GEO简介1.1 GEO常用命令2. 实战示例2.1 场景介绍2.2 实战2.2.1 将店铺数据导入Redis2.2.2 根据地理坐标进行分页查询1. GEO简介 GEO就是Geolocation的简写形式&#xff0c;代表地理坐标。Redis在3.2…

BFS判重和双向广搜

目录 一、前言 二、BFS判重 1、set 判重 2、字典判重 3、跳蚱蜢&#xff08;2017年省赛&#xff0c;lanqiaoOJ题号642&#xff09; &#xff08;1&#xff09;字典去重、用 list 实现队列 &#xff08;2&#xff09;set() 去重、用 list 实现队列 &#xff08;3&#x…

setDaemon python守护进程,队列通信子线程

使用setDaemon()和守护线程这方面知识有关&#xff0c; 比如在启动线程前设置thread.setDaemon(True)&#xff0c;就是设置该线程为守护线程&#xff0c;表示该线程是不重要的,进程退出时不需要等待这个线程执行完成。这样做的意义在于&#xff1a;避免子线程无限死循环&#x…

IK分词工具

https://code.google.com/archive/p/ik-analyzer/ IK Analyzer是一个开源的&#xff0c;基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始&#xff0c; IKAnalyzer已经推出了4个大版本。最初&#xff0c;它是以开源项目Luence为应用主体的&#xff0c;结合…

ruoyi-vue版本(八)登陆页面的验证码是咋实现的

目录1 需求2 配置类3 逻辑1 需求 我们打开若依项目的登陆页面&#xff0c;看到有一个验证码功能&#xff0c;点击一下这个验证码&#xff0c;还会进行变换验证码&#xff0c;那么这个逻辑是咋实现的&#xff1b; 我们刚进这个页面&#xff0c;其实就调用了一个接口&#xff1…

整理指针相关练习

这里收录的是相关指针的练习&#xff0c;主要针对的是指针与sizeof之间的练习&#xff0c;练完你对指针的理解将更进一层喔一维数组指针练习字符数组指针练习二维数组指针练习练习总结&#xff1a;指针笔试真题一维数组指针练习 一维数组相关练习&#xff0c;下面答案是多少呢…

负载均衡的在线OJ

文章目录1.项目宏观结构(1)三个模块(2)项目宏观结构(3)编写顺序2.compile_server(1)compiler.hpp(2)runner.hpp(3)compile_run.hpp(4)compile_server.cc(5)Makefile(6)temp(7)编译运行模块总结3.comm(1)util.hpp(2)log.hpp(3)httplib.h4.基于MVC结构的OJ服务设计(oj_server)(1)…

java访问控制符/导入2023019

访问控制符&#xff08;定义的时候不加访问控制符&#xff0c;默认的就是default&#xff09;&#xff1a; 1.private&#xff08;当前类访问权限&#xff09;&#xff1a;如果类里的一个成员&#xff08;包括成员变量、方法和构造器等&#xff09;使用private访问控制符来修饰…

Java——数组中第k个最大的元素

题目链接 leetcode在线oj题——数组中第k个最大的元素 题目描述 给定整数数组 nums 和整数 k&#xff0c;请返回数组中第 k 个最大的元素。 请注意&#xff0c;你需要找的是数组排序后的第 k 个最大的元素&#xff0c;而不是第 k 个不同的元素。 你必须设计并实现时间复杂…

Move_base_flex(mbf)框架理解

本文章重点在 第二部分类图解析&#xff0c;第四部分代码解析 文章目录1. move_base_flex主体代码结构树2. move_base_flex 类图解析2.1 ROS2 navigation整体架构2.2 mbf类图主体思路详解2.2.1. 抽象层&#xff08;abstract层&#xff09;2.2.2. 外部信号输入&#xff08;Actio…

【NI Multisim 14.0虚拟仪器设计——放置虚拟仪器仪表(4通道示波器)】

目录 序言 &#x1f34d;放置虚拟仪器仪表 &#x1f349;4通道示波器 1.“时基”选项组 2.“通道”选项组 序言 NI Multisim最突出的特点之一就是用户界面友好。它可以使电路设计者方便、快捷地使用虚拟元器件和仪器、仪表进行电路设计和仿真。 首先启动NI Multisim 14.0…

C语言 通讯录最终版(动态内存+实时保存)

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; 前言&#xff1a; 上期通讯录我们实现了动态内存的申请&#xff0c;但数据依然是存放在内存中&#xff0c;当程序退出的时候&#xff0c;通讯录中的数据自然就不存在了&#xff0c;等下次运行通讯录程序的时候…

什么是REST和RESTful

REST&#xff08;Representational State Transfer&#xff09;表象化状态转变&#xff08;表述性状态转变&#xff09;&#xff0c;在2000年被提出&#xff0c;基于HTTP、URI、XML、JSON等标准和协议&#xff0c;支持轻量级、跨平台、跨语言的架构设计。是Web服务的一种新的架…

单身福利专场——Python采集某相亲地数据

嗨害大家好鸭&#xff01;我是小熊猫~ 咳咳年前最后一天… 一点单身福利… 我想… 应该会有需要的吧… 环境开发: Python 3.8Pycharm 模块使用: import parselimport requestsimport csvimport re 爬虫基本思路流程: 一. 数据来源分析: 1. 明确需求: 采集数据是什么 —…

Linux基本功系列之ping命令实战

文章目录一. 命令介绍二. 语法格式及常用选项三. 参考案例3.1 测试本机与指定网站服务器之间的网络连通性3.2 指定ping的次数3.3 指定时间间隔和次数3.4 设置TTL为2553.5 极快速的测试使用大包ping四. 使用ping命令常见问题总结前言&#x1f680;&#x1f680;&#x1f680; 想…