diff --git a/.gitignore b/.gitignore index 0b724ca7c..f8e359232 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ coverage .coverage_cache .coverage_contracts coverage.json -deployments/ \ No newline at end of file +deployments/ + +out/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e12471968 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/ds-test"] + path = lib/ds-test + url = https://github.com/dapphub/ds-test diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 000000000..caa4286b3 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,9 @@ +[default] +src = 'src' +out = 'out' +libs = ['lib'] +remappings = ['ds-test/=lib/ds-test/src/'] +fuzz_runs = 10000 +optimizer = false + +# See more config options https://github.com/gakonst/foundry/tree/master/config \ No newline at end of file diff --git a/lib/ds-test b/lib/ds-test new file mode 160000 index 000000000..0a5da56b0 --- /dev/null +++ b/lib/ds-test @@ -0,0 +1 @@ +Subproject commit 0a5da56b0d65960e6a994d2ec8245e6edd38c248 diff --git a/package.json b/package.json index 78d617fcb..53ff61ad4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "node": ">=16.0.0" }, "scripts": { + "setup:foundry": "foundryup && git submodule update --init", + "install:foundry": "curl -L https://foundry.paradigm.xyz | bash", "size": "npm run compile && npm run hardhat size-contracts", "auth-registry": "npx dotenv-cli -- bash -c 'npm config set //npm.pkg.github.com/:_authToken \"$NODE_AUTH_TOKEN\"'", "run-env": "npm run auth-registry && npm i && tail -f /dev/null", @@ -22,6 +24,7 @@ "prettier:write": "prettier --write 'tasks/**/*.ts' 'contracts/**/*.sol' 'helpers/**/*.ts' 'test-suites/**/*.ts' 'market-config/**/*.ts'", "coverage": ". ./setup-test-env.sh && COVERAGE=true npx hardhat coverage --temp temp-artifacts --testfiles test-suites/emptyrun.coverage.ts && rm -rf coverage.json coverage/ && COVERAGE=true npx hardhat coverage --temp temp-artifacts --testfiles 'test-suites/*.spec.ts'", "test": ". ./setup-test-env.sh && TS_NODE_TRANSPILE_ONLY=1 hardhat test test-suites/*.spec.ts", + "test-fuzz": "forge test", "test-scenarios": ". ./setup-test-env.sh && npx hardhat test test-suites/__setup.spec.ts test-suites/scenario.spec.ts", "test-l2pool": ". ./setup-test-env.sh && npx hardhat test test-suites/__setup.spec.ts test-suites/pool-l2.spec.ts", "test-subgraph:scenarios": ". ./setup-test-env.sh && hardhat --network hardhatevm_docker test test-suites/__setup.spec.ts test-suites/subgraph-scenarios.spec.ts", diff --git a/src/Vm.sol b/src/Vm.sol new file mode 100644 index 000000000..b2cbe273b --- /dev/null +++ b/src/Vm.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +interface Vm { + // Set block.timestamp + function warp(uint256) external; + + // Set block.number + function roll(uint256) external; + + // Set block.basefee + function fee(uint256) external; + + // Loads a storage slot from an address + function load(address account, bytes32 slot) external returns (bytes32); + + // Stores a value to an address' storage slot + function store( + address account, + bytes32 slot, + bytes32 value + ) external; + + // Signs data + function sign(uint256 privateKey, bytes32 digest) + external + returns ( + uint8 v, + bytes32 r, + bytes32 s + ); + + // Computes address for a given private key + function addr(uint256 privateKey) external returns (address); + + // Performs a foreign function call via terminal + function ffi(string[] calldata) external returns (bytes memory); + + // Sets the *next* call's msg.sender to be the input address + function prank(address) external; + + // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called + function startPrank(address) external; + + // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input + function prank(address, address) external; + + // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input + function startPrank(address, address) external; + + // Resets subsequent calls' msg.sender to be `address(this)` + function stopPrank() external; + + // Sets an address' balance + function deal(address who, uint256 newBalance) external; + + // Sets an address' code + function etch(address who, bytes calldata code) external; + + // Expects an error on next call + function expectRevert(bytes calldata) external; + + function expectRevert(bytes4) external; + + // Record all storage reads and writes + function record() external; + + // Gets all accessed reads and write slot from a recording session, for a given address + function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); + + // Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData). + // Call this function, then emit an event, then call a function. Internally after the call, we check if + // logs were emitted in the expected order with the expected topics and data (as specified by the booleans) + function expectEmit( + bool, + bool, + bool, + bool + ) external; + + // Mocks a call to an address, returning specified data. + // Calldata can either be strict or a partial match, e.g. if you only + // pass a Solidity selector to the expected calldata, then the entire Solidity + // function will be mocked. + function mockCall( + address, + bytes calldata, + bytes calldata + ) external; + + // Clears all mocked calls + function clearMockedCalls() external; + + // Expect a call to an address with the specified calldata. + // Calldata can either be strict or a partial match + function expectCall(address, bytes calldata) external; + + function getCode(string calldata) external returns (bytes memory); + + // Label an address in test traces + function label(address addr, string memory label) external; + + // When fuzzing, generate new inputs if conditional not met + function assume(bool) external; +} diff --git a/src/test/CalldataLogic.t.sol b/src/test/CalldataLogic.t.sol new file mode 100644 index 000000000..ff21b01f3 --- /dev/null +++ b/src/test/CalldataLogic.t.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import {DataTypes} from './../../contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveConfiguration} from './../../contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; + +import {CalldataLogic} from './../../contracts/protocol/libraries/logic/CalldataLogic.sol'; +import {L2Encoder} from './../../contracts/misc/L2Encoder.sol'; + +import {Vm} from './../Vm.sol'; +import {TestHelper} from './TestHelper.sol'; + +// TODO: There is an error with the revert messages. For now, we are checking after reverts only, +// as the integration tests are already catching the revert messages +contract CalldataLogicTest is TestHelper { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + Vm constant VM = Vm(HEVM_ADDRESS); + + mapping(uint256 => address) reservesList; + + uint256 constant RESERVE_COUNT = 128; + + function setUp() public { + for (uint256 i = 0; i < RESERVE_COUNT; i++) { + reservesList[i] = address(uint160(400 + i)); + } + } + + function testDecodeSupplyParams( + uint8 assetId, + uint128 amount, + uint16 referralCode + ) public { + VM.assume(assetId < RESERVE_COUNT); + + bytes32 args; + assembly { + args := add(assetId, add(shl(16, amount), shl(144, referralCode))) + } + + (address _asset, uint256 _amount, uint16 _referralCode) = CalldataLogic.decodeSupplyParams( + reservesList, + args + ); + + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_amount, amount); + assertEq(_referralCode, referralCode); + } + + function testDecodeSupplyWithPermitParams( + uint8 assetId, + uint128 amount, + uint16 referralCode, + uint32 deadLine, + uint8 permitV + ) public { + VM.assume(assetId < RESERVE_COUNT); + + bytes32 args; + assembly { + args := add( + assetId, + add( + shl(16, amount), + add(shl(144, referralCode), add(shl(160, deadLine), shl(192, permitV))) + ) + ) + } + + ( + address _asset, + uint256 _amount, + uint16 _referralCode, + uint256 _deadLine, + uint8 _permitV + ) = CalldataLogic.decodeSupplyWithPermitParams(reservesList, args); + + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_amount, amount); + assertEq(_referralCode, referralCode); + assertEq(_deadLine, deadLine); + assertEq(_permitV, permitV); + } + + function testDecodeWithdrawParams(uint8 assetId, uint128 amount) public { + VM.assume(assetId < RESERVE_COUNT); + bytes32 args; + assembly { + args := add(assetId, shl(16, amount)) + } + + (address _asset, uint256 _amount) = CalldataLogic.decodeWithdrawParams(reservesList, args); + + uint256 expectedAmount = amount == type(uint128).max ? type(uint256).max : uint256(amount); + + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_amount, expectedAmount); + } + + function testDecodeBorrowParams( + uint8 assetId, + uint128 amount, + bool stableRateMode, + uint16 referralCode + ) public { + VM.assume(assetId < RESERVE_COUNT); + uint256 interestRateMode = stableRateMode ? 1 : 2; + + bytes32 args; + assembly { + args := add( + assetId, + add(shl(16, amount), add(shl(144, interestRateMode), shl(152, referralCode))) + ) + } + + ( + address _asset, + uint256 _amount, + uint256 _interestRateMode, + uint16 _referralCode + ) = CalldataLogic.decodeBorrowParams(reservesList, args); + + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_amount, amount); + assertEq(_interestRateMode, interestRateMode); + assertEq(_referralCode, referralCode); + } + + function testDecodeRepayParams( + uint8 assetId, + uint128 amount, + bool stableRateMode + ) public { + VM.assume(assetId < RESERVE_COUNT); + uint256 interestRateMode = stableRateMode ? 1 : 2; + uint256 expectedAmount = amount == type(uint128).max ? type(uint256).max : amount; + + bytes32 args; + assembly { + args := add(assetId, add(shl(16, amount), shl(144, interestRateMode))) + } + + (address _asset, uint256 _amount, uint256 _interestRateMode) = CalldataLogic.decodeRepayParams( + reservesList, + args + ); + + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_amount, expectedAmount); + assertEq(_interestRateMode, interestRateMode); + } + + struct DecodeRepayWithPermitHelper { + uint256 expectedAmount; + address _asset; + uint256 _amount; + uint256 _interestRateMode; + uint256 _deadline; + uint8 _permitV; + } + + function testDecodeRepayWithPermitParams( + uint8 assetId, + uint128 amount, + bool stableRateMode, + uint32 deadline, + uint8 permitV + ) public { + VM.assume(assetId < RESERVE_COUNT); + DecodeRepayWithPermitHelper memory vars; + + vars.expectedAmount = amount == type(uint128).max ? type(uint256).max : amount; + + bytes32 args; + assembly { + args := add( + assetId, + add( + shl(16, amount), + add(shl(144, sub(2, stableRateMode)), add(shl(152, deadline), shl(184, permitV))) + ) + ) + } + + ( + vars._asset, + vars._amount, + vars._interestRateMode, + vars._deadline, + vars._permitV + ) = CalldataLogic.decodeRepayWithPermitParams(reservesList, args); + + assertEq(vars._asset, reservesList[assetId]); + assertNotEq(vars._asset, address(0)); + assertEq(vars._amount, vars.expectedAmount); + assertEq(vars._interestRateMode, stableRateMode ? 1 : 2); + assertEq(vars._deadline, uint256(deadline)); + assertEq(vars._permitV, permitV); + } + + function testDecodeSwapBorrowRateModeParams(uint8 assetId, bool stableRateMode) public { + VM.assume(assetId < RESERVE_COUNT); + uint256 interestRateMode = stableRateMode ? 1 : 2; + bytes32 args; + assembly { + args := add(assetId, shl(16, interestRateMode)) + } + + (address _asset, uint256 _interestRateMode) = CalldataLogic.decodeSwapBorrowRateModeParams( + reservesList, + args + ); + + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_interestRateMode, interestRateMode); + } + + function testDecodeRebalanceStableBorrowRateParams(uint8 assetId, address user) public { + VM.assume(assetId < RESERVE_COUNT); + bytes32 args; + assembly { + args := add(assetId, shl(16, user)) + } + + (address _asset, address _user) = CalldataLogic.decodeRebalanceStableBorrowRateParams( + reservesList, + args + ); + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_user, user); + } + + function testDecodeSetUserUseReserveAsCollateralParams(uint8 assetId, bool useAsCollateral) + public + { + VM.assume(assetId < RESERVE_COUNT); + bytes32 args; + assembly { + args := add(assetId, shl(16, useAsCollateral)) + } + + (address _asset, bool _useAsCollateral) = CalldataLogic + .decodeSetUserUseReserveAsCollateralParams(reservesList, args); + + assertEq(_asset, reservesList[assetId]); + assertNotEq(_asset, address(0)); + assertEq(_useAsCollateral, useAsCollateral); + } + + struct DecodeLiquidationCallParamsHelper { + uint256 expectedDebtToCover; + address collateralAsset; + address debtAsset; + address user; + uint256 debtToCover; + bool receiveAToken; + } + + function testDecodeLiquidationCallParams( + uint8 collateralAssetId, + uint8 debtAssetId, + address user, + uint128 debtToCover, + bool receiveAToken + ) public { + VM.assume(collateralAssetId < RESERVE_COUNT && debtAssetId < RESERVE_COUNT); + DecodeLiquidationCallParamsHelper memory vars; + + bytes32 args1; + bytes32 args2; + + vars.expectedDebtToCover = debtToCover == type(uint128).max ? type(uint256).max : debtToCover; + + assembly { + args1 := add(add(collateralAssetId, shl(16, debtAssetId)), shl(32, user)) + args2 := add(debtToCover, shl(128, receiveAToken)) + } + + ( + vars.collateralAsset, + vars.debtAsset, + vars.user, + vars.debtToCover, + vars.receiveAToken + ) = CalldataLogic.decodeLiquidationCallParams(reservesList, args1, args2); + + assertEq(vars.collateralAsset, reservesList[collateralAssetId]); + assertNotEq(vars.collateralAsset, address(0)); + assertEq(vars.debtAsset, reservesList[debtAssetId]); + assertNotEq(vars.debtAsset, address(0)); + assertEq(vars.user, user); + assertEq(vars.debtToCover, vars.expectedDebtToCover); + assertEq(vars.receiveAToken, receiveAToken); + } +} diff --git a/src/test/PercentageMath.t.sol b/src/test/PercentageMath.t.sol new file mode 100644 index 000000000..3410c25a6 --- /dev/null +++ b/src/test/PercentageMath.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import 'ds-test/test.sol'; +import {PercentageMath} from './../../contracts/protocol/libraries/math/PercentageMath.sol'; + +import {Vm} from './../Vm.sol'; + +contract PercentageMathTest is DSTest { + using PercentageMath for uint256; + +Vm constant VM = Vm(HEVM_ADDRESS); + function testPercentMul(uint256 a, uint256 b) public { + bool safe = b == 0 || a <= (type(uint256).max - PercentageMath.HALF_PERCENTAGE_FACTOR) / b; + VM.assume(safe); + assertEq( + a.percentMul(b), + (a * b + PercentageMath.HALF_PERCENTAGE_FACTOR) / PercentageMath.PERCENTAGE_FACTOR + ); + } + + function testPercentDiv(uint256 a, uint256 b) public { + bool safe = b != 0 && a <= (type(uint256).max - b / 2) / PercentageMath.PERCENTAGE_FACTOR; + VM.assume(safe); + assertEq(a.percentDiv(b), (a * PercentageMath.PERCENTAGE_FACTOR + b / 2) / b); + } + + /// NEGATIVES /// + + function testFailPercentMul(uint256 a, uint256 b) public { + bool safe = b == 0 || a <= (type(uint256).max - PercentageMath.HALF_PERCENTAGE_FACTOR) / b; + VM.assume(!safe); + a.percentMul(b); + } + + function testFailPercentDiv(uint256 a, uint256 b) public { + bool safe = b != 0 && a <= (type(uint256).max - b / 2) / PercentageMath.PERCENTAGE_FACTOR; + VM.assume(!safe); + a.percentDiv(b); + } +} diff --git a/src/test/ReserveConfiguration.t.sol b/src/test/ReserveConfiguration.t.sol new file mode 100644 index 000000000..ff6739367 --- /dev/null +++ b/src/test/ReserveConfiguration.t.sol @@ -0,0 +1,612 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import {DataTypes} from './../../contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveConfiguration} from './../../contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {Vm} from './../Vm.sol'; +import {Errors} from './../../contracts/protocol/libraries/helpers/Errors.sol'; + +import {TestHelper} from './TestHelper.sol'; + +// TODO: There is an error with the revert messages. For now, we are checking after reverts only, +// as the integration tests are already catching the revert messages +contract ReserveConfigurationTest is TestHelper { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + Vm constant VM = Vm(HEVM_ADDRESS); + + function testSetLtv(uint256 data, uint256 ltv) public { + ltv = bound(ltv, 0, type(uint16).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: data & ReserveConfiguration.LTV_MASK + }); + config.setLtv(ltv); + assertEq(config.data & ~ReserveConfiguration.LTV_MASK, ltv); + } + + function testFailSetLtvTooHigh(uint256 data, uint256 ltv) public { + ltv = bound(ltv, ReserveConfiguration.MAX_VALID_LTV + 1, type(uint256).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: data & ReserveConfiguration.LTV_MASK + }); + //TODO: VM.expectRevert(bytes(Errors.INVALID_LTV)); + config.setLtv(ltv); + } + + function testGetLtv(uint256 data, uint256 ltv) public { + ltv = bound(ltv, 0, type(uint16).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LTV_MASK) | ltv + }); + assertEq(config.getLtv(), ltv); + } + + function testSetLiquidationThreshold(uint256 data, uint256 threshold) public { + threshold = bound(threshold, 0, type(uint16).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_THRESHOLD_MASK) + }); + + config.setLiquidationThreshold(threshold); + + assertEq( + (config.data & ~ReserveConfiguration.LIQUIDATION_THRESHOLD_MASK) >> + ReserveConfiguration.LIQUIDATION_THRESHOLD_START_BIT_POSITION, + threshold + ); + } + + function testFailSetLiquidationThreshold(uint256 data, uint256 threshold) public { + threshold = bound( + threshold, + ReserveConfiguration.MAX_VALID_LIQUIDATION_THRESHOLD + 1, + type(uint256).max + ); + + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_THRESHOLD_MASK) + }); + + //TODO: VM.expectRevert(bytes(Errors.INVALID_LIQ_THRESHOLD)); + config.setLiquidationThreshold(threshold); + } + + function testGetLiquidationThreshold(uint256 data, uint256 threshold) public { + threshold = bound(threshold, 0, ReserveConfiguration.MAX_VALID_LIQUIDATION_THRESHOLD); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_THRESHOLD_MASK) | + (threshold << ReserveConfiguration.LIQUIDATION_THRESHOLD_START_BIT_POSITION) + }); + assertEq(config.getLiquidationThreshold(), threshold); + } + + function testSetLiquidationBonus(uint256 data, uint256 bonus) public { + bonus = bound(bonus, 0, ReserveConfiguration.MAX_VALID_LIQUIDATION_BONUS); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_BONUS_MASK) + }); + + config.setLiquidationBonus(bonus); + + assertEq( + (config.data & ~ReserveConfiguration.LIQUIDATION_BONUS_MASK) >> + ReserveConfiguration.LIQUIDATION_BONUS_START_BIT_POSITION, + bonus + ); + } + + function testFailSetLiquidationBonus(uint256 data, uint256 bonus) public { + bonus = bound(bonus, ReserveConfiguration.MAX_VALID_LIQUIDATION_BONUS + 1, type(uint256).max); + + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_BONUS_MASK) + }); + + //TODO: VM.expectRevert(bytes(Errors.INVALID_LIQ_THRESHOLD)); + config.setLiquidationBonus(bonus); + } + + function testGetLiquidationBonus(uint256 data, uint256 bonus) public { + bonus = bound(bonus, 0, ReserveConfiguration.MAX_VALID_LIQUIDATION_BONUS); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_BONUS_MASK) | + (bonus << ReserveConfiguration.LIQUIDATION_BONUS_START_BIT_POSITION) + }); + assertEq(config.getLiquidationBonus(), bonus); + } + + function testSetDecimals(uint256 data, uint256 decimals) public { + decimals = bound(decimals, 0, ReserveConfiguration.MAX_VALID_DECIMALS); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.DECIMALS_MASK) + }); + + config.setDecimals(decimals); + + assertEq( + (config.data & ~ReserveConfiguration.DECIMALS_MASK) >> + ReserveConfiguration.RESERVE_DECIMALS_START_BIT_POSITION, + decimals + ); + } + + function testFailSetDecimals(uint256 data, uint256 decimals) public { + decimals = bound(decimals, ReserveConfiguration.MAX_VALID_DECIMALS + 1, type(uint256).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.DECIMALS_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.INVALID_DECIMALS)); + config.setDecimals(decimals); + } + + function testGetDecimals(uint256 data, uint256 decimals) public { + decimals = bound(decimals, 0, ReserveConfiguration.MAX_VALID_DECIMALS); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.DECIMALS_MASK) | + (decimals << ReserveConfiguration.RESERVE_DECIMALS_START_BIT_POSITION) + }); + assertEq(config.getDecimals(), decimals); + } + + function testSetActive(uint256 data, bool active) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.ACTIVE_MASK) + }); + config.setActive(active); + uint256 activeBit = active ? 1 : 0; + assertEq( + (config.data & ~ReserveConfiguration.ACTIVE_MASK) >> + ReserveConfiguration.IS_ACTIVE_START_BIT_POSITION, + activeBit + ); + } + + function testGetActive(uint256 data, bool active) public { + uint256 activeBit = active ? 1 : 0; + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.ACTIVE_MASK) | + (activeBit << ReserveConfiguration.IS_ACTIVE_START_BIT_POSITION) + }); + assertEq(config.getActive(), active); + } + + function testSetFrozen(uint256 data, bool frozen) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.FROZEN_MASK) + }); + config.setFrozen(frozen); + uint256 frozenBit = frozen ? 1 : 0; + assertEq( + (config.data & ~ReserveConfiguration.FROZEN_MASK) >> + ReserveConfiguration.IS_FROZEN_START_BIT_POSITION, + frozenBit + ); + } + + function testGetFrozen(uint256 data, bool frozen) public { + uint256 frozenBit = frozen ? 1 : 0; + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.FROZEN_MASK) | + (frozenBit << ReserveConfiguration.IS_FROZEN_START_BIT_POSITION) + }); + assertEq(config.getFrozen(), frozen); + } + + function testSetPaused(uint256 data, bool paused) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.PAUSED_MASK) + }); + config.setPaused(paused); + uint256 pausedBit = paused ? 1 : 0; + assertEq( + (config.data & ~ReserveConfiguration.PAUSED_MASK) >> + ReserveConfiguration.IS_PAUSED_START_BIT_POSITION, + pausedBit + ); + } + + function testGetPaused(uint256 data, bool paused) public { + uint256 pausedBit = paused ? 1 : 0; + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.PAUSED_MASK) | + (pausedBit << ReserveConfiguration.IS_PAUSED_START_BIT_POSITION) + }); + assertEq(config.getPaused(), paused); + } + + function testSetBorrowableInIsolation(uint256 data, bool enabled) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.BORROWABLE_IN_ISOLATION_MASK) + }); + config.setBorrowableInIsolation(enabled); + uint256 isolationBorrowAbleBit = enabled ? 1 : 0; + assertEq( + (config.data & ~ReserveConfiguration.BORROWABLE_IN_ISOLATION_MASK) >> + ReserveConfiguration.BORROWABLE_IN_ISOLATION_START_BIT_POSITION, + isolationBorrowAbleBit + ); + } + + function testGetBorrowableInIsolation(uint256 data, bool enabled) public { + uint256 isolationBorrowAble = enabled ? 1 : 0; + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.BORROWABLE_IN_ISOLATION_MASK) | + (isolationBorrowAble << ReserveConfiguration.BORROWABLE_IN_ISOLATION_START_BIT_POSITION) + }); + assertEq(config.getBorrowableInIsolation(), enabled); + } + + function testSetSiloedBorrowing(uint256 data, bool enabled) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.SILOED_BORROWING_MASK) + }); + config.setSiloedBorrowing(enabled); + uint256 siloedBorrowingBit = enabled ? 1 : 0; + assertEq( + (config.data & ~ReserveConfiguration.SILOED_BORROWING_MASK) >> + ReserveConfiguration.SILOED_BORROWING_START_BIT_POSITION, + siloedBorrowingBit + ); + } + + function testGetSiloedBorrowing(uint256 data, bool enabled) public { + uint256 siloedBorrowingBit = enabled ? 1 : 0; + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.SILOED_BORROWING_MASK) | + (siloedBorrowingBit << ReserveConfiguration.SILOED_BORROWING_START_BIT_POSITION) + }); + assertEq(config.getSiloedBorrowing(), enabled); + } + + function testSetBorrowingEnabled(uint256 data, bool enabled) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.BORROWING_MASK) + }); + config.setBorrowingEnabled(enabled); + uint256 borrowingBit = enabled ? 1 : 0; + assertEq( + (config.data & ~ReserveConfiguration.BORROWING_MASK) >> + ReserveConfiguration.BORROWING_ENABLED_START_BIT_POSITION, + borrowingBit + ); + } + + function testGetBorrowingEnabled(uint256 data, bool enabled) public { + uint256 borrowingBit = enabled ? 1 : 0; + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.BORROWING_MASK) | + (borrowingBit << ReserveConfiguration.BORROWING_ENABLED_START_BIT_POSITION) + }); + assertEq(config.getBorrowingEnabled(), enabled); + } + + function testSetStableBorrowingEnabled(uint256 data, bool enabled) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.STABLE_BORROWING_MASK) + }); + config.setStableRateBorrowingEnabled(enabled); + uint256 borrowingBit = enabled ? 1 : 0; + assertEq( + (config.data & ~ReserveConfiguration.STABLE_BORROWING_MASK) >> + ReserveConfiguration.STABLE_BORROWING_ENABLED_START_BIT_POSITION, + borrowingBit + ); + } + + function testGetStableBorrowingEnabled(uint256 data, bool enabled) public { + uint256 borrowingBit = enabled ? 1 : 0; + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.STABLE_BORROWING_MASK) | + (borrowingBit << ReserveConfiguration.STABLE_BORROWING_ENABLED_START_BIT_POSITION) + }); + assertEq(config.getStableRateBorrowingEnabled(), enabled); + } + + function testSetReserveFactor(uint256 data, uint256 reserveFactor) public { + reserveFactor = bound(reserveFactor, 0, ReserveConfiguration.MAX_VALID_RESERVE_FACTOR); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.RESERVE_FACTOR_MASK) + }); + + config.setReserveFactor(reserveFactor); + + assertEq( + (config.data & ~ReserveConfiguration.RESERVE_FACTOR_MASK) >> + ReserveConfiguration.RESERVE_FACTOR_START_BIT_POSITION, + reserveFactor + ); + } + + function testFailSetReserveFactor(uint256 data, uint256 reserveFactor) public { + reserveFactor = bound( + reserveFactor, + ReserveConfiguration.MAX_VALID_RESERVE_FACTOR + 1, + type(uint256).max + ); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.RESERVE_FACTOR_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.INVALID_RESERVE_FACTOR)); + config.setReserveFactor(reserveFactor); + } + + function testGetReserveFactor(uint256 data, uint256 reserveFactor) public { + reserveFactor = bound(reserveFactor, 0, ReserveConfiguration.MAX_VALID_RESERVE_FACTOR); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.RESERVE_FACTOR_MASK) | + (reserveFactor << ReserveConfiguration.RESERVE_FACTOR_START_BIT_POSITION) + }); + assertEq(config.getReserveFactor(), reserveFactor); + } + + function testSetBorrowCap(uint256 data, uint256 borrowCap) public { + borrowCap = bound(borrowCap, 0, ReserveConfiguration.MAX_VALID_BORROW_CAP); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.BORROW_CAP_MASK) + }); + + config.setBorrowCap(borrowCap); + + assertEq( + (config.data & ~ReserveConfiguration.BORROW_CAP_MASK) >> + ReserveConfiguration.BORROW_CAP_START_BIT_POSITION, + borrowCap + ); + } + + function testFailSetBorrowCap(uint256 data, uint256 borrowCap) public { + borrowCap = bound(borrowCap, ReserveConfiguration.MAX_VALID_BORROW_CAP + 1, type(uint256).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.BORROW_CAP_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.INVALID_BORROW_CAP)); + config.setBorrowCap(borrowCap); + } + + function testGetBorrowCap(uint256 data, uint256 borrowCap) public { + borrowCap = bound(borrowCap, 0, ReserveConfiguration.MAX_VALID_BORROW_CAP); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.BORROW_CAP_MASK) | + (borrowCap << ReserveConfiguration.BORROW_CAP_START_BIT_POSITION) + }); + assertEq(config.getBorrowCap(), borrowCap); + } + + function testSetSupplyCap(uint256 data, uint256 supplyCap) public { + supplyCap = bound(supplyCap, 0, ReserveConfiguration.MAX_VALID_SUPPLY_CAP); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.SUPPLY_CAP_MASK) + }); + + config.setSupplyCap(supplyCap); + + assertEq( + (config.data & ~ReserveConfiguration.SUPPLY_CAP_MASK) >> + ReserveConfiguration.SUPPLY_CAP_START_BIT_POSITION, + supplyCap + ); + } + + function testFailSetSupplyCap(uint256 data, uint256 supplyCap) public { + supplyCap = bound(supplyCap, ReserveConfiguration.MAX_VALID_SUPPLY_CAP + 1, type(uint256).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.SUPPLY_CAP_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.INVALID_SUPPLY_CAP)); + config.setSupplyCap(supplyCap); + } + + function testGetSupplyCap(uint256 data, uint256 supplyCap) public { + supplyCap = bound(supplyCap, 0, ReserveConfiguration.MAX_VALID_SUPPLY_CAP); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.SUPPLY_CAP_MASK) | + (supplyCap << ReserveConfiguration.SUPPLY_CAP_START_BIT_POSITION) + }); + assertEq(config.getSupplyCap(), supplyCap); + } + + function testSetDebtCeiling(uint256 data, uint256 debtCeiling) public { + debtCeiling = bound(debtCeiling, 0, ReserveConfiguration.MAX_VALID_DEBT_CEILING); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.DEBT_CEILING_MASK) + }); + + config.setDebtCeiling(debtCeiling); + + assertEq( + (config.data & ~ReserveConfiguration.DEBT_CEILING_MASK) >> + ReserveConfiguration.DEBT_CEILING_START_BIT_POSITION, + debtCeiling + ); + } + + function testFailSetDebtCeiling(uint256 data, uint256 debtCeiling) public { + debtCeiling = bound( + debtCeiling, + ReserveConfiguration.MAX_VALID_DEBT_CEILING + 1, + type(uint256).max + ); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.DEBT_CEILING_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.INVALID_DEBT_CEILING)); + config.setDebtCeiling(debtCeiling); + } + + function testGetDebtCeiling(uint256 data, uint256 debtCeiling) public { + debtCeiling = bound(debtCeiling, 0, ReserveConfiguration.MAX_VALID_DEBT_CEILING); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.DEBT_CEILING_MASK) | + (debtCeiling << ReserveConfiguration.DEBT_CEILING_START_BIT_POSITION) + }); + assertEq(config.getDebtCeiling(), debtCeiling); + } + + function testSetLiquidationProtocolFee(uint256 data, uint256 liquidationProtocolFee) public { + liquidationProtocolFee = bound( + liquidationProtocolFee, + 0, + ReserveConfiguration.MAX_VALID_LIQUIDATION_PROTOCOL_FEE + ); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_PROTOCOL_FEE_MASK) + }); + + config.setLiquidationProtocolFee(liquidationProtocolFee); + + assertEq( + (config.data & ~ReserveConfiguration.LIQUIDATION_PROTOCOL_FEE_MASK) >> + ReserveConfiguration.LIQUIDATION_PROTOCOL_FEE_START_BIT_POSITION, + liquidationProtocolFee + ); + } + + function testFailSetLiquidationProtocolFee(uint256 data, uint256 liquidationProtocolFee) public { + liquidationProtocolFee = bound( + liquidationProtocolFee, + ReserveConfiguration.MAX_VALID_LIQUIDATION_PROTOCOL_FEE + 1, + type(uint256).max + ); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_PROTOCOL_FEE_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.INVALID_LIQUIDATION_PROTOCOL_FEE)); + config.setLiquidationProtocolFee(liquidationProtocolFee); + } + + function testGetLiquidationProtocolFee(uint256 data, uint256 liquidationProtocolFee) public { + liquidationProtocolFee = bound( + liquidationProtocolFee, + 0, + ReserveConfiguration.MAX_VALID_LIQUIDATION_PROTOCOL_FEE + ); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.LIQUIDATION_PROTOCOL_FEE_MASK) | + (liquidationProtocolFee << ReserveConfiguration.LIQUIDATION_PROTOCOL_FEE_START_BIT_POSITION) + }); + assertEq(config.getLiquidationProtocolFee(), liquidationProtocolFee); + } + + function testSetUnbackedMintCap(uint256 data, uint256 unbackedMintCap) public { + unbackedMintCap = bound(unbackedMintCap, 0, ReserveConfiguration.MAX_VALID_UNBACKED_MINT_CAP); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.UNBACKED_MINT_CAP_MASK) + }); + + config.setUnbackedMintCap(unbackedMintCap); + + assertEq( + (config.data & ~ReserveConfiguration.UNBACKED_MINT_CAP_MASK) >> + ReserveConfiguration.UNBACKED_MINT_CAP_START_BIT_POSITION, + unbackedMintCap + ); + } + + function testFailSetUnbackedMintCap(uint256 data, uint256 unbackedMintCap) public { + unbackedMintCap = bound( + unbackedMintCap, + ReserveConfiguration.MAX_VALID_UNBACKED_MINT_CAP + 1, + type(uint256).max + ); + VM.assume(unbackedMintCap > ReserveConfiguration.MAX_VALID_UNBACKED_MINT_CAP); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.UNBACKED_MINT_CAP_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.MAX_VALID_UNBACKED_MINT_CAP)); + config.setUnbackedMintCap(unbackedMintCap); + } + + function testGetUnbackedMintCap(uint256 data, uint256 unbackedMintCap) public { + unbackedMintCap = bound(unbackedMintCap, 0, ReserveConfiguration.MAX_VALID_UNBACKED_MINT_CAP); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.UNBACKED_MINT_CAP_MASK) | + (unbackedMintCap << ReserveConfiguration.UNBACKED_MINT_CAP_START_BIT_POSITION) + }); + assertEq(config.getUnbackedMintCap(), unbackedMintCap); + } + + function testSetEModeCategory(uint256 data, uint256 category) public { + category = bound(category, 0, ReserveConfiguration.MAX_VALID_EMODE_CATEGORY); + uint256 category = bound(category, 0, type(uint8).max); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.EMODE_CATEGORY_MASK) + }); + + config.setEModeCategory(category); + + assertEq( + (config.data & ~ReserveConfiguration.EMODE_CATEGORY_MASK) >> + ReserveConfiguration.EMODE_CATEGORY_START_BIT_POSITION, + category + ); + } + + function testFailSetEModeCategory(uint256 data, uint256 category) public { + category = bound( + category, + ReserveConfiguration.MAX_VALID_EMODE_CATEGORY + 1, + type(uint256).max + ); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.EMODE_CATEGORY_MASK) + }); + //TODO: VM.expectRevert(bytes(Errors.MAX_VALID_EMODE_CATEGORY)); + config.setEModeCategory(category); + } + + function testGetEModeCategory(uint256 data, uint256 category) public { + category = bound(category, 0, ReserveConfiguration.MAX_VALID_EMODE_CATEGORY); + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: (data & ReserveConfiguration.EMODE_CATEGORY_MASK) | + (category << ReserveConfiguration.EMODE_CATEGORY_START_BIT_POSITION) + }); + assertEq(config.getEModeCategory(), category); + } + + function testGetFlags(uint256 data) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: data + }); + + (bool active, bool frozen, bool borrowing, bool stableBorrowing, bool paused) = config + .getFlags(); + + assertEq(active, config.getActive()); + assertEq(frozen, config.getFrozen()); + assertEq(borrowing, config.getBorrowingEnabled()); + assertEq(stableBorrowing, config.getStableRateBorrowingEnabled()); + assertEq(paused, config.getPaused()); + } + + function testGetParams(uint256 data) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: data + }); + + ( + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus, + uint256 decimals, + uint256 reserveFactor, + uint256 category + ) = config.getParams(); + + assertEq(ltv, config.getLtv()); + assertEq(liquidationThreshold, config.getLiquidationThreshold()); + assertEq(liquidationBonus, config.getLiquidationBonus()); + assertEq(decimals, config.getDecimals()); + assertEq(reserveFactor, config.getReserveFactor()); + assertEq(category, config.getEModeCategory()); + } + + function testGetCaps(uint256 data) public { + DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap({ + data: data + }); + (uint256 borrowCap, uint256 supplyCap) = config.getCaps(); + + assertEq(borrowCap, config.getBorrowCap()); + assertEq(supplyCap, config.getSupplyCap()); + } +} diff --git a/src/test/TestHelper.sol b/src/test/TestHelper.sol new file mode 100644 index 000000000..10b08e60d --- /dev/null +++ b/src/test/TestHelper.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import 'ds-test/test.sol'; + +contract TestHelper is DSTest { + function assertNotEq(address a, address b) internal { + if (a == b) { + emit log('Error: a != b not satisfied [address]'); + emit log_named_address(' Expected', b); + emit log_named_address(' Actual', a); + fail(); + } + } + + function assertEq(bool a, bool b) internal { + if (a != b) { + emit log('Error: a == b not satisfied [bool]'); + fail(); + } + } + + function assertEq( + bool a, + bool b, + string memory err + ) internal { + if (a != b) { + emit log_named_string('Error', err); + assertEq(a, b); + } + } + + function _countBitsOn(uint256 a) internal returns (uint256) { + uint256 counter = 0; + while (a > 0) { + if (a & 1 == 1) { + counter++; + } + a >>= 1; + } + return counter; + } + + /// From https://github.com/Rari-Capital/solmate/blob/67d907c82f50649f8168061dcfae8617110da361/src/test/utils/DSTestPlus.sol#L116-L135 + function bound( + uint256 x, + uint256 min, + uint256 max + ) internal pure returns (uint256 result) { + require(max >= min, 'MAX_LESS_THAN_MIN'); + + uint256 size = max - min; + + if (max != type(uint256).max) size++; // Make the max inclusive. + if (size == 0) return min; // Using max would be equivalent as well. + // Ensure max is inclusive in cases where x != 0 and max is at uint max. + if (max == type(uint256).max && x != 0) x--; // Accounted for later. + + if (x < min) x += size * (((min - x) / size) + 1); + result = min + ((x - min) % size); + + // Account for decrementing x to make max inclusive. + if (max == type(uint256).max && x != 0) result++; + } + + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } +} diff --git a/src/test/UserConfiguration.t.sol b/src/test/UserConfiguration.t.sol new file mode 100644 index 000000000..13b466ff3 --- /dev/null +++ b/src/test/UserConfiguration.t.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import {UserConfiguration} from './../../contracts/protocol/libraries/configuration/UserConfiguration.sol'; +import {DataTypes} from './../../contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveConfiguration} from './../../contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; + +import {TestHelper} from './TestHelper.sol'; +import {Vm} from './../Vm.sol'; + +contract UserConfigurationTest is TestHelper { + using UserConfiguration for DataTypes.UserConfigurationMap; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + Vm constant VM = Vm(HEVM_ADDRESS); + + DataTypes.UserConfigurationMap internal config; + + mapping(address => DataTypes.ReserveData) internal reservesData; + mapping(uint256 => address) internal reservesList; + + function testSetBorrowing( + uint256 data, + uint8 index, + bool borrowing + ) public { + config = DataTypes.UserConfigurationMap({data: data}); + + uint256 reserveIndex = index % ReserveConfiguration.MAX_RESERVES_COUNT; + config.setBorrowing(reserveIndex, borrowing); + + uint256 bitToCheck = 1 << (reserveIndex * 2); + bool isBorrowing = (config.data & UserConfiguration.BORROWING_MASK) & bitToCheck > 0; + + assertEq(borrowing, isBorrowing); + } + + function testSetAsCollateral( + uint256 data, + uint8 index, + bool usingAsCollateral + ) public { + config = DataTypes.UserConfigurationMap({data: data}); + + uint256 reserveIndex = index % ReserveConfiguration.MAX_RESERVES_COUNT; + config.setUsingAsCollateral(reserveIndex, usingAsCollateral); + + uint256 bitToCheck = 1 << (reserveIndex * 2 + 1); + bool isUsingAsCollateral = (config.data & UserConfiguration.COLLATERAL_MASK) & bitToCheck > 0; + + assertEq(usingAsCollateral, isUsingAsCollateral); + } + + function testIsUsingAsCollateralOrBorrowing(uint256 data, uint8 index) public { + config = DataTypes.UserConfigurationMap({data: data}); + + uint256 reserveIndex = index % ReserveConfiguration.MAX_RESERVES_COUNT; + uint256 bitToCheckBorrowing = 1 << (reserveIndex * 2); + uint256 bitToCheckCollateral = 1 << (reserveIndex * 2 + 1); + + bool isBorrowing = config.data & UserConfiguration.BORROWING_MASK & bitToCheckBorrowing > 0; + bool isUsingAsCollateral = config.data & + UserConfiguration.COLLATERAL_MASK & + bitToCheckCollateral > + 0; + + assertEq( + config.isUsingAsCollateralOrBorrowing(reserveIndex), + isUsingAsCollateral || isBorrowing + ); + } + + function testIsBorrowing(uint256 data, uint8 index) public { + config = DataTypes.UserConfigurationMap({data: data}); + uint256 reserveIndex = index % ReserveConfiguration.MAX_RESERVES_COUNT; + uint256 bitToCheck = 1 << (reserveIndex * 2); + bool isBorrowing = (config.data & UserConfiguration.BORROWING_MASK) & bitToCheck > 0; + assertEq(config.isBorrowing(reserveIndex), isBorrowing); + } + + function testIsUsingAsCollateral(uint256 data, uint8 index) public { + config = DataTypes.UserConfigurationMap({data: data}); + uint256 reserveIndex = index % ReserveConfiguration.MAX_RESERVES_COUNT; + uint256 bitToCheck = 1 << (reserveIndex * 2 + 1); + bool isUsingAsCollateral = (config.data & UserConfiguration.COLLATERAL_MASK) & bitToCheck > 0; + assertEq(config.isUsingAsCollateral(reserveIndex), isUsingAsCollateral); + } + + function testIsUsingAsCollateralOne(uint256 data) public { + config = DataTypes.UserConfigurationMap({data: data}); + assertEq( + config.isUsingAsCollateralOne(), + _countBitsOn(data & UserConfiguration.COLLATERAL_MASK) == 1 + ); + } + + function testIsUsingAsCollateralAny(uint256 data) public { + config = DataTypes.UserConfigurationMap({data: data}); + assertEq( + config.isUsingAsCollateralAny(), + _countBitsOn(data & UserConfiguration.COLLATERAL_MASK) > 0 + ); + } + + function testIsBorrowingOne(uint256 data) public { + config = DataTypes.UserConfigurationMap({data: data}); + assertEq(config.isBorrowingOne(), _countBitsOn(data & UserConfiguration.BORROWING_MASK) == 1); + } + + function testIsBorrowingAny(uint256 data) public { + config = DataTypes.UserConfigurationMap({data: data}); + assertEq(config.isBorrowingAny(), _countBitsOn(data & UserConfiguration.BORROWING_MASK) > 0); + } + + function testIsEmpty(uint256 data) public { + config = DataTypes.UserConfigurationMap({data: data}); + assertEq(config.isEmpty(), data == 0); + } + + function testGetIsolationModeState( + address[6] calldata assets, + uint256[6] calldata assetConfigurationMaps, + uint40[6] calldata debtCeilings, + uint8 collateralCount, + uint8 borrowCount + ) public { + VM.assume(collateralCount <= 3 && borrowCount <= 3); + if (collateralCount == 0) { + borrowCount = 0; + } + + bool expectedIsolated = collateralCount == 1 && + debtCeilings[0] > 0 && + debtCeilings[0] < type(uint32).max; + + uint256 reservesCount = 0; + config = DataTypes.UserConfigurationMap({data: 0}); + + for (uint256 i = 0; i < collateralCount + borrowCount; i++) { + reservesList[reservesCount++] = assets[i]; + DataTypes.ReserveConfigurationMap memory assetConfig = DataTypes.ReserveConfigurationMap({ + data: assetConfigurationMaps[i] & ReserveConfiguration.DEBT_CEILING_MASK + }); + + if (i < collateralCount) { + if (expectedIsolated) { + assetConfig.setDebtCeiling(debtCeilings[i]); + } + config.setUsingAsCollateral(i, true); + } else { + config.setBorrowing(i, true); + } + reservesData[assets[i]].configuration = assetConfig; + } + + (bool isolated, address asset, uint256 ceiling) = config.getIsolationModeState( + reservesData, + reservesList + ); + + uint256 expectedCeiling = expectedIsolated ? debtCeilings[0] : 0; + address expectedAsset = expectedIsolated ? assets[0] : address(0); + + assertEq(isolated, expectedIsolated); + assertEq(asset, expectedAsset); + assertEq(ceiling, expectedCeiling); + } + + function testGetSiloedBorrowingState( + address[6] calldata assets, + uint256[6] calldata assetConfigurationMaps, + uint8 collateralCount, + uint8 borrowCount, + bool siloedBorrowing + ) public { + VM.assume(collateralCount <= 3 && borrowCount <= 3); + if (collateralCount == 0) { + borrowCount = 0; + } + if (siloedBorrowing) { + borrowCount = 1; + } + + uint256 reservesCount = 0; + config = DataTypes.UserConfigurationMap({data: 0}); + + for (uint256 i = 0; i < collateralCount + borrowCount; i++) { + reservesList[reservesCount++] = assets[i]; + DataTypes.ReserveConfigurationMap memory assetConfig = DataTypes.ReserveConfigurationMap({ + data: assetConfigurationMaps[i] & ReserveConfiguration.SILOED_BORROWING_MASK + }); + + if (i < collateralCount) { + config.setUsingAsCollateral(i, true); + } else { + config.setBorrowing(i, true); + if (siloedBorrowing) { + assetConfig.setSiloedBorrowing(true); + } + } + reservesData[assets[i]].configuration = assetConfig; + } + + (bool siloed, address siloedAsset) = config.getSiloedBorrowingState(reservesData, reservesList); + + assertEq(siloed, siloedBorrowing); + assertEq(siloedAsset, siloedBorrowing ? assets[collateralCount] : address(0)); + } + + function Test_getFirstAssetIdByMask(uint256 data, bool collateralMask) internal { + config = DataTypes.UserConfigurationMap({data: data}); + + uint256 mask = ( + collateralMask ? UserConfiguration.COLLATERAL_MASK : UserConfiguration.BORROWING_MASK + ); + + uint256 mapped = data & mask; + uint256 id; + + while ((mapped >>= 1) & 1 == 0) { + id++; + } + id /= 2; + + assertEq(config._getFirstAssetIdByMask(mask), id); + } +} diff --git a/src/test/WadRayMul.t.sol b/src/test/WadRayMul.t.sol new file mode 100644 index 000000000..4ef771120 --- /dev/null +++ b/src/test/WadRayMul.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.10; + +import 'ds-test/test.sol'; + +import {WadRayMath} from './../../contracts/protocol/libraries/math/WadRayMath.sol'; + +import {Vm} from './../Vm.sol'; + +contract WadRayMathTest is DSTest { + using WadRayMath for uint256; + + Vm constant VM = Vm(HEVM_ADDRESS); + + function testWadMul(uint256 a, uint256 b) public { + bool safe = b == 0 || a <= (type(uint256).max - WadRayMath.HALF_WAD) / b; + VM.assume(safe); + assertEq(a.wadMul(b), (a * b + WadRayMath.HALF_WAD) / WadRayMath.WAD); + } + + function testWadDiv(uint256 a, uint256 b) public { + bool safe = b != 0 && a <= (type(uint256).max - b / 2) / WadRayMath.WAD; + VM.assume(safe); + assertEq(a.wadDiv(b), (a * WadRayMath.WAD + b / 2) / b); + } + + function testRayMul(uint256 a, uint256 b) public { + bool safe = b == 0 || a <= (type(uint256).max - WadRayMath.HALF_RAY) / b; + VM.assume(safe); + assertEq(a.rayMul(b), (a * b + WadRayMath.HALF_RAY) / WadRayMath.RAY); + } + + function testRayDiv(uint256 a, uint256 b) public { + bool safe = b != 0 && a <= (type(uint256).max - b / 2) / WadRayMath.RAY; + VM.assume(safe); + assertEq(a.rayDiv(b), (a * WadRayMath.RAY + b / 2) / b); + } + + function testRayToWad(uint256 a) public { + uint256 remainder = a % WadRayMath.WAD_RAY_RATIO; + uint256 expectedResult = a / WadRayMath.WAD_RAY_RATIO; + if (remainder >= WadRayMath.WAD_RAY_RATIO / 2) { + expectedResult++; + } + assertEq(a.rayToWad(), expectedResult); + } + + function testWadToRay(uint256 a) public { + bool safe = a <= type(uint256).max / WadRayMath.WAD_RAY_RATIO; + VM.assume(safe); + assertEq(a.wadToRay(), a * WadRayMath.WAD_RAY_RATIO); + } + + /// NEGATIVES /// + + function testFailWadDiv(uint256 a, uint256 b) public { + bool safe = b != 0 && a <= (type(uint256).max - b / 2) / WadRayMath.WAD; + VM.assume(!safe); + a.wadDiv(b); + } + + function testFailWadMul(uint256 a, uint256 b) public { + bool safe = b == 0 || a <= (type(uint256).max - WadRayMath.HALF_WAD) / b; + VM.assume(!safe); + a.wadMul(b); + } + + function testFailRayMul(uint256 a, uint256 b) public { + bool safe = b == 0 || a <= (type(uint256).max - WadRayMath.HALF_RAY) / b; + VM.assume(!safe); + a.rayMul(b); + } + + function testFailRayDiv(uint256 a, uint256 b) public { + bool safe = b != 0 && a <= (type(uint256).max - b / 2) / WadRayMath.RAY; + VM.assume(!safe); + a.rayDiv(b); + } + + function testFailWadToRay(uint256 a) public { + bool safe = a <= type(uint256).max / WadRayMath.WAD_RAY_RATIO; + VM.assume(!safe); + a.wadToRay(); + } +}