// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.0; import "../../interfaces/IBentoBoxMinimal.sol"; import "../../interfaces/IMasterDeployer.sol"; import "../../interfaces/IPool.sol"; import "../../interfaces/ITridentCallee.sol"; import "../../libraries/TridentMath.sol"; import "./TridentFranchisedERC20.sol"; /// @notice Trident exchange franchised pool template with constant product formula for swapping between an ERC-20 token pair. /// @dev The reserves are stored as bento shares. /// The curve is applied to shares as well. This pool does not care about the underlying amounts. contract FranchisedConstantProductPool is IPool, TridentFranchisedERC20 { event Mint(address indexed sender, uint256 amount0, uint256 amount1, address indexed recipient, uint256 liquidity); event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed recipient, uint256 liquidity); event Sync(uint256 reserve0, uint256 reserve1); uint256 internal constant MINIMUM_LIQUIDITY = 1000; uint8 internal constant PRECISION = 112; uint256 internal constant MAX_FEE = 10000; // @dev 100%. uint256 internal constant MAX_FEE_SQUARE = 100000000; uint256 internal constant E18 = uint256(10)**18; uint256 public immutable swapFee; uint256 internal immutable MAX_FEE_MINUS_SWAP_FEE; address public immutable barFeeTo; address public immutable bento; address public immutable masterDeployer; address public immutable token0; address public immutable token1; uint256 public barFee; uint256 public price0CumulativeLast; uint256 public price1CumulativeLast; uint256 public kLast; uint112 internal reserve0; uint112 internal reserve1; uint32 internal blockTimestampLast; bytes32 public constant override poolIdentifier = "Trident:FranchisedCP"; uint256 internal unlocked; modifier lock() { require(unlocked == 1, "LOCKED"); unlocked = 2; _; unlocked = 1; } constructor(bytes memory _deployData, address _masterDeployer) { ( address _token0, address _token1, uint256 _swapFee, bool _twapSupport, address _whiteListManager, address _operator, bool _level2 ) = abi.decode(_deployData, (address, address, uint256, bool, address, address, bool)); // @dev Factory ensures that the tokens are sorted. require(_token0 != address(0), "ZERO_ADDRESS"); require(_token0 != _token1, "IDENTICAL_ADDRESSES"); require(_token0 != address(this), "INVALID_TOKEN"); require(_token1 != address(this), "INVALID_TOKEN"); require(_swapFee <= MAX_FEE, "INVALID_SWAP_FEE"); TridentFranchisedERC20.initialize(_whiteListManager, _operator, _level2); (, bytes memory _barFee) = _masterDeployer.staticcall(abi.encodeWithSelector(IMasterDeployer.barFee.selector)); (, bytes memory _barFeeTo) = _masterDeployer.staticcall(abi.encodeWithSelector(IMasterDeployer.barFeeTo.selector)); (, bytes memory _bento) = _masterDeployer.staticcall(abi.encodeWithSelector(IMasterDeployer.bento.selector)); token0 = _token0; token1 = _token1; swapFee = _swapFee; // @dev This is safe from underflow - `swapFee` cannot exceed `MAX_FEE` per previous check. unchecked { MAX_FEE_MINUS_SWAP_FEE = MAX_FEE - _swapFee; } barFee = abi.decode(_barFee, (uint256)); barFeeTo = abi.decode(_barFeeTo, (address)); bento = abi.decode(_bento, (address)); masterDeployer = _masterDeployer; unlocked = 1; if (_twapSupport) blockTimestampLast = 1; } /// @dev Mints LP tokens - should be called via the router after transferring `bento` tokens. /// The router must ensure that sufficient LP tokens are minted by using the return value. function mint(bytes calldata data) public override lock returns (uint256 liquidity) { address recipient = abi.decode(data, (address)); _checkWhiteList(recipient); (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) = _getReserves(); (uint256 balance0, uint256 balance1) = _balance(); uint256 _totalSupply = totalSupply; unchecked { _totalSupply += _mintFee(_reserve0, _reserve1, _totalSupply); } uint256 amount0 = balance0 - _reserve0; uint256 amount1 = balance1 - _reserve1; (uint256 fee0, uint256 fee1) = _nonOptimalMintFee(amount0, amount1, _reserve0, _reserve1); uint256 computed = TridentMath.sqrt((balance0 - fee0) * (balance1 - fee1)); if (_totalSupply == 0) { _mint(address(0), MINIMUM_LIQUIDITY); liquidity = computed - MINIMUM_LIQUIDITY; } else { uint256 k = TridentMath.sqrt(uint256(_reserve0) * _reserve1); liquidity = ((computed - k) * _totalSupply) / k; } require(liquidity != 0, "INSUFFICIENT_LIQUIDITY_MINTED"); _mint(recipient, liquidity); _update(balance0, balance1, _reserve0, _reserve1, _blockTimestampLast); kLast = TridentMath.sqrt(balance0 * balance1); uint256 liquidityForEvent = liquidity; emit Mint(msg.sender, amount0, amount1, recipient, liquidityForEvent); } /// @dev Burns LP tokens sent to this contract. The router must ensure that the user gets sufficient output tokens. function burn(bytes calldata data) public override lock returns (IPool.TokenAmount[] memory withdrawnAmounts) { (address recipient, bool unwrapBento) = abi.decode(data, (address, bool)); _checkWhiteList(recipient); (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) = _getReserves(); (uint256 balance0, uint256 balance1) = _balance(); uint256 _totalSupply = totalSupply; uint256 liquidity = balanceOf[address(this)]; unchecked { _totalSupply += _mintFee(_reserve0, _reserve1, _totalSupply); } uint256 amount0 = (liquidity * balance0) / _totalSupply; uint256 amount1 = (liquidity * balance1) / _totalSupply; _burn(address(this), liquidity); _transfer(token0, amount0, recipient, unwrapBento); _transfer(token1, amount1, recipient, unwrapBento); // @dev This is safe from underflow - amounts are lesser figures derived from balances. unchecked { balance0 -= amount0; balance1 -= amount1; } _update(balance0, balance1, _reserve0, _reserve1, _blockTimestampLast); kLast = TridentMath.sqrt(balance0 * balance1); withdrawnAmounts = new TokenAmount[](2); withdrawnAmounts[0] = TokenAmount({token: address(token0), amount: amount0}); withdrawnAmounts[1] = TokenAmount({token: address(token1), amount: amount1}); emit Burn(msg.sender, amount0, amount1, recipient, liquidity); } /// @dev Burns LP tokens sent to this contract and swaps one of the output tokens for another /// - i.e., the user gets a single token out by burning LP tokens. function burnSingle(bytes calldata data) public override lock returns (uint256 amountOut) { (address tokenOut, address recipient, bool unwrapBento) = abi.decode(data, (address, address, bool)); _checkWhiteList(recipient); (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) = _getReserves(); (uint256 balance0, uint256 balance1) = _balance(); uint256 _totalSupply = totalSupply; uint256 liquidity = balanceOf[address(this)]; unchecked { _totalSupply += _mintFee(_reserve0, _reserve1, _totalSupply); } uint256 amount0 = (liquidity * balance0) / _totalSupply; uint256 amount1 = (liquidity * balance1) / _totalSupply; _burn(address(this), liquidity); unchecked { if (tokenOut == token1) { // @dev Swap `token0` for `token1` // - calculate `amountOut` as if the user first withdrew balanced liquidity and then swapped `token0` for `token1`. amount1 += _getAmountOut(amount0, _reserve0 - amount0, _reserve1 - amount1); _transfer(token1, amount1, recipient, unwrapBento); balance1 -= amount1; amountOut = amount1; amount0 = 0; } else { // @dev Swap `token1` for `token0`. require(tokenOut == token0, "INVALID_OUTPUT_TOKEN"); amount0 += _getAmountOut(amount1, _reserve1 - amount1, _reserve0 - amount0); _transfer(token0, amount0, recipient, unwrapBento); balance0 -= amount0; amountOut = amount0; amount1 = 0; } } _update(balance0, balance1, _reserve0, _reserve1, _blockTimestampLast); kLast = TridentMath.sqrt(balance0 * balance1); emit Burn(msg.sender, amount0, amount1, recipient, liquidity); } /// @dev Swaps one token for another. The router must prefund this contract and ensure there isn't too much slippage. function swap(bytes calldata data) public override lock returns (uint256 amountOut) { (address tokenIn, address recipient, bool unwrapBento) = abi.decode(data, (address, address, bool)); if (level2) _checkWhiteList(recipient); (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) = _getReserves(); (uint256 balance0, uint256 balance1) = _balance(); uint256 amountIn; address tokenOut; unchecked { if (tokenIn == token0) { tokenOut = token1; amountIn = balance0 - _reserve0; amountOut = _getAmountOut(amountIn, _reserve0, _reserve1); balance1 -= amountOut; } else { require(tokenIn == token1, "INVALID_INPUT_TOKEN"); tokenOut = token0; amountIn = balance1 - reserve1; amountOut = _getAmountOut(amountIn, _reserve1, _reserve0); balance0 -= amountOut; } } _transfer(tokenOut, amountOut, recipient, unwrapBento); _update(balance0, balance1, _reserve0, _reserve1, _blockTimestampLast); emit Swap(recipient, tokenIn, tokenOut, amountIn, amountOut); } /// @dev Swaps one token for another. The router must support swap callbacks and ensure there isn't too much slippage. function flashSwap(bytes calldata data) public override lock returns (uint256 amountOut) { (address tokenIn, address recipient, bool unwrapBento, uint256 amountIn, bytes memory context) = abi.decode( data, (address, address, bool, uint256, bytes) ); if (level2) _checkWhiteList(recipient); (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) = _getReserves(); unchecked { if (tokenIn == token0) { amountOut = _getAmountOut(amountIn, _reserve0, _reserve1); _transfer(token1, amountOut, recipient, unwrapBento); ITridentCallee(msg.sender).tridentSwapCallback(context); (uint256 balance0, uint256 balance1) = _balance(); require(balance0 - _reserve0 >= amountIn, "INSUFFICIENT_AMOUNT_IN"); _update(balance0, balance1, _reserve0, _reserve1, _blockTimestampLast); emit Swap(recipient, tokenIn, token1, amountIn, amountOut); } else { require(tokenIn == token1, "INVALID_INPUT_TOKEN"); amountOut = _getAmountOut(amountIn, _reserve1, _reserve0); _transfer(token0, amountOut, recipient, unwrapBento); ITridentCallee(msg.sender).tridentSwapCallback(context); (uint256 balance0, uint256 balance1) = _balance(); require(balance1 - _reserve1 >= amountIn, "INSUFFICIENT_AMOUNT_IN"); _update(balance0, balance1, _reserve0, _reserve1, _blockTimestampLast); emit Swap(recipient, tokenIn, token0, amountIn, amountOut); } } } /// @dev Updates `barFee` for Trident protocol. function updateBarFee() public { (, bytes memory _barFee) = masterDeployer.staticcall(abi.encodeWithSelector(IMasterDeployer.barFee.selector)); barFee = abi.decode(_barFee, (uint256)); } function _getReserves() internal view returns ( uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast ) { _reserve0 = reserve0; _reserve1 = reserve1; _blockTimestampLast = blockTimestampLast; } function _balance() internal view returns (uint256 balance0, uint256 balance1) { // @dev balanceOf(address,address). (, bytes memory _balance0) = bento.staticcall(abi.encodeWithSelector(0xf7888aec, token0, address(this))); balance0 = abi.decode(_balance0, (uint256)); // @dev balanceOf(address,address). (, bytes memory _balance1) = bento.staticcall(abi.encodeWithSelector(0xf7888aec, token1, address(this))); balance1 = abi.decode(_balance1, (uint256)); } function _update( uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast ) internal { require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "OVERFLOW"); if (blockTimestampLast == 0) { // @dev TWAP support is disabled for gas efficiency. reserve0 = uint112(balance0); reserve1 = uint112(balance1); } else { uint32 blockTimestamp = uint32(block.timestamp); if (blockTimestamp != _blockTimestampLast && _reserve0 != 0 && _reserve1 != 0) { unchecked { uint32 timeElapsed = blockTimestamp - _blockTimestampLast; uint256 price0 = (uint256(_reserve1) << PRECISION) / _reserve0; price0CumulativeLast += price0 * timeElapsed; uint256 price1 = (uint256(_reserve0) << PRECISION) / _reserve1; price1CumulativeLast += price1 * timeElapsed; } } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; } emit Sync(balance0, balance1); } function _mintFee( uint112 _reserve0, uint112 _reserve1, uint256 _totalSupply ) internal returns (uint256 liquidity) { uint256 _kLast = kLast; if (_kLast != 0) { uint256 computed = TridentMath.sqrt(uint256(_reserve0) * _reserve1); if (computed > _kLast) { // @dev `barFee` % of increase in liquidity. // It's going to be slightly less than `barFee` % in reality due to the math. liquidity = (_totalSupply * (computed - _kLast) * barFee) / computed / MAX_FEE; if (liquidity != 0) { _mint(barFeeTo, liquidity); } } } } function _getAmountOut( uint256 amountIn, uint256 reserveAmountIn, uint256 reserveAmountOut ) internal view returns (uint256 amountOut) { uint256 amountInWithFee = amountIn * MAX_FEE_MINUS_SWAP_FEE; amountOut = (amountInWithFee * reserveAmountOut) / (reserveAmountIn * MAX_FEE + amountInWithFee); } function _transfer( address token, uint256 shares, address to, bool unwrapBento ) internal { if (unwrapBento) { (bool success, ) = bento.call(abi.encodeWithSelector(IBentoBoxMinimal.withdraw.selector, token, address(this), to, 0, shares)); require(success, "WITHDRAW_FAILED"); } else { (bool success, ) = bento.call(abi.encodeWithSelector(IBentoBoxMinimal.transfer.selector, token, address(this), to, shares)); require(success, "TRANSFER_FAILED"); } } /// @dev This fee is charged to cover for `swapFee` when users add unbalanced liquidity. function _nonOptimalMintFee( uint256 _amount0, uint256 _amount1, uint256 _reserve0, uint256 _reserve1 ) internal view returns (uint256 token0Fee, uint256 token1Fee) { if (_reserve0 == 0 || _reserve1 == 0) return (0, 0); uint256 amount1Optimal = (_amount0 * _reserve1) / _reserve0; if (amount1Optimal <= _amount1) { token1Fee = (swapFee * (_amount1 - amount1Optimal)) / (2 * MAX_FEE); } else { uint256 amount0Optimal = (_amount1 * _reserve0) / _reserve1; token0Fee = (swapFee * (_amount0 - amount0Optimal)) / (2 * MAX_FEE); } } function getAssets() public view override returns (address[] memory assets) { assets = new address[](2); assets[0] = token0; assets[1] = token1; } function getAmountOut(bytes calldata data) public view override returns (uint256 finalAmountOut) { (address tokenIn, uint256 amountIn) = abi.decode(data, (address, uint256)); (uint112 _reserve0, uint112 _reserve1, ) = _getReserves(); if (tokenIn == token0) { finalAmountOut = _getAmountOut(amountIn, _reserve0, _reserve1); } else { require(tokenIn == token1, "INVALID_INPUT_TOKEN"); finalAmountOut = _getAmountOut(amountIn, _reserve1, _reserve0); } } function getAmountIn(bytes calldata) public pure override returns (uint256) { revert(); } function getReserves() public view returns ( uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast ) { return _getReserves(); } }