diff --git a/test/adapters/AaveAdapter.sol b/test/adapters/AaveAdapter.sol new file mode 100644 index 00000000..16c81e7c --- /dev/null +++ b/test/adapters/AaveAdapter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.4; + +import "./IAToken.sol"; +import "./IAavePool.sol"; + +contract AaveAdapter { + + uint8 public protocol = 4; + + function exchangeRateCurrent(address a) external view returns (uint256){ + return(IAavePool(IAToken(a).POOL()).getReserveNormalizedIncome(aToken.UNDERLYING_ASSET_ADDRESS())); + } +} \ No newline at end of file diff --git a/test/adapters/CERC20.sol b/test/adapters/CERC20.sol new file mode 100644 index 00000000..f3122d4e --- /dev/null +++ b/test/adapters/CERC20.sol @@ -0,0 +1,47 @@ +pragma solidity >=0.8.4; + +import {PErc20} from "../tokens/PErc20.sol"; + +import {InterestRateModel} from "./InterestRateModel.sol"; + +abstract contract CERC20 is Erc20 { + function mint(uint256) external virtual returns (uint256); + + function borrow(uint256) external virtual returns (uint256); + + function underlying() external view virtual returns (Erc20); + + function totalBorrows() external view virtual returns (uint256); + + function totalFuseFees() external view virtual returns (uint256); + + function repayBorrow(uint256) external virtual returns (uint256); + + function totalReserves() external view virtual returns (uint256); + + function exchangeRateCurrent() external virtual returns (uint256); + + function totalAdminFees() external view virtual returns (uint256); + + function fuseFeeMantissa() external view virtual returns (uint256); + + function adminFeeMantissa() external view virtual returns (uint256); + + function exchangeRateStored() external view virtual returns (uint256); + + function accrualBlockNumber() external view virtual returns (uint256); + + function redeemUnderlying(uint256) external virtual returns (uint256); + + function balanceOfUnderlying(address) external virtual returns (uint256); + + function reserveFactorMantissa() external view virtual returns (uint256); + + function borrowBalanceCurrent(address) external virtual returns (uint256); + + function interestRateModel() external view virtual returns (InterestRateModel); + + function initialExchangeRateMantissa() external view virtual returns (uint256); + + function repayBorrowBehalf(address, uint256) external virtual returns (uint256); +} \ No newline at end of file diff --git a/test/adapters/CompoundAdapter.sol b/test/adapters/CompoundAdapter.sol new file mode 100644 index 00000000..ce270f84 --- /dev/null +++ b/test/adapters/CompoundAdapter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >= 0.8.10; + +import "./LibCompound.sol"; + +contract CompoundAdapter { + + uint8 public protocol = 1; + + function exchangeRateCurrent(address a) external view returns (uint256){ + return(LibCompound.viewExchangeRate(CERC20(a))); + } +} \ No newline at end of file diff --git a/test/adapters/ERC4626Adapter.sol b/test/adapters/ERC4626Adapter.sol new file mode 100644 index 00000000..18bcdaf5 --- /dev/null +++ b/test/adapters/ERC4626Adapter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import "./IERC4626.sol"; + +contract ERC4626Adapter { + + uint8 public protocol = 6; + + function exchangeRateCurrent(address a) public view returns (uint256){ + return (IERC4626(a).convertToAssets(1e26)); + } +} \ No newline at end of file diff --git a/test/adapters/EulerAdapter.sol b/test/adapters/EulerAdapter.sol new file mode 100644 index 00000000..791eafb1 --- /dev/null +++ b/test/adapters/EulerAdapter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import "./IEToken.sol"; + +contract EulerAdapter { + + uint8 public protocol = 5; + + function exchangeRateCurrent(address a) external view returns (uint256){ + return(IEToken(a).convertBalanceToUnderlying(1e27)); + } +} + diff --git a/test/adapters/FixedPointMathLib.sol b/test/adapters/FixedPointMathLib.sol new file mode 100644 index 00000000..a681f39b --- /dev/null +++ b/test/adapters/FixedPointMathLib.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: BUSL-1.1 +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 if 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) // Like multiplying by 2 ** 64. + } + if iszero(lt(y, 0x10000000000000000)) { + y := shr(64, y) // Like dividing by 2 ** 64. + z := shl(32, z) // Like multiplying by 2 ** 32. + } + if iszero(lt(y, 0x100000000)) { + y := shr(32, y) // Like dividing by 2 ** 32. + z := shl(16, z) // Like multiplying by 2 ** 16. + } + if iszero(lt(y, 0x10000)) { + y := shr(16, y) // Like dividing by 2 ** 16. + z := shl(8, z) // Like multiplying by 2 ** 8. + } + if iszero(lt(y, 0x100)) { + y := shr(8, y) // Like dividing by 2 ** 8. + z := shl(4, z) // Like multiplying by 2 ** 4. + } + if iszero(lt(y, 0x10)) { + y := shr(4, y) // Like dividing by 2 ** 4. + z := shl(2, z) // Like multiplying by 2 ** 2. + } + 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 diff --git a/test/adapters/IAToken.sol b/test/adapters/IAToken.sol new file mode 100644 index 00000000..252d167d --- /dev/null +++ b/test/adapters/IAToken.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +interface IAToken { + function POOL() external view returns (address); + function UNDERLYING_ASSET_ADDRESS() external view returns (address); +} \ No newline at end of file diff --git a/test/adapters/IAavePool.sol b/test/adapters/IAavePool.sol new file mode 100644 index 00000000..42eb33d5 --- /dev/null +++ b/test/adapters/IAavePool.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +interface IAavePool { + /** + * @notice Returns the normalized income normalized income of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The reserve's normalized income + */ + function getReserveNormalizedIncome(address asset) external view returns (uint256); + +} diff --git a/test/adapters/IERC20.sol b/test/adapters/IERC20.sol new file mode 100644 index 00000000..b7490382 --- /dev/null +++ b/test/adapters/IERC20.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} \ No newline at end of file diff --git a/test/adapters/IERC4626.sol b/test/adapters/IERC4626.sol new file mode 100644 index 00000000..b7c3bf07 --- /dev/null +++ b/test/adapters/IERC4626.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import "./IERC20.sol"; + +interface IERC4626 is IERC20 { + /*//////////////////////////////////////////////////////// + Events + ////////////////////////////////////////////////////////*/ + + /// @notice `sender` has exchanged `assets` for `shares`, + /// and transferred those `shares` to `receiver`. + event Deposit(address indexed sender, address indexed receiver, uint256 assets, uint256 shares); + + /// @notice `sender` has exchanged `shares` for `assets`, + /// and transferred those `assets` to `receiver`. + event Withdraw(address indexed sender, address indexed receiver, uint256 assets, uint256 shares); + + /*//////////////////////////////////////////////////////// + Vault properties + ////////////////////////////////////////////////////////*/ + + /// @notice The address of the underlying ERC20 token used for + /// the Vault for accounting, depositing, and withdrawing. + function asset() external view returns (address); + + /// @notice Total amount of the underlying asset that + /// is "managed" by Vault. + function totalAssets() external view returns (uint256); + + /*//////////////////////////////////////////////////////// + Deposit/Withdrawal Logic + ////////////////////////////////////////////////////////*/ + + /// @notice Mints `shares` IN ASSETS Vault shares to `receiver` by + /// depositing exactly `assets` of underlying tokens. + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /// @notice Mints exactly `shares` IN SHARES Vault shares to `receiver` + /// by depositing `assets` of underlying tokens. + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /// @notice Redeems `shares` IN ASSETS from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /// @notice Redeems `shares` IN SHARES from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /*//////////////////////////////////////////////////////// + Vault Accounting Logic + ////////////////////////////////////////////////////////*/ + + /// @notice The amount of shares that the vault would + /// exchange for the amount of assets provided, in an + /// ideal scenario where all the conditions are met. + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /// @notice The amount of assets that the vault would + /// exchange for the amount of shares provided, in an + /// ideal scenario where all the conditions are met. + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /// @notice Total number of underlying assets that can + /// be deposited by `owner` into the Vault, where `owner` + /// corresponds to the input parameter `receiver` of a + /// `deposit` call. + function maxDeposit(address owner) external view returns (uint256 maxAssets); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their deposit at the current block, given + /// current on-chain conditions. + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /// @notice Total number of underlying shares that can be minted + /// for `owner`, where `owner` corresponds to the input + /// parameter `receiver` of a `mint` call. + function maxMint(address owner) external view returns (uint256 maxShares); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their mint at the current block, given + /// current on-chain conditions. + function previewMint(uint256 shares) external view returns (uint256 assets); + + /// @notice Total number of underlying assets that can be + /// withdrawn from the Vault by `owner`, where `owner` + /// corresponds to the input parameter of a `withdraw` call. + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their withdrawal at the current block, + /// given current on-chain conditions. + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /// @notice Total number of underlying shares that can be + /// redeemed from the Vault by `owner`, where `owner` corresponds + /// to the input parameter of a `redeem` call. + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their redeemption at the current block, + /// given current on-chain conditions. + function previewRedeem(uint256 shares) external view returns (uint256 assets); +} diff --git a/test/adapters/IEToken.sol b/test/adapters/IEToken.sol new file mode 100644 index 00000000..c0861549 --- /dev/null +++ b/test/adapters/IEToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +/// @notice Tokenised representation of assets +interface IEToken { + + /// @notice Convert an eToken balance to an underlying amount, taking into account current exchange rate + /// @param balance eToken balance, in internal book-keeping units (18 decimals) + /// @return Amount in underlying units, (same decimals as underlying token) + function convertBalanceToUnderlying(uint balance) external view returns (uint); + + /// @notice Transfer underlying tokens from sender to the Euler pool, and increase account's eTokens + /// @param subAccountId 0 for primary, 1-255 for a sub-account + /// @param amount In underlying units (use max uint256 for full underlying token balance) + function deposit(uint subAccountId, uint amount) external; + + /// @notice Transfer underlying tokens from Euler pool to sender, and decrease account's eTokens + /// @param subAccountId 0 for primary, 1-255 for a sub-account + /// @param amount In underlying units (use max uint256 for full pool balance) + function withdraw(uint subAccountId, uint amount) external; +} \ No newline at end of file diff --git a/test/adapters/ILidoEth.sol b/test/adapters/ILidoEth.sol new file mode 100644 index 00000000..1786bd6d --- /dev/null +++ b/test/adapters/ILidoEth.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +interface ILidoETH { + function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); + function submit(address _referral) external returns (uint256); +} \ No newline at end of file diff --git a/test/adapters/IYearnVault.sol b/test/adapters/IYearnVault.sol new file mode 100644 index 00000000..7226ae94 --- /dev/null +++ b/test/adapters/IYearnVault.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +interface IYearnVault { + function token() external view returns (address); + + function underlying() external view returns (address); + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + function controller() external view returns (address); + + function governance() external view returns (address); + + function pricePerShare() external view returns (uint256); + + function deposit(uint256) external; + + function depositAll() external; + + function withdraw(uint256) external; + + function withdrawAll() external; +} \ No newline at end of file diff --git a/test/adapters/InterestRateModel.sol b/test/adapters/InterestRateModel.sol new file mode 100644 index 00000000..4f709e8a --- /dev/null +++ b/test/adapters/InterestRateModel.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +interface InterestRateModel { + 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/test/adapters/LibCompound.sol b/test/adapters/LibCompound.sol new file mode 100644 index 00000000..562c5ae7 --- /dev/null +++ b/test/adapters/LibCompound.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import {FixedPointMathLib} from "./FixedPointMathLib.sol"; + +import {CERC20} from "./CERC20.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(CERC20 cToken, address user) internal view returns (uint256) { + return cToken.balanceOf(user).mulWadDown(viewExchangeRate(cToken)); + } + + function viewExchangeRate(CERC20 cToken) internal view returns (uint256) { + uint256 accrualBlockNumberPrior = cToken.accrualBlockNumber(); + + if (accrualBlockNumberPrior == block.number) return cToken.exchangeRateStored(); + + uint256 totalCash = cToken.underlying().balanceOf(address(cToken)); + uint256 borrowsPrior = cToken.totalBorrows(); + uint256 reservesPrior = cToken.totalReserves(); + + uint256 borrowRateMantissa = cToken.interestRateModel().getBorrowRate(totalCash, borrowsPrior, reservesPrior); + + require(borrowRateMantissa <= 0.0005e16, "RATE_TOO_HIGH"); // Same as borrowRateMaxMantissa in CTokenInterfaces.sol + + 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); + } +} diff --git a/test/adapters/LibFuse.sol b/test/adapters/LibFuse.sol new file mode 100644 index 00000000..3d56cd98 --- /dev/null +++ b/test/adapters/LibFuse.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import {FixedPointMathLib} from "./FixedPointMathLib.sol"; + +import {CERC20} from "./CERC20.sol"; + +/// @notice Get up to date cToken data without mutating state. +/// @author Transmissions11 (https://github.com/transmissions11/libcompound) +library LibFuse { + using FixedPointMathLib for uint256; + + function viewUnderlyingBalanceOf(CERC20 cToken, address user) internal view returns (uint256) { + return cToken.balanceOf(user).mulWadDown(viewExchangeRate(cToken)); + } + + function viewExchangeRate(CERC20 cToken) internal view returns (uint256) { + uint256 accrualBlockNumberPrior = cToken.accrualBlockNumber(); + + if (accrualBlockNumberPrior == block.number) return cToken.exchangeRateStored(); + + uint256 totalCash = cToken.underlying().balanceOf(address(cToken)); + uint256 borrowsPrior = cToken.totalBorrows(); + uint256 reservesPrior = cToken.totalReserves(); + uint256 adminFeesPrior = cToken.totalAdminFees(); + uint256 fuseFeesPrior = cToken.totalFuseFees(); + + uint256 interestAccumulated; // Generated in new scope to avoid stack too deep. + { + uint256 borrowRateMantissa = cToken.interestRateModel().getBorrowRate( + totalCash, + borrowsPrior, + reservesPrior + adminFeesPrior + fuseFeesPrior + ); + + // Same as borrowRateMaxMantissa in CTokenInterfaces.sol + require(borrowRateMantissa <= 0.0005e16, "RATE_TOO_HIGH"); + + interestAccumulated = (borrowRateMantissa * (block.number - accrualBlockNumberPrior)).mulWadDown( + borrowsPrior + ); + } + + uint256 totalReserves = cToken.reserveFactorMantissa().mulWadDown(interestAccumulated) + reservesPrior; + uint256 totalAdminFees = cToken.adminFeeMantissa().mulWadDown(interestAccumulated) + adminFeesPrior; + uint256 totalFuseFees = cToken.fuseFeeMantissa().mulWadDown(interestAccumulated) + fuseFeesPrior; + + uint256 totalSupply = cToken.totalSupply(); + + return + totalSupply == 0 + ? cToken.initialExchangeRateMantissa() + : (totalCash + (interestAccumulated + borrowsPrior) - (totalReserves + totalAdminFees + totalFuseFees)) + .divWadDown(totalSupply); + } +} \ No newline at end of file diff --git a/test/adapters/LidoAdapter.sol b/test/adapters/LidoAdapter.sol new file mode 100644 index 00000000..11f68336 --- /dev/null +++ b/test/adapters/LidoAdapter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import "./ILidoETH.sol"; + +contract LidoAdapter { + + uint8 public protocol = 7; + + function exchangeRateCurrent(address a) external view returns (uint256){ + return(ILidoETH(a).getPooledEthByShares(1e18)); + } +} \ No newline at end of file diff --git a/test/adapters/RariAdapter.sol b/test/adapters/RariAdapter.sol new file mode 100644 index 00000000..bc946f10 --- /dev/null +++ b/test/adapters/RariAdapter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >= 0.8.10; + +import "./LibFuse.sol"; + +contract RariAdapter { + + uint8 public protocol = 2; + + function exchangeRateCurrent(address a) external view returns (uint256){ + return(LibFuse.viewExchangeRate(CERC20(a))); + } +} \ No newline at end of file diff --git a/test/adapters/YearnAdapter.sol b/test/adapters/YearnAdapter.sol new file mode 100644 index 00000000..ada48160 --- /dev/null +++ b/test/adapters/YearnAdapter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import "./IYearnVault.sol"; + +contract YearnAdapter { + + uint8 public protocol = 3; + + function exchangeRateCurrent(address a) external view returns (uint256){ + return(IYearnVault(a).pricePerShare()); + } +} \ No newline at end of file diff --git a/test/marketplace/IAdapter.sol b/test/marketplace/IAdapter.sol new file mode 100644 index 00000000..d0779092 --- /dev/null +++ b/test/marketplace/IAdapter.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +/// @notice Tokenised representation of assets +interface IAdapter { + function exchangeRateCurrent(address cToken) external view returns(uint256); + + function protocol() external view returns(uint8); +} \ No newline at end of file diff --git a/test/marketplace/Interfaces.sol b/test/marketplace/Interfaces.sol index 5a326dcf..5fd1dbb9 100644 --- a/test/marketplace/Interfaces.sol +++ b/test/marketplace/Interfaces.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.4; +pragma solidity >=0.8.4; interface Erc20 { function decimals() external returns (uint8); @@ -10,3 +10,5 @@ interface CErc20 { function exchangeRateCurrent() external returns (uint256); function underlying() external returns (address); } + + diff --git a/test/marketplace/MarketPlace.sol b/test/marketplace/MarketPlace.sol index 8b592465..633a38a8 100644 --- a/test/marketplace/MarketPlace.sol +++ b/test/marketplace/MarketPlace.sol @@ -1,34 +1,42 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.4; +pragma solidity >=0.8.4; import './Interfaces.sol'; import './ZcToken.sol'; import './VaultTracker.sol'; +import './IAdapter.sol'; contract MarketPlace { struct Market { - address cTokenAddr; - address zcTokenAddr; - address vaultAddr; + address cToken; + address zcToken; + address vault; + address adapter; uint256 maturityRate; } - - mapping (address => mapping (uint256 => Market)) public markets; + // Protocol enum => Underlying address => Maturity uint256 => Market + mapping (uint8 => mapping (address => mapping (uint256 => Market))) public markets; address public admin; address public swivel; bool public paused; - event Create(address indexed underlying, uint256 indexed maturity, address cToken, address zcToken, address vaultTracker); - event Mature(address indexed underlying, uint256 indexed maturity, uint256 maturityRate, uint256 matured); - event RedeemZcToken(address indexed underlying, uint256 indexed maturity, address indexed sender, uint256 amount); - event RedeemVaultInterest(address indexed underlying, uint256 indexed maturity, address indexed sender); - event CustodialInitiate(address indexed underlying, uint256 indexed maturity, address zcTarget, address nTarget, uint256 amount); - event CustodialExit(address indexed underlying, uint256 indexed maturity, address zcTarget, address nTarget, uint256 amount); - event P2pZcTokenExchange(address indexed underlying, uint256 indexed maturity, address from, address to, uint256 amount); - event P2pVaultExchange(address indexed underlying, uint256 indexed maturity, address from, address to, uint256 amount); - event TransferVaultNotional(address indexed underlying, uint256 indexed maturity, address from, address to, uint256 amount); + event Create(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address cToken, address zcToken, address vaultTracker, address adapter); + event Mature(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, uint256 maturityRate, uint256 matured); + event RedeemZcToken(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address sender, uint256 amount); + event RedeemVaultInterest(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address sender); + event CustodialInitiate(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address zcTarget, address nTarget, uint256 amount); + event CustodialExit(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address zcTarget, address nTarget, uint256 amount); + event P2pZcTokenExchange(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address from, address to, uint256 amount); + event P2pVaultExchange(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address from, address to, uint256 amount); + event TransferVaultNotional(uint8 indexed protocol, address indexed underlying, uint256 indexed maturity, address from, address to, uint256 amount); + + error Initialize(); + error Maturity(); + error Paused(); + error Unauthorized(); + error Market_Exists(); constructor() { admin = msg.sender; @@ -37,7 +45,10 @@ contract MarketPlace { /// @param s Address of the deployed swivel contract /// @notice We only allow this to be set once function setSwivelAddress(address s) external authorized(admin) returns (bool) { - require(swivel == address(0), 'swivel contract address already set'); + if (swivel != address(0)) { + revert Initialize(); + } + swivel = s; return true; } @@ -51,210 +62,229 @@ contract MarketPlace { /// @notice Allows the owner to create new markets /// @param m Maturity timestamp of the new market /// @param c cToken address associated with underlying for the new market + /// @param a Adapter address associated with the given lending market /// @param n Name of the new zcToken market /// @param s Symbol of the new zcToken market function createMarket( uint256 m, address c, + address a, string memory n, string memory s ) external authorized(admin) unpaused() returns (bool) { - address swivelAddr = swivel; - require(swivelAddr != address(0), 'swivel contract address not set'); - - address underAddr = CErc20(c).underlying(); - require(markets[underAddr][m].vaultAddr == address(0), 'market already exists'); - - uint8 decimals = Erc20(underAddr).decimals(); - address zcTokenAddr = address(new ZcToken(underAddr, m, n, s, decimals)); - address vaultAddr = address(new VaultTracker(m, c, swivelAddr)); - markets[underAddr][m] = Market(c, zcTokenAddr, vaultAddr, 0); + if (swivel == address(0)) { + revert Initialize(); + } + uint8 protocol = IAdapter(a).protocol(); + address under = CErc20(c).underlying(); + + if (markets[protocol][under][m].vault != address(0)) { + revert Market_Exists(); + } + + address zcToken = address(new ZcToken(under, m, n, s, Erc20(under).decimals())); + address vault = address(new VaultTracker(m, c, a, swivel)); + markets[protocol][under][m] = Market(c, zcToken, vault, a, 0); - emit Create(underAddr, m, c, zcTokenAddr, vaultAddr); + emit Create(protocol, under, m, c, zcToken, vault, a); return true; } /// @notice Can be called after maturity, allowing all of the zcTokens to earn floating interest on Compound until they release their funds + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market - function matureMarket(address u, uint256 m) public unpaused() returns (bool) { - Market memory mkt = markets[u][m]; + function matureMarket(uint8 p, address u, uint256 m) public unpaused() returns (bool) { + Market memory mkt = markets[p][u][m]; - require(mkt.maturityRate == 0, 'market already matured'); - require(block.timestamp >= m, "maturity not reached"); + if (mkt.maturityRate != 0 || block.timestamp < m) { + revert Maturity(); + } // set the base maturity cToken exchange rate at maturity to the current cToken exchange rate - uint256 currentExchangeRate = CErc20(mkt.cTokenAddr).exchangeRateCurrent(); - markets[u][m].maturityRate = currentExchangeRate; + uint256 currentExchangeRate = IAdapter(mkt.adapter).exchangeRateCurrent(mkt.cToken); + markets[p][u][m].maturityRate = currentExchangeRate; - require(VaultTracker(mkt.vaultAddr).matureVault(currentExchangeRate), 'mature vault failed'); + VaultTracker(mkt.vault).matureVault(currentExchangeRate); - emit Mature(u, m, currentExchangeRate, block.timestamp); + emit Mature(p, u, m, currentExchangeRate, block.timestamp); return true; } /// @notice Allows Swivel caller to deposit their underlying, in the process splitting it - minting both zcTokens and vault notional. + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param t Address of the depositing user /// @param a Amount of notional being added - function mintZcTokenAddingNotional(address u, uint256 m, address t, uint256 a) external authorized(swivel) unpaused() returns (bool) { - Market memory mkt = markets[u][m]; - require(ZcToken(mkt.zcTokenAddr).mint(t, a), 'mint zcToken failed'); - require(VaultTracker(mkt.vaultAddr).addNotional(t, a), 'add notional failed'); + function mintZcTokenAddingNotional(uint8 p, address u, uint256 m, address t, uint256 a) external authorized(swivel) unpaused() returns (bool) { + Market memory mkt = markets[p][u][m]; + ZcToken(mkt.zcToken).mint(t, a); + VaultTracker(mkt.vault).addNotional(t, a); return true; } /// @notice Allows Swivel caller to deposit/burn both zcTokens + vault notional. This process is "combining" the two and redeeming underlying. + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param t Address of the combining/redeeming user /// @param a Amount of zcTokens being burned - function burnZcTokenRemovingNotional(address u, uint256 m, address t, uint256 a) external authorized(swivel) unpaused() returns(bool) { - Market memory mkt = markets[u][m]; - require(ZcToken(mkt.zcTokenAddr).burn(t, a), 'burn failed'); - require(VaultTracker(mkt.vaultAddr).removeNotional(t, a), 'remove notional failed'); + function burnZcTokenRemovingNotional(uint8 p, address u, uint256 m, address t, uint256 a) external authorized(swivel) unpaused() returns(bool) { + Market memory mkt = markets[p][u][m]; + ZcToken(mkt.zcToken).burn(t, a); + VaultTracker(mkt.vault).removeNotional(t, a); return true; } /// @notice Allows (via swivel) zcToken holders to redeem their tokens for underlying tokens after maturity has been reached. + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param t Address of the redeeming user /// @param a Amount of zcTokens being redeemed - function redeemZcToken(address u, uint256 m, address t, uint256 a) external authorized(swivel) unpaused() returns (uint256) { - Market memory mkt = markets[u][m]; + function redeemZcToken(uint8 p, address u, uint256 m, address t, uint256 a) external authorized(swivel) unpaused() returns (uint256) { + Market memory mkt = markets[p][u][m]; // if the market has not matured, mature it and redeem exactly the amount if (mkt.maturityRate == 0) { - require(matureMarket(u, m), 'failed to mature the market'); + matureMarket(p, u, m); } // burn user's zcTokens - require(ZcToken(mkt.zcTokenAddr).burn(t, a), 'could not burn'); + ZcToken(mkt.zcToken).burn(t, a); - emit RedeemZcToken(u, m, t, a); + emit RedeemZcToken(p, u, m, t, a); if (mkt.maturityRate == 0) { return a; } else { // if the market was already mature the return should include the amount + marginal floating interest generated on Compound since maturity - return calculateReturn(u, m, a); + return calculateReturn(p, u, m, a); } } /// @notice Allows Vault owners (via Swivel) to redeem any currently accrued interest + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param t Address of the redeeming user - function redeemVaultInterest(address u, uint256 m, address t) external authorized(swivel) unpaused() returns (uint256) { + function redeemVaultInterest(uint8 p, address u, uint256 m, address t) external authorized(swivel) unpaused() returns (uint256) { // call to the floating market contract to release the position and calculate the interest generated - uint256 interest = VaultTracker(markets[u][m].vaultAddr).redeemInterest(t); + uint256 interest = VaultTracker(markets[p][u][m].vault).redeemInterest(t); - emit RedeemVaultInterest(u, m, t); + emit RedeemVaultInterest(p, u, m, t); return interest; } /// @notice Calculates the total amount of underlying returned including interest generated since the `matureMarket` function has been called + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param a Amount of zcTokens being redeemed - function calculateReturn(address u, uint256 m, uint256 a) internal returns (uint256) { - Market memory mkt = markets[u][m]; - uint256 rate = CErc20(mkt.cTokenAddr).exchangeRateCurrent(); + function calculateReturn(uint8 p, address u, uint256 m, uint256 a) internal view returns (uint256) { + Market memory mkt = markets[p][u][m]; + uint256 rate = IAdapter(mkt.adapter).exchangeRateCurrent(mkt.cToken); return (a * rate) / mkt.maturityRate; } /// @notice Return the ctoken address for a given market + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market - function cTokenAddress(address u, uint256 m) external view returns (address) { - return markets[u][m].cTokenAddr; + function cTokenAddress(uint8 p, address u, uint256 m) external view returns (address) { + return markets[p][u][m].cToken; } /// @notice Called by swivel IVFZI && IZFVI /// @dev Call with underlying, maturity, mint-target, add-notional-target and an amount + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param z Recipient of the minted zcToken /// @param n Recipient of the added notional /// @param a Amount of zcToken minted and notional added - function custodialInitiate(address u, uint256 m, address z, address n, uint256 a) external authorized(swivel) unpaused() returns (bool) { - Market memory mkt = markets[u][m]; - require(ZcToken(mkt.zcTokenAddr).mint(z, a), 'mint failed'); - require(VaultTracker(mkt.vaultAddr).addNotional(n, a), 'add notional failed'); - emit CustodialInitiate(u, m, z, n, a); + function custodialInitiate(uint8 p, address u, uint256 m, address z, address n, uint256 a) external authorized(swivel) unpaused() returns (bool) { + Market memory mkt = markets[p][u][m]; + ZcToken(mkt.zcToken).mint(z, a); + VaultTracker(mkt.vault).addNotional(n, a); + emit CustodialInitiate(p, u, m, z, n, a); return true; } /// @notice Called by swivel EVFZE FF EZFVE /// @dev Call with underlying, maturity, burn-target, remove-notional-target and an amount + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param z Owner of the zcToken to be burned /// @param n Target to remove notional from /// @param a Amount of zcToken burned and notional removed - function custodialExit(address u, uint256 m, address z, address n, uint256 a) external authorized(swivel) unpaused() returns (bool) { - Market memory mkt = markets[u][m]; - require(ZcToken(mkt.zcTokenAddr).burn(z, a), 'burn failed'); - require(VaultTracker(mkt.vaultAddr).removeNotional(n, a), 'remove notional failed'); - emit CustodialExit(u, m, z, n, a); + function custodialExit(uint8 p, address u, uint256 m, address z, address n, uint256 a) external authorized(swivel) unpaused() returns (bool) { + Market memory mkt = markets[p][u][m]; + ZcToken(mkt.zcToken).burn(z, a); + VaultTracker(mkt.vault).removeNotional(n, a); + emit CustodialExit(p, u, m, z, n, a); return true; } /// @notice Called by swivel IZFZE, EZFZI /// @dev Call with underlying, maturity, transfer-from, transfer-to, amount + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param f Owner of the zcToken to be burned /// @param t Target to be minted to /// @param a Amount of zcToken transfer - function p2pZcTokenExchange(address u, uint256 m, address f, address t, uint256 a) external authorized(swivel) unpaused() returns (bool) { - Market memory mkt = markets[u][m]; - require(ZcToken(mkt.zcTokenAddr).burn(f, a), 'zcToken burn failed'); - require(ZcToken(mkt.zcTokenAddr).mint(t, a), 'zcToken mint failed'); - emit P2pZcTokenExchange(u, m, f, t, a); + function p2pZcTokenExchange(uint8 p, address u, uint256 m, address f, address t, uint256 a) external authorized(swivel) unpaused() returns (bool) { + Market memory mkt = markets[p][u][m]; + ZcToken(mkt.zcToken).burn(f, a); + ZcToken(mkt.zcToken).mint(t, a); + emit P2pZcTokenExchange(p, u, m, f, t, a); return true; } /// @notice Called by swivel IVFVE, EVFVI /// @dev Call with underlying, maturity, remove-from, add-to, amount + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param f Owner of the notional to be transferred /// @param t Target to be transferred to /// @param a Amount of notional transfer - function p2pVaultExchange(address u, uint256 m, address f, address t, uint256 a) external authorized(swivel) unpaused() returns (bool) { - require(VaultTracker(markets[u][m].vaultAddr).transferNotionalFrom(f, t, a), 'transfer notional failed'); - emit P2pVaultExchange(u, m, f, t, a); + function p2pVaultExchange(uint8 p, address u, uint256 m, address f, address t, uint256 a) external authorized(swivel) unpaused() returns (bool) { + VaultTracker(markets[p][u][m].vault).transferNotionalFrom(f, t, a); + emit P2pVaultExchange(p, u, m, f, t, a); return true; } /// @notice External method giving access to this functionality within a given vault /// @dev Note that this method calculates yield and interest as well + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param t Target to be transferred to /// @param a Amount of notional to be transferred - function transferVaultNotional(address u, uint256 m, address t, uint256 a) external unpaused() returns (bool) { - require(VaultTracker(markets[u][m].vaultAddr).transferNotionalFrom(msg.sender, t, a), 'vault transfer failed'); - emit TransferVaultNotional(u, m, msg.sender, t, a); + function transferVaultNotional(uint8 p, address u, uint256 m, address t, uint256 a) external unpaused() returns (bool) { + VaultTracker(markets[p][u][m].vault).transferNotionalFrom(msg.sender, t, a); + emit TransferVaultNotional(p, u, m, msg.sender, t, a); return true; } /// @notice Transfers notional fee to the Swivel contract without recalculating marginal interest for from + /// @param p Protocol of the given market (Enum) /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param f Owner of the amount /// @param a Amount to transfer - function transferVaultNotionalFee(address u, uint256 m, address f, uint256 a) external authorized(swivel) returns (bool) { - VaultTracker(markets[u][m].vaultAddr).transferNotionalFee(f, a); + function transferVaultNotionalFee(uint8 p, address u, uint256 m, address f, uint256 a) external authorized(swivel) returns (bool) { + VaultTracker(markets[p][u][m].vault).transferNotionalFee(f, a); return true; } @@ -266,12 +296,16 @@ contract MarketPlace { } modifier authorized(address a) { - require(msg.sender == a, 'sender must be authorized'); + if (msg.sender != a) { + revert Unauthorized(); + } _; } modifier unpaused() { - require(!paused, 'markets are paused'); + if (paused) { + revert Paused(); + } _; } -} +} \ No newline at end of file diff --git a/test/swivel/IAavePool.sol b/test/swivel/IAavePool.sol new file mode 100644 index 00000000..42eb33d5 --- /dev/null +++ b/test/swivel/IAavePool.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +interface IAavePool { + /** + * @notice Returns the normalized income normalized income of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The reserve's normalized income + */ + function getReserveNormalizedIncome(address asset) external view returns (uint256); + +} diff --git a/test/swivel/IERC20.sol b/test/swivel/IERC20.sol new file mode 100644 index 00000000..b7490382 --- /dev/null +++ b/test/swivel/IERC20.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} \ No newline at end of file diff --git a/test/swivel/IERC4626.sol b/test/swivel/IERC4626.sol new file mode 100644 index 00000000..8dfb9eb6 --- /dev/null +++ b/test/swivel/IERC4626.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +import "./IERC20.sol"; + +interface IERC4626 is IERC20 { + /*//////////////////////////////////////////////////////// + Events + ////////////////////////////////////////////////////////*/ + + /// @notice `sender` has exchanged `assets` for `shares`, + /// and transferred those `shares` to `receiver`. + event Deposit(address indexed sender, address indexed receiver, uint256 assets, uint256 shares); + + /// @notice `sender` has exchanged `shares` for `assets`, + /// and transferred those `assets` to `receiver`. + event Withdraw(address indexed sender, address indexed receiver, uint256 assets, uint256 shares); + + /*//////////////////////////////////////////////////////// + Vault properties + ////////////////////////////////////////////////////////*/ + + /// @notice The address of the underlying ERC20 token used for + /// the Vault for accounting, depositing, and withdrawing. + function asset() external view returns (address); + + /// @notice Total amount of the underlying asset that + /// is "managed" by Vault. + function totalAssets() external view returns (uint256); + + /*//////////////////////////////////////////////////////// + Deposit/Withdrawal Logic + ////////////////////////////////////////////////////////*/ + + /// @notice Mints `shares` IN ASSETS Vault shares to `receiver` by + /// depositing exactly `assets` of underlying tokens. + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /// @notice Mints exactly `shares` IN SHARES Vault shares to `receiver` + /// by depositing `assets` of underlying tokens. + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /// @notice Redeems `shares` IN ASSETS from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /// @notice Redeems `shares` IN SHARES from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /*//////////////////////////////////////////////////////// + Vault Accounting Logic + ////////////////////////////////////////////////////////*/ + + /// @notice The amount of shares that the vault would + /// exchange for the amount of assets provided, in an + /// ideal scenario where all the conditions are met. + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /// @notice The amount of assets that the vault would + /// exchange for the amount of shares provided, in an + /// ideal scenario where all the conditions are met. + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /// @notice Total number of underlying assets that can + /// be deposited by `owner` into the Vault, where `owner` + /// corresponds to the input parameter `receiver` of a + /// `deposit` call. + function maxDeposit(address owner) external view returns (uint256 maxAssets); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their deposit at the current block, given + /// current on-chain conditions. + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /// @notice Total number of underlying shares that can be minted + /// for `owner`, where `owner` corresponds to the input + /// parameter `receiver` of a `mint` call. + function maxMint(address owner) external view returns (uint256 maxShares); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their mint at the current block, given + /// current on-chain conditions. + function previewMint(uint256 shares) external view returns (uint256 assets); + + /// @notice Total number of underlying assets that can be + /// withdrawn from the Vault by `owner`, where `owner` + /// corresponds to the input parameter of a `withdraw` call. + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their withdrawal at the current block, + /// given current on-chain conditions. + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /// @notice Total number of underlying shares that can be + /// redeemed from the Vault by `owner`, where `owner` corresponds + /// to the input parameter of a `redeem` call. + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /// @notice Allows an on-chain or off-chain user to simulate + /// the effects of their redeemption at the current block, + /// given current on-chain conditions. + function previewRedeem(uint256 shares) external view returns (uint256 assets); +} diff --git a/test/swivel/IEToken.sol b/test/swivel/IEToken.sol new file mode 100644 index 00000000..c0861549 --- /dev/null +++ b/test/swivel/IEToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +/// @notice Tokenised representation of assets +interface IEToken { + + /// @notice Convert an eToken balance to an underlying amount, taking into account current exchange rate + /// @param balance eToken balance, in internal book-keeping units (18 decimals) + /// @return Amount in underlying units, (same decimals as underlying token) + function convertBalanceToUnderlying(uint balance) external view returns (uint); + + /// @notice Transfer underlying tokens from sender to the Euler pool, and increase account's eTokens + /// @param subAccountId 0 for primary, 1-255 for a sub-account + /// @param amount In underlying units (use max uint256 for full underlying token balance) + function deposit(uint subAccountId, uint amount) external; + + /// @notice Transfer underlying tokens from Euler pool to sender, and decrease account's eTokens + /// @param subAccountId 0 for primary, 1-255 for a sub-account + /// @param amount In underlying units (use max uint256 for full pool balance) + function withdraw(uint subAccountId, uint amount) external; +} \ No newline at end of file diff --git a/test/swivel/IYearnVault.sol b/test/swivel/IYearnVault.sol new file mode 100644 index 00000000..7226ae94 --- /dev/null +++ b/test/swivel/IYearnVault.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +interface IYearnVault { + function token() external view returns (address); + + function underlying() external view returns (address); + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + function controller() external view returns (address); + + function governance() external view returns (address); + + function pricePerShare() external view returns (uint256); + + function deposit(uint256) external; + + function depositAll() external; + + function withdraw(uint256) external; + + function withdrawAll() external; +} \ No newline at end of file diff --git a/test/swivel/Interfaces.sol b/test/swivel/Interfaces.sol index 8b1b79d8..f2d048dd 100644 --- a/test/swivel/Interfaces.sol +++ b/test/swivel/Interfaces.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.4; +pragma solidity >= 0.8.4; interface Erc20 { function approve(address, uint256) external returns (bool); @@ -9,30 +9,114 @@ interface Erc20 { function transferFrom(address, address, uint256) external returns (bool); } +interface IERC20 is Erc20 { + function totalSupply() external view returns (uint256); +} + interface CErc20 { function mint(uint256) external returns (uint256); function redeemUnderlying(uint256) external returns (uint256); } interface MarketPlace { + + struct Market { + address cToken; + address zcToken; + address vault; + address adapter; + uint256 maturityRate; + } + + function markets (uint8, address, uint256) external view returns (Market memory); + // adds notional and mints zctokens - function mintZcTokenAddingNotional(address, uint256, address, uint256) external returns (bool); + function mintZcTokenAddingNotional(uint8, address, uint256, address, uint256) external returns (bool); // removes notional and burns zctokens - function burnZcTokenRemovingNotional(address, uint256, address, uint256) external returns (bool); - // returns the amount of underlying principal to send - function redeemZcToken(address, uint256, address, uint256) external returns (uint256); + function burnZcTokenRemovingNotional(uint8, address, uint256, address, uint256) external returns (bool); // returns the amount of underlying interest to send - function redeemVaultInterest(address, uint256, address) external returns (uint256); + function redeemVaultInterest(uint8, address, uint256, address) external returns (uint256); + // returns the amount of underlying principal to send + function redeemZcToken(uint8, address, uint256, address, uint256) external returns (uint256); // returns the cToken address for a given market - function cTokenAddress(address, uint256) external returns (address); + function cTokenAddress(uint8, address, uint256) external returns (address); // EVFZE FF EZFVE call this which would then burn zctoken and remove notional - function custodialExit(address, uint256, address, address, uint256) external returns (bool); + function custodialExit(uint8, address, uint256, address, address, uint256) external returns (bool); // IVFZI && IZFVI call this which would then mint zctoken and add notional - function custodialInitiate(address, uint256, address, address, uint256) external returns (bool); + function custodialInitiate(uint8, address, uint256, address, address, uint256) external returns (bool); // IZFZE && EZFZI call this, tranferring zctoken from one party to another - function p2pZcTokenExchange(address, uint256, address, address, uint256) external returns (bool); + function p2pZcTokenExchange(uint8, address, uint256, address, address, uint256) external returns (bool); // IVFVE && EVFVI call this, removing notional from one party and adding to the other - function p2pVaultExchange(address, uint256, address, address, uint256) external returns (bool); + function p2pVaultExchange(uint8, address, uint256, address, address, uint256) external returns (bool); // IVFZI && IVFVE call this which then transfers notional from msg.sender (taker) to swivel - function transferVaultNotionalFee(address, uint256, address, uint256) external returns (bool); + function transferVaultNotionalFee(uint8, address, uint256, address, uint256) external returns (bool); +} + +interface IYearnVault { + function token() external view returns (address); + + function underlying() external view returns (address); + + function pricePerShare() external view returns (uint256); + + function deposit(uint256) external; + + function withdraw(uint256) external; +} + +interface IAavePool { + /** + * @notice Returns the normalized income normalized income of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The reserve's normalized income + */ + function getReserveNormalizedIncome(address asset) external view returns (uint256); + /** + * @dev Emitted on deposit() + * @param asset The address of the underlying asset of the reserve + * @param amount The amount deposited + * @param onBehalfOf The beneficiary of the deposit, receiving the aTokens + * @param referralCode The referral code used + **/ + function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + + function withdraw(address asset, uint256 amount, address to) external; +} + +interface IEulerToken { + function deposit(uint subAccountId, uint amount) external; + function withdraw(uint subAccountId, uint amount) external; +} + +interface IERC4626 is IERC20 { + + /// @notice Mints `shares` IN ASSETS Vault shares to `receiver` by + /// depositing exactly `assets` of underlying tokens. + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /// @notice Mints exactly `shares` IN SHARES Vault shares to `receiver` + /// by depositing `assets` of underlying tokens. + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /// @notice Redeems `shares` IN ASSETS from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /// @notice Redeems `shares` IN SHARES from `owner` and sends `assets` + /// of underlying tokens to `receiver`. + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /*//////////////////////////////////////////////////////// + Vault Accounting Logic + ////////////////////////////////////////////////////////*/ + + /// @notice The amount of shares that the vault would + /// exchange for the amount of assets provided, in an + /// ideal scenario where all the conditions are met. + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /// @notice The amount of assets that the vault would + /// exchange for the amount of shares provided, in an + /// ideal scenario where all the conditions are met. + function convertToAssets(uint256 shares) external view returns (uint256 assets); } diff --git a/test/swivel/Swivel.sol b/test/swivel/Swivel.sol index c842f3ec..3e5ab7c4 100644 --- a/test/swivel/Swivel.sol +++ b/test/swivel/Swivel.sol @@ -1,11 +1,14 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity 0.8.4; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >= 0.8.4; import './Interfaces.sol'; import './Hash.sol'; import './Sig.sol'; import './Safe.sol'; +import './IYearnVault.sol'; +import './IAavePool.sol'; +import './IEToken.sol'; +import './IERC4626.sol'; contract Swivel { /// @dev maps the key of an order to a boolean indicating if an order was cancelled @@ -20,8 +23,10 @@ contract Swivel { uint256 constant public HOLD = 3 days; bytes32 public immutable domain; address public immutable marketPlace; + IAavePool public immutable aaveRouter; address public admin; uint16 constant public MIN_FEENOMINATOR = 33; + /// @dev holds the fee demoninators for [zcTokenInitiate, zcTokenExit, vaultInitiate, vaultExit] uint16[4] public feenominators; @@ -42,12 +47,23 @@ contract Swivel { /// @notice Emitted on a change to the feenominators array event SetFee(uint256 indexed index, uint256 indexed feenominator); + error Invalid(); + error Expired(); + error Cancelled(); + error Maker(); + error Fill_Amount(); + error Authorized(); + error Length(); + error Withdrawal(); + error Fee_Size(); + /// @param m deployed MarketPlace contract address - constructor(address m) { + constructor(address m, address a) { admin = msg.sender; domain = Hash.domain(NAME, VERSION, block.chainid, address(this)); marketPlace = m; feenominators = [200, 600, 400, 200]; + aaveRouter = IAavePool(a); } // ********* INITIATING ************* @@ -89,26 +105,50 @@ contract Swivel { bytes32 hash = validOrderHash(o, c); // checks the side, and the amount compared to available - require((a + filled[hash]) <= o.premium, 'taker amount > available volume'); - + if ((a + filled[hash]) > o.premium) { + revert Fill_Amount(); + } filled[hash] += a; // transfer underlying tokens - Erc20 uToken = Erc20(o.underlying); + ERC20 uToken = ERC20(o.underlying); Safe.transferFrom(uToken, msg.sender, o.maker, a); uint256 principalFilled = (a * o.principal) / o.premium; Safe.transferFrom(uToken, o.maker, address(this), principalFilled); MarketPlace mPlace = MarketPlace(marketPlace); + // mint tokens - require(CErc20(mPlace.cTokenAddress(o.underlying, o.maturity)).mint(principalFilled) == 0, 'minting CToken failed'); + // Compound and Rari + if (o.protocol == 1 || o.protocol == 2) { + CErc20(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).mint(principalFilled); + } + // Yearn + else if (o.protocol == 3) { + IYearnVault(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).deposit(principalFilled); + } + // Aave + else if (o.protocol == 4) { + aaveRouter.deposit(o.underlying, principalFilled, address(this), 0); + } + // Euler + else if (o.protocol == 5) { + IEToken(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).deposit(0, principalFilled); + } + else if (o.protocol == 6) { + IERC4626(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).deposit(a, address(this)); + } + // Potential Lido integration + // else if (o.protocol == 7) { + // } + // alert marketplace - require(mPlace.custodialInitiate(o.underlying, o.maturity, o.maker, msg.sender, principalFilled), 'custodial initiate failed'); + require(mPlace.custodialInitiate(o.protocol, o.underlying, o.maturity, o.maker, msg.sender, principalFilled), 'custodial initiate failed'); // transfer fee in vault notional to swivel (from msg.sender) uint256 fee = principalFilled / feenominators[2]; - require(mPlace.transferVaultNotionalFee(o.underlying, o.maturity, msg.sender, fee), 'notional fee transfer failed'); + require(mPlace.transferVaultNotionalFee(o.protocol, o.underlying, o.maturity, msg.sender, fee), 'notional fee transfer failed'); emit Initiate(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, principalFilled); } @@ -121,11 +161,13 @@ contract Swivel { function initiateZcTokenFillingVaultInitiate(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); - require((a + filled[hash]) <= o.principal, 'taker amount > available volume'); + if ((a + filled[hash]) > o.principal) { + revert Fill_Amount(); + } filled[hash] += a; - Erc20 uToken = Erc20(o.underlying); + ERC20 uToken = ERC20(o.underlying); uint256 premiumFilled = (a * o.premium) / o.principal; Safe.transferFrom(uToken, o.maker, msg.sender, premiumFilled); @@ -135,10 +177,31 @@ contract Swivel { Safe.transferFrom(uToken, msg.sender, address(this), (a + fee)); MarketPlace mPlace = MarketPlace(marketPlace); - // mint tokens - require(CErc20(mPlace.cTokenAddress(o.underlying, o.maturity)).mint(a) == 0, 'minting CToken Failed'); + // mint tokens + // Compound and Rari + if (o.protocol == 1 || o.protocol == 2) { + CErc20(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).mint(a); + } + // Yearn + else if (o.protocol == 3) { + IYearnVault(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).deposit(a); + } + // Aave + else if (o.protocol == 4) { + aaveRouter.deposit(o.underlying, a, address(this), 0); + } + // Euler + else if (o.protocol == 5) { + IEToken(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).deposit(0, a); + } + else if (o.protocol == 6) { + IERC4626(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).deposit(a, address(this)); + } + // Potential Lido integration + // else if (o.protocol == 7) { + // } // alert marketplace - require(mPlace.custodialInitiate(o.underlying, o.maturity, msg.sender, o.maker, a), 'custodial initiate failed'); + require(mPlace.custodialInitiate(o.protocol, o.underlying, o.maturity, msg.sender, o.maker, a), 'custodial initiate failed'); emit Initiate(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, premiumFilled); } @@ -151,13 +214,16 @@ contract Swivel { function initiateZcTokenFillingZcTokenExit(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); - require((a + filled[hash]) <= o.principal, 'taker amount > available volume'); + if ((a + filled[hash]) > o.principal) { + revert Fill_Amount(); + } + filled[hash] += a; uint256 premiumFilled = (a * o.premium) / o.principal; - Erc20 uToken = Erc20(o.underlying); + ERC20 uToken = ERC20(o.underlying); // transfer underlying tokens, then take fee Safe.transferFrom(uToken, msg.sender, o.maker, a - premiumFilled); @@ -165,7 +231,7 @@ contract Swivel { Safe.transferFrom(uToken, msg.sender, address(this), fee); // alert marketplace - require(MarketPlace(marketPlace).p2pZcTokenExchange(o.underlying, o.maturity, o.maker, msg.sender, a), 'zcToken exchange failed'); + require(MarketPlace(marketPlace).p2pZcTokenExchange(o.protocol, o.underlying, o.maturity, o.maker, msg.sender, a), 'zcToken exchange failed'); emit Initiate(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, premiumFilled); } @@ -178,20 +244,22 @@ contract Swivel { function initiateVaultFillingVaultExit(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); - require((a + filled[hash]) <= o.premium, 'taker amount > available volume'); + if ((a + filled[hash]) > o.premium) { + revert Fill_Amount(); + } filled[hash] += a; - Safe.transferFrom(Erc20(o.underlying), msg.sender, o.maker, a); + Safe.transferFrom(ERC20(o.underlying), msg.sender, o.maker, a); MarketPlace mPlace = MarketPlace(marketPlace); uint256 principalFilled = (a * o.principal) / o.premium; // alert marketplace - require(mPlace.p2pVaultExchange(o.underlying, o.maturity, o.maker, msg.sender, principalFilled), 'vault exchange failed'); + require(mPlace.p2pVaultExchange(o.protocol, o.underlying, o.maturity, o.maker, msg.sender, principalFilled), 'vault exchange failed'); // transfer fee (in vault notional) to swivel uint256 fee = principalFilled / feenominators[2]; - require(mPlace.transferVaultNotionalFee(o.underlying, o.maturity, msg.sender, fee), "notional fee transfer failed"); + require(mPlace.transferVaultNotionalFee(o.protocol, o.underlying, o.maturity, msg.sender, fee), "notional fee transfer failed"); emit Initiate(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, principalFilled); } @@ -240,11 +308,13 @@ contract Swivel { function exitZcTokenFillingZcTokenInitiate(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); - require((a + filled[hash]) <= o.premium, 'taker amount > available volume'); + if ((a + filled[hash]) > o.premium) { + revert Fill_Amount(); + } filled[hash] += a; - Erc20 uToken = Erc20(o.underlying); + ERC20 uToken = ERC20(o.underlying); uint256 principalFilled = (a * o.principal) / o.premium; // transfer underlying from initiating party to exiting party, minus the price the exit party pays for the exit (premium), and the fee. @@ -255,7 +325,7 @@ contract Swivel { Safe.transferFrom(uToken, o.maker, address(this), fee); // alert marketplace - require(MarketPlace(marketPlace).p2pZcTokenExchange(o.underlying, o.maturity, msg.sender, o.maker, principalFilled), 'zcToken exchange failed'); + require(MarketPlace(marketPlace).p2pZcTokenExchange(o.protocol, o.underlying, o.maturity, msg.sender, o.maker, principalFilled), 'zcToken exchange failed'); emit Exit(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, principalFilled); } @@ -268,11 +338,13 @@ contract Swivel { function exitVaultFillingVaultInitiate(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); - require((a + filled[hash]) <= o.principal, 'taker amount > available volume'); + if ((a + filled[hash]) > o.principal) { + revert Fill_Amount(); + } filled[hash] += a; - Erc20 uToken = Erc20(o.underlying); + ERC20 uToken = ERC20(o.underlying); // transfer premium from maker to sender uint256 premiumFilled = (a * o.premium) / o.principal; @@ -283,7 +355,7 @@ contract Swivel { Safe.transferFrom(uToken, msg.sender, address(this), fee); // transfer notional from sender to maker - require(MarketPlace(marketPlace).p2pVaultExchange(o.underlying, o.maturity, msg.sender, o.maker, a), 'vault exchange failed'); + require(MarketPlace(marketPlace).p2pVaultExchange(o.protocol, o.underlying, o.maturity, msg.sender, o.maker, a), 'vault exchange failed'); emit Exit(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, premiumFilled); } @@ -296,16 +368,39 @@ contract Swivel { function exitVaultFillingZcTokenExit(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); - require((a + filled[hash]) <= o.principal, 'taker amount > available volume'); + if ((a + filled[hash]) > o.principal) { + revert Fill_Amount(); + } filled[hash] += a; // redeem underlying on Compound and burn cTokens MarketPlace mPlace = MarketPlace(marketPlace); - address cTokenAddr = mPlace.cTokenAddress(o.underlying, o.maturity); - require((CErc20(cTokenAddr).redeemUnderlying(a) == 0), "compound redemption error"); + // mint tokens + // Compound and Rari + if (o.protocol == 1 || o.protocol == 2) { + CErc20(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).redeemUnderlying(a); + } + // Yearn + else if (o.protocol == 3) { + IYearnVault(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).withdraw(a); + } + // Aave + else if (o.protocol == 4) { + aaveRouter.withdraw(o.underlying, a, address(this)); + } + // Euler + else if (o.protocol == 5) { + IEToken(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).withdraw(0, a); + } + else if (o.protocol == 6) { + IERC4626(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).withdraw(a, address(this), address(this)); + } + // Potential Lido integration + // else if (o.protocol == 7) { + // } - Erc20 uToken = Erc20(o.underlying); + ERC20 uToken = ERC20(o.underlying); // transfer principal-premium back to fixed exit party now that the interest coupon and zcb have been redeemed uint256 premiumFilled = (a * o.premium) / o.principal; Safe.transfer(uToken, o.maker, a - premiumFilled); @@ -315,7 +410,7 @@ contract Swivel { Safe.transfer(uToken, msg.sender, premiumFilled - fee); // burn zcTokens + nTokens from o.maker and msg.sender respectively - require(mPlace.custodialExit(o.underlying, o.maturity, o.maker, msg.sender, a), 'custodial exit failed'); + require(mPlace.custodialExit(o.protocol, o.underlying, o.maturity, o.maker, msg.sender, a), 'custodial exit failed'); emit Exit(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, premiumFilled); } @@ -328,25 +423,47 @@ contract Swivel { function exitZcTokenFillingVaultExit(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); - require((a + filled[hash]) <= o.premium, 'taker amount > available volume'); + if ((a + filled[hash]) > o.premium) { + revert Fill_Amount(); + } filled[hash] += a; // redeem underlying on Compound and burn cTokens MarketPlace mPlace = MarketPlace(marketPlace); - address cTokenAddr = mPlace.cTokenAddress(o.underlying, o.maturity); uint256 principalFilled = (a * o.principal) / o.premium; - require((CErc20(cTokenAddr).redeemUnderlying(principalFilled) == 0), "compound redemption error"); + // Compound and Rari + if (o.protocol == 1 || o.protocol == 2) { + CErc20(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).redeemUnderlying(principalFilled); + } + // Yearn + else if (o.protocol == 3) { + IYearnVault(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).withdraw(principalFilled); + } + // Aave + else if (o.protocol == 4) { + aaveRouter.withdraw(o.underlying, principalFilled, address(this)); + } + // Euler + else if (o.protocol == 5) { + IEToken(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).withdraw(0, principalFilled); + } + else if (o.protocol == 6) { + IERC4626(mPlace.cTokenAddress(o.protocol, o.underlying, o.maturity)).withdraw(principalFilled, address(this), address(this)); + } + // Potential Lido integration + // else if (o.protocol == 7) { + // } - Erc20 uToken = Erc20(o.underlying); + ERC20 uToken = ERC20(o.underlying); // transfer principal-premium-fee back to fixed exit party now that the interest coupon and zcb have been redeemed uint256 fee = principalFilled / feenominators[1]; Safe.transfer(uToken, msg.sender, principalFilled - a - fee); Safe.transfer(uToken, o.maker, a); // burn zcTokens + nTokens from msg.sender and o.maker respectively - require(mPlace.custodialExit(o.underlying, o.maturity, msg.sender, o.maker, principalFilled), 'custodial exit failed'); + require(mPlace.custodialExit(o.protocol, o.underlying, o.maturity, msg.sender, o.maker, principalFilled), 'custodial exit failed'); emit Exit(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, principalFilled); } @@ -357,7 +474,9 @@ contract Swivel { function cancel(Hash.Order calldata o, Sig.Components calldata c) external returns (bool) { bytes32 hash = validOrderHash(o, c); - require(msg.sender == o.maker, 'sender must be maker'); + if (msg.sender != o.maker) { + revert Maker(); + } cancelled[hash] = true; @@ -400,13 +519,14 @@ contract Swivel { /// @param e Address of token to withdraw function withdraw(address e) external authorized(admin) returns (bool) { uint256 when = withdrawals[e]; - require (when != 0, 'no withdrawal scheduled'); - require (block.timestamp >= when, 'withdrawal still on hold'); + if (when == 0 || block.timestamp < when) { + revert Withdrawal(); + } withdrawals[e] = 0; - Erc20 token = Erc20(e); + ERC20 token = ERC20(e); Safe.transfer(token, admin, token.balanceOf(address(this))); return true; @@ -416,7 +536,9 @@ contract Swivel { /// @param i The index of the new fee denominator /// @param d The new fee denominator function setFee(uint16 i, uint16 d) external authorized(admin) returns (bool) { - require(d >= MIN_FEENOMINATOR, 'fee too high'); + if (d < MIN_FEENOMINATOR) { + revert Fee_Size(); + } feenominators[i] = d; @@ -430,13 +552,16 @@ contract Swivel { /// @param c array of compound token addresses function approveUnderlying(address[] calldata u, address[] calldata c) external authorized(admin) returns (bool) { uint256 len = u.length; - require (len == c.length, 'array length mismatch'); + + if (len != c.length) { + revert Length(); + } uint256 max = 2**256 - 1; for (uint256 i; i < len; i++) { - Erc20 uToken = Erc20(u[i]); - Safe.approve(uToken, c[i], max); + ERC20 uToken = ERC20(u[i]); + SafeTransferLib.approve(uToken, c[i], max); } return true; @@ -449,58 +574,128 @@ contract Swivel { /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param a Amount of underlying being deposited - function splitUnderlying(address u, uint256 m, uint256 a) external returns (bool) { - Erc20 uToken = Erc20(u); + function splitUnderlying(uint8 p, address u, uint256 m,uint256 a) external returns (bool) { + ERC20 uToken = ERC20(u); Safe.transferFrom(uToken, msg.sender, address(this), a); MarketPlace mPlace = MarketPlace(marketPlace); - require(CErc20(mPlace.cTokenAddress(u, m)).mint(a) == 0, 'minting CToken Failed'); - require(mPlace.mintZcTokenAddingNotional(u, m, msg.sender, a), 'mint ZcToken adding Notional failed'); + + CErc20(mPlace.cTokenAddress(p, u, m)).mint(a); + + require(mPlace.mintZcTokenAddingNotional(p, u, m, msg.sender, a), 'mint ZcToken adding Notional failed'); return true; } /// @notice Allows users deposit/burn 1-1 amounts of both zcTokens and vault notional, /// in the process "combining" the two, and redeeming underlying. Calls mPlace.burnZcTokenRemovingNotional. + /// @param p Protocol being withdrawn from /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param a Amount of zcTokens being redeemed - function combineTokens(address u, uint256 m, uint256 a) external returns (bool) { + function combineTokens(uint8 p, address u, uint256 m, uint256 a) external returns (bool) { MarketPlace mPlace = MarketPlace(marketPlace); - require(mPlace.burnZcTokenRemovingNotional(u, m, msg.sender, a), 'burn ZcToken removing Notional failed'); - address cTokenAddr = mPlace.cTokenAddress(u, m); - require((CErc20(cTokenAddr).redeemUnderlying(a) == 0), "compound redemption error"); - Safe.transfer(Erc20(u), msg.sender, a); + require(mPlace.burnZcTokenRemovingNotional(p, u, m, msg.sender, a), 'burn ZcToken removing Notional failed'); + + // redeem underlying + // Compound and Rari + if (p == 1 || p == 2) { + (CErc20(mPlace.cTokenAddress(p, u, m)).redeemUnderlying(a) == 0); + } + // Yearn + else if (p == 3) { + IYearnVault(mPlace.cTokenAddress(p, u, m)).withdraw(a); + } + // Aave + else if (p == 4) { + aaveRouter.withdraw(u, a, address(this)); + } + // Euler + else if (p == 5) { + IEToken(mPlace.cTokenAddress(p, u, m)).withdraw(0, a); + } + else if (p == 6) { + IERC4626(mPlace.cTokenAddress(p, u, m)).withdraw(a, address(this), address(this)); + } + // Potential Lido integration + // else if (o.protocol == 7) { + // } + Safe.transfer(ERC20(u), msg.sender, a); return true; } /// @notice Allows zcToken holders to redeem their tokens for underlying tokens after maturity has been reached (via MarketPlace). + /// @param p Protocol which is being redeemed from /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market /// @param a Amount of zcTokens being redeemed - function redeemZcToken(address u, uint256 m, uint256 a) external returns (bool) { + function redeemZcToken(uint8 p, address u, uint256 m, uint256 a) external returns (bool) { MarketPlace mPlace = MarketPlace(marketPlace); // call marketplace to determine the amount redeemed - uint256 redeemed = mPlace.redeemZcToken(u, m, msg.sender, a); - // redeem underlying from compound - require(CErc20(mPlace.cTokenAddress(u, m)).redeemUnderlying(redeemed) == 0, 'compound redemption failed'); + uint256 redeemed = mPlace.redeemZcToken(p, u, m, msg.sender, a); + + // redeem underlying + // Compound and Rari + if (p == 1 || p == 2) { + require((CErc20(mPlace.cTokenAddress(p, u, m)).redeemUnderlying(redeemed) == 0), "compound redemption error"); + } + // Yearn + else if (p == 3) { + IYearnVault(mPlace.cTokenAddress(p, u, m)).withdraw(redeemed); + } + // Aave + else if (p == 4) { + aaveRouter.withdraw(u, redeemed, address(this)); + } + // Euler + else if (p == 5) { + IEToken(mPlace.cTokenAddress(p, u, m)).withdraw(0, redeemed); + } + else if (p == 6) { + IERC4626(mPlace.cTokenAddress(p, u, m)).withdraw(redeemed, address(this), address(this)); + } + // Potential Lido integration + // else if (o.protocol == 7) { + // } + // transfer underlying back to msg.sender - Safe.transfer(Erc20(u), msg.sender, redeemed); + Safe.transfer(ERC20(u), msg.sender, redeemed); return true; } /// @notice Allows Vault owners to redeem any currently accrued interest (via MarketPlace) + /// @param p Protocol which is being redeemed from /// @param u Underlying token address associated with the market /// @param m Maturity timestamp of the market - function redeemVaultInterest(address u, uint256 m) external returns (bool) { + function redeemVaultInterest(uint8 p, address u, uint256 m) external returns (bool) { MarketPlace mPlace = MarketPlace(marketPlace); // call marketplace to determine the amount redeemed - uint256 redeemed = mPlace.redeemVaultInterest(u, m, msg.sender); - // redeem underlying from compound - require(CErc20(mPlace.cTokenAddress(u, m)).redeemUnderlying(redeemed) == 0, 'compound redemption failed'); + uint256 redeemed = mPlace.redeemVaultInterest(p, u, m, msg.sender); + + // redeem underlying + // Compound and Rari + if (p == 1 || p == 2) { + CErc20(mPlace.cTokenAddress(p, u, m)).redeemUnderlying(redeemed); + } + // Yearn + else if (p == 3) { + IYearnVault(mPlace.cTokenAddress(p, u, m)).withdraw(redeemed); + } + // Aave + else if (p == 4) { + aaveRouter.withdraw(u, redeemed, address(this)); + } + // Euler + else if (p == 5) { + IEToken(mPlace.cTokenAddress(p, u, m)).withdraw(0, redeemed); + } + else if (p == 6) { + IERC4626(mPlace.cTokenAddress(p, u, m)).withdraw(redeemed, address(this), address(this)); + } + // transfer underlying back to msg.sender - Safe.transfer(Erc20(u), msg.sender, redeemed); + Safe.transfer(ERC20(u), msg.sender, redeemed); return true; } @@ -512,15 +707,23 @@ contract Swivel { function validOrderHash(Hash.Order calldata o, Sig.Components calldata c) internal view returns (bytes32) { bytes32 hash = Hash.order(o); - require(!cancelled[hash], 'order cancelled'); - require(o.expiry >= block.timestamp, 'order expired'); - require(o.maker == Sig.recover(Hash.message(domain, hash), c), 'invalid signature'); + if (cancelled[hash] == true) { + revert Cancelled(); + } + if (o.expiry >= block.timestamp) { + revert Expired(); + } + if (o.maker != Sig.recover(Hash.message(domain, hash), c)) { + revert Invalid(); + } return hash; } modifier authorized(address a) { - require(msg.sender == a, 'sender must be authorized'); + if (msg.sender != a) { + revert Authorized(); + } _; } -} +} \ No newline at end of file diff --git a/test/tokens/PErc20.sol b/test/tokens/PErc20.sol index b1297243..0e7dbab8 100644 --- a/test/tokens/PErc20.sol +++ b/test/tokens/PErc20.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity >=0.8.4; import "./IPErc20.sol"; diff --git a/test/vaulttracker/IAdapter.sol b/test/vaulttracker/IAdapter.sol new file mode 100644 index 00000000..d0779092 --- /dev/null +++ b/test/vaulttracker/IAdapter.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.4; + +/// @notice Tokenised representation of assets +interface IAdapter { + function exchangeRateCurrent(address cToken) external view returns(uint256); + + function protocol() external view returns(uint8); +} \ No newline at end of file diff --git a/test/vaulttracker/Interfaces.sol b/test/vaulttracker/Interfaces.sol deleted file mode 100644 index 539cac13..00000000 --- a/test/vaulttracker/Interfaces.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity 0.8.4; - -interface CErc20 { - function exchangeRateCurrent() external returns (uint256); -} diff --git a/test/vaulttracker/VaultTracker.sol b/test/vaulttracker/VaultTracker.sol index e60f2957..b651c967 100644 --- a/test/vaulttracker/VaultTracker.sol +++ b/test/vaulttracker/VaultTracker.sol @@ -1,8 +1,8 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.4; +pragma solidity >=0.8.4; -import "./Interfaces.sol"; +import "./IAdapter.sol"; contract VaultTracker { struct Vault { @@ -14,25 +14,31 @@ contract VaultTracker { mapping(address => Vault) public vaults; address public immutable admin; - address public immutable cTokenAddr; + address public immutable cToken; address public immutable swivel; + Adapter public immutable adapter; uint256 public immutable maturity; uint256 public maturityRate; + error Balance(); + error Self(); + error Unauthorized(); + /// @param m Maturity timestamp of the new market /// @param c cToken address associated with underlying for the new market /// @param s address of the deployed swivel contract - constructor(uint256 m, address c, address s) { + constructor(uint256 m, address c, address a, address s) { admin = msg.sender; maturity = m; - cTokenAddr = c; + cToken = c; + adapter = Adapter(a); swivel = s; // instantiate swivel's vault (unblocking transferNotionalFee) vaults[s] = Vault({ notional: 0, redeemable: 0, - exchangeRate: CErc20(c).exchangeRateCurrent() + exchangeRate: adapter.exchangeRateCurrent(cToken) }); } @@ -40,7 +46,7 @@ contract VaultTracker { /// @param o Address that owns a vault /// @param a Amount of notional added function addNotional(address o, uint256 a) external authorized(admin) returns (bool) { - uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); + uint256 exchangeRate = adapter.exchangeRateCurrent(cToken); Vault memory vlt = vaults[o]; @@ -76,10 +82,12 @@ contract VaultTracker { Vault memory vlt = vaults[o]; - require(vlt.notional >= a, "amount exceeds vault balance"); + if (vlt.notional < a) { + revert Balance(); + } uint256 yield; - uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); + uint256 exchangeRate = adapter.exchangeRateCurrent(cToken); // if market has matured, calculate marginal interest between the maturity rate and previous position exchange rate // otherwise, calculate marginal exchange rate between current and previous exchange rate. @@ -109,7 +117,7 @@ contract VaultTracker { uint256 redeemable = vlt.redeemable; uint256 yield; - uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); + uint256 exchangeRate = adapter.exchangeRateCurrent(cToken); // if market has matured, calculate marginal interest between the maturity rate and previous position exchange rate // otherwise, calculate marginal exchange rate between current and previous exchange rate. @@ -143,15 +151,19 @@ contract VaultTracker { /// @param t Recipient of the amount /// @param a Amount to transfer function transferNotionalFrom(address f, address t, uint256 a) external authorized(admin) returns (bool) { - require(f != t, 'cannot transfer notional to self'); + if (f == t){ + revert Self(); + } Vault memory from = vaults[f]; Vault memory to = vaults[t]; - require(from.notional >= a, "amount exceeds available balance"); + if (from.notional < a) { + revert Balance(); + } uint256 yield; - uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); + uint256 exchangeRate = adapter.exchangeRateCurrent(cToken); // if market has matured, calculate marginal interest between the maturity rate and previous position exchange rate // otherwise, calculate marginal exchange rate between current and previous exchange rate. @@ -205,7 +217,7 @@ contract VaultTracker { // remove notional from its owner oVault.notional -= a; - uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); + uint256 exchangeRate = adapter.exchangeRateCurrent(cToken); uint256 yield; // check if exchangeRate has been stored already this block. If not, calculate marginal interest + store exchangeRate @@ -239,7 +251,9 @@ contract VaultTracker { } modifier authorized(address a) { - require(msg.sender == a, 'sender must be authorized'); + if (msg.sender != a) { + revert Unauthorized(); + } _; } -} +} \ No newline at end of file