diff --git a/.gitmodules b/.gitmodules index c59f396e6..690924b6a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/solmate"] - path = lib/solmate - url = https://github.com/transmissions11/solmate +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 000000000..fd81a96f0 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90 diff --git a/lib/solmate b/lib/solmate deleted file mode 160000 index bfc9c2586..000000000 --- a/lib/solmate +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bfc9c25865a274a7827fea5abf6e4fb64fc64e6c diff --git a/remappings.txt b/remappings.txt index 2ff87bcb1..a476d54c0 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,3 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ -solmate/=lib/solmate/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ diff --git a/src/Morpho.sol b/src/Morpho.sol index 0fb6eb187..7e6942851 100644 --- a/src/Morpho.sol +++ b/src/Morpho.sol @@ -18,9 +18,9 @@ import {UtilsLib} from "./libraries/UtilsLib.sol"; import {EventsLib} from "./libraries/EventsLib.sol"; import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {MarketLib} from "./libraries/MarketLib.sol"; +import {MathLib, WAD} from "./libraries/MathLib.sol"; import {SharesMathLib} from "./libraries/SharesMathLib.sol"; import {SafeTransferLib} from "./libraries/SafeTransferLib.sol"; -import {FixedPointMathLib, WAD} from "./libraries/FixedPointMathLib.sol"; /// @dev The maximum fee a market can have (25%). uint256 constant MAX_FEE = 0.25e18; @@ -43,10 +43,10 @@ bytes32 constant AUTHORIZATION_TYPEHASH = /// @custom:contact security@morpho.xyz /// @notice The Morpho contract. contract Morpho is IMorpho { + using MathLib for uint256; using MarketLib for Market; using SharesMathLib for uint256; using SafeTransferLib for IERC20; - using FixedPointMathLib for uint256; /* IMMUTABLES */ diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol index e4628359f..c976ff288 100644 --- a/src/interfaces/IERC20.sol +++ b/src/interfaces/IERC20.sol @@ -4,5 +4,7 @@ pragma solidity >=0.5.0; /// @title IERC20 /// @author Morpho Labs /// @custom:contact security@morpho.xyz -/// @dev Empty because we only call functions in assembly. It prevents calling transfer (transferFrom) instead of safeTransfer (safeTransferFrom). -interface IERC20 {} +interface IERC20 { + function transfer(address to, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); +} diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index f23efbe9a..24f10b16e 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; +/// @title ErrorsLib +/// @author Morpho Labs +/// @custom:contact security@morpho.xyz +/// @notice Library exposing error messages. library ErrorsLib { /// @notice Thrown when the caller is not the owner. string internal constant NOT_OWNER = "not owner"; @@ -49,4 +53,10 @@ library ErrorsLib { /// @notice Thrown when the authorization signature is expired. string internal constant SIGNATURE_EXPIRED = "signature expired"; + + /// @notice Thrown when a token transfer has failed. + string internal constant TRANSFER_FAILED = "transfer failed"; + + /// @notice Thrown when a token transferFrom has failed. + string internal constant TRANSFER_FROM_FAILED = "transferFrom failed"; } diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index e71f1ce0c..5705c1347 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -3,6 +3,10 @@ pragma solidity ^0.8.0; import {Id, Market} from "../interfaces/IMorpho.sol"; +/// @title EventsLib +/// @author Morpho Labs +/// @custom:contact security@morpho.xyz +/// @notice Library exposing events. library EventsLib { /// @notice Emitted when setting a new owner. /// @param newOwner The new owner of the contract. diff --git a/src/libraries/FixedPointMathLib.sol b/src/libraries/MathLib.sol similarity index 60% rename from src/libraries/FixedPointMathLib.sol rename to src/libraries/MathLib.sol index 52947732c..95fb2d27a 100644 --- a/src/libraries/FixedPointMathLib.sol +++ b/src/libraries/MathLib.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import {UtilsLib} from "./UtilsLib.sol"; - uint256 constant WAD = 1e18; -/// @notice Fixed-point arithmetic library. -/// @dev Greatly inspired by Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) -/// and by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) -library FixedPointMathLib { +/// @title MathLib +/// @author Morpho Labs +/// @custom:contact security@morpho.xyz +/// @notice Library to manage fixed-point arithmetic. +/// @dev Inspired by https://github.com/morpho-org/morpho-utils. +library MathLib { uint256 internal constant MAX_UINT256 = 2 ** 256 - 1; /// @dev (x * y) / WAD rounded down. @@ -31,38 +31,41 @@ library FixedPointMathLib { return mulDivUp(x, WAD, y); } - /// @dev The sum of the last three terms in a four term taylor series expansion - /// to approximate a continuous compound interest rate: e^(nx) - 1. - function wTaylorCompounded(uint256 x, uint256 n) internal pure returns (uint256) { - uint256 firstTerm = x * n; - uint256 secondTerm = wMulDown(firstTerm, firstTerm) / 2; - uint256 thirdTerm = wMulDown(secondTerm, firstTerm) / 3; - - return firstTerm + secondTerm + thirdTerm; - } - /// @dev (x * y) / denominator rounded down. function mulDivDown(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly + // Division by zero if denominator == 0. + // Overflow if + // x * y > type(uint256).max + // <=> y > 0 and x > type(uint256).max / y assembly { - // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) - if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { revert(0, 0) } + if or(mul(y, gt(x, div(MAX_UINT256, y))), iszero(denominator)) { revert(0, 0) } - // Divide x * y by the denominator. z := div(mul(x, y), denominator) } } /// @dev (x * y) / denominator rounded up. function mulDivUp(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly + // Underflow if denominator == 0. + // Division by 0 if denominator == 0 (this case cannot occure since the above underflow happens before). + // Overflow if + // x * y + denominator - 1 > type(uint256).max + // <=> x * y > type(uint256).max - denominator - 1 + // <=> y > 0 and x > (type(uint256).max - denominator - 1) / y assembly { - // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) - if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { revert(0, 0) } + if or(mul(y, gt(x, div(sub(MAX_UINT256, sub(denominator, 1)), y))), iszero(denominator)) { revert(0, 0) } - // If x * y modulo the denominator is strictly greater than 0, - // 1 is added to round up the division of x * y by the denominator. - z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator)) + z := div(add(mul(x, y), sub(denominator, 1)), denominator) } } + + /// @dev The sum of the last three terms in a four term taylor series expansion + /// to approximate a continuous compound interest rate: e^(nx) - 1. + function wTaylorCompounded(uint256 x, uint256 n) internal pure returns (uint256) { + uint256 firstTerm = x * n; + uint256 secondTerm = wMulDown(firstTerm, firstTerm) / 2; + uint256 thirdTerm = wMulDown(secondTerm, firstTerm) / 3; + + return firstTerm + secondTerm + thirdTerm; + } } diff --git a/src/libraries/SafeTransferLib.sol b/src/libraries/SafeTransferLib.sol index 231e3fbe6..cfb7c2ab9 100644 --- a/src/libraries/SafeTransferLib.sol +++ b/src/libraries/SafeTransferLib.sol @@ -1,69 +1,30 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; +import {ErrorsLib} from "../libraries/ErrorsLib.sol"; + import {IERC20} from "../interfaces/IERC20.sol"; -/// @notice Safe ERC20 transfer library that gracefully handles missing return values. -/// @dev Greatly inspired by Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol). -/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. -/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller. +/// @title SafeTransferLib +/// @author Morpho Labs +/// @custom:contact security@morpho.xyz +/// @notice Library to manage tokens not fully ERC20 compliant: +/// not returning a boolean for `transfer` and `transferFrom` functions. library SafeTransferLib { - function safeTransferFrom(IERC20 token, address from, address to, uint256 amount) internal { - bool success; - - /// @solidity memory-safe-assembly - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) - mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument. - mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. - mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. - - success := - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), - // We use 100 because the length of our calldata totals up like so: 4 + 32 * 3. - // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. - // Counterintuitively, this call must be positioned second to the or() call in the - // surrounding and() call or else returndatasize() will be zero during the computation. - call(gas(), token, 0, freeMemoryPointer, 100, 0, 32) - ) - } - - require(success, "TRANSFER_FROM_FAILED"); + function safeTransfer(IERC20 token, address to, uint256 value) internal { + (bool success, bytes memory returndata) = address(token).call(abi.encodeCall(token.transfer, (to, value))); + require( + success && address(token).code.length > 0 && (returndata.length == 0 || abi.decode(returndata, (bool))), + ErrorsLib.TRANSFER_FAILED + ); } - function safeTransfer(IERC20 token, address to, uint256 amount) internal { - bool success; - - /// @solidity memory-safe-assembly - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) - mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. - mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. - - success := - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), - // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. - // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. - // Counterintuitively, this call must be positioned second to the or() call in the - // surrounding and() call or else returndatasize() will be zero during the computation. - call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) - ) - } - - require(success, "TRANSFER_FAILED"); + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + (bool success, bytes memory returndata) = + address(token).call(abi.encodeCall(token.transferFrom, (from, to, value))); + require( + success && address(token).code.length > 0 && (returndata.length == 0 || abi.decode(returndata, (bool))), + ErrorsLib.TRANSFER_FROM_FAILED + ); } } diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol index f31c9fa3e..ca21a402e 100644 --- a/src/libraries/SharesMathLib.sol +++ b/src/libraries/SharesMathLib.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import {FixedPointMathLib} from "./FixedPointMathLib.sol"; +import {MathLib} from "./MathLib.sol"; -/// @title SharesMath +/// @title SharesMathLib /// @author Morpho Labs /// @custom:contact security@morpho.xyz /// @notice Shares management library. /// @dev This implementation mitigates share price manipulations, using OpenZeppelin's method of virtual shares: -/// https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack. +/// https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack. library SharesMathLib { - using FixedPointMathLib for uint256; + using MathLib for uint256; uint256 internal constant VIRTUAL_SHARES = 1e18; diff --git a/src/libraries/UtilsLib.sol b/src/libraries/UtilsLib.sol index 1b4b958ad..ab4f98288 100644 --- a/src/libraries/UtilsLib.sol +++ b/src/libraries/UtilsLib.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -/// @dev Inspired by morpho-utils. +/// @title UtilsLib +/// @author Morpho Labs +/// @custom:contact security@morpho.xyz +/// @notice Library exposing helpers. +/// @dev Inspired by https://github.com/morpho-org/morpho-utils. library UtilsLib { /// @dev Returns true if there is exactly one zero. function exactlyOneZero(uint256 x, uint256 y) internal pure returns (bool z) { diff --git a/src/mocks/ERC20Mock.sol b/src/mocks/ERC20Mock.sol index cef49b8c3..2f24071c8 100644 --- a/src/mocks/ERC20Mock.sol +++ b/src/mocks/ERC20Mock.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; contract ERC20Mock is ERC20 { - constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20(_name, _symbol, _decimals) {} + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} - function setBalance(address owner, uint256 amount) external { - balanceOf[owner] = amount; + function setBalance(address account, uint256 amount) external { + _burn(account, balanceOf(account)); + _mint(account, amount); } } diff --git a/src/mocks/FlashBorrowerMock.sol b/src/mocks/FlashBorrowerMock.sol index 72d0a840b..b316dbbc2 100644 --- a/src/mocks/FlashBorrowerMock.sol +++ b/src/mocks/FlashBorrowerMock.sol @@ -4,11 +4,9 @@ pragma solidity ^0.8.0; import {IFlashLender} from "../interfaces/IFlashLender.sol"; import {IMorphoFlashLoanCallback} from "../interfaces/IMorphoCallbacks.sol"; -import {ERC20, SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; contract FlashBorrowerMock is IMorphoFlashLoanCallback { - using SafeTransferLib for ERC20; - IFlashLender private immutable MORPHO; constructor(IFlashLender newMorpho) { @@ -22,6 +20,6 @@ contract FlashBorrowerMock is IMorphoFlashLoanCallback { function onMorphoFlashLoan(uint256 assets, bytes calldata data) external { require(msg.sender == address(MORPHO)); address token = abi.decode(data, (address)); - ERC20(token).safeApprove(address(MORPHO), assets); + ERC20(token).approve(address(MORPHO), assets); } } diff --git a/src/mocks/IrmMock.sol b/src/mocks/IrmMock.sol index 52df3fda7..ca98bae2e 100644 --- a/src/mocks/IrmMock.sol +++ b/src/mocks/IrmMock.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.0; import {IIrm} from "../interfaces/IIrm.sol"; import {Id, Market, IMorpho} from "../interfaces/IMorpho.sol"; -import {FixedPointMathLib} from "../libraries/FixedPointMathLib.sol"; +import {MathLib} from "../libraries/MathLib.sol"; import {MarketLib} from "../libraries/MarketLib.sol"; contract IrmMock is IIrm { - using FixedPointMathLib for uint256; + using MathLib for uint256; using MarketLib for Market; IMorpho private immutable MORPHO; diff --git a/src/mocks/OracleMock.sol b/src/mocks/OracleMock.sol index 68699c982..b308f013c 100644 --- a/src/mocks/OracleMock.sol +++ b/src/mocks/OracleMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {IOracle} from "../interfaces/IOracle.sol"; -import {FixedPointMathLib, WAD} from "../libraries/FixedPointMathLib.sol"; +import {MathLib, WAD} from "../libraries/MathLib.sol"; contract OracleMock is IOracle { uint256 public price; diff --git a/test/forge/Math.t.sol b/test/forge/Math.t.sol deleted file mode 100644 index d39ea204f..000000000 --- a/test/forge/Math.t.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; - -import "src/libraries/FixedPointMathLib.sol"; -import "test/forge/helpers/WadMath.sol"; - -contract MathTest is Test { - using FixedPointMathLib for uint256; - - function testWTaylorCompounded(uint256 rate, uint256 timeElapsed) public { - // Assume rate is less than a ~500% APY. (~180% APR) - rate = bound(rate, 0, WAD / 20_000_000); - timeElapsed = bound(timeElapsed, 0, 365 days); - uint256 result = rate.wTaylorCompounded(timeElapsed) + WAD; - uint256 toCompare = WadMath.wadExpUp(rate * timeElapsed); - assertLe(result, toCompare, "rate should be less than the compounded rate"); - assertGe(result, WAD + timeElapsed * rate, "rate should be greater than the simple interest rate"); - assertLe((toCompare - result) * 100_00 / toCompare, 8_00, "The error should be less than or equal to 8%"); - } -} diff --git a/test/forge/MathLib.t.sol b/test/forge/MathLib.t.sol new file mode 100644 index 000000000..47c9eca7d --- /dev/null +++ b/test/forge/MathLib.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "src/libraries/MathLib.sol"; +import "test/forge/helpers/WadMath.sol"; + +contract MathLibTest is Test { + using MathLib for uint256; + + function testWTaylorCompounded(uint256 rate, uint256 timeElapsed) public { + // Assume rate is less than a ~500% APY. (~180% APR) + rate = bound(rate, 0, WAD / 20_000_000); + timeElapsed = bound(timeElapsed, 0, 365 days); + uint256 result = rate.wTaylorCompounded(timeElapsed) + WAD; + uint256 toCompare = WadMath.wadExpUp(rate * timeElapsed); + assertLe(result, toCompare, "rate should be less than the compounded rate"); + assertGe(result, WAD + timeElapsed * rate, "rate should be greater than the simple interest rate"); + assertLe((toCompare - result) * 100_00 / toCompare, 8_00, "The error should be less than or equal to 8%"); + } + + function testMulDivDown(uint256 x, uint256 y, uint256 denominator) public { + // Ignore cases where x * y overflows or denominator is 0. + unchecked { + if (denominator == 0 || (x != 0 && (x * y) / x != y)) return; + } + + assertEq(MathLib.mulDivDown(x, y, denominator), (x * y) / denominator); + } + + function testMulDivDownOverflow(uint256 x, uint256 y, uint256 denominator) public { + vm.assume(denominator > 0 && y > 0 && x > type(uint256).max / y); + + vm.expectRevert(); + MathLib.mulDivDown(x, y, denominator); + } + + function testMulDivDownZeroDenominator(uint256 x, uint256 y) public { + vm.expectRevert(); + MathLib.mulDivDown(x, y, 0); + } + + function testMulDivUp(uint256 x, uint256 y, uint256 denominator) public { + vm.assume( + denominator > 0 && denominator < type(uint256).max && y > 0 + && x <= (type(uint256).max - denominator - 1) / y + ); + + assertEq(MathLib.mulDivUp(x, y, denominator), x * y == 0 ? 0 : (x * y - 1) / denominator + 1); + } + + function testMulDivUpOverflow(uint256 x, uint256 y, uint256 denominator) public { + denominator = bound(denominator, 1, type(uint256).max); + uint256 denominatorMinusOne = denominator - 1; // Needed to avoid overflow in the next line. + vm.assume(y > 0 && x > (type(uint256).max - denominatorMinusOne) / y); + + vm.expectRevert(); + MathLib.mulDivUp(x, y, denominator); + } + + function testMulDivUpUnderverflow(uint256 x, uint256 y) public { + vm.assume(x > 0 && y > 0); + + vm.expectRevert(); + MathLib.mulDivUp(x, y, 0); + } + + function testMulDivUpZeroDenominator(uint256 x, uint256 y) public { + vm.expectRevert(); + MathLib.mulDivUp(x, y, 0); + } +} diff --git a/test/forge/Morpho.t.sol b/test/forge/Morpho.t.sol index 8acb6f8a7..27d57929a 100644 --- a/test/forge/Morpho.t.sol +++ b/test/forge/Morpho.t.sol @@ -25,10 +25,10 @@ contract MorphoTest is IMorphoRepayCallback, IMorphoLiquidateCallback { + using MathLib for uint256; using MarketLib for Market; using SharesMathLib for uint256; using stdStorage for StdStorage; - using FixedPointMathLib for uint256; address private constant BORROWER = address(0x1234); address private constant LIQUIDATOR = address(0x5678); @@ -48,8 +48,8 @@ contract MorphoTest is morpho = new Morpho(OWNER); // List a market. - borrowableToken = new ERC20("borrowable", "B", 18); - collateralToken = new ERC20("collateral", "C", 18); + borrowableToken = new ERC20("borrowable", "B"); + collateralToken = new ERC20("collateral", "C"); oracle = new Oracle(); irm = new Irm(morpho); @@ -952,7 +952,7 @@ contract MorphoTest is borrowableToken.approve(address(morpho), 0); - vm.expectRevert("TRANSFER_FROM_FAILED"); + vm.expectRevert(bytes(ErrorsLib.TRANSFER_FROM_FAILED)); morpho.repay(market, assets, 0, address(this), hex""); morpho.repay(market, assets, 0, address(this), abi.encode(this.testRepayCallback.selector, hex"")); } @@ -972,7 +972,7 @@ contract MorphoTest is borrowableToken.setBalance(address(this), assets); borrowableToken.approve(address(morpho), 0); - vm.expectRevert("TRANSFER_FROM_FAILED"); + vm.expectRevert(bytes(ErrorsLib.TRANSFER_FROM_FAILED)); morpho.liquidate(market, address(this), collateralAmount, hex""); morpho.liquidate( market, address(this), collateralAmount, abi.encode(this.testLiquidateCallback.selector, hex"") diff --git a/test/forge/SafeTransferLib.t.sol b/test/forge/SafeTransferLib.t.sol new file mode 100644 index 000000000..1d6454b31 --- /dev/null +++ b/test/forge/SafeTransferLib.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "src/libraries/ErrorsLib.sol"; +import {IERC20, SafeTransferLib} from "src/libraries/SafeTransferLib.sol"; + +/// @dev Token not returning any boolean on transfer and transferFrom. +contract ERC20WithoutBoolean { + mapping(address => uint256) public balanceOf; + + function transfer(address to, uint256 amount) public { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + } + + function transferFrom(address from, address to, uint256 amount) public { + // Skip allowance check. + balanceOf[from] -= amount; + balanceOf[to] += amount; + } + + function setBalance(address account, uint256 amount) public { + balanceOf[account] = amount; + } +} + +/// @dev Token returning false on transfer and transferFrom. +contract ERC20WithBooleanAlwaysFalse { + mapping(address => uint256) public balanceOf; + + function transfer(address to, uint256 amount) public returns (bool failure) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + failure = false; // To silence warning. + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool failure) { + // Skip allowance check. + balanceOf[from] -= amount; + balanceOf[to] += amount; + failure = false; // To silence warning. + } + + function setBalance(address account, uint256 amount) public { + balanceOf[account] = amount; + } +} + +contract SafeTransferLibTest is Test { + using SafeTransferLib for *; + + ERC20WithoutBoolean public tokenWithoutBoolean; + ERC20WithBooleanAlwaysFalse public tokenWithBooleanAlwaysFalse; + + function setUp() public { + tokenWithoutBoolean = new ERC20WithoutBoolean(); + tokenWithBooleanAlwaysFalse = new ERC20WithBooleanAlwaysFalse(); + } + + function testSafeTransferShouldRevertOnTokenWithEmptyCode(address noCode) public { + vm.assume(noCode.code.length == 0); + + vm.expectRevert(bytes(ErrorsLib.TRANSFER_FAILED)); + this.safeTransfer(noCode, address(0), 0); + } + + function testSafeTransfer(address to, uint256 amount) public { + tokenWithoutBoolean.setBalance(address(this), amount); + + this.safeTransfer(address(tokenWithoutBoolean), to, amount); + } + + function testSafeTransferFrom(address from, address to, uint256 amount) public { + tokenWithoutBoolean.setBalance(from, amount); + + this.safeTransferFrom(address(tokenWithoutBoolean), from, to, amount); + } + + function testSafeTransferWithBoolFalse(address to, uint256 amount) public { + tokenWithBooleanAlwaysFalse.setBalance(address(this), amount); + + vm.expectRevert(bytes(ErrorsLib.TRANSFER_FAILED)); + this.safeTransfer(address(tokenWithBooleanAlwaysFalse), to, amount); + } + + function testSafeTransferFromWithBoolFalse(address from, address to, uint256 amount) public { + tokenWithBooleanAlwaysFalse.setBalance(from, amount); + + vm.expectRevert(bytes(ErrorsLib.TRANSFER_FROM_FAILED)); + this.safeTransferFrom(address(tokenWithBooleanAlwaysFalse), from, to, amount); + } + + function safeTransfer(address token, address to, uint256 amount) external { + IERC20(token).safeTransfer(to, amount); + } + + function safeTransferFrom(address token, address from, address to, uint256 amount) external { + IERC20(token).safeTransferFrom(from, to, amount); + } +} diff --git a/test/hardhat/Morpho.spec.ts b/test/hardhat/Morpho.spec.ts index 9afedf506..938bc6b44 100644 --- a/test/hardhat/Morpho.spec.ts +++ b/test/hardhat/Morpho.spec.ts @@ -10,7 +10,8 @@ import { MarketStruct } from "types/src/Morpho"; import { FlashBorrowerMock } from "types/src/mocks/FlashBorrowerMock"; const closePositions = false; -const initBalance = constants.MaxUint256.div(2); +// Without the division it overflows. +const initBalance = constants.MaxUint256.div(parseUnits("10000000000000000")); const oraclePriceScale = parseUnits("1", 36); let seed = 42; @@ -61,8 +62,8 @@ describe("Morpho", () => { const ERC20MockFactory = await hre.ethers.getContractFactory("ERC20Mock", admin); - borrowable = await ERC20MockFactory.deploy("DAI", "DAI", 18); - collateral = await ERC20MockFactory.deploy("Wrapped BTC", "WBTC", 18); + borrowable = await ERC20MockFactory.deploy("DAI", "DAI"); + collateral = await ERC20MockFactory.deploy("Wrapped BTC", "WBTC"); const OracleMockFactory = await hre.ethers.getContractFactory("OracleMock", admin);