前言
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溢出校验语句,视具体场景是可能存在拒绝服务风险的
唉,智能合约太难了,千里之堤毁于蚁穴,稍有一点细节没做好可能都会导致很严重的漏洞