SafeMath溢出校验导致的拒绝服务
2021-01-11 # 智能合约

前言

9号晚上突然接到消息,客户的合约出现问题,代币卡死在合约中,无法取出,据称是在第28天出现溢出问题卡死

分析处理后,通过这件事学到挺多,便记录一下

问题代码

问题主要代码在update_initreward函数中

uint256 DURATION = 1 days;
int128 dayNums = 0;
uint256 public base_ = 20*10e3;
uint256 public rate_forReward = 1;
uint256 public base_Rate_Reward = 100;
......
function update_initreward() private {
    dayNums = dayNums + 1;
    uint256 thisreward = base_.mul(rate_forReward).mul(10**18).mul((base_Rate_Reward.sub(rate_forReward))**(uint256(dayNums-1))).div(base_Rate_Reward**(uint256(dayNums)));
    _initReward = uint256(thisreward);
}

thisreward的计算公式整理如下:

其中

代入公式(1)化简可得:

分析

可以看到公式中存在$99^{dayNums-1}$和$100^{dayNums}$,数值大小是呈指数级增长的,这是个非常恐怖的数量级

dayNums到40时,$99^{dayNums-1}$整体将大于$2^{256}$即uint256的大小,造成数值溢出

$99^{dayNums-1}$还只是公式中的一个小因子,在分子中,前面同样还有$2 \times 10^{23}$这样一个大因子

计算分子整体的溢出情况,可以发现分子的算式在dayNums到28的时候就已经发生溢出了

正好和客户目前的情况一致,在第28天的时候合约功能出现问题

虽然公式中已经使用了SafeMath安全算法,但由于SafeMath安全算法中存在require的溢出校验语句,而导致整个调用失败而回滚,最终表现为拒绝服务

该函数在合约启动后仅由修饰器checkHalve调用,而checkHalve修饰了很多函数,其中包括取款函数,于是导致了用户不能提取合约中质押的代币,合约大半个功能瘫痪,无法运作

修复建议

问题的本质是算式分子计算过程中产生的数值过大导致溢出,进而触发SafeMath的溢出校验而回滚,造成了拒绝服务的危害

那么修复自然是围绕公式做思考,通过上面的分析可以清楚这么几点:

一是公式的计算目的是按天数逐渐累乘计算出奖励数额,这是一个规律性渐进的特点;

其二,进一步化简整理公式(2),可得:

从公式(3)中可以看出,这个公式实际上就是在$2 \times 10^{21}$的基础上逐天取99%,而$2 \times 10^{21}$并未超过uint256的大小,所以公式的计算结果必定是逐渐变小的,并不会产生溢出

从公式的计算角度来看,thisreward的计算结果是并不大的,而计算过程的中间值过大,产生了溢出

从公式的算法逻辑来看,问题代码对于thisreward的计算是直接使用天数从0累乘到当前天数来获取结果,简单粗暴,计算数值庞大

那么修复思路就很清晰了,拆分累乘

初始化定好第一次的thisreward数值,后面的每一次调用仅在上一次的thisreward的数值基础上乘以99%就行

所以需要多定义一个变量用于每次存储上一次的thisreward的值

修改后的新函数示例如下:

uint256 DURATION = 1 days;
int128 dayNums = 0;
uint256 public base_ = 20*10e3;
uint256 public rate_forReward = 1;
uint256 public base_Rate_Reward = 100;
//knownsec// lastReward用于存储上一次的thisrewrad的值
uint256 lastReward = base_.mul(rate_forReward).mul(10**18).div(base_Rate_Reward);

......

//knownsec// 原函数,存在拒绝服务风险
function update_initreward_old() private {
    dayNums = dayNums + 1;
    uint256 thisreward = base_.mul(rate_forReward).mul(10**18).mul((base_Rate_Reward.sub(rate_forReward))**(uint256(dayNums-1))).div(base_Rate_Reward**(uint256(dayNums)));
    _initReward = uint256(thisreward);
}

//knownsec// 新函数
function update_initreward() private {
    dayNums = dayNums +1;
    if (dayNums == 1){
        return lastReward;
    } else {
        uint256 thisreward = lastReward.mul(base_Rate_Reward.sub(rate_forReward)).div(base_Rate_Reward);
        lastReward = thisreward;
        return thisreward;
    }
}

经测试,不再存在风险,并且数额匹配(存在少量精度丢失)

总结

通过这件事学到了很多,在涉及运算的地方并不是用了SafeMath的安全算法就一定是安全的了,由于SafeMath安全算法内部的require溢出校验语句,视具体场景是可能存在拒绝服务风险的

唉,智能合约太难了,千里之堤毁于蚁穴,稍有一点细节没做好可能都会导致很严重的漏洞