// SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.11; // External references import { ERC20 } from "@rari-capital/solmate/src/tokens/ERC20.sol"; import { SafeTransferLib } from "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; import { FixedMath } from "./external/FixedMath.sol"; import { BalancerVault, IAsset } from "./external/balancer/Vault.sol"; import { BalancerPool } from "./external/balancer/Pool.sol"; // Internal references import { Errors } from "@sense-finance/v1-utils/src/libs/Errors.sol"; import { Levels } from "@sense-finance/v1-utils/src/libs/Levels.sol"; import { Trust } from "@sense-finance/v1-utils/src/Trust.sol"; import { BaseAdapter as Adapter } from "./adapters/BaseAdapter.sol"; import { BaseFactory as AdapterFactory } from "./adapters/BaseFactory.sol"; import { Divider } from "./Divider.sol"; import { PoolManager } from "@sense-finance/v1-fuse/src/PoolManager.sol"; interface SpaceFactoryLike { function create(address, uint256) external returns (address); function pools(address adapter, uint256 maturity) external view returns (address); } /// @title Periphery contract Periphery is Trust { using FixedMath for uint256; using SafeTransferLib for ERC20; using Levels for uint256; /* ========== PUBLIC CONSTANTS ========== */ /// @notice Lower bound on the amount of Claim tokens one can swap in for Target uint256 public constant MIN_YT_SWAP_IN = 0.000001e18; /// @notice Acceptable error when estimating the tokens resulting from a specific swap uint256 public constant PRICE_ESTIMATE_ACCEPTABLE_ERROR = 0.00000001e18; /* ========== PUBLIC IMMUTABLES ========== */ /// @notice Sense core Divider address Divider public immutable divider; /// @notice Sense money market manager PoolManager public immutable poolManager; /// @notice Sense Space Factory SpaceFactoryLike public immutable spaceFactory; /// @notice Balancer Vault BalancerVault public immutable balancerVault; /* ========== PUBLIC MUTABLE STORAGE ========== */ /// @notice adapter factories -> is supported mapping(address => bool) public factories; /// @notice adapter -> bool mapping(address => bool) public verified; /* ========== DATA STRUCTURES ========== */ struct PoolLiquidity { ERC20[] tokens; uint256[] amounts; } constructor( address _divider, address _poolManager, address _spaceFactory, address _balancerVault ) Trust(msg.sender) { divider = Divider(_divider); poolManager = PoolManager(_poolManager); spaceFactory = SpaceFactoryLike(_spaceFactory); balancerVault = BalancerVault(_balancerVault); } /* ========== SERIES / ADAPTER MANAGEMENT ========== */ /// @notice Sponsor a new Series /// @dev Calls divider to initalise a new series /// @param adapter Adapter to associate with the Series /// @param maturity Maturity date for the Series, in units of unix time /// @param withPool Whether to deploy a Space pool or not (only works for unverified adapters) function sponsorSeries( address adapter, uint256 maturity, bool withPool ) external returns (address pt, address yt) { (, address stake, uint256 stakeSize) = Adapter(adapter).getStakeAndTarget(); // Transfer stakeSize from sponsor into this contract ERC20(stake).safeTransferFrom(msg.sender, address(this), stakeSize); // Approve divider to withdraw stake assets ERC20(stake).approve(address(divider), stakeSize); (pt, yt) = divider.initSeries(adapter, maturity, msg.sender); // Space pool is always created for verified adapters whilst is optional for unverified ones. // Automatically queueing series is only for verified adapters if (verified[adapter]) { poolManager.queueSeries(adapter, maturity, spaceFactory.create(adapter, maturity)); } else { if (withPool) { spaceFactory.create(adapter, maturity); } } emit SeriesSponsored(adapter, maturity, msg.sender); } /// @notice Deploy and onboard a verified Adapter /// @dev Deploys a new verified Adapter via a whitelisted Adapter Factory /// @param f Factory to use /// @param target Target to onboard function deployAdapter(address f, address target) external returns (address adapter) { if (!factories[f]) revert Errors.FactoryNotSupported(); if (!AdapterFactory(f).exists(target)) revert Errors.TargetNotSupported(); adapter = AdapterFactory(f).deployAdapter(target); emit AdapterDeployed(adapter); verifyAdapter(adapter, true); onboardAdapter(adapter); } /// @dev Onboards an unverifeid Adapter /// @param adapter Adapter to onboard function onboardAdapter(address adapter) public { ERC20 target = ERC20(Adapter(adapter).target()); target.approve(address(divider), type(uint256).max); target.approve(address(adapter), type(uint256).max); divider.addAdapter(adapter); emit AdapterOnboarded(adapter); } /* ========== LIQUIDITY UTILS ========== */ /// @notice Swap Target to Principal Tokens of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param tBal Balance of Target to sell /// @param minAccepted Min accepted amount of PT /// @return ptBal amount of PT received function swapTargetForPTs( address adapter, uint256 maturity, uint256 tBal, uint256 minAccepted ) external returns (uint256 ptBal) { ERC20(Adapter(adapter).target()).safeTransferFrom(msg.sender, address(this), tBal); // pull target return _swapTargetForPTs(adapter, maturity, tBal, minAccepted); } /// @notice Swap Underlying to Principal Tokens of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param uBal Balance of Underlying to sell /// @param minAccepted Min accepted amount of PT /// @return ptBal amount of PT received function swapUnderlyingForPTs( address adapter, uint256 maturity, uint256 uBal, uint256 minAccepted ) external returns (uint256 ptBal) { ERC20 underlying = ERC20(Adapter(adapter).underlying()); underlying.safeTransferFrom(msg.sender, address(this), uBal); // pull underlying underlying.approve(adapter, uBal); // approve adapter to pull uBal uint256 tBal = Adapter(adapter).wrapUnderlying(uBal); // wrap underlying into target ptBal = _swapTargetForPTs(adapter, maturity, tBal, minAccepted); } /// @notice Swap Target to Yield Tokens of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param tBal Balance of Target to sell /// @param minAccepted Min accepted amount of YT /// @return ytBal amount of YT received function swapTargetForYTs( address adapter, uint256 maturity, uint256 tBal, uint256 minAccepted ) external returns (uint256 ytBal) { ERC20(Adapter(adapter).target()).safeTransferFrom(msg.sender, address(this), tBal); ytBal = _swapTargetForYTs(adapter, maturity, tBal, minAccepted); } /// @notice Swap Underlying to Yield of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param uBal Balance of Underlying to sell /// @param minAccepted Min accepted amount of YT /// @return ytBal amount of YT received function swapUnderlyingForYTs( address adapter, uint256 maturity, uint256 uBal, uint256 minAccepted ) external returns (uint256 ytBal) { ERC20 underlying = ERC20(Adapter(adapter).underlying()); underlying.safeTransferFrom(msg.sender, address(this), uBal); // pull underlying underlying.approve(adapter, uBal); // approve adapter to pull underlying uint256 tBal = Adapter(adapter).wrapUnderlying(uBal); // wrap underlying into target ytBal = _swapTargetForYTs(adapter, maturity, tBal, minAccepted); } /// @notice Swap Principal Tokens for Target of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param ptBal Balance of PT to sell /// @param minAccepted Min accepted amount of Target function swapPTsForTarget( address adapter, uint256 maturity, uint256 ptBal, uint256 minAccepted ) external returns (uint256 tBal) { tBal = _swapPTsForTarget(adapter, maturity, ptBal, minAccepted); // swap Principal Tokens for target ERC20(Adapter(adapter).target()).safeTransfer(msg.sender, tBal); // transfer target to msg.sender } /// @notice Swap Principal Tokens for Underlying of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param ptBal Balance of PT to sell /// @param minAccepted Min accepted amount of Target function swapPTsForUnderlying( address adapter, uint256 maturity, uint256 ptBal, uint256 minAccepted ) external returns (uint256 uBal) { uint256 tBal = _swapPTsForTarget(adapter, maturity, ptBal, minAccepted); // swap Principal Tokens for target ERC20(Adapter(adapter).target()).approve(adapter, tBal); // approve adapter to pull target uBal = Adapter(adapter).unwrapTarget(tBal); // unwrap target into underlying ERC20(Adapter(adapter).underlying()).safeTransfer(msg.sender, uBal); // transfer underlying to msg.sender } /// @notice Swap YT for Target of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param ytBal Balance of Yield Tokens to swap function swapYTsForTarget( address adapter, uint256 maturity, uint256 ytBal ) external returns (uint256 tBal) { tBal = _swapYTsForTarget(msg.sender, adapter, maturity, ytBal); ERC20(Adapter(adapter).target()).safeTransfer(msg.sender, tBal); } /// @notice Swap YT for Underlying of a particular series /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param ytBal Balance of Yield Tokens to swap function swapYTsForUnderlying( address adapter, uint256 maturity, uint256 ytBal ) external returns (uint256 uBal) { uint256 tBal = _swapYTsForTarget(msg.sender, adapter, maturity, ytBal); uBal = Adapter(adapter).unwrapTarget(tBal); ERC20(Adapter(adapter).underlying()).safeTransfer(msg.sender, uBal); } /// @notice Adds liquidity, providing target /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param tBal Balance of Target to provide /// @param mode 0 = issues and sell YT, 1 = issue and hold YT /// @dev see return description of _addLiquidity function addLiquidityFromTarget( address adapter, uint256 maturity, uint256 tBal, uint8 mode ) external returns ( uint256 tAmount, uint256 issued, uint256 lpShares ) { ERC20(Adapter(adapter).target()).safeTransferFrom(msg.sender, address(this), tBal); (tAmount, issued, lpShares) = _addLiquidity(adapter, maturity, tBal, mode); } /// @notice Adds liquidity providing underlying /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param uBal Balance of Underlying to provide /// @param mode 0 = issues and sell YT, 1 = issue and hold YT /// @dev see return description of _addLiquidity function addLiquidityFromUnderlying( address adapter, uint256 maturity, uint256 uBal, uint8 mode ) external returns ( uint256 tAmount, uint256 issued, uint256 lpShares ) { ERC20 underlying = ERC20(Adapter(adapter).underlying()); underlying.safeTransferFrom(msg.sender, address(this), uBal); underlying.approve(adapter, uBal); // Wrap Underlying into Target uint256 tBal = Adapter(adapter).wrapUnderlying(uBal); (tAmount, issued, lpShares) = _addLiquidity(adapter, maturity, tBal, mode); } /// @notice Removes liquidity providing an amount of LP tokens and returns target /// @dev More info on `minAmountsOut`: https://github.com/balancer-labs/docs-developers/blob/main/resources/joins-and-exits/pool-exits.md#minamountsout /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param lpBal Balance of LP tokens to provide /// @param minAmountsOut minimum accepted amounts of PTs and Target given the amount of LP shares provided /// @param minAccepted only used when removing liquidity on/after maturity and its the min accepted when swapping Principal Tokens to underlying /// @return tBal amount of target received and ptBal amount of Principal Tokens (in case it's called after maturity and redeem is restricted) function removeLiquidityToTarget( address adapter, uint256 maturity, uint256 lpBal, uint256[] memory minAmountsOut, uint256 minAccepted ) external returns (uint256 tBal, uint256 ptBal) { (tBal, ptBal) = _removeLiquidity(adapter, maturity, lpBal, minAmountsOut, minAccepted); ERC20(Adapter(adapter).target()).safeTransfer(msg.sender, tBal); // Send Target back to the User } /// @notice Removes liquidity providing an amount of LP tokens and returns underlying /// @dev More info on `minAmountsOut`: https://github.com/balancer-labs/docs-developers/blob/main/resources/joins-and-exits/pool-exits.md#minamountsout /// @param adapter Adapter address for the Series /// @param maturity Maturity date for the Series /// @param lpBal Balance of LP tokens to provide /// @param minAmountsOut minimum accepted amounts of PTs and Target given the amount of LP shares provided /// @param minAccepted only used when removing liquidity on/after maturity and its the min accepted when swapping Principal Tokens to underlying /// @return uBal amount of underlying received and ptBal Principal Tokens (in case it's called after maturity and redeem is restricted) function removeLiquidityToUnderlying( address adapter, uint256 maturity, uint256 lpBal, uint256[] memory minAmountsOut, uint256 minAccepted ) external returns (uint256 uBal, uint256 ptBal) { uint256 tBal; (tBal, ptBal) = _removeLiquidity(adapter, maturity, lpBal, minAmountsOut, minAccepted); ERC20(Adapter(adapter).target()).approve(adapter, tBal); ERC20(Adapter(adapter).underlying()).safeTransfer(msg.sender, uBal = Adapter(adapter).unwrapTarget(tBal)); // Send Underlying back to the User } /// @notice Migrates liquidity position from one series to another /// @dev More info on `minAmountsOut`: https://github.com/balancer-labs/docs-developers/blob/main/resources/joins-and-exits/pool-exits.md#minamountsout /// @param srcAdapter Adapter address for the source Series /// @param dstAdapter Adapter address for the destination Series /// @param srcMaturity Maturity date for the source Series /// @param dstMaturity Maturity date for the destination Series /// @param lpBal Balance of LP tokens to provide /// @param minAmountsOut Minimum accepted amounts of PTs and Target given the amount of LP shares provided /// @param minAccepted Min accepted amount of target when swapping Principal Tokens (only used when removing liquidity on/after maturity) /// @param mode 0 = issues and sell YT, 1 = issue and hold YT /// @dev see return description of _addLiquidity. It also returns amount of Principal Tokens (in case it's called after maturity and redeem is restricted) function migrateLiquidity( address srcAdapter, address dstAdapter, uint256 srcMaturity, uint256 dstMaturity, uint256 lpBal, uint256[] memory minAmountsOut, uint256 minAccepted, uint8 mode ) external returns ( uint256 tAmount, uint256 issued, uint256 lpShares, uint256 ptBal ) { if (Adapter(srcAdapter).target() != Adapter(dstAdapter).target()) revert Errors.TargetMismatch(); uint256 tBal; (tBal, ptBal) = _removeLiquidity(srcAdapter, srcMaturity, lpBal, minAmountsOut, minAccepted); (tAmount, issued, lpShares) = _addLiquidity(dstAdapter, dstMaturity, tBal, mode); } /* ========== ADMIN ========== */ /// @notice Enable or disable a factory /// @param f Factory's address /// @param isOn Flag setting this factory to enabled or disabled function setFactory(address f, bool isOn) external requiresTrust { if (factories[f] == isOn) revert Errors.ExistingValue(); factories[f] = isOn; emit FactoryChanged(f, isOn); } /// @dev Verifies an Adapter and optionally adds the Target to the money market /// @param adapter Adapter to verify function verifyAdapter(address adapter, bool addToPool) public requiresTrust { verified[adapter] = true; if (addToPool) poolManager.addTarget(Adapter(adapter).target(), adapter); emit AdapterVerified(adapter); } /* ========== INTERNAL UTILS ========== */ function _swap( address assetIn, address assetOut, uint256 amountIn, bytes32 poolId, uint256 minAccepted ) internal returns (uint256 amountOut) { // approve vault to spend tokenIn ERC20(assetIn).approve(address(balancerVault), amountIn); BalancerVault.SingleSwap memory request = BalancerVault.SingleSwap({ poolId: poolId, kind: BalancerVault.SwapKind.GIVEN_IN, assetIn: IAsset(assetIn), assetOut: IAsset(assetOut), amount: amountIn, userData: hex"" }); BalancerVault.FundManagement memory funds = BalancerVault.FundManagement({ sender: address(this), fromInternalBalance: false, recipient: payable(address(this)), toInternalBalance: false }); amountOut = balancerVault.swap(request, funds, minAccepted, type(uint256).max); emit Swapped(msg.sender, poolId, assetIn, assetOut, amountIn, amountOut, msg.sig); } function _swapPTsForTarget( address adapter, uint256 maturity, uint256 ptBal, uint256 minAccepted ) internal returns (uint256 tBal) { address principalToken = divider.pt(adapter, maturity); ERC20(principalToken).safeTransferFrom(msg.sender, address(this), ptBal); // pull principal BalancerPool pool = BalancerPool(spaceFactory.pools(adapter, maturity)); tBal = _swap(principalToken, Adapter(adapter).target(), ptBal, pool.getPoolId(), minAccepted); // swap Principal Tokens for underlying } function _swapTargetForPTs( address adapter, uint256 maturity, uint256 tBal, uint256 minAccepted ) internal returns (uint256 ptBal) { address principalToken = divider.pt(adapter, maturity); BalancerPool pool = BalancerPool(spaceFactory.pools(adapter, maturity)); ptBal = _swap(Adapter(adapter).target(), principalToken, tBal, pool.getPoolId(), minAccepted); // swap target for Principal Tokens ERC20(principalToken).safeTransfer(msg.sender, ptBal); // transfer bought principal to user } function _swapTargetForYTs( address adapter, uint256 maturity, uint256 tBal, uint256 minAccepted ) internal returns (uint256 issued) { BalancerPool pool = BalancerPool(spaceFactory.pools(adapter, maturity)); // issue pts and yts & swap pts for target issued = divider.issue(adapter, maturity, tBal); tBal = _swap(divider.pt(adapter, maturity), Adapter(adapter).target(), issued, pool.getPoolId(), minAccepted); // transfer yts & target to user ERC20(Adapter(adapter).target()).safeTransfer(msg.sender, tBal); ERC20(divider.yt(adapter, maturity)).safeTransfer(msg.sender, issued); } function _swapYTsForTarget( address sender, address adapter, uint256 maturity, uint256 ytBal ) internal returns (uint256 tBal) { address yt = divider.yt(adapter, maturity); // Because there's some margin of error in the pricing functions here, smaller // swaps will be unreliable. Tokens with more than 18 decimals are not supported. if (ytBal * 10**(18 - ERC20(yt).decimals()) <= MIN_YT_SWAP_IN) revert Errors.SwapTooSmall(); BalancerPool pool = BalancerPool(spaceFactory.pools(adapter, maturity)); // Transfer YTs into this contract if needed if (sender != address(this)) ERC20(yt).safeTransferFrom(msg.sender, address(this), ytBal); // Calculate target to borrow by calling AMM bytes32 poolId = pool.getPoolId(); (uint256 pti, uint256 targeti) = pool.getIndices(); (ERC20[] memory tokens, uint256[] memory balances, ) = balancerVault.getPoolTokens(poolId); // Determine how much Target we'll need in to get `ytBal` balance of PT out // (space doesn't directly use of the fields from `SwapRequest` beyond `poolId`, so the values after are placeholders) uint256 targetToBorrow = BalancerPool(pool).onSwap( BalancerPool.SwapRequest({ kind: BalancerVault.SwapKind.GIVEN_OUT, tokenIn: tokens[targeti], tokenOut: tokens[pti], amount: ytBal, poolId: poolId, lastChangeBlock: 0, from: address(0), to: address(0), userData: "" }), balances[targeti], balances[pti] ); // Flash borrow target (following actions in `onFlashLoan`) tBal = _flashBorrowAndSwap("0x", adapter, maturity, ytBal, targetToBorrow); } /// @return tAmount if mode = 0, target received from selling YTs, otherwise, returns 0 /// @return issued returns amount of YTs issued (and received) except first provision which returns 0 /// @return lpShares Space LP shares received given the liquidity added function _addLiquidity( address adapter, uint256 maturity, uint256 tBal, uint8 mode ) internal returns ( uint256 tAmount, uint256 issued, uint256 lpShares ) { // (1) compute target, issue Ps & Yts & add liquidity to space (issued, lpShares) = _computeIssueAddLiq(adapter, maturity, tBal); if (issued > 0) { // issue = 0 means that we are on the first pool provision or that the pt:target ratio is 0:target if (mode == 0) { // (2) Sell YTs tAmount = _swapYTsForTarget(address(this), adapter, maturity, issued); // (3) Send remaining Target back to the User ERC20(Adapter(adapter).target()).safeTransfer(msg.sender, tAmount); } else { // (4) Send YTs back to the User ERC20(divider.yt(adapter, maturity)).safeTransfer(msg.sender, issued); } } } /// @dev Calculates amount of Principal Tokens in target terms (see description on `_computeTarget`) then issues /// PTs and YTs with the calculated amount and finally adds liquidity to space with the PTs issued /// and the diff between the target initially passed and the calculated amount function _computeIssueAddLiq( address adapter, uint256 maturity, uint256 tBal ) internal returns (uint256 issued, uint256 lpShares) { BalancerPool pool = BalancerPool(spaceFactory.pools(adapter, maturity)); // Compute target (ERC20[] memory tokens, uint256[] memory balances, ) = balancerVault.getPoolTokens(pool.getPoolId()); (uint256 pti, uint256 targeti) = pool.getIndices(); // Ensure we have the right token Indices // We do not add Principal Token liquidity if it haven't been initialized yet bool ptInitialized = balances[pti] != 0; uint256 ptBalInTarget = ptInitialized ? _computeTarget(adapter, balances[pti], balances[targeti], tBal) : 0; // Issue PT & YT (skip if first pool provision) issued = ptBalInTarget > 0 ? divider.issue(adapter, maturity, ptBalInTarget) : 0; // Add liquidity to Space & send the LP Shares to recipient uint256[] memory amounts = new uint256[](2); amounts[targeti] = tBal - ptBalInTarget; amounts[pti] = issued; lpShares = _addLiquidityToSpace(pool, PoolLiquidity(tokens, amounts)); } /// @dev Based on pt:target ratio from current pool reserves and tBal passed /// calculates amount of tBal needed so as to issue PTs that would keep the ratio function _computeTarget( address adapter, uint256 ptiBal, uint256 targetiBal, uint256 tBal ) internal returns (uint256 tBalForIssuance) { uint256 tBase = 10**ERC20(Adapter(adapter).target()).decimals(); uint256 ifee = Adapter(adapter).ifee(); return tBal.fmul( ptiBal.fdiv(Adapter(adapter).scale().fmul(FixedMath.WAD - ifee).fmul(targetiBal) + ptiBal, tBase), tBase ); } function _removeLiquidity( address adapter, uint256 maturity, uint256 lpBal, uint256[] memory minAmountsOut, uint256 minAccepted ) internal returns (uint256 tBal, uint256 ptBal) { address target = Adapter(adapter).target(); address pt = divider.pt(adapter, maturity); BalancerPool pool = BalancerPool(spaceFactory.pools(adapter, maturity)); bytes32 poolId = pool.getPoolId(); // (0) Pull LP tokens from sender ERC20(address(pool)).safeTransferFrom(msg.sender, address(this), lpBal); // (1) Remove liquidity from Space uint256 _ptBal; (tBal, _ptBal) = _removeLiquidityFromSpace(poolId, pt, target, minAmountsOut, lpBal); // If the series has matured if (divider.mscale(adapter, maturity) > 0) { // Some adapters restrict redemption if (uint256(Adapter(adapter).level()).redeemRestricted()) { ERC20(pt).safeTransfer(msg.sender, _ptBal); ptBal = _ptBal; } else { // (2) Redeem Principal Tokens for Target tBal += divider.redeem(adapter, maturity, _ptBal); } } else { // (2) Sell Principal Tokens for Target tBal += _swap(pt, target, _ptBal, poolId, minAccepted); } } /// @notice Initiates a flash loan of Target, swaps target amount to PTs and combines /// @param adapter adapter /// @param maturity maturity /// @param ytBalIn YT amount the user has sent in /// @param amount target amount to borrow /// @return tBal amount of Target obtained from a sale of YTs function _flashBorrowAndSwap( bytes memory data, address adapter, uint256 maturity, uint256 ytBalIn, uint256 amount ) internal returns (uint256 tBal) { ERC20 target = ERC20(Adapter(adapter).target()); uint256 _allowance = target.allowance(address(this), address(adapter)); if (_allowance < amount) target.approve(address(adapter), type(uint256).max); bool result; (result, tBal) = Adapter(adapter).flashLoan(data, address(this), adapter, maturity, ytBalIn, amount); if (!result) revert Errors.FlashBorrowFailed(); } /// @dev inspired by the ERC-3156 Flash loan callback function onFlashLoan( bytes calldata, address initiator, address adapter, uint256 maturity, uint256 ytBalIn, uint256 amount ) external returns (bytes32 id, uint256 tBalNet) { if (msg.sender != address(adapter)) revert Errors.FlashUntrustedBorrower(); if (initiator != address(this)) revert Errors.FlashUntrustedLoanInitiator(); address yt = divider.yt(adapter, maturity); BalancerPool pool = BalancerPool(spaceFactory.pools(adapter, maturity)); // Because Space utilizes power ofs liberally in its invariant, there is some error // in the amountIn we estimated that we'd need in `_swapYTsForTarget` to get a `ptBal` out // that matched our Yield Token balance. Tokens with more than 18 decimals are not supported. uint256 acceptableError = ERC20(yt).decimals() < 9 ? 1 : PRICE_ESTIMATE_ACCEPTABLE_ERROR / 10**(18 - ERC20(yt).decimals()); // Swap Target for PTs uint256 ptBal = _swap( Adapter(adapter).target(), divider.pt(adapter, maturity), amount, pool.getPoolId(), ytBalIn - acceptableError ); // We take the lowest of the two balances, as long as they're within a margin of acceptable error. if (ptBal >= ytBalIn + acceptableError && ptBal <= ytBalIn - acceptableError) revert Errors.UnexpectedSwapAmount(); // Combine PTs and YTs uint256 tBal = divider.combine(adapter, maturity, ptBal < ytBalIn ? ptBal : ytBalIn); return (keccak256("ERC3156FlashBorrower.onFlashLoan"), tBal - amount); } function _addLiquidityToSpace(BalancerPool pool, PoolLiquidity memory liq) internal returns (uint256 lpBal) { bytes32 poolId = pool.getPoolId(); IAsset[] memory assets = _convertERC20sToAssets(liq.tokens); for (uint8 i; i < liq.tokens.length; i++) { // Tokens and amounts must be in same order liq.tokens[i].approve(address(balancerVault), liq.amounts[i]); } // Behaves like EXACT_TOKENS_IN_FOR_BPT_OUT, user sends precise quantities of tokens, // and receives an estimated but unknown (computed at run time) quantity of BPT BalancerVault.JoinPoolRequest memory request = BalancerVault.JoinPoolRequest({ assets: assets, maxAmountsIn: liq.amounts, userData: abi.encode(liq.amounts), fromInternalBalance: false }); balancerVault.joinPool(poolId, address(this), msg.sender, request); lpBal = ERC20(address(pool)).balanceOf(msg.sender); } function _removeLiquidityFromSpace( bytes32 poolId, address pt, address target, uint256[] memory minAmountsOut, uint256 lpBal ) internal returns (uint256 tBal, uint256 ptBal) { // ExitPoolRequest params (ERC20[] memory tokens, , ) = balancerVault.getPoolTokens(poolId); IAsset[] memory assets = _convertERC20sToAssets(tokens); BalancerVault.ExitPoolRequest memory request = BalancerVault.ExitPoolRequest({ assets: assets, minAmountsOut: minAmountsOut, userData: abi.encode(lpBal), toInternalBalance: false }); balancerVault.exitPool(poolId, address(this), payable(address(this)), request); tBal = ERC20(target).balanceOf(address(this)); ptBal = ERC20(pt).balanceOf(address(this)); } /// @notice From: https://github.com/balancer-labs/balancer-examples/blob/master/packages/liquidity-provision/contracts/LiquidityProvider.sol#L33 /// @dev This helper function is a fast and cheap way to convert between IERC20[] and IAsset[] types function _convertERC20sToAssets(ERC20[] memory tokens) internal pure returns (IAsset[] memory assets) { assembly { assets := tokens } } /* ========== LOGS ========== */ event FactoryChanged(address indexed adapter, bool indexed isOn); event SeriesSponsored(address indexed adapter, uint256 indexed maturity, address indexed sponsor); event AdapterDeployed(address indexed adapter); event AdapterOnboarded(address indexed adapter); event AdapterVerified(address indexed adapter); event Swapped( address indexed sender, bytes32 indexed poolId, address assetIn, address assetOut, uint256 amountIn, uint256 amountOut, bytes4 indexed sig ); }