Skip to content

Instantly share code, notes, and snippets.

@JTraversa
Created September 21, 2022 23:48
Show Gist options
  • Save JTraversa/9eff8ada74bfb1736554a6223dcc33d4 to your computer and use it in GitHub Desktop.
Save JTraversa/9eff8ada74bfb1736554a6223dcc33d4 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.14+commit.80d49f37.js&optimize=true&runs=2000&gist=
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.14;
import "./IERC20.sol";
import "./IERC20Metadata.sol";
import "./MinimalTransferHelper.sol";
import "./CastU256U128.sol";
import "./CastU256U112.sol";
import "./CastU256I256.sol";
import "./CastU128U112.sol";
import "./CastU128I128.sol";
import "./IPool.sol";
import "./IFYToken.sol";
import "./YieldMath.sol";
import "./ERC20Permit.sol";
/// @dev The Pool contract exchanges base for fyToken at a price defined by a specific formula.
contract Pool is IPool, ERC20Permit {
using CastU256U128 for uint256;
using CastU256U112 for uint256;
using CastU256I256 for uint256;
using CastU128U112 for uint128;
using CastU128I128 for uint128;
using MinimalTransferHelper for IERC20;
event Trade(uint32 maturity, address indexed from, address indexed to, int256 bases, int256 fyTokens);
event Liquidity(uint32 maturity, address indexed from, address indexed to, address indexed fyTokenTo, int256 bases, int256 fyTokens, int256 poolTokens);
event Sync(uint112 baseCached, uint112 fyTokenCached, uint256 cumulativeBalancesRatio);
int128 public immutable override ts; // 1 / Seconds in 10 years, in 64.64
int128 public immutable override g1; // To be used when selling base to the pool
int128 public immutable override g2; // To be used when selling fyToken to the pool
uint32 public immutable override maturity;
uint96 public immutable override scaleFactor; // Scale up to 18 low decimal tokens to get the right precision in YieldMath
IERC20 public immutable override base;
IFYToken public immutable override fyToken;
uint112 private baseCached; // uses single storage slot, accessible via getCache
uint112 private fyTokenCached; // uses single storage slot, accessible via getCache
uint32 private blockTimestampLast; // uses single storage slot, accessible via getCache
uint256 public override cumulativeBalancesRatio; // Fixed point factor with 27 decimals (ray)
/// @dev Deploy a Pool.
/// Make sure that the fyToken follows ERC20 standards with regards to name, symbol and decimals
constructor(IERC20 base_, IFYToken fyToken_, int128 ts_, int128 g1_, int128 g2_)
ERC20Permit(
string(abi.encodePacked(IERC20Metadata(address(fyToken_)).name(), " LP")),
string(abi.encodePacked(IERC20Metadata(address(fyToken_)).symbol(), "LP")),
IERC20Metadata(address(fyToken_)).decimals()
)
{
fyToken = fyToken_;
base = base_;
uint256 maturity_ = fyToken_.maturity();
require (maturity_ <= type(uint32).max, "Pool: Maturity too far in the future");
maturity = uint32(maturity_);
ts = ts_;
g1 = g1_;
g2 = g2_;
scaleFactor = uint96(10 ** (18 - uint96(decimals)));
}
/// @dev Trading can only be done before maturity
modifier beforeMaturity() {
require(
block.timestamp < maturity,
"Pool: Too late"
);
_;
}
// ---- Balances management ----
/// @dev Updates the cache to match the actual balances.
function sync() external {
_update(_getBaseBalance(), _getFYTokenBalance(), baseCached, fyTokenCached);
}
/// @dev Returns the cached balances & last updated timestamp.
/// @return Cached base token balance.
/// @return Cached virtual FY token balance.
/// @return Timestamp that balances were last cached.
function getCache()
external view override
returns (uint112, uint112, uint32)
{
return (baseCached, fyTokenCached, blockTimestampLast);
}
/// @dev Returns the "virtual" fyToken balance, which is the real balance plus the pool token supply.
function getFYTokenBalance()
public view override
returns(uint112)
{
return _getFYTokenBalance();
}
/// @dev Returns the base balance
function getBaseBalance()
public view override
returns(uint112)
{
return _getBaseBalance();
}
/// @dev Returns the "virtual" fyToken balance, which is the real balance plus the pool token supply.
function _getFYTokenBalance()
internal view
returns(uint112)
{
return (fyToken.balanceOf(address(this)) + _totalSupply).u112();
}
/// @dev Returns the base balance
function _getBaseBalance()
internal view
returns(uint112)
{
return base.balanceOf(address(this)).u112();
}
/// @dev Retrieve any base tokens not accounted for in the cache
function retrieveBase(address to)
external override
returns(uint128 retrieved)
{
retrieved = _getBaseBalance() - baseCached; // Cache can never be above balances
base.safeTransfer(to, retrieved);
// Now the current balances match the cache, so no need to update the TWAR
}
/// @dev Retrieve any fyTokens not accounted for in the cache
function retrieveFYToken(address to)
external override
returns(uint128 retrieved)
{
retrieved = _getFYTokenBalance() - fyTokenCached; // Cache can never be above balances
IERC20(address(fyToken)).safeTransfer(to, retrieved);
// Now the balances match the cache, so no need to update the TWAR
}
/// @dev Update cache and, on the first call per block, ratio accumulators
function _update(uint128 baseBalance, uint128 fyBalance, uint112 _baseCached, uint112 _fyTokenCached) private {
uint32 blockTimestamp = uint32(block.timestamp);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _baseCached != 0 && _fyTokenCached != 0) {
// We multiply by 1e27 here so that r = t * y/x is a fixed point factor with 27 decimals
uint256 scaledFYTokenCached = uint256(_fyTokenCached) * 1e27;
cumulativeBalancesRatio += scaledFYTokenCached * timeElapsed / _baseCached;
}
baseCached = baseBalance.u112();
fyTokenCached = fyBalance.u112();
blockTimestampLast = blockTimestamp;
emit Sync(baseCached, fyTokenCached, cumulativeBalancesRatio);
}
// ---- Liquidity ----
/// @dev Mint liquidity tokens in exchange for adding base and fyToken
/// The amount of liquidity tokens to mint is calculated from the amount of unaccounted for fyToken in this contract.
/// A proportional amount of base tokens need to be present in this contract, also unaccounted for.
/// @param to Wallet receiving the minted liquidity tokens.
/// @param remainder Wallet receiving any surplus base.
/// @param minRatio Minimum ratio of base to fyToken in the pool.
/// @param maxRatio Maximum ratio of base to fyToken in the pool.
/// @return The amount of liquidity tokens minted.
function mint(address to, address remainder, uint256 minRatio, uint256 maxRatio)
external override
returns (uint256, uint256, uint256)
{
return _mintInternal(to, remainder, 0, minRatio, maxRatio);
}
/// @dev Mint liquidity tokens in exchange for adding only base
/// The amount of liquidity tokens is calculated from the amount of fyToken to buy from the pool,
/// plus the amount of unaccounted for fyToken in this contract.
/// The base tokens need to be present in this contract, unaccounted for.
/// @param to Wallet receiving the minted liquidity tokens.
/// @param remainder Wallet receiving any surplus base.
/// @param fyTokenToBuy Amount of `fyToken` being bought in the Pool, from this we calculate how much base it will be taken in.
/// @param minRatio Minimum ratio of base to fyToken in the pool.
/// @param maxRatio Maximum ratio of base to fyToken in the pool.
/// @return The amount of liquidity tokens minted.
function mintWithBase(address to, address remainder, uint256 fyTokenToBuy, uint256 minRatio, uint256 maxRatio)
external override
returns (uint256, uint256, uint256)
{
return _mintInternal(to, remainder, fyTokenToBuy, minRatio, maxRatio);
}
/// @dev Mint liquidity tokens, with an optional internal trade to buy fyToken beforehand.
/// The amount of liquidity tokens is calculated from the amount of fyToken to buy from the pool,
/// plus the amount of unaccounted for fyToken in this contract.
/// The base tokens need to be present in this contract, unaccounted for.
/// @param to Wallet receiving the minted liquidity tokens.
/// @param remainder Wallet receiving any surplus base.
/// @param fyTokenToBuy Amount of `fyToken` being bought in the Pool, from this we calculate how much base it will be taken in.
/// @param minRatio Minimum ratio of base to fyToken in the pool.
/// @param maxRatio Minimum ratio of base to fyToken in the pool.
function _mintInternal(address to, address remainder, uint256 fyTokenToBuy, uint256 minRatio, uint256 maxRatio)
internal
returns (uint256 baseIn, uint256 fyTokenIn, uint256 tokensMinted)
{
// Gather data
uint256 supply = _totalSupply;
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
uint256 _realFYTokenCached = _fyTokenCached - supply; // The fyToken cache includes the virtual fyToken, equal to the supply
uint256 baseBalance = base.balanceOf(address(this));
uint256 fyTokenBalance = fyToken.balanceOf(address(this));
uint256 baseAvailable = baseBalance - _baseCached;
// Check the burn wasn't sandwiched
require (
_realFYTokenCached == 0 || (
uint256(_baseCached) * 1e18 / _realFYTokenCached >= minRatio &&
uint256(_baseCached) * 1e18 / _realFYTokenCached <= maxRatio
),
"Pool: Reserves ratio changed"
);
// Calculate token amounts
if (supply == 0) { // Initialize at 1 pool token minted per base token supplied
baseIn = baseAvailable;
tokensMinted = baseIn;
} else if (_realFYTokenCached == 0) { // Edge case, no fyToken in the Pool after initialization
baseIn = baseAvailable;
tokensMinted = supply * baseIn / _baseCached;
} else {
// There is an optional virtual trade before the mint
uint256 baseToSell;
if (fyTokenToBuy > 0) {
baseToSell = _buyFYTokenPreview(
fyTokenToBuy.u128(),
_baseCached,
_fyTokenCached
);
}
// We use all the available fyTokens, plus a virtual trade if it happened, surplus is in base tokens
fyTokenIn = fyTokenBalance - _realFYTokenCached;
tokensMinted = (supply * (fyTokenToBuy + fyTokenIn)) / (_realFYTokenCached - fyTokenToBuy);
baseIn = baseToSell + ((_baseCached + baseToSell) * tokensMinted) / supply;
require(baseAvailable >= baseIn, "Pool: Not enough base token in");
}
// Update TWAR
_update(
(_baseCached + baseIn).u128(),
(_fyTokenCached + fyTokenIn + tokensMinted).u128(), // Account for the "virtual" fyToken from the new minted LP tokens
_baseCached,
_fyTokenCached
);
// Execute mint
_mint(to, tokensMinted);
// Return any unused base
if (baseAvailable - baseIn > 0) base.safeTransfer(remainder, baseAvailable - baseIn);
emit Liquidity(maturity, msg.sender, to, address(0), -(baseIn.i256()), -(fyTokenIn.i256()), tokensMinted.i256());
}
/// @dev Burn liquidity tokens in exchange for base and fyToken.
/// The liquidity tokens need to be in this contract.
/// @param baseTo Wallet receiving the base.
/// @param fyTokenTo Wallet receiving the fyToken.
/// @param minRatio Minimum ratio of base to fyToken in the pool.
/// @param maxRatio Maximum ratio of base to fyToken in the pool.
/// @return The amount of tokens burned and returned (tokensBurned, bases, fyTokens).
function burn(address baseTo, address fyTokenTo, uint256 minRatio, uint256 maxRatio)
external override
returns (uint256, uint256, uint256)
{
return _burnInternal(baseTo, fyTokenTo, false, minRatio, maxRatio);
}
/// @dev Burn liquidity tokens in exchange for base.
/// The liquidity provider needs to have called `pool.approve`.
/// @param to Wallet receiving the base and fyToken.
/// @param minRatio Minimum ratio of base to fyToken in the pool.
/// @param maxRatio Minimum ratio of base to fyToken in the pool.
/// @return tokensBurned The amount of lp tokens burned.
/// @return baseOut The amount of base tokens returned.
function burnForBase(address to, uint256 minRatio, uint256 maxRatio)
external override
returns (uint256 tokensBurned, uint256 baseOut)
{
(tokensBurned, baseOut, ) = _burnInternal(to, address(0), true, minRatio, maxRatio);
}
/// @dev Burn liquidity tokens in exchange for base.
/// The liquidity provider needs to have called `pool.approve`.
/// @param baseTo Wallet receiving the base.
/// @param fyTokenTo Wallet receiving the fyToken.
/// @param tradeToBase Whether the resulting fyToken should be traded for base tokens.
/// @param minRatio Minimum ratio of base to fyToken in the pool.
/// @param maxRatio Minimum ratio of base to fyToken in the pool.
/// @return tokensBurned The amount of pool tokens burned.
/// @return tokenOut The amount of base tokens returned.
/// @return fyTokenOut The amount of fyTokens returned.
function _burnInternal(address baseTo, address fyTokenTo, bool tradeToBase, uint256 minRatio, uint256 maxRatio)
internal
returns (uint256 tokensBurned, uint256 tokenOut, uint256 fyTokenOut)
{
// Gather data
tokensBurned = _balanceOf[address(this)];
uint256 supply = _totalSupply;
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
uint256 _realFYTokenCached = _fyTokenCached - supply; // The fyToken cache includes the virtual fyToken, equal to the supply
// Check the burn wasn't sandwiched
require (
_realFYTokenCached == 0 || (
uint256(_baseCached) * 1e18 / _realFYTokenCached >= minRatio &&
uint256(_baseCached) * 1e18 / _realFYTokenCached <= maxRatio
),
"Pool: Reserves ratio changed"
);
// Calculate trade
tokenOut = (tokensBurned * _baseCached) / supply;
fyTokenOut = (tokensBurned * _realFYTokenCached) / supply;
if (tradeToBase) {
tokenOut += YieldMath.baseOutForFYTokenIn( // This is a virtual sell
(_baseCached - tokenOut.u128()) * scaleFactor, // Cache, minus virtual burn
(_fyTokenCached - fyTokenOut.u128()) * scaleFactor, // Cache, minus virtual burn
fyTokenOut.u128() * scaleFactor, // Sell the virtual fyToken obtained
maturity - uint32(block.timestamp), // This can't be called after maturity
ts,
g2
) / scaleFactor;
fyTokenOut = 0;
}
// Update TWAR
_update(
(_baseCached - tokenOut).u128(),
(_fyTokenCached - fyTokenOut - tokensBurned).u128(),
_baseCached,
_fyTokenCached
);
// Transfer assets
_burn(address(this), tokensBurned);
base.safeTransfer(baseTo, tokenOut);
if (fyTokenOut > 0) IERC20(address(fyToken)).safeTransfer(fyTokenTo, fyTokenOut);
emit Liquidity(maturity, msg.sender, baseTo, fyTokenTo, tokenOut.i256(), fyTokenOut.i256(), -(tokensBurned.i256()));
}
// ---- Trading ----
/// @dev Sell base for fyToken.
/// The trader needs to have transferred the amount of base to sell to the pool before in the same transaction.
/// @param to Wallet receiving the fyToken being bought
/// @param min Minimm accepted amount of fyToken
/// @return Amount of fyToken that will be deposited on `to` wallet
function sellBase(address to, uint128 min)
external override
returns(uint128)
{
// Calculate trade
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
uint112 _baseBalance = _getBaseBalance();
uint112 _fyTokenBalance = _getFYTokenBalance();
uint128 baseIn = _baseBalance - _baseCached;
uint128 fyTokenOut = _sellBasePreview(
baseIn,
_baseCached,
_fyTokenBalance
);
// Slippage check
require(
fyTokenOut >= min,
"Pool: Not enough fyToken obtained"
);
// Update TWAR
_update(
_baseBalance,
_fyTokenBalance - fyTokenOut,
_baseCached,
_fyTokenCached
);
// Transfer assets
IERC20(address(fyToken)).safeTransfer(to, fyTokenOut);
emit Trade(maturity, msg.sender, to, -(baseIn.i128()), fyTokenOut.i128());
return fyTokenOut;
}
/// @dev Returns how much fyToken would be obtained by selling `baseIn` base
/// @param baseIn Amount of base hypothetically sold.
/// @return Amount of fyToken hypothetically bought.
function sellBasePreview(uint128 baseIn)
external view override
returns(uint128)
{
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
return _sellBasePreview(baseIn, _baseCached, _fyTokenCached);
}
/// @dev Returns how much fyToken would be obtained by selling `baseIn` base
function _sellBasePreview(
uint128 baseIn,
uint112 baseBalance,
uint112 fyTokenBalance
)
private view
beforeMaturity
returns(uint128)
{
uint128 fyTokenOut = YieldMath.fyTokenOutForBaseIn(
baseBalance * scaleFactor,
fyTokenBalance * scaleFactor,
baseIn * scaleFactor,
maturity - uint32(block.timestamp), // This can't be called after maturity
ts,
g1
) / scaleFactor;
require(
fyTokenBalance - fyTokenOut >= baseBalance + baseIn,
"Pool: fyToken balance too low"
);
return fyTokenOut;
}
/// @dev Buy base for fyToken
/// The trader needs to have called `fyToken.approve`
/// @param to Wallet receiving the base being bought
/// @param tokenOut Amount of base being bought that will be deposited in `to` wallet
/// @param max Maximum amount of fyToken that will be paid for the trade
/// @return Amount of fyToken that will be taken from caller
function buyBase(address to, uint128 tokenOut, uint128 max)
external override
returns(uint128)
{
// Calculate trade
uint128 fyTokenBalance = _getFYTokenBalance();
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
uint128 fyTokenIn = _buyBasePreview(
tokenOut,
_baseCached,
_fyTokenCached
);
require(
fyTokenBalance - _fyTokenCached >= fyTokenIn,
"Pool: Not enough fyToken in"
);
// Slippage check
require(
fyTokenIn <= max,
"Pool: Too much fyToken in"
);
// Update TWAR
_update(
_baseCached - tokenOut,
_fyTokenCached + fyTokenIn,
_baseCached,
_fyTokenCached
);
// Transfer assets
base.safeTransfer(to, tokenOut);
emit Trade(maturity, msg.sender, to, tokenOut.i128(), -(fyTokenIn.i128()));
return fyTokenIn;
}
/// @dev Returns how much fyToken would be required to buy `tokenOut` base.
/// @param tokenOut Amount of base hypothetically desired.
/// @return Amount of fyToken hypothetically required.
function buyBasePreview(uint128 tokenOut)
external view override
returns(uint128)
{
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
return _buyBasePreview(tokenOut, _baseCached, _fyTokenCached);
}
/// @dev Returns how much fyToken would be required to buy `tokenOut` base.
function _buyBasePreview(
uint128 tokenOut,
uint112 baseBalance,
uint112 fyTokenBalance
)
private view
beforeMaturity
returns(uint128)
{
return YieldMath.fyTokenInForBaseOut(
baseBalance * scaleFactor,
fyTokenBalance * scaleFactor,
tokenOut * scaleFactor,
maturity - uint32(block.timestamp), // This can't be called after maturity
ts,
g2
) / scaleFactor;
}
/// @dev Sell fyToken for base
/// The trader needs to have transferred the amount of fyToken to sell to the pool before in the same transaction.
/// @param to Wallet receiving the base being bought
/// @param min Minimm accepted amount of base
/// @return Amount of base that will be deposited on `to` wallet
function sellFYToken(address to, uint128 min)
external override
returns(uint128)
{
// Calculate trade
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
uint112 _fyTokenBalance = _getFYTokenBalance();
uint112 _baseBalance = _getBaseBalance();
uint128 fyTokenIn = _fyTokenBalance - _fyTokenCached;
uint128 baseOut = _sellFYTokenPreview(
fyTokenIn,
_baseCached,
_fyTokenCached
);
// Slippage check
require(
baseOut >= min,
"Pool: Not enough base obtained"
);
// Update TWAR
_update(
_baseBalance - baseOut,
_fyTokenBalance,
_baseCached,
_fyTokenCached
);
// Transfer assets
base.safeTransfer(to, baseOut);
emit Trade(maturity, msg.sender, to, baseOut.i128(), -(fyTokenIn.i128()));
return baseOut;
}
/// @dev Returns how much base would be obtained by selling `fyTokenIn` fyToken.
/// @param fyTokenIn Amount of fyToken hypothetically sold.
/// @return Amount of base hypothetically bought.
function sellFYTokenPreview(uint128 fyTokenIn)
external view override
returns(uint128)
{
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
return _sellFYTokenPreview(fyTokenIn, _baseCached, _fyTokenCached);
}
/// @dev Returns how much base would be obtained by selling `fyTokenIn` fyToken.
function _sellFYTokenPreview(
uint128 fyTokenIn,
uint112 baseBalance,
uint112 fyTokenBalance
)
private view
beforeMaturity
returns(uint128)
{
return YieldMath.baseOutForFYTokenIn(
baseBalance * scaleFactor,
fyTokenBalance * scaleFactor,
fyTokenIn * scaleFactor,
maturity - uint32(block.timestamp), // This can't be called after maturity
ts,
g2
) / scaleFactor;
}
/// @dev Buy fyToken for base
/// The trader needs to have called `base.approve`
/// @param to Wallet receiving the fyToken being bought
/// @param fyTokenOut Amount of fyToken being bought that will be deposited in `to` wallet
/// @param max Maximum amount of base token that will be paid for the trade
/// @return Amount of base that will be taken from caller's wallet
function buyFYToken(address to, uint128 fyTokenOut, uint128 max)
external override
returns(uint128)
{
// Calculate trade
uint128 baseBalance = _getBaseBalance();
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
uint128 baseIn = _buyFYTokenPreview(
fyTokenOut,
_baseCached,
_fyTokenCached
);
require(
baseBalance - _baseCached >= baseIn,
"Pool: Not enough base token in"
);
// Slippage check
require(
baseIn <= max,
"Pool: Too much base token in"
);
// Update TWAR
_update(
_baseCached + baseIn,
_fyTokenCached - fyTokenOut,
_baseCached,
_fyTokenCached
);
// Transfer assets
IERC20(address(fyToken)).safeTransfer(to, fyTokenOut);
emit Trade(maturity, msg.sender, to, -(baseIn.i128()), fyTokenOut.i128());
return baseIn;
}
/// @dev Returns how much base would be required to buy `fyTokenOut` fyToken.
/// @param fyTokenOut Amount of fyToken hypothetically desired.
/// @return Amount of base hypothetically required.
function buyFYTokenPreview(uint128 fyTokenOut)
external view override
returns(uint128)
{
(uint112 _baseCached, uint112 _fyTokenCached) =
(baseCached, fyTokenCached);
return _buyFYTokenPreview(fyTokenOut, _baseCached, _fyTokenCached);
}
/// @dev Returns how much base would be required to buy `fyTokenOut` fyToken.
function _buyFYTokenPreview(
uint128 fyTokenOut,
uint128 baseBalance,
uint128 fyTokenBalance
)
private view
beforeMaturity
returns(uint128)
{
uint128 baseIn = YieldMath.baseInForFYTokenOut(
baseBalance * scaleFactor,
fyTokenBalance * scaleFactor,
fyTokenOut * scaleFactor,
maturity - uint32(block.timestamp), // This can't be called after maturity
ts,
g1
) / scaleFactor;
require(
fyTokenBalance - fyTokenOut >= baseBalance + baseIn,
"Pool: fyToken balance too low"
);
return baseIn;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment