├── .solcover.js ├── .eslintignore ├── audits └── Yala - Zenith Audit Report.pdf ├── .prettierignore ├── .npmignore ├── .prettierrc.js ├── contracts ├── test │ ├── MockLayerZeroEndpoint.sol │ ├── MockCollateralToken.sol │ ├── MockMetadataNFT.sol │ ├── MockPriceFeed.sol │ ├── MockDecimalCollateralToken.sol │ ├── MockAggregatorV3Interface.sol │ └── MultiTroveGetters.sol ├── core │ ├── GasPool.sol │ ├── Factory.sol │ ├── YalaCore.sol │ ├── DebtToken.sol │ ├── PSM.sol │ ├── PriceFeed.sol │ ├── BorrowerOperations.sol │ ├── StabilityPool.sol │ └── TroveManager.sol ├── interfaces │ ├── IMetadataNFT.sol │ ├── IAggregatorV3Interface.sol │ ├── IDebtToken.sol │ ├── IYalaCore.sol │ ├── IPriceFeed.sol │ ├── IPSM.sol │ ├── IFactory.sol │ ├── IStabilityPool.sol │ ├── IBorrowerOperations.sol │ └── ITroveManager.sol └── dependencies │ ├── DelegatedOps.sol │ ├── YalaOwnable.sol │ ├── YalaBase.sol │ ├── YalaMath.sol │ └── EnumerableCollateral.sol ├── .gitignore ├── test ├── Factory.test.ts ├── PriceFeed.test.ts ├── PSM.test.ts ├── utils.ts ├── fixture.ts ├── BorrowerOperations.test.ts ├── TroveManager.test.ts └── StabilityPool.test.ts ├── tsconfig.json ├── LICENSE ├── .solhint.json ├── .eslintrc ├── README.md ├── hardhat.config.ts └── package.json /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | 'dependencies/console.sol', 4 | ] 5 | }; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | export/ 2 | deployments/ 3 | artifacts/ 4 | cache/ 5 | coverage/ 6 | node_modules/ 7 | package.json 8 | types/ 9 | -------------------------------------------------------------------------------- /audits/Yala - Zenith Audit Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yalaorg/yala-protocol-contracts/HEAD/audits/Yala - Zenith Audit Report.pdf -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | export/ 2 | deployments/ 3 | artifacts/ 4 | cache/ 5 | coverage/ 6 | node_modules/ 7 | package.json 8 | typechain/ 9 | test/ 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test/ 3 | hbs/ 4 | contracts/ 5 | artifacts/ 6 | cache/ 7 | deploy/ 8 | tasks/ 9 | deployments/ 10 | .env 11 | !types/**/test 12 | !types/**/contracts 13 | !contracts/test 14 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | files: '*.sol', 5 | options: { 6 | printWidth: 300, 7 | useTabs: true, 8 | singleQuote: false, 9 | bracketSpacing: true, 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /contracts/test/MockLayerZeroEndpoint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockLayerZeroEndpoint { 8 | function setDelegate(address _delegate) external {} 9 | } 10 | -------------------------------------------------------------------------------- /contracts/test/MockCollateralToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockCollateralToken is ERC20 { 7 | constructor() ERC20("Mock Collateral Token", "MCT") { 8 | _mint(msg.sender, 1e30); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/test/MockMetadataNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "../interfaces/IMetadataNFT.sol"; 6 | 7 | contract MockMetadataNFT is IMetadataNFT { 8 | function uri(TroveData memory _troveData) external view override returns (string memory) { 9 | return ""; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/core/GasPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | /** 6 | @title Yala Gas Pool 7 | @notice Placeholder contract for tokens to be used as gas compensation 8 | See https://github.com/liquity/dev#gas-compensation 9 | */ 10 | contract GasPool { 11 | // do nothing, as the core contracts have permission to send to and burn from this address 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | cache/ 3 | artifacts/ 4 | exports/ 5 | # deploy/ 6 | deployments/localhost 7 | deployments/**/solcInputs 8 | 9 | docs/output 10 | coverage* 11 | .vscode/* 12 | !.vscode/settings.json.default 13 | !.vscode/launch.json.default 14 | !.vscode/extensions.json.default 15 | 16 | node_modules/ 17 | .env 18 | .yalc 19 | yalc.lock 20 | # test/ 21 | types/ 22 | contractsInfo.json 23 | deployments/ 24 | !contracts/test 25 | .husky 26 | -------------------------------------------------------------------------------- /contracts/test/MockPriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | contract MockPriceFeed { 7 | mapping(IERC20 => uint256) public prices; 8 | 9 | function updatePrice(IERC20 token, uint256 _amount) external { 10 | prices[token] = _amount; 11 | } 12 | 13 | function fetchPrice(IERC20 token) public view returns (uint256) { 14 | return prices[token]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contracts/interfaces/IMetadataNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | interface IMetadataNFT { 8 | struct TroveData { 9 | uint256 tokenId; 10 | address owner; 11 | IERC20 collToken; 12 | IERC20 debtToken; 13 | uint256 collAmount; 14 | uint256 debtAmount; 15 | uint256 interest; 16 | } 17 | 18 | function uri(TroveData memory _troveData) external view returns (string memory); 19 | } 20 | -------------------------------------------------------------------------------- /test/Factory.test.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-ethers' 2 | import { loadFixture, IFixture } from './fixture' 3 | import { deployNewCDP, deployNewPSM } from './utils' 4 | 5 | describe('Test Factory', () => { 6 | let fixture: IFixture 7 | 8 | beforeEach(async () => { 9 | fixture = await loadFixture() 10 | }) 11 | 12 | it('deployNewCDP', async () => { 13 | await deployNewCDP(fixture) 14 | }) 15 | 16 | it('deployNewPSM', async () => { 17 | await deployNewPSM(fixture) 18 | }) 19 | 20 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": [ 13 | "./*" 14 | ], 15 | "@dep/*": [ 16 | "deployments/*" 17 | ] 18 | }, 19 | }, 20 | "include": [ 21 | "hardhat.config.ts", 22 | "./deploy", 23 | "./test", 24 | "./tasks", 25 | "./types" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } -------------------------------------------------------------------------------- /contracts/test/MockDecimalCollateralToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockDecimalCollateralToken is ERC20 { 8 | uint8 public DECIMALS; 9 | 10 | constructor(uint8 _decimals) ERC20("Mock Collateral Token", "bfBTC") { 11 | DECIMALS = _decimals; 12 | _mint(msg.sender, 1e30); 13 | } 14 | 15 | function decimals() public view override returns (uint8) { 16 | return DECIMALS; 17 | } 18 | 19 | function burn(uint256 amount) external { 20 | _burn(msg.sender, amount); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/dependencies/DelegatedOps.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | contract DelegatedOps { 6 | event DelegateApprovalSet(address indexed caller, address indexed delegate, bool isApproved); 7 | 8 | mapping(address owner => mapping(address caller => bool isApproved)) public isApprovedDelegate; 9 | 10 | modifier callerOrDelegated(address _account) { 11 | require(msg.sender == _account || isApprovedDelegate[_account][msg.sender], "Delegate not approved"); 12 | _; 13 | } 14 | 15 | function setDelegateApproval(address _delegate, bool _isApproved) external { 16 | isApprovedDelegate[msg.sender][_delegate] = _isApproved; 17 | emit DelegateApprovalSet(msg.sender, _delegate, _isApproved); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/dependencies/YalaOwnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "../interfaces/IYalaCore.sol"; 6 | 7 | /** 8 | @title Yala Ownable 9 | @notice Contracts inheriting `YalaOwnable` have the same owner as `YalaCore`. 10 | The ownership cannot be independently modified or renounced. 11 | */ 12 | contract YalaOwnable { 13 | IYalaCore public immutable YALA_CORE; 14 | 15 | constructor(address _yalaCore) { 16 | YALA_CORE = IYalaCore(_yalaCore); 17 | } 18 | 19 | modifier onlyOwner() { 20 | require(msg.sender == YALA_CORE.owner(), "Only owner"); 21 | _; 22 | } 23 | 24 | function owner() public view returns (address) { 25 | return YALA_CORE.owner(); 26 | } 27 | 28 | function guardian() public view returns (address) { 29 | return YALA_CORE.guardian(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/dependencies/YalaBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | /* 6 | * Base contract for TroveManager, BorrowerOperations. Contains global system constants and 7 | * common functions. 8 | */ 9 | contract YalaBase { 10 | uint256 public constant DECIMAL_PRECISION = 1e18; 11 | 12 | // Amount of debt to be locked in gas pool on opening troves 13 | uint256 public immutable DEBT_GAS_COMPENSATION; 14 | 15 | constructor(uint256 _gasCompensation) { 16 | DEBT_GAS_COMPENSATION = _gasCompensation; 17 | } 18 | 19 | // --- Gas compensation functions --- 20 | 21 | // Returns the composite debt (drawn debt + gas compensation) of a trove, for the purpose of ICR calculation 22 | function _getCompositeDebt(uint256 _debt) internal view returns (uint256) { 23 | return _debt + DEBT_GAS_COMPENSATION; 24 | } 25 | 26 | function _getNetDebt(uint256 _debt) internal view returns (uint256) { 27 | return _debt - DEBT_GAS_COMPENSATION; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interfaces/IAggregatorV3Interface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Code from https://github.com/smartcontractkit/chainlink/blob/master/evm-contracts/src/v0.6/interfaces/AggregatorV3Interface.sol 3 | 4 | pragma solidity 0.8.28; 5 | 6 | interface IAggregatorV3Interface { 7 | function decimals() external view returns (uint8); 8 | 9 | function description() external view returns (string memory); 10 | 11 | function version() external view returns (uint256); 12 | 13 | // getRoundData and latestRoundData should both raise "No data present" 14 | // if they do not have data to report, instead of returning unset values 15 | // which could be misinterpreted as actual reported values. 16 | function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); 17 | 18 | function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": [ 4 | "prettier" 5 | ], 6 | "rules": { 7 | "prettier/prettier": [ 8 | "error", 9 | { 10 | "endOfLine": "auto" 11 | } 12 | ], 13 | "custom-errors": "off", 14 | "immutable-vars-naming": "off", 15 | "quotes": [ 16 | "error", 17 | "double" 18 | ], 19 | "code-complexity": [ 20 | "error", 21 | 60 22 | ], 23 | "compiler-version": [ 24 | "error", 25 | ">=0.8.0" 26 | ], 27 | "const-name-snakecase": "off", 28 | "func-name-mixedcase": "off", 29 | "constructor-syntax": "error", 30 | "no-global-import": "off", 31 | "func-visibility": [ 32 | "error", 33 | { 34 | "ignoreConstructors": true 35 | } 36 | ], 37 | "check-send-result": "off", 38 | "no-empty-blocks": "off", 39 | "var-name-mixedcase": "off", 40 | "not-rely-on-time": "off", 41 | "not-rely-on-block-hash": "off", 42 | "no-inline-assembly": "off", 43 | "avoid-low-level-calls": "off", 44 | "reentrancy": "off", 45 | "max-states-count": 30, 46 | "reason-string": [ 47 | "warn", 48 | { 49 | "maxLength": 100 50 | } 51 | ] 52 | } 53 | } -------------------------------------------------------------------------------- /contracts/interfaces/IDebtToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "./ITroveManager.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | interface IDebtToken is IERC20 { 9 | function burn(address _account, uint256 _amount) external; 10 | function burnWithGasCompensation(address _account, uint256 _amount) external returns (bool); 11 | 12 | function enableTroveManager(address _troveManager) external; 13 | 14 | function enablePSM(address _psm) external; 15 | 16 | function mint(address _account, uint256 _amount) external; 17 | 18 | function mintWithGasCompensation(address _account, uint256 _amount) external returns (bool); 19 | 20 | function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external; 21 | 22 | function sendToSP(address _sender, uint256 _amount) external; 23 | 24 | function DEBT_GAS_COMPENSATION() external view returns (uint256); 25 | 26 | function borrowerOperationsAddress() external view returns (address); 27 | 28 | function factory() external view returns (address); 29 | 30 | function gasPool() external view returns (address); 31 | 32 | function troveManager(address) external view returns (bool); 33 | } 34 | -------------------------------------------------------------------------------- /contracts/dependencies/YalaMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | library YalaMath { 6 | uint256 internal constant DECIMAL_PRECISION = 1e18; 7 | 8 | function _min(uint256 _a, uint256 _b) internal pure returns (uint256) { 9 | return (_a < _b) ? _a : _b; 10 | } 11 | 12 | function _max(uint256 _a, uint256 _b) internal pure returns (uint256) { 13 | return (_a >= _b) ? _a : _b; 14 | } 15 | 16 | function _computeCR(uint256 _coll, uint256 _debt, uint256 _price) internal pure returns (uint256) { 17 | if (_debt > 0) { 18 | uint256 newCollRatio = (_coll * _price) / _debt; 19 | 20 | return newCollRatio; 21 | } 22 | // Return the maximal value for uint256 if the Trove has a debt of 0. Represents "infinite" CR. 23 | else { 24 | // if (_debt == 0) 25 | return 2 ** 256 - 1; 26 | } 27 | } 28 | 29 | function _computeCR(uint256 _coll, uint256 _debt) internal pure returns (uint256) { 30 | if (_debt > 0) { 31 | uint256 newCollRatio = (_coll) / _debt; 32 | 33 | return newCollRatio; 34 | } 35 | // Return the maximal value for uint256 if the Trove has a debt of 0. Represents "infinite" CR. 36 | else { 37 | // if (_debt == 0) 38 | return 2 ** 256 - 1; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/interfaces/IYalaCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IYalaCore { 6 | event NewOwnerCommitted(address owner, address pendingOwner, uint256 deadline); 7 | 8 | event NewOwnerAccepted(address oldOwner, address owner); 9 | 10 | event NewOwnerRevoked(address owner, address revokedOwner); 11 | 12 | event FeeReceiverSet(address feeReceiver); 13 | 14 | event GuardianSet(address guardian); 15 | 16 | event Paused(); 17 | 18 | event Unpaused(); 19 | 20 | function acceptTransferOwnership() external; 21 | 22 | function commitTransferOwnership(address newOwner) external; 23 | 24 | function revokeTransferOwnership() external; 25 | 26 | function setFeeReceiver(address _feeReceiver) external; 27 | 28 | function setGuardian(address _guardian) external; 29 | 30 | function setPaused(bool _paused) external; 31 | 32 | function OWNERSHIP_TRANSFER_DELAY() external view returns (uint256); 33 | 34 | function feeReceiver() external view returns (address); 35 | 36 | function guardian() external view returns (address); 37 | 38 | function owner() external view returns (address); 39 | 40 | function ownershipTransferDeadline() external view returns (uint256); 41 | 42 | function paused() external view returns (bool); 43 | 44 | function pendingOwner() external view returns (address); 45 | } 46 | -------------------------------------------------------------------------------- /contracts/interfaces/IPriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IPriceFeed { 6 | event NewOracleRegistered(address token, address chainlinkAggregator, bool isEthIndexed); 7 | event PriceFeedStatusUpdated(address token, address oracle, bool isWorking); 8 | event PriceRecordUpdated(address indexed token, uint256 _price); 9 | 10 | function fetchPrice(address _token) external returns (uint256); 11 | 12 | function setOracle(address _token, address _chainlinkOracle, bytes4 sharePriceSignature, uint8 sharePriceDecimals, bool _isEthIndexed) external; 13 | 14 | function MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND() external view returns (uint256); 15 | 16 | function YALA_CORE() external view returns (address); 17 | 18 | function RESPONSE_TIMEOUT() external view returns (uint256); 19 | 20 | function TARGET_DIGITS() external view returns (uint256); 21 | 22 | function guardian() external view returns (address); 23 | 24 | function oracleRecords(address) external view returns (address chainLinkOracle, uint8 decimals, bytes4 sharePriceSignature, uint8 sharePriceDecimals, bool isFeedWorking, bool isEthIndexed); 25 | 26 | function owner() external view returns (address); 27 | 28 | function priceRecords(address) external view returns (uint96 scaledPrice, uint32 timestamp, uint32 lastUpdated, uint80 roundId); 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2020, 6 | "sourceType": "module" 7 | }, 8 | "env": { 9 | "commonjs": true 10 | }, 11 | "plugins": ["@typescript-eslint"], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "prettier" 16 | ], 17 | "ignorePatterns": ["**/node_modules/**"], 18 | "rules": { 19 | "no-empty": "off", 20 | "no-empty-function": "warn", 21 | "@typescript-eslint/no-unused-vars": "off", 22 | "@typescript-eslint/no-require-imports": "off", 23 | "@typescript-eslint/no-empty-function": "off", 24 | "@typescript-eslint/explicit-function-return-type": "off", 25 | "@typescript-eslint/explicit-module-boundary-types": "off", 26 | "@typescript-eslint/no-explicit-any": "off", 27 | "@typescript-eslint/no-namespace": "off", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/no-empty-interface": "off", 30 | "@typescript-eslint/explicit-member-accessibility": [ 31 | "error", 32 | { 33 | "accessibility": "explicit", 34 | "overrides": { 35 | "accessors": "explicit", 36 | "constructors": "no-public", 37 | "methods": "explicit", 38 | "properties": "explicit", 39 | "parameterProperties": "explicit" 40 | } 41 | } 42 | ], 43 | "no-tabs": "off", 44 | "vue/comment-directive": "off", 45 | "no-prototype-builtins": "off", 46 | "no-ex-assign": "off", 47 | "no-console": "off", 48 | "semi": ["error", "never"], 49 | "quotes": ["error", "single"], 50 | "indent": [ 51 | "error", 52 | "tab", 53 | { 54 | "SwitchCase": 1 55 | } 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /contracts/interfaces/IPSM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 6 | import "./IDebtToken.sol"; 7 | 8 | interface IPSM { 9 | event PegTokenUpdated(IERC20Metadata pegToken); 10 | event FeeInUpdated(uint256 feeIn); 11 | event FeeOutUpdated(uint256 feeOut); 12 | event Buy(address from, uint256 amountDebtToken, uint256 amountPegToken, uint256 fee); 13 | event Sell(address from, uint256 amountDebtToken, uint256 amountPegToken, uint256 fee); 14 | event SupplyCapUpdated(uint256 newCap); 15 | event DebtCeilingUpdated(uint256 newCeiling); 16 | 17 | function factory() external view returns (address); 18 | function pegToken() external view returns (IERC20Metadata); 19 | function debtToken() external view returns (IDebtToken); 20 | function feeIn() external view returns (uint256); 21 | function feeOut() external view returns (uint256); 22 | function priceFactor() external view returns (uint256); 23 | function initialize(IERC20Metadata _pegToken, uint256 _feeIn, uint256 _feeOut, uint256 _supplyCap) external; // only called by owner 24 | function setFeeIn(uint256 _feeIn) external; 25 | function setFeeOut(uint256 _feeOut) external; 26 | 27 | function buy(uint256 amountPegToken) external returns (uint256 amountDebtTokenUsed, uint256 fee); 28 | function sell(uint256 amountDebtToken) external returns (uint256 amountPegTokenReceived, uint256 fee); 29 | function estimateBuy(uint256 amountDebtToken) external view returns (uint256 amountPegTokenUsed, uint256 fee); 30 | function estimateSell(uint256 amountDebtToken) external returns (uint256 amountPegTokenReceived, uint256 fee); 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![License: GPL](https://img.shields.io/badge/license-GPLv3-blue)![Version Badge](https://img.shields.io/badge/version-0.0.1-lightgrey.svg) 2 | 3 | # Bitcoin liquidity layer protocol contracts 4 | 5 | Yala is a Bitcoin-native liquidity protocol that enables Bitcoin holders to earn real yield from DeFi and RWAs without giving up ownership of their assets. The repository contains protocol smart contracts with LayerZero cross-chain infrastructure and Chainlink oracle integration. 6 | Core mechanisms include collateral management, stablity pool and PSM, establishing a liquidity infrastructure layer for the Bitcoin ecosystem. 7 | 8 | ## Install 9 | 10 | `npm i -f` 11 | 12 | ## Get start 13 | 14 | 1. create `.env` file and set your environment variables as below 15 | 16 | ``` 17 | MNEMONIC="yourn mnemonic" 18 | MAINNET="mainnet rpc" 19 | SEPOLIA="seplolia rpc" 20 | APIKEY_MAINNET="etherscan mainnet api key" 21 | APIKEY_SEPOLIA="etherscan sepoliad api key" 22 | OWENR="protocol admin address" ## required on local development 23 | GUARDIAN="protocol guardian address" ## required on local development 24 | FEE_RECEIVER="protocol fee receiver address" ## required on local development 25 | LAYER_ZERO_ENDPOINT="layer zero endpoint" 26 | LAYER_ZERO_DELEGATE="layer zero delegate" ## required on local development 27 | COLLATERAL_TOEKN="collateral token address" 28 | CHAINLINK_AGGREGATORV3="chainlink aggregator v3 address" 29 | ``` 30 | 31 | 2. compile contracts, it will generate contract artifacts also typechains 32 | 33 | `yarn build` 34 | 35 | 3. test contracts 36 | 37 | `yarn test` 38 | 39 | --- 40 | 41 | Once the rpc url is unavailable, check it [here](https://chainlist.org/) 42 | -------------------------------------------------------------------------------- /contracts/interfaces/IFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | interface IFactory { 6 | // commented values are suggested default parameters 7 | struct DeploymentParams { 8 | uint256 minuteDecayFactor; // 999037758833783000 (half life of 12 hours) 9 | uint256 redemptionFeeFloor; // 1e18 / 1000 * 5 (0.5%) 10 | uint256 maxRedemptionFee; // 1e18 (100%) 11 | uint256 borrowingFeeFloor; // 1e18 / 1000 * 5 (0.5%) 12 | uint256 maxBorrowingFee; // 1e18 / 100 * 5 (5%) 13 | uint256 interestRateInBps; // 100 (1%) 14 | uint256 maxDebt; 15 | uint256 MCR; // 12 * 1e17 (120%) 16 | } 17 | 18 | event NewDeployment(address collateral, address priceFeed, address troveManager, address sortedTroves); 19 | 20 | function deployNewInstance(address collateral, address priceFeed, address customTroveManagerImpl, address customSortedTrovesImpl, DeploymentParams calldata params) external; 21 | 22 | function setImplementations(address _troveManagerImpl, address _sortedTrovesImpl) external; 23 | 24 | function YALA_CORE() external view returns (address); 25 | 26 | function borrowerOperations() external view returns (address); 27 | 28 | function debtToken() external view returns (address); 29 | 30 | function guardian() external view returns (address); 31 | 32 | function liquidationManager() external view returns (address); 33 | 34 | function owner() external view returns (address); 35 | 36 | function sortedTrovesImpl() external view returns (address); 37 | 38 | function stabilityPool() external view returns (address); 39 | 40 | function troveManagerCount() external view returns (uint256); 41 | 42 | function troveManagerImpl() external view returns (address); 43 | 44 | function troveManagers(uint256) external view returns (address); 45 | } 46 | -------------------------------------------------------------------------------- /contracts/test/MockAggregatorV3Interface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "../interfaces/IAggregatorV3Interface.sol"; 7 | 8 | contract MockAggregatorV3Interface is IAggregatorV3Interface, Ownable { 9 | struct RoundData { 10 | uint80 roundId; 11 | int256 answer; 12 | uint256 startedAt; 13 | uint256 updatedAt; 14 | uint80 answeredInRound; 15 | } 16 | 17 | uint8 public DECIMALS = 8; 18 | RoundData internal latestData; 19 | mapping(uint80 => RoundData) public datas; 20 | 21 | constructor(address newOwner) { 22 | _transferOwnership(newOwner); 23 | } 24 | 25 | function updateDecimals(uint8 _decimals) external onlyOwner { 26 | DECIMALS = _decimals; 27 | } 28 | 29 | function updateRoundData(RoundData memory roundData) external onlyOwner { 30 | latestData = roundData; 31 | datas[roundData.roundId] = roundData; 32 | } 33 | 34 | function decimals() external view returns (uint8) { 35 | return DECIMALS; 36 | } 37 | 38 | function description() external pure returns (string memory) { 39 | return "Mock AggregatorV3Interface"; 40 | } 41 | 42 | function version() external pure returns (uint256) { 43 | return 1; 44 | } 45 | 46 | function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { 47 | return (datas[_roundId].roundId, datas[_roundId].answer, datas[_roundId].startedAt, datas[_roundId].updatedAt, datas[_roundId].answeredInRound); 48 | } 49 | 50 | function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { 51 | return (latestData.roundId, latestData.answer, latestData.startedAt, latestData.updatedAt, latestData.answeredInRound); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/PriceFeed.test.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-ethers' 2 | import { loadFixture, IFixture } from './fixture' 3 | import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers' 4 | import { expect } from 'chai' 5 | import { updateMockAggregatorV3Price } from './utils' 6 | import { parseUnits } from 'ethers' 7 | import { ethers } from 'hardhat' 8 | 9 | describe('Test PriceFeed', () => { 10 | let fixture: IFixture 11 | 12 | beforeEach(async () => { 13 | fixture = await loadFixture() 14 | }) 15 | 16 | it('fetchPrice', async () => { 17 | const { PriceFeed, MockCollateralToken, MockAggregatorV3Interface } = fixture 18 | await PriceFeed.fetchPrice.staticCall(MockCollateralToken.target) 19 | await time.increase(7200) 20 | // feed frozen 21 | await expect(PriceFeed.fetchPrice(MockCollateralToken.target)).to.be.reverted 22 | const newPrice = parseUnits('90000', 8) 23 | await updateMockAggregatorV3Price(MockAggregatorV3Interface, 101, newPrice) 24 | expect(await PriceFeed.fetchPrice.staticCall(MockCollateralToken.target)).to.be.eq(newPrice * (10n ** 10n)) 25 | }) 26 | 27 | it('set new oracle', async () => { 28 | const { signer, PriceFeed } = fixture 29 | const newCollateraToken = await ethers.deployContract('MockCollateralToken', signer) 30 | const newMockAggregatorV3Interface = await ethers.deployContract('MockAggregatorV3Interface', [signer.address], signer) 31 | await expect(PriceFeed.setOracle(newCollateraToken.target, newMockAggregatorV3Interface.target, 3600)).to.be.reverted 32 | const price = parseUnits('10000', 8) 33 | await updateMockAggregatorV3Price(newMockAggregatorV3Interface, 100, price) 34 | await PriceFeed.setOracle(newCollateraToken.target, newMockAggregatorV3Interface.target, 3600) 35 | expect(await PriceFeed.fetchPrice.staticCall(newCollateraToken.target)).to.be.eq(price * (10n ** 10n)) 36 | 37 | }) 38 | 39 | }) -------------------------------------------------------------------------------- /contracts/dependencies/EnumerableCollateral.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 6 | 7 | library EnumerableCollateral { 8 | using EnumerableSet for EnumerableSet.AddressSet; 9 | error EnumerableMapNonexistentKey(address key); 10 | 11 | struct TroveManagerToCollateral { 12 | EnumerableSet.AddressSet _keys; 13 | mapping(address => IERC20) _values; 14 | } 15 | 16 | function set(TroveManagerToCollateral storage map, address key, IERC20 value) internal returns (bool) { 17 | map._values[key] = value; 18 | return map._keys.add(address(key)); 19 | } 20 | 21 | function remove(TroveManagerToCollateral storage map, address key) internal returns (bool) { 22 | delete map._values[key]; 23 | return map._keys.remove(key); 24 | } 25 | 26 | function contains(TroveManagerToCollateral storage map, address key) internal view returns (bool) { 27 | return map._keys.contains(key); 28 | } 29 | 30 | function length(TroveManagerToCollateral storage map) internal view returns (uint256) { 31 | return map._keys.length(); 32 | } 33 | 34 | function at(TroveManagerToCollateral storage map, uint256 index) internal view returns (address, IERC20) { 35 | address key = map._keys.at(index); 36 | return (key, map._values[key]); 37 | } 38 | 39 | function tryGet(TroveManagerToCollateral storage map, address key) internal view returns (bool exists, IERC20 value) { 40 | value = map._values[key]; 41 | exists = address(value) != address(0); 42 | } 43 | 44 | function get(TroveManagerToCollateral storage map, address key) internal view returns (IERC20) { 45 | if (!contains(map, key)) { 46 | revert EnumerableMapNonexistentKey(key); 47 | } 48 | return map._values[key]; 49 | } 50 | 51 | function keys(TroveManagerToCollateral storage map) internal view returns (address[] memory) { 52 | return map._keys.values(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contracts/interfaces/IStabilityPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | interface IStabilityPool { 8 | struct AccountDeposit { 9 | uint128 amount; 10 | uint128 timestamp; // timestamp of the last deposit 11 | } 12 | 13 | struct Snapshots { 14 | uint256 P; 15 | uint256 G; 16 | uint128 scale; 17 | uint128 epoch; 18 | } 19 | 20 | event StabilityPoolDebtBalanceUpdated(uint256 _newBalance); 21 | event P_Updated(uint256 _P); 22 | event S_Updated(IERC20 collateral, uint256 _S, uint128 _epoch, uint128 _scale); 23 | event G_Updated(uint256 _G, uint128 _epoch, uint128 _scale); 24 | event EpochUpdated(uint128 _currentEpoch); 25 | event ScaleUpdated(uint128 _currentScale); 26 | 27 | event DepositSnapshotUpdated(address indexed _depositor, uint256 _P, uint256 _G); 28 | event Deposit(address indexed _depositor, uint256 _newDeposit, uint256 amount); 29 | event Withdraw(address indexed _depositor, uint256 _newDeposit, uint256 amount); 30 | 31 | event TriggerYiedRewards(address troveManager, uint256 amount); 32 | 33 | event CollateralGainWithdrawn(address indexed _depositor, IERC20 collateral, uint256 gains); 34 | event YieldClaimed(address indexed account, address indexed recipient, uint256 claimed); 35 | function enableTroveManager(address troveManager) external; 36 | function getTotalDeposits() external view returns (uint256); 37 | function offset(uint256 _debtToOffset, uint256 _collToAdd) external; 38 | function triggerSPYield(uint256 _yield) external; 39 | function provideToSP(uint256 _amount) external; 40 | function withdrawFromSP(uint256 _amount) external; 41 | function getCompoundedDebtDeposit(address _depositor) external view returns (uint256); 42 | function claimYield(address recipient) external returns (uint256 amount); 43 | function claimAllCollateralGains(address recipient) external; 44 | function getYieldGains(address _depositor) external view returns (uint256); 45 | function getDepositorCollateralGain(address _depositor) external view returns (IERC20[] memory collaterals, uint256[] memory collateralGains); 46 | } 47 | -------------------------------------------------------------------------------- /contracts/interfaces/IBorrowerOperations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "./ITroveManager.sol"; 6 | 7 | interface IBorrowerOperations { 8 | struct LocalVariables_openTrove { 9 | IERC20 collateralToken; 10 | uint256 price; 11 | uint256 totalCollateral; 12 | uint256 totalDebt; 13 | uint256 totalInterest; 14 | uint256 netDebt; 15 | uint256 compositeDebt; 16 | uint256 MCR; 17 | uint256 CCR; 18 | uint256 ICR; 19 | } 20 | 21 | struct LocalVariables_adjustTrove { 22 | IERC20 collateralToken; 23 | uint256 price; 24 | uint256 totalCollateral; 25 | uint256 totalDebt; 26 | uint256 totalInterest; 27 | uint256 collChange; 28 | bool isCollIncrease; 29 | uint256 debt; 30 | uint256 coll; 31 | uint256 interest; 32 | uint256 newDebt; 33 | uint256 newColl; 34 | uint256 stake; 35 | uint256 debtChange; 36 | uint256 interestRepayment; 37 | address account; 38 | uint256 MCR; 39 | uint256 CCR; 40 | bool isBelowCriticalThreshold; 41 | } 42 | 43 | struct LocalVariables_closeTrove { 44 | ITroveManager troveManager; 45 | IERC20 collateralToken; 46 | address account; 47 | uint256 totalCollateral; 48 | uint256 totalDebt; 49 | uint256 totalInterest; 50 | uint256 compositeDebt; 51 | uint256 price; 52 | uint256 CCR; 53 | } 54 | 55 | enum BorrowerOperation { 56 | openTrove, 57 | closeTrove, 58 | adjustTrove 59 | } 60 | 61 | event MinNetDebtUpdated(uint256 minNetDebt); 62 | event CollateralConfigured(ITroveManager troveManager, IERC20 collateralToken); 63 | event TroveManagerRemoved(ITroveManager troveManager); 64 | event TroveCreated(address borrower, ITroveManager troveManager, uint256 id, uint256 _collateralAmount, uint256 _debtAmount); 65 | event AdjustTrove(address borrower, ITroveManager troveManager, uint256 id, uint256 _collDeposit, uint256 _collWithdrawal, uint256 _debtChange, bool _isDebtIncrease); 66 | event CloseTrove(address borrower, ITroveManager troveManager, uint256 id, address receiver, uint256 coll, uint256 debt, uint256 interest); 67 | 68 | function minNetDebt() external view returns (uint256); 69 | function collateraTokens(ITroveManager troveManager) external view returns (IERC20); 70 | function repay(ITroveManager troveManager, uint256 id, uint256 _debtAmount) external; 71 | function configureCollateral(ITroveManager troveManager, IERC20 collateralToken) external; 72 | } 73 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-chai-matchers' 2 | import '@nomicfoundation/hardhat-ethers' 3 | import '@nomicfoundation/hardhat-verify' 4 | import '@typechain/hardhat' 5 | 6 | import 'hardhat-deploy' 7 | import 'hardhat-gas-reporter' 8 | import 'solidity-coverage' 9 | import 'hardhat-storage-layout' 10 | import 'solidity-docgen' 11 | 12 | import { config as dotenvConfig } from 'dotenv' 13 | import { resolve } from 'path' 14 | 15 | const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || './.env' 16 | dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) }) 17 | 18 | if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'production') { 19 | require('./tasks') 20 | } 21 | 22 | const accounts = { 23 | mnemonic: process.env.MNEMONIC || 'test test test test test test test test test test test test', 24 | } 25 | 26 | const config = { 27 | solidity: { 28 | overrides: {}, 29 | compilers: [ 30 | { 31 | version: '0.8.28', 32 | settings: { 33 | optimizer: { enabled: true, runs: 100 }, 34 | }, 35 | } 36 | ], 37 | }, 38 | namedAccounts: { 39 | deployer: 0, 40 | simpleERC20Beneficiary: 1 41 | }, 42 | networks: { 43 | sepolia: { 44 | url: process.env.SEPOLIA ?? '', 45 | accounts, 46 | gas: 'auto', 47 | gasPrice: 'auto', 48 | gasMultiplier: 1.3, 49 | timeout: 100000 50 | }, 51 | 'monad-testnet': { 52 | url: process.env.MONAD_TESTNET_RPC ?? '', 53 | accounts, 54 | gas: 'auto', 55 | gasPrice: 'auto', 56 | gasMultiplier: 1.3, 57 | timeout: 100000 58 | }, 59 | mainnet: { 60 | url: process.env.MAINNET ?? '', 61 | accounts, 62 | gas: 'auto', 63 | gasPrice: 'auto', 64 | gasMultiplier: 1.3, 65 | timeout: 100000 66 | }, 67 | localhost: { 68 | url: 'http://127.0.0.1:8545', 69 | accounts, 70 | gas: 'auto', 71 | gasPrice: 'auto', 72 | gasMultiplier: 1.5, 73 | timeout: 100000 74 | }, 75 | hardhat: { 76 | // forking: { 77 | // enabled: true, 78 | // url: process.env.MAINNET, 79 | // blockNumber: Number(process.env.FORK_BLOCK), 80 | // }, 81 | accounts, 82 | gas: 'auto', 83 | gasPrice: 'auto', 84 | gasMultiplier: 2, 85 | chainId: 1337, 86 | mining: { 87 | auto: true, 88 | interval: 5000 89 | } 90 | } 91 | }, 92 | etherscan: { 93 | apiKey: { 94 | mainnet: process.env.APIKEY_MAINNET!, 95 | sepolia: process.env.APIKEY_SEPOLIA! 96 | } 97 | }, 98 | paths: { 99 | deploy: 'deploy', 100 | artifacts: 'artifacts', 101 | cache: 'cache', 102 | sources: 'contracts', 103 | tests: 'test' 104 | }, 105 | gasReporter: { 106 | currency: 'USD', 107 | gasPrice: 100, 108 | enabled: process.env.REPORT_GAS ? true : false, 109 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 110 | maxMethodDiff: 10, 111 | }, 112 | docgen: { 113 | templates: './docs/templates', 114 | exclude: ['dependencies', 'test'], 115 | root: './', 116 | sourcesDir: './contracts', 117 | pages: 'files', 118 | outputDir: './docs/output' 119 | }, 120 | typechain: { 121 | outDir: 'types', 122 | target: 'ethers-v6', 123 | }, 124 | mocha: { 125 | timeout: 0, 126 | } 127 | } 128 | 129 | export default config 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yahub/yala-protocol-contracts", 3 | "version": "0.1.0", 4 | "description": "yala protocol contracts for typescript", 5 | "main": "./types/index.js", 6 | "types": "./types/index.ts", 7 | "engines": { 8 | "node": ">= 20.9.0" 9 | }, 10 | "author": "yala", 11 | "license": "MIT", 12 | "keywords": [ 13 | "ethereum", 14 | "smart-contracts", 15 | "hardhat", 16 | "solidity" 17 | ], 18 | "devDependencies": { 19 | "@layerzerolabs/lz-evm-protocol-v2": "^2.0.6", 20 | "@layerzerolabs/lz-v2-utilities": "^3.0.74", 21 | "@layerzerolabs/oapp-evm": "^0.3.0", 22 | "@layerzerolabs/oft-evm": "^3.1.0", 23 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.1", 24 | "@nomicfoundation/hardhat-ethers": "^3.0.2", 25 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 26 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 27 | "@nomicfoundation/hardhat-verify": "^2.0.12", 28 | "@openzeppelin/contracts": "^4.8.3", 29 | "@openzeppelin/contracts-upgradeable": "^4.8.3", 30 | "@typechain/ethers-v6": "^0.5.1", 31 | "@typechain/hardhat": "^9.1.0", 32 | "@types/chai": "^4.2.18", 33 | "@types/dotenv": "^8.2.0", 34 | "@types/mocha": "^10.0.1", 35 | "@types/node": "^20.9.0", 36 | "@typescript-eslint/eslint-plugin": "^8.16.0", 37 | "@typescript-eslint/parser": "^8.16.0", 38 | "@typescript-eslint/typescript-estree": "^8.16.0", 39 | "chai": "^4.2.0", 40 | "cross-env": "^7.0.3", 41 | "dotenv": "^16.0.3", 42 | "eslint": "^8.37.0", 43 | "eslint-config-prettier": "^8.8.0", 44 | "ethers": "^6.6.0", 45 | "hardhat": "^2.22.5", 46 | "hardhat-deploy": "^0.12.2", 47 | "hardhat-ethers": "^1.0.1", 48 | "hardhat-gas-reporter": "^2.0.0", 49 | "hardhat-storage-layout": "^0.1.7", 50 | "husky": "^9.1.7", 51 | "minimatch": "^5.1.2", 52 | "mocha": "^10.2.0", 53 | "prettier": "^3.4.1", 54 | "prettier-plugin-solidity": "^1.4.1", 55 | "solc-0.8.28": "npm:solc@0.8.28", 56 | "solhint": "^4.5.4", 57 | "solhint-plugin-prettier": "^0.1.0", 58 | "solidity-coverage": "^0.8.12", 59 | "solidity-docgen": "^0.6.0-beta.36", 60 | "ts-node": "^10.9.1", 61 | "typechain": "^8.3.2", 62 | "typescript": "^5.1.6" 63 | }, 64 | "scripts": { 65 | "doc": "hardhat docgen", 66 | "build:types": "cross-env NODE_ENV=build hardhat typechain && tsc ./types/**.ts --module NodeNext --moduleResolution NodeNext --target ESNext", 67 | "clean": "hardhat clean", 68 | "build": "cross-env NODE_ENV=build hardhat compile && yarn build:types", 69 | "console": "hardhat console", 70 | "coverage": "cross-env HARDHAT_DEPLOY_FIXTURE=true NODE_ENV=dev hardhat coverage", 71 | "gas": "yarn clean && yarn build && cross-env REPORT_GAS=true hardhat test", 72 | "test": "yarn clean && yarn build && hardhat test", 73 | "sepolia": "yarn build && cross-env NODE_ENV=dev hardhat --network sepolia", 74 | "monad-testnet": "yarn build && cross-env NODE_ENV=dev hardhat --network monad-testnet", 75 | "mainnet": "yarn build && cross-env NODE_ENV=production hardhat --network mainnet", 76 | "dev": "yarn build && cross-env NODE_ENV=dev hardhat node --tags dev", 77 | "lint": "prettier --list-different --plugin=prettier-plugin-solidity 'contracts/**/*.sol'", 78 | "lint:fix": "prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol'", 79 | "prepare": "husky" 80 | } 81 | } -------------------------------------------------------------------------------- /contracts/core/Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/proxy/Clones.sol"; 6 | import "../dependencies/YalaOwnable.sol"; 7 | import "../interfaces/ITroveManager.sol"; 8 | import "../interfaces/IBorrowerOperations.sol"; 9 | import "../interfaces/IDebtToken.sol"; 10 | import "../interfaces/IStabilityPool.sol"; 11 | import "../interfaces/IPSM.sol"; 12 | 13 | contract Factory is YalaOwnable { 14 | using Clones for address; 15 | 16 | IDebtToken public immutable debtToken; 17 | IStabilityPool public immutable stabilityPool; 18 | IBorrowerOperations public immutable borrowerOperations; 19 | 20 | address public troveManagerImpl; 21 | address public psmImpl; 22 | 23 | ITroveManager[] public troveManagers; 24 | address[] public psms; 25 | 26 | event NewCDPDeployment(IERC20 collateral, IPriceFeed priceFeed, ITroveManager troveManager); 27 | event NewPSMDeployment(address psm, IERC20Metadata pegToken, uint256 feeIn, uint256 feeOut, uint256 supplyCap); 28 | 29 | constructor(address _yalaCore, IDebtToken _debtToken, IStabilityPool _stabilityPool, IBorrowerOperations _borrowerOperations, address _troveManager, address _psm) YalaOwnable(_yalaCore) { 30 | debtToken = _debtToken; 31 | stabilityPool = _stabilityPool; 32 | borrowerOperations = _borrowerOperations; 33 | troveManagerImpl = _troveManager; 34 | psmImpl = _psm; 35 | } 36 | 37 | function psmCount() external view returns (uint256) { 38 | return psms.length; 39 | } 40 | 41 | function troveManagerCount() external view returns (uint256) { 42 | return troveManagers.length; 43 | } 44 | 45 | function deployNewCDP(IERC20 collateral, bytes32 salt, IPriceFeed priceFeed, address customTroveManagerImpl, ITroveManager.DeploymentParams memory params) external onlyOwner returns (ITroveManager troveManager) { 46 | address implementation = customTroveManagerImpl == address(0) ? troveManagerImpl : customTroveManagerImpl; 47 | troveManager = ITroveManager(implementation.cloneDeterministic(keccak256(abi.encodePacked(address(collateral), salt)))); 48 | troveManagers.push(troveManager); 49 | troveManager.setParameters(priceFeed, collateral, params); 50 | // verify that the oracle is correctly working 51 | troveManager.fetchPrice(); 52 | 53 | stabilityPool.enableTroveManager(address(troveManager)); 54 | debtToken.enableTroveManager(address(troveManager)); 55 | borrowerOperations.configureCollateral(troveManager, collateral); 56 | 57 | emit NewCDPDeployment(collateral, priceFeed, troveManager); 58 | } 59 | 60 | function deployNewPSM(address customPSMImpl, IERC20Metadata pegToken, uint256 feeIn, uint256 feeOut, uint256 supplyCap) external onlyOwner returns (address psm) { 61 | address implementation = customPSMImpl == address(0) ? psmImpl : customPSMImpl; 62 | psm = implementation.cloneDeterministic(bytes32(bytes20(address(pegToken)))); 63 | IPSM(psm).initialize(pegToken, feeIn, feeOut, supplyCap); 64 | debtToken.enablePSM(psm); 65 | psms.push(psm); 66 | emit NewPSMDeployment(psm, pegToken, feeIn, feeOut, supplyCap); 67 | } 68 | 69 | function setTroveMangerImpl(address _troveManagerImpl) external onlyOwner { 70 | troveManagerImpl = _troveManagerImpl; 71 | } 72 | 73 | function setPSMImpl(address _psmImpl) external onlyOwner { 74 | psmImpl = _psmImpl; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /contracts/core/YalaCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "../interfaces/IYalaCore.sol"; 6 | 7 | contract YalaCore is IYalaCore { 8 | address public feeReceiver; 9 | address public priceFeed; 10 | 11 | address public owner; 12 | address public pendingOwner; 13 | uint256 public ownershipTransferDeadline; 14 | 15 | address public guardian; 16 | 17 | // We enforce a three day delay between committing and applying 18 | // an ownership change, as a sanity check on a proposed new owner 19 | // and to give users time to react in case the act is malicious. 20 | uint256 public constant OWNERSHIP_TRANSFER_DELAY = 86400 * 3; 21 | 22 | // System-wide pause. When true, disables trove adjustments across all collaterals. 23 | bool public paused; 24 | 25 | constructor(address _owner, address _guardian, address _feeReceiver) { 26 | owner = _owner; 27 | guardian = _guardian; 28 | feeReceiver = _feeReceiver; 29 | emit GuardianSet(_guardian); 30 | emit FeeReceiverSet(_feeReceiver); 31 | } 32 | 33 | modifier onlyOwner() { 34 | require(msg.sender == owner, "Only owner"); 35 | _; 36 | } 37 | 38 | /** 39 | * @notice Set the receiver of all fees across the protocol 40 | * @param _feeReceiver Address of the fee's recipient 41 | */ 42 | function setFeeReceiver(address _feeReceiver) external onlyOwner { 43 | feeReceiver = _feeReceiver; 44 | emit FeeReceiverSet(_feeReceiver); 45 | } 46 | 47 | /** 48 | * @notice Set the guardian address 49 | The guardian can execute some emergency actions 50 | * @param _guardian Guardian address 51 | */ 52 | function setGuardian(address _guardian) external onlyOwner { 53 | guardian = _guardian; 54 | emit GuardianSet(_guardian); 55 | } 56 | 57 | /** 58 | * @notice Sets the global pause state of the protocol 59 | * Pausing is used to mitigate risks in exceptional circumstances 60 | * Functionalities affected by pausing are: 61 | * - New borrowing is not possible 62 | * - New collateral deposits are not possible 63 | * - New stability pool deposits are not possible 64 | * @param _paused If true the protocol is paused 65 | */ 66 | function setPaused(bool _paused) external { 67 | require((_paused && msg.sender == guardian) || msg.sender == owner, "Unauthorized"); 68 | paused = _paused; 69 | if (_paused) { 70 | emit Paused(); 71 | } else { 72 | emit Unpaused(); 73 | } 74 | } 75 | 76 | function commitTransferOwnership(address newOwner) external onlyOwner { 77 | pendingOwner = newOwner; 78 | ownershipTransferDeadline = block.timestamp + OWNERSHIP_TRANSFER_DELAY; 79 | 80 | emit NewOwnerCommitted(msg.sender, newOwner, block.timestamp + OWNERSHIP_TRANSFER_DELAY); 81 | } 82 | 83 | function acceptTransferOwnership() external { 84 | require(msg.sender == pendingOwner, "Only new owner"); 85 | require(block.timestamp >= ownershipTransferDeadline, "Deadline not passed"); 86 | 87 | emit NewOwnerAccepted(owner, msg.sender); 88 | 89 | owner = pendingOwner; 90 | pendingOwner = address(0); 91 | ownershipTransferDeadline = 0; 92 | } 93 | 94 | function revokeTransferOwnership() external onlyOwner { 95 | emit NewOwnerRevoked(msg.sender, pendingOwner); 96 | 97 | pendingOwner = address(0); 98 | ownershipTransferDeadline = 0; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /contracts/core/DebtToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@layerzerolabs/oft-evm/contracts/OFT.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; 7 | import "../interfaces/IYalaCore.sol"; 8 | import "../interfaces/IDebtToken.sol"; 9 | 10 | contract DebtToken is IDebtToken, OFT, ERC20Permit { 11 | // --- Addresses --- 12 | address public immutable stabilityPoolAddress; 13 | address public immutable borrowerOperationsAddress; 14 | address public immutable factory; 15 | address public immutable gasPool; 16 | 17 | mapping(address => bool) public troveManager; 18 | mapping(address => bool) public psm; 19 | // Amount of debt to be locked in gas pool on opening troves 20 | uint256 public immutable DEBT_GAS_COMPENSATION; 21 | 22 | constructor(string memory _name, string memory _symbol, address _stabilityPoolAddress, address _borrowerOperationsAddress, address _lzEndpoint, address _delegate, address _factory, address _gasPool, uint256 _gasCompensation) OFT(_name, _symbol, _lzEndpoint, _delegate) ERC20Permit(_name) { 23 | stabilityPoolAddress = _stabilityPoolAddress; 24 | borrowerOperationsAddress = _borrowerOperationsAddress; 25 | factory = _factory; 26 | gasPool = _gasPool; 27 | DEBT_GAS_COMPENSATION = _gasCompensation; 28 | } 29 | 30 | function enableTroveManager(address _troveManager) external { 31 | require(msg.sender == factory, "DebtToken: !Factory"); 32 | troveManager[_troveManager] = true; 33 | } 34 | 35 | function enablePSM(address _psm) external { 36 | require(msg.sender == factory, "DebtToken: !Factory"); 37 | psm[_psm] = true; 38 | } 39 | 40 | function mintWithGasCompensation(address _account, uint256 _amount) external returns (bool) { 41 | require(msg.sender == borrowerOperationsAddress); 42 | _mint(_account, _amount); 43 | _mint(gasPool, DEBT_GAS_COMPENSATION); 44 | return true; 45 | } 46 | 47 | function burnWithGasCompensation(address _account, uint256 _amount) external returns (bool) { 48 | require(msg.sender == borrowerOperationsAddress); 49 | _burn(_account, _amount); 50 | _burn(gasPool, DEBT_GAS_COMPENSATION); 51 | return true; 52 | } 53 | 54 | function mint(address _account, uint256 _amount) external { 55 | require(msg.sender == borrowerOperationsAddress || troveManager[msg.sender] || psm[msg.sender], "Debt: Caller not BO/TM/PSM"); 56 | _mint(_account, _amount); 57 | } 58 | 59 | function burn(address _account, uint256 _amount) external { 60 | require(troveManager[msg.sender] || psm[msg.sender], "DebtToken: Caller not TM/PSM"); 61 | _burn(_account, _amount); 62 | } 63 | 64 | function sendToSP(address _sender, uint256 _amount) external { 65 | require(msg.sender == stabilityPoolAddress, "DebtToken: Caller not StabilityPool"); 66 | _transfer(_sender, msg.sender, _amount); 67 | } 68 | 69 | function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external { 70 | require(msg.sender == stabilityPoolAddress || troveManager[msg.sender], "DebtToken: Caller not TM/SP"); 71 | _transfer(_poolAddress, _receiver, _amount); 72 | } 73 | 74 | function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) { 75 | _requireValidRecipient(recipient); 76 | return super.transfer(recipient, amount); 77 | } 78 | 79 | function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) { 80 | _requireValidRecipient(recipient); 81 | return super.transferFrom(sender, recipient, amount); 82 | } 83 | 84 | function _requireValidRecipient(address _recipient) internal view { 85 | require(_recipient != address(0) && _recipient != address(this), "DebtToken: Cannot transfer tokens directly to the Debt token contract or the zero address"); 86 | require(_recipient != stabilityPoolAddress && !troveManager[_recipient] && _recipient != borrowerOperationsAddress, "DebtToken: Cannot transfer tokens directly to the StabilityPool, TroveManager or BorrowerOps"); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/PSM.test.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-ethers' 2 | import { expect } from 'chai' 3 | import { MaxUint256, parseEther, ZeroAddress } from 'ethers' 4 | import { IFixture, loadFixture } from './fixture' 5 | import { deployNewPSM } from './utils' 6 | 7 | describe('Test PSM', () => { 8 | let fixture: IFixture 9 | 10 | beforeEach(async () => { 11 | fixture = await loadFixture() 12 | }) 13 | 14 | it('initialize PSM', async () => { 15 | const { psm, pegToken } = await deployNewPSM(fixture) 16 | const feeIn = await psm.feeIn() 17 | const feeOut = await psm.feeOut() 18 | const supplyCap = await psm.supplyCap() 19 | 20 | expect(pegToken.target).to.not.equal(ZeroAddress) 21 | expect(feeIn).to.equal(parseEther('0.01')) 22 | expect(feeOut).to.equal(parseEther('0.02')) 23 | expect(supplyCap).to.equal(parseEther('10000000')) 24 | }) 25 | 26 | it('buy debt tokens', async () => { 27 | const { DebtToken, account } = fixture 28 | const { psm, pegToken } = await deployNewPSM(fixture) 29 | await pegToken.approve(psm.target, MaxUint256) 30 | const pegAmount = parseEther('1000') 31 | const initialDebtBalance = await DebtToken.balanceOf(account) 32 | const { amountDebtTokenReceived } = await psm.estimateBuy(pegAmount) 33 | await psm.buy(pegAmount) 34 | const finalDebtBalance = await DebtToken.balanceOf(account) 35 | expect(finalDebtBalance - initialDebtBalance).to.be.eq(amountDebtTokenReceived) 36 | }) 37 | 38 | it('sell debt tokens', async () => { 39 | const { DebtToken, account } = fixture 40 | const { psm, pegToken } = await deployNewPSM(fixture) 41 | 42 | // First buy some debt tokens 43 | await pegToken.transfer(account, parseEther('2000')) 44 | await pegToken.approve(psm.target, MaxUint256) 45 | await psm.buy(parseEther('1000')) 46 | 47 | await pegToken.transfer(psm.target, parseEther('2000')) 48 | 49 | // Now sell them 50 | await DebtToken.approve(psm.target, parseEther('2000')) 51 | const sellAmount = parseEther('5') 52 | const initialDebtBalance = await DebtToken.balanceOf(account) 53 | await psm.sell(sellAmount) 54 | 55 | const finalDebtBalance = await DebtToken.balanceOf(account) 56 | expect(initialDebtBalance - finalDebtBalance).to.be.eq(sellAmount) 57 | }) 58 | 59 | it('respect supply cap', async () => { 60 | const { psm, pegToken } = await deployNewPSM(fixture, { supplyCap: parseEther('1000') }) 61 | 62 | //await pegToken.mint(account, parseEther('2000')) 63 | await pegToken.approve(psm.target, MaxUint256) 64 | 65 | await expect(psm.buy(parseEther('1200'))).to.be.revertedWith( 66 | 'PSM: Supply cap reached', 67 | ) 68 | }) 69 | 70 | it('update fees', async () => { 71 | const { psm } = await deployNewPSM(fixture) 72 | 73 | await psm.setFeeIn(parseEther('0.02')) 74 | await psm.setFeeOut(parseEther('0.03')) 75 | 76 | const feeIn = await psm.feeIn() 77 | const feeOut = await psm.feeOut() 78 | expect(feeIn).to.equal(parseEther('0.02')) 79 | expect(feeOut).to.equal(parseEther('0.03')) 80 | }) 81 | 82 | it('update supply cap', async () => { 83 | const { psm } = await deployNewPSM(fixture) 84 | 85 | await psm.setSupplyCap(parseEther('20000000')) 86 | 87 | const supplyCap = await psm.supplyCap() 88 | expect(supplyCap).to.equal(parseEther('20000000')) 89 | }) 90 | 91 | it('pause and unpause', async () => { 92 | const { psm, pegToken } = await deployNewPSM(fixture) 93 | 94 | //await pegToken.mint(account, parseEther('2000')) 95 | await pegToken.approve(psm.target, MaxUint256) 96 | 97 | await psm.pause() 98 | await expect(psm.buy(parseEther('100'))).to.be.revertedWith( 99 | 'Pausable: paused', 100 | ) 101 | 102 | await psm.unpause() 103 | await expect(psm.buy(parseEther('100'))).to.not.be.reverted 104 | }) 105 | 106 | it('check totalActivedebt', async () => { 107 | const { DebtToken, account } = fixture 108 | const { psm, pegToken } = await deployNewPSM(fixture) 109 | 110 | await pegToken.transfer(account, parseEther('2000')) 111 | await pegToken.approve(psm.target, MaxUint256) 112 | 113 | const buyAmount = parseEther('1000') 114 | await psm.buy(buyAmount) 115 | const { amountDebtTokenReceived, fee } = await psm.estimateBuy(buyAmount) 116 | let totalActivedebt = await psm.totalActivedebt() 117 | expect(totalActivedebt).to.equal(amountDebtTokenReceived + fee) 118 | 119 | const sellAmount = parseEther('500') 120 | await DebtToken.approve(psm.target, MaxUint256) 121 | await psm.sell(sellAmount) 122 | 123 | totalActivedebt = await psm.totalActivedebt() 124 | expect(totalActivedebt).to.equal(amountDebtTokenReceived + fee - sellAmount) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /contracts/core/PSM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/security/Pausable.sol"; 7 | import "../interfaces/IDebtToken.sol"; 8 | import "../dependencies/YalaOwnable.sol"; 9 | import "../interfaces/IPSM.sol"; 10 | 11 | contract PSM is IPSM, YalaOwnable, Pausable { 12 | using SafeERC20 for IERC20; 13 | 14 | uint256 public constant DECIMAL_PRECISIONS = 1e18; 15 | address public immutable override factory; 16 | IDebtToken public immutable override debtToken; 17 | IERC20Metadata public override pegToken; 18 | uint256 public override priceFactor; 19 | uint256 public override feeIn; // Fee for buying debtToken (in DECIMAL_PRECISIONS) 20 | uint256 public override feeOut; // Fee for selling debtToken (in DECIMAL_PRECISIONS) 21 | uint256 public supplyCap; // Maximum amount of debtToken that can be held by PSM 22 | uint256 public totalActivedebt; 23 | 24 | constructor(address _yalaCore, address _factory, address _debtToken) YalaOwnable(_yalaCore) { 25 | factory = _factory; 26 | debtToken = IDebtToken(_debtToken); 27 | } 28 | 29 | function initialize(IERC20Metadata _pegToken, uint256 _feeIn, uint256 _feeOut, uint256 _supplyCap) external override { 30 | require(msg.sender == factory, "PSM: !Factory"); 31 | uint8 pegTokenDecimals = _pegToken.decimals(); 32 | require(pegTokenDecimals <= 18, "PSM: Peg token decimals not supported"); 33 | priceFactor = 10 ** (18 - pegTokenDecimals); 34 | pegToken = _pegToken; 35 | _setFeeIn(_feeIn); 36 | _setFeeOut(_feeOut); 37 | _setSupplyCap(_supplyCap); 38 | emit PegTokenUpdated(_pegToken); 39 | } 40 | 41 | function setFeeIn(uint256 _feeIn) external override onlyOwner { 42 | _setFeeIn(_feeIn); 43 | } 44 | 45 | function setFeeOut(uint256 _feeOut) external override onlyOwner { 46 | _setFeeOut(_feeOut); 47 | } 48 | 49 | function setSupplyCap(uint256 _supplyCap) external onlyOwner { 50 | _setSupplyCap(_supplyCap); 51 | } 52 | 53 | // New functions for pausing and unpausing 54 | function pause() external onlyOwner { 55 | _pause(); 56 | } 57 | 58 | function unpause() external onlyOwner { 59 | _unpause(); 60 | } 61 | 62 | function buy(uint256 amountPegToken) external override whenNotPaused returns (uint256 amountDebtTokenReceived, uint256 fee) { 63 | require(amountPegToken > 0, "PSM: Amount peg token must be greater than 0"); 64 | (amountDebtTokenReceived, fee) = estimateBuy(amountPegToken); 65 | require(totalActivedebt + amountDebtTokenReceived <= supplyCap, "PSM: Supply cap reached"); 66 | if (feeIn > 0) require(fee > 0, "PSM: Fee must be greater than 0"); 67 | IERC20(pegToken).safeTransferFrom(msg.sender, address(this), amountPegToken); 68 | debtToken.mint(msg.sender, amountDebtTokenReceived); 69 | if (fee > 0) debtToken.mint(YALA_CORE.feeReceiver(), fee); 70 | totalActivedebt = totalActivedebt + amountDebtTokenReceived + fee; 71 | 72 | emit Buy(msg.sender, amountDebtTokenReceived, amountPegToken, fee); 73 | } 74 | 75 | function sell(uint256 amountDebtToken) external override whenNotPaused returns (uint256 amountPegTokenReceived, uint256 fee) { 76 | require(amountDebtToken > 0, "PSM: Amount debt token must be greater than 0"); 77 | (amountPegTokenReceived, fee) = estimateSell(amountDebtToken); 78 | if (feeOut > 0) require(fee > 0, "PSM: Fee must be greater than 0"); 79 | require(pegToken.balanceOf(address(this)) >= amountPegTokenReceived, "PSM: Insufficient peg token balance"); 80 | debtToken.burn(msg.sender, amountDebtToken - fee); 81 | IERC20(pegToken).safeTransfer(msg.sender, amountPegTokenReceived); 82 | if (fee > 0) debtToken.transferFrom(msg.sender, YALA_CORE.feeReceiver(), fee); 83 | totalActivedebt = totalActivedebt - amountDebtToken; 84 | 85 | emit Sell(msg.sender, amountDebtToken, amountPegTokenReceived, fee); 86 | } 87 | 88 | function estimateBuy(uint256 amountPegToken) public view override returns (uint256 amountDebtTokenReceived, uint256 fee) { 89 | if (feeIn > 0) { 90 | fee = (amountPegToken * feeIn * priceFactor) / DECIMAL_PRECISIONS; 91 | } 92 | amountDebtTokenReceived = amountPegToken * priceFactor - fee; 93 | } 94 | 95 | function estimateSell(uint256 amountDebtToken) public view override returns (uint256 amountPegTokenReceived, uint256 fee) { 96 | if (feeOut > 0) { 97 | fee = (amountDebtToken * feeOut) / DECIMAL_PRECISIONS; 98 | } 99 | amountPegTokenReceived = (amountDebtToken - fee) / priceFactor; 100 | } 101 | 102 | function _setFeeIn(uint256 _feeIn) internal { 103 | require(_feeIn <= DECIMAL_PRECISIONS, "PSM: Fee in must be less than 1"); 104 | feeIn = _feeIn; 105 | emit FeeInUpdated(_feeIn); 106 | } 107 | 108 | function _setFeeOut(uint256 _feeOut) internal { 109 | require(_feeOut <= DECIMAL_PRECISIONS, "PSM: Fee out must be less than 1"); 110 | feeOut = _feeOut; 111 | emit FeeOutUpdated(_feeOut); 112 | } 113 | 114 | function _setSupplyCap(uint256 _supplyCap) internal { 115 | supplyCap = _supplyCap; 116 | emit SupplyCapUpdated(_supplyCap); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { Addressable, MaxUint256, parseEther, Signer, ZeroAddress } from 'ethers' 2 | import { ICRSMFixture, IFixture } from './fixture' 3 | import { TroveManager__factory, TroveManager, MockAggregatorV3Interface, PSM__factory, CRSM__factory } from '../types' 4 | import { ethers } from 'hardhat' 5 | 6 | export async function deployNewCDP( 7 | fixture: IFixture, 8 | params?: { 9 | interestRate?: bigint, 10 | maxDebt?: bigint, 11 | spYieldPCT?: bigint, 12 | maxCollGasCompensation?: bigint, 13 | liquidationPenaltySP?: bigint, 14 | liquidationPenaltyRedistribution?: bigint, 15 | MCR?: bigint, 16 | SCR?: bigint, 17 | CCR?: bigint, 18 | metadataNFT?: string | Addressable, 19 | }, 20 | opts?: { 21 | collateralToken?: string | Addressable 22 | } 23 | ) { 24 | const { Factory, MockCollateralToken, MockPriceFeed, signer } = fixture 25 | params = { 26 | interestRate: parseEther('0.1'), 27 | maxDebt: parseEther('20000000000'), 28 | spYieldPCT: parseEther('0.8'), 29 | maxCollGasCompensation: parseEther('0.1'), 30 | liquidationPenaltySP: parseEther('0.05'), 31 | liquidationPenaltyRedistribution: parseEther('0.05'), 32 | MCR: parseEther('1.1'), 33 | SCR: parseEther('1.3'), 34 | CCR: parseEther('1.5'), 35 | metadataNFT: ZeroAddress, 36 | ...params 37 | } 38 | opts = { 39 | collateralToken: MockCollateralToken.target as string, 40 | ...opts 41 | } 42 | const salt = '0x' + Buffer.alloc(32, '0').toString('hex') 43 | const troveManagerAddress = await Factory.deployNewCDP.staticCall(opts.collateralToken!, salt, MockPriceFeed.target, ZeroAddress, params as any) 44 | await Factory.deployNewCDP(opts.collateralToken!, salt, MockPriceFeed.target, ZeroAddress, params as any) 45 | return TroveManager__factory.connect(troveManagerAddress, signer) 46 | } 47 | 48 | export async function deployNewPSM( 49 | fixture: IFixture, 50 | opts?: { 51 | pegToken?: string | Addressable, 52 | feeIn?: bigint, 53 | feeOut?: bigint, 54 | supplyCap?: bigint 55 | } 56 | ) { 57 | const { Factory, signer } = fixture 58 | const newPegToken = await ethers.deployContract('MockCollateralToken', signer) 59 | opts = { 60 | pegToken: newPegToken.target, 61 | feeIn: parseEther('0.01'), 62 | feeOut: parseEther('0.02'), 63 | supplyCap: parseEther('10000000'), 64 | ...opts 65 | } 66 | const psmAddress = await Factory.deployNewPSM.staticCall(ZeroAddress, opts!.pegToken!, opts.feeIn!, opts.feeOut!, opts.supplyCap!) 67 | await Factory.deployNewPSM(ZeroAddress, opts!.pegToken!, opts.feeIn!, opts.feeOut!, opts.supplyCap!) 68 | return { psm: PSM__factory.connect(psmAddress, signer), pegToken: newPegToken } 69 | } 70 | 71 | export async function openTrove( 72 | fixture: IFixture, 73 | params?: { 74 | coll?: bigint, 75 | debt?: bigint, 76 | price?: bigint, 77 | troveManager?: TroveManager, 78 | signer?: Signer, 79 | }, 80 | ) { 81 | const { BorrowerOperations, MockCollateralToken, MockPriceFeed, signer } = fixture 82 | params = { 83 | coll: parseEther('1'), 84 | debt: parseEther('1800'), 85 | price: parseEther('100000'), 86 | signer, 87 | ...params 88 | } 89 | 90 | params.troveManager = params.troveManager ?? await deployNewCDP(fixture) 91 | const token = await params.troveManager.collateralToken() 92 | await (MockCollateralToken.attach(token) as any).connect(signer).transfer(await params.signer!.getAddress(), params.coll) 93 | await (MockCollateralToken.attach(token) as any).connect(params.signer!).approve(BorrowerOperations.target, MaxUint256) 94 | await MockPriceFeed.connect(params.signer).updatePrice(token, params.price!) 95 | const id = await BorrowerOperations.connect(params.signer).openTrove.staticCall(params.troveManager.target, await params.signer!.getAddress(), params.coll!, params.debt!) 96 | await BorrowerOperations.connect(params.signer).openTrove(params.troveManager.target, await params.signer!.getAddress(), params.coll!, params.debt!) 97 | return { troveManager: params.troveManager, id } 98 | } 99 | 100 | 101 | export async function updateMockAggregatorV3Price( 102 | aggregatorV3Interface: MockAggregatorV3Interface, roundId: number, price: bigint) { 103 | const updateTime = (await ethers.provider.getBlock('latest'))!.timestamp 104 | await aggregatorV3Interface.updateRoundData({ 105 | roundId, 106 | answer: price, 107 | startedAt: updateTime, 108 | updatedAt: updateTime, 109 | answeredInRound: price 110 | }) 111 | } 112 | 113 | export async function createNewCRSM(fixture: IFixture, crsmFixture: ICRSMFixture, 114 | params?: { 115 | TRGCR?: bigint, 116 | TARCR?: bigint, 117 | MAX_TARCR?: bigint, 118 | debtGas?: bigint 119 | amount?: bigint, 120 | troveParams?: { 121 | coll?: bigint, 122 | debt?: bigint, 123 | price?: bigint, 124 | troveManager?: TroveManager, 125 | signer?: Signer, 126 | } 127 | }) { 128 | const { troveManager, id } = await openTrove(fixture, params?.troveParams) 129 | const { CRSMFactory } = crsmFixture 130 | const { DebtToken, signer } = fixture 131 | params = { 132 | TRGCR: parseEther('1.2'), 133 | TARCR: parseEther('1.4'), 134 | MAX_TARCR: parseEther('1.5'), 135 | debtGas: 0n, 136 | amount: parseEther('200'), 137 | ...params 138 | } 139 | await DebtToken.approve(CRSMFactory.target, MaxUint256) 140 | const crsmAddress = await CRSMFactory.createNewCRSM.staticCall(troveManager.target, id, params.TRGCR!, params.TARCR!, params.MAX_TARCR!, params.debtGas!, params.amount!) 141 | await CRSMFactory.createNewCRSM(troveManager.target, id, params.TRGCR!, params.TARCR!, params.MAX_TARCR!, params.debtGas!, params.amount!) 142 | return { crsm: CRSM__factory.connect(crsmAddress, signer), troveManager, id } 143 | } -------------------------------------------------------------------------------- /contracts/interfaces/ITroveManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 7 | import "./IPriceFeed.sol"; 8 | import "./IMetadataNFT.sol"; 9 | 10 | interface ITroveManager is IERC721Enumerable { 11 | // Store the necessary data for a trove 12 | struct Trove { 13 | uint256 debt; 14 | uint256 coll; 15 | uint256 stake; 16 | uint256 interest; 17 | } 18 | 19 | struct LocalTroveUpdateVariables { 20 | uint256 id; 21 | uint256 debtChange; 22 | uint256 collChange; 23 | uint256 interestRepayment; 24 | bool isCollIncrease; 25 | bool isDebtIncrease; 26 | address receiver; 27 | } 28 | 29 | struct DeploymentParams { 30 | uint256 interestRate; // 1e16 (1%) 31 | uint256 maxDebt; 32 | uint256 spYieldPCT; 33 | uint256 liquidationPenaltySP; 34 | uint256 liquidationPenaltyRedistribution; 35 | uint256 maxCollGasCompensation; 36 | uint256 MCR; // 11e17 (110%) 37 | uint256 SCR; 38 | uint256 CCR; 39 | IMetadataNFT metadataNFT; 40 | } 41 | 42 | // Object containing the collateral and debt snapshots for a given active trove 43 | struct RewardSnapshot { 44 | uint256 collateral; 45 | uint256 debt; 46 | uint256 activeInterest; 47 | uint256 defaultedInterest; 48 | } 49 | 50 | struct LiquidationValues { 51 | uint256 collOffset; 52 | uint256 debtOffset; 53 | uint256 interestOffset; 54 | uint256 collRedist; 55 | uint256 debtRedist; 56 | uint256 interestRedist; 57 | uint256 debtGasCompensation; 58 | uint256 collGasCompensation; 59 | uint256 remainingDeposits; 60 | uint256 collSurplus; 61 | } 62 | 63 | struct SingleLiquidation { 64 | uint256 coll; 65 | uint256 debt; 66 | uint256 interest; 67 | uint256 collGasCompensation; 68 | uint256 debtGasCompensation; 69 | uint256 collToLiquidate; 70 | uint256 collOffset; 71 | uint256 debtOffset; 72 | uint256 interestOffset; 73 | uint256 collRedist; 74 | uint256 debtRedist; 75 | uint256 interestRedist; 76 | uint256 collSurplus; 77 | } 78 | 79 | enum TroveManagerOperation { 80 | open, 81 | close, 82 | adjust, 83 | liquidate 84 | } 85 | event CRUpdated(uint256 _MCR, uint256 _SCR, uint256 _CCR); 86 | event ShutDown(); 87 | event PauseUpdated(bool _paused); 88 | event PriceFeedUpdated(IPriceFeed priceFeed); 89 | event MetadataNFTUpdated(IMetadataNFT _metadataNFT); 90 | event InterestRateUpdated(uint256 _interestRate); 91 | event MaxSystemDebtUpdated(uint256 _cap); 92 | event SPYieldPCTUpdated(uint256 _spYielPCT); 93 | event LIQUIDATION_PENALTY_SP_Updated(uint256 _penaltySP); 94 | event LIQUIDATION_PENALTY_REDISTRIBUTION_Updated(uint256 _penaltyRedist); 95 | event MAX_COLL_GAS_COMPENSATION_Updated(uint256 _maxCollGasCompensation); 96 | 97 | event TroveOpened(uint256 id, address owner, uint256 _collateralAmount, uint256 _compositeDebt, uint256 stake); 98 | event TroveUpdated(uint256 id, uint256 newColl, uint256 newDebt, uint256 newStake, uint256 newInterest, address _receiver, TroveManagerOperation operation); 99 | event TotalStakesUpdated(uint256 newTotalStakes); 100 | event LTermsUpdated(uint256 new_L_collateral, uint256 new_L_debt, uint256 new_L_defaulted_interest); 101 | event Liquidated(address owner, uint256 id, uint256 coll, uint256 debt, uint256 interest, uint256 collSurplus); 102 | event CollateralSent(address _account, uint256 _amount); 103 | event CollSurplusClaimed(address _account, uint256 _amount); 104 | event TroveClosed(uint256 id); 105 | event InterestAccrued(uint256 interest); 106 | event SPYieldAccrued(uint256 yieldFee); 107 | function accrueInterests() external returns (uint256 yieldSP, uint256 yieldFee); 108 | function collateralToken() external view returns (IERC20); 109 | function totalActiveDebt() external view returns (uint256); 110 | function defaultedDebt() external view returns (uint256); 111 | function shutdownAt() external view returns (uint256); 112 | function getEntireSystemDebt() external view returns (uint256); 113 | function getEntireSystemBalances() external returns (uint256 coll, uint256 debt, uint256 interest, uint256 price); 114 | function interestRate() external view returns (uint256); 115 | function MCR() external view returns (uint256); 116 | function SCR() external view returns (uint256); 117 | function CCR() external view returns (uint256); 118 | function maxSystemDebt() external view returns (uint256); 119 | function SP_YIELD_PCT() external view returns (uint256); 120 | function MAX_COLL_GAS_COMPENSATION() external view returns (uint256); 121 | function LIQUIDATION_PENALTY_SP() external view returns (uint256); 122 | function LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256); 123 | function getTCR() external returns (uint256); 124 | function setParameters(IPriceFeed _priceFeed, IERC20 _collateral, DeploymentParams memory params) external; 125 | function openTrove(address owner, uint256 _collateralAmount, uint256 _compositeDebt) external returns (uint256 id); 126 | function updateTroveFromAdjustment(uint256 id, bool _isDebtIncrease, uint256 _debtChange, bool _isCollIncrease, uint256 _collChange, uint256 _interestRepayment, address _receiver) external returns (uint256, uint256, uint256, uint256); 127 | function closeTrove(uint256 id, address _receiver, uint256 collAmount, uint256 debtAmount, uint256 interest) external; 128 | function applyPendingRewards(uint256 id) external returns (uint256 coll, uint256 debt, uint256 interest); 129 | function fetchPrice() external returns (uint256); 130 | function getCurrentTrove(uint256 id) external view returns (Trove memory); 131 | function getPendingYieldSP() external view returns (uint256); 132 | function accountCollSurplus(address account) external view returns (uint256); 133 | function hasShutdown() external view returns (bool); 134 | function batchLiquidate(uint256[] memory ids) external; 135 | } 136 | -------------------------------------------------------------------------------- /test/fixture.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-ethers' 2 | import 'hardhat-deploy' 3 | import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' 4 | import { 5 | YalaCore, 6 | DebtToken, 7 | Factory, 8 | BorrowerOperations, 9 | MockPriceFeed, 10 | MockCollateralToken, 11 | PSM, 12 | MultiTroveGetters, 13 | MockAggregatorV3Interface, 14 | PriceFeed, 15 | StabilityPool, 16 | } from '../types' 17 | import { ethers } from 'hardhat' 18 | import { Contract, encodeRlp, keccak256, parseEther, parseUnits, ZeroAddress } from 'ethers' 19 | import { updateMockAggregatorV3Price } from './utils' 20 | 21 | export interface IFixture { 22 | signer: HardhatEthersSigner 23 | signers: HardhatEthersSigner[] 24 | gasPool: Contract 25 | YalaCore: YalaCore 26 | DebtToken: DebtToken 27 | Factory: Factory 28 | BorrowerOperations: BorrowerOperations 29 | PriceFeed: PriceFeed 30 | MockAggregatorV3Interface: MockAggregatorV3Interface 31 | MockCollateralToken: MockCollateralToken 32 | MockPriceFeed: MockPriceFeed, 33 | StabilityPool: StabilityPool 34 | PSM: PSM 35 | MultiTroveGetters: MultiTroveGetters 36 | account: string 37 | accounts: string[] 38 | } 39 | 40 | export interface ICRSMFixture { 41 | NullCRSMNFTMetadata: NullCRSMNFTMetadata 42 | CRSM: CRSM 43 | CRSMFactory: CRSMFactory 44 | } 45 | 46 | async function computeAddress(account: string, nonce?: number): Promise { 47 | if (!nonce) { 48 | nonce = await ethers.provider.getTransactionCount(account) 49 | } 50 | let nonceHex = BigInt(nonce!).toString(16) 51 | if (nonceHex.length % 2 != 0) { 52 | nonceHex = '0' + nonceHex 53 | } 54 | return '0x' + keccak256(encodeRlp([account, '0x' + nonceHex])).substring(26) 55 | } 56 | 57 | export async function loadFixture(): Promise { 58 | const gasCompensation = parseEther('200') 59 | const minNetDebt = parseEther('2000') 60 | const maxCollGasCompensation = parseEther('0.1') 61 | const signers = await ethers.getSigners() 62 | const signer = signers[0] 63 | const account = signer.address 64 | const accounts = signers.map(s => s.address) 65 | const gasPool = await ethers.deployContract('GasPool', signer) 66 | const MockLayerZeroEndpoint = await ethers.deployContract('MockLayerZeroEndpoint', signer) 67 | const MockCollateralToken = await ethers.deployContract('MockCollateralToken', signer) 68 | const MockPriceFeed = await ethers.deployContract('MockPriceFeed', signer) 69 | const nonce = await signer.getNonce() 70 | const yalaCoreAddress = await computeAddress(account, nonce) 71 | const debtTokenAddress = await computeAddress(account, nonce + 1) 72 | const stablityPoolAddress = await computeAddress(account, nonce + 2) 73 | const borrowerOperationsAddress = await computeAddress(account, nonce + 3) 74 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 75 | const troveManagerImplAddress = computeAddress(account, nonce + 4) 76 | const psmImplAddress = await computeAddress(account, nonce + 5) 77 | const factoryAddress = await computeAddress(account, nonce + 6) 78 | const YalaCore = await ethers.deployContract('YalaCore', [accounts[0], accounts[1], accounts[2]], signer) 79 | const DebtToken = await ethers.deployContract('DebtToken', [ 80 | 'Yala Stable Coin', 81 | 'YU', 82 | stablityPoolAddress, 83 | borrowerOperationsAddress, 84 | MockLayerZeroEndpoint.target, 85 | accounts[0], 86 | factoryAddress, 87 | gasPool.target, 88 | gasCompensation 89 | ]) 90 | const StabilityPool = await ethers.deployContract('StabilityPool', [yalaCoreAddress, factoryAddress, debtTokenAddress], signer) 91 | const BorrowerOperations = await ethers.deployContract('BorrowerOperations', [yalaCoreAddress, debtTokenAddress, factoryAddress, minNetDebt, gasCompensation], signer) 92 | const TroveManagerImpl = await ethers.deployContract('TroveManager', [ 93 | yalaCoreAddress, 94 | factoryAddress, 95 | gasPool.target, 96 | debtTokenAddress, 97 | borrowerOperationsAddress, 98 | stablityPoolAddress, 99 | gasCompensation, 100 | maxCollGasCompensation], signer) 101 | const PSM = await ethers.deployContract('PSM', [yalaCoreAddress, factoryAddress, debtTokenAddress], signer) 102 | const Factory = await ethers.deployContract('Factory', 103 | [ 104 | yalaCoreAddress, 105 | debtTokenAddress, 106 | stablityPoolAddress, 107 | borrowerOperationsAddress, 108 | TroveManagerImpl.target, 109 | PSM.target 110 | ], signer) 111 | const MockAggregatorV3Interface = await ethers.deployContract('MockAggregatorV3Interface', [account], signer) 112 | const tokenPrice = parseUnits('100000', 8) 113 | await updateMockAggregatorV3Price(MockAggregatorV3Interface, 99, tokenPrice) 114 | const PriceFeed = await ethers.deployContract('PriceFeed', [yalaCoreAddress, [[MockCollateralToken.target, MockAggregatorV3Interface.target, 3600]]], signer) 115 | const MultiTroveGetters = await ethers.deployContract('MultiTroveGetters', [borrowerOperationsAddress], signer) 116 | return { 117 | signers, 118 | signer, 119 | MultiTroveGetters, 120 | gasPool, 121 | MockCollateralToken, 122 | MockPriceFeed, 123 | PriceFeed, 124 | PSM, 125 | MockAggregatorV3Interface, 126 | StabilityPool, 127 | YalaCore, 128 | DebtToken, 129 | Factory, 130 | BorrowerOperations, 131 | account, 132 | accounts, 133 | } 134 | } 135 | 136 | export async function loadCRSMFixture(fixture: IFixture): Promise { 137 | const { account, signer, YalaCore, BorrowerOperations, StabilityPool, DebtToken } = fixture 138 | const NullCRSMNFTMetadata = await ethers.deployContract('NullCRSMNFTMetadata', signer) 139 | const nonce = await signer.getNonce() 140 | const crsmAddress = await computeAddress(account, nonce) 141 | const crsmFactoryAddress = await computeAddress(account, nonce + 1) 142 | const CRSM = await ethers.deployContract('CRSM', [crsmFactoryAddress, BorrowerOperations.target, StabilityPool.target, DebtToken.target], signer) 143 | const CRSMFactory = await ethers.deployContract('CRSMFactory', [YalaCore.target, BorrowerOperations.target, DebtToken.target, crsmAddress, NullCRSMNFTMetadata.target], signer) 144 | return { 145 | NullCRSMNFTMetadata, 146 | CRSM, 147 | CRSMFactory 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /contracts/test/MultiTroveGetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/utils/math/Math.sol"; 6 | import "../interfaces/IBorrowerOperations.sol"; 7 | import "../interfaces/ITroveManager.sol"; 8 | 9 | contract MultiTroveGetters { 10 | struct Params { 11 | uint256 CCR; 12 | uint256 MCR; 13 | uint256 SCR; 14 | uint256 TCR; 15 | uint256 LIQUIDATION_PENALTY_SP; 16 | uint256 totalColl; 17 | uint256 totalDebt; 18 | uint256 totalInterest; 19 | uint256 price; 20 | uint256 maxSystemDebt; 21 | uint256 interestRate; 22 | uint256 minNetDebt; 23 | uint256 accountCollSurplus; 24 | bool hasShutdown; 25 | ITroveManager troveManager; 26 | } 27 | 28 | struct TroveState { 29 | uint256 ICR; 30 | uint256 id; 31 | address owner; 32 | uint256 entireDebt; 33 | uint256 maxDebtToMint; 34 | uint256 maxCollToWithdraw; 35 | uint256 liquidationPrice; 36 | ITroveManager.Trove trove; 37 | } 38 | 39 | IBorrowerOperations public borrowerOperations; 40 | 41 | constructor(IBorrowerOperations _borrowerOperations) { 42 | borrowerOperations = _borrowerOperations; 43 | } 44 | 45 | function getTroves(ITroveManager troveManager, uint256 startIndex, uint256 endIndex) external returns (Params memory params, TroveState[] memory states) { 46 | uint256 totalSupply = troveManager.totalSupply(); 47 | if (endIndex > totalSupply) { 48 | endIndex = totalSupply; 49 | } 50 | require(endIndex > startIndex, "endIndex must be greater than startIndex"); 51 | params.troveManager = troveManager; 52 | params.CCR = troveManager.CCR(); 53 | params.MCR = troveManager.MCR(); 54 | params.SCR = troveManager.SCR(); 55 | params.TCR = troveManager.getTCR(); 56 | params.maxSystemDebt = troveManager.maxSystemDebt(); 57 | params.interestRate = troveManager.interestRate(); 58 | params.LIQUIDATION_PENALTY_SP = troveManager.LIQUIDATION_PENALTY_SP(); 59 | params.minNetDebt = borrowerOperations.minNetDebt(); 60 | (params.totalColl, params.totalDebt, params.totalInterest, params.price) = params.troveManager.getEntireSystemBalances(); 61 | params.hasShutdown = params.troveManager.hasShutdown(); 62 | uint256 length = endIndex - startIndex; 63 | states = new TroveState[](length); 64 | for (uint256 i = 0; i < length; i++) { 65 | uint256 index = startIndex + i; 66 | uint256 id = troveManager.tokenByIndex(index); 67 | ITroveManager.Trove memory trove = troveManager.getCurrentTrove(id); 68 | states[i].trove = trove; 69 | states[i].id = id; 70 | states[i].entireDebt = trove.debt + trove.interest; 71 | address owner = troveManager.ownerOf(id); 72 | states[i].owner = owner; 73 | states[i].ICR = (trove.coll * params.price) / states[i].entireDebt; 74 | if (params.TCR > params.CCR) { 75 | calcMaxDebtToMint(params, states[i]); 76 | calcMaxCollToWithdraw(params, states[i]); 77 | } 78 | states[i].liquidationPrice = (params.MCR * states[i].entireDebt) / trove.coll; 79 | } 80 | } 81 | 82 | function getTrovesByOwner(ITroveManager troveManager, address owner) external returns (Params memory params, TroveState[] memory states) { 83 | uint256 balance = troveManager.balanceOf(owner); 84 | params.troveManager = troveManager; 85 | params.CCR = troveManager.CCR(); 86 | params.MCR = troveManager.MCR(); 87 | params.SCR = troveManager.SCR(); 88 | params.TCR = troveManager.getTCR(); 89 | params.maxSystemDebt = troveManager.maxSystemDebt(); 90 | params.interestRate = troveManager.interestRate(); 91 | params.LIQUIDATION_PENALTY_SP = troveManager.LIQUIDATION_PENALTY_SP(); 92 | params.minNetDebt = borrowerOperations.minNetDebt(); 93 | (params.totalColl, params.totalDebt, params.totalInterest, params.price) = params.troveManager.getEntireSystemBalances(); 94 | params.hasShutdown = params.troveManager.hasShutdown(); 95 | params.accountCollSurplus = troveManager.accountCollSurplus(owner); 96 | states = new TroveState[](balance); 97 | for (uint256 i = 0; i < balance; i++) { 98 | uint256 index = i; 99 | uint256 id = troveManager.tokenOfOwnerByIndex(owner, index); 100 | ITroveManager.Trove memory trove = troveManager.getCurrentTrove(id); 101 | states[i].trove = trove; 102 | states[i].id = id; 103 | states[i].owner = owner; 104 | states[i].entireDebt = trove.debt + trove.interest; 105 | states[i].ICR = (trove.coll * params.price) / states[i].entireDebt; 106 | if (params.TCR > params.CCR) { 107 | calcMaxDebtToMint(params, states[i]); 108 | calcMaxCollToWithdraw(params, states[i]); 109 | } 110 | states[i].liquidationPrice = (params.MCR * states[i].entireDebt) / trove.coll; 111 | } 112 | } 113 | 114 | function getSingleTrove(ITroveManager troveManager, uint256 id) external returns (Params memory params, TroveState memory state) { 115 | params.troveManager = troveManager; 116 | params.CCR = troveManager.CCR(); 117 | params.MCR = troveManager.MCR(); 118 | params.SCR = troveManager.SCR(); 119 | params.TCR = troveManager.getTCR(); 120 | params.maxSystemDebt = troveManager.maxSystemDebt(); 121 | params.interestRate = troveManager.interestRate(); 122 | params.LIQUIDATION_PENALTY_SP = troveManager.LIQUIDATION_PENALTY_SP(); 123 | params.minNetDebt = borrowerOperations.minNetDebt(); 124 | (params.totalColl, params.totalDebt, params.totalInterest, params.price) = params.troveManager.getEntireSystemBalances(); 125 | params.hasShutdown = params.troveManager.hasShutdown(); 126 | ITroveManager.Trove memory trove = troveManager.getCurrentTrove(id); 127 | state.trove = trove; 128 | state.id = id; 129 | state.entireDebt = trove.debt + trove.interest; 130 | address owner = troveManager.ownerOf(id); 131 | state.owner = owner; 132 | params.accountCollSurplus = troveManager.accountCollSurplus(owner); 133 | state.ICR = (trove.coll * params.price) / state.entireDebt; 134 | if (params.TCR > params.CCR) { 135 | calcMaxDebtToMint(params, state); 136 | calcMaxCollToWithdraw(params, state); 137 | } 138 | state.liquidationPrice = (params.MCR * state.entireDebt) / trove.coll; 139 | } 140 | 141 | function calcMaxDebtToMint(Params memory params, TroveState memory state) internal pure { 142 | if (state.ICR > params.MCR) { 143 | uint256 maxDebt = (state.trove.coll * params.price) / params.MCR; 144 | uint256 totalMaxDebt = (params.totalColl * params.price) / params.CCR; 145 | uint256 maxDebtMint = Math.min(maxDebt - state.entireDebt, totalMaxDebt - params.totalDebt - params.totalInterest); 146 | state.maxDebtToMint = maxDebtMint; 147 | } 148 | } 149 | 150 | function calcMaxCollToWithdraw(Params memory params, TroveState memory state) internal pure { 151 | if (state.ICR > params.MCR) { 152 | uint256 minColl = (state.entireDebt * params.MCR) / params.price; 153 | uint256 totalMinColl = ((params.totalDebt + params.totalInterest) * params.CCR) / params.price; 154 | uint256 collWithdraw = Math.min(state.trove.coll - minColl, params.totalColl - totalMinColl); 155 | state.maxCollToWithdraw = collWithdraw; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/BorrowerOperations.test.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-ethers' 2 | import { loadFixture, IFixture } from './fixture' 3 | import { expect } from 'chai' 4 | import { formatEther, MaxUint256, parseEther } from 'ethers' 5 | import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers' 6 | import { deployNewCDP, openTrove } from './utils' 7 | import { ethers } from 'hardhat' 8 | 9 | describe('Test BorrowerOperations', () => { 10 | let fixture: IFixture 11 | 12 | beforeEach(async () => { 13 | fixture = await loadFixture() 14 | }) 15 | 16 | it('open trove', async () => { 17 | const { BorrowerOperations, MockCollateralToken, MockPriceFeed, account } = fixture 18 | const troveManager = await deployNewCDP(fixture) 19 | await MockCollateralToken.approve(BorrowerOperations.target, MaxUint256) 20 | // min net debt not reached 21 | await expect(BorrowerOperations.openTrove(troveManager.target, account, parseEther('1'), parseEther('1000'))).to.be.reverted 22 | // cr not reached 23 | await expect(BorrowerOperations.openTrove(troveManager.target, account, parseEther('1'), parseEther('1800'))).to.be.reverted 24 | const price = parseEther('100000') 25 | await MockPriceFeed.updatePrice(MockCollateralToken.target, price) 26 | await BorrowerOperations.openTrove(troveManager.target, account, parseEther('1'), parseEther('1800')) 27 | const TCR = await troveManager.getTCR.staticCall() 28 | // console.log('TCR', formatEther(TCR)) 29 | // 100000 * 1 / (1800+200) 30 | expect(TCR).to.be.eq(parseEther('50')) 31 | const ICR = await troveManager.getCurrentICR.staticCall(0, price) 32 | // only one Trove 33 | expect(ICR).to.be.eq(TCR) 34 | await MockPriceFeed.updatePrice(MockCollateralToken.target, parseEther('2999')) 35 | const newTCR = await troveManager.getTCR.staticCall() 36 | expect(newTCR).to.be.lt(await troveManager.CCR()) 37 | await expect(BorrowerOperations.openTrove(troveManager.target, account, parseEther('1'), parseEther('1800'))).to.be.reverted 38 | await BorrowerOperations.openTrove(troveManager.target, account, parseEther('1.01'), parseEther('1800')) 39 | }) 40 | 41 | it('add coll', async () => { 42 | const { troveManager, id } = await openTrove(fixture) 43 | const { BorrowerOperations } = fixture 44 | await BorrowerOperations.addColl(troveManager.target, id, parseEther('1')) 45 | const newTCR = await troveManager.getTCR.staticCall() 46 | // console.log('newTCR', formatEther(newTCR)) 47 | }) 48 | 49 | it('withdraw coll', async () => { 50 | const { troveManager, id } = await openTrove(fixture) 51 | const { BorrowerOperations } = fixture 52 | await BorrowerOperations.withdrawColl(troveManager.target, id, parseEther('0.2')) 53 | const newTCR = await troveManager.getTCR.staticCall() 54 | // console.log('newTCR', formatEther(newTCR)) 55 | }) 56 | 57 | it('withdraw debt', async () => { 58 | const { troveManager, id } = await openTrove(fixture) 59 | const { BorrowerOperations } = fixture 60 | await BorrowerOperations.withdrawDebt(troveManager.target, id, parseEther('2000')) 61 | const newTCR = await troveManager.getTCR.staticCall() 62 | // console.log('newTCR', formatEther(newTCR)) 63 | }) 64 | 65 | it('repay', async () => { 66 | const { troveManager, id } = await openTrove(fixture, { debt: parseEther('4800') }) 67 | const { BorrowerOperations } = fixture 68 | await time.increase(24 * 3600 * 365) 69 | // anyonw can pay a trove 70 | await BorrowerOperations.repay(troveManager.target, id, parseEther('500')) 71 | await BorrowerOperations.repay(troveManager.target, id, parseEther('500')) 72 | const debtRepaidTrove = await troveManager.Troves(id) 73 | expect(debtRepaidTrove.debt).to.be.gt(0n) 74 | expect(debtRepaidTrove.interest).to.be.gt(0n) 75 | await BorrowerOperations.repay(troveManager.target, id, parseEther('2000')) 76 | const trove = await troveManager.Troves(id) 77 | expect(trove.debt).to.be.eq(parseEther('2000')) 78 | expect(trove.interest).to.be.gt(0n) 79 | await BorrowerOperations.repay(troveManager.target, id, parseEther('501')) 80 | const interestRepaidTrove = await troveManager.Troves(id) 81 | expect(interestRepaidTrove.debt).to.be.eq(parseEther('2000')) 82 | expect(interestRepaidTrove.interest).to.be.eq(0n) 83 | }) 84 | 85 | it('close trove', async () => { 86 | const troveManager = await deployNewCDP(fixture, { spYieldPCT: 0n }) 87 | const { id } = await openTrove(fixture, { debt: parseEther('4800'), troveManager }) 88 | const { id: id2 } = await openTrove(fixture, { debt: parseEther('9800'), troveManager }) 89 | const { BorrowerOperations, accounts, signers, DebtToken, MockCollateralToken } = fixture 90 | await time.increase(24 * 3600 * 365) 91 | await BorrowerOperations.closeTrove(troveManager, id, accounts[1]) 92 | const coll = await MockCollateralToken.balanceOf(accounts[1]) 93 | expect(coll).to.be.eq(parseEther('1')) 94 | await troveManager.setApprovalForAll(accounts[2], true) 95 | await DebtToken.transfer(accounts[2], await DebtToken.balanceOf(accounts[0])) 96 | await BorrowerOperations.connect(signers[2]).closeTrove(troveManager, id2, accounts[2]) 97 | const totalSupply = await troveManager.totalSupply() 98 | expect(totalSupply).to.be.eq(0n) 99 | }) 100 | 101 | it('adjust trove', async () => { 102 | const troveManager = await deployNewCDP(fixture, { spYieldPCT: 0n }) 103 | const { id } = await openTrove(fixture, { debt: parseEther('4800'), troveManager }) 104 | await openTrove(fixture, { debt: parseEther('9800'), troveManager }) 105 | const { BorrowerOperations, MockPriceFeed, MockCollateralToken } = fixture 106 | await time.increase(24 * 3600 * 365) 107 | const TCR = await troveManager.getTCR.staticCall() 108 | // add coll 109 | await BorrowerOperations.adjustTrove(troveManager.target, id, parseEther('1'), 0, 0, false) 110 | // withdraw coll 111 | await BorrowerOperations.adjustTrove(troveManager.target, id, 0, parseEther('1'), 0, false) 112 | // withdraw debt 113 | await BorrowerOperations.adjustTrove(troveManager.target, id, 0, 0, parseEther('20000'), true) 114 | // repay debt 115 | await BorrowerOperations.adjustTrove(troveManager.target, id, 0, 0, parseEther('20000'), false) 116 | // TCR < CCR 117 | const price = parseEther('12000') 118 | await MockPriceFeed.updatePrice(MockCollateralToken.target, price) 119 | const newTCR = await troveManager.getTCR.staticCall() 120 | expect(newTCR).to.be.lt(await troveManager.CCR()) 121 | await expect(BorrowerOperations.adjustTrove(troveManager.target, id, 0, 0, parseEther('20000'), true)).to.be.reverted 122 | await expect(BorrowerOperations.adjustTrove(troveManager.target, id, 0, parseEther('0.1'), parseEther('1199'), false)).to.be.reverted 123 | await BorrowerOperations.adjustTrove(troveManager.target, id, 0, parseEther('0.1'), parseEther('1200'), false) 124 | 125 | }) 126 | 127 | it('multi collateral', async () => { 128 | const { signer, account, MockCollateralToken, BorrowerOperations } = fixture 129 | const newCollateral = await ethers.deployContract('MockCollateralToken', signer) 130 | const troveManager0 = await deployNewCDP(fixture, { spYieldPCT: 0n }, { collateralToken: MockCollateralToken.target, }) 131 | const troveManager1 = await deployNewCDP(fixture, { spYieldPCT: 0n }, { collateralToken: newCollateral.target }) 132 | const { id: id0 } = await openTrove(fixture, { debt: parseEther('4800'), coll: parseEther('1'), price: parseEther('100000'), troveManager: troveManager0 }) 133 | const { id: id1 } = await openTrove(fixture, { debt: parseEther('9800'), coll: parseEther('100'), price: parseEther('5000'), troveManager: troveManager1 }) 134 | await time.increase(24 * 3600 * 365) 135 | const { id: id2 } = await openTrove(fixture, { debt: parseEther('4800'), coll: parseEther('1'), price: parseEther('100000'), troveManager: troveManager0 }) 136 | await time.increase(24 * 3600 * 365) 137 | const { id: id3 } = await openTrove(fixture, { debt: parseEther('4800'), coll: parseEther('1'), price: parseEther('100000'), troveManager: troveManager0 }) 138 | await BorrowerOperations.closeTrove(troveManager0, id0, account) 139 | await BorrowerOperations.closeTrove(troveManager0, id2, account) 140 | await BorrowerOperations.closeTrove(troveManager0, id3, account) 141 | const totalSupply = await troveManager0.totalSupply() 142 | expect(totalSupply).to.be.eq(0n) 143 | }) 144 | 145 | }) -------------------------------------------------------------------------------- /contracts/core/PriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "../interfaces/IAggregatorV3Interface.sol"; 6 | import "../dependencies/YalaMath.sol"; 7 | import "../dependencies/YalaOwnable.sol"; 8 | 9 | import "@openzeppelin/contracts/utils/Address.sol"; 10 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 11 | /** 12 | @title Yala Multi Token Price Feed 13 | @notice Based on Gravita's PriceFeed: 14 | https://github.com/Gravita-Protocol/Gravita-SmartContracts/blob/9b69d555f3567622b0f84df8c7f1bb5cd9323573/contracts/PriceFeed.sol 15 | 16 | Yala's implementation additionally caches price values within a block 17 | */ 18 | contract PriceFeed is YalaOwnable { 19 | struct OracleRecord { 20 | IAggregatorV3Interface chainLinkOracle; 21 | uint8 decimals; 22 | uint32 heartbeat; 23 | bool isFeedWorking; 24 | } 25 | 26 | struct PriceRecord { 27 | uint256 scaledPrice; 28 | uint64 timestamp; 29 | uint64 lastUpdated; 30 | uint128 roundId; 31 | } 32 | 33 | struct FeedResponse { 34 | uint80 roundId; 35 | int256 answer; 36 | uint256 timestamp; 37 | bool success; 38 | } 39 | 40 | struct OracleSetup { 41 | address token; 42 | address chainlink; 43 | uint32 heartbeat; 44 | } 45 | 46 | uint256 public constant RESPONSE_TIMEOUT_BUFFER = 1 hours; 47 | uint256 public constant MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND = 5e17; // 50% 48 | uint8 public constant MAX_DIGITS = 36; 49 | 50 | mapping(address => uint8) public TARGET_DIGITS; 51 | mapping(address => OracleRecord) public oracleRecords; 52 | mapping(address => PriceRecord) public priceRecords; 53 | 54 | error PriceFeed__UnsupportedTokenDecimalsError(address token, uint8 decimals); 55 | error PriceFeed__InvalidFeedResponseError(address token); 56 | error PriceFeed__FeedFrozenError(address token); 57 | error PriceFeed__UnknownFeedError(address token); 58 | error PriceFeed__HeartbeatOutOfBoundsError(); 59 | 60 | event NewOracleRegistered(address token, address chainlinkAggregator); 61 | event PriceFeedStatusUpdated(address token, address oracle, bool isWorking); 62 | event PriceRecordUpdated(address indexed token, uint256 _price); 63 | 64 | constructor(address _yalaCore, OracleSetup[] memory oracles) YalaOwnable(_yalaCore) { 65 | for (uint8 i = 0; i < oracles.length; i++) { 66 | OracleSetup memory o = oracles[i]; 67 | _setOracle(o.token, o.chainlink, o.heartbeat); 68 | } 69 | } 70 | 71 | function setOracle(address _token, address _chainlinkOracle, uint32 _heartbeat) external onlyOwner { 72 | _setOracle(_token, _chainlinkOracle, _heartbeat); 73 | } 74 | 75 | function _setOracle(address _token, address _chainlinkOracle, uint32 _heartbeat) internal { 76 | uint8 decimals = IERC20Metadata(_token).decimals(); 77 | if (decimals > 18) { 78 | revert PriceFeed__UnsupportedTokenDecimalsError(_token, decimals); 79 | } 80 | TARGET_DIGITS[_token] = MAX_DIGITS - decimals; 81 | 82 | if (_heartbeat > 86400) revert PriceFeed__HeartbeatOutOfBoundsError(); 83 | 84 | IAggregatorV3Interface newFeed = IAggregatorV3Interface(_chainlinkOracle); 85 | FeedResponse memory currResponse = _fetchFeedResponses(newFeed); 86 | if (!_isFeedWorking(currResponse)) { 87 | revert PriceFeed__InvalidFeedResponseError(_token); 88 | } 89 | 90 | if (_isPriceStale(currResponse.timestamp, _heartbeat)) { 91 | revert PriceFeed__FeedFrozenError(_token); 92 | } 93 | 94 | OracleRecord memory record = OracleRecord({ chainLinkOracle: newFeed, decimals: newFeed.decimals(), heartbeat: _heartbeat, isFeedWorking: true }); 95 | oracleRecords[_token] = record; 96 | PriceRecord memory _priceRecord = priceRecords[_token]; 97 | _processFeedResponses(_token, record, currResponse, _priceRecord); 98 | emit NewOracleRegistered(_token, _chainlinkOracle); 99 | } 100 | 101 | /** 102 | @notice Get the latest price returned from the oracle 103 | @dev You can obtain these values by calling `TroveManager.fetchPrice()` 104 | rather than directly interacting with this contract. 105 | @param _token Token to fetch the price for 106 | @return The latest valid price for the requested token 107 | */ 108 | function fetchPrice(address _token) public returns (uint256) { 109 | PriceRecord memory priceRecord = priceRecords[_token]; 110 | OracleRecord memory oracle = oracleRecords[_token]; 111 | 112 | uint256 scaledPrice = priceRecord.scaledPrice; 113 | // We short-circuit only if the price was already correct in the current block 114 | if (priceRecord.lastUpdated != block.timestamp) { 115 | if (priceRecord.lastUpdated == 0) { 116 | revert PriceFeed__UnknownFeedError(_token); 117 | } 118 | FeedResponse memory currResponse = _fetchFeedResponses(oracle.chainLinkOracle); 119 | scaledPrice = _processFeedResponses(_token, oracle, currResponse, priceRecord); 120 | } 121 | return scaledPrice; 122 | } 123 | 124 | function _processFeedResponses(address _token, OracleRecord memory oracle, FeedResponse memory _currResponse, PriceRecord memory priceRecord) internal returns (uint256) { 125 | uint8 decimals = oracle.decimals; 126 | bool isValidResponse = _isFeedWorking(_currResponse) && !_isPriceStale(_currResponse.timestamp, oracle.heartbeat); 127 | if (isValidResponse) { 128 | uint256 scaledPrice = _scalePriceByDigits(_token, uint256(_currResponse.answer), decimals); 129 | if (!oracle.isFeedWorking) { 130 | _updateFeedStatus(_token, oracle, true); 131 | } 132 | _storePrice(_token, scaledPrice, _currResponse.timestamp, _currResponse.roundId); 133 | return scaledPrice; 134 | } else { 135 | if (oracle.isFeedWorking) { 136 | _updateFeedStatus(_token, oracle, false); 137 | } 138 | if (_isPriceStale(priceRecord.timestamp, oracle.heartbeat)) { 139 | revert PriceFeed__FeedFrozenError(_token); 140 | } 141 | return priceRecord.scaledPrice; 142 | } 143 | } 144 | 145 | function _fetchFeedResponses(IAggregatorV3Interface oracle) internal view returns (FeedResponse memory currResponse) { 146 | currResponse = _fetchCurrentFeedResponse(oracle); 147 | } 148 | 149 | function _isPriceStale(uint256 _priceTimestamp, uint256 _heartbeat) internal view returns (bool) { 150 | return block.timestamp - _priceTimestamp > _heartbeat + RESPONSE_TIMEOUT_BUFFER; 151 | } 152 | 153 | function _isFeedWorking(FeedResponse memory _currentResponse) internal view returns (bool) { 154 | return _isValidResponse(_currentResponse); 155 | } 156 | 157 | function _isValidResponse(FeedResponse memory _response) internal view returns (bool) { 158 | return (_response.success) && (_response.roundId != 0) && (_response.timestamp != 0) && (_response.timestamp <= block.timestamp) && (_response.answer > 0); 159 | } 160 | 161 | function _scalePriceByDigits(address token, uint256 _price, uint256 _answerDigits) internal view returns (uint256) { 162 | uint8 targetDigits = TARGET_DIGITS[token]; 163 | if (_answerDigits == targetDigits) { 164 | return _price; 165 | } else if (_answerDigits < targetDigits) { 166 | // Scale the returned price value up to target precision 167 | return _price * (10 ** (targetDigits - _answerDigits)); 168 | } else { 169 | // Scale the returned price value down to target precision 170 | return _price / (10 ** (_answerDigits - targetDigits)); 171 | } 172 | } 173 | 174 | function _updateFeedStatus(address _token, OracleRecord memory _oracle, bool _isWorking) internal { 175 | oracleRecords[_token].isFeedWorking = _isWorking; 176 | emit PriceFeedStatusUpdated(_token, address(_oracle.chainLinkOracle), _isWorking); 177 | } 178 | 179 | function _storePrice(address _token, uint256 _price, uint256 _timestamp, uint128 roundId) internal { 180 | priceRecords[_token] = PriceRecord({ scaledPrice: _price, timestamp: uint64(_timestamp), lastUpdated: uint64(block.timestamp), roundId: roundId }); 181 | emit PriceRecordUpdated(_token, _price); 182 | } 183 | 184 | function _fetchCurrentFeedResponse(IAggregatorV3Interface _priceAggregator) internal view returns (FeedResponse memory response) { 185 | try _priceAggregator.latestRoundData() returns (uint80 roundId, int256 answer, uint256 /* startedAt */, uint256 timestamp, uint80 /* answeredInRound */) { 186 | // If call to Chainlink succeeds, return the response and success = true 187 | response.roundId = roundId; 188 | response.answer = answer; 189 | response.timestamp = timestamp; 190 | response.success = true; 191 | } catch { 192 | // If call to Chainlink aggregator reverts, return a zero response with success = false 193 | return response; 194 | } 195 | } 196 | 197 | function _fetchPrevFeedResponse(IAggregatorV3Interface _priceAggregator, uint80 _currentRoundId) internal view returns (FeedResponse memory prevResponse) { 198 | if (_currentRoundId == 0) { 199 | return prevResponse; 200 | } 201 | unchecked { 202 | try _priceAggregator.getRoundData(_currentRoundId - 1) returns (uint80 roundId, int256 answer, uint256 /* startedAt */, uint256 timestamp, uint80 /* answeredInRound */) { 203 | prevResponse.roundId = roundId; 204 | prevResponse.answer = answer; 205 | prevResponse.timestamp = timestamp; 206 | prevResponse.success = true; 207 | } catch {} 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /test/TroveManager.test.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-ethers' 2 | import { loadFixture, IFixture } from './fixture' 3 | import { expect } from 'chai' 4 | import { formatEther, parseEther } from 'ethers' 5 | import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers' 6 | import { deployNewCDP, openTrove } from './utils' 7 | import { ethers } from 'hardhat' 8 | 9 | describe('Test TroveManager', () => { 10 | let fixture: IFixture 11 | 12 | beforeEach(async () => { 13 | fixture = await loadFixture() 14 | }) 15 | 16 | it('batchLiquidate with redistribute', async () => { 17 | const { signers, BorrowerOperations, accounts, MockCollateralToken, MockPriceFeed, DebtToken } = fixture 18 | const price = parseEther('100000') 19 | const troveManager = await deployNewCDP(fixture, { spYieldPCT: 0n }, { collateralToken: MockCollateralToken.target }) 20 | const MCR = await troveManager.MCR() 21 | const { id: id0 } = await openTrove(fixture, { debt: parseEther('4800'), coll: parseEther('1'), price, troveManager }) 22 | await time.increase(24 * 3600 * 365) 23 | const { id: id1 } = await openTrove(fixture, { debt: parseEther('49800'), coll: parseEther('2'), price, troveManager }) 24 | await time.increase(24 * 3600 * 365) 25 | const { id: id2 } = await openTrove(fixture, { debt: parseEther('79800'), coll: parseEther('1'), price, troveManager }) 26 | await time.increase(24 * 3600 * 365) 27 | const { id: id3 } = await openTrove(fixture, { debt: parseEther('80800'), coll: parseEther('1'), price, troveManager }) 28 | await time.increase(24 * 3600 * 365) 29 | const TCR = await troveManager.getTCR.staticCall() 30 | // console.log('TCR', formatEther(TCR)) 31 | const ICR0 = await troveManager.getCurrentICR(id0, price) 32 | const ICR1 = await troveManager.getCurrentICR(id1, price) 33 | const ICR2 = await troveManager.getCurrentICR(id2, price) 34 | const ICR3 = await troveManager.getCurrentICR(id3, price) 35 | // console.log('ICR0', formatEther(ICR0), 'ICR1', formatEther(ICR1), 'ICR2', formatEther(ICR2), 'ICR3', formatEther(ICR3)) 36 | const newPrice = parseEther('96000') 37 | await MockPriceFeed.updatePrice(MockCollateralToken.target, newPrice) 38 | const NewTCR = await troveManager.getTCR.staticCall() 39 | // console.log('NewTCR', formatEther(NewTCR)) 40 | const NewICR0 = await troveManager.getCurrentICR(id0, newPrice) 41 | const NewICR1 = await troveManager.getCurrentICR(id1, newPrice) 42 | const NewICR2 = await troveManager.getCurrentICR(id2, newPrice) 43 | const NewICR3 = await troveManager.getCurrentICR(id3, newPrice) 44 | // console.log('NewICR0', formatEther(NewICR0), 'NewICR1', formatEther(NewICR1), 'NewICR2', formatEther(NewICR2), 'NewICR3', formatEther(NewICR3)) 45 | expect(NewICR2).to.be.lt(MCR) 46 | expect(NewICR3).to.be.lt(MCR) 47 | await troveManager.connect(signers[3]).batchLiquidate([id2, id3]) 48 | const balanceCollateral = await MockCollateralToken.balanceOf(accounts[3]) 49 | const balanceDebtToken = await DebtToken.balanceOf(accounts[3]) 50 | expect(balanceCollateral).to.be.eq(parseEther('0.01')) 51 | expect(balanceDebtToken).to.be.eq(parseEther('400')) 52 | const collSurplus = await troveManager.accountCollSurplus(accounts[0]) 53 | expect(collSurplus).to.be.gt(0n) 54 | await expect(troveManager.claimCollSurplus(accounts[1], collSurplus)).to.be.reverted 55 | await troveManager.claimCollSurplus(accounts[0], collSurplus) 56 | // close all trove with all received interest 57 | await troveManager.setApprovalForAll(accounts[2], true) 58 | const totalReceived = await DebtToken.balanceOf(accounts[0]) 59 | await DebtToken.transfer(accounts[2], totalReceived) 60 | await DebtToken.connect(signers[3]).transfer(accounts[2], balanceDebtToken) 61 | await BorrowerOperations.connect(signers[2]).closeTrove(troveManager.target, id0, accounts[0]) 62 | await BorrowerOperations.connect(signers[2]).closeTrove(troveManager.target, id1, accounts[0]) 63 | }) 64 | 65 | it('batchLiquidate with stability pool offset', async () => { 66 | const { signers, BorrowerOperations, StabilityPool, accounts, MockCollateralToken, MockPriceFeed, DebtToken } = fixture 67 | const price = parseEther('100000') 68 | const troveManager = await deployNewCDP(fixture, undefined, { collateralToken: MockCollateralToken.target }) 69 | const MCR = await troveManager.MCR() 70 | const _100PCT = await troveManager.DECIMAL_PRECISION() 71 | const { id: id0 } = await openTrove(fixture, { debt: parseEther('4800'), coll: parseEther('1'), price, troveManager }) 72 | await time.increase(24 * 3600 * 365) 73 | const { id: id1 } = await openTrove(fixture, { debt: parseEther('49800'), coll: parseEther('2'), price, troveManager }) 74 | await time.increase(24 * 3600 * 365) 75 | const { id: id2 } = await openTrove(fixture, { debt: parseEther('79800'), coll: parseEther('1'), price, troveManager }) 76 | await time.increase(24 * 3600 * 365) 77 | const { id: id3 } = await openTrove(fixture, { debt: parseEther('80800'), coll: parseEther('1'), price, troveManager }) 78 | await time.increase(24 * 3600 * 365) 79 | const TCR = await troveManager.getTCR.staticCall() 80 | // console.log('TCR', formatEther(TCR)) 81 | const ICR0 = await troveManager.getCurrentICR(id0, price) 82 | const ICR1 = await troveManager.getCurrentICR(id1, price) 83 | const ICR2 = await troveManager.getCurrentICR(id2, price) 84 | const ICR3 = await troveManager.getCurrentICR(id3, price) 85 | // console.log('ICR0', formatEther(ICR0), 'ICR1', formatEther(ICR1), 'ICR2', formatEther(ICR2), 'ICR3', formatEther(ICR3)) 86 | const newPrice = parseEther('96000') 87 | await MockPriceFeed.updatePrice(MockCollateralToken.target, newPrice) 88 | const NewTCR = await troveManager.getTCR.staticCall() 89 | // console.log('NewTCR', formatEther(NewTCR)) 90 | const NewICR0 = await troveManager.getCurrentICR(id0, newPrice) 91 | const NewICR1 = await troveManager.getCurrentICR(id1, newPrice) 92 | const NewICR2 = await troveManager.getCurrentICR(id2, newPrice) 93 | const NewICR3 = await troveManager.getCurrentICR(id3, newPrice) 94 | // console.log('NewICR0', formatEther(NewICR0), 'NewICR1', formatEther(NewICR1), 'NewICR2', formatEther(NewICR2), 'NewICR3', formatEther(NewICR3)) 95 | expect(NewICR2).to.be.lt(MCR) 96 | expect(NewICR2).to.be.lt(_100PCT) 97 | expect(NewICR3).to.be.gt(_100PCT) 98 | expect(NewICR3).to.be.lt(MCR) 99 | await StabilityPool.provideToSP(parseEther('100000') + parseEther('91000')) 100 | await troveManager.connect(signers[3]).batchLiquidate([id3, id2]) 101 | const balanceCollateral = await MockCollateralToken.balanceOf(accounts[3]) 102 | const balanceDebtToken = await DebtToken.balanceOf(accounts[3]) 103 | expect(balanceCollateral).to.be.eq(parseEther('0.01')) 104 | expect(balanceDebtToken).to.be.eq(parseEther('400')) 105 | 106 | }) 107 | 108 | it('batchLiquidate with stability pool offset and redistribute', async () => { 109 | const { signers, BorrowerOperations, StabilityPool, accounts, MockCollateralToken, MockPriceFeed, DebtToken } = fixture 110 | const price = parseEther('100000') 111 | const troveManager = await deployNewCDP(fixture, { liquidationPenaltyRedistribution: parseEther('0.05') }, { collateralToken: MockCollateralToken.target }) 112 | const MCR = await troveManager.MCR() 113 | const _100PCT = await troveManager.DECIMAL_PRECISION() 114 | const { id: id0 } = await openTrove(fixture, { debt: parseEther('4800'), coll: parseEther('1'), price, troveManager }) 115 | await time.increase(24 * 3600 * 365) 116 | const { id: id1 } = await openTrove(fixture, { debt: parseEther('49800'), coll: parseEther('2'), price, troveManager }) 117 | await time.increase(24 * 3600 * 365) 118 | const { id: id2 } = await openTrove(fixture, { debt: parseEther('79800'), coll: parseEther('1'), price, troveManager }) 119 | await time.increase(24 * 3600 * 365) 120 | const { id: id3 } = await openTrove(fixture, { debt: parseEther('80800'), coll: parseEther('1'), price, troveManager }) 121 | await time.increase(24 * 3600 * 365) 122 | const TCR = await troveManager.getTCR.staticCall() 123 | // console.log('TCR', formatEther(TCR)) 124 | const ICR0 = await troveManager.getCurrentICR(id0, price) 125 | const ICR1 = await troveManager.getCurrentICR(id1, price) 126 | const ICR2 = await troveManager.getCurrentICR(id2, price) 127 | const ICR3 = await troveManager.getCurrentICR(id3, price) 128 | // console.log('ICR0', formatEther(ICR0), 'ICR1', formatEther(ICR1), 'ICR2', formatEther(ICR2), 'ICR3', formatEther(ICR3)) 129 | const newPrice = parseEther('96000') 130 | await MockPriceFeed.updatePrice(MockCollateralToken.target, newPrice) 131 | const NewTCR = await troveManager.getTCR.staticCall() 132 | // console.log('NewTCR', formatEther(NewTCR)) 133 | const NewICR0 = await troveManager.getCurrentICR(id0, newPrice) 134 | const NewICR1 = await troveManager.getCurrentICR(id1, newPrice) 135 | const NewICR2 = await troveManager.getCurrentICR(id2, newPrice) 136 | const NewICR3 = await troveManager.getCurrentICR(id3, newPrice) 137 | // console.log('NewICR0', formatEther(NewICR0), 'NewICR1', formatEther(NewICR1), 'NewICR2', formatEther(NewICR2), 'NewICR3', formatEther(NewICR3)) 138 | expect(NewICR2).to.be.lt(MCR) 139 | expect(NewICR2).to.be.lt(_100PCT) 140 | expect(NewICR3).to.be.gt(_100PCT) 141 | expect(NewICR3).to.be.lt(MCR) 142 | await StabilityPool.provideToSP(parseEther('80000') + parseEther('91000') - parseEther('10000')) 143 | await troveManager.connect(signers[3]).batchLiquidate([id3, id2]) 144 | const balanceCollateral = await MockCollateralToken.balanceOf(accounts[3]) 145 | const balanceDebtToken = await DebtToken.balanceOf(accounts[3]) 146 | expect(balanceCollateral).to.be.eq(parseEther('0.01')) 147 | expect(balanceDebtToken).to.be.eq(parseEther('400')) 148 | }) 149 | 150 | it('shutdown smoothly', async () => { 151 | const { MockCollateralToken } = fixture 152 | const price = parseEther('100000') 153 | const troveManager = await deployNewCDP(fixture, undefined, { collateralToken: MockCollateralToken.target }) 154 | await openTrove(fixture, { debt: parseEther('4800'), coll: parseEther('1'), price, troveManager }) 155 | await time.increase(24 * 3600 * 365) 156 | await troveManager.shutdown() 157 | const totalActiveInterest = await troveManager.totalActiveInterest() 158 | await time.increase(24 * 3600 * 365) 159 | await troveManager.accrueInterests() 160 | const newTotalActiveInterest = await troveManager.totalActiveInterest() 161 | expect(totalActiveInterest).to.be.equal(newTotalActiveInterest) 162 | }) 163 | }) -------------------------------------------------------------------------------- /contracts/core/BorrowerOperations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 8 | 9 | import "../interfaces/IDebtToken.sol"; 10 | import "../interfaces/IBorrowerOperations.sol"; 11 | import "../dependencies/YalaBase.sol"; 12 | import "../dependencies/YalaMath.sol"; 13 | import "../dependencies/YalaOwnable.sol"; 14 | import "../dependencies/DelegatedOps.sol"; 15 | 16 | /** 17 | @title Yala Borrower Operations 18 | @notice Based on Liquity's `BorrowerOperations` 19 | https://github.com/liquity/dev/blob/main/packages/contracts/contracts/BorrowerOperations.sol 20 | 21 | */ 22 | contract BorrowerOperations is IBorrowerOperations, YalaBase, YalaOwnable, DelegatedOps { 23 | using SafeERC20 for IERC20; 24 | 25 | IDebtToken public immutable debtToken; 26 | address public immutable factory; 27 | uint256 public minNetDebt; 28 | 29 | mapping(ITroveManager => IERC20) public collateraTokens; 30 | 31 | modifier auth(ITroveManager troveManager, uint256 id) { 32 | address owner = troveManager.ownerOf(id); 33 | require(msg.sender == owner || troveManager.getApproved(id) == msg.sender || troveManager.isApprovedForAll(owner, msg.sender), "BorrowerOps: Not authorized"); 34 | _; 35 | } 36 | 37 | constructor(address _yalaCore, address _debtTokenAddress, address _factory, uint256 _minNetDebt, uint256 _gasCompensation) YalaOwnable(_yalaCore) YalaBase(_gasCompensation) { 38 | debtToken = IDebtToken(_debtTokenAddress); 39 | factory = _factory; 40 | _setMinNetDebt(_minNetDebt); 41 | } 42 | 43 | function _setMinNetDebt(uint256 _minNetDebt) internal { 44 | require(_minNetDebt > 0, "BorrowerOps: Invalid min net debt"); 45 | minNetDebt = _minNetDebt; 46 | emit MinNetDebtUpdated(_minNetDebt); 47 | } 48 | 49 | function configureCollateral(ITroveManager troveManager, IERC20 collateralToken) external { 50 | require(msg.sender == factory, "BorrowerOps: !Factory"); 51 | require(address(collateraTokens[troveManager]) == address(0), "BorrowerOps: Collateral configured"); 52 | collateraTokens[troveManager] = collateralToken; 53 | emit CollateralConfigured(troveManager, collateralToken); 54 | } 55 | 56 | function removeTroveManager(ITroveManager troveManager) external { 57 | require(address(collateraTokens[troveManager]) != address(0) && troveManager.shutdownAt() != 0 && troveManager.getEntireSystemDebt() == 0, "Trove Manager cannot be removed"); 58 | delete collateraTokens[troveManager]; 59 | emit TroveManagerRemoved(troveManager); 60 | } 61 | 62 | function getCompositeDebt(uint256 _debt) public view returns (uint256) { 63 | return _getCompositeDebt(_debt); 64 | } 65 | 66 | function openTrove(ITroveManager troveManager, address account, uint256 _collateralAmount, uint256 _debtAmount) external callerOrDelegated(account) returns (uint256 id) { 67 | require(!YALA_CORE.paused(), "BorrowerOps: Deposits are paused"); 68 | LocalVariables_openTrove memory vars; 69 | vars.netDebt = _debtAmount; 70 | vars.compositeDebt = _getCompositeDebt(vars.netDebt); 71 | _requireAtLeastMinNetDebt(vars.compositeDebt); 72 | troveManager.accrueInterests(); 73 | (vars.collateralToken, vars.totalCollateral, vars.totalDebt, vars.totalInterest, vars.price) = _getCollateralAndTCRData(troveManager); 74 | (vars.MCR, vars.CCR) = (troveManager.MCR(), troveManager.CCR()); 75 | vars.ICR = YalaMath._computeCR(_collateralAmount, vars.compositeDebt, vars.price); 76 | _requireICRisAboveMCR(vars.ICR, vars.MCR); 77 | uint256 TCR = YalaMath._computeCR(vars.totalCollateral, vars.totalDebt + vars.totalInterest, vars.price); 78 | if (TCR >= vars.CCR) { 79 | uint256 newTCR = _getNewTCRFromTroveChange(vars.totalCollateral * vars.price, vars.totalDebt + vars.totalInterest, _collateralAmount * vars.price, true, vars.compositeDebt, true); 80 | _requireNewTCRisAboveCCR(newTCR, vars.CCR); 81 | } else { 82 | _requireICRisAboveCCR(vars.ICR, vars.CCR); 83 | } 84 | // Create the trove 85 | id = troveManager.openTrove(account, _collateralAmount, vars.compositeDebt); 86 | // Move the collateral to the Trove Manager 87 | vars.collateralToken.safeTransferFrom(msg.sender, address(troveManager), _collateralAmount); 88 | // and mint the DebtAmount to the caller and gas compensation for Gas Pool 89 | debtToken.mintWithGasCompensation(account, vars.netDebt); 90 | emit TroveCreated(account, troveManager, id, _collateralAmount, vars.compositeDebt); 91 | } 92 | 93 | // Send collateral to a trove 94 | function addColl(ITroveManager troveManager, uint256 id, uint256 _collateralAmount) external { 95 | _adjustTrove(troveManager, id, _collateralAmount, 0, 0, false); 96 | } 97 | 98 | // Withdraw collateral from a trove 99 | function withdrawColl(ITroveManager troveManager, uint256 id, uint256 _collWithdrawal) external { 100 | _adjustTrove(troveManager, id, 0, _collWithdrawal, 0, false); 101 | } 102 | 103 | // Withdraw Debt tokens from a trove: mint new Debt tokens to the owner, and increase the trove's debt accordingly 104 | function withdrawDebt(ITroveManager troveManager, uint256 id, uint256 _debtAmount) external { 105 | _adjustTrove(troveManager, id, 0, 0, _debtAmount, true); 106 | } 107 | 108 | // Repay Debt tokens to a Trove: Burn the repaid Debt tokens, and reduce the trove's debt accordingly 109 | function repay(ITroveManager troveManager, uint256 id, uint256 _debtAmount) external { 110 | _adjustTrove(troveManager, id, 0, 0, _debtAmount, false); 111 | } 112 | 113 | function adjustTrove(ITroveManager troveManager, uint256 id, uint256 _collDeposit, uint256 _collWithdrawal, uint256 _debtChange, bool _isDebtIncrease) external { 114 | _adjustTrove(troveManager, id, _collDeposit, _collWithdrawal, _debtChange, _isDebtIncrease); 115 | } 116 | 117 | function _adjustTrove(ITroveManager troveManager, uint256 id, uint256 _collDeposit, uint256 _collWithdrawal, uint256 _debtChange, bool _isDebtIncrease) internal { 118 | require((_collDeposit == 0 && !_isDebtIncrease) || !YALA_CORE.paused(), "BorrowerOps: Trove adjustments are paused"); 119 | require(_collDeposit == 0 || _collWithdrawal == 0, "BorrowerOps: Cannot withdraw and add coll"); 120 | require(_collDeposit != 0 || _collWithdrawal != 0 || _debtChange != 0, "BorrowerOps: There must be either a collateral change or a debt change"); 121 | LocalVariables_adjustTrove memory vars; 122 | vars.account = troveManager.ownerOf(id); 123 | if (_collDeposit != 0 || _collWithdrawal != 0 || _isDebtIncrease) { 124 | require(msg.sender == vars.account || troveManager.getApproved(id) == msg.sender || troveManager.isApprovedForAll(vars.account, msg.sender), "BorrowerOps: Not authorized"); 125 | } 126 | if (_isDebtIncrease) { 127 | require(_debtChange > 0, "BorrowerOps: Debt increase requires non-zero debtChange"); 128 | } 129 | (vars.coll, vars.debt, vars.interest) = troveManager.applyPendingRewards(id); 130 | (vars.collateralToken, vars.totalCollateral, vars.totalDebt, vars.totalInterest, vars.price) = _getCollateralAndTCRData(troveManager); 131 | (vars.debtChange, vars.MCR, vars.CCR) = (_debtChange, troveManager.MCR(), troveManager.CCR()); 132 | uint256 TCR = YalaMath._computeCR(vars.totalCollateral, vars.totalDebt + vars.totalInterest, vars.price); 133 | vars.isBelowCriticalThreshold = TCR < vars.CCR; 134 | // Get the collChange based on whether or not collateral was sent in the transaction 135 | (vars.collChange, vars.isCollIncrease) = _getCollChange(_collDeposit, _collWithdrawal); 136 | if (!_isDebtIncrease && _debtChange > 0) { 137 | if (_debtChange > (vars.debt - minNetDebt)) { 138 | vars.debtChange = vars.debt - minNetDebt; 139 | _debtChange = _debtChange - vars.debtChange; 140 | vars.interestRepayment = YalaMath._min(_debtChange, vars.interest); 141 | } else { 142 | vars.debtChange = _debtChange; 143 | } 144 | } 145 | _requireValidAdjustment(_isDebtIncrease, vars); 146 | // If we are incrasing collateral, send tokens to the trove manager prior to adjusting the trove 147 | if (vars.isCollIncrease) vars.collateralToken.safeTransferFrom(msg.sender, address(troveManager), vars.collChange); 148 | troveManager.updateTroveFromAdjustment(id, _isDebtIncrease, vars.debtChange, vars.isCollIncrease, vars.collChange, vars.interestRepayment, msg.sender); 149 | emit AdjustTrove(vars.account, troveManager, id, _collDeposit, _collWithdrawal, vars.debtChange + vars.interestRepayment, _isDebtIncrease); 150 | } 151 | 152 | function closeTrove(ITroveManager troveManager, uint256 id, address receiver) external auth(troveManager, id) { 153 | LocalVariables_closeTrove memory vars; 154 | (vars.troveManager, vars.CCR, vars.account) = (troveManager, troveManager.CCR(), troveManager.ownerOf(id)); 155 | (uint256 coll, uint256 debt, uint256 interest) = vars.troveManager.applyPendingRewards(id); 156 | (vars.collateralToken, vars.totalCollateral, vars.totalDebt, vars.totalInterest, vars.price) = _getCollateralAndTCRData(troveManager); 157 | vars.compositeDebt = debt + interest; 158 | if (!troveManager.hasShutdown()) { 159 | uint256 newTCR = _getNewTCRFromTroveChange(vars.totalCollateral * vars.price, vars.totalDebt + vars.totalInterest, coll * vars.price, false, vars.compositeDebt, false); 160 | _requireNewTCRisAboveCCR(newTCR, vars.CCR); 161 | } 162 | troveManager.closeTrove(id, receiver, coll, debt, interest); 163 | // Burn the repaid Debt from the user's balance and the gas compensation from the Gas Pool 164 | debtToken.burnWithGasCompensation(msg.sender, vars.compositeDebt - DEBT_GAS_COMPENSATION); 165 | emit CloseTrove(vars.account, troveManager, id, receiver, coll, debt, interest); 166 | } 167 | 168 | function _getCollChange(uint256 _collReceived, uint256 _requestedCollWithdrawal) internal pure returns (uint256 collChange, bool isCollIncrease) { 169 | if (_collReceived != 0) { 170 | collChange = _collReceived; 171 | isCollIncrease = true; 172 | } else { 173 | collChange = _requestedCollWithdrawal; 174 | } 175 | } 176 | 177 | function _requireValidAdjustment(bool _isDebtIncrease, LocalVariables_adjustTrove memory _vars) internal pure { 178 | uint256 newICR = _getNewICRFromTroveChange(_vars.coll, _vars.debt + _vars.interest, _vars.collChange, _vars.isCollIncrease, _vars.debtChange + _vars.interestRepayment, _isDebtIncrease, _vars.price); 179 | _requireICRisAboveMCR(newICR, _vars.MCR); 180 | uint256 newTCR = _getNewTCRFromTroveChange(_vars.totalCollateral * _vars.price, _vars.totalDebt + _vars.totalInterest, _vars.collChange * _vars.price, _vars.isCollIncrease, _vars.debtChange + _vars.interestRepayment, _isDebtIncrease); 181 | if (_vars.isBelowCriticalThreshold) { 182 | if (_isDebtIncrease) { 183 | _requireNewTCRisAboveCCR(newTCR, _vars.CCR); 184 | } else if (!_vars.isCollIncrease) { 185 | require((_vars.debtChange + _vars.interestRepayment) * DECIMAL_PRECISION >= _vars.collChange * _vars.price, "BorrowerOps: Cannot withdraw collateral without paying back debt"); 186 | } 187 | } else { 188 | _requireNewTCRisAboveCCR(newTCR, _vars.CCR); 189 | } 190 | } 191 | 192 | function _requireICRisAboveMCR(uint256 _newICR, uint256 MCR) internal pure { 193 | require(_newICR >= MCR, "BorrowerOps: An operation that would result in ICR < MCR is not permitted"); 194 | } 195 | 196 | function _requireICRisAboveCCR(uint256 _newICR, uint256 CCR) internal pure { 197 | require(_newICR >= CCR, "BorrowerOps: An operation that would result in ICR < CCR is not permitted"); 198 | } 199 | 200 | function _requireNewTCRisAboveCCR(uint256 _newTCR, uint256 CCR) internal pure { 201 | require(_newTCR >= CCR, "BorrowerOps: An operation that would result in TCR < CCR is not permitted"); 202 | } 203 | 204 | function _requireAtLeastMinNetDebt(uint256 _netDebt) internal view { 205 | require(_netDebt >= minNetDebt, "BorrowerOps: Trove's net debt must be greater than minimum"); 206 | } 207 | 208 | // Compute the new collateral ratio, considering the change in coll and debt. Assumes 0 pending rewards. 209 | function _getNewICRFromTroveChange(uint256 _coll, uint256 _debt, uint256 _collChange, bool _isCollIncrease, uint256 _debtChange, bool _isDebtIncrease, uint256 _price) internal pure returns (uint256) { 210 | (uint256 newColl, uint256 newDebt) = _getNewTroveAmounts(_coll, _debt, _collChange, _isCollIncrease, _debtChange, _isDebtIncrease); 211 | uint256 newICR = YalaMath._computeCR(newColl, newDebt, _price); 212 | return newICR; 213 | } 214 | 215 | function _getNewTroveAmounts(uint256 _coll, uint256 _debt, uint256 _collChange, bool _isCollIncrease, uint256 _debtChange, bool _isDebtIncrease) internal pure returns (uint256, uint256) { 216 | uint256 newColl = _coll; 217 | uint256 newDebt = _debt; 218 | newColl = _isCollIncrease ? _coll + _collChange : _coll - _collChange; 219 | newDebt = _isDebtIncrease ? _debt + _debtChange : _debt - _debtChange; 220 | return (newColl, newDebt); 221 | } 222 | 223 | function _getNewTCRFromTroveChange(uint256 totalColl, uint256 totalDebt, uint256 _collChange, bool _isCollIncrease, uint256 _debtChange, bool _isDebtIncrease) internal pure returns (uint256) { 224 | totalDebt = _isDebtIncrease ? totalDebt + _debtChange : totalDebt - _debtChange; 225 | totalColl = _isCollIncrease ? totalColl + _collChange : totalColl - _collChange; 226 | uint256 newTCR = YalaMath._computeCR(totalColl, totalDebt); 227 | return newTCR; 228 | } 229 | 230 | function _getCollateralAndTCRData(ITroveManager troveManager) internal returns (IERC20 collateraToken, uint256 coll, uint256 debt, uint256 interest, uint256 price) { 231 | collateraToken = collateraTokens[troveManager]; 232 | require(address(collateraToken) != address(0), "BorrowerOps: nonexistent collateral"); 233 | (coll, debt, interest, price) = troveManager.getEntireSystemBalances(); 234 | } 235 | 236 | function getTCR(ITroveManager troveManager) public returns (uint256) { 237 | return troveManager.getTCR(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /test/StabilityPool.test.ts: -------------------------------------------------------------------------------- 1 | import '@nomicfoundation/hardhat-ethers' 2 | import { loadFixture, IFixture } from './fixture' 3 | import { formatEther, formatUnits, parseEther, parseUnits, Signer } from 'ethers' 4 | import { deployNewCDP, openTrove } from './utils' 5 | import { ethers } from 'hardhat' 6 | import { TroveManager } from '../types' 7 | import { time } from '@nomicfoundation/hardhat-network-helpers' 8 | 9 | describe('Test StabilityPool', () => { 10 | let fixture: IFixture 11 | 12 | beforeEach(async () => { 13 | fixture = await loadFixture() 14 | }) 15 | async function createMultipleTroves(troveManager: TroveManager, debt: bigint, coll: bigint, price: bigint, signer: Signer, amount: number) { 16 | const troveIds = [] 17 | for (let i = 0; i < amount; i++) { 18 | const { id } = await openTrove(fixture, { debt, coll, signer, price, troveManager }) 19 | troveIds.push(id) 20 | } 21 | return troveIds 22 | } 23 | 24 | it('get yield gains', async () => { 25 | const { StabilityPool, MockPriceFeed, MultiTroveGetters, DebtToken, signer, signers, accounts } = fixture 26 | const CollateralToken0 = await ethers.deployContract('MockDecimalCollateralToken', [18], signer) 27 | const CollateralToken1 = await ethers.deployContract('MockDecimalCollateralToken', [8], signer) 28 | 29 | const troveManager0 = await deployNewCDP(fixture, undefined, { collateralToken: CollateralToken0.target }) 30 | const troveManager1 = await deployNewCDP(fixture, undefined, { collateralToken: CollateralToken1.target }) 31 | const id00s = await createMultipleTroves(troveManager0, parseEther('5000'), parseEther('1'), parseEther('100000'), signers[0], 100) 32 | const id01s = await createMultipleTroves(troveManager0, parseEther('5000'), parseEther('1'), parseEther('100000'), signers[1], 100) 33 | const id10s = await createMultipleTroves(troveManager1, parseEther('50000'), parseUnits('1', 8), parseUnits('100000', 28), signers[0], 100) 34 | const id11s = await createMultipleTroves(troveManager1, parseEther('50000'), parseUnits('1', 8), parseUnits('100000', 28), signers[1], 100) 35 | await StabilityPool.connect(signers[0]).provideToSP(parseEther('1000000')) 36 | await StabilityPool.connect(signers[1]).provideToSP(parseEther('500000')) 37 | 38 | const oneYear = 365 * 24 * 3600 39 | await time.increase(oneYear) 40 | await MockPriceFeed.updatePrice(CollateralToken1.target, parseUnits('50000', 28)) 41 | const allTroves1 = await MultiTroveGetters.getTroves.staticCall(troveManager1.target, 0, 400) 42 | const liquidationIds1 = [] 43 | for (const trove of allTroves1.states) { 44 | if (trove.ICR < parseEther('1.1')) { 45 | liquidationIds1.push(trove.id) 46 | } 47 | } 48 | { 49 | const getYieldGains0 = await StabilityPool.getYieldGains(accounts[0]) 50 | // console.log('getYieldGains0', formatEther(getYieldGains0)) 51 | const getYieldGains1 = await StabilityPool.getYieldGains(accounts[1]) 52 | // console.log('getYieldGains1', formatEther(getYieldGains1)) 53 | } 54 | { 55 | await troveManager1.batchLiquidate(liquidationIds1.slice(0, 20)) 56 | await time.increase(oneYear) 57 | const getYieldGains0 = await StabilityPool.getYieldGains(accounts[0]) 58 | // console.log('getYieldGains0', formatEther(getYieldGains0)) 59 | const getYieldGains1 = await StabilityPool.getYieldGains(accounts[1]) 60 | // console.log('getYieldGains1', formatEther(getYieldGains1)) 61 | } 62 | { 63 | await StabilityPool.connect(signers[0]).claimYield(accounts[10]) 64 | // console.log('claimYield0', formatEther(await DebtToken.balanceOf(accounts[10]))) 65 | await StabilityPool.connect(signers[1]).claimYield(accounts[11]) 66 | // console.log('claimYield1', formatEther(await DebtToken.balanceOf(accounts[11]))) 67 | await time.increase(oneYear) 68 | } 69 | { 70 | // console.log('accounts0', formatEther(await DebtToken.balanceOf(accounts[0]))) 71 | const deposit0 = await StabilityPool.getCompoundedDebtDeposit(accounts[0]) 72 | // console.log('deposit0', formatEther(deposit0)) 73 | await StabilityPool.connect(signers[0]).withdrawFromSP(deposit0) 74 | // console.log('accounts0', formatEther(await DebtToken.balanceOf(accounts[0]))) 75 | 76 | // console.log('accounts1', formatEther(await DebtToken.balanceOf(accounts[1]))) 77 | const deposit1 = await StabilityPool.getCompoundedDebtDeposit(accounts[1]) 78 | // console.log('deposit1', formatEther(deposit1)) 79 | await StabilityPool.connect(signers[1]).withdrawFromSP(deposit1) 80 | // console.log('accounts1', formatEther(await DebtToken.balanceOf(accounts[1]))) 81 | await StabilityPool.connect(signers[0]).claimYield(accounts[10]) 82 | // console.log('claimYield0', formatEther(await DebtToken.balanceOf(accounts[10]))) 83 | await StabilityPool.connect(signers[1]).claimYield(accounts[11]) 84 | // console.log('claimYield1', formatEther(await DebtToken.balanceOf(accounts[11]))) 85 | // console.log('StabilityPool', formatEther(await DebtToken.balanceOf(StabilityPool.target))) 86 | 87 | } 88 | }) 89 | 90 | it('provide and withdraw', async () => { 91 | const { StabilityPool, MockPriceFeed, MultiTroveGetters, DebtToken, signer, signers, accounts } = fixture 92 | const CollateralToken0 = await ethers.deployContract('MockDecimalCollateralToken', [18], signer) 93 | const CollateralToken1 = await ethers.deployContract('MockDecimalCollateralToken', [8], signer) 94 | const CollateralToken2 = await ethers.deployContract('MockDecimalCollateralToken', [9], signer) 95 | 96 | const troveManager0 = await deployNewCDP(fixture, undefined, { collateralToken: CollateralToken0.target }) 97 | const troveManager1 = await deployNewCDP(fixture, undefined, { collateralToken: CollateralToken1.target }) 98 | const troveManager2 = await deployNewCDP(fixture, undefined, { collateralToken: CollateralToken2.target }) 99 | const id00s = await createMultipleTroves(troveManager0, parseEther('5000'), parseEther('1'), parseEther('100000'), signers[0], 50) 100 | const id01s = await createMultipleTroves(troveManager0, parseEther('80000'), parseEther('1'), parseEther('100000'), signers[1], 50) 101 | const id02s = await createMultipleTroves(troveManager0, parseEther('50000'), parseEther('1'), parseEther('100000'), signers[2], 50) 102 | const id03s = await createMultipleTroves(troveManager0, parseEther('15000'), parseEther('2'), parseEther('100000'), signers[3], 50) 103 | const id10s = await createMultipleTroves(troveManager1, parseEther('5000'), parseUnits('1', 8), parseUnits('100000', 28), signers[0], 50) 104 | const id11s = await createMultipleTroves(troveManager1, parseEther('60000'), parseUnits('1', 8), parseUnits('100000', 28), signers[1], 50) 105 | const id12s = await createMultipleTroves(troveManager1, parseEther('50000'), parseUnits('1', 8), parseUnits('100000', 28), signers[2], 50) 106 | const id13s = await createMultipleTroves(troveManager1, parseEther('8000'), parseUnits('2', 8), parseUnits('100000', 28), signers[3], 50) 107 | const id20s = await createMultipleTroves(troveManager2, parseEther('50000'), parseUnits('1', 9), parseUnits('100000', 27), signers[0], 50) 108 | const id21s = await createMultipleTroves(troveManager2, parseEther('80000'), parseUnits('1', 9), parseUnits('100000', 27), signers[1], 50) 109 | const id22s = await createMultipleTroves(troveManager2, parseEther('10000'), parseUnits('1', 9), parseUnits('100000', 27), signers[2], 50) 110 | const id23s = await createMultipleTroves(troveManager2, parseEther('150000'), parseUnits('2', 9), parseUnits('100000', 27), signers[3], 50) 111 | await StabilityPool.connect(signers[0]).provideToSP(parseEther('1000000')) 112 | await StabilityPool.connect(signers[1]).provideToSP(parseEther('2000000')) 113 | await StabilityPool.connect(signers[2]).provideToSP(parseEther('3000000')) 114 | await StabilityPool.connect(signers[3]).provideToSP(parseEther('4000000')) 115 | 116 | await MockPriceFeed.updatePrice(CollateralToken0.target, parseEther('88000')) 117 | await MockPriceFeed.updatePrice(CollateralToken1.target, parseUnits('65000', 28)) 118 | await MockPriceFeed.updatePrice(CollateralToken2.target, parseUnits('85000', 27)) 119 | const allTroves0 = await MultiTroveGetters.getTroves.staticCall(troveManager0.target, 0, 400) 120 | const allTroves1 = await MultiTroveGetters.getTroves.staticCall(troveManager1.target, 0, 400) 121 | const allTroves2 = await MultiTroveGetters.getTroves.staticCall(troveManager2.target, 0, 400) 122 | const liquidationIds0 = [] 123 | for (const trove of allTroves0.states) { 124 | if (trove.ICR < parseEther('1.1')) { 125 | liquidationIds0.push(trove.id) 126 | } 127 | } 128 | const liquidationIds1 = [] 129 | for (const trove of allTroves1.states) { 130 | if (trove.ICR < parseEther('1.1')) { 131 | liquidationIds1.push(trove.id) 132 | } 133 | } 134 | const liquidationIds2 = [] 135 | for (const trove of allTroves2.states) { 136 | if (trove.ICR < parseEther('1.1')) { 137 | liquidationIds2.push(trove.id) 138 | } 139 | } 140 | 141 | { 142 | await troveManager0.batchLiquidate(liquidationIds0.slice(0, 20)) 143 | const getCompoundedDebtDeposit0 = await StabilityPool.getCompoundedDebtDeposit(accounts[0]) 144 | // console.log('getCompoundedDebtDeposit0', getCompoundedDebtDeposit0, formatEther(getCompoundedDebtDeposit0)) 145 | const getCompoundedDebtDeposit1 = await StabilityPool.getCompoundedDebtDeposit(accounts[1]) 146 | // console.log('getCompoundedDebtDeposit1', getCompoundedDebtDeposit1, formatEther(getCompoundedDebtDeposit1)) 147 | const getCompoundedDebtDeposit2 = await StabilityPool.getCompoundedDebtDeposit(accounts[2]) 148 | // console.log('getCompoundedDebtDeposit2', getCompoundedDebtDeposit2, formatEther(getCompoundedDebtDeposit2)) 149 | const getCompoundedDebtDeposit3 = await StabilityPool.getCompoundedDebtDeposit(accounts[3]) 150 | // console.log('getCompoundedDebtDeposit3', getCompoundedDebtDeposit3, formatEther(getCompoundedDebtDeposit3)) 151 | const getDepositorCollateralGain0 = await StabilityPool.getDepositorCollateralGain(accounts[0]) 152 | // console.log('getDepositorCollateralGain0', getDepositorCollateralGain0) 153 | const getDepositorCollateralGain1 = await StabilityPool.getDepositorCollateralGain(accounts[1]) 154 | // console.log('getDepositorCollateralGain1', getDepositorCollateralGain1) 155 | const getDepositorCollateralGain2 = await StabilityPool.getDepositorCollateralGain(accounts[2]) 156 | // console.log('getDepositorCollateralGain2', getDepositorCollateralGain2) 157 | const getDepositorCollateralGain3 = await StabilityPool.getDepositorCollateralGain(accounts[3]) 158 | // console.log('getDepositorCollateralGain3', getDepositorCollateralGain3) 159 | await time.increase(365 * 24 * 3600) 160 | } 161 | { 162 | await troveManager1.batchLiquidate(liquidationIds1.slice(0, 20)) 163 | const getCompoundedDebtDeposit0 = await StabilityPool.getCompoundedDebtDeposit(accounts[0]) 164 | // console.log('getCompoundedDebtDeposit0', getCompoundedDebtDeposit0, formatEther(getCompoundedDebtDeposit0)) 165 | const getCompoundedDebtDeposit1 = await StabilityPool.getCompoundedDebtDeposit(accounts[1]) 166 | // console.log('getCompoundedDebtDeposit1', getCompoundedDebtDeposit1, formatEther(getCompoundedDebtDeposit1)) 167 | const getCompoundedDebtDeposit2 = await StabilityPool.getCompoundedDebtDeposit(accounts[2]) 168 | // console.log('getCompoundedDebtDeposit2', getCompoundedDebtDeposit2, formatEther(getCompoundedDebtDeposit2)) 169 | const getCompoundedDebtDeposit3 = await StabilityPool.getCompoundedDebtDeposit(accounts[3]) 170 | // console.log('getCompoundedDebtDeposit3', getCompoundedDebtDeposit3, formatEther(getCompoundedDebtDeposit3)) 171 | const getDepositorCollateralGain0 = await StabilityPool.getDepositorCollateralGain(accounts[0]) 172 | // console.log('getDepositorCollateralGain0', getDepositorCollateralGain0) 173 | const getDepositorCollateralGain1 = await StabilityPool.getDepositorCollateralGain(accounts[1]) 174 | // console.log('getDepositorCollateralGain1', getDepositorCollateralGain1) 175 | const getDepositorCollateralGain2 = await StabilityPool.getDepositorCollateralGain(accounts[2]) 176 | // console.log('getDepositorCollateralGain2', getDepositorCollateralGain2) 177 | const getDepositorCollateralGain3 = await StabilityPool.getDepositorCollateralGain(accounts[3]) 178 | // console.log('getDepositorCollateralGain3', getDepositorCollateralGain3) 179 | await time.increase(365 * 24 * 3600) 180 | 181 | } 182 | 183 | { 184 | // console.log('liquidationIds2', liquidationIds2) 185 | await troveManager2.batchLiquidate(liquidationIds2.slice(0, 20)) 186 | const getCompoundedDebtDeposit0 = await StabilityPool.getCompoundedDebtDeposit(accounts[0]) 187 | // console.log('getCompoundedDebtDeposit0', getCompoundedDebtDeposit0, formatEther(getCompoundedDebtDeposit0)) 188 | const getCompoundedDebtDeposit1 = await StabilityPool.getCompoundedDebtDeposit(accounts[1]) 189 | // console.log('getCompoundedDebtDeposit1', getCompoundedDebtDeposit1, formatEther(getCompoundedDebtDeposit1)) 190 | const getCompoundedDebtDeposit2 = await StabilityPool.getCompoundedDebtDeposit(accounts[2]) 191 | // console.log('getCompoundedDebtDeposit2', getCompoundedDebtDeposit2, formatEther(getCompoundedDebtDeposit2)) 192 | const getCompoundedDebtDeposit3 = await StabilityPool.getCompoundedDebtDeposit(accounts[3]) 193 | // console.log('getCompoundedDebtDeposit3', getCompoundedDebtDeposit3, formatEther(getCompoundedDebtDeposit3)) 194 | const getDepositorCollateralGain0 = await StabilityPool.getDepositorCollateralGain(accounts[0]) 195 | // console.log('getDepositorCollateralGain0', getDepositorCollateralGain0) 196 | const getDepositorCollateralGain1 = await StabilityPool.getDepositorCollateralGain(accounts[1]) 197 | // console.log('getDepositorCollateralGain1', getDepositorCollateralGain1) 198 | const getDepositorCollateralGain2 = await StabilityPool.getDepositorCollateralGain(accounts[2]) 199 | // console.log('getDepositorCollateralGain2', getDepositorCollateralGain2) 200 | const getDepositorCollateralGain3 = await StabilityPool.getDepositorCollateralGain(accounts[3]) 201 | // console.log('getDepositorCollateralGain3', getDepositorCollateralGain3) 202 | await time.increase(365 * 24 * 3600) 203 | 204 | } 205 | { 206 | const getCompoundedDebtDeposit0 = await StabilityPool.getCompoundedDebtDeposit(accounts[0]) 207 | await StabilityPool.connect(signers[0]).withdrawFromSP(getCompoundedDebtDeposit0) 208 | const getCompoundedDebtDeposit1 = await StabilityPool.getCompoundedDebtDeposit(accounts[1]) 209 | await StabilityPool.connect(signers[1]).withdrawFromSP(getCompoundedDebtDeposit1) 210 | const getCompoundedDebtDeposit2 = await StabilityPool.getCompoundedDebtDeposit(accounts[2]) 211 | await StabilityPool.connect(signers[2]).withdrawFromSP(getCompoundedDebtDeposit2) 212 | const getCompoundedDebtDeposit3 = await StabilityPool.getCompoundedDebtDeposit(accounts[3]) 213 | await StabilityPool.connect(signers[3]).withdrawFromSP(getCompoundedDebtDeposit3) 214 | const getDepositorCollateralGain0 = await StabilityPool.getDepositorCollateralGain(accounts[0]) 215 | // console.log('getDepositorCollateralGain0', getDepositorCollateralGain0) 216 | const getDepositorCollateralGain1 = await StabilityPool.getDepositorCollateralGain(accounts[1]) 217 | // console.log('getDepositorCollateralGain1', getDepositorCollateralGain1) 218 | const getDepositorCollateralGain2 = await StabilityPool.getDepositorCollateralGain(accounts[2]) 219 | // console.log('getDepositorCollateralGain2', getDepositorCollateralGain2) 220 | const getDepositorCollateralGain3 = await StabilityPool.getDepositorCollateralGain(accounts[3]) 221 | // console.log('getDepositorCollateralGain3', getDepositorCollateralGain3) 222 | await time.increase(365 * 24 * 3600) 223 | await StabilityPool.connect(signers[0]).claimAllCollateralGains(accounts[10]) 224 | await StabilityPool.connect(signers[1]).claimAllCollateralGains(accounts[11]) 225 | await StabilityPool.connect(signers[2]).claimAllCollateralGains(accounts[12]) 226 | await StabilityPool.connect(signers[3]).claimAllCollateralGains(accounts[13]) 227 | } 228 | const getYieldGains0 = await StabilityPool.getYieldGains(accounts[0]) 229 | // console.log('getYieldGains0', formatEther(getYieldGains0)) 230 | const getYieldGains1 = await StabilityPool.getYieldGains(accounts[1]) 231 | // console.log('getYieldGains1', formatEther(getYieldGains1)) 232 | const getYieldGains2 = await StabilityPool.getYieldGains(accounts[2]) 233 | // console.log('getYieldGains2', formatEther(getYieldGains2)) 234 | const getYieldGains3 = await StabilityPool.getYieldGains(accounts[3]) 235 | // console.log('getYieldGains3', formatEther(getYieldGains3)) 236 | await StabilityPool.connect(signers[0]).claimYield(accounts[10]) 237 | // console.log('yield0', formatEther(await DebtToken.balanceOf(accounts[10]))) 238 | await StabilityPool.connect(signers[1]).claimYield(accounts[11]) 239 | // console.log('yield1', formatEther(await DebtToken.balanceOf(accounts[11]))) 240 | await StabilityPool.connect(signers[2]).claimYield(accounts[12]) 241 | // console.log('yield2', formatEther(await DebtToken.balanceOf(accounts[12]))) 242 | await StabilityPool.connect(signers[3]).claimYield(accounts[13]) 243 | // console.log('yield3', formatEther(await DebtToken.balanceOf(accounts[13]))) 244 | // console.log('StabilityPool balance', formatEther(await DebtToken.balanceOf(StabilityPool.target))) 245 | 246 | }) 247 | 248 | }) -------------------------------------------------------------------------------- /contracts/core/StabilityPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 7 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 8 | import "@openzeppelin/contracts/utils/Multicall.sol"; 9 | 10 | import "../dependencies/YalaOwnable.sol"; 11 | import "../dependencies/YalaMath.sol"; 12 | import "../dependencies/EnumerableCollateral.sol"; 13 | import "../interfaces/IDebtToken.sol"; 14 | import "../interfaces/ITroveManager.sol"; 15 | import "../interfaces/IStabilityPool.sol"; 16 | 17 | /** 18 | @title Yala Stability Pool 19 | @notice Based on Liquity's `StabilityPool` 20 | https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol 21 | 22 | Yala's implementation is modified to support multiple collaterals. Deposits into 23 | the stability pool may be used to liquidate any supported collateral type. 24 | */ 25 | contract StabilityPool is IStabilityPool, Multicall, ReentrancyGuard, YalaOwnable { 26 | using SafeERC20 for IERC20; 27 | using EnumerableCollateral for EnumerableCollateral.TroveManagerToCollateral; 28 | 29 | uint256 public constant DECIMAL_PRECISION = 1e18; 30 | IDebtToken public immutable debtToken; 31 | address public immutable factory; 32 | uint256 internal totalDebtTokenDeposits; 33 | uint8 internal constant TARGET_DIGITS = 18; 34 | 35 | /* Product 'P': Running product by which to multiply an initial deposit, in order to find the current compounded deposit, 36 | * after a series of liquidations have occurred, each of which cancel some debt with the deposit. 37 | * 38 | * During its lifetime, a deposit's value evolves from d_t to d_t * P / P_t , where P_t 39 | * is the snapshot of P taken at the instant the deposit was made. 18 decimal. 40 | */ 41 | uint256 public P = DECIMAL_PRECISION; 42 | 43 | uint256 public constant SCALE_FACTOR = 1e9; 44 | 45 | // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 46 | uint128 public currentScale; 47 | 48 | // With each offset that fully empties the Pool, the epoch is incremented by 1 49 | uint128 public currentEpoch; 50 | 51 | uint256 public yieldGainsPending; 52 | uint256 public lastDebtLossErrorByP_Offset; 53 | uint256 public lastDebtLossError_TotalDeposits; 54 | uint256 public lastYieldError; 55 | 56 | EnumerableCollateral.TroveManagerToCollateral internal collateralTokens; 57 | 58 | // Error trackers for the error correction in the offset calculation 59 | mapping(IERC20 => uint256) public lastCollateralError_Offset; 60 | mapping(IERC20 => uint8) public collateralDecimals; 61 | mapping(address => AccountDeposit) public accountDeposits; // depositor address -> initial deposit 62 | mapping(address => Snapshots) public depositSnapshots; // depositor address -> snapshots struct 63 | 64 | // index values are mapped against the values within `collateralTokens` 65 | mapping(address => mapping(IERC20 => uint256)) public depositSums; // depositor address -> sums 66 | 67 | mapping(address => mapping(IERC20 => uint256)) public collateralGainsByDepositor; 68 | 69 | mapping(address => uint256) public storedPendingYield; 70 | 71 | /* collateral Gain sum 'S': During its lifetime, each deposit d_t earns a collateral gain of ( d_t * [S - S_t] )/P_t, where S_t 72 | * is the depositor's snapshot of S taken at the time t when the deposit was made. 73 | * 74 | * The 'S' sums are stored in a nested mapping (epoch => scale => sum): 75 | * 76 | * - The inner mapping records the sum S at different scales 77 | * - The outer mapping records the (scale => sum) mappings, for different epochs. 78 | */ 79 | mapping(uint128 => mapping(uint128 => mapping(IERC20 => uint256))) public epochToScaleToSums; 80 | 81 | /* 82 | * Similarly, the sum 'G' is used to calculate yield gains. During it's lifetime, each deposit d_t earns a yield gain of 83 | * ( d_t * [G - G_t] )/P_t, where G_t is the depositor's snapshot of G taken at time t when the deposit was made. 84 | * 85 | * Yala reward events occur are triggered by depositor operations (new deposit, topup, withdrawal), and liquidations. 86 | * In each case, the Yala reward is issued (i.e. G is updated), before other state changes are made. 87 | */ 88 | mapping(uint128 => mapping(uint128 => uint256)) public epochToScaleToG; 89 | 90 | constructor(address _yalaCore, address _factory, IDebtToken _debtTokenAddress) YalaOwnable(_yalaCore) { 91 | factory = _factory; 92 | debtToken = _debtTokenAddress; 93 | } 94 | 95 | function enableTroveManager(address troveManager) external { 96 | require(msg.sender == factory, "StabilityPool: Not factory"); 97 | IERC20 token = ITroveManager(troveManager).collateralToken(); 98 | collateralTokens.set(troveManager, token); 99 | collateralDecimals[token] = IERC20Metadata(address(token)).decimals(); 100 | } 101 | 102 | function provideToSP(uint256 _amount) external nonReentrant { 103 | require(!YALA_CORE.paused(), "StabilityPool: Deposits are paused"); 104 | require(_amount > 0, "StabilityPool: Amount must be non-zero"); 105 | uint256 initialDeposit = accountDeposits[msg.sender].amount; 106 | _accrueRewards(msg.sender, initialDeposit); 107 | uint256 compoundedDebtDeposit = _getCompoundedDebtDeposit(msg.sender, initialDeposit); 108 | debtToken.sendToSP(msg.sender, _amount); 109 | uint256 newTotalDebtTokenDeposits = totalDebtTokenDeposits + _amount; 110 | totalDebtTokenDeposits = newTotalDebtTokenDeposits; 111 | emit StabilityPoolDebtBalanceUpdated(newTotalDebtTokenDeposits); 112 | 113 | uint256 newDeposit = compoundedDebtDeposit + _amount; 114 | accountDeposits[msg.sender] = AccountDeposit({ amount: uint128(newDeposit), timestamp: uint128(block.timestamp) }); 115 | _updateSnapshots(msg.sender, newDeposit); 116 | emit Deposit(msg.sender, newDeposit, _amount); 117 | } 118 | 119 | function withdrawFromSP(uint256 _amount) external nonReentrant { 120 | uint256 initialDeposit = accountDeposits[msg.sender].amount; 121 | uint128 depositTimestamp = accountDeposits[msg.sender].timestamp; 122 | require(initialDeposit > 0, "StabilityPool: User must have a non-zero deposit"); 123 | require(depositTimestamp < block.timestamp, "StabilityPool: !Deposit and withdraw same block"); 124 | _accrueRewards(msg.sender, initialDeposit); 125 | uint256 compoundedDebtDeposit = YalaMath._min(totalDebtTokenDeposits, _getCompoundedDebtDeposit(msg.sender, initialDeposit)); 126 | uint256 debtToWithdraw = YalaMath._min(_amount, compoundedDebtDeposit); 127 | uint256 newDeposit = compoundedDebtDeposit - debtToWithdraw; 128 | accountDeposits[msg.sender] = AccountDeposit({ amount: uint128(newDeposit), timestamp: depositTimestamp }); 129 | if (debtToWithdraw > 0) { 130 | debtToWithdraw = YalaMath._min(debtToken.balanceOf(address(this)), debtToWithdraw); 131 | debtToken.returnFromPool(address(this), msg.sender, debtToWithdraw); 132 | _decreaseDebt(debtToWithdraw); 133 | } 134 | _updateSnapshots(msg.sender, newDeposit); 135 | emit Withdraw(msg.sender, newDeposit, debtToWithdraw); 136 | } 137 | 138 | /* 139 | * Cancels out the specified debt against the Debt contained in the Stability Pool (as far as possible) 140 | */ 141 | function offset(uint256 _debtToOffset, uint256 _collToAdd) external { 142 | IERC20 collateral = collateralTokens.get(msg.sender); 143 | require(address(collateral) != address(0), "StabilityPool: nonexistent collateral"); 144 | _accrueAllYieldGains(); 145 | _collToAdd = _computeCollateralAmount(collateral, _collToAdd); 146 | _offset(collateral, _debtToOffset, _collToAdd); 147 | } 148 | 149 | function _offset(IERC20 collateral, uint256 _debtToOffset, uint256 _collToAdd) internal { 150 | uint256 totalDebt = totalDebtTokenDeposits; // cached to save an SLOAD 151 | if (totalDebt == 0 || _debtToOffset == 0) { 152 | return; 153 | } 154 | _updateCollRewardSumAndProduct(collateral, _collToAdd, _debtToOffset, totalDebt); // updates S and P 155 | // Cancel the liquidated Debt debt with the Debt in the stability pool 156 | _decreaseDebt(_debtToOffset); 157 | } 158 | 159 | function _computeCollRewardsPerUnitStaked(IERC20 collateral, uint256 _collToAdd, uint256 _debtToOffset, uint256 _totalDebtDeposits) internal returns (uint256 collGainPerUnitStaked, uint256 debtLossPerUnitStaked, uint256 newLastDebtLossErrorOffset) { 160 | /* 161 | * Compute the Debt and Coll rewards. Uses a "feedback" error correction, to keep 162 | * the cumulative error in the P and S state variables low: 163 | * 164 | * 1) Form numerators which compensate for the floor division errors that occurred the last time this 165 | * function was called. 166 | * 2) Calculate "per-unit-staked" ratios. 167 | * 3) Multiply each ratio back by its denominator, to reveal the current floor division error. 168 | * 4) Store these errors for use in the next correction when this function is called. 169 | * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. 170 | */ 171 | uint256 collNumerator = _collToAdd * DECIMAL_PRECISION + lastCollateralError_Offset[collateral]; 172 | 173 | assert(_debtToOffset <= _totalDebtDeposits); 174 | if (_debtToOffset == _totalDebtDeposits) { 175 | debtLossPerUnitStaked = DECIMAL_PRECISION; // When the Pool depletes to 0, so does each deposit 176 | newLastDebtLossErrorOffset = 0; 177 | } else { 178 | uint256 debtLossNumerator = _debtToOffset * DECIMAL_PRECISION; 179 | /* 180 | * Add 1 to make error in quotient positive. We want "slightly too much" Debt loss, 181 | * which ensures the error in any given compoundedDebtDeposit favors the Stability Pool. 182 | */ 183 | debtLossPerUnitStaked = debtLossNumerator / _totalDebtDeposits + 1; 184 | newLastDebtLossErrorOffset = debtLossPerUnitStaked * _totalDebtDeposits - debtLossNumerator; 185 | } 186 | 187 | collGainPerUnitStaked = collNumerator / _totalDebtDeposits; 188 | lastCollateralError_Offset[collateral] = collNumerator - collGainPerUnitStaked * _totalDebtDeposits; 189 | return (collGainPerUnitStaked, debtLossPerUnitStaked, newLastDebtLossErrorOffset); 190 | } 191 | 192 | // Update the Stability Pool reward sum S and product P 193 | function _updateCollRewardSumAndProduct(IERC20 collateral, uint256 _collToAdd, uint256 _debtToOffset, uint256 _totalDeposits) internal { 194 | (uint256 collGainPerUnitStaked, uint256 debtLossPerUnitStaked, uint256 newLastDebtLossErrorOffset) = _computeCollRewardsPerUnitStaked(collateral, _collToAdd, _debtToOffset, _totalDeposits); 195 | 196 | uint256 currentP = P; 197 | uint256 newP; 198 | 199 | assert(debtLossPerUnitStaked <= DECIMAL_PRECISION); 200 | /* 201 | * The newProductFactor is the factor by which to change all deposits, due to the depletion of Stability Pool Debt in the liquidation. 202 | * We make the product factor 0 if there was a pool-emptying. Otherwise, it is (1 - debtLossPerUnitStaked) 203 | */ 204 | uint256 newProductFactor = uint256(DECIMAL_PRECISION) - debtLossPerUnitStaked; 205 | 206 | uint128 currentScaleCached = currentScale; 207 | uint128 currentEpochCached = currentEpoch; 208 | uint256 currentS = epochToScaleToSums[currentEpochCached][currentScaleCached][collateral]; 209 | 210 | /* 211 | * Calculate the new S first, before we update P. 212 | * The Coll gain for any given depositor from a liquidation depends on the value of their deposit 213 | * (and the value of totalDeposits) prior to the Stability being depleted by the debt in the liquidation. 214 | * 215 | * Since S corresponds to Coll gain, and P to deposit loss, we update S first. 216 | */ 217 | IERC20 collateralCached = collateral; 218 | uint256 marginalCollGain = collGainPerUnitStaked * (currentP - 1); 219 | uint256 newS = currentS + marginalCollGain; 220 | epochToScaleToSums[currentEpochCached][currentScaleCached][collateralCached] = newS; 221 | emit S_Updated(collateralCached, newS, currentEpochCached, currentScaleCached); 222 | 223 | // If the Stability Pool was emptied, increment the epoch, and reset the scale and product P 224 | if (newProductFactor == 0) { 225 | currentEpoch = currentEpochCached + 1; 226 | emit EpochUpdated(currentEpoch); 227 | currentScale = 0; 228 | emit ScaleUpdated(currentScale); 229 | newP = DECIMAL_PRECISION; 230 | } else { 231 | uint256 lastDebtLossErrorByP_Offset_Cached = lastDebtLossErrorByP_Offset; 232 | uint256 lastDebtLossError_TotalDeposits_Cached = lastDebtLossError_TotalDeposits; 233 | newP = _getNewPByScale(currentP, newProductFactor, lastDebtLossErrorByP_Offset_Cached, lastDebtLossError_TotalDeposits_Cached, 1); 234 | 235 | // If multiplying P by a non-zero product factor would reduce P below the scale boundary, increment the scale 236 | if (newP < SCALE_FACTOR) { 237 | newP = _getNewPByScale(currentP, newProductFactor, lastDebtLossErrorByP_Offset_Cached, lastDebtLossError_TotalDeposits_Cached, SCALE_FACTOR); 238 | currentScale = currentScaleCached + 1; 239 | 240 | // Increment the scale again if it's still below the boundary. This ensures the invariant P >= 1e9 holds and 241 | // addresses this issue from Liquity v1: https://github.com/liquity/dev/security/advisories/GHSA-m9f3-hrx8-x2g3 242 | if (newP < SCALE_FACTOR) { 243 | newP = _getNewPByScale(currentP, newProductFactor, lastDebtLossErrorByP_Offset_Cached, lastDebtLossError_TotalDeposits_Cached, SCALE_FACTOR * SCALE_FACTOR); 244 | currentScale = currentScaleCached + 2; 245 | } 246 | emit ScaleUpdated(currentScale); 247 | } 248 | // If there's no scale change and no pool-emptying, just do a standard multiplication 249 | } 250 | lastDebtLossErrorByP_Offset = currentP * newLastDebtLossErrorOffset; 251 | lastDebtLossError_TotalDeposits = _totalDeposits; 252 | 253 | assert(newP > 0); 254 | P = newP; 255 | 256 | emit P_Updated(newP); 257 | } 258 | 259 | function _getNewPByScale(uint256 _currentP, uint256 _newProductFactor, uint256 _lastDebtLossErrorByP_Offset, uint256 _lastDebtLossError_TotalDeposits, uint256 _scale) internal pure returns (uint256) { 260 | uint256 errorFactor; 261 | if (_lastDebtLossErrorByP_Offset > 0) { 262 | errorFactor = (_lastDebtLossErrorByP_Offset * _newProductFactor * _scale) / _lastDebtLossError_TotalDeposits / DECIMAL_PRECISION; 263 | } 264 | return (_currentP * _newProductFactor * _scale + errorFactor) / DECIMAL_PRECISION; 265 | } 266 | 267 | function _decreaseDebt(uint256 _amount) internal { 268 | uint256 newTotalDebtTokenDeposits = totalDebtTokenDeposits - _amount; 269 | totalDebtTokenDeposits = newTotalDebtTokenDeposits; 270 | emit StabilityPoolDebtBalanceUpdated(newTotalDebtTokenDeposits); 271 | } 272 | 273 | /* Calculates the collateral gain earned by the deposit since its last snapshots were taken. 274 | * Given by the formula: E = d0 * (S - S(0))/P(0) 275 | * where S(0) and P(0) are the depositor's snapshots of the sum S and product P, respectively. 276 | * d0 is the last recorded deposit value. 277 | */ 278 | function getDepositorCollateralGain(address _depositor) external view returns (IERC20[] memory collaterals, uint256[] memory collateralGains) { 279 | uint256 length = collateralTokens.length(); 280 | collaterals = new IERC20[](length); 281 | collateralGains = new uint256[](length); 282 | uint256 P_Snapshot = depositSnapshots[_depositor].P; 283 | if (P_Snapshot == 0) { 284 | for (uint256 i = 0; i < length; i++) { 285 | (, IERC20 collateral) = collateralTokens.at(i); 286 | collaterals[i] = collateral; 287 | collateralGains[i] = collateralGainsByDepositor[_depositor][collateral]; 288 | } 289 | return (collaterals, collateralGains); 290 | } 291 | uint256 initialDeposit = accountDeposits[_depositor].amount; 292 | uint128 epochSnapshot = depositSnapshots[_depositor].epoch; 293 | uint128 scaleSnapshot = depositSnapshots[_depositor].scale; 294 | for (uint256 i = 0; i < length; i++) { 295 | (, IERC20 collateral) = collateralTokens.at(i); 296 | collaterals[i] = collateral; 297 | collateralGains[i] = collateralGainsByDepositor[_depositor][collateral]; 298 | uint256 sum = epochToScaleToSums[epochSnapshot][scaleSnapshot][collateral]; 299 | uint256 nextSum = epochToScaleToSums[epochSnapshot][scaleSnapshot + 1][collateral]; 300 | uint256 depSum = depositSums[_depositor][collateral]; 301 | if (sum == 0) continue; 302 | uint256 firstPortion = sum - depSum; 303 | uint256 secondPortion = nextSum / SCALE_FACTOR; 304 | collateralGains[i] += (initialDeposit * (firstPortion + secondPortion)) / P_Snapshot / DECIMAL_PRECISION; 305 | } 306 | return (collaterals, collateralGains); 307 | } 308 | 309 | /* 310 | * Calculate the yield gain earned by a deposit since its last snapshots were taken. 311 | * Given by the formula: Yield = d0 * (G - G(0))/P(0) 312 | * where G(0) and P(0) are the depositor's snapshots of the sum G and product P, respectively. 313 | * d0 is the last recorded deposit value. 314 | */ 315 | function getYieldGains(address _depositor) external view returns (uint256 gains) { 316 | uint256 initialDeposit = accountDeposits[_depositor].amount; 317 | if (initialDeposit == 0) { 318 | return storedPendingYield[_depositor]; 319 | } 320 | uint256 pendingYield = yieldGainsPending; 321 | uint256 length = collateralTokens.length(); 322 | for (uint256 i = 0; i < length; i++) { 323 | (address troveManager, ) = collateralTokens.at(i); 324 | uint256 yieldSP = ITroveManager(troveManager).getPendingYieldSP(); 325 | pendingYield += yieldSP; 326 | } 327 | uint256 firstPortionPending; 328 | uint256 secondPortionPending; 329 | Snapshots memory snapshots = depositSnapshots[_depositor]; 330 | if (pendingYield > 0 && snapshots.epoch == currentEpoch && totalDebtTokenDeposits >= DECIMAL_PRECISION) { 331 | uint256 yieldNumerator = pendingYield * DECIMAL_PRECISION + lastYieldError; 332 | uint256 yieldPerUnitStaked = yieldNumerator / totalDebtTokenDeposits; 333 | uint256 marginalYieldGain = yieldPerUnitStaked * (P - 1); 334 | if (currentScale == snapshots.scale) firstPortionPending = marginalYieldGain; 335 | else if (currentScale == snapshots.scale + 1) secondPortionPending = marginalYieldGain; 336 | } 337 | uint256 firstPortion = epochToScaleToG[snapshots.epoch][snapshots.scale] + firstPortionPending - snapshots.G; 338 | uint256 secondPortion = (epochToScaleToG[snapshots.epoch][snapshots.scale + 1] + secondPortionPending) / SCALE_FACTOR; 339 | gains = storedPendingYield[_depositor] + (initialDeposit * (firstPortion + secondPortion)) / snapshots.P / DECIMAL_PRECISION; 340 | } 341 | 342 | // --- Sender functions for Debt deposit, collateral gains and Yala gains --- 343 | function claimAllCollateralGains(address recipient) external nonReentrant { 344 | uint256 initialDeposit = accountDeposits[msg.sender].amount; 345 | uint128 depositTimestamp = accountDeposits[msg.sender].timestamp; 346 | require(depositTimestamp < block.timestamp, "StabilityPool: !Deposit and claim collateral gains same block"); 347 | _accrueRewards(msg.sender, initialDeposit); 348 | uint256 newDeposit = _getCompoundedDebtDeposit(msg.sender, initialDeposit); 349 | accountDeposits[msg.sender].amount = uint128(newDeposit); 350 | _updateSnapshots(msg.sender, newDeposit); 351 | uint256 length = collateralTokens.length(); 352 | for (uint256 i = 0; i < length; i++) { 353 | (, IERC20 collateral) = collateralTokens.at(i); 354 | uint256 gains = collateralGainsByDepositor[msg.sender][collateral]; 355 | if (gains > 0) { 356 | collateralGainsByDepositor[msg.sender][collateral] = 0; 357 | gains = YalaMath._min(collateral.balanceOf(address(this)), _computeCollateralWithdrawable(collateral, gains)); 358 | collateral.safeTransfer(recipient, gains); 359 | emit CollateralGainWithdrawn(msg.sender, collateral, gains); 360 | } 361 | } 362 | } 363 | 364 | function claimYield(address recipient) external nonReentrant returns (uint256 amount) { 365 | uint256 initialDeposit = accountDeposits[msg.sender].amount; 366 | _accrueRewards(msg.sender, initialDeposit); 367 | uint256 newDeposit = _getCompoundedDebtDeposit(msg.sender, initialDeposit); 368 | accountDeposits[msg.sender].amount = uint128(newDeposit); 369 | _updateSnapshots(msg.sender, newDeposit); 370 | amount = storedPendingYield[msg.sender]; 371 | if (amount > 0) { 372 | storedPendingYield[msg.sender] = 0; 373 | amount = YalaMath._min(debtToken.balanceOf(address(this)), amount); 374 | debtToken.returnFromPool(address(this), recipient, amount); 375 | emit YieldClaimed(msg.sender, recipient, amount); 376 | } 377 | return amount; 378 | } 379 | 380 | function _computeCollateralAmount(IERC20 collateral, uint256 amount) internal view returns (uint256) { 381 | uint8 decimals = collateralDecimals[collateral]; 382 | if (decimals <= TARGET_DIGITS) { 383 | return amount * (10 ** (TARGET_DIGITS - decimals)); 384 | } 385 | return amount / (10 ** (decimals - TARGET_DIGITS)); 386 | } 387 | 388 | function _computeCollateralWithdrawable(IERC20 collateral, uint256 amount) internal view returns (uint256) { 389 | uint8 decimals = collateralDecimals[collateral]; 390 | if (decimals <= TARGET_DIGITS) { 391 | return amount / (10 ** (TARGET_DIGITS - decimals)); 392 | } 393 | return amount * (10 ** (decimals - TARGET_DIGITS)); 394 | } 395 | 396 | function _accrueRewards(address depositor, uint256 initialDeposit) internal { 397 | _accrueAllYieldGains(); 398 | _accrueDepositorYield(depositor, initialDeposit); 399 | _accrueDepositorCollateralGains(depositor, initialDeposit); 400 | } 401 | 402 | function triggerSPYield(uint256 _yield) external { 403 | require(collateralTokens.contains(msg.sender), "StabilityPool: Nonexistent TM"); 404 | uint256 accumulatedYieldGains = yieldGainsPending + _yield; 405 | if (accumulatedYieldGains == 0) return; 406 | if (totalDebtTokenDeposits < DECIMAL_PRECISION) { 407 | yieldGainsPending = accumulatedYieldGains; 408 | return; 409 | } 410 | yieldGainsPending = 0; 411 | uint256 yieldNumerator = accumulatedYieldGains * DECIMAL_PRECISION + lastYieldError; 412 | uint256 yieldPerUnitStaked = yieldNumerator / totalDebtTokenDeposits; 413 | lastYieldError = yieldNumerator - yieldPerUnitStaked * totalDebtTokenDeposits; 414 | uint256 marginalYieldGain = yieldPerUnitStaked * (P - 1); 415 | epochToScaleToG[currentEpoch][currentScale] = epochToScaleToG[currentEpoch][currentScale] + marginalYieldGain; 416 | emit G_Updated(epochToScaleToG[currentEpoch][currentScale], currentEpoch, currentScale); 417 | } 418 | 419 | function _accrueAllYieldGains() internal { 420 | uint256 length = collateralTokens.length(); 421 | for (uint256 i = 0; i < length; i++) { 422 | (address troveManager, ) = collateralTokens.at(i); 423 | // trigger SP yiled 424 | ITroveManager(troveManager).accrueInterests(); 425 | } 426 | } 427 | 428 | function _accrueDepositorYield(address account, uint256 initialDeposit) internal { 429 | if (initialDeposit == 0) { 430 | return; 431 | } 432 | Snapshots memory snapshots = depositSnapshots[account]; 433 | uint128 epochSnapshot = snapshots.epoch; 434 | uint128 scaleSnapshot = snapshots.scale; 435 | uint256 G_Snapshot = snapshots.G; 436 | uint256 P_Snapshot = snapshots.P; 437 | uint256 firstPortion = epochToScaleToG[epochSnapshot][scaleSnapshot] - G_Snapshot; 438 | uint256 secondPortion = epochToScaleToG[epochSnapshot][scaleSnapshot + 1] / SCALE_FACTOR; 439 | uint256 amount = (initialDeposit * (firstPortion + secondPortion)) / P_Snapshot / DECIMAL_PRECISION; 440 | storedPendingYield[account] += amount; 441 | } 442 | 443 | function _accrueDepositorCollateralGains(address _depositor, uint256 initialDeposit) internal { 444 | if (initialDeposit == 0) { 445 | return; 446 | } 447 | uint256 length = collateralTokens.length(); 448 | address depositor = _depositor; 449 | uint256 deposit = initialDeposit; 450 | for (uint256 i = 0; i < length; i++) { 451 | (, IERC20 collateral) = collateralTokens.at(i); 452 | uint128 epochSnapshot = depositSnapshots[depositor].epoch; 453 | uint128 scaleSnapshot = depositSnapshots[depositor].scale; 454 | uint256 P_Snapshot = depositSnapshots[depositor].P; 455 | uint256 sum = epochToScaleToSums[epochSnapshot][scaleSnapshot][collateral]; 456 | uint256 nextSum = epochToScaleToSums[epochSnapshot][scaleSnapshot + 1][collateral]; 457 | uint256 depSum = depositSums[depositor][collateral]; 458 | if (sum == 0 || sum == depSum) return; 459 | uint256 firstPortion = sum - depSum; 460 | uint256 secondPortion = nextSum / SCALE_FACTOR; 461 | uint256 gains = (deposit * (firstPortion + secondPortion)) / P_Snapshot / DECIMAL_PRECISION; 462 | collateralGainsByDepositor[depositor][collateral] += gains; 463 | } 464 | } 465 | 466 | function getCompoundedDebtDeposit(address _depositor) public view returns (uint256) { 467 | return _getCompoundedDebtDeposit(_depositor, accountDeposits[_depositor].amount); 468 | } 469 | 470 | function _getCompoundedDebtDeposit(address _depositor, uint256 initialDeposit) internal view returns (uint256) { 471 | if (initialDeposit == 0) { 472 | return 0; 473 | } 474 | Snapshots memory snapshots = depositSnapshots[_depositor]; 475 | uint256 snapshot_P = snapshots.P; 476 | uint128 scaleSnapshot = snapshots.scale; 477 | uint128 epochSnapshot = snapshots.epoch; 478 | // If stake was made before a pool-emptying event, then it has been fully cancelled with debt -- so, return 0 479 | if (epochSnapshot < currentEpoch) { 480 | return 0; 481 | } 482 | uint256 compoundedStake; 483 | uint128 scaleDiff = currentScale - scaleSnapshot; 484 | uint256 cachedP = P; 485 | uint256 currentPToUse = cachedP != snapshot_P ? cachedP - 1 : cachedP; 486 | /* Compute the compounded stake. If a scale change in P was made during the stake's lifetime, 487 | * account for it. If more than one scale change was made, then the stake has decreased by a factor of 488 | * at least 1e-9 -- so return 0. 489 | */ 490 | if (scaleDiff == 0) { 491 | compoundedStake = (initialDeposit * currentPToUse) / snapshot_P; 492 | } else if (scaleDiff == 1) { 493 | compoundedStake = (initialDeposit * currentPToUse) / snapshot_P / SCALE_FACTOR; 494 | } else { 495 | compoundedStake = 0; 496 | } 497 | 498 | if (compoundedStake < initialDeposit / 1e9) { 499 | return 0; 500 | } 501 | 502 | return compoundedStake; 503 | } 504 | 505 | function _computeYieldPerUnitStaked(uint256 _yield, uint256 _totalDebtTokenDeposits) internal returns (uint256) { 506 | /* 507 | * Calculate the Yala-per-unit staked. Division uses a "feedback" error correction, to keep the 508 | * cumulative error low in the running total G: 509 | * 510 | * 1) Form a numerator which compensates for the floor division error that occurred the last time this 511 | * function was called. 512 | * 2) Calculate "per-unit-staked" ratio. 513 | * 3) Multiply the ratio back by its denominator, to reveal the current floor division error. 514 | * 4) Store this error for use in the next correction when this function is called. 515 | * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. 516 | */ 517 | uint256 yieldNumerator = (_yield * DECIMAL_PRECISION) + lastYieldError; 518 | uint256 yieldPerUnitStaked = yieldNumerator / _totalDebtTokenDeposits; 519 | lastYieldError = yieldNumerator - (yieldPerUnitStaked * _totalDebtTokenDeposits); 520 | return yieldPerUnitStaked; 521 | } 522 | 523 | function _updateSnapshots(address _depositor, uint256 _newValue) internal { 524 | uint256 length = collateralTokens.length(); 525 | if (_newValue == 0) { 526 | delete depositSnapshots[_depositor]; 527 | for (uint256 i = 0; i < length; i++) { 528 | (, IERC20 collateral) = collateralTokens.at(i); 529 | depositSums[_depositor][collateral] = 0; 530 | } 531 | emit DepositSnapshotUpdated(_depositor, 0, 0); 532 | return; 533 | } 534 | uint128 currentScaleCached = currentScale; 535 | uint128 currentEpochCached = currentEpoch; 536 | uint256 currentG = epochToScaleToG[currentEpochCached][currentScaleCached]; 537 | uint256 currentP = P; 538 | // Record new snapshots of the latest running product P, sum S, and sum G, for the depositor 539 | depositSnapshots[_depositor].P = currentP; 540 | depositSnapshots[_depositor].G = currentG; 541 | depositSnapshots[_depositor].scale = currentScaleCached; 542 | depositSnapshots[_depositor].epoch = currentEpochCached; 543 | for (uint256 i = 0; i < length; i++) { 544 | (, IERC20 collateral) = collateralTokens.at(i); 545 | // Get S and G for the current epoch and current scale 546 | uint256 currentS = epochToScaleToSums[currentEpochCached][currentScaleCached][collateral]; 547 | depositSums[_depositor][collateral] = currentS; 548 | } 549 | emit DepositSnapshotUpdated(_depositor, currentP, currentG); 550 | } 551 | 552 | function getTotalDeposits() external view returns (uint256) { 553 | return totalDebtTokenDeposits; 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /contracts/core/TroveManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.28; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/utils/Multicall.sol"; 7 | import "@openzeppelin/contracts/utils/math/Math.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 9 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 10 | 11 | import "../interfaces/ITroveManager.sol"; 12 | import "../interfaces/IStabilityPool.sol"; 13 | import "../interfaces/IDebtToken.sol"; 14 | import "../interfaces/IPriceFeed.sol"; 15 | 16 | import "../dependencies/YalaBase.sol"; 17 | import "../dependencies/YalaMath.sol"; 18 | import "../dependencies/YalaOwnable.sol"; 19 | 20 | /** 21 | @title Yala Trove Manager 22 | @notice Based on Liquity's `TroveManager` 23 | https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol 24 | */ 25 | contract TroveManager is ITroveManager, Multicall, ERC721Enumerable, YalaBase, YalaOwnable { 26 | using SafeERC20 for IERC20; 27 | 28 | // --- Connected contract declarations --- 29 | address public immutable factoryAddress; 30 | address public immutable borrowerOperationsAddress; 31 | address public immutable gasPoolAddress; 32 | IDebtToken public immutable debtToken; 33 | IStabilityPool public immutable stabilityPool; 34 | IPriceFeed public priceFeed; 35 | IERC20 public collateralToken; 36 | IMetadataNFT public metadataNFT; 37 | uint256 public shutdownAt; 38 | 39 | uint256 public MCR; 40 | uint256 public SCR; 41 | uint256 public CCR; 42 | uint256 public maxSystemDebt; 43 | uint256 public interestRate; 44 | uint256 public SP_YIELD_PCT; 45 | uint256 public MAX_COLL_GAS_COMPENSATION; 46 | uint256 public LIQUIDATION_PENALTY_SP; 47 | uint256 public LIQUIDATION_PENALTY_REDISTRIBUTION; 48 | uint256 public constant INTEREST_PRECISION = 1e18; 49 | uint256 public constant SECONDS_IN_YEAR = 365 days; 50 | uint256 public constant COLL_GAS_COMPENSATION_DIVISOR = 200; // 0.5% 51 | bool public paused; 52 | 53 | uint256 public totalStakes; 54 | 55 | // Snapshot of the value of totalStakes, taken immediately after the latest liquidation 56 | uint256 public totalStakesSnapshot; 57 | 58 | // Snapshot of the total collateral taken immediately after the latest liquidation. 59 | uint256 public totalCollateralSnapshot; 60 | 61 | /* 62 | * L_collateral and L_debt track the sums of accumulated liquidation rewards per unit staked. During its lifetime, each stake earns: 63 | * 64 | * An collateral gain of ( stake * [L_collateral - L_collateral(0)] ) 65 | * A debt increase of ( stake * [L_debt - L_debt(0)] ) 66 | * A defaulted interest increase of ( stake * [L_defaulted_interest - L_defaulted_interest(0)] ) 67 | * Where L_collateral(0) and L_debt(0) are snapshots of L_collateral and L_debt for the active Trove taken at the instant the stake was made 68 | */ 69 | uint256 public L_collateral; 70 | uint256 public L_debt; 71 | uint256 public L_defaulted_interest; 72 | /** 73 | * A interest increase of ( debt * [L_active_interest - L_active_interest(0)] ) 74 | */ 75 | uint256 public L_active_interest; 76 | 77 | // Error trackers for the trove redistribution calculation 78 | uint256 public lastCollateralError_Redistribution; 79 | uint256 public lastDebtError_Redistribution; 80 | uint256 public lastActiveInterestError_Redistribution; 81 | uint256 public lastDefaultedInterestError_Redistribution; 82 | 83 | uint256 public totalActiveCollateral; 84 | uint256 public totalActiveDebt; 85 | uint256 public totalActiveInterest; 86 | uint256 public defaultedCollateral; 87 | uint256 public defaultedDebt; 88 | uint256 public defaultedInterest; 89 | uint256 public lastInterestUpdate; 90 | 91 | mapping(uint256 => Trove) public Troves; 92 | 93 | // Map trove id with active troves to their RewardSnapshot 94 | mapping(uint256 => RewardSnapshot) public rewardSnapshots; 95 | 96 | mapping(address => uint256) public accountCollSurplus; 97 | 98 | uint256 public nonce; 99 | 100 | modifier whenNotPaused() { 101 | require(!paused, "TM: Collateral Paused"); 102 | _; 103 | } 104 | 105 | modifier whenNotShutdown() { 106 | require(!hasShutdown(), "TM: Collateral Shutdown"); 107 | _; 108 | } 109 | 110 | constructor( 111 | address _yalaCore, 112 | address _factoryAddress, 113 | address _gasPoolAddress, 114 | IDebtToken _debtToken, 115 | address _borrowerOperationsAddress, 116 | IStabilityPool _stabilityPool, 117 | uint256 _gasCompensation, 118 | uint256 _maxCollGasCompensation 119 | ) ERC721("Yala NFT", "Yala") YalaOwnable(_yalaCore) YalaBase(_gasCompensation) { 120 | factoryAddress = _factoryAddress; 121 | gasPoolAddress = _gasPoolAddress; 122 | debtToken = _debtToken; 123 | borrowerOperationsAddress = _borrowerOperationsAddress; 124 | stabilityPool = _stabilityPool; 125 | MAX_COLL_GAS_COMPENSATION = _maxCollGasCompensation; 126 | } 127 | 128 | function setParameters(IPriceFeed _priceFeed, IERC20 _collateral, DeploymentParams memory params) external { 129 | require(address(collateralToken) == address(0) && msg.sender == factoryAddress, "TM: parameters set"); 130 | require(params.interestRate < DECIMAL_PRECISION, "TM: interest rate too high"); 131 | require(params.maxDebt > 0, "TM: interest rate too high"); 132 | require(params.spYieldPCT <= DECIMAL_PRECISION, "TM: sp yield pct too high"); 133 | require(params.MCR > DECIMAL_PRECISION && params.SCR >= params.MCR && params.CCR >= params.SCR, "TM: invalid cr parameters"); 134 | collateralToken = _collateral; 135 | _setPriceFeed(_priceFeed); 136 | _setMetadataNFT(params.metadataNFT); 137 | _setInterestRate(params.interestRate); 138 | _setMaxSystemDebt(params.maxDebt); 139 | _setSPYielPCT(params.spYieldPCT); 140 | _setMaxCollGasCompensation(params.maxCollGasCompensation); 141 | _setLiquidationPenaltySP(params.liquidationPenaltySP); 142 | _setLiquidationPenaltyRedist(params.liquidationPenaltyRedistribution); 143 | MCR = params.MCR; 144 | SCR = params.SCR; 145 | CCR = params.CCR; 146 | emit CRUpdated(MCR, SCR, CCR); 147 | } 148 | 149 | function shutdown() external whenNotShutdown { 150 | require(msg.sender == owner() || getTCR() < SCR, "TM: Not allowed"); 151 | shutdownAt = block.timestamp; 152 | accrueInterests(); 153 | maxSystemDebt = 0; 154 | emit ShutDown(); 155 | } 156 | 157 | function setPaused(bool _paused) external { 158 | require((_paused && msg.sender == guardian()) || msg.sender == owner(), "TM: Unauthorized"); 159 | _setPaused(_paused); 160 | } 161 | 162 | function _setPaused(bool _paused) internal { 163 | paused = _paused; 164 | emit PauseUpdated(_paused); 165 | } 166 | 167 | function setPriceFeed(IPriceFeed _priceFeed) external onlyOwner { 168 | _setPriceFeed(_priceFeed); 169 | } 170 | 171 | function _setPriceFeed(IPriceFeed _priceFeed) internal { 172 | priceFeed = _priceFeed; 173 | emit PriceFeedUpdated(_priceFeed); 174 | } 175 | 176 | function setMetadataNFT(IMetadataNFT _metadataNFT) external onlyOwner { 177 | _setMetadataNFT(_metadataNFT); 178 | } 179 | 180 | function _setMetadataNFT(IMetadataNFT _metadataNFT) internal { 181 | metadataNFT = _metadataNFT; 182 | emit MetadataNFTUpdated(_metadataNFT); 183 | } 184 | 185 | function setInterestRate(uint256 _interestRate) external onlyOwner { 186 | accrueInterests(); 187 | _setInterestRate(_interestRate); 188 | } 189 | 190 | function _setInterestRate(uint256 _interestRate) internal { 191 | interestRate = _interestRate; 192 | emit InterestRateUpdated(_interestRate); 193 | } 194 | 195 | function setMaxSystemDebt(uint256 _cap) external onlyOwner { 196 | _setMaxSystemDebt(_cap); 197 | } 198 | 199 | function _setMaxSystemDebt(uint256 _cap) internal { 200 | maxSystemDebt = _cap; 201 | emit MaxSystemDebtUpdated(_cap); 202 | } 203 | 204 | function setSPYielPCT(uint256 _spYielPCT) external onlyOwner { 205 | accrueInterests(); 206 | _setSPYielPCT(_spYielPCT); 207 | } 208 | 209 | function _setSPYielPCT(uint256 _spYielPCT) internal { 210 | require(_spYielPCT <= DECIMAL_PRECISION, "TM: SP yield pct too high"); 211 | SP_YIELD_PCT = _spYielPCT; 212 | emit SPYieldPCTUpdated(_spYielPCT); 213 | } 214 | 215 | function setLiquidationPenaltySP(uint256 _penaltySP) external onlyOwner { 216 | _setLiquidationPenaltySP(_penaltySP); 217 | } 218 | 219 | function _setLiquidationPenaltySP(uint256 _penaltySP) internal { 220 | LIQUIDATION_PENALTY_SP = _penaltySP; 221 | emit LIQUIDATION_PENALTY_SP_Updated(_penaltySP); 222 | } 223 | 224 | function setLiquidationPenaltyRedist(uint256 _penaltyRedist) external onlyOwner { 225 | _setLiquidationPenaltyRedist(_penaltyRedist); 226 | } 227 | 228 | function _setLiquidationPenaltyRedist(uint256 _penaltyRedist) internal { 229 | LIQUIDATION_PENALTY_REDISTRIBUTION = _penaltyRedist; 230 | emit LIQUIDATION_PENALTY_REDISTRIBUTION_Updated(_penaltyRedist); 231 | } 232 | 233 | function setMaxCollGasCompensation(uint256 _maxCollGasCompensation) external onlyOwner { 234 | _setMaxCollGasCompensation(_maxCollGasCompensation); 235 | } 236 | 237 | function _setMaxCollGasCompensation(uint256 _maxCollGasCompensation) internal { 238 | MAX_COLL_GAS_COMPENSATION = _maxCollGasCompensation; 239 | emit MAX_COLL_GAS_COMPENSATION_Updated(_maxCollGasCompensation); 240 | } 241 | 242 | function fetchPrice() public returns (uint256) { 243 | return priceFeed.fetchPrice(address(collateralToken)); 244 | } 245 | 246 | function troveExists(uint256 id) external view returns (bool) { 247 | return _exists(id); 248 | } 249 | 250 | function tokenURI(uint256 id) public view override returns (string memory) { 251 | Trove memory t = getCurrentTrove(id); 252 | return metadataNFT.uri(IMetadataNFT.TroveData({ tokenId: id, owner: ownerOf(id), collToken: collateralToken, debtToken: debtToken, collAmount: t.coll, debtAmount: t.debt, interest: t.interest })); 253 | } 254 | 255 | function getTroveStake(uint256 id) external view returns (uint256) { 256 | return Troves[id].stake; 257 | } 258 | 259 | function getEntireSystemColl() public view returns (uint256) { 260 | return totalActiveCollateral + defaultedCollateral - lastCollateralError_Redistribution / DECIMAL_PRECISION; 261 | } 262 | 263 | function getEntireSystemDebt() public view returns (uint256) { 264 | return totalActiveDebt + defaultedDebt - lastDebtError_Redistribution / DECIMAL_PRECISION; 265 | } 266 | 267 | function getEntireSystemInterest() public view returns (uint256) { 268 | return totalActiveInterest + defaultedInterest - lastDefaultedInterestError_Redistribution / DECIMAL_PRECISION; 269 | } 270 | 271 | function getEntireSystemBalances() external returns (uint256 coll, uint256 debt, uint256 interest, uint256 price) { 272 | return (getEntireSystemColl(), getEntireSystemDebt(), getEntireSystemInterest(), fetchPrice()); 273 | } 274 | 275 | function getTCR() public returns (uint256) { 276 | accrueInterests(); 277 | return YalaMath._computeCR(getEntireSystemColl(), getEntireSystemDebt() + getEntireSystemInterest(), fetchPrice()); 278 | } 279 | 280 | function getTotalActiveCollateral() public view returns (uint256) { 281 | return totalActiveCollateral; 282 | } 283 | 284 | function getPendingRewards(uint256 id) public view returns (uint256, uint256, uint256) { 285 | RewardSnapshot memory snapshot = rewardSnapshots[id]; 286 | uint256 coll = L_collateral - snapshot.collateral; 287 | uint256 debt = L_debt - snapshot.debt; 288 | uint256 defaulted = L_defaulted_interest - snapshot.defaultedInterest; 289 | if (coll + debt + defaulted == 0 || !_exists(id)) return (0, 0, 0); 290 | uint256 stake = Troves[id].stake; 291 | return ((stake * coll) / DECIMAL_PRECISION, (stake * debt) / DECIMAL_PRECISION, (stake * defaulted) / DECIMAL_PRECISION); 292 | } 293 | 294 | function openTrove(address owner, uint256 _collateralAmount, uint256 _compositeDebt) external whenNotPaused whenNotShutdown returns (uint256 id) { 295 | _requireCallerIsBO(); 296 | uint256 supply = totalActiveDebt; 297 | id = nonce++; 298 | Trove storage t = Troves[id]; 299 | _mint(owner, id); 300 | t.coll = _collateralAmount; 301 | t.debt = _compositeDebt; 302 | _updateTroveRewardSnapshots(id); 303 | uint256 stake = _updateStakeAndTotalStakes(t); 304 | totalActiveCollateral = totalActiveCollateral + _collateralAmount; 305 | uint256 _newTotalDebt = supply + _compositeDebt; 306 | require(_newTotalDebt + defaultedDebt <= maxSystemDebt, "TM: Collateral debt limit reached"); 307 | totalActiveDebt = _newTotalDebt; 308 | emit TroveOpened(id, owner, _collateralAmount, _compositeDebt, stake); 309 | } 310 | 311 | function updateTroveFromAdjustment(uint256 id, bool _isDebtIncrease, uint256 _debtChange, bool _isCollIncrease, uint256 _collChange, uint256 _interestRepayment, address _receiver) external whenNotPaused whenNotShutdown returns (uint256, uint256, uint256, uint256) { 312 | _requireCallerIsBO(); 313 | require(!paused, "TM: Collateral Paused"); 314 | require(_exists(id), "TM: Nonexistent trove"); 315 | LocalTroveUpdateVariables memory vars = LocalTroveUpdateVariables(id, _debtChange, _collChange, _interestRepayment, _isCollIncrease, _isDebtIncrease, _receiver); 316 | Trove storage t = Troves[vars.id]; 317 | uint256 newDebt = t.debt; 318 | uint256 newInterest = t.interest; 319 | if (vars.debtChange > 0 || vars.interestRepayment > 0) { 320 | if (vars.isDebtIncrease) { 321 | newDebt = newDebt + vars.debtChange; 322 | _increaseDebt(vars.receiver, vars.debtChange); 323 | } else { 324 | newDebt = newDebt - vars.debtChange; 325 | _decreaseDebt(vars.receiver, vars.debtChange); 326 | newInterest -= vars.interestRepayment; 327 | _decreaseInterest(vars.receiver, vars.interestRepayment); 328 | } 329 | t.debt = newDebt; 330 | t.interest = newInterest; 331 | } 332 | 333 | uint256 newColl = t.coll; 334 | if (vars.collChange > 0) { 335 | if (vars.isCollIncrease) { 336 | newColl = newColl + vars.collChange; 337 | totalActiveCollateral = totalActiveCollateral + vars.collChange; 338 | } else { 339 | newColl = newColl - vars.collChange; 340 | _sendCollateral(vars.receiver, vars.collChange); 341 | } 342 | t.coll = newColl; 343 | } 344 | uint256 newStake = _updateStakeAndTotalStakes(t); 345 | emit TroveUpdated(vars.id, newColl, newDebt, newStake, newInterest, vars.receiver, TroveManagerOperation.adjust); 346 | return (newColl, newDebt, newInterest, newStake); 347 | } 348 | 349 | function batchLiquidate(uint256[] memory ids) external whenNotPaused whenNotShutdown { 350 | uint256 price = fetchPrice(); 351 | LiquidationValues memory totals; 352 | totals.remainingDeposits = stabilityPool.getTotalDeposits(); 353 | for (uint256 i = 0; i < ids.length; i++) { 354 | uint256 id = ids[i]; 355 | SingleLiquidation memory singleLiquidation; 356 | (singleLiquidation.coll, singleLiquidation.debt, singleLiquidation.interest) = applyPendingRewards(id); 357 | _liquidate(id, totals, singleLiquidation, price); 358 | } 359 | require(totalSupply() > 0, "TM: at least one trove to redistribute coll and debt"); 360 | require(totals.debtGasCompensation > 0, "TM: nothing to liquidate"); 361 | totals.interestOffset = YalaMath._min(totalActiveInterest, totals.interestOffset); 362 | totalActiveInterest = totalActiveInterest - totals.interestOffset; 363 | totalActiveDebt = totalActiveDebt - totals.debtOffset; 364 | uint256 totalOffset = totals.debtOffset + totals.interestOffset; 365 | if (totalOffset > 0) { 366 | _sendCollateral(address(stabilityPool), totals.collOffset); 367 | debtToken.burn(address(stabilityPool), totalOffset); 368 | stabilityPool.offset(totalOffset, totals.collOffset); 369 | } 370 | _redistribute(totals.collRedist, totals.debtRedist, totals.interestRedist); 371 | totalActiveCollateral = totalActiveCollateral - totals.collSurplus; 372 | totalCollateralSnapshot = totalActiveCollateral - totals.collGasCompensation + defaultedCollateral; 373 | totalStakesSnapshot = totalStakes; 374 | debtToken.returnFromPool(gasPoolAddress, msg.sender, totals.debtGasCompensation); 375 | _sendCollateral(msg.sender, totals.collGasCompensation); 376 | } 377 | 378 | function claimCollSurplus(address account, uint256 _amount) external { 379 | require(accountCollSurplus[account] >= _amount, "TM: insufficient coll surplus"); 380 | accountCollSurplus[account] -= _amount; 381 | collateralToken.safeTransfer(account, _amount); 382 | emit CollSurplusClaimed(account, _amount); 383 | } 384 | 385 | function _getCollGasCompensation(uint256 _entireColl) internal view returns (uint256) { 386 | return YalaMath._min(_entireColl / COLL_GAS_COMPENSATION_DIVISOR, MAX_COLL_GAS_COMPENSATION); 387 | } 388 | 389 | function _accrueGasCompensation(LiquidationValues memory totals, SingleLiquidation memory singleLiquidation) internal view { 390 | singleLiquidation.debtGasCompensation = DEBT_GAS_COMPENSATION; 391 | totals.debtGasCompensation += singleLiquidation.debtGasCompensation; 392 | singleLiquidation.collGasCompensation = _getCollGasCompensation(singleLiquidation.coll); 393 | totals.collGasCompensation += singleLiquidation.collGasCompensation; 394 | singleLiquidation.collToLiquidate = singleLiquidation.coll - singleLiquidation.collGasCompensation; 395 | } 396 | 397 | function _liquidatePenalty(LiquidationValues memory totals, SingleLiquidation memory singleLiquidation, uint256 _price) internal view { 398 | uint256 collSPPortion; 399 | if (totals.remainingDeposits > 0) { 400 | singleLiquidation.debtOffset = YalaMath._min(singleLiquidation.debt, totals.remainingDeposits); 401 | totals.remainingDeposits -= singleLiquidation.debtOffset; 402 | if (totals.remainingDeposits > 0) { 403 | singleLiquidation.interestOffset = YalaMath._min(singleLiquidation.interest, totals.remainingDeposits); 404 | totals.remainingDeposits -= singleLiquidation.interestOffset; 405 | } 406 | collSPPortion = (singleLiquidation.collToLiquidate * (singleLiquidation.debtOffset + singleLiquidation.interestOffset)) / (singleLiquidation.debt + singleLiquidation.interest); 407 | (singleLiquidation.collOffset, singleLiquidation.collSurplus) = _getCollPenaltyAndSurplus(collSPPortion, singleLiquidation.debtOffset + singleLiquidation.interestOffset, LIQUIDATION_PENALTY_SP, _price); 408 | } 409 | // Redistribution 410 | singleLiquidation.debtRedist = singleLiquidation.debt - singleLiquidation.debtOffset; 411 | singleLiquidation.interestRedist = singleLiquidation.interest - singleLiquidation.interestOffset; 412 | if (singleLiquidation.debtRedist > 0 || singleLiquidation.interestRedist > 0) { 413 | uint256 collRedistPortion = singleLiquidation.collToLiquidate - collSPPortion; 414 | if (collRedistPortion > 0) { 415 | (singleLiquidation.collRedist, singleLiquidation.collSurplus) = _getCollPenaltyAndSurplus( 416 | collRedistPortion + singleLiquidation.collSurplus, // Coll surplus from offset can be eaten up by red. penalty 417 | singleLiquidation.debtRedist + singleLiquidation.interestRedist, 418 | LIQUIDATION_PENALTY_REDISTRIBUTION, // _penaltyRatio 419 | _price 420 | ); 421 | } 422 | } 423 | totals.debtOffset += singleLiquidation.debtOffset; 424 | totals.debtRedist += singleLiquidation.debtRedist; 425 | totals.collOffset += singleLiquidation.collOffset; 426 | totals.collRedist += singleLiquidation.collRedist; 427 | totals.interestOffset += singleLiquidation.interestOffset; 428 | totals.interestRedist += singleLiquidation.interestRedist; 429 | // assert(singleLiquidation.collToLiquidate == singleLiquidation.collOffset + singleLiquidation.collRedist + singleLiquidation.collSurplus); 430 | } 431 | 432 | function _getCollPenaltyAndSurplus(uint256 _collToLiquidate, uint256 _debt, uint256 _penaltyRatio, uint256 _price) internal pure returns (uint256 seizedColl, uint256 collSurplus) { 433 | uint256 maxSeizedColl = (_debt * (DECIMAL_PRECISION + _penaltyRatio)) / _price; 434 | if (_collToLiquidate > maxSeizedColl) { 435 | seizedColl = maxSeizedColl; 436 | collSurplus = _collToLiquidate - maxSeizedColl; 437 | } else { 438 | seizedColl = _collToLiquidate; 439 | collSurplus = 0; 440 | } 441 | } 442 | 443 | function _liquidate(uint256 id, LiquidationValues memory totals, SingleLiquidation memory singleLiquidation, uint256 price) internal { 444 | uint256 entireDebt = singleLiquidation.debt + singleLiquidation.interest; 445 | uint256 ICR = YalaMath._computeCR(singleLiquidation.coll, entireDebt, price); 446 | if (ICR < MCR) { 447 | _accrueGasCompensation(totals, singleLiquidation); 448 | _liquidatePenalty(totals, singleLiquidation, price); 449 | address owner = ownerOf(id); 450 | if (singleLiquidation.collSurplus > 0) { 451 | accountCollSurplus[owner] += singleLiquidation.collSurplus; 452 | totals.collSurplus = totals.collSurplus + singleLiquidation.collSurplus; 453 | } 454 | _closeTrove(id); 455 | emit Liquidated(owner, id, singleLiquidation.coll, singleLiquidation.debt, singleLiquidation.interest, singleLiquidation.collSurplus); 456 | } 457 | } 458 | 459 | function closeTrove(uint256 id, address _receiver, uint256 collAmount, uint256 debtAmount, uint256 interest) external { 460 | _requireCallerIsBO(); 461 | require(_exists(id), "TM: nonexistent trove"); 462 | totalActiveDebt = totalActiveDebt - debtAmount; 463 | totalActiveInterest = totalActiveInterest - interest; 464 | _sendCollateral(_receiver, collAmount); 465 | _removeStake(id); 466 | _closeTrove(id); 467 | _resetState(); 468 | } 469 | 470 | function _resetState() private { 471 | if (totalSupply() == 0) { 472 | totalStakes = 0; 473 | totalStakesSnapshot = 0; 474 | totalCollateralSnapshot = 0; 475 | L_collateral = 0; 476 | L_debt = 0; 477 | L_defaulted_interest = 0; 478 | L_active_interest = 0; 479 | lastCollateralError_Redistribution = 0; 480 | lastDebtError_Redistribution = 0; 481 | lastActiveInterestError_Redistribution = 0; 482 | lastDefaultedInterestError_Redistribution = 0; 483 | totalActiveCollateral = 0; 484 | totalActiveDebt = 0; 485 | totalActiveInterest = 0; 486 | defaultedCollateral = 0; 487 | defaultedDebt = 0; 488 | defaultedInterest = 0; 489 | lastInterestUpdate = 0; 490 | nonce = 0; 491 | } 492 | } 493 | 494 | // This function must be called any time the debt or the interest changes 495 | function accrueInterests() public returns (uint256 yieldSP, uint256 yieldFee) { 496 | (uint256 applicable, uint256 mintAmount) = getPendingInterest(); 497 | lastInterestUpdate = applicable; 498 | if (mintAmount > 0) { 499 | uint256 interestNumerator = (mintAmount * DECIMAL_PRECISION) + lastActiveInterestError_Redistribution; 500 | uint256 interestRewardPerUnit = interestNumerator / totalActiveDebt; 501 | lastActiveInterestError_Redistribution = interestNumerator - totalActiveDebt * interestRewardPerUnit; 502 | L_active_interest += interestRewardPerUnit; 503 | yieldFee = (totalActiveDebt * interestRewardPerUnit) / DECIMAL_PRECISION; 504 | totalActiveInterest += yieldFee; 505 | if (SP_YIELD_PCT > 0 && stabilityPool.getTotalDeposits() >= DECIMAL_PRECISION) { 506 | yieldSP = (yieldFee * SP_YIELD_PCT) / DECIMAL_PRECISION; 507 | yieldFee -= yieldSP; 508 | debtToken.mint(address(stabilityPool), yieldSP); 509 | stabilityPool.triggerSPYield(yieldSP); 510 | emit SPYieldAccrued(yieldSP); 511 | } 512 | debtToken.mint(YALA_CORE.feeReceiver(), yieldFee); 513 | emit InterestAccrued(yieldFee); 514 | } 515 | } 516 | 517 | function _accrueTroveInterest(uint256 id) internal returns (uint256 total, uint256 accrued) { 518 | accrueInterests(); 519 | Trove storage t = Troves[id]; 520 | if (rewardSnapshots[id].activeInterest < L_active_interest) { 521 | accrued = ((L_active_interest - rewardSnapshots[id].activeInterest) * t.debt) / DECIMAL_PRECISION; 522 | t.interest += accrued; 523 | } 524 | total = t.interest; 525 | } 526 | 527 | function getPendingInterest() public view returns (uint256 applicable, uint256 amount) { 528 | applicable = hasShutdown() ? shutdownAt : block.timestamp; 529 | if (lastInterestUpdate == 0 || lastInterestUpdate == applicable) { 530 | return (applicable, amount); 531 | } 532 | uint256 diff = applicable - lastInterestUpdate; 533 | if (diff == 0) { 534 | return (applicable, amount); 535 | } 536 | if (diff > 0) { 537 | amount = (totalActiveDebt * diff * interestRate) / SECONDS_IN_YEAR / DECIMAL_PRECISION; 538 | } 539 | return (applicable, amount); 540 | } 541 | 542 | function getPendingYieldSP() public view returns (uint256) { 543 | if (SP_YIELD_PCT == 0 || stabilityPool.getTotalDeposits() < DECIMAL_PRECISION) { 544 | return 0; 545 | } 546 | (, uint256 amount) = getPendingInterest(); 547 | return (amount * SP_YIELD_PCT) / DECIMAL_PRECISION; 548 | } 549 | 550 | function getCurrentICR(uint256 id, uint256 price) public view returns (uint256) { 551 | Trove memory trove = getCurrentTrove(id); 552 | return YalaMath._computeCR(trove.coll, trove.debt + trove.interest, price); 553 | } 554 | 555 | function getCurrentTrove(uint256 id) public view returns (Trove memory) { 556 | require(_exists(id), "TM: nonexistent trove"); 557 | (, uint256 mintAmount) = getPendingInterest(); 558 | uint256 interestNumerator = (mintAmount * DECIMAL_PRECISION) + lastActiveInterestError_Redistribution; 559 | uint256 interestRewardPerUnit = interestNumerator / totalActiveDebt; 560 | uint256 new_L_active_interest = L_active_interest + interestRewardPerUnit; 561 | RewardSnapshot memory snapshot = rewardSnapshots[id]; 562 | Trove memory t = Troves[id]; 563 | uint256 interest = ((new_L_active_interest - rewardSnapshots[id].activeInterest) * t.debt) / DECIMAL_PRECISION; 564 | uint256 coll = L_collateral - snapshot.collateral; 565 | uint256 debt = L_debt - snapshot.debt; 566 | uint256 defaulted = L_defaulted_interest - snapshot.defaultedInterest; 567 | uint256 stake = t.stake; 568 | (uint256 pendingColl, uint256 pendingDebt, uint256 pendingInterest) = ((stake * coll) / DECIMAL_PRECISION, (stake * debt) / DECIMAL_PRECISION, (stake * defaulted) / DECIMAL_PRECISION); 569 | interest += pendingInterest; 570 | return Trove({ coll: t.coll + pendingColl, debt: t.debt + pendingDebt, stake: t.stake, interest: t.interest + interest }); 571 | } 572 | 573 | function _closeTrove(uint256 id) internal { 574 | totalStakes -= Troves[id].stake; 575 | delete Troves[id]; 576 | delete rewardSnapshots[id]; 577 | _burn(id); 578 | emit TroveClosed(id); 579 | } 580 | 581 | function applyPendingRewards(uint256 id) public returns (uint256 coll, uint256 debt, uint256 interest) { 582 | Trove storage t = Troves[id]; 583 | if (_exists(id)) { 584 | debt = t.debt; 585 | coll = t.coll; 586 | (interest, ) = _accrueTroveInterest(id); 587 | if (rewardSnapshots[id].collateral < L_collateral || rewardSnapshots[id].debt < L_debt || rewardSnapshots[id].defaultedInterest < L_defaulted_interest) { 588 | // Compute pending rewards 589 | (uint256 pendingCollateralReward, uint256 pendingDebtReward, uint256 pendingDefaultedInterest) = getPendingRewards(id); 590 | // Apply pending rewards to trove's state 591 | coll = coll + pendingCollateralReward; 592 | t.coll = coll; 593 | debt = debt + pendingDebtReward; 594 | t.debt = debt; 595 | interest = interest + pendingDefaultedInterest; 596 | _movePendingTroveRewardsToActiveBalance(pendingDebtReward, pendingCollateralReward, pendingDefaultedInterest); 597 | } 598 | t.interest = YalaMath._min(totalActiveInterest, interest); 599 | _updateTroveRewardSnapshots(id); 600 | } 601 | return (coll, debt, t.interest); 602 | } 603 | 604 | function _updateTroveRewardSnapshots(uint256 id) internal { 605 | rewardSnapshots[id].collateral = L_collateral; 606 | rewardSnapshots[id].debt = L_debt; 607 | rewardSnapshots[id].activeInterest = L_active_interest; 608 | rewardSnapshots[id].defaultedInterest = L_defaulted_interest; 609 | } 610 | 611 | // Remove borrower's stake from the totalStakes sum, and set their stake to 0 612 | function _removeStake(uint256 id) internal { 613 | uint256 stake = Troves[id].stake; 614 | totalStakes = totalStakes - stake; 615 | Troves[id].stake = 0; 616 | } 617 | 618 | // Update borrower's stake based on their latest collateral value 619 | function _updateStakeAndTotalStakes(Trove storage t) internal returns (uint256) { 620 | uint256 newStake = _computeNewStake(t.coll); 621 | uint256 oldStake = t.stake; 622 | t.stake = newStake; 623 | uint256 newTotalStakes = totalStakes - oldStake + newStake; 624 | totalStakes = newTotalStakes; 625 | emit TotalStakesUpdated(newTotalStakes); 626 | 627 | return newStake; 628 | } 629 | 630 | // Calculate a new stake based on the snapshots of the totalStakes and totalCollateral taken at the last liquidation 631 | function _computeNewStake(uint256 _coll) internal view returns (uint256) { 632 | uint256 stake; 633 | if (totalCollateralSnapshot == 0) { 634 | stake = _coll; 635 | } else { 636 | /* 637 | * The following assert() holds true because: 638 | * - The system always contains >= 1 trove 639 | * - When we close or liquidate a trove, we redistribute the pending rewards, so if all troves were closed/liquidated, 640 | * rewards would’ve been emptied and totalCollateralSnapshot would be zero too. 641 | */ 642 | stake = (_coll * totalStakesSnapshot) / totalCollateralSnapshot; 643 | } 644 | return stake; 645 | } 646 | 647 | function _movePendingTroveRewardsToActiveBalance(uint256 _debt, uint256 _collateral, uint256 _defaultedInterest) internal { 648 | defaultedDebt -= _debt; 649 | totalActiveDebt += _debt; 650 | defaultedCollateral -= _collateral; 651 | totalActiveCollateral += _collateral; 652 | defaultedInterest -= _defaultedInterest; 653 | totalActiveInterest += _defaultedInterest; 654 | } 655 | 656 | function _redistribute(uint256 _coll, uint256 _debt, uint256 _interest) internal { 657 | if (_debt == 0 && _interest == 0) { 658 | return; 659 | } 660 | /* 661 | * Add distributed coll and debt rewards-per-unit-staked to the running totals. Division uses a "feedback" 662 | * error correction, to keep the cumulative error low in the running totals L_collateral and L_debt: 663 | * 664 | * 1) Form numerators which compensate for the floor division errors that occurred the last time this 665 | * function was called. 666 | * 2) Calculate "per-unit-staked" ratios. 667 | * 3) Multiply each ratio back by its denominator, to reveal the current floor division error. 668 | * 4) Store these errors for use in the next correction when this function is called. 669 | * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. 670 | */ 671 | uint256 collateralNumerator = (_coll * DECIMAL_PRECISION) + lastCollateralError_Redistribution; 672 | uint256 debtNumerator = (_debt * DECIMAL_PRECISION) + lastDebtError_Redistribution; 673 | uint256 interestNumerator = (_interest * DECIMAL_PRECISION) + lastDefaultedInterestError_Redistribution; 674 | uint256 totalStakesCached = totalStakes; 675 | // Get the per-unit-staked terms 676 | uint256 collateralRewardPerUnitStaked = collateralNumerator / totalStakesCached; 677 | uint256 debtRewardPerUnitStaked = debtNumerator / totalStakesCached; 678 | uint256 defaultedInterestRewardPerUnitStaked = interestNumerator / totalStakesCached; 679 | 680 | lastCollateralError_Redistribution = collateralNumerator - (collateralRewardPerUnitStaked * totalStakesCached); 681 | lastDebtError_Redistribution = debtNumerator - (debtRewardPerUnitStaked * totalStakesCached); 682 | lastDefaultedInterestError_Redistribution = interestNumerator - (defaultedInterestRewardPerUnitStaked * totalStakesCached); 683 | // Add per-unit-staked terms to the running totals 684 | uint256 new_L_collateral = L_collateral + collateralRewardPerUnitStaked; 685 | uint256 new_L_debt = L_debt + debtRewardPerUnitStaked; 686 | uint256 new_L_defaulted_interest = L_defaulted_interest + defaultedInterestRewardPerUnitStaked; 687 | 688 | L_collateral = new_L_collateral; 689 | L_debt = new_L_debt; 690 | L_defaulted_interest = new_L_defaulted_interest; 691 | emit LTermsUpdated(new_L_collateral, new_L_debt, new_L_defaulted_interest); 692 | 693 | totalActiveDebt -= _debt; 694 | defaultedDebt += _debt; 695 | defaultedCollateral += _coll; 696 | totalActiveCollateral -= _coll; 697 | totalActiveInterest -= _interest; 698 | defaultedInterest += _interest; 699 | } 700 | 701 | function _sendCollateral(address _account, uint256 _amount) private { 702 | if (_amount > 0) { 703 | totalActiveCollateral = totalActiveCollateral - _amount; 704 | collateralToken.safeTransfer(_account, _amount); 705 | emit CollateralSent(_account, _amount); 706 | } 707 | } 708 | 709 | function _increaseDebt(address account, uint256 debtAmount) internal { 710 | uint256 _newTotalDebt = totalActiveDebt + debtAmount; 711 | require(_newTotalDebt + defaultedDebt <= maxSystemDebt, "Collateral debt limit reached"); 712 | totalActiveDebt = _newTotalDebt; 713 | debtToken.mint(account, debtAmount); 714 | } 715 | 716 | function _decreaseDebt(address account, uint256 amount) internal { 717 | debtToken.burn(account, amount); 718 | totalActiveDebt = totalActiveDebt - amount; 719 | } 720 | 721 | function _decreaseInterest(address account, uint256 amount) internal { 722 | debtToken.burn(account, amount); 723 | totalActiveInterest = totalActiveInterest - amount; 724 | } 725 | 726 | function hasShutdown() public view returns (bool) { 727 | return shutdownAt != 0; 728 | } 729 | 730 | function _requireCallerIsBO() internal view { 731 | require(msg.sender == borrowerOperationsAddress, "Caller not BO"); 732 | } 733 | } 734 | --------------------------------------------------------------------------------