diff --git a/src/Helios.sol b/src/Helios.sol index 0114376..c19e7fc 100644 --- a/src/Helios.sol +++ b/src/Helios.sol @@ -1,23 +1,40 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.19; -import {ERC6909} from "./utils/ERC6909.sol"; import {Math2} from "./libraries/Math2.sol"; +import {ERC6909} from "./utils/ERC6909.sol"; import {ReentrancyGuard} from "./utils/ReentrancyGuard.sol"; import {SafeTransferLib} from "./libraries/SafeTransferLib.sol"; -/// @dev Simple XYK Exchange for ERC20s. -/// LP shares are tokenized using ERC6909. +/// @notice Simple xyk-style exchange for ERC20 tokens. +/// LP shares are tokenized using the ERC6909 interface. +/// @author Modified from Uniswap V2 +/// (https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol) contract Helios is ERC6909, ReentrancyGuard { + /// ========================= LIBRARIES ========================= /// + + /// @dev Did the maths. using Math2 for uint224; + + /// @dev Safety library for ERC20. using SafeTransferLib for address; - uint256 internal constant MINIMUM_LIQUIDITY = 1000; - bytes4 internal constant SELECTOR = bytes4(keccak256(bytes("transfer(address,uint256)"))); + /// ========================= CONSTANTS ========================= /// + /// @dev Minimum liquidity to start pool. + uint256 internal constant MIN_LIQ = 1000; + + /// ========================== STORAGE ========================== /// + + /// @dev Pool swapping data mapping. mapping(uint256 => Pool) public pools; + + /// @dev Pool cumulative price mapping. mapping(uint256 => Price) public prices; + /// ========================== STRUCTS ========================== /// + + /// @dev Pool data. struct Pool { address token0; address token1; @@ -26,68 +43,20 @@ contract Helios is ERC6909, ReentrancyGuard { uint32 blockTimestampLast; } + /// @dev Price data. struct Price { uint256 price0CumulativeLast; uint256 price1CumulativeLast; uint256 kLast; } - constructor() payable {} - - /// @dev Update reserves and, on the first call per block, price accumulators. - function _update( - uint256 id, - uint256 balance0, - uint256 balance1, - uint112 _reserve0, - uint112 _reserve1 - ) internal virtual { - Pool storage pool = pools[id]; - Price storage price = prices[id]; - - require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "Helios: OVERFLOW"); - uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32); - unchecked { - uint32 timeElapsed = blockTimestamp - pool.blockTimestampLast; // Overflow is desired. - if (timeElapsed != 0 && _reserve0 != 0 && _reserve1 != 0) { - // * Never overflows, and + overflow is desired. - price.price0CumulativeLast += - uint256(Math2.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; - price.price1CumulativeLast += - uint256(Math2.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; - } - } - pool.reserve0 = uint112(balance0); - pool.reserve1 = uint112(balance1); - pool.blockTimestampLast = blockTimestamp; - } + /// ======================== CONSTRUCTOR ======================== /// - function _feeTo() public view virtual returns (address) {} + /// @dev Constructs + /// this implementation. + constructor() payable {} - /// @dev If fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k). - function _mintFee(uint256 id, uint112 _reserve0, uint112 _reserve1) - internal - virtual - returns (bool feeOn) - { - Price storage price = prices[id]; - address feeTo = _feeTo(); - feeOn = feeTo != address(0); - if (feeOn) { - if (price.kLast != 0) { - uint256 rootK = Math2.sqrt(uint256(_reserve0) * (_reserve1)); - uint256 rootKLast = Math2.sqrt(price.kLast); - if (rootK > rootKLast) { - uint256 numerator = totalSupply[id] * (rootK - rootKLast); - uint256 denominator = rootK * 5 + rootKLast; - uint256 liquidity = numerator / denominator; - if (liquidity != 0) _mint(feeTo, id, liquidity); - } - } - } else if (price.kLast != 0) { - price.kLast = 0; - } - } + /// ======================== MINT & BURN ======================== /// /// @dev This low-level function should be called from a contract which performs important safety checks. function mint(uint256 id, address to) public payable nonReentrant returns (uint256 liquidity) { @@ -103,10 +72,10 @@ contract Helios is ERC6909, ReentrancyGuard { uint256 amount1 = balance1 - _reserve1; bool feeOn = _mintFee(id, _reserve0, _reserve1); - uint256 _totalSupply = totalSupply[id]; // Gas savings, must be defined here since totalSupply can update in _mintFee. + uint256 _totalSupply = totalSupply[id]; // Gas savings, must be defined here since totalSupply can update in `_mintFee`. if (_totalSupply == 0) { - liquidity = Math2.sqrt((amount0 * amount1) - (MINIMUM_LIQUIDITY)); - _mint(address(0), id, MINIMUM_LIQUIDITY); // Permanently lock the first MINIMUM_LIQUIDITY tokens. + liquidity = Math2.sqrt((amount0 * amount1) - MIN_LIQ); + _mint(address(0), id, MIN_LIQ); // Permanently lock the first `MIN_LIQ` tokens. } else { liquidity = Math2.min(amount0 * _totalSupply / _reserve0, (amount1 * _totalSupply) / _reserve1); @@ -114,7 +83,7 @@ contract Helios is ERC6909, ReentrancyGuard { require(liquidity != 0, "Helios: INSUFFICIENT_LIQUIDITY_MINTED"); _mint(to, id, liquidity); _update(id, balance0, balance1, _reserve0, _reserve1); - if (feeOn) prices[id].kLast = uint256(pool.reserve0) * (pool.reserve1); // Reserve0 and reserve1 are up-to-date. + if (feeOn) prices[id].kLast = uint256(pool.reserve0) * (pool.reserve1); // `reserve0` and `reserve1` are up-to-date. } /// @dev This low-level function should be called from a contract which performs important safety checks. @@ -132,7 +101,7 @@ contract Helios is ERC6909, ReentrancyGuard { uint256 liquidity; bool feeOn = _mintFee(id, pool.reserve0, pool.reserve1); - uint256 _totalSupply = totalSupply[id]; // Gas savings, must be defined here since totalSupply can update in _mintFee. + uint256 _totalSupply = totalSupply[id]; // Gas savings, must be defined here since totalSupply can update in `_mintFee`. amount0 = liquidity * balance0 / _totalSupply; // Using balances ensures pro-rata distribution. amount1 = liquidity * balance1 / _totalSupply; // Using balances ensures pro-rata distribution. require(amount0 != 0 && amount1 != 0, "Helios: INSUFFICIENT_LIQUIDITY_BURNED"); @@ -142,9 +111,11 @@ contract Helios is ERC6909, ReentrancyGuard { balance0 = pool.token0.balanceOf(address(this)); balance1 = pool.token1.balanceOf(address(this)); _update(id, balance0, balance1, pool.reserve0, pool.reserve1); - if (feeOn) prices[id].kLast = uint256(pool.reserve0) * (pool.reserve1); // Reserve0 and reserve1 are up-to-date. + if (feeOn) prices[id].kLast = uint256(pool.reserve0) * (pool.reserve1); // `reserve0` and `reserve1` are up-to-date. } + /// ======================== SWAP ======================== /// + /// @dev This low-level function should be called from a contract which performs important safety checks. function swap( uint256 id, @@ -205,6 +176,83 @@ contract Helios is ERC6909, ReentrancyGuard { pool.reserve1 ); } + + /// ======================== INTERNAL ======================== /// + + function _feeTo() public view virtual returns (address) {} + + /// @dev Update reserves and, on the first call per block, price accumulators. + function _update( + uint256 id, + uint256 balance0, + uint256 balance1, + uint112 _reserve0, + uint112 _reserve1 + ) internal virtual { + Pool storage pool = pools[id]; + Price storage price = prices[id]; + + require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "Helios: OVERFLOW"); + uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32); + unchecked { + uint32 timeElapsed = blockTimestamp - pool.blockTimestampLast; // Overflow is desired. + if (timeElapsed != 0 && _reserve0 != 0 && _reserve1 != 0) { + // * Never overflows, and + overflow is desired. + price.price0CumulativeLast += + uint256(Math2.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; + price.price1CumulativeLast += + uint256(Math2.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; + } + } + pool.reserve0 = uint112(balance0); + pool.reserve1 = uint112(balance1); + pool.blockTimestampLast = blockTimestamp; + } + + /// @dev If fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k). + function _mintFee(uint256 id, uint112 _reserve0, uint112 _reserve1) + internal + virtual + returns (bool feeOn) + { + Price storage price = prices[id]; + address feeTo = _feeTo(); + feeOn = feeTo != address(0); + if (feeOn) { + if (price.kLast != 0) { + uint256 rootK = Math2.sqrt(uint256(_reserve0) * (_reserve1)); + uint256 rootKLast = Math2.sqrt(price.kLast); + if (rootK > rootKLast) { + uint256 numerator = totalSupply[id] * (rootK - rootKLast); + uint256 denominator = rootK * 5 + rootKLast; + uint256 liquidity = numerator / denominator; + if (liquidity != 0) _mint(feeTo, id, liquidity); + } + } + } else if (price.kLast != 0) { + price.kLast = 0; + } + } + + /// =================== EXTERNAL TOKEN HELPERS =================== /// + + /// @dev Returns the amount of ERC20 `token` owned by `account`. + /// Returns zero if the `token` does not exist. + function _balanceOf(address token, address account) internal view returns (uint256 amount) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x14, account) // Store the `account` argument. + mstore(0x00, 0x70a08231000000000000000000000000) // `balanceOf(address)`. + amount := + mul( + mload(0x20), + and( // The arguments of `and` are evaluated from right to left. + gt(returndatasize(), 0x1f), // At least 32 bytes returned. + staticcall(gas(), token, 0x10, 0x24, 0x20, 0x20) + ) + ) + } + } } /// @dev Simple external call interface for swaps.