|慢雾:DeFi 当红项目 YAM 闪电折戟,一行代码如何蒸发数亿美元?


|慢雾:DeFi 当红项目 YAM 闪电折戟,一行代码如何蒸发数亿美元?
本文插图

免责声明:本文旨在传递更多市场信息 , 不构成任何投资建议 。 文章仅代表作者观点 , 不代表火星财经官方立场 。
小编:记得关注哦
来源:慢雾科技
原文标题:慢雾:DeFi 当红项目 YAM 闪电折戟 , 一行代码如何蒸发数亿美元?
当红流动性挖矿项目 YAM 创始人披露该项目合约漏洞 , 慢雾技术详解漏洞细节 。
原文标题:《DeFi YAM , 一行代码如何蒸发数亿美元?》 撰文:yudan @ 慢雾安全团队
|慢雾:DeFi 当红项目 YAM 闪电折戟,一行代码如何蒸发数亿美元?
本文插图

发生了什么?
|慢雾:DeFi 当红项目 YAM 闪电折戟,一行代码如何蒸发数亿美元?
本文插图

以上是 YAM 官方对本次事件的 简短说明 。
简单来说就是官方在合约中发现负责调整供应量的函数发生了问题 , 这个问题导致多余的 YAM 代币放进了 YAM 的 reserves 合约中 , 并且如果不修正这个问题 , 将会导致 YAM 的后续治理变为不可能 。 同时 , 官方给出了此次漏洞的具体问题代码 , 如下:
|慢雾:DeFi 当红项目 YAM 闪电折戟,一行代码如何蒸发数亿美元?
本文插图

从上图可知 , 由于编码不规范 , YAM 合约在调整 totalSupply 的时候 , 本应将最后的结果除以 BASE 变量 , 但是在实际开发过程中却忽略了 , 导致 totoalSupply 计算不正确 , 比原来的值要大 10^18 倍 。 但是代币供应量问题和治理是怎么扯上关系呢?这需要我们针对代码做进一步的分析 。
YAM 会变成怎样?
为了深入了解此次漏洞造成的影响 , 需要对 YAM 项目代码进行深入的了解 。 根据官方给出的问题代码及项目 Github 地址(https://github.com/yam-finance/yam-protocol) , 可以定位出调整供应量的rebase函数位于 YAMDelegator.sol 合约中 , 具体代码如下:
function rebase( uint256 epoch, uint256 indexDelta, bool positive ) external returns (uint256) { epoch; indexDelta; positive; delegateAndReturn; }通过跟踪rebase函数 , 发现rebase函数最终调用了 delegateAndReturn 函数 , 代码如下:
function delegateAndReturn private returns (bytes memory) { (bool success, ) = implementation.delegatecall(msg.data); assembly { let free_mem_ptr := mload(0x40) returndatacopy(free_mem_ptr, 0, returndatasize) switch success case 0 { revert(free_mem_ptr, returndatasize) } default { return(free_mem_ptr, returndatasize) } } }通过分析代码 , 可以发现 delegateAndReturn 函数最终使用 delegatecall 的方式调用了 implementation 地址中的逻辑 , 也就是说 , 这是一个可升级的合约模型 。 而真正的rebase逻辑位于 YAM.sol 中 , 继续跟进 rebase 函数的具体逻辑 , 如下:
function rebase( uint256 epoch, uint256 indexDelta, bool positive ) external onlyRebaser returns (uint256) { if (indexDelta == 0) { emit Rebase(epoch, yamsScalingFactor, yamsScalingFactor); return totalSupply; } uint256 prevYamsScalingFactor = yamsScalingFactor; if (!positive) { yamsScalingFactor = yamsScalingFactor.mul(BASE.sub(indexDelta)).div(BASE); } else { uint256 newScalingFactor = yamsScalingFactor.mul(BASE.add(indexDelta)).div(BASE); if (newScalingFactor _maxScalingFactor) { yamsScalingFactor = newScalingFactor; } else { yamsScalingFactor =_maxScalingFactor; } } //SlowMist// 问题代码 totalSupply = initSupply.mul(yamsScalingFactor); emit Rebase(epoch, prevYamsScalingFactor, yamsScalingFactor); return totalSupply; } }通过分析最终的rebase函数的逻辑 , 不难发现代码中根据 yamsScalingFactor 来对 totalSupply 进行调整 , 由于 yamsScalingFactor 是一个高精度的值 , 在调整完成后应当除以 BASE 来去除计算过程中的精度 , 获得正确的值 。 但是项目方在对 totalSupply 进行调整时 , 竟忘记了对计算结果进行调整 , 导致了 totalSupply 意外变大 , 计算出错误的结果 。
分析到这里还没结束 , 要将漏洞和社区治理关联起来 , 需要对代码进行进一步的分析 。 通过观察rebase函数的修饰器 , 不难发现此处限定了只能是 rebaser 进行调用 。 而 rebaser 是 YAM 中用与实现供应量相关逻辑的合约 , 也就是说 , 是 rebaser 合约最终调用了 YAM.sol 合约中的rebase函数 。 通过跟踪相关代码 , 发现 rebaser 合约中对应供应量调整的逻辑为rebase函数 , 代码如下:
function rebase public { // EOA only require(msg.sender == tx.origin); // ensure rebasing at correct time _inRebaseWindow; // This comparison also ensures there is no reentrancy. require(lastRebaseTimestampSec.add(minRebaseTimeIntervalSec)now); // Snap the rebase time to the start of this window. lastRebaseTimestampSec = now.sub( now.mod(minRebaseTimeIntervalSec)).add(rebaseWindowOffsetSec); epoch = epoch.add(1); // get twap from uniswap v2; uint256 exchangeRate = getTWAP; // calculates % change to supply (uint256 offPegPerc, bool positive) = computeOffPegPerc(exchangeRate); uint256 indexDelta = offPegPerc; // Apply the Dampening factor. indexDelta = indexDelta.div(rebaseLag); YAMTokenInterface yam = YAMTokenInterface(yamAddress); if (positive) { require(yam.yamsScalingFactor.mul(uint256(10**18).add(indexDelta)).div(10**18)yam.maxScalingFactor, ''new scaling factor will be too big''); } //SlowMist// 取当前 YAM 代币的供应量 uint256 currSupply = yam.totalSupply; uint256 mintAmount; // reduce indexDelta to account for minting //SlowMist// 计算要调整的供应量 if (positive) { uint256 mintPerc = indexDelta.mul(rebaseMintPerc).div(10**18); indexDelta = indexDelta.sub(mintPerc); mintAmount = currSupply.mul(mintPerc).div(10**18); } // rebase //SlowMist// 调用 YAM 的 rebase 逻辑 uint256 supplyAfterRebase = yam.rebase(epoch, indexDelta, positive); assert(yam.yamsScalingFactoryam.maxScalingFactor); // perform actions after rebase //SlowMist// 进入调整逻辑 afterRebase(mintAmount, offPegPerc); }通过分析代码 , 可以发现函数在进行了一系列的检查后 , 首先获取了当前 YAM 的供应量 , 计算此次的铸币数量 , 然后再调用 YAM.sol 中的rebase函数对 totalSupply 进行调整 , 也就是说 rebase 过后的对 totalSupply 的影响要在下一次调用 rebaser 合约的rebase函数才会生效 。 最后rebase函数调用了 afterRebase 函数 。 我们继续跟进 afterRebase 函数中的代码:


推荐阅读