diff --git a/certora/specs/Blue.spec b/certora/specs/Blue.spec index fb864b4af..505fba105 100644 --- a/certora/specs/Blue.spec +++ b/certora/specs/Blue.spec @@ -1,17 +1,14 @@ methods { - function supply(Blue.Market, uint256 amount) external; + function _.borrowRate(Blue.Market) external => DISPATCHER(true); - - function _.borrowRate(Blue.Market) external returns (uint256) => DISPATCH(true); - - function _.safeTransfer(address, uint256) internal returns (bool) envfree => DISPATCH(true); - function _.safeTransferFrom(address, address, uint256) internal returns (bool) envfree => DISPATCH(true); + function _.transfer(address, uint256) external => DISPATCHER(true); + function _.transferFrom(address, address, uint256) external => DISPATCHER(true); } rule supplyRevertZero(Blue.Market market) { env e; - supply@withrevert(market, 0); + supply@withrevert(e, market, 0, e.msg.sender); assert lastReverted; } diff --git a/src/Blue.sol b/src/Blue.sol index 35b849d8f..2f9b12dde 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -3,41 +3,25 @@ pragma solidity 0.8.20; import {IIrm} from "src/interfaces/IIrm.sol"; import {IERC20} from "src/interfaces/IERC20.sol"; -import {IOracle} from "src/interfaces/IOracle.sol"; import {MathLib} from "src/libraries/MathLib.sol"; +import {Id, Market, MarketLib} from "src/libraries/MarketLib.sol"; import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol"; uint256 constant WAD = 1e18; uint256 constant ALPHA = 0.5e18; -// Market id. -type Id is bytes32; - -// Market. -struct Market { - IERC20 borrowableAsset; - IERC20 collateralAsset; - IOracle borrowableOracle; - IOracle collateralOracle; - IIrm irm; - uint256 lltv; -} - -using {toId} for Market; - -function toId(Market memory market) pure returns (Id) { - return Id.wrap(keccak256(abi.encode(market))); -} - contract Blue { using MathLib for uint256; + using MarketLib for Market; using SafeTransferLib for IERC20; // Storage. // Owner. address public owner; + // Fee recipient. + address public feeRecipient; // User' supply balances. mapping(Id => mapping(address => uint256)) public supplyShare; // User' borrow balances. @@ -54,10 +38,14 @@ contract Blue { mapping(Id => uint256) public totalBorrowShares; // Interests last update (used to check if a market has been created). mapping(Id => uint256) public lastUpdate; + // Fee. + mapping(Id => uint256) public fee; // Enabled IRMs. mapping(IIrm => bool) public isIrmEnabled; // Enabled LLTVs. mapping(uint256 => bool) public isLltvEnabled; + // User's managers. + mapping(address => mapping(address => bool)) public isApproved; // Constructor. @@ -87,10 +75,22 @@ contract Blue { isLltvEnabled[lltv] = true; } + // @notice It is the owner's responsibility to ensure a fee recipient is set before setting a non-zero fee. + function setFee(Market calldata market, uint256 newFee) external onlyOwner { + Id id = market.id(); + require(lastUpdate[id] != 0, "unknown market"); + require(newFee <= WAD, "fee must be <= 1"); + fee[id] = newFee; + } + + function setFeeRecipient(address recipient) external onlyOwner { + feeRecipient = recipient; + } + // Markets management. - function createMarket(Market memory market) external { - Id id = market.toId(); + function createMarket(Market calldata market) external { + Id id = market.id(); require(isIrmEnabled[market.irm], "IRM not enabled"); require(isLltvEnabled[market.lltv], "LLTV not enabled"); require(lastUpdate[id] == 0, "market already exists"); @@ -100,19 +100,19 @@ contract Blue { // Supply management. - function supply(Market memory market, uint256 amount) external { - Id id = market.toId(); + function supply(Market calldata market, uint256 amount, address onBehalf) external { + Id id = market.id(); require(lastUpdate[id] != 0, "unknown market"); require(amount != 0, "zero amount"); accrueInterests(market, id); if (totalSupply[id] == 0) { - supplyShare[id][msg.sender] = WAD; + supplyShare[id][onBehalf] = WAD; totalSupplyShares[id] = WAD; } else { uint256 shares = amount.wMul(totalSupplyShares[id]).wDiv(totalSupply[id]); - supplyShare[id][msg.sender] += shares; + supplyShare[id][onBehalf] += shares; totalSupplyShares[id] += shares; } @@ -121,15 +121,16 @@ contract Blue { market.borrowableAsset.safeTransferFrom(msg.sender, address(this), amount); } - function withdraw(Market memory market, uint256 amount) external { - Id id = market.toId(); + function withdraw(Market calldata market, uint256 amount, address onBehalf) external { + Id id = market.id(); require(lastUpdate[id] != 0, "unknown market"); require(amount != 0, "zero amount"); + require(isSenderOrIsApproved(onBehalf), "not approved"); accrueInterests(market, id); uint256 shares = amount.wMul(totalSupplyShares[id]).wDiv(totalSupply[id]); - supplyShare[id][msg.sender] -= shares; + supplyShare[id][onBehalf] -= shares; totalSupplyShares[id] -= shares; totalSupply[id] -= amount; @@ -141,39 +142,40 @@ contract Blue { // Borrow management. - function borrow(Market memory market, uint256 amount) external { - Id id = market.toId(); + function borrow(Market calldata market, uint256 amount, address onBehalf) external { + Id id = market.id(); require(lastUpdate[id] != 0, "unknown market"); require(amount != 0, "zero amount"); + require(isSenderOrIsApproved(onBehalf), "not approved"); accrueInterests(market, id); if (totalBorrow[id] == 0) { - borrowShare[id][msg.sender] = WAD; + borrowShare[id][onBehalf] = WAD; totalBorrowShares[id] = WAD; } else { uint256 shares = amount.wMul(totalBorrowShares[id]).wDiv(totalBorrow[id]); - borrowShare[id][msg.sender] += shares; + borrowShare[id][onBehalf] += shares; totalBorrowShares[id] += shares; } totalBorrow[id] += amount; - require(isHealthy(market, id, msg.sender), "not enough collateral"); + require(isHealthy(market, id, onBehalf), "not enough collateral"); require(totalBorrow[id] <= totalSupply[id], "not enough liquidity"); market.borrowableAsset.safeTransfer(msg.sender, amount); } - function repay(Market memory market, uint256 amount) external { - Id id = market.toId(); + function repay(Market calldata market, uint256 amount, address onBehalf) external { + Id id = market.id(); require(lastUpdate[id] != 0, "unknown market"); require(amount != 0, "zero amount"); accrueInterests(market, id); uint256 shares = amount.wMul(totalBorrowShares[id]).wDiv(totalBorrow[id]); - borrowShare[id][msg.sender] -= shares; + borrowShare[id][onBehalf] -= shares; totalBorrowShares[id] -= shares; totalBorrow[id] -= amount; @@ -184,36 +186,37 @@ contract Blue { // Collateral management. /// @dev Don't accrue interests because it's not required and it saves gas. - function supplyCollateral(Market memory market, uint256 amount) external { - Id id = market.toId(); + function supplyCollateral(Market calldata market, uint256 amount, address onBehalf) external { + Id id = market.id(); require(lastUpdate[id] != 0, "unknown market"); require(amount != 0, "zero amount"); // Don't accrue interests because it's not required and it saves gas. - collateral[id][msg.sender] += amount; + collateral[id][onBehalf] += amount; market.collateralAsset.safeTransferFrom(msg.sender, address(this), amount); } - function withdrawCollateral(Market memory market, uint256 amount) external { - Id id = market.toId(); + function withdrawCollateral(Market calldata market, uint256 amount, address onBehalf) external { + Id id = market.id(); require(lastUpdate[id] != 0, "unknown market"); require(amount != 0, "zero amount"); + require(isSenderOrIsApproved(onBehalf), "not approved"); accrueInterests(market, id); - collateral[id][msg.sender] -= amount; + collateral[id][onBehalf] -= amount; - require(isHealthy(market, id, msg.sender), "not enough collateral"); + require(isHealthy(market, id, onBehalf), "not enough collateral"); market.collateralAsset.safeTransfer(msg.sender, amount); } // Liquidation. - function liquidate(Market memory market, address borrower, uint256 seized) external { - Id id = market.toId(); + function liquidate(Market calldata market, address borrower, uint256 seized) external { + Id id = market.id(); require(lastUpdate[id] != 0, "unknown market"); require(seized != 0, "zero amount"); @@ -246,9 +249,19 @@ contract Blue { market.borrowableAsset.safeTransferFrom(msg.sender, address(this), repaid); } + // Position management. + + function setApproval(address manager, bool isAllowed) external { + isApproved[msg.sender][manager] = isAllowed; + } + + function isSenderOrIsApproved(address user) internal view returns (bool) { + return msg.sender == user || isApproved[user][msg.sender]; + } + // Interests management. - function accrueInterests(Market memory market, Id id) private { + function accrueInterests(Market calldata market, Id id) private { uint256 marketTotalBorrow = totalBorrow[id]; if (marketTotalBorrow != 0) { @@ -256,6 +269,14 @@ contract Blue { uint256 accruedInterests = marketTotalBorrow.wMul(borrowRate).wMul(block.timestamp - lastUpdate[id]); totalBorrow[id] = marketTotalBorrow + accruedInterests; totalSupply[id] += accruedInterests; + + if (fee[id] != 0) { + uint256 feeAmount = accruedInterests.wMul(fee[id]); + // The fee amount is subtracted from the total supply in this calculation to compensate for the fact that total supply is already updated. + uint256 feeShares = feeAmount.wMul(totalSupplyShares[id]).wDiv(totalSupply[id] - feeAmount); + supplyShare[id][feeRecipient] += feeShares; + totalSupplyShares[id] += feeShares; + } } lastUpdate[id] = block.timestamp; @@ -263,7 +284,7 @@ contract Blue { // Health check. - function isHealthy(Market memory market, Id id, address user) private view returns (bool) { + function isHealthy(Market calldata market, Id id, address user) private view returns (bool) { uint256 borrowShares = borrowShare[id][user]; if (borrowShares == 0) return true; // totalBorrowShares[id] > 0 when borrowShares > 0. diff --git a/src/interfaces/IIrm.sol b/src/interfaces/IIrm.sol index 1e9fbedd8..128fcba55 100644 --- a/src/interfaces/IIrm.sol +++ b/src/interfaces/IIrm.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.5.0; -import {Market} from "src/Blue.sol"; +import {Market} from "src/libraries/MarketLib.sol"; interface IIrm { - function borrowRate(Market memory market) external returns (uint256); + function borrowRate(Market calldata market) external returns (uint256); } diff --git a/src/libraries/MarketLib.sol b/src/libraries/MarketLib.sol new file mode 100644 index 000000000..4ca6f47a5 --- /dev/null +++ b/src/libraries/MarketLib.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {IIrm} from "src/interfaces/IIrm.sol"; +import {IERC20} from "src/interfaces/IERC20.sol"; +import {IOracle} from "src/interfaces/IOracle.sol"; + +type Id is bytes32; + +struct Market { + IERC20 borrowableAsset; + IERC20 collateralAsset; + IOracle borrowableOracle; + IOracle collateralOracle; + IIrm irm; + uint256 lltv; +} + +library MarketLib { + function id(Market calldata market) internal pure returns (Id) { + return Id.wrap(keccak256(abi.encode(market))); + } +} diff --git a/src/mocks/IrmMock.sol b/src/mocks/IrmMock.sol index dff266280..894582042 100644 --- a/src/mocks/IrmMock.sol +++ b/src/mocks/IrmMock.sol @@ -1,12 +1,16 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.20; +import {IIrm} from "src/interfaces/IIrm.sol"; + import {MathLib} from "src/libraries/MathLib.sol"; +import {Id, Market, MarketLib} from "src/libraries/MarketLib.sol"; -import "src/Blue.sol"; +import {Blue} from "src/Blue.sol"; contract IrmMock is IIrm { using MathLib for uint256; + using MarketLib for Market; Blue public immutable blue; @@ -14,8 +18,8 @@ contract IrmMock is IIrm { blue = Blue(blueInstance); } - function borrowRate(Market memory market) external view returns (uint256) { - Id id = Id.wrap(keccak256(abi.encode(market))); + function borrowRate(Market calldata market) external view returns (uint256) { + Id id = market.id(); uint256 utilization = blue.totalBorrow(id).wDiv(blue.totalSupply(id)); // Divide by the number of seconds in a year. diff --git a/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index 029d78ccf..5e622ee4a 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -190,6 +190,92 @@ contract BlueTest is Test { blue.enableLltv(newLltv); } + function testSetFee(uint256 fee) public { + fee = bound(fee, 0, WAD); + + vm.prank(OWNER); + blue.setFee(market, fee); + + assertEq(blue.fee(id), fee); + } + + function testSetFeeShouldRevertIfTooHigh(uint256 fee) public { + fee = bound(fee, WAD + 1, type(uint256).max); + + vm.prank(OWNER); + vm.expectRevert("fee must be <= 1"); + blue.setFee(market, fee); + } + + function testSetFeeShouldRevertIfMarketNotCreated(Market memory marketFuzz, uint256 fee) public { + vm.assume(neq(marketFuzz, market)); + fee = bound(fee, 0, WAD); + + vm.prank(OWNER); + vm.expectRevert("unknown market"); + blue.setFee(marketFuzz, fee); + } + + function testSetFeeShouldRevertIfNotOwner(uint256 fee, address caller) public { + vm.assume(caller != OWNER); + fee = bound(fee, 0, WAD); + + vm.expectRevert("not owner"); + blue.setFee(market, fee); + } + + function testSetFeeRecipient(address recipient) public { + vm.prank(OWNER); + blue.setFeeRecipient(recipient); + + assertEq(blue.feeRecipient(), recipient); + } + + function testSetFeeRecipientShouldRevertIfNotOwner(address caller, address recipient) public { + vm.assume(caller != OWNER); + + vm.expectRevert("not owner"); + vm.prank(caller); + blue.setFeeRecipient(recipient); + } + + function testFeeAccrues(uint256 amountLent, uint256 amountBorrowed, uint256 fee, uint256 timeElapsed) public { + amountLent = bound(amountLent, 1, 2 ** 64); + amountBorrowed = bound(amountBorrowed, 1, amountLent); + timeElapsed = bound(timeElapsed, 1, 365 days); + fee = bound(fee, 0, 1e18); + address recipient = OWNER; + + vm.startPrank(OWNER); + blue.setFee(market, fee); + blue.setFeeRecipient(recipient); + vm.stopPrank(); + + borrowableAsset.setBalance(address(this), amountLent); + blue.supply(market, amountLent, address(this)); + + vm.prank(BORROWER); + blue.borrow(market, amountBorrowed, BORROWER); + + uint256 totalSupplyBefore = blue.totalSupply(id); + + // Trigger an accrue. + vm.warp(block.timestamp + timeElapsed); + collateralAsset.setBalance(address(this), 1); + blue.supplyCollateral(market, 1, address(this)); + blue.withdrawCollateral(market, 1, address(this)); + uint256 totalSupplyAfter = blue.totalSupply(id); + + vm.assume(totalSupplyAfter > totalSupplyBefore); + + uint256 accrued = totalSupplyAfter - totalSupplyBefore; + uint256 expectedFee = accrued.wMul(fee); + uint256 expectedFeeShares = + expectedFee.wMul(blue.totalSupplyShares(id)).wDiv(blue.totalSupply(id) - expectedFee); + + assertEq(blue.supplyShare(id, recipient), expectedFeeShares); + } + function testCreateMarketWithNotEnabledLltv(Market memory marketFuzz) public { vm.assume(marketFuzz.lltv != LLTV); marketFuzz.irm = irm; @@ -199,14 +285,15 @@ contract BlueTest is Test { blue.createMarket(marketFuzz); } - function testSupply(uint256 amount) public { + function testSupplyOnBehalf(uint256 amount, address onBehalf) public { + vm.assume(onBehalf != address(blue)); amount = bound(amount, 1, 2 ** 64); borrowableAsset.setBalance(address(this), amount); - blue.supply(market, amount); + blue.supply(market, amount, onBehalf); - assertEq(blue.supplyShare(id, address(this)), 1e18, "supply share"); - assertEq(borrowableAsset.balanceOf(address(this)), 0, "lender balance"); + assertEq(blue.supplyShare(id, onBehalf), 1e18, "supply share"); + assertEq(borrowableAsset.balanceOf(onBehalf), 0, "lender balance"); assertEq(borrowableAsset.balanceOf(address(blue)), amount, "blue balance"); } @@ -215,22 +302,22 @@ contract BlueTest is Test { amountBorrowed = bound(amountBorrowed, 1, 2 ** 64); borrowableAsset.setBalance(address(this), amountLent); - blue.supply(market, amountLent); + blue.supply(market, amountLent, address(this)); if (amountBorrowed == 0) { - blue.borrow(market, amountBorrowed); + blue.borrow(market, amountBorrowed, address(this)); return; } if (amountBorrowed > amountLent) { vm.prank(BORROWER); vm.expectRevert("not enough liquidity"); - blue.borrow(market, amountBorrowed); + blue.borrow(market, amountBorrowed, BORROWER); return; } vm.prank(BORROWER); - blue.borrow(market, amountBorrowed); + blue.borrow(market, amountBorrowed, BORROWER); assertEq(blue.borrowShare(id, BORROWER), 1e18, "borrow share"); assertEq(borrowableAsset.balanceOf(BORROWER), amountBorrowed, "BORROWER balance"); @@ -244,10 +331,10 @@ contract BlueTest is Test { vm.assume(amountLent >= amountBorrowed); borrowableAsset.setBalance(address(this), amountLent); - blue.supply(market, amountLent); + blue.supply(market, amountLent, address(this)); vm.prank(BORROWER); - blue.borrow(market, amountBorrowed); + blue.borrow(market, amountBorrowed, BORROWER); if (amountWithdrawn > amountLent - amountBorrowed) { if (amountWithdrawn > amountLent) { @@ -255,11 +342,11 @@ contract BlueTest is Test { } else { vm.expectRevert("not enough liquidity"); } - blue.withdraw(market, amountWithdrawn); + blue.withdraw(market, amountWithdrawn, address(this)); return; } - blue.withdraw(market, amountWithdrawn); + blue.withdraw(market, amountWithdrawn, address(this)); assertApproxEqAbs( blue.supplyShare(id, address(this)), (amountLent - amountWithdrawn) * 1e18 / amountLent, 1e3, "supply share" @@ -287,20 +374,20 @@ contract BlueTest is Test { borrowableAsset.setBalance(address(this), amountBorrowed); collateralAsset.setBalance(BORROWER, amountCollateral); - blue.supply(market, amountBorrowed); + blue.supply(market, amountBorrowed, address(this)); vm.prank(BORROWER); - blue.supplyCollateral(market, amountCollateral); + blue.supplyCollateral(market, amountCollateral, BORROWER); uint256 collateralValue = amountCollateral.wMul(priceCollateral); uint256 borrowValue = amountBorrowed.wMul(priceBorrowable); if (borrowValue == 0 || (collateralValue > 0 && borrowValue <= collateralValue.wMul(LLTV))) { vm.prank(BORROWER); - blue.borrow(market, amountBorrowed); + blue.borrow(market, amountBorrowed, BORROWER); } else { vm.prank(BORROWER); vm.expectRevert("not enough collateral"); - blue.borrow(market, amountBorrowed); + blue.borrow(market, amountBorrowed, BORROWER); } } @@ -310,11 +397,11 @@ contract BlueTest is Test { amountRepaid = bound(amountRepaid, 1, amountBorrowed); borrowableAsset.setBalance(address(this), amountLent); - blue.supply(market, amountLent); + blue.supply(market, amountLent, address(this)); vm.startPrank(BORROWER); - blue.borrow(market, amountBorrowed); - blue.repay(market, amountRepaid); + blue.borrow(market, amountBorrowed, BORROWER); + blue.repay(market, amountRepaid, BORROWER); vm.stopPrank(); assertApproxEqAbs( @@ -324,14 +411,39 @@ contract BlueTest is Test { assertEq(borrowableAsset.balanceOf(address(blue)), amountLent - amountBorrowed + amountRepaid, "blue balance"); } - function testSupplyCollateral(uint256 amount) public { + function testRepayOnBehalf(uint256 amountLent, uint256 amountBorrowed, uint256 amountRepaid, address onBehalf) + public + { + vm.assume(onBehalf != address(blue)); + vm.assume(onBehalf != address(this)); + amountLent = bound(amountLent, 1, 2 ** 64); + amountBorrowed = bound(amountBorrowed, 1, amountLent); + amountRepaid = bound(amountRepaid, 1, amountBorrowed); + + borrowableAsset.setBalance(address(this), amountLent + amountRepaid); + blue.supply(market, amountLent, address(this)); + + vm.prank(onBehalf); + blue.borrow(market, amountBorrowed, onBehalf); + + blue.repay(market, amountRepaid, onBehalf); + + assertApproxEqAbs( + blue.borrowShare(id, onBehalf), (amountBorrowed - amountRepaid) * 1e18 / amountBorrowed, 1e3, "borrow share" + ); + assertEq(borrowableAsset.balanceOf(onBehalf), amountBorrowed, "onBehalf balance"); + assertEq(borrowableAsset.balanceOf(address(blue)), amountLent - amountBorrowed + amountRepaid, "blue balance"); + } + + function testSupplyCollateralOnBehalf(uint256 amount, address onBehalf) public { + vm.assume(onBehalf != address(blue)); amount = bound(amount, 1, 2 ** 64); collateralAsset.setBalance(address(this), amount); - blue.supplyCollateral(market, amount); + blue.supplyCollateral(market, amount, onBehalf); - assertEq(blue.collateral(id, address(this)), amount, "collateral"); - assertEq(collateralAsset.balanceOf(address(this)), 0, "this balance"); + assertEq(blue.collateral(id, onBehalf), amount, "collateral"); + assertEq(collateralAsset.balanceOf(onBehalf), 0, "onBehalf balance"); assertEq(collateralAsset.balanceOf(address(blue)), amount, "blue balance"); } @@ -340,15 +452,15 @@ contract BlueTest is Test { amountWithdrawn = bound(amountWithdrawn, 1, 2 ** 64); collateralAsset.setBalance(address(this), amountDeposited); - blue.supplyCollateral(market, amountDeposited); + blue.supplyCollateral(market, amountDeposited, address(this)); if (amountWithdrawn > amountDeposited) { vm.expectRevert(stdError.arithmeticError); - blue.withdrawCollateral(market, amountWithdrawn); + blue.withdrawCollateral(market, amountWithdrawn, address(this)); return; } - blue.withdrawCollateral(market, amountWithdrawn); + blue.withdrawCollateral(market, amountWithdrawn, address(this)); assertEq(blue.collateral(id, address(this)), amountDeposited - amountWithdrawn, "this collateral"); assertEq(collateralAsset.balanceOf(address(this)), amountWithdrawn, "this balance"); @@ -370,12 +482,12 @@ contract BlueTest is Test { borrowableAsset.setBalance(LIQUIDATOR, amountBorrowed); // Supply - blue.supply(market, amountLent); + blue.supply(market, amountLent, address(this)); // Borrow vm.startPrank(BORROWER); - blue.supplyCollateral(market, amountCollateral); - blue.borrow(market, amountBorrowed); + blue.supplyCollateral(market, amountCollateral, BORROWER); + blue.borrow(market, amountBorrowed, BORROWER); vm.stopPrank(); // Price change @@ -412,12 +524,12 @@ contract BlueTest is Test { borrowableAsset.setBalance(LIQUIDATOR, amountBorrowed); // Supply - blue.supply(market, amountLent); + blue.supply(market, amountLent, address(this)); // Borrow vm.startPrank(BORROWER); - blue.supplyCollateral(market, amountCollateral); - blue.borrow(market, amountBorrowed); + blue.supplyCollateral(market, amountCollateral, BORROWER); + blue.borrow(market, amountBorrowed, BORROWER); vm.stopPrank(); // Price change @@ -448,11 +560,11 @@ contract BlueTest is Test { secondAmount = bound(secondAmount, 1, 2 ** 64); borrowableAsset.setBalance(address(this), firstAmount); - blue.supply(market, firstAmount); + blue.supply(market, firstAmount, address(this)); borrowableAsset.setBalance(BORROWER, secondAmount); vm.prank(BORROWER); - blue.supply(market, secondAmount); + blue.supply(market, secondAmount, BORROWER); assertApproxEqAbs(supplyBalance(address(this)), firstAmount, 100, "same balance first user"); assertEq(blue.supplyShare(id, address(this)), 1e18, "expected shares first user"); @@ -464,22 +576,22 @@ contract BlueTest is Test { vm.assume(neq(marketFuzz, market)); vm.expectRevert("unknown market"); - blue.supply(marketFuzz, 1); + blue.supply(marketFuzz, 1, address(this)); vm.expectRevert("unknown market"); - blue.withdraw(marketFuzz, 1); + blue.withdraw(marketFuzz, 1, address(this)); vm.expectRevert("unknown market"); - blue.borrow(marketFuzz, 1); + blue.borrow(marketFuzz, 1, address(this)); vm.expectRevert("unknown market"); - blue.repay(marketFuzz, 1); + blue.repay(marketFuzz, 1, address(this)); vm.expectRevert("unknown market"); - blue.supplyCollateral(marketFuzz, 1); + blue.supplyCollateral(marketFuzz, 1, address(this)); vm.expectRevert("unknown market"); - blue.withdrawCollateral(marketFuzz, 1); + blue.withdrawCollateral(marketFuzz, 1, address(this)); vm.expectRevert("unknown market"); blue.liquidate(marketFuzz, address(0), 1); @@ -487,22 +599,22 @@ contract BlueTest is Test { function testAmountZero() public { vm.expectRevert("zero amount"); - blue.supply(market, 0); + blue.supply(market, 0, address(this)); vm.expectRevert("zero amount"); - blue.withdraw(market, 0); + blue.withdraw(market, 0, address(this)); vm.expectRevert("zero amount"); - blue.borrow(market, 0); + blue.borrow(market, 0, address(this)); vm.expectRevert("zero amount"); - blue.repay(market, 0); + blue.repay(market, 0, address(this)); vm.expectRevert("zero amount"); - blue.supplyCollateral(market, 0); + blue.supplyCollateral(market, 0, address(this)); vm.expectRevert("zero amount"); - blue.withdrawCollateral(market, 0); + blue.withdrawCollateral(market, 0, address(this)); vm.expectRevert("zero amount"); blue.liquidate(market, address(0), 0); @@ -512,13 +624,51 @@ contract BlueTest is Test { vm.assume(amount > 0); vm.expectRevert(stdError.divisionError); - blue.withdraw(market, amount); + blue.withdraw(market, amount, address(this)); vm.expectRevert(stdError.divisionError); - blue.repay(market, amount); + blue.repay(market, amount, address(this)); vm.expectRevert(stdError.arithmeticError); - blue.withdrawCollateral(market, amount); + blue.withdrawCollateral(market, amount, address(this)); + } + + function testSetApproval(address manager, bool isAllowed) public { + blue.setApproval(manager, isAllowed); + assertEq(blue.isApproved(address(this), manager), isAllowed); + } + + function testNotApproved(address attacker) public { + vm.assume(attacker != address(this)); + + vm.startPrank(attacker); + + vm.expectRevert("not approved"); + blue.withdraw(market, 1, address(this)); + vm.expectRevert("not approved"); + blue.withdrawCollateral(market, 1, address(this)); + vm.expectRevert("not approved"); + blue.borrow(market, 1, address(this)); + + vm.stopPrank(); + } + + function testApproved(address manager) public { + borrowableAsset.setBalance(address(this), 100 ether); + collateralAsset.setBalance(address(this), 100 ether); + + blue.supply(market, 100 ether, address(this)); + blue.supplyCollateral(market, 100 ether, address(this)); + + blue.setApproval(manager, true); + + vm.startPrank(manager); + + blue.withdraw(market, 1 ether, address(this)); + blue.withdrawCollateral(market, 1 ether, address(this)); + blue.borrow(market, 1 ether, address(this)); + + vm.stopPrank(); } } diff --git a/test/hardhat/Blue.spec.ts b/test/hardhat/Blue.spec.ts index 2ad4cb5ed..ba697a670 100644 --- a/test/hardhat/Blue.spec.ts +++ b/test/hardhat/Blue.spec.ts @@ -113,8 +113,8 @@ describe("Blue", () => { let supplyOnly: boolean = random() < 2 / 3; if (supplyOnly) { if (amount > BigNumber.from(0)) { - await blue.connect(user).supply(market, amount); - await blue.connect(user).withdraw(market, amount.div(2)); + await blue.connect(user).supply(market, amount, user.address); + await blue.connect(user).withdraw(market, amount.div(2), user.address); } } else { const totalSupply = await blue.totalSupply(id); @@ -123,10 +123,10 @@ describe("Blue", () => { amount = BigNumber.min(amount, BigNumber.from(liq).div(2)); if (amount > BigNumber.from(0)) { - await blue.connect(user).supplyCollateral(market, amount); - await blue.connect(user).borrow(market, amount.div(2)); - await blue.connect(user).repay(market, amount.div(4)); - await blue.connect(user).withdrawCollateral(market, amount.div(8)); + await blue.connect(user).supplyCollateral(market, amount, user.address); + await blue.connect(user).borrow(market, amount.div(2), user.address); + await blue.connect(user).repay(market, amount.div(4), user.address); + await blue.connect(user).withdrawCollateral(market, amount.div(8), user.address); } } } @@ -165,10 +165,10 @@ describe("Blue", () => { await collateral.setBalance(user.address, initBalance); await collateral.connect(user).approve(blue.address, constants.MaxUint256); - await blue.connect(user).supply(market, amount); - await blue.connect(user).supplyCollateral(market, amount); + await blue.connect(user).supply(market, amount, user.address); + await blue.connect(user).supplyCollateral(market, amount, user.address); - await blue.connect(user).borrow(market, borrowedAmount); + await blue.connect(user).borrow(market, borrowedAmount, user.address); } await borrowableOracle.connect(admin).setPrice(BigNumber.WAD.mul(1000));