ERC functionality split into internal/external

This commit is contained in:
tim
2025-10-03 12:48:45 -04:00
parent 269236cfba
commit 2e675bceb9
7 changed files with 343 additions and 43 deletions

123
src/ERC20External.sol Normal file
View File

@@ -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;
}
}

175
src/ERC20Internal.sol Normal file
View File

@@ -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);
}
}
}
}

View File

@@ -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.

View File

@@ -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);

View File

@@ -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)
---------------------- */

View File

@@ -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);
}

View File

@@ -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);
}