diff --git a/src/ERC20External.sol b/src/ERC20External.sol new file mode 100644 index 0000000..2ed0606 --- /dev/null +++ b/src/ERC20External.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import {IERC20Errors} from "../lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Context} from "../lib/openzeppelin-contracts/contracts/utils/Context.sol"; +import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {ERC20Internal} from "./ERC20Internal.sol"; + +// Copied from OpenZeppelin's ERC20 implementation, but split into internal and external parts + +contract ERC20External is ERC20Internal, IERC20Metadata { + /** + * @dev Sets the values for {name} and {symbol}. + * + * Both values are immutable: they can only be set once during construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return 18; + } + + /// @inheritdoc IERC20 + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /// @inheritdoc IERC20 + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + return true; + } + + /// @inheritdoc IERC20 + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Skips emitting an {Approval} event indicating an allowance update. This is not + * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve]. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + return true; + } +} diff --git a/src/ERC20Internal.sol b/src/ERC20Internal.sol new file mode 100644 index 0000000..a42a488 --- /dev/null +++ b/src/ERC20Internal.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import {IERC20Errors} from "../lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Context} from "../lib/openzeppelin-contracts/contracts/utils/Context.sol"; + +// Copied from OpenZeppelin's ERC20 implementation, but split into internal and external parts + +abstract contract ERC20Internal is Context, IERC20Errors { + mapping(address account => uint256) internal _balances; + mapping(address account => mapping(address spender => uint256)) internal _allowances; + uint256 internal _totalSupply; + string internal _name; + string internal _symbol; + + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + + emit IERC20.Transfer(from, to, value); + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to + * true using the following override: + * + * ```solidity + * function _approve(address owner, address spender, uint256 value, bool) internal virtual override { + * super._approve(owner, spender, value, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowances[owner][spender] = value; + if (emitEvent) { + emit IERC20.Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner`'s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = _allowances[owner][spender]; + if (currentAllowance < type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } + +} diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index 746f524..f505858 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -82,8 +82,12 @@ interface IPartyPool is IERC20Metadata { /// @notice Address that will receive collected protocol tokens when collectProtocolFees() is called. function protocolFeeAddress() external view returns (address); - /// @notice Per-token protocol fee ledger accessor. Returns tokens owed (raw uint token units) for token index i. - function protocolFeesOwed(uint256) external view returns (uint256); + /// @notice Protocol fee ledger accessor. Returns tokens owed (raw uint token units) from this pool as protocol fees + /// that have not yet been transferred out. + function allProtocolFeesOwed() external view returns (uint256[] memory); + + /// @notice Callable by anyone, sends any owed protocol fees to the protocol fee address. + function collectProtocolFees() external; /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool. diff --git a/src/PartyPool.sol b/src/PartyPool.sol index cf13337..5927d0c 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -1,18 +1,19 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import "forge-std/console2.sol"; -import "@abdk/ABDKMath64x64.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; -import "./LMSRStabilized.sol"; -import "./LMSRStabilizedBalancedPair.sol"; -import "./IPartyPool.sol"; -import "./IPartyFlashCallback.sol"; -import "./PartyPoolBase.sol"; -import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "../lib/openzeppelin-contracts/contracts/utils/Address.sol"; +import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; +import {IPartyFlashCallback} from "./IPartyFlashCallback.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {LMSRStabilized} from "./LMSRStabilized.sol"; +import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol"; +import {PartyPoolBase} from "./PartyPoolBase.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; +import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; +import {ERC20External} from "./ERC20External.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. @@ -27,7 +28,7 @@ import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; /// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls. /// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool /// (i.e., floor outputs to users, ceil inputs/fees where appropriate). -contract PartyPool is PartyPoolBase, IPartyPool { +contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { using ABDKMath64x64 for int128; using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; @@ -54,6 +55,9 @@ contract PartyPool is PartyPoolBase, IPartyPool { address private immutable PROTOCOL_FEE_ADDRESS; function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; } + // @inheritdoc IPartyPool + function allProtocolFeesOwed() external view returns (uint256[] memory) { return protocolFeesOwed; } + /// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations. bool immutable private IS_STABLE_PAIR; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled @@ -101,7 +105,7 @@ contract PartyPool is PartyPoolBase, IPartyPool { bool stable_, PartyPoolSwapMintImpl swapMintImpl_, PartyPoolMintImpl mintImpl_ - ) PartyPoolBase(name_, symbol_) { + ) ERC20External(name_, symbol_) { require(tokens_.length > 1, "Pool: need >1 asset"); require(tokens_.length == bases_.length, "Pool: lengths mismatch"); tokens = tokens_; @@ -226,9 +230,6 @@ contract PartyPool is PartyPoolBase, IPartyPool { } - // Per-token owed protocol fees (raw token units). Public getter autogenerated. - uint256[] public protocolFeesOwed; - /// @notice Transfer all protocol fees to the configured protocolFeeAddress and zero the ledger. /// @dev Anyone can call; must have protocolFeeAddress != address(0) to be operational. function collectProtocolFees() external nonReentrant { @@ -411,7 +412,6 @@ contract PartyPool is PartyPoolBase, IPartyPool { // Compute internal amounts using LMSR (exact-input with price limit) // if _stablePair is true, use the optimized path - console2.log('stablepair optimization?', IS_STABLE_PAIR); (amountInInternalUsed, amountOutInternal) = IS_STABLE_PAIR ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice) : lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice); diff --git a/src/PartyPoolBase.sol b/src/PartyPoolBase.sol index a023888..4015558 100644 --- a/src/PartyPoolBase.sol +++ b/src/PartyPoolBase.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import "@abdk/ABDKMath64x64.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "./LMSRStabilized.sol"; +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; +import {ERC20Internal} from "./ERC20Internal.sol"; +import {LMSRStabilized} from "./LMSRStabilized.sol"; /// @notice Abstract base contract that contains storage and internal helpers only. /// No external/public functions or constructor here — derived implementations own immutables and constructors. -abstract contract PartyPoolBase is ERC20, ReentrancyGuard { +abstract contract PartyPoolBase is ERC20Internal, ReentrancyGuard { using ABDKMath64x64 for int128; using LMSRStabilized for LMSRStabilized.State; @@ -28,6 +28,10 @@ abstract contract PartyPoolBase is ERC20, ReentrancyGuard { /// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays. IERC20[] internal tokens; // effectively immutable since there is no interface to change the tokens + /// @notice Amounts of token owed as protocol fees but not yet collected. Subtract this amount from the pool's token + /// balances to compute the tokens owned by LP's. + uint256[] internal protocolFeesOwed; + /// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation. /// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals. uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal @@ -41,9 +45,6 @@ abstract contract PartyPoolBase is ERC20, ReentrancyGuard { uint256[] internal cachedUintBalances; - constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} - - /* ---------------------- Conversion & fee helpers (internal) ---------------------- */ diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index edb752f..c13fbd7 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -20,14 +20,12 @@ contract PartyPoolMintImpl is PartyPoolBase { event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted); event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned); - constructor() PartyPoolBase('','') {} - function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external returns (uint256 lpMinted) { uint256 n = tokens.length; // Check if this is initial deposit - revert if not - bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0; + bool isInitialDeposit = _totalSupply == 0 || lmsr.nAssets == 0; require(isInitialDeposit, "initialMint: pool already initialized"); // Update cached balances for all assets @@ -62,7 +60,7 @@ contract PartyPoolMintImpl is PartyPoolBase { uint256 n = tokens.length; // Check if this is NOT initial deposit - revert if it is - bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0; + bool isInitialDeposit = _totalSupply == 0 || lmsr.nAssets == 0; require(!isInitialDeposit, "mint: use initialMint for pool initialization"); require(lpTokenAmount > 0, "mint: zero LP amount"); @@ -71,7 +69,7 @@ contract PartyPoolMintImpl is PartyPoolBase { uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); // Calculate required deposit amounts for the desired LP tokens - uint256[] memory depositAmounts = mintAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances); + uint256[] memory depositAmounts = mintAmounts(lpTokenAmount, lmsr.nAssets, _totalSupply, cachedUintBalances); // Transfer in all token amounts for (uint i = 0; i < n; ) { @@ -104,7 +102,7 @@ contract PartyPoolMintImpl is PartyPoolBase { // Proportional issuance: totalSupply * delta / oldScaled if (delta > 0) { // floor truncation rounds in favor of the pool - actualLpToMint = (totalSupply() * delta) / oldScaled; + actualLpToMint = (_totalSupply * delta) / oldScaled; } else { actualLpToMint = 0; } @@ -135,10 +133,10 @@ contract PartyPoolMintImpl is PartyPoolBase { uint256 n = tokens.length; require(lpAmount > 0, "burn: zero lp"); - uint256 supply = totalSupply(); + uint256 supply = _totalSupply; require(supply > 0, "burn: empty supply"); require(lmsr.nAssets > 0, "burn: uninit pool"); - require(balanceOf(payer) >= lpAmount, "burn: insufficient LP"); + require(_balances[payer] >= lpAmount, "burn: insufficient LP"); // Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts for (uint i = 0; i < n; ) { @@ -148,7 +146,7 @@ contract PartyPoolMintImpl is PartyPoolBase { } // Compute proportional withdrawal amounts for the requested LP amount (rounded down) - withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, totalSupply(), cachedUintBalances); + withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, _totalSupply, cachedUintBalances); // Transfer underlying tokens out to receiver according to computed proportions for (uint i = 0; i < n; ) { @@ -185,7 +183,7 @@ contract PartyPoolMintImpl is PartyPoolBase { // Burn exactly the requested LP amount from payer (authorization via allowance) if (msg.sender != payer) { - uint256 allowed = allowance(payer, msg.sender); + uint256 allowed = _allowances[payer][msg.sender]; require(allowed >= lpAmount, "burn: allowance insufficient"); _approve(payer, msg.sender, allowed - lpAmount); } diff --git a/src/PartyPoolSwapMintImpl.sol b/src/PartyPoolSwapMintImpl.sol index c0168af..b821d98 100644 --- a/src/PartyPoolSwapMintImpl.sol +++ b/src/PartyPoolSwapMintImpl.sol @@ -21,7 +21,6 @@ contract PartyPoolSwapMintImpl is PartyPoolBase { event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted); event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned); - constructor() PartyPoolBase('','') {} /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal. @@ -85,7 +84,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase { uint256 actualLpToMint; // Use natural ERC20 function since base contract inherits from ERC20 - uint256 currentSupply = totalSupply(); + uint256 currentSupply = _totalSupply; if (currentSupply == 0) { // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled actualLpToMint = newScaled; @@ -259,9 +258,9 @@ contract PartyPoolSwapMintImpl is PartyPoolBase { require(lpAmount > 0, "burnSwap: zero lp"); require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline"); - uint256 supply = totalSupply(); + uint256 supply = _totalSupply; require(supply > 0, "burnSwap: empty supply"); - require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP"); + require(_balances[payer] >= lpAmount, "burnSwap: insufficient LP"); // alpha = lpAmount / supply as Q64.64 (adjusted for fee) int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn @@ -285,7 +284,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase { // Burn LP tokens from payer (authorization via allowance) if (msg.sender != payer) { - uint256 allowed = allowance(payer, msg.sender); + uint256 allowed = _allowances[payer][msg.sender]; require(allowed >= lpAmount, "burnSwap: allowance insufficient"); _approve(payer, msg.sender, allowed - lpAmount); }