├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── foundry.toml ├── remappings.txt ├── script └── LendingVault.sol ├── src ├── LendingVault.sol └── libraries │ ├── Math.sol │ └── ReEntrancyGuard.sol └── test ├── BaseSetup.sol ├── LendingVault.t.sol ├── interfaces ├── IUniswap.sol └── IWETH.sol └── utils └── Util.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | [submodule "lib/foundry-chainlink-toolkit"] 8 | path = lib/foundry-chainlink-toolkit 9 | url = https://github.com/smartcontractkit/foundry-chainlink-toolkit 10 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | remappings = ["@openzeppelin/=lib/openzeppelin-contracts", "@chainlink/=lib/foundry-chainlink-toolkit/lib/chainlink-brownie-contracts"] 7 | 8 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @chainlink-testing/=lib/foundry-chainlink-toolkit/lib/chainlink-testing-framework/contracts/ethereum/ 2 | @chainlink/=lib/foundry-chainlink-toolkit/lib/chainlink-brownie-contracts/ 3 | @openzeppelin/=lib/openzeppelin-contracts/ 4 | chainlink-brownie-contracts/=lib/foundry-chainlink-toolkit/lib/chainlink-brownie-contracts/ 5 | chainlink-testing-framework/=lib/foundry-chainlink-toolkit/lib/chainlink-testing-framework/contracts/ 6 | ds-test/=lib/forge-std/lib/ds-test/src/ 7 | erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ 8 | forge-std/=lib/forge-std/src/ 9 | foundry-chainlink-toolkit/=lib/foundry-chainlink-toolkit/ 10 | openzeppelin-contracts/=lib/openzeppelin-contracts/ 11 | openzeppelin/=lib/openzeppelin-contracts/contracts/ 12 | -------------------------------------------------------------------------------- /script/LendingVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract LendingVault is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/LendingVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 7 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 8 | import "@openzeppelin/contracts/access/Ownable.sol"; 9 | import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; 10 | import "./libraries/Math.sol"; 11 | import "./libraries/ReEntrancyGuard.sol"; 12 | 13 | import "forge-std/Test.sol"; 14 | import "forge-std/console.sol"; 15 | 16 | contract LendingVault is Ownable, ReEntrancyGuard { 17 | using SafeMath for uint256; 18 | using Math for uint256; 19 | using SafeERC20 for IERC20; 20 | 21 | IERC20 internal constant usdcToken = 22 | IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 23 | AggregatorV3Interface internal constant usdcEthPriceFeed = 24 | AggregatorV3Interface(0x986b5E1e1755e3C2440e960477f25201B0a8bbD4); 25 | 26 | uint256 internal constant USDC_DECIMAL = 1e6; 27 | uint256 internal constant ETHER_DECIMAL = 1e18; 28 | uint256 internal constant SECONDS_PER_YEAR = 365 days; 29 | uint256 internal constant ETHER_DECIMAL_FACTOR = 10 ** 2; 30 | uint256 internal constant DISCOUNT_RATE = 95; 31 | 32 | struct Loan { 33 | uint256 collateralAmount; 34 | uint256 borrowedAmount; 35 | uint256 repayAmount; 36 | uint256 feeAmount; 37 | uint256 interestAmount; 38 | uint256 timestamp; 39 | uint256 duration; 40 | } 41 | 42 | struct Depositor { 43 | uint256 assetAmount; 44 | uint256 rewardDebt; 45 | uint256 lendingAmount; 46 | } 47 | 48 | // Daily interest rate 49 | uint8 public interestRate; 50 | // Fee for borrower 51 | uint8 public reserveFeeRate; 52 | // Collateral factor 53 | uint8 public collateralFactor; 54 | 55 | // Current total borrowing amount 56 | uint256 public totalBorrowAmount; 57 | 58 | // Current total asset amount based on LP's total deposited amount 59 | uint256 public totalAssetAmount; 60 | 61 | // Current total reserver amount 62 | uint256 public totalReserveAmount; 63 | 64 | // Current balance amount 65 | uint256 public currentBalanceAmount; 66 | 67 | mapping(address => Loan) public loans; 68 | mapping(address => Depositor) public depositors; 69 | 70 | event Deposited(address indexed depositor, uint256 amount); 71 | event Withdraw(uint256 amount); 72 | event BorrowToken( 73 | address indexed borrower, 74 | uint256 collateralAmount, 75 | uint256 borrowedAmount, 76 | uint256 dueTimestamp 77 | ); 78 | 79 | event LoanRepaid(address indexed borrower, uint256 amount); 80 | event CollateralSold( 81 | address indexed borrower, 82 | uint256 collateralAmount, 83 | uint256 proceeds 84 | ); 85 | 86 | error ZeroAmountForDeposit(); 87 | error InsufficientBalanceForDeposit(); 88 | error ZeroAmountForWithdraw(); 89 | error NotAvailableAmountForWithdraw(); 90 | error ZeroCollateralAmountForBorrow(); 91 | error InsufficientBalanceForBorrow(); 92 | error AlreadyBorrowed(); 93 | error InsufficientCollateral(); 94 | error InsufficientTokenInBalance(); 95 | error NotExistLoan(); 96 | error ZeroRepayAmount(); 97 | error NotAvailableForWithdraw(); 98 | error NotAvailableForLoanOwner(); 99 | error LoanHasNoCollateral(); 100 | error LoanNotInLiquidate(); 101 | error ZeroAmountForExchange(); 102 | error InsufficientBalanceForLiquidate(); 103 | 104 | constructor( 105 | uint8 _interestRate, 106 | uint8 _collateralFactor, 107 | uint8 _reserveFeeRate 108 | ) { 109 | interestRate = _interestRate; 110 | collateralFactor = _collateralFactor; 111 | reserveFeeRate = _reserveFeeRate; 112 | } 113 | 114 | function setInterestRate(uint8 _interestRate) external onlyOwner { 115 | interestRate = _interestRate; 116 | } 117 | 118 | function setCollateralFactor(uint8 _collateralFactor) external onlyOwner { 119 | collateralFactor = _collateralFactor; 120 | } 121 | 122 | function setReserveFeeRate(uint8 _reserveFeeRate) external onlyOwner { 123 | reserveFeeRate = _reserveFeeRate; 124 | } 125 | 126 | /** 127 | @dev Allows a user to deposit tokens into a pool by providing the pool ID and the amount of tokens to deposit. 128 | @param _amount The amount of tokens the user wants to deposit. 129 | @notice This function checks if the amount of tokens to deposit is not zero. If the amount of tokens to deposit is not zero, the function calculates the asset amount based on the total liquidity of the pool and the amount of tokens deposited. 130 | If the pool uses Ether as collateral, the function checks if the transfer Ether amount is not zero and if the transfer Ether amount is less than the amount of tokens to deposit. If the transfer Ether amount is less than the amount of tokens to deposit, the function sets the amount of tokens to deposit to the transfer Ether amount. 131 | If the pool uses USDC as collateral, the function checks if the user has sufficient balance for deposit and transfers the USDC from the user to the vault contract. 132 | The function then updates the depositor's asset amount and the pool's data and emits a Deposited event. 133 | */ 134 | function deposit(uint256 _amount) external payable noReentrant { 135 | if (_amount == 0) revert ZeroAmountForDeposit(); 136 | 137 | Depositor storage depositor = depositors[msg.sender]; 138 | 139 | uint256 assetAmount; 140 | 141 | // check if user has sufficient balance for deposit 142 | if (usdcToken.balanceOf(msg.sender) < _amount) 143 | revert InsufficientBalanceForDeposit(); 144 | 145 | // calculate asset amount based on total liquidity 146 | assetAmount = calculateAssetAmount(_amount); 147 | 148 | // transfer USDC from user to vault contract 149 | usdcToken.safeTransferFrom(msg.sender, address(this), _amount); 150 | 151 | // update depositor's asset amount 152 | depositor.assetAmount += assetAmount; 153 | 154 | // pool's deposit amount 155 | currentBalanceAmount += _amount; 156 | 157 | // pool's total amount 158 | totalAssetAmount += assetAmount; 159 | 160 | emit Deposited(msg.sender, _amount); 161 | } 162 | 163 | /** 164 | @dev Allows a user to withdraw tokens from Vault. 165 | @notice This function checks if the user has sufficient withdraw amount. If the user has sufficient withdraw amount, the function calculates the amount the user can withdraw based on the pool's current liquidity amount and the user's asset amount. 166 | The function then updates the depositor's asset amount and the pool's data and transfers the withdrawn tokens to the user. 167 | If the pool uses Ether as collateral, the function transfers Ether to the user. If the pool uses USDC as collateral, the function approves the transfer of USDC to the user and then transfers the USDC to the user. 168 | */ 169 | function withdraw() external noReentrant { 170 | Depositor storage depositor = depositors[msg.sender]; 171 | 172 | uint256 assetAmount = depositor.assetAmount; 173 | 174 | // check if User has sufficient withdraw amount 175 | if (assetAmount == 0) revert ZeroAmountForWithdraw(); 176 | 177 | // calculate amount user can withdraw 178 | uint256 amount = calculateAmount(assetAmount); 179 | 180 | if (amount > currentBalanceAmount) revert NotAvailableForWithdraw(); 181 | 182 | // update depositor's asset amount 183 | depositor.assetAmount -= assetAmount; 184 | 185 | // update current liquidity amount 186 | currentBalanceAmount -= amount; 187 | // update pool's total asset amount 188 | totalAssetAmount -= assetAmount; 189 | 190 | usdcToken.safeTransfer(msg.sender, amount); 191 | 192 | emit Withdraw(amount); 193 | } 194 | 195 | /** 196 | @dev Allows a user to borrow tokens from a pool by providing collateral and specifying the duration of the loan. 197 | @param _amount The amount of collateral the user wants to provide for the loan. 198 | @param _duration The duration of the loan in days. 199 | @return A tuple containing the amount of tokens borrowed and the amount to be repaid. 200 | @notice This function checks if the borrower has already borrowed tokens from the pool and reverts if they have. 201 | It then calculates the amount of tokens the borrower can borrow based on the collateral provided and the pool's collateral factor. 202 | If the pool uses Ether as collateral, the function checks if the borrower has provided enough Ether to borrow the requested amount of tokens. 203 | If the pool uses USDC as collateral, the function checks if the borrower has provided enough USDC to borrow the requested amount of tokens. 204 | The function then calculates the repayment amount based on the borrowed amount and the loan duration. 205 | Finally, the function updates the borrower's loan data and the pool's data and transfers the borrowed tokens to the borrower. 206 | */ 207 | function borrowToken( 208 | uint256 _amount, 209 | uint256 _duration 210 | ) external payable noReentrant returns (uint256, uint256) { 211 | Loan storage loanData = loans[msg.sender]; 212 | 213 | // check if borrower already rent 214 | if (loanData.collateralAmount > 0) revert AlreadyBorrowed(); 215 | 216 | uint256 borrowableAmount; 217 | 218 | if (msg.value == 0) revert ZeroCollateralAmountForBorrow(); 219 | // Borrower is going to borrow USDC 220 | if (msg.value < _amount) revert InsufficientCollateral(); 221 | 222 | borrowableAmount = _amount 223 | .mul(collateralFactor) 224 | .mul(USDC_DECIMAL) 225 | .div(getUsdcEthPrice()) 226 | .div(100); 227 | 228 | // check if there is sufficient the borrowable USDC amount in Vault. 229 | if (usdcToken.balanceOf(address(this)) < borrowableAmount) 230 | revert InsufficientTokenInBalance(); 231 | 232 | // update borrower's collateral amount 233 | loanData.collateralAmount = msg.value; 234 | 235 | // update borrower's borrow amount; 236 | loanData.borrowedAmount = borrowableAmount; 237 | // update borrower's borrwed timestamp; 238 | loanData.timestamp = block.timestamp; 239 | // update borrowing period; 240 | loanData.duration = _duration; 241 | 242 | // calculate repayment amount 243 | ( 244 | uint256 repayAmount, 245 | uint256 interestAmount, 246 | uint256 feeAmount 247 | ) = calculateRepaymentAmount(borrowableAmount, _duration); 248 | 249 | // set borrower's pay amount 250 | loanData.repayAmount = repayAmount; 251 | loanData.interestAmount = interestAmount; 252 | loanData.feeAmount = feeAmount; 253 | 254 | // update total borrow amount 255 | totalBorrowAmount += repayAmount; 256 | // update total reserve amount 257 | totalReserveAmount += feeAmount; 258 | // update current liquidity amount 259 | currentBalanceAmount -= borrowableAmount; 260 | 261 | // transfer Token to borrower 262 | usdcToken.safeTransfer(msg.sender, borrowableAmount); 263 | 264 | emit BorrowToken( 265 | msg.sender, 266 | _amount, 267 | borrowableAmount, 268 | loanData.timestamp + _duration 269 | ); 270 | 271 | return (borrowableAmount, repayAmount); 272 | } 273 | 274 | /** 275 | @dev Allows a user to repay a loan by providing the pool ID and the amount of tokens to repay. 276 | @param _amount The amount of tokens the user wants to repay. 277 | @notice This function checks if the borrower has an active loan. If the borrower has an active loan, the function checks if the repay amount is bigger than zero. 278 | If the pool uses Ether as collateral, the function checks if the transfer Ether amount is not zero and sets the amount of tokens to repay to the transfer Ether amount. 279 | If the pool uses USDC as collateral, the function checks if the user has sufficient balance for repayment and transfers the USDC from the user to the vault contract. 280 | The function then updates the loan's repay amount, the pool's data, and emits a LoanRepaid event. 281 | If the borrower doesn't need to repay more, the function updates the loan data and transfers the borrower's collateral back to the borrower. 282 | */ 283 | function repayLoan(uint256 _amount) external payable noReentrant { 284 | Loan storage loanData = loans[msg.sender]; 285 | 286 | // check if borrower has an active loan 287 | if (loanData.repayAmount == 0) revert NotExistLoan(); 288 | 289 | // check if repay amount is bigger than zero 290 | if (_amount == 0 || usdcToken.balanceOf(msg.sender) == 0) 291 | revert ZeroRepayAmount(); 292 | 293 | // If Borrower repays the amount bigger than the current repay amount, _amount should be loanData.repayAmount 294 | if (_amount >= loanData.repayAmount) _amount = loanData.repayAmount; 295 | 296 | // Borrower repays the borrowable token as USDC 297 | usdcToken.transferFrom(msg.sender, address(this), _amount); 298 | 299 | // update loan's repay amount 300 | loanData.repayAmount -= _amount; 301 | 302 | // update pools' total borrow amount 303 | totalBorrowAmount -= _amount; 304 | currentBalanceAmount += _amount; 305 | 306 | // If Borrower doesn't need to repay more, he can get his collateral 307 | if (loanData.repayAmount == 0) { 308 | // update loan's interest amount 309 | loanData.interestAmount = 0; 310 | // update loan's fee amount 311 | loanData.feeAmount = 0; 312 | // update borrower's borrow amount; 313 | loanData.borrowedAmount = 0; 314 | // update borrower's borrwed timestamp; 315 | loanData.timestamp = 0; 316 | // update borrowing period; 317 | loanData.duration = 0; 318 | // update user collateral amount; 319 | uint256 collateralAmount = loanData.collateralAmount; 320 | 321 | // update user collateral amount; 322 | loanData.collateralAmount = 0; 323 | 324 | // Borrower receives the collateral as Ether 325 | payable(msg.sender).transfer(collateralAmount); 326 | } 327 | 328 | emit LoanRepaid(msg.sender, _amount); 329 | } 330 | 331 | /** 332 | @dev Allows a user to liquidate a loan by providing the pool ID and the borrower's address. 333 | @param _account The address of the borrower whose loan is being liquidated. 334 | @notice This function checks if the caller is not the loan owner and if the loan has collateral and is at liquidate state. 335 | If the pool uses Ether as collateral, the function checks if the caller has sufficient balance for liquidation and transfers the USDC with a discount percent to the caller. 336 | If the pool uses USDC as collateral, the function checks if the user has sufficient balance for liquidation and transfers the USDC from the user to the vault contract. 337 | The function then updates the pool's data and the loan data and returns the collateral amount to the caller. 338 | */ 339 | function liquidate( 340 | address _account 341 | ) external payable noReentrant returns (uint256) { 342 | Loan storage loanData = loans[_account]; 343 | 344 | // check if Loan owner call liquidate 345 | if (msg.sender == _account) revert NotAvailableForLoanOwner(); 346 | 347 | uint256 collateralAmount = loanData.collateralAmount; 348 | 349 | // check if Loan has collateral 350 | if (collateralAmount == 0) revert LoanHasNoCollateral(); 351 | 352 | // check if Loan is at liquidate state 353 | if (loanData.timestamp + loanData.duration > block.timestamp) 354 | revert LoanNotInLiquidate(); 355 | 356 | uint256 payAmount = getPayAmountForLiquidateLoan(_account); 357 | 358 | // check if user's USDC token balance is less than amount 359 | if (usdcToken.balanceOf(msg.sender) < payAmount) 360 | revert InsufficientBalanceForLiquidate(); 361 | 362 | // receive Usdc token and transfer Ether to user 363 | usdcToken.safeTransferFrom(msg.sender, address(this), payAmount); 364 | 365 | // update loan data's collateral amount 366 | loanData.collateralAmount = 0; 367 | payable(msg.sender).transfer(loanData.collateralAmount); 368 | 369 | // update current total amount 370 | currentBalanceAmount += payAmount; 371 | totalBorrowAmount -= loanData.repayAmount; 372 | 373 | // check if liquidate payment is more than loan's repay 374 | if (loanData.repayAmount < payAmount) { 375 | // update pool's reserve amount again 376 | totalReserveAmount -= loanData.feeAmount; 377 | totalReserveAmount += 378 | payAmount - 379 | loanData.interestAmount - 380 | loanData.borrowedAmount; 381 | } else { 382 | // update pool's reserve amount again 383 | totalReserveAmount -= loanData.feeAmount; 384 | } 385 | 386 | // update loan's data 387 | loanData.borrowedAmount = 0; 388 | loanData.feeAmount = 0; 389 | loanData.interestAmount = 0; 390 | loanData.repayAmount = 0; 391 | loanData.timestamp = 0; 392 | 393 | return collateralAmount; 394 | } 395 | 396 | /** 397 | @dev Calculates the amount of tokens to pay for liquidating a loan by providing the pool ID and the borrower's address. 398 | @param _account The address of the borrower whose loan is being liquidated. 399 | @return The amount of tokens to pay for liquidating the loan. 400 | @notice This function retrieves the pool and loan data based on the pool ID and the borrower's address. 401 | If the pool uses Ether as collateral, the function calculates the amount of USDC to pay for liquidating the loan based on the collateral amount, the USDC/ETH price, and the discount rate. 402 | If the pool uses USDC as collateral, the function calculates the amount of USDC to pay for liquidating the loan based on the collateral amount and the discount rate. 403 | */ 404 | function getPayAmountForLiquidateLoan( 405 | address _account 406 | ) public view returns (uint256) { 407 | Loan memory loanData = loans[_account]; 408 | 409 | uint256 collateralAmount = loanData.collateralAmount; 410 | uint256 payAmount = collateralAmount 411 | .mul(DISCOUNT_RATE) 412 | .mul(USDC_DECIMAL) 413 | .div(getUsdcEthPrice()) 414 | .div(100); 415 | 416 | return payAmount; 417 | } 418 | 419 | /** 420 | @dev Returns the amount of tokens to repay for the loan of the caller by providing the pool ID. 421 | @return The amount of tokens to repay for the loan of the caller. 422 | @notice This function retrieves the loan data of the caller based on the pool ID and returns the amount of tokens to repay for the loan. 423 | */ 424 | function getRepayAmount() public view returns (uint256) { 425 | Loan memory loanData = loans[msg.sender]; 426 | return loanData.repayAmount; 427 | } 428 | 429 | /** 430 | @dev Returns the total liquidity of a pool by providing the pool ID. 431 | @return The total liquidity of the pool. 432 | @notice This function retrieves the pool data based on the pool ID and calculates the total liquidity of the pool by adding the total borrow amount and the current amount and subtracting the total reserve amount. 433 | */ 434 | function getTotalLiquidity() internal view returns (uint256) { 435 | return 436 | totalBorrowAmount.add(currentBalanceAmount).sub(totalReserveAmount); 437 | } 438 | 439 | /** 440 | @dev Returns the current USDC/ETH price from the Chainlink price feed. 441 | @return The current USDC/ETH price with 18 decimal places. 442 | @notice This function retrieves the latest round data from the Chainlink price feed for the USDC/ETH pair and returns the price with 18 decimal places. 443 | According to the documentation, the return value is a fixed point number with 18 decimals for ETH data feeds 444 | */ 445 | function getUsdcEthPrice() internal view returns (uint256) { 446 | (, int256 answer, , , ) = usdcEthPriceFeed.latestRoundData(); 447 | // Convert the USDC/ETH price to a decimal value with 18 decimal places 448 | return uint256(answer); 449 | } 450 | 451 | // Function to calculate total repayment amount including interest and fees 452 | function calculateRepaymentAmount( 453 | uint256 _loanAmount, 454 | uint256 _duration 455 | ) internal view returns (uint256, uint256, uint256) { 456 | // Calculate interest charged on the loan 457 | uint256 interestAmount = calculateInterest( 458 | _loanAmount, 459 | interestRate, 460 | _duration 461 | ); 462 | 463 | // Calculate fees charged on the loan 464 | uint256 feeAmount = (_loanAmount * reserveFeeRate) / 100; 465 | 466 | // Calculate total amount due including interest and fees 467 | uint256 repayAmount = _loanAmount + interestAmount + feeAmount; 468 | 469 | return (repayAmount, interestAmount, feeAmount); 470 | } 471 | 472 | /* 473 | function calculateLinearInterest( 474 | uint256 _rate, 475 | uint256 _fromTimestamp, 476 | uint256 _toTimestamp 477 | ) internal pure returns (uint256) { 478 | return 479 | _rate.mul(_toTimestamp.sub(_fromTimestamp)).div(SECONDS_PER_YEAR); 480 | } 481 | */ 482 | 483 | /** 484 | @dev Calculates the amount of tokens to deposit or withdraw based on the asset amount and the total liquidity of a pool by providing the pool ID. 485 | @param _assetAmount The amount of asset tokens the caller wants to deposit or withdraw. 486 | @return The amount of tokens to deposit or withdraw based on the asset amount and the total liquidity of the pool. 487 | @notice This function retrieves the pool data based on the pool ID and calculates the amount of tokens to deposit or withdraw based on the asset amount and the total liquidity of the pool. 488 | */ 489 | function calculateAmount( 490 | uint256 _assetAmount 491 | ) internal view returns (uint256) { 492 | uint256 totalLiquidityAmount = getTotalLiquidity(); 493 | 494 | uint256 amount = _assetAmount.mul(totalLiquidityAmount).divCeil( 495 | totalAssetAmount 496 | ); 497 | 498 | return amount; 499 | } 500 | 501 | /** 502 | @dev Calculates the asset amount based on the pool ID and the amount. 503 | @param _amount The amount to calculate the asset amount for. 504 | @return The calculated asset amount. 505 | */ 506 | function calculateAssetAmount( 507 | uint256 _amount 508 | ) internal view returns (uint256) { 509 | uint256 totalLiquidityAmount = getTotalLiquidity(); 510 | 511 | if (totalAssetAmount == 0 || totalLiquidityAmount == 0) return _amount; 512 | 513 | uint256 assetAmount = _amount.mul(totalAssetAmount).div( 514 | totalLiquidityAmount 515 | ); 516 | 517 | return assetAmount; 518 | } 519 | 520 | /** 521 | @dev Calculates the total interest charged on a loan based on the loan amount, interest rate, and duration. 522 | @param _loanAmount The amount of the loan. 523 | @param _interestRate The interest rate charged on the loan. 524 | @param _duration The duration of the loan. 525 | @return The total interest charged on the loan. 526 | */ 527 | function calculateInterest( 528 | uint256 _loanAmount, 529 | uint256 _interestRate, 530 | uint256 _duration 531 | ) internal pure returns (uint256) { 532 | // Calculate interest charged on the loan 533 | uint256 yearlyInterest = (_loanAmount * _interestRate) / 100; 534 | uint256 dailyInterest = yearlyInterest / 365; 535 | uint256 totalInterest = dailyInterest * _duration; 536 | 537 | return totalInterest; 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /src/libraries/Math.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.13; 3 | 4 | /** 5 | * @title Math library 6 | * @notice The math library. 7 | * @author Alpha 8 | **/ 9 | 10 | library Math { 11 | /** 12 | * @notice a ceiling division 13 | * @return the ceiling result of division 14 | */ 15 | function divCeil(uint256 a, uint256 b) internal pure returns (uint256) { 16 | require(b > 0, "divider must more than 0"); 17 | uint256 c = a / b; 18 | if (a % b != 0) { 19 | c = c + 1; 20 | } 21 | return c; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/libraries/ReEntrancyGuard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.13; 3 | 4 | contract ReEntrancyGuard { 5 | bool internal locked; 6 | 7 | modifier noReentrant() { 8 | require(!locked, "No re-entrancy"); 9 | locked = true; 10 | _; 11 | locked = false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/BaseSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.0; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | import {stdStorage, StdStorage, Test} from "forge-std/Test.sol"; 6 | 7 | import {Utils} from "./utils/Util.sol"; 8 | import {IUniswapRouter} from "./interfaces/IUniswap.sol"; 9 | import {IWETH} from "./interfaces/IWETH.sol"; 10 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 11 | 12 | contract BaseSetup is Test { 13 | address internal constant USDC_ADDRESS = 14 | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 15 | address internal constant UNISWAP_ROUTER_ADDRESS = 16 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; 17 | address internal constant WETH_ADDRESS = 18 | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 19 | 20 | // Skip forward block.timestamp for 3 days. 21 | uint256 internal constant SKIP_FORWARD_PERIOD = 3600 * 24 * 3; 22 | uint256 internal constant USDC_DECIMAL = 1e6; 23 | uint256 internal constant ETHER_DECIMAL = 1e18; 24 | 25 | address[] internal pathUSDC; 26 | 27 | Utils internal utils; 28 | 29 | address payable[] internal users; 30 | address internal alice; 31 | address internal bob; 32 | address internal carol; 33 | address internal david; 34 | address internal edward; 35 | address internal fraig; 36 | 37 | IERC20 internal usdc; 38 | IWETH internal weth; 39 | 40 | IUniswapRouter internal uniswapRouter; 41 | 42 | function setUp() public virtual { 43 | utils = new Utils(); 44 | users = utils.createUsers(6); 45 | 46 | alice = users[0]; 47 | vm.label(alice, "Alice"); 48 | 49 | bob = users[1]; 50 | vm.label(bob, "Bob"); 51 | 52 | carol = users[2]; 53 | vm.label(carol, "Carol"); 54 | 55 | david = users[3]; 56 | vm.label(david, "David"); 57 | 58 | edward = users[4]; 59 | vm.label(edward, "Edward"); 60 | 61 | fraig = users[5]; 62 | vm.label(fraig, "Fraig"); 63 | 64 | initPathForSwap(); 65 | getStableCoinBalanceForTesting(); 66 | } 67 | 68 | function initPathForSwap() internal { 69 | usdc = IERC20(USDC_ADDRESS); 70 | weth = IWETH(WETH_ADDRESS); 71 | 72 | pathUSDC = new address[](2); 73 | pathUSDC[0] = WETH_ADDRESS; 74 | pathUSDC[1] = USDC_ADDRESS; 75 | } 76 | 77 | function swapETHToToken( 78 | address[] memory _path, 79 | address _to, 80 | uint256 _amount 81 | ) internal { 82 | uint256 deadline = block.timestamp + 3600000; 83 | 84 | uniswapRouter.swapExactETHForTokens{value: _amount}( 85 | 0, 86 | _path, 87 | _to, 88 | deadline 89 | ); 90 | } 91 | 92 | function getStableCoinBalanceForTesting() internal { 93 | uint wethAmount = 50 * 1e18; 94 | 95 | uniswapRouter = IUniswapRouter(UNISWAP_ROUTER_ADDRESS); 96 | 97 | weth.approve(UNISWAP_ROUTER_ADDRESS, wethAmount * 10); 98 | 99 | swapETHToToken(pathUSDC, address(alice), wethAmount); 100 | swapETHToToken(pathUSDC, address(bob), wethAmount); 101 | swapETHToToken(pathUSDC, address(carol), wethAmount); 102 | swapETHToToken(pathUSDC, address(david), wethAmount); 103 | swapETHToToken(pathUSDC, address(fraig), wethAmount); 104 | 105 | console.log( 106 | "Alice's usdc balance = %d", 107 | usdc.balanceOf(address(alice)) 108 | ); 109 | console.log("Bob's usdc balance = %d", usdc.balanceOf(address(bob))); 110 | console.log( 111 | "Carol's usdc balance = %d", 112 | usdc.balanceOf(address(carol)) 113 | ); 114 | console.log( 115 | "David's usdc balance = %d", 116 | usdc.balanceOf(address(david)) 117 | ); 118 | console.log( 119 | "Edward's usdc balance = %d", 120 | usdc.balanceOf(address(edward)) 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /test/LendingVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/LendingVault.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | import {BaseSetup} from "./BaseSetup.sol"; 9 | 10 | contract LendingVaultTest is BaseSetup { 11 | using SafeERC20 for IERC20; 12 | 13 | LendingVault public vault; 14 | 15 | uint256 internal constant PERIOD_180_DAYS = 180; 16 | uint256 internal constant SKIP_PERIOD_181_DAYS = 181 * 3600 * 24; 17 | 18 | function setUp() public virtual override { 19 | BaseSetup.setUp(); 20 | 21 | // interest rate = 20, collateral factor = 90m and reserve fee rate = 0 22 | vault = new LendingVault(20, 90, 0); 23 | } 24 | 25 | function test_deposit() public { 26 | // when LP deposit 0, should revert 27 | vm.startPrank(alice); 28 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 29 | vm.expectRevert(LendingVault.ZeroAmountForDeposit.selector); 30 | vault.deposit(0); 31 | vm.stopPrank(); 32 | 33 | // when LP's USDC balance is less than the deposit mount, should revert 34 | vm.startPrank(edward); 35 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 36 | vm.expectRevert(LendingVault.InsufficientBalanceForDeposit.selector); 37 | vault.deposit(1000 * USDC_DECIMAL); 38 | vm.stopPrank(); 39 | 40 | // deposit 1000 USDC successfully. 41 | vm.startPrank(alice); 42 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 43 | vault.deposit(1000 * USDC_DECIMAL); 44 | vm.stopPrank(); 45 | } 46 | 47 | function test_borrowToken() public { 48 | console.log("Vault usdc balance = %d", usdc.balanceOf(address(vault))); 49 | console.log("Vault ether balance = %d", address(vault).balance); 50 | 51 | // Alice deposit 1000 USDC to USDC/ETH pool 52 | vm.startPrank(alice); 53 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 54 | vault.deposit(1000 * USDC_DECIMAL); 55 | vm.stopPrank(); 56 | 57 | // Bob deposit 1000 USDC to USDC/ETH pool 58 | vm.startPrank(bob); 59 | usdc.safeApprove(address(vault), 4000 * USDC_DECIMAL); 60 | vault.deposit(4000 * USDC_DECIMAL); 61 | vm.stopPrank(); 62 | 63 | // Bob transfer 0 Ether and borrows X USDC, should revert 64 | vm.startPrank(carol); 65 | vm.expectRevert(LendingVault.ZeroCollateralAmountForBorrow.selector); 66 | vault.borrowToken{value: 0}(0, PERIOD_180_DAYS); 67 | 68 | // Bob transfer 1 Ether, but amount is 2 Ether, should revert 69 | vm.expectRevert(LendingVault.InsufficientCollateral.selector); 70 | vault.borrowToken{value: 1 * ETHER_DECIMAL}( 71 | 2 * ETHER_DECIMAL, 72 | PERIOD_180_DAYS 73 | ); 74 | 75 | // Bob transfer 20 Ether, but LendingVault hasn't enough USDC balance, should revert 76 | vm.expectRevert(LendingVault.InsufficientTokenInBalance.selector); 77 | vault.borrowToken{value: 20 * ETHER_DECIMAL}( 78 | 20 * ETHER_DECIMAL, 79 | PERIOD_180_DAYS 80 | ); 81 | 82 | (uint256 borrowAmount, uint256 repayAmount) = vault.borrowToken{ 83 | value: 1e18 84 | }(1e18, PERIOD_180_DAYS); 85 | // Bob's borrow amount should be bigger than 1669913000 86 | assertGe(borrowAmount, 1667902000); 87 | // Bob's repay amount should be same with the vaule pulled from getRepayAmount() 88 | assertEq(repayAmount, vault.getRepayAmount()); 89 | 90 | // If Bob already borrowed once, should revert 91 | vm.expectRevert(LendingVault.AlreadyBorrowed.selector); 92 | vault.borrowToken{value: 1e18}(1e18, PERIOD_180_DAYS); 93 | 94 | vm.stopPrank(); 95 | } 96 | 97 | function test_repayLoan() public { 98 | // Alice deposit 1000 USDC 99 | vm.startPrank(alice); 100 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 101 | vault.deposit(1000 * USDC_DECIMAL); 102 | vm.stopPrank(); 103 | 104 | // Bob deposit 4000 USDC 105 | vm.startPrank(bob); 106 | usdc.safeApprove(address(vault), 4000 * USDC_DECIMAL); 107 | vault.deposit(4000 * USDC_DECIMAL); 108 | vm.stopPrank(); 109 | 110 | // Carol tries to repay 0 as the repay amount, but should revert, cause Carol hasnt a loan yet 111 | vm.startPrank(carol); 112 | vm.expectRevert(LendingVault.NotExistLoan.selector); 113 | vault.repayLoan(0); 114 | 115 | // Carol transfer 1 ether as collateral and receive X USDC 116 | vm.startPrank(carol); 117 | (uint256 borrowAmount, uint256 repayAmount) = vault.borrowToken{ 118 | value: 1e18 119 | }(1e18, 180); 120 | assertGe(borrowAmount, 0); 121 | assertEq(repayAmount, vault.getRepayAmount()); 122 | 123 | // Carol repay 0 as the repay amount, should revert 124 | vm.expectRevert(LendingVault.ZeroRepayAmount.selector); 125 | vault.repayLoan(0); 126 | 127 | uint256 balanceBefore = address(carol).balance; 128 | 129 | // Carol repay and receive his collateral 130 | usdc.safeApprove(address(vault), repayAmount); 131 | vault.repayLoan(repayAmount); 132 | 133 | // After Carol repay amount, his collateral amount should be equal with the amount. 134 | assertEq(address(carol).balance, balanceBefore + 1 * ETHER_DECIMAL); 135 | 136 | vm.stopPrank(); 137 | } 138 | 139 | function test_withdraw() public { 140 | // Alice tries to withdraw, but he hasn't deposited before, should revert 141 | vm.startPrank(alice); 142 | vm.expectRevert(LendingVault.ZeroAmountForWithdraw.selector); 143 | vault.withdraw(); 144 | 145 | // Alice deposit 1000 USDC 146 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 147 | vault.deposit(1000 * USDC_DECIMAL); 148 | 149 | uint256 balanceBefore = usdc.balanceOf(address(alice)); 150 | // Alice withdraw 151 | vault.withdraw(); 152 | 153 | // After Carol withdraw, his USDC balance should be equal with the amount that he deposit before. 154 | assertEq( 155 | usdc.balanceOf(address(alice)), 156 | balanceBefore + 1000 * USDC_DECIMAL 157 | ); 158 | vm.stopPrank(); 159 | } 160 | 161 | function test_liquidate() public { 162 | // Alice deposit 1000 USDC 163 | vm.startPrank(alice); 164 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 165 | vault.deposit(1000 * USDC_DECIMAL); 166 | vm.stopPrank(); 167 | 168 | // Bob deposit 4000 USDC 169 | vm.startPrank(bob); 170 | usdc.safeApprove(address(vault), 4000 * USDC_DECIMAL); 171 | vault.deposit(4000 * USDC_DECIMAL); 172 | vm.stopPrank(); 173 | 174 | // Alice tries to liquidate loan, but there is no loan yet, should revert 175 | vm.startPrank(alice); 176 | vm.expectRevert(LendingVault.LoanHasNoCollateral.selector); 177 | vault.liquidate(address(carol)); 178 | vm.stopPrank(); 179 | 180 | // Carol transfer 1 ether as collateral and receive X USDC 181 | vm.startPrank(carol); 182 | (uint256 borrowAmount, uint256 repayAmount) = vault.borrowToken{ 183 | value: 1 * ETHER_DECIMAL 184 | }(1 * ETHER_DECIMAL, PERIOD_180_DAYS); 185 | assertGe(borrowAmount, 0); 186 | assertEq(repayAmount, vault.getRepayAmount()); 187 | 188 | // Carol tries to liquidate his loan, should revert 189 | vm.expectRevert(LendingVault.NotAvailableForLoanOwner.selector); 190 | vault.liquidate(address(carol)); 191 | 192 | vm.stopPrank(); 193 | 194 | vm.startPrank(alice); 195 | // Alice tries to liquidate carol loan, but the loan is not in liquidate, should revert 196 | vm.expectRevert(LendingVault.LoanNotInLiquidate.selector); 197 | vault.liquidate(address(carol)); 198 | vm.stopPrank(); 199 | 200 | // Skip 181 days 201 | skip(SKIP_PERIOD_181_DAYS); 202 | 203 | // Edward tries to liquidate Carol's loan, but he hasn't sufficient balance, should revert 204 | vm.startPrank(edward); 205 | vm.expectRevert(LendingVault.InsufficientBalanceForLiquidate.selector); 206 | vault.liquidate(address(carol)); 207 | vm.stopPrank(); 208 | 209 | // Alice liquidate Carol's loan 210 | vm.startPrank(alice); 211 | usdc.safeApprove( 212 | address(vault), 213 | vault.getPayAmountForLiquidateLoan(address(carol)) 214 | ); 215 | uint256 collateralAmount = vault.liquidate(address(carol)); 216 | assertEq(collateralAmount, 1 * ETHER_DECIMAL); 217 | vm.stopPrank(); 218 | } 219 | 220 | function test_integrateTest() public { 221 | uint256 balanceBefore; 222 | uint256 balanceAfter; 223 | uint256 reward; 224 | 225 | // Alice deposit 1000 USDC 226 | vm.startPrank(alice); 227 | usdc.safeApprove(address(vault), 1000 * USDC_DECIMAL); 228 | vault.deposit(1000 * USDC_DECIMAL); 229 | vm.stopPrank(); 230 | 231 | // Bob deposit 4000 USDC 232 | vm.startPrank(bob); 233 | usdc.safeApprove(address(vault), 4000 * USDC_DECIMAL); 234 | vault.deposit(4000 * USDC_DECIMAL); 235 | vm.stopPrank(); 236 | 237 | // Carol transfer 1 ether as collateral and receive X USDC 238 | vm.startPrank(carol); 239 | (uint256 borrowAmount, uint256 repayAmount) = vault.borrowToken{ 240 | value: 1e18 241 | }(1e18, 180); 242 | assertGe(borrowAmount, 0); 243 | assertEq(repayAmount, vault.getRepayAmount()); 244 | vm.stopPrank(); 245 | 246 | // David deposit 5000 USDC 247 | vm.startPrank(david); 248 | balanceBefore = usdc.balanceOf(address(david)); 249 | 250 | usdc.safeApprove(address(vault), 5000 * USDC_DECIMAL); 251 | vault.deposit(5000 * USDC_DECIMAL); 252 | // David withdraw his deposit again 253 | vault.withdraw(); 254 | 255 | balanceAfter = usdc.balanceOf(address(david)); 256 | console.log("David's balance before = %d", balanceBefore); 257 | console.log("David's balance after = %d", balanceAfter); 258 | reward = balanceAfter - balanceBefore; 259 | //David's reward is 0, because he deposited, after Carol borrow 260 | assertEq(reward, 0); 261 | vm.stopPrank(); 262 | 263 | /* 264 | Alice's reward should be greater than zero, because Carol borrow Ether 265 | Alice deposit = 1000, his asset = 1000 266 | Bob deposit 4000 = 4000, his asset = 4000 267 | Carol's collateral = 1 ether, latestRoundData = 538949909995365 (0.00053894) 268 | Carol's borrow amount = 1 ether(10 ** 18) * 90 % (pool's collateral factor) * 10 ** 6 (Usdc decimal) / 538949909995365 (latestRoundData) / 100 269 | Then, Carol's borrow amount is 1669914000 ( 1669.914 USDC ) 270 | And, Carol's repay amount is 1834.617780 USDC (1834617780) = 1669.914 + 164.703780 (interest amount) + 0 ( fee rate is 0) 271 | Based on Carol's interest amount 164.703780, 272 | Alice's reward = Alice's asset amount * (Pool's current amount + Pool's total borrow amount - Pool's total reserve amount) 273 | Alice's withdraw amount = 1000000000 * ((5000000000 - 1669914000) + 1834617780 - 0) / 5000000000 = 1032940756 (1032.940756) 274 | Alice's reward = 1032.940756 - 1000 = 32.940756 275 | Bob's reward = 164.703780 - 32.940756 = 131.763024 276 | */ 277 | 278 | // Alice withdraw and get reward 279 | balanceBefore = usdc.balanceOf(address(alice)); 280 | vm.startPrank(alice); 281 | vault.withdraw(); 282 | vm.stopPrank(); 283 | balanceAfter = usdc.balanceOf(address(alice)); 284 | reward = balanceAfter - balanceBefore - 1000 * USDC_DECIMAL; 285 | assertGe(reward, 0); 286 | console.log("Alice's reward = %d", reward); 287 | 288 | // David deposit 5000 USDC 289 | vm.startPrank(fraig); 290 | usdc.safeApprove(address(vault), 3000 * USDC_DECIMAL); 291 | vault.deposit(3000 * USDC_DECIMAL); 292 | vm.stopPrank(); 293 | 294 | // Alice withdraw and get reward 295 | balanceBefore = usdc.balanceOf(address(bob)); 296 | vm.startPrank(bob); 297 | vault.withdraw(); 298 | vm.stopPrank(); 299 | 300 | balanceAfter = usdc.balanceOf(address(bob)); 301 | reward = balanceAfter - balanceBefore - 4000 * USDC_DECIMAL; 302 | assertGe(reward, 0); 303 | console.log("Bob's reward = %d", reward); 304 | 305 | // Carol repay and receive his collateral 306 | vm.startPrank(carol); 307 | usdc.safeApprove(address(vault), repayAmount); 308 | vault.repayLoan(repayAmount); 309 | vm.stopPrank(); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /test/interfaces/IUniswap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapRouter { 5 | function swapExactETHForTokens( 6 | uint amountOutMin, 7 | address[] calldata path, 8 | address to, 9 | uint deadline 10 | ) external payable returns (uint[] memory amounts); 11 | } 12 | -------------------------------------------------------------------------------- /test/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function transfer(address to, uint value) external returns (bool); 8 | 9 | function withdraw(uint) external; 10 | 11 | function approve(address, uint) external; 12 | } 13 | -------------------------------------------------------------------------------- /test/utils/Util.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | contract Utils is Test { 7 | bytes32 internal nextUser = keccak256(abi.encodePacked("user address")); 8 | 9 | function getNextUserAddress() external returns (address payable) { 10 | address payable user = payable(address(uint160(uint256(nextUser)))); 11 | nextUser = keccak256(abi.encodePacked(nextUser)); 12 | return user; 13 | } 14 | 15 | // create users with 100 ETH balance each 16 | function createUsers( 17 | uint256 userNum 18 | ) external returns (address payable[] memory) { 19 | address payable[] memory users = new address payable[](userNum); 20 | for (uint256 i = 0; i < userNum; i++) { 21 | address payable user = this.getNextUserAddress(); 22 | vm.deal(user, 100 ether); 23 | users[i] = user; 24 | } 25 | 26 | return users; 27 | } 28 | 29 | // move block.number forward by a given number of blocks 30 | function mineBlocks(uint256 numBlocks) external { 31 | uint256 targetBlock = block.number + numBlocks; 32 | vm.roll(targetBlock); 33 | } 34 | } 35 | --------------------------------------------------------------------------------