Skip to content

Commit

Permalink
feat: external calls + new invariants
Browse files Browse the repository at this point in the history
  • Loading branch information
Van0k committed Nov 29, 2023
1 parent 8605137 commit 3b43a8c
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 10 deletions.
2 changes: 1 addition & 1 deletion contracts/test/invaritants/AdapterAttacker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ contract AdapterAttacker is IAdapter {
targetContract = _targetContract;
}

function executeAllApprove(address tokenIn, address tokenOut, bytes memory callData)
function executeAllApprove(bytes memory callData)
external
returns (uint256 tokensToEnable, uint256 tokensToDisable, bytes memory result)
{
Expand Down
3 changes: 2 additions & 1 deletion contracts/test/invaritants/Deployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ contract GearboxInstance is IntegrationTestHelper {
creditConfigurator.makeTokenQuoted(tokenTestSuite.addressOf(Tokens.WETH));
vm.stopPrank();

targetAttacker = address(new TargetAttacker(address(creditManager)));
targetAttacker =
address(new TargetAttacker(address(creditManager), address(priceOracle), address(tokenTestSuite)));
adapterAttacker = address(new AdapterAttacker(address(creditManager), address(targetAttacker)));

vm.prank(CONFIGURATOR);
Expand Down
2 changes: 1 addition & 1 deletion contracts/test/invaritants/Handler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ contract Handler {
gi.tokenTestSuite().mint(token, address(this), type(uint80).max);
}

b = block.timestamp;
b = block.number;
}

function randomCall(uint256 _seed, uint16 _account) public {
Expand Down
24 changes: 21 additions & 3 deletions contracts/test/invaritants/MulticallGenerator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ contract MulticallGenerator is Random {
// todo: 2. make executable multicalls

function generateRandomCall() public returns (MultiCall memory call) {
function() returns (MultiCall memory, bool)[9] memory fns = [
function() returns (MultiCall memory, bool)[10] memory fns = [
randomAddCollateral,
randomUpdateQuota,
randomSetFullCheckParams,
Expand All @@ -99,7 +99,8 @@ contract MulticallGenerator is Random {
randomWithdrawCollateral,
randomEnabledToken,
randomDisabledToken,
randomRevokeAdapterAllowances
randomRevokeAdapterAllowances,
randomExternalCall
];

bool success;
Expand Down Expand Up @@ -146,7 +147,10 @@ contract MulticallGenerator is Random {
if (getRandomP() > pThreshold || debt != 0) {
uint256 quotedTokensMask = creditManager.quotedTokensMask();

if (quotedTokensMask == 0) revert("Cant find quota token");
if (quotedTokensMask == 0) {
MultiCall memory _call;
return (_call, false);
}

uint256 collateralTokensCount = creditManager.collateralTokensCount();
uint256 mask;
Expand Down Expand Up @@ -318,6 +322,20 @@ contract MulticallGenerator is Random {
if (!followPermissions || (permissions & REVOKE_ALLOWANCES_PERMISSION != 0)) {}
}

function randomExternalCall() internal returns (MultiCall memory, bool success) {
if (!followPermissions || (permissions & EXTERNAL_CALLS_PERMISSION != 0)) {
bytes memory targetCalldata = abi.encodeCall(TargetAttacker.act, (seed));

return (
MultiCall({
target: adapterAttacker,
callData: abi.encodeCall(AdapterAttacker.executeAllApprove, (targetCalldata))
}),
true
);
}
}

//
// INTERNAL
//
Expand Down
65 changes: 65 additions & 0 deletions contracts/test/invaritants/OpenInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {Handler} from "./Handler.sol";
import {ICreditManagerV3, CollateralDebtData, CollateralCalcTask} from "../../interfaces/ICreditManagerV3.sol";

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../lib/constants.sol";

contract InvariantGearboxTest is Test {
Expand Down Expand Up @@ -55,12 +56,76 @@ contract InvariantGearboxTest is Test {
}
}

function invariant_system() external {
_checkSystemInvariants();
}

function _checkAccountInvariants(CollateralDebtData memory cdd) internal {
if (cdd.debt == 0) {
assertEq(cdd.quotedTokens.length, 0, "Incorrect quota length");
assertEq(cdd.totalDebtUSD, 0, "Debt is 0 while total debt is not");
} else {
// todo: for changed account true (for all if onDemandPrice was not called)
assertTrue(cdd.twvUSD >= cdd.totalDebtUSD, "Accounts with hf < 1 exists");
}
}

function _checkSystemInvariants() internal {
inv_system_01();
inv_system_02();
inv_system_03();
}

// tokenQuotaParams.limit >= tokenQuotaParams.totalQuoted without limit change
function inv_system_01() internal {
uint256 cTokensQty = creditManager.collateralTokensCount();

for (uint256 i = 0; i < cTokensQty; ++i) {
(address token,) = creditManager.collateralTokenByMask(1 << i);

(,,, uint96 totalQuoted, uint96 limit,) = gi.poolQuotaKeeper().getTokenQuotaParams(token);

assertGe(limit, totalQuoted, "Total quoted is larger than limit");
}
}

// expectedLiquidity ~ availableLiquidity + sum of total debts
function inv_system_02() internal {
uint256 el = gi.pool().expectedLiquidity();
uint256 al = gi.pool().availableLiquidity();

if (al >= el) return;

uint256 totalDebts = 0;

address[] memory accounts = creditManager.creditAccounts();
uint256 len = accounts.length;
unchecked {
for (uint256 i; i < len; ++i) {
address creditAccount = accounts[i];
CollateralDebtData memory cdd =
creditManager.calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_ONLY);

totalDebts += cdd.debt + cdd.accruedInterest;
}
}

uint256 eel = al + totalDebts;

uint256 diff = eel > el ? (eel - el) * 10000 / el : (el - eel) * 10000 / el;

assertLe(diff, 10, "Expected liquidity discrepancy is larger than 0.1%");
}

function inv_system_03() internal {
bool isDefault;

try creditManager.getActiveCreditAccountOrRevert() returns (address) {
isDefault = false;
} catch {
isDefault = true;
}

assertTrue(isDefault, "Active credit account is non-zero between transactions");
}
}
2 changes: 1 addition & 1 deletion contracts/test/invaritants/Random.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ contract Random {

uint8 pThreshold = 95;

uint256 private seed;
uint256 internal seed;

function setSeed(uint256 _seed) internal {
seed = _seed;
Expand Down
50 changes: 47 additions & 3 deletions contracts/test/invaritants/TargetAttacker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@
pragma solidity ^0.8.17;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ICreditManagerV3, CollateralCalcTask} from "../../interfaces/ICreditManagerV3.sol";
import {ICreditManagerV3, CollateralCalcTask, CollateralDebtData} from "../../interfaces/ICreditManagerV3.sol";
import {IPriceOracleV3} from "../../interfaces/IPriceOracleV3.sol";
import {ITokenTestSuite} from "../interfaces/ITokenTestSuite.sol";
import {PriceFeedMock} from "../mocks/oracles/PriceFeedMock.sol";
import {Random} from "./Random.sol";

/// @title Target Hacker
/// This contract simulates different technics to hack the system by provided seed

contract TargetAttacker is Random {
ICreditManagerV3 creditManager;
IPriceOracleV3 priceOracle;
ITokenTestSuite tokenTestSuite;
address creditAccount;

constructor(address _creditManager) {
constructor(address _creditManager, address _priceOracle, address _tokenTestSuite) {
creditManager = ICreditManagerV3(_creditManager);
priceOracle = IPriceOracleV3(_priceOracle);
tokenTestSuite = ITokenTestSuite(_tokenTestSuite);
}

// Act function tests different scenarios related to any action
Expand All @@ -27,11 +35,47 @@ contract TargetAttacker is Random {
setSeed(_seed);
creditAccount = msg.sender;

function ()[1] memory fnActions = [_stealTokens];
function ()[3] memory fnActions = [_stealTokens, _changeTokenPrice, _swapTokens];

fnActions[getRandomInRange(fnActions.length)]();
}

function _changeTokenPrice() internal {
uint256 cTokensQty = creditManager.collateralTokensCount();
uint256 mask = 1 << getRandomInRange(cTokensQty);
(address token,) = creditManager.collateralTokenByMask(mask);

address priceFeed = IPriceOracleV3(priceOracle).priceFeeds(token);

(, int256 price,,,) = PriceFeedMock(priceFeed).latestRoundData();

uint256 sign = getRandomInRange(2);
uint256 deltaPct = getRandomInRange(500);

int256 newPrice =
sign == 1 ? price * (10000 + int256(deltaPct)) / 10000 : price * (10000 - int256(deltaPct)) / 10000;

PriceFeedMock(priceFeed).setPrice(newPrice);
}

function _swapTokens() internal {
uint256 cTokensQty = creditManager.collateralTokensCount();
uint256 mask0 = 1 << getRandomInRange(cTokensQty);
uint256 mask1 = 1 << getRandomInRange(cTokensQty);

(address tokenIn,) = creditManager.collateralTokenByMask(mask0);
(address tokenOut,) = creditManager.collateralTokenByMask(mask1);

uint256 balance = IERC20(tokenIn).balanceOf(creditAccount);

uint256 tokenInAmount = getRandomInRange(balance);

uint256 tokenInEq = priceOracle.convert(tokenInAmount, tokenIn, tokenOut);

IERC20(tokenIn).transferFrom(creditAccount, address(this), tokenInAmount);
tokenTestSuite.mint(tokenOut, creditAccount, getRandomInRange(tokenInEq));
}

function _stealTokens() internal {
uint256 cTokensQty = creditManager.collateralTokensCount();
uint256 mask = 1 << getRandomInRange(cTokensQty);
Expand Down

0 comments on commit 3b43a8c

Please sign in to comment.