diff --git a/contracts/adapters/compound/CompoundTokenAdapter.sol b/contracts/adapters/compound/CompoundTokenAdapter.sol new file mode 100644 index 0000000..26a9274 --- /dev/null +++ b/contracts/adapters/compound/CompoundTokenAdapter.sol @@ -0,0 +1,78 @@ +pragma solidity 0.8.11; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {LibCompound} from "../../libraries/LibCompound.sol"; + +import {ICERC20} from "../../interfaces/compound/ICERC20.sol"; +import {ITokenAdapter} from "../../interfaces/ITokenAdapter.sol"; +import {TokenUtils} from "../../libraries/TokenUtils.sol"; + +contract CompoundTokenAdapter is ITokenAdapter { + /// @dev Compound error code for a noop. + uint256 private constant NO_ERROR = 0; + + /// @dev Scalar for all fixed point numbers returned by Compound. + uint256 private constant FIXED_POINT_SCALAR = 1e18; + + /// @notice An error used when a call to Compound fails. + /// + /// @param code The error code. + error CompoundError(uint256 code); + + /// @inheritdoc ITokenAdapter + address public override token; + + /// @inheritdoc ITokenAdapter + address public override underlyingToken; + + constructor(address _token) { + token = _token; + underlyingToken = ICERC20(token).underlying(); + } + + /// @inheritdoc ITokenAdapter + function price() external view override returns (uint256) { + return LibCompound.viewExchangeRate(ICERC20(token)) / FIXED_POINT_SCALAR; + } + + /// @inheritdoc ITokenAdapter + function wrap(uint256 amount, address recipient, uint256 maximumLoss) external override returns (uint256) { + TokenUtils.safeTransferFrom(underlyingToken, msg.sender, address(this), amount); + TokenUtils.safeApprove(underlyingToken, token, amount); + + uint256 startingBalance = TokenUtils.safeBalanceOf(token, address(this)); + + uint256 error; + if ((error = ICERC20(token).mint(amount)) != NO_ERROR) { + revert CompoundError(error); + } + + uint256 endingBalance = TokenUtils.safeBalanceOf(token, address(this)); + uint256 mintedAmount = endingBalance - startingBalance; + + TokenUtils.safeTransfer(token, recipient, mintedAmount); + + return mintedAmount; + } + + /// @inheritdoc ITokenAdapter + function unwrap(uint256 amount, address recipient, uint256 maximumLoss) external override returns (uint256) { + TokenUtils.safeTransferFrom(token, msg.sender, address(this), amount); + + uint256 startingBalance = TokenUtils.safeBalanceOf(underlyingToken, address(this)); + + uint256 error; + if ((error = ICERC20(token).redeemUnderlying(amount)) != NO_ERROR) { + revert CompoundError(error); + } + + uint256 endingBalance = TokenUtils.safeBalanceOf(underlyingToken, address(this)); + uint256 redeemedAmount = endingBalance - startingBalance; + + TokenUtils.safeTransfer(underlyingToken, recipient, redeemedAmount); + + return redeemedAmount; + } +} \ No newline at end of file diff --git a/contracts/interfaces/compound/ICERC20.sol b/contracts/interfaces/compound/ICERC20.sol new file mode 100644 index 0000000..d97e187 --- /dev/null +++ b/contracts/interfaces/compound/ICERC20.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.4; + +import {IERC20Minimal} from "../IERC20Minimal.sol"; + +import {IInterestRateModel} from "./IInterestRateModel.sol"; + +interface ICERC20 is IERC20Minimal { + function mint(uint256) external returns (uint256); + + function borrow(uint256) external returns (uint256); + + function underlying() external view returns (address); + + function totalBorrows() external view returns (uint256); + + function totalFuseFees() external view returns (uint256); + + function repayBorrow(uint256) external returns (uint256); + + function totalReserves() external view returns (uint256); + + function exchangeRateCurrent() external returns (uint256); + + function totalAdminFees() external view returns (uint256); + + function fuseFeeMantissa() external view returns (uint256); + + function adminFeeMantissa() external view returns (uint256); + + function exchangeRateStored() external view returns (uint256); + + function accrualBlockNumber() external view returns (uint256); + + function redeemUnderlying(uint256) external returns (uint256); + + function balanceOfUnderlying(address) external returns (uint256); + + function reserveFactorMantissa() external view returns (uint256); + + function borrowBalanceCurrent(address) external returns (uint256); + + function interestRateModel() external view returns (IInterestRateModel); + + function initialExchangeRateMantissa() external view returns (uint256); + + function repayBorrowBehalf(address, uint256) external returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/compound/IInterestRateModel.sol b/contracts/interfaces/compound/IInterestRateModel.sol new file mode 100644 index 0000000..1b754c5 --- /dev/null +++ b/contracts/interfaces/compound/IInterestRateModel.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.4; + +interface IInterestRateModel { + function getBorrowRate( + uint256, + uint256, + uint256 + ) external view returns (uint256); + + function getSupplyRate( + uint256, + uint256, + uint256, + uint256 + ) external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/libraries/LibCompound.sol b/contracts/libraries/LibCompound.sol new file mode 100644 index 0000000..b82d799 --- /dev/null +++ b/contracts/libraries/LibCompound.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.11; + +import {IllegalState} from "../base/Errors.sol"; + +import {FixedPointMathLib} from "./solmate/FixedPointMathLib.sol"; + +import {IERC20Minimal} from "../interfaces/IERC20Minimal.sol"; +import {ICERC20} from "../interfaces/compound/ICERC20.sol"; + +/// @notice Get up to date cToken data without mutating state. +/// @author Transmissions11 (https://github.com/transmissions11/libcompound) +library LibCompound { + using FixedPointMathLib for uint256; + + function viewUnderlyingBalanceOf(ICERC20 cToken, address user) internal view returns (uint256) { + return cToken.balanceOf(user).mulWadDown(viewExchangeRate(cToken)); + } + + function viewExchangeRate(ICERC20 cToken) internal view returns (uint256) { + uint256 accrualBlockNumberPrior = cToken.accrualBlockNumber(); + + if (accrualBlockNumberPrior == block.number) return cToken.exchangeRateStored(); + + uint256 totalCash = IERC20Minimal(cToken.underlying()).balanceOf(address(cToken)); + uint256 borrowsPrior = cToken.totalBorrows(); + uint256 reservesPrior = cToken.totalReserves(); + + uint256 borrowRateMantissa = cToken.interestRateModel().getBorrowRate(totalCash, borrowsPrior, reservesPrior); + + if (borrowRateMantissa > 0.0005e16) { + revert IllegalState(); + } + + uint256 interestAccumulated = (borrowRateMantissa * (block.number - accrualBlockNumberPrior)).mulWadDown( + borrowsPrior + ); + + uint256 totalReserves = cToken.reserveFactorMantissa().mulWadDown(interestAccumulated) + reservesPrior; + uint256 totalBorrows = interestAccumulated + borrowsPrior; + uint256 totalSupply = cToken.totalSupply(); + + return totalSupply == 0 + ? cToken.initialExchangeRateMantissa() + : (totalCash + totalBorrows - totalReserves).divWadDown(totalSupply); + } +} \ No newline at end of file diff --git a/contracts/libraries/solmate/FixedPointMathLib.sol b/contracts/libraries/solmate/FixedPointMathLib.sol new file mode 100644 index 0000000..1f5eedc --- /dev/null +++ b/contracts/libraries/solmate/FixedPointMathLib.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Arithmetic library with operations for fixed-point numbers. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) +/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) +library FixedPointMathLib { + /*/////////////////////////////////////////////////////////////// + SIMPLIFIED FIXED POINT OPERATIONS + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s. + + function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down. + } + + function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up. + } + + function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down. + } + + function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up. + } + + /*/////////////////////////////////////////////////////////////// + LOW LEVEL FIXED POINT OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function mulDivDown( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 z) { + assembly { + // Store x * y in z for now. + z := mul(x, y) + + // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y)) + if iszero(and(iszero(iszero(denominator)), or(iszero(x), eq(div(z, x), y)))) { + revert(0, 0) + } + + // Divide z by the denominator. + z := div(z, denominator) + } + } + + function mulDivUp( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 z) { + assembly { + // Store x * y in z for now. + z := mul(x, y) + + // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y)) + if iszero(and(iszero(iszero(denominator)), or(iszero(x), eq(div(z, x), y)))) { + revert(0, 0) + } + + // First, divide z - 1 by the denominator and add 1. + // We allow z - 1 to underflow is z is 0, because we multiply the + // end result by 0 if z is zero, ensuring we return 0 if z is zero. + z := mul(iszero(iszero(z)), add(div(sub(z, 1), denominator), 1)) + } + } + + function rpow( + uint256 x, + uint256 n, + uint256 scalar + ) internal pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { + // 0 ** 0 = 1 + z := scalar + } + default { + // 0 ** n = 0 + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + // If n is even, store scalar in z for now. + z := scalar + } + default { + // If n is odd, store x in z for now. + z := x + } + + // Shifting right by 1 is like dividing by 2. + let half := shr(1, scalar) + + for { + // Shift n right by 1 before looping to halve it. + n := shr(1, n) + } n { + // Shift n right by 1 each iteration to halve it. + n := shr(1, n) + } { + // Revert immediately if x ** 2 would overflow. + // Equivalent to iszero(eq(div(xx, x), x)) here. + if shr(128, x) { + revert(0, 0) + } + + // Store x squared. + let xx := mul(x, x) + + // Round to the nearest number. + let xxRound := add(xx, half) + + // Revert if xx + half overflowed. + if lt(xxRound, xx) { + revert(0, 0) + } + + // Set x to scaled xxRound. + x := div(xxRound, scalar) + + // If n is even: + if mod(n, 2) { + // Compute z * x. + let zx := mul(z, x) + + // If z * x overflowed: + if iszero(eq(div(zx, x), z)) { + // Revert if x is non-zero. + if iszero(iszero(x)) { + revert(0, 0) + } + } + + // Round to the nearest number. + let zxRound := add(zx, half) + + // Revert if zx + half overflowed. + if lt(zxRound, zx) { + revert(0, 0) + } + + // Return properly scaled zxRound. + z := div(zxRound, scalar) + } + } + } + } + } + + /*/////////////////////////////////////////////////////////////// + GENERAL NUMBER UTILITIES + //////////////////////////////////////////////////////////////*/ + + function sqrt(uint256 x) internal pure returns (uint256 z) { + assembly { + // Start off with z at 1. + z := 1 + + // Used below to help find a nearby power of 2. + let y := x + + // Find the lowest power of 2 that is at least sqrt(x). + if iszero(lt(y, 0x100000000000000000000000000000000)) { + y := shr(128, y) // Like dividing by 2 ** 128. + z := shl(64, z) + } + if iszero(lt(y, 0x10000000000000000)) { + y := shr(64, y) // Like dividing by 2 ** 64. + z := shl(32, z) + } + if iszero(lt(y, 0x100000000)) { + y := shr(32, y) // Like dividing by 2 ** 32. + z := shl(16, z) + } + if iszero(lt(y, 0x10000)) { + y := shr(16, y) // Like dividing by 2 ** 16. + z := shl(8, z) + } + if iszero(lt(y, 0x100)) { + y := shr(8, y) // Like dividing by 2 ** 8. + z := shl(4, z) + } + if iszero(lt(y, 0x10)) { + y := shr(4, y) // Like dividing by 2 ** 4. + z := shl(2, z) + } + if iszero(lt(y, 0x8)) { + // Equivalent to 2 ** z. + z := shl(1, z) + } + + // Shifting right by 1 is like dividing by 2. + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + // Compute a rounded down version of z. + let zRoundDown := div(x, z) + + // If zRoundDown is smaller, use it. + if lt(zRoundDown, z) { + z := zRoundDown + } + } + } +} \ No newline at end of file