Skip to content

Commit

Permalink
Merge pull request #849 from morpho-org/fix/flashloan-inflation-hack
Browse files Browse the repository at this point in the history
Fix flashloan inflation trick
  • Loading branch information
Rubilmax authored Jun 8, 2023
2 parents 0f494b8 + e9ad64c commit bfe6316
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .github/actions/install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ runs:
cache: yarn

- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/forge-fmt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: Forge format

on:
workflow_call:
secrets:
MACHINE_USER_PRIVATE_KEY:
required: true

jobs:
forge-fmt:
Expand All @@ -15,10 +12,9 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

Expand Down
12 changes: 1 addition & 11 deletions .github/workflows/forge-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ on:
required: false
type: number
secrets:
MACHINE_USER_PRIVATE_KEY:
required: true
ALCHEMY_KEY:
required: false
CODECOV_TOKEN:
Expand All @@ -60,10 +58,9 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

Expand All @@ -78,7 +75,6 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- uses: ./.github/actions/install

Expand All @@ -103,7 +99,6 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- uses: ./.github/actions/install

Expand All @@ -125,7 +120,6 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- uses: ./.github/actions/install

Expand All @@ -147,7 +141,6 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- uses: ./.github/actions/install

Expand All @@ -169,7 +162,6 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- uses: ./.github/actions/install

Expand Down Expand Up @@ -198,7 +190,6 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- uses: ./.github/actions/install

Expand Down Expand Up @@ -226,7 +217,6 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
ssh-key: ${{ secrets.MACHINE_USER_PRIVATE_KEY }}

- uses: ./.github/actions/install

Expand Down
6 changes: 3 additions & 3 deletions src/MorphoGetters.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ abstract contract MorphoGetters is IMorphoGetters, MorphoInternal {

/// @notice Returns the total supply balance of `user` on the `underlying` market (in underlying).
function supplyBalance(address underlying, address user) external view returns (uint256) {
(, Types.Indexes256 memory indexes) = _computeIndexes(underlying);
Types.Indexes256 memory indexes = _computeIndexes(underlying);

return _getUserSupplyBalanceFromIndexes(underlying, user, indexes);
}

/// @notice Returns the total borrow balance of `user` on the `underlying` market (in underlying).
function borrowBalance(address underlying, address user) external view returns (uint256) {
(, Types.Indexes256 memory indexes) = _computeIndexes(underlying);
Types.Indexes256 memory indexes = _computeIndexes(underlying);

return _getUserBorrowBalanceFromIndexes(underlying, user, indexes);
}
Expand Down Expand Up @@ -147,7 +147,7 @@ abstract contract MorphoGetters is IMorphoGetters, MorphoInternal {

/// @notice Returns the updated indexes (peer-to-peer and pool).
function updatedIndexes(address underlying) external view returns (Types.Indexes256 memory indexes) {
(, indexes) = _computeIndexes(underlying);
indexes = _computeIndexes(underlying);
}

/// @notice Returns the liquidity data about the position of `user`.
Expand Down
21 changes: 8 additions & 13 deletions src/MorphoInternal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,10 @@ abstract contract MorphoInternal is MorphoStorage {
(uint256 underlyingPrice, uint256 ltv, uint256 liquidationThreshold, uint256 underlyingUnit) =
_assetLiquidityData(underlying, vars);

(, Types.Indexes256 memory indexes) = _computeIndexes(underlying);
uint256 rawCollateral = (_getUserCollateralBalanceFromIndex(underlying, vars.user, indexes.supply.poolIndex))
* underlyingPrice / underlyingUnit;
Types.Indexes256 memory indexes = _computeIndexes(underlying);
uint256 rawCollateral = (
(_getUserCollateralBalanceFromIndex(underlying, vars.user, indexes.supply.poolIndex)) * underlyingPrice
) / underlyingUnit;

// Morpho has a slightly different method of health factor calculation from the underlying pool.
// This method is used to account for a potential rounding error in calculateUserAccountData,
Expand All @@ -307,7 +308,7 @@ abstract contract MorphoInternal is MorphoStorage {
(, uint256 underlyingPrice, uint256 underlyingUnit) =
_assetData(underlying, vars.oracle, config, vars.eModeCategory.priceSource);

(, Types.Indexes256 memory indexes) = _computeIndexes(underlying);
Types.Indexes256 memory indexes = _computeIndexes(underlying);
debtValue =
(_getUserBorrowBalanceFromIndexes(underlying, vars.user, indexes) * underlyingPrice).divUp(underlyingUnit);
}
Expand Down Expand Up @@ -453,22 +454,16 @@ abstract contract MorphoInternal is MorphoStorage {

/// @dev Updates the indexes of the `underlying` market and returns them.
function _updateIndexes(address underlying) internal returns (Types.Indexes256 memory indexes) {
bool cached;
(cached, indexes) = _computeIndexes(underlying);
indexes = _computeIndexes(underlying);

if (!cached) {
_market[underlying].setIndexes(indexes);
}
_market[underlying].setIndexes(indexes);
}

/// @dev Computes the updated indexes of the `underlying` market (if not already updated) and returns them.
function _computeIndexes(address underlying) internal view returns (bool cached, Types.Indexes256 memory indexes) {
function _computeIndexes(address underlying) internal view returns (Types.Indexes256 memory indexes) {
Types.Market storage market = _market[underlying];
Types.Indexes256 memory lastIndexes = market.getIndexes();

cached = block.timestamp == market.lastUpdateTimestamp;
if (cached) return (true, lastIndexes);

(indexes.supply.poolIndex, indexes.borrow.poolIndex) = _pool.getCurrentPoolIndexes(underlying);

(indexes.supply.p2pIndex, indexes.borrow.p2pIndex) = InterestRatesLib.computeP2PIndexes(
Expand Down
1 change: 1 addition & 0 deletions src/libraries/MarketLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ library MarketLib {
if (idleSupply == 0) return 0;

uint256 totalP2PSupplied = market.deltas.supply.scaledP2PTotal.rayMul(market.indexes.supply.p2pIndex);
if (totalP2PSupplied == 0) return 0;

// We take the minimum to handle the case where the proportion is rounded to greater than 1.
return Math.min(idleSupply.rayDivUp(totalP2PSupplied), WadRayMath.RAY);
Expand Down
15 changes: 12 additions & 3 deletions test/helpers/ForkTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {PermitHash} from "@permit2/libraries/PermitHash.sol";
import {IAllowanceTransfer, AllowanceTransfer} from "@permit2/AllowanceTransfer.sol";

import {PriceOracleSentinelMock} from "test/mocks/PriceOracleSentinelMock.sol";
import {FlashBorrowerMock} from "test/mocks/FlashBorrowerMock.sol";
import {AaveOracleMock} from "test/mocks/AaveOracleMock.sol";
import {PoolAdminMock} from "test/mocks/PoolAdminMock.sol";
import {Configured} from "config/Configured.sol";
Expand Down Expand Up @@ -71,19 +72,21 @@ contract ForkTest is BaseTest, Configured {
AaveOracleMock internal oracle;
PoolAdminMock internal poolAdmin;
PriceOracleSentinelMock internal oracleSentinel;
FlashBorrowerMock internal flashBorrower;

uint256 internal snapshotId = type(uint256).max;

constructor() {
_initConfig();
_loadConfig();

deal(address(this), type(uint128).max);
_setBalances(address(this), type(uint96).max);

_mockPoolAdmin();
_mockOracle();
_mockOracleSentinel();

deal(address(this), type(uint128).max);
_setBalances(address(this), type(uint96).max);
_mockFlashBorrower();
}

function setUp() public virtual {
Expand Down Expand Up @@ -150,6 +153,12 @@ contract ForkTest is BaseTest, Configured {
addressesProvider.setPriceOracleSentinel(address(oracleSentinel));
}

function _mockFlashBorrower() internal {
flashBorrower = new FlashBorrowerMock(address(pool));

_setBalances(address(flashBorrower), type(uint96).max);
}

function _setBalances(address user, uint256 balance) internal {
for (uint256 i; i < allUnderlyings.length; ++i) {
address underlying = allUnderlyings[i];
Expand Down
16 changes: 14 additions & 2 deletions test/helpers/IntegrationTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ contract IntegrationTest is ForkTest {
internal
bypassSupplyCap(underlying, amount)
{
deal(underlying, address(this), type(uint256).max);
_deal(underlying, address(this), amount);
ERC20(underlying).safeApprove(address(pool), amount);
pool.deposit(underlying, amount, onBehalf, 0);
}
Expand Down Expand Up @@ -321,9 +321,21 @@ contract IntegrationTest is ForkTest {
address onBehalf,
address receiver,
uint256 maxIterations
) internal returns (uint256 borrowed) {
) internal returns (uint256) {
oracle.setAssetPrice(market.underlying, 0);

return _borrowPriceZero(borrower, market, amount, onBehalf, receiver, maxIterations);
}

/// @dev Borrows a zero-priced asset from `user` on behalf of `onBehalf`.
function _borrowPriceZero(
address borrower,
TestMarket storage market,
uint256 amount,
address onBehalf,
address receiver,
uint256 maxIterations
) internal returns (uint256 borrowed) {
vm.prank(borrower);
borrowed = morpho.borrow(market.underlying, amount, onBehalf, receiver, maxIterations);

Expand Down
34 changes: 21 additions & 13 deletions test/integration/TestIntegrationBorrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,12 @@ contract TestIntegrationBorrow is IntegrationTest {

test.balanceBefore = ERC20(market.underlying).balanceOf(receiver);

oracle.setAssetPrice(market.underlying, 0);

vm.expectEmit(true, true, true, false, address(morpho));
emit Events.Borrowed(address(user), onBehalf, receiver, market.underlying, 0, 0, 0);

test.borrowed =
_borrowWithoutCollateral(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);
test.borrowed = _borrowPriceZero(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);

test = _assertBorrowPool(market, amount, onBehalf, receiver, test);

Expand All @@ -167,6 +168,8 @@ contract TestIntegrationBorrow is IntegrationTest {

test.balanceBefore = ERC20(market.underlying).balanceOf(receiver);

oracle.setAssetPrice(market.underlying, 0);

vm.expectEmit(true, true, true, false, address(morpho));
emit Events.SupplyPositionUpdated(address(promoter1), market.underlying, 0, 0);

Expand All @@ -176,8 +179,7 @@ contract TestIntegrationBorrow is IntegrationTest {
vm.expectEmit(true, true, true, false, address(morpho));
emit Events.Borrowed(address(user), onBehalf, receiver, market.underlying, 0, 0, 0);

test.borrowed =
_borrowWithoutCollateral(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);
test.borrowed = _borrowPriceZero(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);

test = _assertBorrowP2P(market, amount, onBehalf, receiver, test);
}
Expand All @@ -198,6 +200,8 @@ contract TestIntegrationBorrow is IntegrationTest {

test.balanceBefore = ERC20(market.underlying).balanceOf(receiver);

oracle.setAssetPrice(market.underlying, 0);

vm.expectEmit(true, true, true, true, address(morpho));
emit Events.IdleSupplyUpdated(market.underlying, 0);

Expand All @@ -207,8 +211,7 @@ contract TestIntegrationBorrow is IntegrationTest {
vm.expectEmit(true, true, true, false, address(morpho));
emit Events.Borrowed(address(user), onBehalf, receiver, market.underlying, 0, 0, 0);

test.borrowed =
_borrowWithoutCollateral(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);
test.borrowed = _borrowPriceZero(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);

test = _assertBorrowP2P(market, amount, onBehalf, receiver, test);
}
Expand All @@ -232,11 +235,12 @@ contract TestIntegrationBorrow is IntegrationTest {

test.balanceBefore = ERC20(market.underlying).balanceOf(receiver);

oracle.setAssetPrice(market.underlying, 0);

vm.expectEmit(true, true, true, false, address(morpho));
emit Events.Borrowed(address(user), onBehalf, receiver, market.underlying, 0, 0, 0);

test.borrowed =
_borrowWithoutCollateral(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);
test.borrowed = _borrowPriceZero(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);

test = _assertBorrowPool(market, amount, onBehalf, receiver, test);

Expand All @@ -259,6 +263,8 @@ contract TestIntegrationBorrow is IntegrationTest {

test.balanceBefore = ERC20(market.underlying).balanceOf(receiver);

oracle.setAssetPrice(market.underlying, 0);

vm.expectEmit(true, true, true, false, address(morpho));
emit Events.P2PSupplyDeltaUpdated(market.underlying, 0);

Expand All @@ -268,8 +274,7 @@ contract TestIntegrationBorrow is IntegrationTest {
vm.expectEmit(true, true, true, false, address(morpho));
emit Events.Borrowed(address(user), onBehalf, receiver, market.underlying, 0, 0, 0);

test.borrowed =
_borrowWithoutCollateral(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);
test.borrowed = _borrowPriceZero(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);

test = _assertBorrowP2P(market, amount, onBehalf, receiver, test);
}
Expand Down Expand Up @@ -298,11 +303,12 @@ contract TestIntegrationBorrow is IntegrationTest {

test.balanceBefore = ERC20(market.underlying).balanceOf(receiver);

oracle.setAssetPrice(market.underlying, 0);

vm.expectEmit(true, true, true, false, address(morpho));
emit Events.Borrowed(address(user), onBehalf, receiver, market.underlying, 0, 0, 0);

test.borrowed =
_borrowWithoutCollateral(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);
test.borrowed = _borrowPriceZero(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS);

test = _assertBorrowPool(market, amount, onBehalf, receiver, test);

Expand Down Expand Up @@ -442,10 +448,12 @@ contract TestIntegrationBorrow is IntegrationTest {

Types.Indexes256 memory futureIndexes = morpho.updatedIndexes(market.underlying);

oracle.setAssetPrice(market.underlying, 0);

vm.expectEmit(true, true, true, false, address(morpho));
emit Events.IndexesUpdated(market.underlying, 0, 0, 0, 0);

_borrowWithoutCollateral(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS); // 100% pool.
_borrowPriceZero(address(user), market, amount, onBehalf, receiver, DEFAULT_MAX_ITERATIONS); // 100% pool.

_assertMarketUpdatedIndexes(morpho.market(market.underlying), futureIndexes);
}
Expand Down
Loading

0 comments on commit bfe6316

Please sign in to comment.