Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/NFTStrategyFactory.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import {Ownable} from "solady/auth/Ownable.sol";
import {NFTStrategy} from "./NFTStrategy.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IUniswapV4Router04} from "v4-router/interfaces/IUniswapV4Router04.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import "./Interfaces.sol";
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
import {LibClone} from "solady/utils/LibClone.sol";
/// @title NFTStrategyFactory - Factory for deploying NFTStrategy contracts
/// @author TokenWorks (https://token.works/)
/// @notice This factory deploys and manages NFTStrategy contracts with Uniswap V4 integration
/// @dev Uses ERC1967 proxy pattern for upgradeable NFTStrategy deployments
contract NFTStrategyFactory is Ownable, ReentrancyGuard {
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™ ™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™ ™™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTANTS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice ETH amount used for initial liquidity pairing
uint256 private constant ethToPair = 2 wei;
/// @notice Uniswap V4 Position Manager for liquidity operations
IPositionManager private immutable posm;
/// @notice Permit2 contract for token approvals
IAllowanceTransfer private immutable permit2;
/// @notice Uniswap V4 Router for token swaps
IUniswapV4Router04 private immutable router;
/// @notice Uniswap V4 Pool Manager for pool operations
IPoolManager private immutable poolManager;
/// @notice PunkStrategy token contract address
address public constant PNKSTR_ADDRESS = 0xc50673EDb3A7b94E8CAD8a7d4E0cD68864E33eDF;
/// @notice PunkStrategy hook contract address
address public constant PNKSTR_HOOK_ADDRESS = 0xfAaad5B731F52cDc9746F2414c823eca9B06E844;
/// @notice Dead address for burning tokens
address public constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;
/// @notice Previous factory contract address for migration checks
address public constant OLD_FACTORY = 0xA1a196b5BE89Be04a2c1dc71643689CE013c22e5;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* STATE VARIABLES */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Mapping of NFT collection addresses to their NFTStrategy contracts
mapping(address => address) public collectionToNFTStrategy;
/// @notice Mapping of NFTStrategy addresses to their collection contracts
mapping(address => address) public nftStrategyToCollection;
/// @notice Mapping of addresses that can launch strategies
mapping(address => bool) public launchers;
/// @notice The Uniswap V4 hook that controls the logic of new deployments
address public hookAddress;
/// @notice Gates the NFTStrategyHook to only when we're loading a new token
bool public loadingLiquidity;
/// @notice Controls upgradeability of new NFTStrategy contracts
bool public launchUpgradeable;
/// @notice The address to send deployment fees to
address public feeAddress;
/// @notice Implementation contract for NFTStrategy proxies
address public nftStrategyImplementation;
/// @notice TWAP increment when buying PNKSTR tokens
uint256 public twapIncrement = 1 ether;
/// @notice TWAP delay in blocks when buying PNKSTR tokens
uint256 public twapDelayInBlocks = 1;
/// @notice Last block number when TWAP was executed
uint256 public lastTwapBlock;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CUSTOM ERRORS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Hook address has not been set
error HookNotSet();
/// @notice Collection already has an NFTStrategy deployed
error CollectionAlreadyLaunched();
/// @notice Incorrect ETH amount sent with launch transaction
error WrongEthAmount();
/// @notice Contract does not implement ERC721 interface
error NotERC721();
/// @notice Launch is restricted to collection owner only
error GatedByCollectionOwner();
/// @notice Launching is currently disabled
error CannotLaunch();
/// @notice No ETH available for TWAP operations
error NoETHToTwap();
/// @notice Not enough blocks have passed since last TWAP
error TwapDelayNotMet();
/// @notice Buy increment is outside valid range
error InvalidIncrement();
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CUSTOM EVENTS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Event emitted when a new NFTStrategy instance is launched
/// @param collection The NFT collection address
/// @param nftStrategy The deployed NFTStrategy contract address
/// @param tokenName The name of the strategy token
/// @param tokenSymbol The symbol of the strategy token
event NFTStrategyLaunched(
address indexed collection, address indexed nftStrategy, string tokenName, string tokenSymbol
);
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTRUCTOR */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Constructor initializes the factory with required dependencies
/// @param _posm Uniswap V4 Position Manager address
/// @param _permit2 Permit2 contract address
/// @param _router Uniswap V4 Router address
/// @param _poolManager Uniswap V4 Pool Manager address
/// @param _feeAddress Address to receive deployment fees
/// @dev Sets up immutable references and creates NFTStrategy implementation
constructor(address _posm, address _permit2, address payable _router, address _poolManager, address _feeAddress) {
posm = IPositionManager(_posm);
permit2 = IAllowanceTransfer(_permit2);
router = IUniswapV4Router04(_router);
poolManager = IPoolManager(_poolManager);
feeAddress = _feeAddress;
// Enable upgradeability by default
launchUpgradeable = true;
nftStrategyImplementation = LibClone.clone(address(new NFTStrategy()));
_initializeOwner(msg.sender);
}
/// @notice Restricts function access to authorized launchers or owner
modifier onlyLauncher() {
if (!launchers[msg.sender] && msg.sender != Ownable.owner()) revert Unauthorized();
_;
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* ADMIN FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Sets the implementation of the NFTStrategy contract
/// @param _nftStrategyImplementation Address of the NFTStrategy implementation contract
/// @dev Only callable by owner
function setNftStrategyImplementation(address _nftStrategyImplementation) external onlyOwner {
nftStrategyImplementation = _nftStrategyImplementation;
}
/// @notice Disables the upgradeability of new NFTStrategy launches
/// @dev Only callable by owner
function disableLaunchUpgradeable() external onlyOwner {
launchUpgradeable = false;
}
/// @notice Deploys a new upgradeable NFTStrategy contract
/// @param _collection The NFT collection address
/// @param _hook The hook contract address
/// @param _tokenName The name of the strategy token
/// @param _tokenSymbol The symbol of the strategy token
/// @param _buyIncrement The buy increment for price calculations
/// @param _owner The owner of the NFTStrategy contract
/// @return proxy The deployed NFTStrategy proxy contract
/// @dev Uses ERC1967 proxy pattern with packed immutable args
function _deployUpgradeableNFTStrategy(
address _collection,
address _hook,
string memory _tokenName,
string memory _tokenSymbol,
uint256 _buyIncrement,
address _owner
) internal returns (NFTStrategy proxy) {
bytes memory args = abi.encodePacked(address(this), IUniswapV4Router04(router), IPoolManager(poolManager));
proxy = NFTStrategy(payable(LibClone.deployERC1967(address(nftStrategyImplementation), args)));
proxy.initialize(
_collection, _hook, _tokenName, _tokenSymbol, _buyIncrement, launchUpgradeable ? _owner : address(0)
);
}
/// @notice Updates the hook attached to new NFTStrategy pools
/// @param _hookAddress New Uniswap v4 hook address
/// @dev Only callable by owner
function updateHookAddress(address _hookAddress) external onlyOwner {
hookAddress = _hookAddress;
}
/// @notice Updates whether an address is authorized as a launcher
/// @param _launcher Address to update launcher status for
/// @param _authorized Whether the address should be authorized as a launcher
/// @dev Only callable by owner
function updateLauncher(address _launcher, bool _authorized) external onlyOwner {
launchers[_launcher] = _authorized;
}
/// @notice Updates the name of a specific NFTStrategy token
/// @param nftStrategy Address of the NFTStrategy contract
/// @param tokenName New name for the token
function updateTokenName(address nftStrategy, string memory tokenName) external onlyOwner {
INFTStrategy(nftStrategy).updateName(tokenName);
}
/// @notice Updates the symbol of a specific NFTStrategy token
/// @param nftStrategy Address of the NFTStrategy contract
/// @param tokenSymbol New symbol for the token
function updateTokenSymbol(address nftStrategy, string memory tokenSymbol) external onlyOwner {
INFTStrategy(nftStrategy).updateSymbol(tokenSymbol);
}
/// @notice Updates the price multiplier for a specific NFTStrategy
/// @param nftStrategy Address of the NFTStrategy contract
/// @param newMultiplier New multiplier in basis points (1100 = 1.1x)
function updatePriceMultiplier(address nftStrategy, uint256 newMultiplier) external onlyOwner {
INFTStrategy(nftStrategy).setPriceMultiplier(newMultiplier);
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* INTERNAL FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Internal function to load initial liquidity into the Uniswap V4 pool
/// @param _token Address of the NFTStrategy ERC20 token
/// @dev Creates pool, initializes with starting price, and adds liquidity
function _loadLiquidity(address _token) internal {
loadingLiquidity = true;
// Create the pool with ETH (currency0) and TOKEN (currency1)
Currency currency0 = Currency.wrap(address(0)); // ETH
Currency currency1 = Currency.wrap(_token); // NFTStrategy Token
uint24 lpFee = 0;
int24 tickSpacing = 60;
uint256 token0Amount = 1; // 1 wei
uint256 token1Amount = 1_000_000_000 * 10 ** 18; // 1B TOKEN
// 10e18 ETH = 1_000_000_000e18 TOKEN
uint160 startingPrice = 501082896750095888663770159906816;
int24 tickLower = TickMath.minUsableTick(tickSpacing);
int24 tickUpper = int24(175020);
PoolKey memory key = PoolKey(currency0, currency1, lpFee, tickSpacing, IHooks(hookAddress));
bytes memory hookData = new bytes(0);
// Hardcoded from LiquidityAmounts.getLiquidityForAmounts
uint128 liquidity = 158372218983990412488087;
uint256 amount0Max = token0Amount + 1 wei;
uint256 amount1Max = token1Amount + 1 wei;
(bytes memory actions, bytes[] memory mintParams) =
_mintLiquidityParams(key, tickLower, tickUpper, liquidity, amount0Max, amount1Max, DEAD_ADDRESS, hookData);
bytes[] memory params = new bytes[](2);
params[0] = abi.encodeWithSelector(posm.initializePool.selector, key, startingPrice, hookData);
params[1] = abi.encodeWithSelector(
posm.modifyLiquidities.selector, abi.encode(actions, mintParams), block.timestamp + 60
);
uint256 valueToPass = amount0Max;
permit2.approve(_token, address(posm), type(uint160).max, type(uint48).max);
posm.multicall{value: valueToPass}(params);
loadingLiquidity = false;
}
/// @notice Creates parameters for minting liquidity in Uniswap V4
/// @param poolKey The pool key for the liquidity position
/// @param _tickLower Lower tick boundary
/// @param _tickUpper Upper tick boundary
/// @param liquidity Amount of liquidity to mint
/// @param amount0Max Maximum amount of token0 to use
/// @param amount1Max Maximum amount of token1 to use
/// @param recipient Address to receive the liquidity position
/// @param hookData Additional data for hooks
/// @return Encoded actions and parameters for position manager
/// @dev Internal helper for liquidity operations
function _mintLiquidityParams(
PoolKey memory poolKey,
int24 _tickLower,
int24 _tickUpper,
uint256 liquidity,
uint256 amount0Max,
uint256 amount1Max,
address recipient,
bytes memory hookData
) internal pure returns (bytes memory, bytes[] memory) {
bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));
bytes[] memory params = new bytes[](2);
params[0] = abi.encode(poolKey, _tickLower, _tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData);
params[1] = abi.encode(poolKey.currency0, poolKey.currency1);
return (actions, params);
}
/// @notice Buys PNKSTR tokens with ETH and burns them by sending to dead address
/// @param amountIn The amount of ETH to spend on PNKSTR tokens
/// @dev Uses PunkStrategy pool to swap ETH for PNKSTR and burn
function _buyAndBurnPNKSTR(uint256 amountIn) internal {
PoolKey memory key =
PoolKey(Currency.wrap(address(0)), Currency.wrap(PNKSTR_ADDRESS), 0, 60, IHooks(PNKSTR_HOOK_ADDRESS));
router.swapExactTokensForTokens{value: amountIn}(amountIn, 0, true, key, "", DEAD_ADDRESS, block.timestamp);
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* USER FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Launches a new NFTStrategy contract with owner permissions
/// @param collection Address of the NFT collection contract
/// @param tokenName Name of the strategy token
/// @param tokenSymbol Symbol of the strategy token
/// @param collectionOwner Address that will receive fees from the strategy
/// @param buyIncrement The buy increment for price calculations
/// @return The deployed NFTStrategy contract
/// @dev Only callable by contract owner, deploys NFTStrategy and initializes liquidity
function ownerLaunchNFTStrategy(
address collection,
string memory tokenName,
string memory tokenSymbol,
address collectionOwner,
uint256 buyIncrement
) external payable onlyLauncher returns (NFTStrategy) {
// Validate the parameters passed
if (hookAddress == address(0)) revert HookNotSet();
if (checkIfAlreadyLaunched(collection)) revert CollectionAlreadyLaunched();
if (buyIncrement < 0.01 ether || buyIncrement > 0.1 ether) {
revert InvalidIncrement();
}
if (!IERC721(collection).supportsInterface(0x80ac58cd)) revert NotERC721();
NFTStrategy nftStrategy =
_deployUpgradeableNFTStrategy(collection, hookAddress, tokenName, tokenSymbol, buyIncrement, owner());
collectionToNFTStrategy[collection] = address(nftStrategy);
nftStrategyToCollection[address(nftStrategy)] = collection;
// Costs 2 wei
_loadLiquidity(address(nftStrategy));
// Set fees to collectionOwner
INFTStrategyHook(hookAddress).adminUpdateFeeAddress(address(nftStrategy), collectionOwner);
emit NFTStrategyLaunched(collection, address(nftStrategy), tokenName, tokenSymbol);
return nftStrategy;
}
/// @notice Processes PNKSTR token buyback using TWAP mechanism
/// @dev Can be called once every twapDelayInBlocks, caller receives 0.5% reward
/// @dev Uses contract's ETH balance to buy and burn PNKSTR tokens
function processTokenTwap() external nonReentrant {
uint256 balance = address(this).balance;
if (balance == 0) revert NoETHToTwap();
// Check if enough blocks have passed since last TWAP
if (block.number < lastTwapBlock + twapDelayInBlocks) revert TwapDelayNotMet();
// Calculate amount to burn - either twapIncrement or remaining ethToTwap
uint256 burnAmount = twapIncrement;
if (balance < twapIncrement) {
burnAmount = balance;
}
// Set reward to 0.5% of burnAmount
uint256 reward = (burnAmount * 5) / 1000;
burnAmount -= reward;
// Update state
lastTwapBlock = block.number;
_buyAndBurnPNKSTR(burnAmount);
// Send reward to caller
SafeTransferLib.forceSafeTransferETH(msg.sender, reward);
}
/// @notice Checks if a collection already has a strategy launched
/// @param collection The address of the NFT collection to check
/// @return True if collection already has a strategy, false otherwise
/// @dev Checks both current factory and old factory for existing strategies
function checkIfAlreadyLaunched(address collection) public view returns (bool) {
return collectionToNFTStrategy[collection] != address(0)
|| INFTStrategyFactory(OLD_FACTORY).collectionToNFTStrategy(collection) != address(0);
}
/// @notice Allows the contract to receive ETH for twap
receive() external payable {}
}
"
},
"lib/solady/src/auth/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Simple single owner authorization mixin.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/Ownable.sol)
///
/// @dev Note:
/// This implementation does NOT auto-initialize the owner to `msg.sender`.
/// You MUST call the `_initializeOwner` in the constructor / initializer.
///
/// While the ownable portion follows
/// [EIP-173](https://eips.ethereum.org/EIPS/eip-173) for compatibility,
/// the nomenclature for the 2-step ownership handover may be unique to this codebase.
abstract contract Ownable {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The caller is not authorized to call the function.
error Unauthorized();
/// @dev The `newOwner` cannot be the zero address.
error NewOwnerIsZeroAddress();
/// @dev The `pendingOwner` does not have a valid handover request.
error NoHandoverRequest();
/// @dev Cannot double-initialize.
error AlreadyInitialized();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EVENTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The ownership is transferred from `oldOwner` to `newOwner`.
/// This event is intentionally kept the same as OpenZeppelin's Ownable to be
/// compatible with indexers and [EIP-173](https://eips.ethereum.org/EIPS/eip-173),
/// despite it not being as lightweight as a single argument event.
event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);
/// @dev An ownership handover to `pendingOwner` has been requested.
event OwnershipHandoverRequested(address indexed pendingOwner);
/// @dev The ownership handover to `pendingOwner` has been canceled.
event OwnershipHandoverCanceled(address indexed pendingOwner);
/// @dev `keccak256(bytes("OwnershipTransferred(address,address)"))`.
uint256 private constant _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE =
0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0;
/// @dev `keccak256(bytes("OwnershipHandoverRequested(address)"))`.
uint256 private constant _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE =
0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d;
/// @dev `keccak256(bytes("OwnershipHandoverCanceled(address)"))`.
uint256 private constant _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE =
0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The owner slot is given by:
/// `bytes32(~uint256(uint32(bytes4(keccak256("_OWNER_SLOT_NOT")))))`.
/// It is intentionally chosen to be a high value
/// to avoid collision with lower slots.
/// The choice of manual storage layout is to enable compatibility
/// with both regular and upgradeable contracts.
bytes32 internal constant _OWNER_SLOT =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff74873927;
/// The ownership handover slot of `newOwner` is given by:
/// ```
/// mstore(0x00, or(shl(96, user), _HANDOVER_SLOT_SEED))
/// let handoverSlot := keccak256(0x00, 0x20)
/// ```
/// It stores the expiry timestamp of the two-step ownership handover.
uint256 private constant _HANDOVER_SLOT_SEED = 0x389a75e1;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Override to return true to make `_initializeOwner` prevent double-initialization.
function _guardInitializeOwner() internal pure virtual returns (bool guard) {}
/// @dev Initializes the owner directly without authorization guard.
/// This function must be called upon initialization,
/// regardless of whether the contract is upgradeable or not.
/// This is to enable generalization to both regular and upgradeable contracts,
/// and to save gas in case the initial owner is not the caller.
/// For performance reasons, this function will not check if there
/// is an existing owner.
function _initializeOwner(address newOwner) internal virtual {
if (_guardInitializeOwner()) {
/// @solidity memory-safe-assembly
assembly {
let ownerSlot := _OWNER_SLOT
if sload(ownerSlot) {
mstore(0x00, 0x0dc149f0) // `AlreadyInitialized()`.
revert(0x1c, 0x04)
}
// Clean the upper 96 bits.
newOwner := shr(96, shl(96, newOwner))
// Store the new value.
sstore(ownerSlot, or(newOwner, shl(255, iszero(newOwner))))
// Emit the {OwnershipTransferred} event.
log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
}
} else {
/// @solidity memory-safe-assembly
assembly {
// Clean the upper 96 bits.
newOwner := shr(96, shl(96, newOwner))
// Store the new value.
sstore(_OWNER_SLOT, newOwner)
// Emit the {OwnershipTransferred} event.
log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
}
}
}
/// @dev Sets the owner directly without authorization guard.
function _setOwner(address newOwner) internal virtual {
if (_guardInitializeOwner()) {
/// @solidity memory-safe-assembly
assembly {
let ownerSlot := _OWNER_SLOT
// Clean the upper 96 bits.
newOwner := shr(96, shl(96, newOwner))
// Emit the {OwnershipTransferred} event.
log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
// Store the new value.
sstore(ownerSlot, or(newOwner, shl(255, iszero(newOwner))))
}
} else {
/// @solidity memory-safe-assembly
assembly {
let ownerSlot := _OWNER_SLOT
// Clean the upper 96 bits.
newOwner := shr(96, shl(96, newOwner))
// Emit the {OwnershipTransferred} event.
log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
// Store the new value.
sstore(ownerSlot, newOwner)
}
}
}
/// @dev Throws if the sender is not the owner.
function _checkOwner() internal view virtual {
/// @solidity memory-safe-assembly
assembly {
// If the caller is not the stored owner, revert.
if iszero(eq(caller(), sload(_OWNER_SLOT))) {
mstore(0x00, 0x82b42900) // `Unauthorized()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Returns how long a two-step ownership handover is valid for in seconds.
/// Override to return a different value if needed.
/// Made internal to conserve bytecode. Wrap it in a public function if needed.
function _ownershipHandoverValidFor() internal view virtual returns (uint64) {
return 48 * 3600;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PUBLIC UPDATE FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Allows the owner to transfer the ownership to `newOwner`.
function transferOwnership(address newOwner) public payable virtual onlyOwner {
/// @solidity memory-safe-assembly
assembly {
if iszero(shl(96, newOwner)) {
mstore(0x00, 0x7448fbae) // `NewOwnerIsZeroAddress()`.
revert(0x1c, 0x04)
}
}
_setOwner(newOwner);
}
/// @dev Allows the owner to renounce their ownership.
function renounceOwnership() public payable virtual onlyOwner {
_setOwner(address(0));
}
/// @dev Request a two-step ownership handover to the caller.
/// The request will automatically expire in 48 hours (172800 seconds) by default.
function requestOwnershipHandover() public payable virtual {
unchecked {
uint256 expires = block.timestamp + _ownershipHandoverValidFor();
/// @solidity memory-safe-assembly
assembly {
// Compute and set the handover slot to `expires`.
mstore(0x0c, _HANDOVER_SLOT_SEED)
mstore(0x00, caller())
sstore(keccak256(0x0c, 0x20), expires)
// Emit the {OwnershipHandoverRequested} event.
log2(0, 0, _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE, caller())
}
}
}
/// @dev Cancels the two-step ownership handover to the caller, if any.
function cancelOwnershipHandover() public payable virtual {
/// @solidity memory-safe-assembly
assembly {
// Compute and set the handover slot to 0.
mstore(0x0c, _HANDOVER_SLOT_SEED)
mstore(0x00, caller())
sstore(keccak256(0x0c, 0x20), 0)
// Emit the {OwnershipHandoverCanceled} event.
log2(0, 0, _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE, caller())
}
}
/// @dev Allows the owner to complete the two-step ownership handover to `pendingOwner`.
/// Reverts if there is no existing ownership handover requested by `pendingOwner`.
function completeOwnershipHandover(address pendingOwner) public payable virtual onlyOwner {
/// @solidity memory-safe-assembly
assembly {
// Compute and set the handover slot to 0.
mstore(0x0c, _HANDOVER_SLOT_SEED)
mstore(0x00, pendingOwner)
let handoverSlot := keccak256(0x0c, 0x20)
// If the handover does not exist, or has expired.
if gt(timestamp(), sload(handoverSlot)) {
mstore(0x00, 0x6f5e8818) // `NoHandoverRequest()`.
revert(0x1c, 0x04)
}
// Set the handover slot to 0.
sstore(handoverSlot, 0)
}
_setOwner(pendingOwner);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PUBLIC READ FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns the owner of the contract.
function owner() public view virtual returns (address result) {
/// @solidity memory-safe-assembly
assembly {
result := sload(_OWNER_SLOT)
}
}
/// @dev Returns the expiry timestamp for the two-step ownership handover to `pendingOwner`.
function ownershipHandoverExpiresAt(address pendingOwner)
public
view
virtual
returns (uint256 result)
{
/// @solidity memory-safe-assembly
assembly {
// Compute the handover slot.
mstore(0x0c, _HANDOVER_SLOT_SEED)
mstore(0x00, pendingOwner)
// Load the handover slot.
result := sload(keccak256(0x0c, 0x20))
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MODIFIERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Marks a function as only callable by the owner.
modifier onlyOwner() virtual {
_checkOwner();
_;
}
}
"
},
"src/NFTStrategy.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {ERC20} from "solady/tokens/ERC20.sol";
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {IUniswapV4Router04} from "v4-router/interfaces/IUniswapV4Router04.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import "./Interfaces.sol";
import {Initializable} from "solady/utils/Initializable.sol";
import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol";
import {Ownable} from "solady/auth/Ownable.sol";
import {LibClone} from "solady/utils/LibClone.sol";
/// @title NFTStrategy - An ERC20 token that constantly churns NFTs from a collection
/// @author TokenWorks (https://token.works/)
/// @notice This contract implements an ERC20 token backed by NFTs from a specific collection.
/// Users can trade the token on Uniswap V4, and the contract uses trading fees to buy and sell NFTs.
/// @dev Uses ERC1967 proxy pattern with immutable args for gas-efficient upgrades
contract NFTStrategy is Initializable, UUPSUpgradeable, Ownable, ReentrancyGuard, ERC20 {
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™ ™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ ™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™™™ ™™™™™™™™™™™™™™ */
/* ™™™™™™™™™™™ ™™™™™™™™™™™™ ™™™™™™™™™™™™ */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTANTS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice The name of the ERC20 token
string tokenName;
/// @notice The symbol of the ERC20 token
string tokenSymbol;
/// @notice Address of the Uniswap V4 hook contract
address public hookAddress;
/// @notice The NFT collection this strategy is tied to
IERC721 public collection;
/// @notice Maximum token supply (1 billion tokens)
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 1e18;
/// @notice Dead address for burning tokens
address public constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;
/// @notice Contract version for upgrade tracking
uint256 public constant VERSION = 1;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* STATE VARIABLES */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Multiplier for NFT resale price (in basis points, e.g., 1200 = 1.2x)
uint256 public priceMultiplier;
/// @notice Mapping of NFT token IDs to their sale prices
mapping(uint256 => uint256) public nftForSale;
/// @notice Current accumulated fees available for NFT purchases
uint256 public currentFees;
/// @notice ETH accumulated from NFT sales, waiting to be used for token buyback
uint256 public ethToTwap;
/// @notice Amount of ETH to use per TWAP buyback operation
uint256 public twapIncrement;
/// @notice Number of blocks to wait between TWAP operations
uint256 public twapDelayInBlocks;
/// @notice Block number of the last TWAP operation
uint256 public lastTwapBlock;
/// @notice Block number when this contract was deployed
uint256 public deployBlock;
/// @notice ETH amount increment for maximum buy price calculation
uint256 public buyIncrement;
/// @notice Storage gap for future upgrades (prevents storage collisions)
uint256[50] private __gap;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CUSTOM EVENTS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Emitted when the protocol buys an NFT
event NFTBoughtByProtocol(uint256 indexed tokenId, uint256 purchasePrice, uint256 listPrice);
/// @notice Emitted when the protocol sells an NFT
event NFTSoldByProtocol(uint256 indexed tokenId, uint256 price, address buyer);
/// @notice Emitted when transfer allowance is increased by the hook
event AllowanceIncreased(uint256 amount);
/// @notice Emitted when transfer allowance is spent
event AllowanceSpent(address indexed from, address indexed to, uint256 amount);
/// @notice Emitted when the contract implementation is upgraded
event ContractUpgraded(address indexed oldImplementation, address indexed newImplementation, uint256 version);
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CUSTOM ERRORS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice NFT is not currently for sale
error NFTNotForSale();
/// @notice Sent ETH amount is less than the NFT sale price
error NFTPriceTooLow();
/// @notice Contract doesn't have enough ETH balance
error InsufficientContractBalance();
/// @notice Price multiplier is outside valid range
error InvalidMultiplier();
/// @notice No ETH available for TWAP operations
error NoETHToTwap();
/// @notice Not enough blocks have passed since last TWAP
error TwapDelayNotMet();
/// @notice Not enough ETH in fees to make purchase
error NotEnoughEth();
/// @notice Purchase price exceeds time-based maximum
error PriceTooHigh();
/// @notice Caller is not the factory contract
error NotFactory();
/// @notice Contract already owns this NFT
error AlreadyNFTOwner();
/// @notice External call didn't result in NFT acquisition
error NeedToBuyNFT();
/// @notice Contract doesn't own the specified NFT
error NotNFTOwner();
/// @notice Caller is not the authorized hook contract
error OnlyHook();
/// @notice Invalid NFT collection address
error InvalidCollection();
/// @notice External call to marketplace failed
error ExternalCallFailed(bytes reason);
/// @notice Invalid target address for external call
error InvalidTarget();
/// @notice Token transfer not authorized
error InvalidTransfer();
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTRUCTOR */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Constructor disables initializers to prevent implementation contract initialization
/// @dev This is required for the proxy pattern to work correctly
constructor() {
_disableInitializers();
}
/// @notice Initializes the contract with required addresses and permissions
/// @param _collection Address of the NFT collection contract
/// @param _hook Address of the NFTStrategyHook contract
/// @param _tokenName Name of the token
/// @param _tokenSymbol Symbol of the token
/// @param _buyIncrement Buy increment for the token
/// @param _owner Owner of the contract
function initialize(
address _collection,
address _hook,
string memory _tokenName,
string memory _tokenSymbol,
uint256 _buyIncrement,
address _owner
) external initializer {
require(_collection != address(0), "Invalid collection");
require(bytes(_tokenName).length > 0, "Empty name");
require(bytes(_tokenSymbol).length > 0, "Empty symbol");
collection = IERC721(_collection);
hookAddress = _hook;
tokenName = _tokenName;
tokenSymbol = _tokenSymbol;
deployBlock = block.number;
buyIncrement = _buyIncrement;
// Initialize owner without validation in-case we want to disable upgradeability
_initializeOwner(_owner);
// Initialize state variables that have default values
priceMultiplier = 1200; // 1.2x
twapIncrement = 1 ether;
twapDelayInBlocks = 1;
_mint(factory(), MAX_SUPPLY);
}
/// @notice Restricts function access to the factory contract only
modifier onlyFactory() {
if (msg.sender != factory()) revert NotFactory();
_;
}
/// @notice Authorizes contract upgrades (UUPS pattern)
/// @param newImplementation Address of the new implementation contract
/// @dev Only callable by contract owner, validates implementation is a contract
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
require(newImplementation != address(0), "Invalid implementation");
require(newImplementation.code.length > 0, "Implementation must be contract");
emit ContractUpgraded(address(this), newImplementation, VERSION);
}
/// @notice Returns the current implementation address
/// @return result The address of the current implementation contract
/// @dev Reads from the ERC1967 implementation slot
function getImplementation() external view returns (address result) {
assembly {
result := sload(_ERC1967_IMPLEMENTATION_SLOT)
}
}
/// @notice Returns the name of the token
/// @return The token name as a string
function name() public view override returns (string memory) {
return tokenName;
}
/// @notice Returns the symbol of the token
/// @return The token symbol as a string
function symbol() public view override returns (string memory) {
return tokenSymbol;
}
/// @notice Updates the hook address
/// @dev Can only be called by the owner
/// @param _hookAddress New hook address
function updateHookAddress(address _hookAddress) external onlyOwner {
hookAddress = _hookAddress;
}
/// @notice Updates the name of the token
/// @dev Can only be called by the factory
/// @param _tokenName New name for the token
function updateName(string memory _tokenName) external onlyFactory {
tokenName = _tokenName;
}
/// @notice Updates the symbol of the token
/// @dev Can only be called by the factory
/// @param _tokenSymbol New symbol for the token
function updateSymbol(string memory _tokenSymbol) external onlyFactory {
tokenSymbol = _tokenSymbol;
}
/// @notice Updates the price multiplier for relisting punks
/// @param _newMultiplier New multiplier in basis points (1100 = 1.1x, 10000 = 10.0x)
/// @dev Only callable by factory. Must be between 1.1x (1100) and 10.0x (10000)
function setPriceMultiplier(uint256 _newMultiplier) external onlyFactory {
if (_newMultiplier < 1100 || _newMultiplier > 10000) revert InvalidMultiplier();
priceMultiplier = _newMultiplier;
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* MECHANISM FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Returns the maximum price allowed for buying an NFT, increasing over time
/// @return The maximum price in ETH (wei) that can be used for buying
/// @dev Increases by buyIncrement every 5 blocks from deployment
function getMaxPriceForBuy() public view returns (uint256) {
// Calculate number of 5-block periods since deployment
uint256 blocksSinceDeployment = block.number - deployBlock;
uint256 periods = blocksSinceDeployment / 5;
// Return buyIncrement for each period, starting at 1 period minimum
return (periods + 1) * buyIncrement;
}
/// @notice Allows the hook to deposit trading fees into the contract
/// @dev Only callable by the authorized hook contract, uses msg.value for fee amount
function addFees() external payable {
if (msg.sender != hookAddress) revert OnlyHook();
currentFees += msg.value;
}
/// @notice Increases the transient transfer allowance for pool operations
/// @param amountAllowed Amount to add to the current allowance
/// @dev Only callable by the hook contract, uses transient storage
function increaseTransferAllowance(uint256 amountAllowed) external {
if (msg.sender != hookAddress) revert OnlyHook();
uint256 currentAllowance = getTransferAllowance();
assembly {
tstore(0, add(currentAllowance, amountAllowed))
}
emit AllowanceIncreased(amountAllowed);
}
/// @notice Buys a specific NFT using accumulated fees
/// @param value Amount of ETH to spend on the NFT purchase
/// @param data Calldata for the external marketplace call
/// @param expectedId The token ID expected to be acquired
/// @param target The marketplace contract to call
/// @dev Protected against reentrancy, validates NFT acquisition
function buyTargetNFT(uint256 value, bytes calldata data, uint256 expectedId, address target)
external
nonReentrant
{
// Store both balance and nft amount before calling external
uint256 ethBalanceBefore = address(this).balance;
uint256 nftBalanceBefore = collection.balanceOf(address(this));
// Make sure we are not owner of the expected id
if (collection.ownerOf(expectedId) == address(this)) {
revert AlreadyNFTOwner();
}
// Ensure value is not more than currentFees
if (value > currentFees) {
revert NotEnoughEth();
}
// Ensure value doesn't exceed the time-based maximum price
if (value > getMaxPriceForBuy()) {
revert PriceTooHigh();
}
// Ensure target is not the collection itself
if (target == address(collection)) revert InvalidTarget();
// Call external
(bool success, bytes memory reason) = target.call{value: value}(data);
if (!success) {
revert ExternalCallFailed(reason);
}
// Ensure we now have one more NFT
uint256 nftBalanceAfter = collection.balanceOf(address(this));
if (nftBalanceAfter != nftBalanceBefore + 1) {
revert NeedToBuyNFT();
}
// Ensure we are now owner of expectedId
if (collection.ownerOf(expectedId) != address(this)) {
revert NotNFTOwner();
}
// Calculate actual cost of the NFT to base new price on
uint256 cost = ethBalanceBefore - address(this).balance;
currentFees -= cost;
// List NFT for sale at priceMultiplier times the cost
uint256 salePrice = cost * priceMultiplier / 1000;
nftForSale[expectedId] = salePrice;
emit NFTBoughtByProtocol(expectedId, cost, salePrice);
}
/// @notice Sells an NFT owned by the contract for the listed price
/// @param tokenId The ID of the NFT to sell
function sellTargetNFT(uint256 tokenId) external payable nonReentrant {
// Get sale price
uint256 salePrice = nftForSale[tokenId];
// Verify NFT is for sale
if (salePrice == 0) revert NFTNotForSale();
// Verify sent ETH matches sale price
if (msg.value != salePrice) revert NFTPriceTooLow();
// Verify contract owns the NFT
if (collection.ownerOf(tokenId) != address(this)) revert NotNFTOwner();
// Transfer NFT to buyer
collection.transferFrom(address(this), msg.sender, tokenId);
// Remove NFT from sale
delete nftForSale[tokenId];
// Add sale price to fees
ethToTwap += salePrice;
emit NFTSoldByProtocol(tokenId, salePrice, msg.sender);
}
/// @notice Processes token buyback using TWAP mechanism
/// @dev Can be called once every twapDelayInBlocks, uses ethToTwap for buyback
/// @dev Caller receives 0.5% reward, remaining ETH is used to buy and burn tokens
function processTokenTwap() external nonReentrant {
if (ethToTwap == 0) revert NoETHToTwap();
// Check if enough blocks have passed since last TWAP
if (block.number < lastTwapBlock + twapDelayInBlocks) revert TwapDelayNotMet();
// Calculate amount to burn - either twapIncrement or remaining ethToTwap
uint256 burnAmount = twapIncrement;
if (ethToTwap < twapIncrement) {
burnAmount = ethToTwap;
}
// Set reward to 0.5% of burnAmount
uint256 reward = (burnAmount * 5) / 1000;
burnAmount -= reward;
// Update state
ethToTwap -= burnAmount + reward;
lastTwapBlock = block.number;
_buyAndBurnTokens(burnAmount);
// Send reward to caller
SafeTransferLib.forceSafeTransferETH(msg.sender, reward);
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* INTERNAL FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Buys tokens with ETH and burns them by sending to dead address
/// @param amountIn The amount of ETH to spend on tokens that will be burned
/// @dev Creates a pool key and swaps ETH for tokens, sending tokens to dead address
function _buyAndBurnTokens(uint256 amountIn) internal {
PoolKey memory key =
PoolKey(Currency.wrap(address(0)), Currency.wrap(address(this)), 0, 60, IHooks(hookAddress));
router().swapExactTokensForTokens{value: amountIn}(amountIn, 0, true, key, "", DEAD_ADDRESS, block.timestamp);
}
/// @notice Validates token transfers using a transient allowance system
/// @param from The address sending tokens
/// @param to The address receiving tokens
/// @param amount The amount of tokens being transferred
/// @dev Reverts if transfer isn't through the hook
function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
// On strategy launch, we need to allow for supply mint transfer
if (from == address(0)) {
return;
}
// Transfers to and from the poolManager require a transient allowance thats set by the hook
if ((from == address(poolManager()) || to == address(poolManager()))) {
uint256 transferAllowance = getTransferAllowance();
require(transferAllowance >= amount, InvalidTransfer());
assembly {
let newAllowance := sub(transferAllowance, amount)
tstore(0, newAllowance)
}
emit AllowanceSpent(from, to, amount);
return;
}
revert InvalidTransfer();
}
/// @notice Gets the current transient transfer allowance
/// @return transferAllowance The current allowance amount
/// @dev Reads from transient storage slot 0
function getTransferAllowance() public view returns (uint256 transferAllowance) {
assembly {
transferAllowance := tload(0)
}
}
/// @notice Handles receipt of NFTs (ERC721 receiver)
/// @dev Only accepts NFTs from the designated collection
/// @return The function selector to confirm receipt
function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
if (msg.sender != address(collection)) {
revert InvalidCollection();
}
return this.onERC721Received.selector;
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* GETTER FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Returns the factory address from proxy bytecode
/// @return The factory contract address
/// @dev Reads from bytes 0-20 of the proxy's immutable args
function factory() public view returns (address) {
bytes memory args = LibClone.argsOnERC1967(address(this), 0, 20);
return address(bytes20(args));
}
/// @notice Returns the router address from proxy bytecode
/// @return The Uniswap V4 router contract interface
/// @dev Reads from bytes 20-40 of the proxy's immutable args
function router() public view returns (IUniswapV4Router04) {
bytes memory args = LibClone.argsOnERC1967(address(this), 20, 40);
return IUniswapV4Router04(payable(address(bytes20(args))));
}
/// @notice Returns the pool manager address from proxy bytecode
/// @return The Uniswap V4 pool manager contract interface
/// @dev Reads from bytes 40-60 of the proxy's immutable args
function po
Submitted on: 2025-10-06 15:28:13
Comments
Log in to comment.
No comments yet.