diff --git a/foundry.toml b/foundry.toml index 55846add..4ecdee2d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -evm_version = "cancun" +evm_version = "prague" libs = ['lib'] optimizer = true optimizer_runs = 10000 diff --git a/lib/forge-std b/lib/forge-std index 52715a21..77041d2c 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 52715a217dc51d0de15877878ab8213f6cbbbab5 +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/scripts/borrowControllerSetup.s.sol b/scripts/borrowControllerSetup.s.sol index ccd01291..e1fd14f8 100644 --- a/scripts/borrowControllerSetup.s.sol +++ b/scripts/borrowControllerSetup.s.sol @@ -1,6 +1,7 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; -import {Market} from "src/Market.sol"; +import {Market, IERC20} from "src/Market.sol"; +import {IMarket} from "src/interfaces/IMarket.sol"; import {BorrowController} from "src/BorrowController.sol"; import "src/DBR.sol"; @@ -11,44 +12,96 @@ interface IBorrowController { function setOperator(address gov) external; } +interface IBC is IBorrowController { + function minDebts(address market) external returns(uint); +} + +interface IErc20 is IERC20 { + function name() external view returns(string memory); +} contract borrowControllerSetup is Script { address gov = 0x926dF14a23BE491164dCF93f4c468A50ef659D5B; address deployerAddress = 0x11EC78492D53c9276dD7a184B1dbfB34E50B710D; - IBorrowController oldBorrowController = IBorrowController(0x20C7349f6D6A746a25e66f7c235E96DAC880bc0D); - BorrowController newBorrowController; // = BorrowController(0x81ff13c46f363D13fC25FB801a4335c6097B7862); + //IBC oldBorrowController = IBC(0x2DbAd53A647A86b8988E007a33FE78bd55e9Dd6f); + BorrowController newBorrowController; DolaBorrowingRights DBR = DolaBorrowingRights(0xAD038Eb671c44b853887A7E32528FaB35dC5D710); address[] markets = [ - 0x93685185666c8D34ad4c574B3DBF41231bbfB31b, //cvxFxs 0x3474ad0e3a9775c9F68B415A7a9880B0CAB9397a, //cvxCrv 0x63fAd99705a255fE2D500e498dbb3A9aE5AA1Ee8, //crv - 0x7Cd3ab8354289BEF52c84c2BF0A54E3608e66b37, //gohm 0xb516247596Ca36bf32876199FBdCaD6B3322330B, //inv - 0x743A502cf0e213F6FEE56cD9C6B03dE7Fa951dCf, //steth 0x63Df5e23Db45a2066508318f172bA45B9CD37035, //weth - 0x27b6c301Fd441f3345d61B7a4245E1F823c3F9c4 //stycrv + 0x27b6c301Fd441f3345d61B7a4245E1F823c3F9c4, //stycrv + 0xB907Dcc926b5991A149d04Cb7C0a4a25dC2D8f9a, //deUSD-DOLA + 0x4f5ea72d932f554f08e97CB78DD25F8AAE43C08e, //yv-deUSD-DOLA + 0xD68d3a44d46dd50BFeBa8Cca544717B76e7C4b29, //sUSDS-sDOLA + 0xb427fC22561f3963B04202F9bb5BCEbd76c14A99, //sUSDE-sDOLA + 0x4A33baFA8a31E4ec9649f65646022cAD1957808b, //yv-sUSDS-sDOLA + 0x2fed508aAc87c0e6f0b647Fe83164A7AA6eb2FC9, //scrvUSD-DOLA + 0x5bb8f6aAcFF2971B42F9fE6945D24726A2541CF2, //yv-scrvUSD-DOLA + 0x4E264618dC015219CD83dbc53B31251D73c2db1a, //yv-sUSDE-sDOLA + 0x63D27fC9d463Ed727676367D3F818999962737E8, //scrvUSD-sDOLA + 0xb8bc1E9c0a2d445bc39d2A745F47619E954dD565, //yv-scrvUSD-sDOLA + 0x0971B1690d101169BFca4715897aD3a9b3C39b26, //DAI + 0x3FD3daBB9F9480621C8A111603D3Ba70F17550BC, //wstETH + 0x79eF6d28C41e47A588E2F2ffB4140Eb6d952AEc4, //sUSDe + 0x6A522f3BD3fDA15e74180953f203cf55aA6C631E, //crvUSD-DOLA + 0x2A256306D8ba899E33B01e495982656884Ac77FF, //cbBTC + 0xe85943e280776254ee6C9801553B93F10Ef4C99C, //yv-crvUSD-DOLA + 0xFEA3A862eE4b3F9b6015581d6d2D25AF816C54f1, //sFRAX + 0x0DFE3D04536a74Dd532dd0cEf5005bA14c5f4112, //pt-sUSDe-mar27 + 0x0c0bb843FAbda441edeFB93331cFff8EC92bD168, //st-yETH + 0x4797A68c8feB383c3372c0e098533aCf8eD95B26, //FraxPyUsd-DOLA + 0x29fe42F4F71Ba5b9a7aaE794468e7ca4128a93b8, //COMP + 0xf013D998D4cf7f45547958094F1EEE75Ca43c4f5, //yv-FraxPyUsd-DOLA + 0x87df9A00f0e4908e61756d2Fcb348ADF95Ce72Ea, //FraxBP-DOLA + 0x8205bE13cC245740F9EA23Dc88a9B56206bEC0e3 //yv-FraxBP-DOLA ]; + //0x743A502cf0e213F6FEE56cD9C6B03dE7Fa951dCf, //steth + //0x7Cd3ab8354289BEF52c84c2BF0A54E3608e66b37, //gohm function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.broadcast(deployerPrivateKey); - newBorrowController = new BorrowController(deployerAddress, address(DBR)); - + vm.createSelectFork(vm.envString("RPC_MAINNET")); + vm.broadcast(deployerPrivateKey); + newBorrowController = new BorrowController(gov, address(DBR)); + /** for(uint i; i < markets.length; ++i){ address market = markets[i]; require(DBR.markets(market), "Not a market"); + console.log("Market:", IErc20(IMarket(market).collateral()).name()); + IBC oldBorrowController = IBC(IMarket(market).borrowController()); + //console.log("Old BorrowController:", address(oldBorrowController)); + uint newLimit = newBorrowController.dailyLimits(market); + uint newMinDebt = newBorrowController.minDebts(market); uint oldLimit = oldBorrowController.dailyLimits(market); - vm.broadcast(deployerPrivateKey); - newBorrowController.setDailyLimit(market, oldLimit); + uint oldMinDebt = oldBorrowController.minDebts(market); + if(newLimit != oldLimit || newMinDebt != oldMinDebt){ + console.log("Old Limit :", oldLimit); + console.log("Old MinDebt:", oldMinDebt); + console.log("New Limit :", newLimit); + console.log("New MinDebt:", newMinDebt); + //vm.startBroadcast(deployerPrivateKey); + //newBorrowController.setDailyLimit(market, oldLimit); + //newBorrowController.setMinDebt(market, oldMinDebt); + //vm.stopBroadcast(); + } + console.log("----------------"); } - + */ //Add helper contract to allowList - vm.broadcast(deployerPrivateKey); - newBorrowController.allow(0xae8165f37FC453408Fb1cd1064973df3E6499a76); + /* + vm.startBroadcast(deployerPrivateKey); + newBorrowController.allow(0x0aBb47c564296D34B0F5B068361985f507fe123c); + newBorrowController.allow(0x0591926d5d3b9Cc48ae6eFB8Db68025ddc3adFA5); + newBorrowController.allow(0x496a3Fc15209350487F7136b7c3c163F9204eE70); + newBorrowController.allow(0x495886947EAce9788360F46be55c758f92Ecd074); + newBorrowController.allow(0x5233f4C2515ae21B540c438862Abb5603506dEBC); + //Transfer ownership to gov - vm.broadcast(deployerPrivateKey); newBorrowController.setOperator(gov); + */ } } diff --git a/src/BorrowController.sol b/src/BorrowController.sol index abaaf803..c52b5b80 100644 --- a/src/BorrowController.sol +++ b/src/BorrowController.sol @@ -100,7 +100,8 @@ contract BorrowController { uint lastUpdated = DBR.lastUpdated(borrower); uint debts = DBR.debts(borrower); //Check to prevent effects of edge case bug - if(lastUpdated > 0 && debts == 0 && lastUpdated != block.timestamp){ + uint timeElapsed = block.timestamp - lastUpdated; + if(lastUpdated > 0 && debts * timeElapsed < 365 days && lastUpdated != block.timestamp){ //Important check, otherwise a user could repeatedly mint themsevles DBR require(DBR.markets(msg.sender), "Message sender is not a market"); uint deficit = (block.timestamp - lastUpdated) * amount / 365 days; @@ -114,7 +115,7 @@ contract BorrowController { //If the chainlink oracle price feed is stale, deny borrow if(isPriceStale(msg.sender)) return false; //If the message sender is not a contract, then there's no need check allowlist - if(msgSender == tx.origin) return true; + if(msgSender == tx.origin && msgSender.code.length == 0) return true; return contractAllowlist[msgSender]; } diff --git a/test/BorrowController.t.sol b/test/BorrowController.t.sol index 6473b09e..f7fdfc91 100644 --- a/test/BorrowController.t.sol +++ b/test/BorrowController.t.sol @@ -18,6 +18,22 @@ contract BorrowContractTxOrigin { market.borrow((AMOUNT * COLLATERAL_FACTOR_BPS * PRICE) / BPS_BASIS); } } +contract BatchApprove { + IDBR immutable dbr; + + constructor(address _dbr){ + dbr = IDBR(_dbr); + } + + function approveDepositAndBorrow(address _market, uint depositAmount, uint borrowAmount) external { + require(msg.sender == address(this), "Invalid authority"); + require(tx.origin == address(this), "Invalid origin"); + require(dbr.markets(_market), "Invalid market"); + IMarket market = IMarket(_market); + IERC20(market.collateral()).approve(_market, depositAmount); + market.depositAndBorrow(depositAmount, borrowAmount); + } +} contract BorrowControllerTest is FiRMBaseTest { BorrowContract borrowContract; @@ -170,6 +186,130 @@ contract BorrowControllerTest is FiRMBaseTest { ); } + function test_BorrowAllowed_True_Where_EdgeCaseBugDebtNonZero() + public + { + vm.prank(gov); + borrowController.setDailyLimit(address(market), 2000 ether); + uint testAmount = 1e18; + gibWeth(user, testAmount); + uint halfBorrow = getMaxBorrowAmount(testAmount) / 2; + gibDOLA(address(market), halfBorrow * 2); + vm.startPrank(user, user); + deposit(testAmount); + market.borrow(halfBorrow); + market.repay(user, halfBorrow-1); + vm.stopPrank(); + + assertEq(market.debts(user), 1, "User debt not 1"); + assertEq(dbr.balanceOf(user), 0, "DBR balance of user is not 0 before time skip"); + vm.warp(block.timestamp + 30 days); + ethFeed.changeUpdatedAt(block.timestamp); + vm.prank(gov); + dbr.addMinter(address(borrowController)); + vm.prank(address(market), user); + assertTrue( + borrowController.borrowAllowed(user, user, 1) + ); + vm.startPrank(user, user); + assertGt(market.getCreditLimit(user), 0, "User has no credit limit"); + market.borrow(market.getCreditLimit(user) / 100); + assertLe(dbr.deficitOf(user), 30 days * 1, "Deficit of user more than expected"); + assertEq(dbr.balanceOf(user), 0, "DBR balance of user is not 0"); + } + + function test_BorrowAllowed_True_Where_EdgeCaseBugDebtNonZero365Days() + public + { + vm.prank(gov); + borrowController.setDailyLimit(address(market), 2000 ether); + uint testAmount = 1e18; + gibWeth(user, testAmount); + uint halfBorrow = getMaxBorrowAmount(testAmount) / 2; + gibDOLA(address(market), halfBorrow * 2); + vm.startPrank(user, user); + deposit(testAmount); + market.borrow(halfBorrow); + market.repay(user, halfBorrow-1); + vm.stopPrank(); + + assertEq(market.debts(user), 1, "User debt not 1"); + assertEq(dbr.balanceOf(user), 0, "DBR balance of user is not 0 before time skip"); + vm.warp(block.timestamp + 365 days); + ethFeed.changeUpdatedAt(block.timestamp); + vm.prank(gov); + dbr.addMinter(address(borrowController)); + vm.prank(address(market), user); + assertTrue( + borrowController.borrowAllowed(user, user, 1) + ); + vm.startPrank(user, user); + assertGt(market.getCreditLimit(user), 0, "User has no credit limit"); + uint creditLimit = market.getCreditLimit(user); + vm.expectRevert("DBR Deficit"); + market.borrow(creditLimit / 100); + dbr.accrueDueTokens(user); + assertEq(dbr.deficitOf(user), 1); + } + + + function test_BorrowAllowed_True_Where_EdgeCaseBugDebtNonZeroFuzz(uint timeElapsed) + public + { + vm.prank(gov); + borrowController.setDailyLimit(address(market), 2000 ether); + uint timeElapsed = timeElapsed % 365 days; + uint testAmount = 1e18; + gibWeth(user, testAmount); + uint halfBorrow = getMaxBorrowAmount(testAmount) / 2; + gibDOLA(address(market), halfBorrow * 2); + vm.startPrank(user, user); + deposit(testAmount); + market.borrow(halfBorrow); + market.repay(user, halfBorrow-1); + vm.stopPrank(); + + assertEq(market.debts(user), 1, "User debt not 1"); + assertEq(dbr.balanceOf(user), 0, "DBR balance of user is not 0 before time skip"); + vm.warp(block.timestamp + timeElapsed); + ethFeed.changeUpdatedAt(block.timestamp); + vm.prank(gov); + dbr.addMinter(address(borrowController)); + vm.prank(address(market), user); + assertTrue( + borrowController.borrowAllowed(user, user, 1) + ); + vm.startPrank(user, user); + assertGt(market.getCreditLimit(user), 0, "User has no credit limit"); + market.borrow(market.getCreditLimit(user) / 100); + assertLe(dbr.deficitOf(user), timeElapsed * 1, "Deficit of user more than expected"); + assertEq(dbr.balanceOf(user), 0, "DBR balance of user is not 0"); + } + + function test_BorrowAllowed_False_Where_EdgeCaseBugTriggeredWithMinimalDebt() + public + { + vm.prank(gov); + borrowController.setDailyLimit(address(market), 2000 ether); + uint testAmount = 1e18; + gibWeth(user, testAmount); + uint maxBorrow = getMaxBorrowAmount(testAmount); + gibDOLA(address(market), maxBorrow); + vm.startPrank(user, user); + deposit(testAmount); + market.borrow(maxBorrow); + market.repay(user, maxBorrow-1); + vm.stopPrank(); + + vm.warp(block.timestamp + 1); + vm.prank(address(market), user); + assertFalse( + borrowController.borrowAllowed(user, user, 1), + "User was allowed to borrow" + ); + } + + function test_BorrowAllowed_True_Where_EdgeCaseBugTriggeredAndAMinter() public { @@ -231,6 +371,22 @@ contract BorrowControllerTest is FiRMBaseTest { vm.stopPrank(); } + function test_borrowAllowed_False_whenCalledByEIP7702tx() public { + uint ALICE_PK = 1; + address ALICE = vm.addr(ALICE_PK); + BatchApprove implementation = new BatchApprove(address(dbr)); + //Simulated borrow + uint testAmount = 1e18; + gibWeth(ALICE, testAmount); + uint maxBorrow = getMaxBorrowAmount(testAmount); + gibDOLA(address(market), maxBorrow); + vm.signAndAttachDelegation(address(implementation), ALICE_PK); + vm.startPrank(ALICE, ALICE); + vm.expectRevert("Denied by borrow controller"); + BatchApprove(ALICE).approveDepositAndBorrow(address(market), testAmount, maxBorrow); + vm.stopPrank(); + } + function test_BorrowAllowed_False_Where_DebtIsBelowMininimum() public { vm.startPrank(gov); borrowController.setMinDebt(address(market), 1 ether); @@ -312,6 +468,9 @@ contract BorrowControllerTest is FiRMBaseTest { ); } + + //Access Control + function test_setDailyLimit() public { vm.expectRevert(onlyOperatorLowercase); borrowController.setDailyLimit(address(0), 1); @@ -321,8 +480,6 @@ contract BorrowControllerTest is FiRMBaseTest { assertEq(borrowController.dailyLimits(address(0)), 1); } - //Access Control - function test_accessControl_setOperator() public { vm.prank(gov); borrowController.setOperator(address(0)); diff --git a/test/marketForkTests/MarketBaseForkTest.sol b/test/marketForkTests/MarketBaseForkTest.sol index f8c64ece..a24e541b 100644 --- a/test/marketForkTests/MarketBaseForkTest.sol +++ b/test/marketForkTests/MarketBaseForkTest.sol @@ -135,8 +135,6 @@ abstract contract MarketBaseForkTest is MarketForkTest { deposit(testAmount); uint borrowAmount = market.getCreditLimit(user) / 2; - uint timestamp = block.timestamp; - vm.warp(timestamp + 0.5 hours); uint dbrBal = dbr.balanceOf(user); market.borrow(borrowAmount); assertEq( @@ -144,7 +142,7 @@ abstract contract MarketBaseForkTest is MarketForkTest { testAmount, "DBR balance burned immediately after borrow" ); - vm.warp(timestamp + 0.5 hours + 1); + vm.warp(block.timestamp + 1); dbr.accrueDueTokens(user); assertEq( dbr.balanceOf(user),