diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ae6ce90 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +# Ignore artifacts: +lib +out +broadcast +cache + +src/interfaces/external diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6e4320d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "printWidth": 100, + "tabWidth": 4, + "singleQuote": true, + "overrides": [ + { + "files": "*.sol", + "options": { + "semi": false, + "printWidth": 120, + "tabWidth": 4, + "singleQuote": false + } + } + ] +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index ce9789e..c36d19d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,4 +12,17 @@ mainnet = "${ALCHEMY_ETH_API_URL}" goerli = "${GOERLI_RPC_URL}" sepolia = "${SEPOLIA_RPC_URL}" -# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file +# See more config options https://github.com/foundry-rs/foundry/tree/master/config + +# NOTE Project is using prettier formatting, as opposed to forge. +# See more format options https://book.getfoundry.sh/reference/config/formatter +# [fmt] +# line_length = 120 +# tab_width = 4 +# bracket_spacing = false +# int_types = "long" +# # If function parameters are multiline then always put the function attributes on separate lines +# func_attrs_with_params_multiline = false +# multiline_func_header = "params_first" +# quote_style = "double" +# number_underscore = "thousands" diff --git a/src/Bookkeeper.sol b/src/Bookkeeper.sol index ab9c913..6630c7b 100644 --- a/src/Bookkeeper.sol +++ b/src/Bookkeeper.sol @@ -46,14 +46,14 @@ contract Bookkeeper is Tractor, ReentrancyGuard { event LiquidationKicked(address liquidator, address position); constructor() Tractor(PROTOCOL_NAME, PROTOCOL_VERSION) {} + // receive() external {} // fallback() external {} - function fillOrder(Fill calldata fill, SignedBlueprint calldata orderBlueprint) - external - nonReentrant - verifySignature(orderBlueprint) - { + function fillOrder( + Fill calldata fill, + SignedBlueprint calldata orderBlueprint + ) external nonReentrant verifySignature(orderBlueprint) { // decode order blueprint data and ensure blueprint metadata is valid pairing with embedded data (bytes1 blueprintDataType, bytes memory blueprintData) = unpackDataField(orderBlueprint.blueprint.data); require(uint8(blueprintDataType) == uint8(BlueprintDataType.ORDER), "BKDTMM"); @@ -78,24 +78,28 @@ contract Bookkeeper is Tractor, ReentrancyGuard { Agreement memory agreement = _agreementFromOrder(fill, order); - uint256 loanValue = - IOracle(agreement.loanOracle.addr).getResistantValue(agreement.loanAmount, agreement.loanOracle.parameters); + uint256 loanValue = IOracle(agreement.loanOracle.addr).getResistantValue( + agreement.loanAmount, + agreement.loanOracle.parameters + ); uint256 collateralValue; if (order.isOffer) { agreement.lenderAccount = order.account; agreement.borrowerAccount = fill.account; - collateralValue = loanValue * fill.borrowerConfig.initCollateralRatio / C.RATIO_FACTOR; + collateralValue = (loanValue * fill.borrowerConfig.initCollateralRatio) / C.RATIO_FACTOR; agreement.position.parameters = fill.borrowerConfig.positionParameters; } else { agreement.lenderAccount = fill.account; agreement.borrowerAccount = order.account; - collateralValue = loanValue * order.borrowerConfig.initCollateralRatio / C.RATIO_FACTOR; + collateralValue = (loanValue * order.borrowerConfig.initCollateralRatio) / C.RATIO_FACTOR; agreement.position.parameters = order.borrowerConfig.positionParameters; } console.log("agreement coll value: %s", collateralValue); - agreement.collAmount = - IOracle(agreement.collOracle.addr).getResistantAmount(collateralValue, agreement.collOracle.parameters); + agreement.collAmount = IOracle(agreement.collOracle.addr).getResistantAmount( + collateralValue, + agreement.collOracle.parameters + ); console.log("agreement coll amount: %s", agreement.collAmount); // Set Position data that cannot be computed off chain by caller. agreement.deploymentTime = block.timestamp; @@ -113,15 +117,13 @@ contract Bookkeeper is Tractor, ReentrancyGuard { } // NOTE CEI? - function exitPosition(SignedBlueprint calldata agreementBlueprint) - external - payable - nonReentrant - verifySignature(agreementBlueprint) - { + function exitPosition( + SignedBlueprint calldata agreementBlueprint + ) external payable nonReentrant verifySignature(agreementBlueprint) { (bytes1 blueprintDataType, bytes memory blueprintData) = unpackDataField(agreementBlueprint.blueprint.data); require( - blueprintDataType == bytes1(uint8(BlueprintDataType.AGREEMENT)), "Bookkeeper: Invalid blueprint data type" + blueprintDataType == bytes1(uint8(BlueprintDataType.AGREEMENT)), + "Bookkeeper: Invalid blueprint data type" ); Agreement memory agreement = abi.decode(blueprintData, (Agreement)); require( @@ -141,11 +143,14 @@ contract Bookkeeper is Tractor, ReentrancyGuard { if (LibUtils.isValidLoanAssetAsCost(agreement.loanAsset, costAsset)) { lenderOwed += cost; distributeValue = msg.value; - } // If cost in eth but loan asset is not eth. + } + // If cost in eth but loan asset is not eth. else if (costAsset.standard == ETH_STANDARD) { require(msg.value == cost, "exitPosition: msg.value mismatch from Eth denoted cost"); IAccount(agreement.lenderAccount.addr).loadFromPosition{value: msg.value}( - costAsset, cost, agreement.lenderAccount.parameters + costAsset, + cost, + agreement.lenderAccount.parameters ); } else { revert("exitPosition: isLiquidatable: invalid cost asset"); @@ -155,18 +160,18 @@ contract Bookkeeper is Tractor, ReentrancyGuard { position.distribute{value: distributeValue}(msg.sender, lenderOwed, agreement); IAccount(agreement.borrowerAccount.addr).unlockCollateral( - agreement.collAsset, agreement.collAmount, agreement.borrowerAccount.parameters + agreement.collAsset, + agreement.collAmount, + agreement.borrowerAccount.parameters ); // Marks position as closed from Bookkeeper pov. position.transferContract(msg.sender); } - function kick(SignedBlueprint calldata agreementBlueprint) - external - nonReentrant - verifySignature(agreementBlueprint) - { + function kick( + SignedBlueprint calldata agreementBlueprint + ) external nonReentrant verifySignature(agreementBlueprint) { (, bytes memory blueprintData) = unpackDataField(agreementBlueprint.blueprint.data); // require(blueprintDataType == bytes1(uint8(BlueprintDataType.AGREEMENT)), "BKKIBDT"); // decoding will fail Agreement memory agreement = abi.decode(blueprintData, (Agreement)); @@ -205,19 +210,22 @@ contract Bookkeeper is Tractor, ReentrancyGuard { // NOTE lots of gas savings if collateral can be kept in borrower account until absolutely necessary. console.log("locking %s of %s as collateral", agreement.collAmount, agreement.collAsset.addr); IAccount(agreement.borrowerAccount.addr).lockCollateral( - agreement.collAsset, agreement.collAmount, agreement.borrowerAccount.parameters + agreement.collAsset, + agreement.collAmount, + agreement.borrowerAccount.parameters ); IPosition(agreement.position.addr).deploy( - agreement.loanAsset, agreement.loanAmount, agreement.position.parameters + agreement.loanAsset, + agreement.loanAmount, + agreement.position.parameters ); } /// @dev assumes compatibility between match, offer, and request already verified. - function _agreementFromOrder(Fill calldata fill, Order memory order) - private - pure - returns (Agreement memory agreement) - { + function _agreementFromOrder( + Fill calldata fill, + Order memory order + ) private pure returns (Agreement memory agreement) { // NOTE MAKE CHECKS FOR VALIDITY // NOTE this is prly not gas efficient bc of zero -> non-zero changes... @@ -241,8 +249,10 @@ contract Bookkeeper is Tractor, ReentrancyGuard { function _signAgreement(Agreement memory agreement) private returns (SignedBlueprint memory signedBlueprint) { // Create blueprint to store signed Agreement off chain via events. signedBlueprint.blueprint.publisher = address(this); - signedBlueprint.blueprint.data = - packDataField(bytes1(uint8(BlueprintDataType.AGREEMENT)), abi.encode(agreement)); + signedBlueprint.blueprint.data = packDataField( + bytes1(uint8(BlueprintDataType.AGREEMENT)), + abi.encode(agreement) + ); signedBlueprint.blueprint.endTime = type(uint256).max; signedBlueprint.blueprintHash = getBlueprintHash(signedBlueprint.blueprint); // NOTE: Security: Is is possible to intentionally manufacture a blueprint with different data that creates the same hash? diff --git a/src/README.md b/src/README.md index d7f5132..e5e1721 100644 --- a/src/README.md +++ b/src/README.md @@ -20,6 +20,15 @@ https://fravoll.github.io/solidity-patterns/pull_over_push.html - What are Pharos invariants? https://www.nascent.xyz/idea/youre-writing-require-statements-wrong + +## Temporary Security Limitations +In order to ensure a more secure launch, Pharos will limit some early functionality. This provides us more time to +test, audit, and improve some of the elements that are most novel. + +- Testnet / L2 testing will have agreement size limitations. +- Permissionless use of 3rd party modules will be disabled. +- Direct interaction with Position protocol wrappers will not be implemented. + ### Core: ### Entity-Centric diff --git a/src/interfaces/IAccount.sol b/src/interfaces/IAccount.sol index 14193d2..f377b4b 100644 --- a/src/interfaces/IAccount.sol +++ b/src/interfaces/IAccount.sol @@ -45,11 +45,14 @@ import {Asset} from "src/libraries/LibUtils.sol"; interface IAccount { /// @notice Transfer asset and increment account balance. Pulls asset from sender or uses msg.value. function loadFromUser(Asset calldata asset, uint256 amount, bytes calldata parameters) external payable; + /// @notice Transfer asset and increment account balance. Pulls asset from sender or uses msg.value. /// @dev Assets may not literally be coming from a position. function loadFromPosition(Asset calldata asset, uint256 amount, bytes calldata parameters) external payable; + /// @notice Transfer asset out and decrement account balance. Pushes asset to sender. function unloadToUser(Asset calldata asset, uint256 amount, bytes calldata parameters) external; + /// @notice Transfer loan or collateral asset from account to Position MPC. Pushes. function unloadToPosition( address position, @@ -60,6 +63,7 @@ interface IAccount { ) external; function lockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) external; + function unlockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) external; // NOTE is is possible to (securely) require the owner addr to be the first parameter so that owner can diff --git a/src/interfaces/IAssessor.sol b/src/interfaces/IAssessor.sol index 43c4f1a..dc79c9c 100644 --- a/src/interfaces/IAssessor.sol +++ b/src/interfaces/IAssessor.sol @@ -16,9 +16,10 @@ import {Asset} from "src/libraries/LibUtils.sol"; interface IAssessor { /// @notice Returns the cost of a loan (not including principle) and the asset it is denoted in. /// @dev Asset must be ETH or ERC20. - function getCost(Agreement calldata agreement, uint256 currentAmount) - external - view - returns (Asset memory asset, uint256 amount); + function getCost( + Agreement calldata agreement, + uint256 currentAmount + ) external view returns (Asset memory asset, uint256 amount); + function canHandleAsset(Asset calldata asset, bytes calldata parameters) external view returns (bool); } diff --git a/src/interfaces/IBookkeeper.sol b/src/interfaces/IBookkeeper.sol index 454ea36..b772024 100644 --- a/src/interfaces/IBookkeeper.sol +++ b/src/interfaces/IBookkeeper.sol @@ -8,7 +8,10 @@ import {SignedBlueprint} from "lib/tractor/Tractor.sol"; interface IBookkeeper is ITractor { function signPublishOrder(Order calldata order, uint256 endTime) external; + function fillOrder(Fill calldata fill, SignedBlueprint calldata orderBlueprint) external; + function exitPosition(SignedBlueprint calldata agreementBlueprint) external payable; + function kick(SignedBlueprint calldata agreementBlueprint) external; } diff --git a/src/interfaces/ILiquidator.sol b/src/interfaces/ILiquidator.sol index 3da04e0..566f5dc 100644 --- a/src/interfaces/ILiquidator.sol +++ b/src/interfaces/ILiquidator.sol @@ -28,8 +28,9 @@ interface ILiquidator { /// @notice Handles receipt of a position that the bookkeeper has passed along for liquidation. function receiveKick(address kicker, Agreement calldata agreement) external; - function canHandleAssets(Asset calldata loanAsset, Asset calldata collAsset, bytes calldata parameters) - external - view - returns (bool); + function canHandleAssets( + Asset calldata loanAsset, + Asset calldata collAsset, + bytes calldata parameters + ) external view returns (bool); } diff --git a/src/interfaces/IPosition.sol b/src/interfaces/IPosition.sol index c950d6a..3c29b1a 100644 --- a/src/interfaces/IPosition.sol +++ b/src/interfaces/IPosition.sol @@ -25,16 +25,19 @@ interface IPosition is IAccessControl { /// @notice Get current exitable value of the position, denoted in loan asset. function getCloseAmount(bytes calldata parameters) external view returns (uint256); + /// @notice Transfer the position to a new controller. Used for liquidations. /// @dev Do not set admin role to prevent liquidator from pushing the position back into the protocol. function transferContract(address controller) external; function canHandleAsset(Asset calldata asset, bytes calldata parameters) external pure returns (bool); + /// @notice Pass through function to allow the position to interact with other contracts after liquidation. /// @dev Internal functions are not reachable. // NOTE right? bc allowing controller to be set *back* to bookkeeper will open exploits - function passThrough(address payable destination, bytes calldata data, bool delegateCall) - external - payable - returns (bool, bytes memory); + function passThrough( + address payable destination, + bytes calldata data, + bool delegateCall + ) external payable returns (bool, bytes memory); // function removeEth(address payable recipient) external // ONLY_ROLE(BOOKKEEPER_ROLE) } diff --git a/src/libraries/LibBookkeeper.sol b/src/libraries/LibBookkeeper.sol index f5446d7..ffdf5a2 100644 --- a/src/libraries/LibBookkeeper.sol +++ b/src/libraries/LibBookkeeper.sol @@ -103,20 +103,26 @@ library LibBookkeeper { uint256 outstandingValue; if (LibUtils.isValidLoanAssetAsCost(agreement.loanAsset, costAsset)) { if (cost > exitAmount) return true; - outstandingValue = - IOracle(agreement.loanOracle.addr).getSpotValue(exitAmount - cost, agreement.loanOracle.parameters); + outstandingValue = IOracle(agreement.loanOracle.addr).getSpotValue( + exitAmount - cost, + agreement.loanOracle.parameters + ); } else if (costAsset.standard == ETH_STANDARD) { - uint256 positionValue = - IOracle(agreement.loanOracle.addr).getSpotValue(exitAmount, agreement.loanOracle.parameters); + uint256 positionValue = IOracle(agreement.loanOracle.addr).getSpotValue( + exitAmount, + agreement.loanOracle.parameters + ); if (positionValue > cost) return true; outstandingValue = positionValue - cost; } else { revert("isLiquidatable: invalid cost asset"); } - uint256 collValue = - IOracle(agreement.collOracle.addr).getSpotValue(agreement.collAmount, agreement.collOracle.parameters); + uint256 collValue = IOracle(agreement.collOracle.addr).getSpotValue( + agreement.collAmount, + agreement.collOracle.parameters + ); - uint256 collateralRatio = C.RATIO_FACTOR * outstandingValue / collValue; + uint256 collateralRatio = (C.RATIO_FACTOR * outstandingValue) / collValue; if (collateralRatio < agreement.minCollateralRatio) { return true; diff --git a/src/libraries/LibUniswapV3.sol b/src/libraries/LibUniswapV3.sol index d59d1ff..375e9f0 100644 --- a/src/libraries/LibUniswapV3.sol +++ b/src/libraries/LibUniswapV3.sol @@ -49,8 +49,10 @@ library LibUniswapV3 { (tokenIn, tokenOut, fee) = path.decodeFirstPool(); tokens[i] = tokenIn; // console.log("tokenIn: %s, tokenOut: %s, fee: %s", tokenIn, tokenOut, fee); - address pool = - PoolAddress.computeAddress(C.UNI_V3_FACTORY, PoolAddress.getPoolKey(tokenIn, tokenOut, fee)); + address pool = PoolAddress.computeAddress( + C.UNI_V3_FACTORY, + PoolAddress.getPoolKey(tokenIn, tokenOut, fee) + ); // console.log("pool: %s", pool); // Computation depends on PoolAddress.POOL_INIT_CODE_HASH. Default value in Uni repo may not be correct. ticks[i] = getTWATick(pool, twapTime); @@ -72,14 +74,17 @@ library LibUniswapV3 { /// Get the TWAP of the pool across interval. token1/token0. function getTWATick(address pool, uint32 twapTime) internal view returns (int24 arithmeticMeanTick) { if (twapTime == 0) { - (, arithmeticMeanTick,,,,,) = IUniswapV3PoolState(pool).slot0(); + (, arithmeticMeanTick, , , , , ) = IUniswapV3PoolState(pool).slot0(); } else { require(LibUtils.isDeployedContract(pool), "Invalid pool, no contract at address"); - require(OracleLibrary.getOldestObservationSecondsAgo(pool) >= twapTime, "UniV3 pool observations too young"); // ensure needed data is available + require( + OracleLibrary.getOldestObservationSecondsAgo(pool) >= twapTime, + "UniV3 pool observations too young" + ); // ensure needed data is available // console.log("oldest observation seconds ago: %s", OracleLibrary.getOldestObservationSecondsAgo(pool)); - (,,, uint16 observationCardinality,,,) = IUniswapV3PoolState(pool).slot0(); + (, , , uint16 observationCardinality, , , ) = IUniswapV3PoolState(pool).slot0(); require(observationCardinality >= twapTime / 12, "UniV3 pool cardinality too low"); // shortest case scenario should always cover twap time - (arithmeticMeanTick,) = OracleLibrary.consult(pool, twapTime); + (arithmeticMeanTick, ) = OracleLibrary.consult(pool, twapTime); // console.log("arithmeticMeanTick:"); // console.logInt(arithmeticMeanTick); } diff --git a/src/libraries/LibUtils.sol b/src/libraries/LibUtils.sol index 91312f0..e3bd9a3 100644 --- a/src/libraries/LibUtils.sol +++ b/src/libraries/LibUtils.sol @@ -60,4 +60,3 @@ library LibUtils { return true; } } - \ No newline at end of file diff --git a/src/libraries/LibUtilsPublic.sol b/src/libraries/LibUtilsPublic.sol index a40c773..e963dc6 100644 --- a/src/libraries/LibUtilsPublic.sol +++ b/src/libraries/LibUtilsPublic.sol @@ -6,7 +6,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /* * This util functions are public so that they can be called from decoded calldata. Specifically this pattern - * is used with position passthrough functions. + * is used with position passthrough functions. */ library LibUtilsPublic { @@ -23,8 +23,9 @@ library LibUtilsPublic { /// @notice Transfers tokens from the targeted address to the given destination. /// @dev Return value is optional. function safeErc20TransferFrom(address token, address from, address to, uint256 value) public { - (bool success, bytes memory data) = - token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value)); + (bool success, bytes memory data) = token.call( + abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value) + ); require(success && (data.length == 0 || abi.decode(data, (bool))), "UtilsPublic.safeErc20TransferFrom failed"); } } diff --git a/src/modules/CloneFactory.sol b/src/modules/CloneFactory.sol index 8baac8f..50278e0 100644 --- a/src/modules/CloneFactory.sol +++ b/src/modules/CloneFactory.sol @@ -62,7 +62,7 @@ abstract contract CloneFactory is AccessControl, Initializable { */ function createPosition() external implementationExecution onlyRole(C.ADMIN_ROLE) returns (address addr) { addr = Clones.clone(address(this)); - (bool success,) = addr.call(abi.encodeWithSignature("initialize()")); + (bool success, ) = addr.call(abi.encodeWithSignature("initialize()")); require(success, "createPosition: initialize fail"); emit PositionCreated(addr); } diff --git a/src/modules/account/Account.sol b/src/modules/account/Account.sol index a48e4c2..c018f2a 100644 --- a/src/modules/account/Account.sol +++ b/src/modules/account/Account.sol @@ -27,11 +27,11 @@ abstract contract Account is IAccount, AccessControl, Module { emit LoadedFromUser(asset, amount, parameters); } - function loadFromPosition(Asset calldata asset, uint256 amount, bytes calldata parameters) - external - payable - override - { + function loadFromPosition( + Asset calldata asset, + uint256 amount, + bytes calldata parameters + ) external payable override { _loadFromPosition(asset, amount, parameters); emit LoadedFromPosition(asset, amount, parameters); } @@ -52,27 +52,30 @@ abstract contract Account is IAccount, AccessControl, Module { emit UnloadedToPosition(position, asset, amount, isLockedColl, parameters); } - function lockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) - external - override - onlyRole(C.BOOKKEEPER_ROLE) - { + function lockCollateral( + Asset calldata asset, + uint256 amount, + bytes calldata parameters + ) external override onlyRole(C.BOOKKEEPER_ROLE) { _lockCollateral(asset, amount, parameters); emit LockedCollateral(asset, amount, parameters); } - function unlockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) - external - override - onlyRole(C.BOOKKEEPER_ROLE) - { + function unlockCollateral( + Asset calldata asset, + uint256 amount, + bytes calldata parameters + ) external override onlyRole(C.BOOKKEEPER_ROLE) { _unlockCollateral(asset, amount, parameters); emit UnlockedCollateral(asset, amount, parameters); } function _loadFromUser(Asset calldata asset, uint256 amount, bytes calldata parameters) internal virtual; + function _loadFromPosition(Asset calldata asset, uint256 amount, bytes calldata parameters) internal virtual; + function _unloadToUser(Asset calldata asset, uint256 amount, bytes calldata parameters) internal virtual; + function _unloadToPosition( address position, Asset calldata asset, @@ -80,6 +83,8 @@ abstract contract Account is IAccount, AccessControl, Module { bool isLockedColl, bytes calldata parameters ) internal virtual; + function _lockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) internal virtual; + function _unlockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) internal virtual; } diff --git a/src/modules/account/implementations/SoloAccount.sol b/src/modules/account/implementations/SoloAccount.sol index 792fc51..c6a661b 100644 --- a/src/modules/account/implementations/SoloAccount.sol +++ b/src/modules/account/implementations/SoloAccount.sol @@ -45,8 +45,11 @@ contract SoloAccount is Account { Parameters memory params = abi.decode(parameters, (Parameters)); bytes32 id = _getId(params.owner, params.salt); bytes32 assetHash = keccak256(abi.encode(asset)); - unlockedBalances[id][assetHash] = - LibUtils.addWithMsg(unlockedBalances[id][assetHash], amount, "_load: balance too large"); + unlockedBalances[id][assetHash] = LibUtils.addWithMsg( + unlockedBalances[id][assetHash], + amount, + "_load: balance too large" + ); if (msg.value > 0 && asset.addr == C.WETH) { assert(msg.value == amount); @@ -61,8 +64,11 @@ contract SoloAccount is Account { require(msg.sender == params.owner, "unload: not owner"); bytes32 id = _getId(params.owner, params.salt); bytes32 assetHash = keccak256(abi.encode(asset)); - unlockedBalances[id][assetHash] = - LibUtils.subWithMsg(unlockedBalances[id][assetHash], amount, "_unloadToUser: balance too low"); + unlockedBalances[id][assetHash] = LibUtils.subWithMsg( + unlockedBalances[id][assetHash], + amount, + "_unloadToUser: balance too low" + ); LibUtilsPublic.safeErc20Transfer(asset.addr, msg.sender, amount); } @@ -83,8 +89,11 @@ contract SoloAccount is Account { bytes32 id = _getId(params.owner, params.salt); if (!isLockedColl) { bytes32 assetHash = keccak256(abi.encode(asset)); - unlockedBalances[id][assetHash] = - LibUtils.subWithMsg(unlockedBalances[id][assetHash], amount, "_unloadToPosition: balance too low"); + unlockedBalances[id][assetHash] = LibUtils.subWithMsg( + unlockedBalances[id][assetHash], + amount, + "_unloadToPosition: balance too low" + ); } // AUDIT any method to take out of other users locked balance? LibUtilsPublic.safeErc20Transfer(asset.addr, position, amount); @@ -92,30 +101,36 @@ contract SoloAccount is Account { // Without wasting gas on ERC20 transfer, lock assets here. In normal case (healthy position close) no transfers // of collateral are necessary. - function _lockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) - internal - override - onlyRole(C.BOOKKEEPER_ROLE) - { + function _lockCollateral( + Asset calldata asset, + uint256 amount, + bytes calldata parameters + ) internal override onlyRole(C.BOOKKEEPER_ROLE) { Parameters memory params = abi.decode(parameters, (Parameters)); bytes32 id = _getId(params.owner, params.salt); bytes32 assetHash = keccak256(abi.encode(asset)); - unlockedBalances[id][assetHash] = - LibUtils.subWithMsg(unlockedBalances[id][assetHash], amount, "_lockCollateral: balance too low"); + unlockedBalances[id][assetHash] = LibUtils.subWithMsg( + unlockedBalances[id][assetHash], + amount, + "_lockCollateral: balance too low" + ); } - function _unlockCollateral(Asset calldata asset, uint256 amount, bytes calldata parameters) - internal - override - onlyRole(C.BOOKKEEPER_ROLE) - { + function _unlockCollateral( + Asset calldata asset, + uint256 amount, + bytes calldata parameters + ) internal override onlyRole(C.BOOKKEEPER_ROLE) { Parameters memory params = abi.decode(parameters, (Parameters)); bytes32 id = _getId(params.owner, params.salt); bytes32 assetHash = keccak256(abi.encode(asset)); - unlockedBalances[id][assetHash] = - LibUtils.addWithMsg(unlockedBalances[id][assetHash], amount, "_unlockCollateral: balance too large"); + unlockedBalances[id][assetHash] = LibUtils.addWithMsg( + unlockedBalances[id][assetHash], + amount, + "_unlockCollateral: balance too large" + ); } function getOwner(bytes calldata parameters) external pure override returns (address) { @@ -127,12 +142,10 @@ contract SoloAccount is Account { return false; } - function getBalance(Asset calldata asset, bytes calldata parameters) - external - view - override - returns (uint256 amounts) - { + function getBalance( + Asset calldata asset, + bytes calldata parameters + ) external view override returns (uint256 amounts) { Parameters memory params = abi.decode(parameters, (Parameters)); bytes32 accountId = _getId(params.owner, params.salt); return unlockedBalances[accountId][keccak256(abi.encode(asset))]; diff --git a/src/modules/assessor/Assessor.sol b/src/modules/assessor/Assessor.sol index e19ec93..568c0bd 100644 --- a/src/modules/assessor/Assessor.sol +++ b/src/modules/assessor/Assessor.sol @@ -13,19 +13,17 @@ import {Asset, ETH_STANDARD, ERC20_STANDARD} from "src/libraries/LibUtils.sol"; import {Module} from "src/modules/Module.sol"; abstract contract Assessor is IAssessor, Module { - function getCost(Agreement calldata agreement, uint256 currentAmount) - external - view - returns (Asset memory asset, uint256 amount) - { + function getCost( + Agreement calldata agreement, + uint256 currentAmount + ) external view returns (Asset memory asset, uint256 amount) { (asset, amount) = _getCost(agreement, currentAmount); // Invariant check. require(asset.standard == ETH_STANDARD || asset.standard == ERC20_STANDARD, "getCost: invalid asset"); } - function _getCost(Agreement calldata agreement, uint256 currentAmount) - internal - view - virtual - returns (Asset memory asset, uint256 amount); + function _getCost( + Agreement calldata agreement, + uint256 currentAmount + ) internal view virtual returns (Asset memory asset, uint256 amount); } diff --git a/src/modules/assessor/implementations/StandardAssessor.sol b/src/modules/assessor/implementations/StandardAssessor.sol index ea51263..54d991b 100644 --- a/src/modules/assessor/implementations/StandardAssessor.sol +++ b/src/modules/assessor/implementations/StandardAssessor.sol @@ -24,19 +24,19 @@ contract StandardAssessor is Assessor { /// @notice Return the cost of a loan, quantified in the Loan Asset. This simplifies compatibility matrix. // NOTE this will not be compatible if borrowing a non-divisible asset. - function _getCost(Agreement calldata agreement, uint256 currentAmount) - internal - view - override - returns (Asset memory asset, uint256 amount) - { + function _getCost( + Agreement calldata agreement, + uint256 currentAmount + ) internal view override returns (Asset memory asset, uint256 amount) { Parameters memory params = abi.decode(agreement.assessor.parameters, (Parameters)); - uint256 originationFee = agreement.loanAmount * params.originationFeeRatio / C.RATIO_FACTOR; - uint256 interest = - agreement.loanAmount * (block.timestamp - agreement.deploymentTime) * params.interestRatio / C.RATIO_FACTOR; + uint256 originationFee = (agreement.loanAmount * params.originationFeeRatio) / C.RATIO_FACTOR; + uint256 interest = (agreement.loanAmount * + (block.timestamp - agreement.deploymentTime) * + params.interestRatio) / C.RATIO_FACTOR; uint256 lenderAmount = originationFee + interest + agreement.loanAmount; - uint256 profitShare = - currentAmount > lenderAmount ? (currentAmount - lenderAmount) * params.profitShareRatio / C.RATIO_FACTOR : 0; + uint256 profitShare = currentAmount > lenderAmount + ? ((currentAmount - lenderAmount) * params.profitShareRatio) / C.RATIO_FACTOR + : 0; return (params.asset, originationFee + interest + profitShare); } diff --git a/src/modules/liquidator/README.md b/src/modules/liquidator/README.md new file mode 100644 index 0000000..8a9f5a1 --- /dev/null +++ b/src/modules/liquidator/README.md @@ -0,0 +1,3 @@ + +## Implementation notes +- Must be able to handle loan asset == collateral asset type \ No newline at end of file diff --git a/src/modules/liquidator/implementations/InstantCloseTakeCollateral.sol b/src/modules/liquidator/implementations/InstantCloseTakeCollateral.sol index e1e096c..97a22a0 100644 --- a/src/modules/liquidator/implementations/InstantCloseTakeCollateral.sol +++ b/src/modules/liquidator/implementations/InstantCloseTakeCollateral.sol @@ -6,7 +6,7 @@ import {Agreement} from "src/libraries/LibBookkeeper.sol"; import {InstantErc20} from "./InstantErc20.sol"; /* - * Liquidate a position at kick time by giving closing the position and having position contract distribute loan and + * Liquidate a position at kick time by giving closing the position and having position contract distribute loan and * collateral assets between liquidator, lender, and borrower. Only useable with ERC20s due to need for divisibility. * Liquidator reward is all of the collateral. */ diff --git a/src/modules/liquidator/implementations/InstantCloseTakeValueRatio.sol b/src/modules/liquidator/implementations/InstantCloseTakeValueRatio.sol index 5324dd1..e2efd06 100644 --- a/src/modules/liquidator/implementations/InstantCloseTakeValueRatio.sol +++ b/src/modules/liquidator/implementations/InstantCloseTakeValueRatio.sol @@ -8,7 +8,7 @@ import {InstantErc20} from "./InstantErc20.sol"; import {IOracle} from "src/interfaces/IOracle.sol"; /* - * Liquidate a position at kick time by giving closing the position and having position contract distribute loan and + * Liquidate a position at kick time by giving closing the position and having position contract distribute loan and * collateral assets between liquidator, lender, and borrower. Only useable with ERC20s due to need for divisibility. * Liquidator reward is a ratio of loan value, and maximum is 100% of collateral assets. */ @@ -26,9 +26,11 @@ contract InstantCloseTakeValueRatio is InstantErc20 { function getRewardCollAmount(Agreement memory agreement) public view override returns (uint256 rewardCollAmount) { Parameters memory params = abi.decode(agreement.liquidator.parameters, (Parameters)); - uint256 loanValue = - IOracle(agreement.loanOracle.addr).getResistantValue(agreement.loanAmount, agreement.loanOracle.parameters); - uint256 rewardValue = loanValue * params.loanValueRatio / C.RATIO_FACTOR; + uint256 loanValue = IOracle(agreement.loanOracle.addr).getResistantValue( + agreement.loanAmount, + agreement.loanOracle.parameters + ); + uint256 rewardValue = (loanValue * params.loanValueRatio) / C.RATIO_FACTOR; return IOracle(agreement.collOracle.addr).getResistantAmount(rewardValue, agreement.collOracle.parameters); } } diff --git a/src/modules/liquidator/implementations/InstantErc20.sol b/src/modules/liquidator/implementations/InstantErc20.sol index fbe8889..fef7bd2 100644 --- a/src/modules/liquidator/implementations/InstantErc20.sol +++ b/src/modules/liquidator/implementations/InstantErc20.sol @@ -17,7 +17,7 @@ import {IAccount} from "src/interfaces/IAccount.sol"; import {IOracle} from "src/interfaces/IOracle.sol"; /* - * Liquidate a position at kick time and distribute loan and + * Liquidate a position at kick time and distribute loan and * collateral assets between liquidator, lender, and borrower. Only useable with ERC20s due to need for divisibility. */ @@ -40,10 +40,13 @@ abstract contract InstantErc20 is Liquidator { // Reward goes direct to liquidator. if (rewardCollAmount > 0) { // d4e3bdb6: LibUtilsPublic.safeErc20Transfer(address,address,uint256) - (bool success,) = IPosition(agreement.position.addr).passThrough( + (bool success, ) = IPosition(agreement.position.addr).passThrough( payable(address(LibUtilsPublic)), abi.encodeWithSelector( - LibUtilsPublic.safeErc20Transfer.selector, agreement.collAsset.addr, sender, rewardCollAmount + LibUtilsPublic.safeErc20Transfer.selector, + agreement.collAsset.addr, + sender, + rewardCollAmount ), true ); @@ -52,7 +55,10 @@ abstract contract InstantErc20 is Liquidator { // Spare collateral goes back to borrower. if (agreement.collAmount > rewardCollAmount) { _loadFromPosition( - position, agreement.borrowerAccount, agreement.collAsset, agreement.collAmount - rewardCollAmount + position, + agreement.borrowerAccount, + agreement.collAsset, + agreement.collAmount - rewardCollAmount ); } @@ -77,9 +83,12 @@ abstract contract InstantErc20 is Liquidator { } else if (costAsset.standard == ETH_STANDARD) { require(msg.value == cost, "_liquidate: msg.value mismatch from Eth denoted cost"); IAccount(agreement.lenderAccount.addr).loadFromPosition{value: cost}( - costAsset, cost, agreement.lenderAccount.parameters + costAsset, + cost, + agreement.lenderAccount.parameters ); - } // SECURITY are these else revert checks necessary? + } + // SECURITY are these else revert checks necessary? else { revert("exitPosition: isLiquidatable: invalid cost asset"); } @@ -95,18 +104,23 @@ abstract contract InstantErc20 is Liquidator { // bookkeeper report Asset(s) and Loaded amount(s) at kick time? Trusted bookkeeper sideload reduces # of // asset transfers by 1 per asset. /// @notice Load assets from position to an account. - function _loadFromPosition(IPosition position, ModuleReference memory account, Asset memory asset, uint256 amount) - private - { - (bool success,) = position.passThrough( - payable(asset.addr), abi.encodeWithSelector(IERC20.approve.selector, account.addr, amount), false + function _loadFromPosition( + IPosition position, + ModuleReference memory account, + Asset memory asset, + uint256 amount + ) private { + (bool success, ) = position.passThrough( + payable(asset.addr), + abi.encodeWithSelector(IERC20.approve.selector, account.addr, amount), + false ); require(success, "Failed to approve position ERC20 spend"); // SECURITY why does anyone involved in the agreement care if liquidator uses _loadFromPosition vs // loadFromUser? It is basically passing up on ownership of account assets. A hostile liquidator // implementation could then essentially siphon off assets in an account without loss by lender // or borrower. - (success,) = position.passThrough( + (success, ) = position.passThrough( payable(account.addr), abi.encodeWithSelector(IAccount.loadFromPosition.selector, asset, amount, account.parameters), false @@ -118,12 +132,11 @@ abstract contract InstantErc20 is Liquidator { /// @dev may return a number that is larger than the total collateral amount. function getRewardCollAmount(Agreement memory agreement) public view virtual returns (uint256 rewardCollAmount); - function canHandleAssets(Asset calldata loanAsset, Asset calldata collAsset, bytes calldata) - external - pure - override - returns (bool) - { + function canHandleAssets( + Asset calldata loanAsset, + Asset calldata collAsset, + bytes calldata + ) external pure override returns (bool) { if (loanAsset.standard == ERC20_STANDARD && collAsset.standard == ERC20_STANDARD) return true; return false; } diff --git a/src/modules/liquidator/implementations/InstantKeepTakeCollateral.sol b/src/modules/liquidator/implementations/InstantKeepTakeCollateral.sol index affc924..c4b5860 100644 --- a/src/modules/liquidator/implementations/InstantKeepTakeCollateral.sol +++ b/src/modules/liquidator/implementations/InstantKeepTakeCollateral.sol @@ -6,7 +6,7 @@ import {Agreement} from "src/libraries/LibBookkeeper.sol"; import {InstantErc20} from "./InstantErc20.sol"; /* - * Liquidate a position at kick time by giving closing the position and having position contract distribute loan and + * Liquidate a position at kick time by giving closing the position and having position contract distribute loan and * collateral assets between liquidator, lender, and borrower. Only useable with ERC20s due to need for divisibility. * Liquidator reward is all of the collateral. */ diff --git a/src/modules/oracle/implementations/StaticValue.sol b/src/modules/oracle/implementations/StaticValue.sol index 5547fa3..7ccee24 100644 --- a/src/modules/oracle/implementations/StaticValue.sol +++ b/src/modules/oracle/implementations/StaticValue.sol @@ -26,7 +26,7 @@ contract StaticPriceOracle is Oracle { function getResistantAmount(uint256 ethAmount, bytes calldata parameters) external pure returns (uint256) { Parameters memory params = abi.decode(parameters, (Parameters)); - return ethAmount * params.ratio / (10 ** C.ETH_DECIMALS); // AUDIT rounding? + return (ethAmount * params.ratio) / (10 ** C.ETH_DECIMALS); // AUDIT rounding? } function canHandleAsset(Asset calldata, bytes calldata) external pure override returns (bool) { @@ -35,6 +35,6 @@ contract StaticPriceOracle is Oracle { function _value(uint256 amount, bytes calldata parameters) private pure returns (uint256) { Parameters memory params = abi.decode(parameters, (Parameters)); - return amount * (10 ** C.ETH_DECIMALS) / params.ratio; // AUDIT rounding? + return (amount * (10 ** C.ETH_DECIMALS)) / params.ratio; // AUDIT rounding? } } diff --git a/src/modules/oracle/implementations/UniswapV3Oracle.sol b/src/modules/oracle/implementations/UniswapV3Oracle.sol index 5230117..7cf5637 100644 --- a/src/modules/oracle/implementations/UniswapV3Oracle.sol +++ b/src/modules/oracle/implementations/UniswapV3Oracle.sol @@ -25,20 +25,23 @@ contract UniswapV3Oracle is Oracle { function getResistantValue(uint256 amount, bytes calldata parameters) external view returns (uint256 value) { Parameters memory params = abi.decode(parameters, (Parameters)); - return LibUniswapV3.getPathTWAP(params.pathToEth, amount, params.twapTime) - * (C.RATIO_FACTOR - params.stepSlippage * params.pathToEth.numPools()) / C.RATIO_FACTOR; + return + (LibUniswapV3.getPathTWAP(params.pathToEth, amount, params.twapTime) * + (C.RATIO_FACTOR - params.stepSlippage * params.pathToEth.numPools())) / C.RATIO_FACTOR; } function getSpotValue(uint256 amount, bytes calldata parameters) external view returns (uint256 value) { Parameters memory params = abi.decode(parameters, (Parameters)); - return LibUniswapV3.getPathSpotPrice(params.pathToEth, amount) - * (C.RATIO_FACTOR - params.stepSlippage * params.pathToEth.numPools()) / C.RATIO_FACTOR; + return + (LibUniswapV3.getPathSpotPrice(params.pathToEth, amount) * + (C.RATIO_FACTOR - params.stepSlippage * params.pathToEth.numPools())) / C.RATIO_FACTOR; } function getResistantAmount(uint256 value, bytes calldata parameters) external view returns (uint256) { Parameters memory params = abi.decode(parameters, (Parameters)); - return LibUniswapV3.getPathTWAP(params.pathFromEth, value, params.twapTime) - * (C.RATIO_FACTOR - params.stepSlippage * params.pathFromEth.numPools()) / C.RATIO_FACTOR; + return + (LibUniswapV3.getPathTWAP(params.pathFromEth, value, params.twapTime) * + (C.RATIO_FACTOR - params.stepSlippage * params.pathFromEth.numPools())) / C.RATIO_FACTOR; } /// @notice verify that parameters are valid combination with this implementation. Users should be able to use diff --git a/src/modules/position/Position.sol b/src/modules/position/Position.sol index 53df950..0393c5c 100644 --- a/src/modules/position/Position.sol +++ b/src/modules/position/Position.sol @@ -21,34 +21,28 @@ abstract contract Position is IPosition, CloneFactory, Module { // _setupRole } - function deploy(Asset calldata asset, uint256 amount, bytes calldata parameters) - external - override - proxyExecution - onlyRole(C.ADMIN_ROLE) - { + function deploy( + Asset calldata asset, + uint256 amount, + bytes calldata parameters + ) external override proxyExecution onlyRole(C.ADMIN_ROLE) { _deploy(asset, amount, parameters); } function _deploy(Asset calldata asset, uint256 amount, bytes calldata parameters) internal virtual; - function close(address sender, Agreement calldata agreement) - external - override - proxyExecution - onlyRole(C.ADMIN_ROLE) - returns (uint256) - { + function close( + address sender, + Agreement calldata agreement + ) external override proxyExecution onlyRole(C.ADMIN_ROLE) returns (uint256) { return _close(sender, agreement); } - function distribute(address sender, uint256 lenderAmount, Agreement calldata agreement) - external - payable - override - proxyExecution - onlyRole(C.ADMIN_ROLE) - { + function distribute( + address sender, + uint256 lenderAmount, + Agreement calldata agreement + ) external payable override proxyExecution onlyRole(C.ADMIN_ROLE) { return _distribute(sender, lenderAmount, agreement); } @@ -82,13 +76,11 @@ abstract contract Position is IPosition, CloneFactory, Module { emit ControlTransferred(msg.sender, controller); } - function passThrough(address payable destination, bytes calldata data, bool delegateCall) - external - payable - proxyExecution - onlyRole(C.ADMIN_ROLE) - returns (bool, bytes memory) - { + function passThrough( + address payable destination, + bytes calldata data, + bool delegateCall + ) external payable proxyExecution onlyRole(C.ADMIN_ROLE) returns (bool, bytes memory) { if (!delegateCall) { return destination.call{value: msg.value}(data); } else { diff --git a/src/modules/position/implementations/UniV3Hold.sol b/src/modules/position/implementations/UniV3Hold.sol index fdce0e8..d61811b 100644 --- a/src/modules/position/implementations/UniV3Hold.sol +++ b/src/modules/position/implementations/UniV3Hold.sol @@ -81,9 +81,10 @@ contract UniV3HoldFactory is Position { using Path for bytes; using BytesLib for bytes; - constructor(address protocolAddr) Position(protocolAddr) - // Component(compatibleLoanAssets, compatibleCollAssets) - {} + constructor(address protocolAddr) Position(protocolAddr) // Component(compatibleLoanAssets, compatibleCollAssets) + { + + } function canHandleAsset(Asset calldata asset, bytes calldata parameters) external pure override returns (bool) { Parameters memory params = abi.decode(parameters, (Parameters)); @@ -164,7 +165,7 @@ contract UniV3HoldFactory is Position { function _close(address, Agreement calldata agreement) internal override returns (uint256 closedAmount) { Parameters memory params = abi.decode(agreement.position.parameters, (Parameters)); // require(heldAsset.standard == ERC20_STANDARD, "UniV3Hold: exit asset must be ETH or ERC20"); - (address heldAsset,,) = params.exitPath.decodeFirstPool(); + (address heldAsset, , ) = params.exitPath.decodeFirstPool(); uint256 transferAmount = amountHeld; amountHeld = 0; @@ -186,18 +187,17 @@ contract UniV3HoldFactory is Position { } } - function _distribute(address sender, uint256 lenderAmount, Agreement calldata agreement) - internal - - override - { + function _distribute(address sender, uint256 lenderAmount, Agreement calldata agreement) internal override { IERC20 erc20 = IERC20(agreement.loanAsset.addr); uint256 balance = erc20.balanceOf(address(this)); // If there are not enough assets to pay lender, pull missing from sender. if (lenderAmount > balance) { LibUtilsPublic.safeErc20TransferFrom( - agreement.loanAsset.addr, sender, address(this), lenderAmount - balance + agreement.loanAsset.addr, + sender, + address(this), + lenderAmount - balance ); balance += lenderAmount - balance; } @@ -207,7 +207,9 @@ contract UniV3HoldFactory is Position { // loadFromUser rathe than loadFromPosition. erc20.approve(agreement.lenderAccount.addr, lenderAmount); IAccount(agreement.lenderAccount.addr).loadFromPosition( - agreement.loanAsset, lenderAmount, agreement.lenderAccount.parameters + agreement.loanAsset, + lenderAmount, + agreement.lenderAccount.parameters ); balance -= lenderAmount; } @@ -216,7 +218,9 @@ contract UniV3HoldFactory is Position { if (balance > 0) { erc20.approve(agreement.borrowerAccount.addr, balance); IAccount(agreement.borrowerAccount.addr).loadFromPosition( - agreement.loanAsset, balance, agreement.borrowerAccount.parameters + agreement.loanAsset, + balance, + agreement.borrowerAccount.parameters ); } // Collateral is still in borrower account and is unlocked by the bookkeeper. @@ -233,8 +237,9 @@ contract UniV3HoldFactory is Position { // NOTE this is an inexact method of computing multistep slippage. but exponentials are hard. function amountOutMin(Parameters memory params) private view returns (uint256) { - return LibUniswapV3.getPathTWAP(params.exitPath, amountHeld, TWAP_TIME) - * (C.RATIO_FACTOR - STEP_SLIPPAGE_RATIO * params.exitPath.numPools()) / C.RATIO_FACTOR; + return + (LibUniswapV3.getPathTWAP(params.exitPath, amountHeld, TWAP_TIME) * + (C.RATIO_FACTOR - STEP_SLIPPAGE_RATIO * params.exitPath.numPools())) / C.RATIO_FACTOR; } function validParameters(bytes calldata parameters) private view returns (bool) { diff --git a/src/modules/position/implementations/Wallet.sol b/src/modules/position/implementations/Wallet.sol index cb32828..491849e 100644 --- a/src/modules/position/implementations/Wallet.sol +++ b/src/modules/position/implementations/Wallet.sol @@ -41,8 +41,10 @@ contract WalletFactory is Position { uint256 returnAmount = amountDistributed; // Positions do not typically factor in a cost, but doing so here often saves an ERC20 transfer in distribute. - (Asset memory costAsset, uint256 cost) = - IAssessor(agreement.assessor.addr).getCost(agreement, amountDistributed); + (Asset memory costAsset, uint256 cost) = IAssessor(agreement.assessor.addr).getCost( + agreement, + amountDistributed + ); if (LibUtils.isValidLoanAssetAsCost(agreement.loanAsset, costAsset)) { returnAmount += cost; } @@ -60,14 +62,19 @@ contract WalletFactory is Position { // If there are not enough assets to pay lender, pull missing from sender. if (lenderAmount > balance) { LibUtilsPublic.safeErc20TransferFrom( - agreement.loanAsset.addr, sender, address(this), lenderAmount - balance + agreement.loanAsset.addr, + sender, + address(this), + lenderAmount - balance ); } if (lenderAmount > 0) { erc20.approve(agreement.lenderAccount.addr, lenderAmount); IAccount(agreement.lenderAccount.addr).loadFromPosition( - agreement.loanAsset, lenderAmount, agreement.lenderAccount.parameters + agreement.loanAsset, + lenderAmount, + agreement.lenderAccount.parameters ); } } diff --git a/test/EndToEnd.t.sol b/test/EndToEnd.t.sol index 83734f0..fe3ee15 100644 --- a/test/EndToEnd.t.sol +++ b/test/EndToEnd.t.sol @@ -100,15 +100,18 @@ contract EndToEndTest is TestUtils { address borrower = vm.addr(BORROWER_PRIVATE_KEY); SoloAccount.Parameters memory lenderAccountParams = SoloAccount.Parameters({owner: lender, salt: bytes32(0)}); - SoloAccount.Parameters memory borrowerAccountParams = - SoloAccount.Parameters({owner: borrower, salt: bytes32(0)}); + SoloAccount.Parameters memory borrowerAccountParams = SoloAccount.Parameters({ + owner: borrower, + salt: bytes32(0) + }); fundAccount(lenderAccountParams); fundAccount(borrowerAccountParams); assertEq(accountModule.getBalance(WETH_ASSET, abi.encode(lenderAccountParams)), 10e18); assertEq( - accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), 5_000 * (10 ** C.USDC_DECIMALS) + accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), + 5_000 * (10 ** C.USDC_DECIMALS) ); Order memory order = createOrder(lenderAccountParams); @@ -134,7 +137,8 @@ contract EndToEndTest is TestUtils { assertEq(accountModule.getBalance(WETH_ASSET, abi.encode(lenderAccountParams)), 8e18); assertLt( - accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), 5_000 * (10 ** C.USDC_DECIMALS) + accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), + 5_000 * (10 ** C.USDC_DECIMALS) ); assertGt(accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), 0); @@ -160,7 +164,9 @@ contract EndToEndTest is TestUtils { bookkeeper.exitPosition(agreementSignedBlueprint); assertGe( - accountModule.getBalance(WETH_ASSET, abi.encode(lenderAccountParams)), 10e18, "lender act funds missing" + accountModule.getBalance(WETH_ASSET, abi.encode(lenderAccountParams)), + 10e18, + "lender act funds missing" ); assertEq( accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), @@ -178,15 +184,18 @@ contract EndToEndTest is TestUtils { address liquidator = vm.addr(LIQUIDATOR_PRIVATE_KEY); SoloAccount.Parameters memory lenderAccountParams = SoloAccount.Parameters({owner: lender, salt: bytes32(0)}); - SoloAccount.Parameters memory borrowerAccountParams = - SoloAccount.Parameters({owner: borrower, salt: bytes32(0)}); + SoloAccount.Parameters memory borrowerAccountParams = SoloAccount.Parameters({ + owner: borrower, + salt: bytes32(0) + }); fundAccount(lenderAccountParams); fundAccount(borrowerAccountParams); assertEq(accountModule.getBalance(WETH_ASSET, abi.encode(lenderAccountParams)), 10e18); assertEq( - accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), 5_000 * (10 ** C.USDC_DECIMALS) + accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), + 5_000 * (10 ** C.USDC_DECIMALS) ); Order memory order = createOrder(lenderAccountParams); @@ -208,7 +217,8 @@ contract EndToEndTest is TestUtils { assertEq(accountModule.getBalance(WETH_ASSET, abi.encode(lenderAccountParams)), 8e18); assertLt( - accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), 5_000 * (10 ** C.USDC_DECIMALS) + accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), + 5_000 * (10 ** C.USDC_DECIMALS) ); assertGt(accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), 0); @@ -237,7 +247,8 @@ contract EndToEndTest is TestUtils { assertGe(accountModule.getBalance(WETH_ASSET, abi.encode(lenderAccountParams)), 10e18); assertLt( - accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), 5_000 * (10 ** C.USDC_DECIMALS) + accountModule.getBalance(USDC_ASSET, abi.encode(borrowerAccountParams)), + 5_000 * (10 ** C.USDC_DECIMALS) ); assertGt(IERC20(USDC_ASSET.addr).balanceOf(liquidator), 0); @@ -259,8 +270,10 @@ contract EndToEndTest is TestUtils { function createOrder(SoloAccount.Parameters memory accountParams) private view returns (Order memory) { // Set individual structs here for cleanliness and solidity ease. - ModuleReference memory account = - ModuleReference({addr: address(accountModule), parameters: abi.encode(accountParams)}); + ModuleReference memory account = ModuleReference({ + addr: address(accountModule), + parameters: abi.encode(accountParams) + }); // Solidity array syntax is so bad D: address[] memory takers = new address[](0); uint256[] memory minLoanAmounts = new uint256[](2); @@ -320,29 +333,30 @@ contract EndToEndTest is TestUtils { interestRatio: C.RATIO_FACTOR / 1000000000, profitShareRatio: C.RATIO_FACTOR / 20 }) - ) + ) }); ModuleReference memory liquidator = ModuleReference({addr: address(liquidatorModule), parameters: ""}); // Lender creates an offer. - return Order({ - minLoanAmounts: minLoanAmounts, - loanAssets: loanAssets, - collAssets: collAssets, - takers: takers, - maxDuration: 7 days, - minCollateralRatio: C.RATIO_FACTOR / 5, - account: account, - assessor: assessor, - liquidator: liquidator, - /* Allowlisted variables */ - loanOracles: loanOracles, - collOracles: collOracles, - // Lender would need to list parameters for all possible holdable tokens from all possible lent tokens. Instead just allow a whole factory. - factories: factories, - isOffer: true, - borrowerConfig: BorrowerConfig(0, "") - }); + return + Order({ + minLoanAmounts: minLoanAmounts, + loanAssets: loanAssets, + collAssets: collAssets, + takers: takers, + maxDuration: 7 days, + minCollateralRatio: C.RATIO_FACTOR / 5, + account: account, + assessor: assessor, + liquidator: liquidator, + /* Allowlisted variables */ + loanOracles: loanOracles, + collOracles: collOracles, + // Lender would need to list parameters for all possible holdable tokens from all possible lent tokens. Instead just allow a whole factory. + factories: factories, + isOffer: true, + borrowerConfig: BorrowerConfig(0, "") + }); } function createFill(SoloAccount.Parameters memory borrowerAccountParams) private view returns (Fill memory) { @@ -356,29 +370,29 @@ contract EndToEndTest is TestUtils { // ) // }); BorrowerConfig memory borrowerConfig = BorrowerConfig({ - initCollateralRatio: C.RATIO_FACTOR * 11 / 10, // 110% + initCollateralRatio: (C.RATIO_FACTOR * 11) / 10, // 110% positionParameters: abi.encode(WalletFactory.Parameters({recipient: borrowerAccountParams.owner})) }); - return Fill({ - account: ModuleReference({addr: address(accountModule), parameters: abi.encode(borrowerAccountParams)}), - loanAmount: 2e18, // must be valid with init CR and available collateral value - takerIdx: 0, - loanAssetIdx: 0, - collAssetIdx: 0, - loanOracleIdx: 0, - collOracleIdx: 0, - factoryIdx: 0, - isOfferFill: true, - borrowerConfig: borrowerConfig - }); + return + Fill({ + account: ModuleReference({addr: address(accountModule), parameters: abi.encode(borrowerAccountParams)}), + loanAmount: 2e18, // must be valid with init CR and available collateral value + takerIdx: 0, + loanAssetIdx: 0, + collAssetIdx: 0, + loanOracleIdx: 0, + collOracleIdx: 0, + factoryIdx: 0, + isOfferFill: true, + borrowerConfig: borrowerConfig + }); } - function createSignedBlueprint(Blueprint memory blueprint, uint256 privateKey) - private - view - returns (SignedBlueprint memory) - { + function createSignedBlueprint( + Blueprint memory blueprint, + uint256 privateKey + ) private view returns (SignedBlueprint memory) { bytes32 blueprintHash = bookkeeper.getBlueprintHash(blueprint); (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, blueprintHash); diff --git a/test/_test_brownie_tmpl.py b/test/_test_brownie_tmpl.py index 650dad9..25491d8 100644 --- a/test/_test_brownie_tmpl.py +++ b/test/_test_brownie_tmpl.py @@ -1,5 +1,9 @@ +from brownie.test import given, strategy +from brownie import Token, accounts +import pytest from brownie import accounts + def test_account_balance(): balance = accounts[0].balance() accounts[0].transfer(accounts[1], "10 ether", gas_price=0) @@ -7,30 +11,26 @@ def test_account_balance(): assert balance - "10 ether" == accounts[0].balance() - - -import pytest - -from brownie import Token, accounts - @pytest.fixture def token(): return accounts[0].deploy(Token, "Test Token", "TST", 18, 1000) + def test_transfer(token): token.transfer(accounts[1], 100, {'from': accounts[0]}) assert token.balanceOf(accounts[0]) == 900 - @pytest.fixture(scope="module") def token(Token): return accounts[0].deploy(Token, "Test Token", "TST", 18, 1000) + def test_approval(token, accounts): token.approve(accounts[1], 500, {'from': accounts[0]}) assert token.allowance(accounts[0], accounts[1]) == 500 + def test_transfer(token, accounts): token.transfer(accounts[1], 100, {'from': accounts[0]}) assert token.balanceOf(accounts[0]) == 900 @@ -43,15 +43,14 @@ def token(Token, accounts): t = accounts[0].deploy(Token, "Test Token", "TST", 18, 1000) yield t + @pytest.fixture(autouse=True) def isolation(fn_isolation): pass # Parametrizing Tests - -import pytest @pytest.mark.parametrize('amount', [0, 100, 500]) def test_transferFrom_reverts(token, accounts, amount): @@ -59,9 +58,7 @@ def test_transferFrom_reverts(token, accounts, amount): assert token.allowance(accounts[0], accounts[1]) == amount -from brownie.test import given, strategy - @given(amount=strategy('uint', max_value=1000)) def test_transferFrom_reverts(token, accounts, amount): token.approve(accounts[1], amount, {'from': accounts[0]}) - assert token.allowance(accounts[0], accounts[1]) == amount \ No newline at end of file + assert token.allowance(accounts[0], accounts[1]) == amount diff --git a/test/mocks/MockAssessor.sol b/test/mocks/MockAssessor.sol index efa72d7..b839ce1 100644 --- a/test/mocks/MockAssessor.sol +++ b/test/mocks/MockAssessor.sol @@ -15,12 +15,7 @@ contract MockAssessor is Assessor { finalCost = cost; } - function _getCost(Agreement calldata, uint256) - internal - view - override - returns (Asset memory asset, uint256 amount) - { + function _getCost(Agreement calldata, uint256) internal view override returns (Asset memory asset, uint256 amount) { return (instanceAsset, finalCost); } diff --git a/test/mocks/MockPosition.sol b/test/mocks/MockPosition.sol index dfd26bc..e733d81 100644 --- a/test/mocks/MockPosition.sol +++ b/test/mocks/MockPosition.sol @@ -27,7 +27,7 @@ contract MockPosition is Position { } function _close(address, Agreement calldata agreement) internal view override returns (uint256 closedAmount) { - (Asset memory costAsset,) = IAssessor(agreement.assessor.addr).getCost(agreement, closedAmount); + (Asset memory costAsset, ) = IAssessor(agreement.assessor.addr).getCost(agreement, closedAmount); require( LibUtils.isValidLoanAssetAsCost(agreement.loanAsset, costAsset), "MockPosition, _close(): cost asset invalid" @@ -44,7 +44,10 @@ contract MockPosition is Position { // Lender is owed more than the position is worth. // Sender pays the difference. LibUtilsPublic.safeErc20TransferFrom( - agreement.loanAsset.addr, sender, address(this), lenderAmount - balance + agreement.loanAsset.addr, + sender, + address(this), + lenderAmount - balance ); balance += lenderAmount - balance; } @@ -52,7 +55,9 @@ contract MockPosition is Position { if (lenderAmount > 0) { erc20.approve(agreement.lenderAccount.addr, lenderAmount); IAccount(agreement.lenderAccount.addr).loadFromPosition( - agreement.loanAsset, lenderAmount, agreement.lenderAccount.parameters + agreement.loanAsset, + lenderAmount, + agreement.lenderAccount.parameters ); balance -= lenderAmount; } @@ -60,7 +65,9 @@ contract MockPosition is Position { if (balance > 0) { erc20.approve(agreement.borrowerAccount.addr, balance); IAccount(agreement.borrowerAccount.addr).loadFromPosition( - agreement.loanAsset, balance, agreement.borrowerAccount.parameters + agreement.loanAsset, + balance, + agreement.borrowerAccount.parameters ); } } diff --git a/test/modules/account/DoubleSidedAccount.t.sol b/test/modules/account/DoubleSidedAccount.t.sol index 0dfc97c..412c4a8 100644 --- a/test/modules/account/DoubleSidedAccount.t.sol +++ b/test/modules/account/DoubleSidedAccount.t.sol @@ -141,6 +141,7 @@ contract Handler is Test, HandlerUtils { SoloAccount public accountModule; Asset[] public ASSETS; uint256[] public assetBalances; + // uint256[] public assetsIn; // uint256[] public assetsOut; @@ -151,12 +152,12 @@ contract Handler is Test, HandlerUtils { accountModule = new SoloAccount(address(1)); } - function load(uint256 assetIdx, uint256 amount, address owner, bytes32 salt) - external - payable - createActor - countCall("load") - { + function load( + uint256 assetIdx, + uint256 amount, + address owner, + bytes32 salt + ) external payable createActor countCall("load") { assetIdx = bound(assetIdx, 0, ASSETS.length - 1); amount = bound(amount, 0, type(uint128).max); Asset memory asset = ASSETS[assetIdx]; @@ -180,11 +181,13 @@ contract Handler is Test, HandlerUtils { accountModule.loadFromUser{value: value}(asset, amount, parameters); } - function unload(uint256 actorIndexSeed, uint256 assetIdx, uint256 amount, address owner, bytes32 salt) - external - useActor(actorIndexSeed) - countCall("unload") - { + function unload( + uint256 actorIndexSeed, + uint256 assetIdx, + uint256 amount, + address owner, + bytes32 salt + ) external useActor(actorIndexSeed) countCall("unload") { assetIdx = bound(assetIdx, 0, ASSETS.length - 1); amount = bound(amount, 0, type(uint128).max); Asset memory asset = ASSETS[assetIdx]; @@ -230,7 +233,7 @@ contract InvariantAccountTest is Test { function invariant_ExpectedCumulativeBalances() public { for (uint256 j; j < handler.ASSETSLength(); j++) { - (bytes3 standard, address addr,,,) = handler.ASSETS(j); + (bytes3 standard, address addr, , , ) = handler.ASSETS(j); if (standard == ETH_STANDARD) { assertEq(address(handler.accountModule()).balance, handler.assetBalances(j)); } else if (standard == ERC20_STANDARD) { diff --git a/test/modules/assessor/StandardAssessor.t.sol b/test/modules/assessor/StandardAssessor.t.sol index 227b3d3..19ae752 100644 --- a/test/modules/assessor/StandardAssessor.t.sol +++ b/test/modules/assessor/StandardAssessor.t.sol @@ -49,7 +49,7 @@ contract StandardAssessorTest is Test { vm.prank(address(0)); position = IPosition(positionFactory.createPosition()); vm.prank(address(0)); - position.deploy(mockAsset, loanAmount * currentValueRatio / C.RATIO_FACTOR, ""); + position.deploy(mockAsset, (loanAmount * currentValueRatio) / C.RATIO_FACTOR, ""); Agreement memory agreement; agreement.loanAmount = loanAmount; @@ -76,13 +76,13 @@ contract StandardAssessorTest is Test { Asset memory asset; uint256 cost; - (asset, cost) = getCost(10 * C.RATIO_FACTOR / 100, 0, 0, loanAmount, 1, timePassed); // 10% origination fee + (asset, cost) = getCost((10 * C.RATIO_FACTOR) / 100, 0, 0, loanAmount, 1, timePassed); // 10% origination fee assertEq(cost, loanAmount / 10); // certainly going to have rounding error here (asset, cost) = getCost(0, C.RATIO_FACTOR / 1_000_000, 0, loanAmount, 1, timePassed); // 0.0001% interest per second for 1 day - assertEq(cost, loanAmount * timePassed / 1_000_000); // rounding errors? - (asset, cost) = getCost(0, 0, 5 * C.RATIO_FACTOR / 100, loanAmount, 120 * C.RATIO_FACTOR / 100, timePassed); // 5% of 20% profit - assertEq(cost, loanAmount * 20 * 5 / 100 / 100); // rounding errors? - (asset, cost) = getCost(0, 0, 5 * C.RATIO_FACTOR / 100, loanAmount, 90 * C.RATIO_FACTOR / 100, timePassed); // 5% of 10% loss + assertEq(cost, (loanAmount * timePassed) / 1_000_000); // rounding errors? + (asset, cost) = getCost(0, 0, (5 * C.RATIO_FACTOR) / 100, loanAmount, (120 * C.RATIO_FACTOR) / 100, timePassed); // 5% of 20% profit + assertEq(cost, (loanAmount * 20 * 5) / 100 / 100); // rounding errors? + (asset, cost) = getCost(0, 0, (5 * C.RATIO_FACTOR) / 100, loanAmount, (90 * C.RATIO_FACTOR) / 100, timePassed); // 5% of 10% loss assertEq(cost, 0); assertEq(asset.standard, ERC20_STANDARD); } @@ -103,33 +103,49 @@ contract StandardAssessorTest is Test { loanAmount = bound(loanAmount, 0, type(uint64).max); currentValueRatio = bound(currentValueRatio, 0, 3 * C.RATIO_FACTOR); timePassed = bound(timePassed, 0, 365 * 24 * 60 * 60); - (, uint256 cost) = - getCost(originationFeeRatio, interestRatio, profitShareRatio, loanAmount, currentValueRatio, timePassed); - uint256 currentValue = loanAmount * currentValueRatio / C.RATIO_FACTOR; + (, uint256 cost) = getCost( + originationFeeRatio, + interestRatio, + profitShareRatio, + loanAmount, + currentValueRatio, + timePassed + ); + uint256 currentValue = (loanAmount * currentValueRatio) / C.RATIO_FACTOR; { // assertLe(cost, loanAmount); // May not be true if cost parameters v high. - uint256 originationFee = loanAmount * originationFeeRatio / C.RATIO_FACTOR; + uint256 originationFee = (loanAmount * originationFeeRatio) / C.RATIO_FACTOR; assertGe(cost, originationFee); - uint256 interest = loanAmount * timePassed * interestRatio / C.RATIO_FACTOR; + uint256 interest = (loanAmount * timePassed * interestRatio) / C.RATIO_FACTOR; assertGe(cost, interest); uint256 nonProfitCost = loanAmount + originationFee + interest; if (currentValue > nonProfitCost) { - uint256 profitShare = (currentValue - nonProfitCost) * profitShareRatio / C.RATIO_FACTOR; + uint256 profitShare = ((currentValue - nonProfitCost) * profitShareRatio) / C.RATIO_FACTOR; assertGe(cost, profitShare); } } { scaleUpRatio = bound(scaleUpRatio, 0, C.RATIO_FACTOR) + C.RATIO_FACTOR; - uint256 loanAmountBig = loanAmount * scaleUpRatio / C.RATIO_FACTOR; - uint256 timePassedBig = timePassed * scaleUpRatio / C.RATIO_FACTOR; + uint256 loanAmountBig = (loanAmount * scaleUpRatio) / C.RATIO_FACTOR; + uint256 timePassedBig = (timePassed * scaleUpRatio) / C.RATIO_FACTOR; (, uint256 newCost) = getCost( - originationFeeRatio, interestRatio, profitShareRatio, loanAmountBig, currentValueRatio, timePassed + originationFeeRatio, + interestRatio, + profitShareRatio, + loanAmountBig, + currentValueRatio, + timePassed ); assertLe(cost, newCost); (, newCost) = getCost( - originationFeeRatio, interestRatio, profitShareRatio, loanAmount, currentValueRatio, timePassedBig + originationFeeRatio, + interestRatio, + profitShareRatio, + loanAmount, + currentValueRatio, + timePassedBig ); assertLe(cost, newCost); } diff --git a/test/modules/liquidator/InstantLiquidator.t.sol b/test/modules/liquidator/InstantLiquidator.t.sol index fd09d37..49cbeef 100644 --- a/test/modules/liquidator/InstantLiquidator.t.sol +++ b/test/modules/liquidator/InstantLiquidator.t.sol @@ -37,8 +37,8 @@ contract InstantLiquidatorTest is TestUtils { constructor() { ASSETS.push(Asset({standard: ERC20_STANDARD, addr: C.WETH, decimals: 18, id: 0, data: ""})); // Tests expect 0 index to be WETH - // NOTE why is USDC breaking? And why does USDC look like it is using a proxy wrapper contract...? - // ASSETS.push(Asset({standard: ERC20_STANDARD, addr: C.USDC, decimals: C.USDC_DECIMALS, id: 0, data: ""})); // Tests expect 1 index to be an ERC20} + // NOTE why is USDC breaking? And why does USDC look like it is using a proxy wrapper contract...? + // ASSETS.push(Asset({standard: ERC20_STANDARD, addr: C.USDC, decimals: C.USDC_DECIMALS, id: 0, data: ""})); // Tests expect 1 index to be an ERC20} } // invoked before each test case is run diff --git a/test/modules/oracle/UniswapV3Oracle.t.sol b/test/modules/oracle/UniswapV3Oracle.t.sol index 44ddc9c..77abdee 100644 --- a/test/modules/oracle/UniswapV3Oracle.t.sol +++ b/test/modules/oracle/UniswapV3Oracle.t.sol @@ -78,7 +78,7 @@ contract UniswapV3OracleTest is Test, Module { uint256 spotValue = oracleModule.getSpotValue(baseAmount, parameters); uint256 newAmount = oracleModule.getResistantAmount(value, parameters); - uint256 expectedAmount = baseAmount * (C.RATIO_FACTOR - params.stepSlippage) ** 2 / C.RATIO_FACTOR ** 2; + uint256 expectedAmount = (baseAmount * (C.RATIO_FACTOR - params.stepSlippage) ** 2) / C.RATIO_FACTOR ** 2; // Matching rounding here is difficult. Uni internal rounding is different that Oracle application of slippage. // Use Uni math to match rounding. Fees are round up. // uint256 expectedAmount = FullMath.mulDivRoundingUp(