// SPDX-License-Identifier: MIT pragma solidity =0.8.18; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol"; import "../../SomaGuard/utils/GuardHelper.sol"; import "../../SecurityTokens/ERC20/utils/ERC20Helper.sol"; import "../../SecurityTokens/extensions/ERC20Security.sol"; import "./interfaces/ISomaSwapPair.sol"; import "./libraries/Math.sol"; import "./libraries/UQ112x112.sol"; import "./interfaces/ISomaSwapFactory.sol"; import "./interfaces/ISomaSwapCallee.sol"; /** * @notice Implementation of the {ISomaSwapPair} interface. */ contract SomaSwapPair is ISomaSwapPair, ERC20Security { using SafeMath for uint256; using UQ112x112 for uint224; /** * @inheritdoc ISomaSwapPair */ uint256 public constant override MINIMUM_LIQUIDITY = 10 ** 3; bytes4 private constant SELECTOR = bytes4(keccak256(bytes("transfer(address,uint256)"))); /** * @inheritdoc ISomaSwapPair */ address public override factory; /** * @inheritdoc ISomaSwapPair */ address public override token0; /** * @inheritdoc ISomaSwapPair */ address public override token1; uint112 private reserve0; // uses single storage slot, accessible via getReserves uint112 private reserve1; // uses single storage slot, accessible via getReserves uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves /** * @inheritdoc ISomaSwapPair */ uint256 public override price0CumulativeLast; /** * @inheritdoc ISomaSwapPair */ uint256 public override price1CumulativeLast; /** * @inheritdoc ISomaSwapPair */ uint256 public override kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event uint256 private locked; /** * @notice Swap Data structure. Helper structure to save variable space. * @param balance0 The pair address' balance of token0 before reserves are updated. * @param balance1 The pair address' balance of token1 before reserves are updated. * @param amount0In The amount of token0 swapped. * @param amount1In The amount of token1 swapped. */ struct SwapData { uint256 balance0; uint256 balance1; uint256 amount0In; uint256 amount1In; } /** * @notice Modifier to ensure the contract is not locked upon a function call. */ modifier lock() { require(locked == 0, "SomaSwap: LOCKED"); locked = 1; _; locked = 0; } /** * @inheritdoc ISomaSwapPair * @dev Called once by the factory at time of deployment */ function initialize(address _token0, address _token1) external override initializer { // need to avoid _msgSender here so that we define the factory and _msgSender can be used factory = msg.sender; token0 = _token0; token1 = _token1; string memory _name = "SOMASwap"; string memory _symbol; { string memory token0Name = IERC20Metadata(token0).name(); string memory token1Name = IERC20Metadata(token1).name(); _name = string( abi.encodePacked( _name, bytes(token0Name).length > 0 || bytes(token1Name).length > 0 ? ": " : "", bytes(token0Name).length > 0 ? token0Name : "", bytes(token0Name).length > 0 && bytes(token1Name).length > 0 ? " - " : "", bytes(token1Name).length > 0 ? token1Name : "" ) ); } { string memory token0Symbol = IERC20Metadata(token0).symbol(); string memory token1Symbol = IERC20Metadata(token1).symbol(); _symbol = string( abi.encodePacked( bytes(token0Symbol).length > 0 ? token0Symbol : "", bytes(token0Symbol).length > 0 && bytes(token1Symbol).length > 0 ? "-" : "", bytes(token1Symbol).length > 0 ? token1Symbol : "" ) ); } __ERC20Security_init("SOMAswap", _name, _symbol); } /** * @notice Checks if SomaSwapPair inherits a given contract interface. * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(ISomaSwapPair).interfaceId || super.supportsInterface(interfaceId); } /** * @notice Returns the required privileges. */ function requiredPrivileges() public view virtual override returns (bytes32) { bytes32 token0Privileges = GuardHelper.requiredPrivileges(token0); bytes32 token1Privileges = GuardHelper.requiredPrivileges(token1); return GuardHelper.mergePrivileges(token0Privileges, token1Privileges, super.requiredPrivileges()); } /** * @inheritdoc ISomaSwapPair */ function getReserves() public view override returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { _reserve0 = reserve0; _reserve1 = reserve1; _blockTimestampLast = blockTimestampLast; } function _safeTransfer(address token, address to, uint256 value) private { (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); require(success && (data.length == 0 || abi.decode(data, (bool))), "SomaSwap: TRANSFER_FAILED"); } // update reserves and, on the first call per block, price accumulators function _update(uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "SomaSwap: OVERFLOW"); // slither-disable-next-line weak-prng uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint256(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint256(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); } // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { address feeTo = ISomaSwapFactory(factory).feeTo(); feeOn = feeTo != address(0); uint256 _kLast = kLast; // gas savings if (feeOn) { if (_kLast != 0) { uint256 rootK = Math.sqrt(uint256(_reserve0).mul(_reserve1)); uint256 rootKLast = Math.sqrt(_kLast); if (rootK > rootKLast) { uint256 numerator = totalSupply().mul(rootK.sub(rootKLast)); uint256 denominator = rootK.mul(5).add(rootKLast); uint256 liquidity = numerator / denominator; if (liquidity > 0) _mint(feeTo, liquidity); } } } else if (_kLast != 0) { kLast = 0; } } /** * @inheritdoc ISomaSwapPair * @dev This low-level function should be called from a contract which performs important safety checks */ function mint(address to) external override lock returns (uint256 liquidity) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings uint256 balance0 = IERC20(token0).balanceOf(address(this)); uint256 balance1 = IERC20(token1).balanceOf(address(this)); uint256 amount0 = balance0.sub(_reserve0); uint256 amount1 = balance1.sub(_reserve1); bool feeOn = _mintFee(_reserve0, _reserve1); uint256 _totalSupply = totalSupply(); // gas savings, must be defined here since totalSupply can update in _mintFee if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); // Here we mint to the factory instead of self, because the burn requires burning all of tokens on self _mint(factory, MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); } require(liquidity > 0, "SomaSwap: INSUFFICIENT_LIQUIDITY_MINTED"); _mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint256(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Mint(msg.sender, amount0, amount1); } /** * @inheritdoc ISomaSwapPair * @dev This low-level function should be called from a contract which performs important safety checks */ function burn(address to) external override lock returns (uint256 amount0, uint256 amount1) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings address _token0 = token0; // gas savings address _token1 = token1; // gas savings uint256 balance0 = IERC20(_token0).balanceOf(address(this)); uint256 balance1 = IERC20(_token1).balanceOf(address(this)); uint256 liquidity = balanceOf(address(this)); bool feeOn = _mintFee(_reserve0, _reserve1); uint256 _totalSupply = totalSupply(); // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution require(amount0 > 0 && amount1 > 0, "SomaSwap: INSUFFICIENT_LIQUIDITY_BURNED"); _burn(address(this), liquidity); // slither-disable-next-line reentrancy-no-eth _safeTransfer(_token0, to, amount0); // slither-disable-next-line reentrancy-no-eth _safeTransfer(_token1, to, amount1); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint256(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Burn(msg.sender, amount0, amount1, to); } /** * @inheritdoc ISomaSwapPair * @dev This low-level function should be called from a contract which performs important safety checks */ //slither-disable-next-line external-function function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external override lock onlyApprovedPrivileges(_forwardSender()) { require(amount0Out > 0 || amount1Out > 0, "SomaSwap: INSUFFICIENT_OUTPUT_AMOUNT"); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, "SomaSwap: INSUFFICIENT_LIQUIDITY"); SwapData memory _data; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, "SomaSwap: INVALID_TO"); // slither-disable-next-line reentrancy-no-eth if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens // slither-disable-next-line reentrancy-no-eth if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens // slither-disable-next-line reentrancy-no-eth if (data.length > 0) ISomaSwapCallee(to).somaSwapCall(msg.sender, amount0Out, amount1Out, data); _data.balance0 = IERC20(_token0).balanceOf(address(this)); _data.balance1 = IERC20(_token1).balanceOf(address(this)); } _data.amount0In = _data.balance0 > _reserve0 - amount0Out ? _data.balance0 - (_reserve0 - amount0Out) : 0; _data.amount1In = _data.balance1 > _reserve1 - amount1Out ? _data.balance1 - (_reserve1 - amount1Out) : 0; require(_data.amount0In > 0 || _data.amount1In > 0, "SomaSwap: INSUFFICIENT_INPUT_AMOUNT"); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint256 balance0Adjusted = _data.balance0.mul(1000).sub(_data.amount0In.mul(3)); uint256 balance1Adjusted = _data.balance1.mul(1000).sub(_data.amount1In.mul(3)); require( balance0Adjusted.mul(balance1Adjusted) >= uint256(_reserve0).mul(_reserve1).mul(1000 ** 2), "SomaSwap: K" ); } _update(_data.balance0, _data.balance1, _reserve0, _reserve1); emit Swap(msg.sender, _data.amount0In, _data.amount1In, amount0Out, amount1Out, to); } /** * @inheritdoc ISomaSwapPair * @dev Force balances to match reserves */ function skim(address to) external override lock { address _token0 = token0; // gas savings address _token1 = token1; // gas savings _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0)); _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1)); } /** * @inheritdoc ISomaSwapPair * @dev Force reserves to match balances */ function sync() external override lock { _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1); } function _forwardSender() private view returns (address sender) { if (ISomaSwapFactory(factory).isRouter(msg.sender)) { // The assembly code is more direct than the Solidity version using `abi.decode`. assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) } } else { sender = msg.sender; } } }