Full code for lender
Lender that is compliant to ERC 3156 and 4626
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "lib/yield-utils-v2/contracts/mocks/ERC20Mock.sol";
//import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
import "lib/yield-utils-v2/contracts/token/IERC20.sol";
import "src/IERC3156FlashLender.sol";
import "src/IERC3156FlashBorrower.sol";
/**
@title Flash Loan Server
@author Calnix
@dev Contract allows users to exchange a pre-specified ERC20 token for some other wrapped ERC20 tokens.
@notice Wrapped tokens will be burned, when user withdraws their deposited tokens.
*/
contract FlashLoanVault is ERC20Mock, IERC3156FlashLender {
///@dev The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan"
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
///@notice Fee is a constant 0.1%
///@dev 1000 == 0.1% (1 == 0.0001 %)
uint256 public constant fee = 1000;
///@dev mapping of supported addresses by Flash loan provider
mapping(address => bool) public supportedTokens;
///@dev ERC20 interface specifying token contract functions
IERC20 public immutable underlying;
///@notice Creates a new wrapper token for a specified token
///@dev Token will have 18 decimal places as ERC20Mock inherits from ERC20Permit
///@param underlying_ Address of underlying ERC20 token (e.g. DAI)
///@param tokenName Name of FractionalWrapper tokens
///@param tokenSymbol Symbol of FractionalWrapper tokens (e.g. yvDAI)
constructor(IERC20 underlying_, string memory tokenName, string memory tokenSymbol) ERC20Mock(tokenName, tokenSymbol) {
underlying = underlying_;
supportedTokens[address(underlying_)] = true;
}
/*/////////////////////////////////////////////////////////////*/
/* FLASHLOAN */
/*/////////////////////////////////////////////////////////////*/
/** Note: The flashLoan function MUST include a callback to the onFlashLoan function in a IERC3156FlashBorrower contract.
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @param data A data parameter to be passed on to the `receiver` for any custom use.
*/
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external returns(bool) {
require(supportedTokens[token], "FlashLender: Unsupported currency");
uint256 _fee = _flashFee(token, amount);
require(IERC20(token).transfer(address(receiver), amount), "FlashLender: Transfer failed");
require(receiver.onFlashLoan(msg.sender, token, amount, _fee, data) == CALLBACK_SUCCESS, "IERC3156: Callback failed");
require(IERC20(token).transferFrom(address(receiver), address(this), amount + _fee), "FlashLender: Repay failed");
return true;
}
//Note: The flashFee function MUST return the fee charged for a loan of amount token. If the token is not supported flashFee MUST revert.
///@dev The fee to be charged for a given loan.
///@param token The loan currency.
///@param amount The amount of tokens lent.
///@return The amount of `token` to be charged for the loan, on top of the returned principal.
function flashFee(address token, uint256 amount) external view returns (uint256) {
require(supportedTokens[token], "FlashLender: Unsupported currency");
return _flashFee(token, amount);
}
///@dev The fee to be charged for a given loan. Internal function with no checks.
///@param token The loan currency.
///@param amount The amount of tokens lent.
///@return The amount of `token` to be charged for the loan, on top of the returned principal.
///Note: division of 10000 is for rebasing fee from its integer to percentage form.
function _flashFee(address token, uint256 amount) internal view returns (uint256) {
return amount * fee / 10000;
}
///Note: The maxFlashLoan function MUST return the maximum loan possible for token. If a token is not currently supported maxFlashLoan MUST return 0, instead of reverting.
///@dev The amount of currency available to be lended.
///@param token The loan currency.
///@return The amount of `token` that can be borrowed -> max of DAI deposited.
function maxFlashLoan(address token) external view returns (uint256) {
return supportedTokens[token] ? IERC20(token).balanceOf(address(this)) : 0;
}
/*/////////////////////////////////////////////////////////////*/
/* FRACTIONAL WRAPPER */
/*/////////////////////////////////////////////////////////////*/
///@dev Exchange rate at inception: 1 underlying (DAI) == 1 share (yvDAI) | Ex-rate: 1 DAI/yvDAI = 0.5 -> 1 DAI gets you 1/2 yvDAI
//uint exRate = 1e27;
/// @notice Emit event when ERC20 tokens are deposited into Fractionalized Wrapper
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
/// @notice Emit event when ERC20 tokens are withdrawn from Fractionalized Wrapper
event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);
/// @notice User to deposit underlying tokens for FractionalWrapper tokens
/// @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens
/// @param assets The amount of underlying tokens to deposit
/// @param receiver Address of receiver of Fractional Wrapper shares
function deposit(uint256 assets, address receiver) external returns(uint256 shares) {
receiver = msg.sender;
shares = convertToShares(assets);
//transfer DAI from user
bool success = underlying.transferFrom(receiver, address(this), assets);
require(success, "Deposit failed!");
//mint yvDAI to user
bool sent = _mint(receiver, shares);
require(sent, "Mint failed!");
emit Deposit(msg.sender, receiver, assets, shares);
}
/// @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver; based on the exchange rate.
/// @param assets The amount of underlying tokens to withdraw
/// @param receiver Address of receiver of underlying tokens - DAI
/// @param owner Address of owner of Fractional Wrapper shares - yvDAI
function withdraw(uint256 assets, address receiver, address owner) external returns(uint256 shares) {
shares = convertToShares(assets);
// MUST support a withdraw flow where the shares are burned from owner directly where owner is msg.sender,
// OR msg.sender has ERC-20 approval over the shares of owner
if(msg.sender != owner){
uint allowedShares = _allowance[owner][receiver] ;
require(allowedShares >= shares, "Allowance exceeded!");
_allowance[owner][receiver] = allowedShares - shares;
}
//burn wrapped tokens(shares) -> yvDAI
burn(owner, shares);
//transfer assets
bool success = underlying.transfer(receiver, assets);
require(success, "Transfer failed!");
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
/// @dev Burns shares from owner and sends assets of underlying tokens to receiver; based on the exchange rate.
/// @param shares The amount of wrapped tokens to redeem for underlying tokens (assets)
/// @param receiver Address of receiver of underlying tokens - DAI
/// @param owner Address of owner of Fractional Wrapper shares - yvDAI
function redeem(uint256 shares, address receiver, address owner) external returns(uint256 assets) {
assets = convertToAssets(shares);
// MUST support a redeem flow where the shares are burned from owner directly where owner is msg.sender,
// OR msg.sender has ERC-20 approval over the shares of owner
if(msg.sender != owner){
uint allowedShares = _allowance[owner][receiver] ;
require(allowedShares >= shares, "Allowance exceeded!");
_allowance[owner][receiver] = allowedShares - shares;
}
//burn wrapped tokens(shares) -> yvDAI
burn(owner, shares);
//transfer assets
bool success = underlying.transfer(receiver, assets);
require(success, "Transfer failed!");
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
/// @notice Returns the unit 'exchange rate'; assuming 1 unit of underlying was deposited, how much shares would be received
/// @dev wrapperMinted = (underlyingDeposited,(1) * wrapperSupply) / underlyingInWrapper
/// Note: Exchange rate is floating, it's dynamic based on capital in/out-flows
/// Note: _totalSupply returns a value extended over decimal precision, in this case 18 dp. hence the scaling before divsion.
function getExchangeRate() internal view returns(uint256) {
uint256 sharesMinted;
return _totalSupply == 0 ? sharesMinted = 1e18 : sharesMinted = (_totalSupply * 1e18) / underlying.balanceOf(address(this));
// if(_totalSupply == 0) {
// return sharesMinted = 1;
// }
// return sharesMinted = (_totalSupply) / underlying.balanceOf(address(this));
}
/// @notice Calculate how much yvDAI user should get based on flaoting exchange rate
/// @dev getExchangeRate() returns shares minted per unit of underlying asset deposited; at present moment.
/// @param assets Amount of underlying tokens (assets) to be converted to wrapped tokens (shares)
function convertToShares(uint256 assets) internal view returns(uint256 shares){
return assets * getExchangeRate() / 1e18;
}
/// @notice Calculate how much DAI user should get based on floating exchange rate
/// @dev getExchangeRate() returns shares minted per unit of underlying asset deposited; at present moment.
/// @param shares Amount of wrapped tokens (shares) to be converted to underlying tokens (assets)
function convertToAssets(uint256 shares) internal view returns(uint256 assets){
return shares * 1e18 / getExchangeRate();
}
/// @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
/// @dev Any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage
/// @param assets Amount of assets to deposit
function previewDeposit(uint256 assets) external view returns(uint256 shares) {
return convertToShares(assets);
}
/// @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.
/// @dev Any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage
/// @param assets Amount of assets to withdraw
function previewWithdraw(uint256 assets) external view returns(uint256 shares) {
shares = convertToShares(assets);
}
///Note: newly added
///@notice Function returns contract address of underlying token utilized by the vault (e.g. DAI)
///@dev MUST be an ERC-20 token contract
function asset() external view returns(address assetTokenAddress){
assetTokenAddress = address(underlying);
}
///@notice Returns total amount of the underlying asset (e.g. DAI) that is “managed” by Vault.
///@dev SHOULD include any compounding that occurs from yield, account for any fees that are charged against assets in the Vault
function totalAssets() external view returns(uint256 totalManagedAssets) {
totalManagedAssets = underlying.balanceOf(address(this));
}
///@notice Maximum amount of the underlying asset that can be deposited into the Vault by the receiver, through a deposit call.
///@dev Return the maximum amount of assets that would be allowed for deposit by receiver, and not cause revert
///Note: In this Vault implementation there are no restrictions in minting supply, therefore, no deposit restrictions.
///Note: Consequently, maxAssets = type(uint256).max for all users => therefore we drop the 'receiver' param as specified in EIP4626
///Note: To allow for this to be overwritten as per EIP4626, function is set to virtual
function maxDeposit() external view virtual returns (uint256 maxAssets) {
maxAssets = type(uint256).max;
}
///@notice Maximum amount of shares that can be minted from the Vault for the receiver, through a mint call.
///@dev MUST return the maximum amount of shares mint would allow to be deposited to receiver+
///Note: In this Vault implementation there are no restrictions in minting supply
///Note: Consequently, maxShares = type(uint256).max for all users => therefore we drop the 'receiver' param as specified in EIP4626
///Note: To allow for this to be overwritten as per EIP4626, function is set to virtual
function maxMint() external view virtual returns(uint maxShares) {
maxShares = type(uint256).max;
}
///@notice Allows a user to simulate the effects of their mint at the current block, given current on-chain conditions.
///@dev Return as close to and no fewer than the exact amount of assets that would be deposited in a mint call in the same transaction
///@param shares Amount of shares to be minted
function previewMint(uint256 shares) external view returns (uint256 assets) {
assets = convertToAssets(shares);
}
///@notice Mints Vault shares to receiver based on the deposited amount of underlying tokens
///@param shares Amount of shares to be minted
///@param receiver Address of receiver
function mint(uint256 shares, address receiver) external returns (uint256 assets) {
assets = convertToAssets(shares);
bool sent = underlying.transferFrom(msg.sender, address(this), assets);
require(sent, "Transfer failed!");
bool success = _mint(receiver, shares);
require(success, "Mint failed!");
emit Deposit(msg.sender, receiver, assets, shares);
}
///@notice Maximum amount of the underlying asset that can be withdrawn from the owner balance in the Vault, through a withdraw call.
///@param owner Address of owner of assets
function maxWithdraw(address owner) external view returns(uint256 maxAssets) {
maxAssets = convertToAssets(_balanceOf[owner]);
}
///@notice Maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, through a redeem call.
///@param owner Address of owner of assets
function maxRedeem(address owner) external view returns(uint256 maxShares) {
maxShares = _balanceOf[owner];
}
///@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.
///@param shares Amount of shares to be redeemed
function previewRedeem(uint256 shares) external view returns(uint256 assets) {
assets = convertToAssets(shares);
}
}
Last updated