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/LaunchSERC20.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
// The first ERC20 strategy launchpad.
// https://sterc20.xyz/
// https://x.com/sterc20/
pragma solidity ^0.8.13;
import {SERC20} from "./SERC20.sol";
import {Ownable} from "@solady/auth/Ownable.sol";
import {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUniswapV2Router02} from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IUniversalRouter} from "../lib/universal-router/contracts/interfaces/IUniversalRouter.sol";
import {Commands} from "../lib/universal-router/contracts/libraries/Commands.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
interface ISwapRouter02 {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(
ExactInputSingleParams calldata params
) external payable returns (uint256 amountOut);
}
contract LaunchSERC20 is Ownable, ReentrancyGuard {
using StateLibrary for IPoolManager;
using PoolIdLibrary for PoolKey;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTANTS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
uint256 private constant MIN_ETH_THRESHOLD = 1 ether;
// Core protocol addresses
IPositionManager private constant POSM =
IPositionManager(0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e);
// Uniswap routers
IUniswapV2Router02 private constant UNISWAP_V2_ROUTER =
IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
IUniversalRouter private constant UNIVERSAL_ROUTER =
IUniversalRouter(0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af);
address private constant SWAP_ROUTER_02 =
0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
// DEAD ADDRESS
address private constant DEAD_ADDRESS =
0x000000000000000000000000000000000000dEaD;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* STATE VARIABLES */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
// Array to store all deployed SERC20 contracts
address[] public deployedStrategies;
// Mapping from strategy token to deployed SERC20 contract
mapping(address => address) public strategyToSERC20;
// Mapping to check if a strategy token already has a deployment
mapping(address => bool) public isStrategyDeployed;
// Mapping from SERC20 contract to original deployer
mapping(address => address) public serc20ToDeployer;
// Structure to store token information
struct TokenInfo {
address strategyToken;
string ammVersion;
address pairAddress;
uint24 poolFee; // For V3 only, ignored for V2
string name;
string symbol;
address deployer;
}
// Mapping from SERC20 address to TokenInfo
mapping(address => TokenInfo) public tokenInfoBySERC20;
// Purchase tracking per SERC20 contract
mapping(address => uint256) public nextPurchaseId;
mapping(address => mapping(uint256 => Purchase)) public purchases;
struct Purchase {
string method; // "V2", "V3"
address poolAddress; // Address of the pool where it was purchased
uint256 tokenAmount; // Amount of strategy tokens received
uint256 ethAmount; // Amount of ETH spent
uint256 purchasePrice; // Price per token (ethAmount / tokenAmount)
uint256 timestamp; // When the purchase was made
bool sold; // Whether this purchase has been sold
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CUSTOM EVENTS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
event StrategyLaunched(
address indexed strategyToken,
address indexed serc20Contract,
string name,
string symbol,
address indexed deployer
);
event StrategyTokenPurchased(
address indexed strategyToken,
address indexed recipient,
uint256 ethAmount,
string version
);
event SERC20Burned(
uint256 ethUsed,
uint256 serc20Received,
uint256 serc20Burned
);
event PurchaseTracked(
address indexed serc20Address,
uint256 indexed purchaseId,
string method,
address poolAddress,
uint256 tokenAmount,
uint256 ethAmount,
uint256 purchasePrice
);
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CUSTOM ERRORS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
error StrategyAlreadyDeployed();
error InvalidStrategyToken();
error EmptyName();
error EmptySymbol();
error PoolNotInitialized();
error InsufficientETHFees();
error TokenNotDeployed();
error InvalidAMMVersion();
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTRUCTOR */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
constructor(address owner_) {
_initializeOwner(owner_);
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Launch a new SERC20 strategy contract
/// @param strategyToken The address of the strategy token to be used for buybacks
/// @param name The name of the new SERC20 token
/// @param symbol The symbol of the new SERC20 token
/// @param ammVersion The AMM version where the strategyToken is traded ("V2" or "V3")
/// @param pairAddress The address of the trading pair
/// @param poolFee The pool fee for V3 (500, 3000, or 10000), ignored for V2
/// @return serc20Address The address of the newly deployed SERC20 contract
function LaunchStrategy(
address strategyToken,
string memory name,
string memory symbol,
string memory ammVersion,
address pairAddress,
uint24 poolFee
) external nonReentrant returns (address serc20Address) {
if (strategyToken == address(0)) revert InvalidStrategyToken();
if (bytes(name).length == 0) revert EmptyName();
if (bytes(symbol).length == 0) revert EmptySymbol();
if (isStrategyDeployed[strategyToken]) revert StrategyAlreadyDeployed();
// 1. Deploy the SERC20
SERC20 newSERC20 = new SERC20(
name,
symbol,
strategyToken,
msg.sender,
address(this)
);
serc20Address = address(newSERC20);
newSERC20.loadLiquidity(address(this));
// 3. Update mappings
deployedStrategies.push(serc20Address);
strategyToSERC20[strategyToken] = serc20Address;
isStrategyDeployed[strategyToken] = true;
serc20ToDeployer[serc20Address] = msg.sender;
// Store the info in the structure
tokenInfoBySERC20[serc20Address] = TokenInfo({
strategyToken: strategyToken,
ammVersion: ammVersion,
pairAddress: pairAddress,
poolFee: poolFee,
name: name,
symbol: symbol,
deployer: msg.sender
});
// 4. Emit event
emit StrategyLaunched(
strategyToken,
serc20Address,
name,
symbol,
msg.sender
);
return serc20Address;
}
/// @notice Get the number of deployed strategies
/// @return count The total number of deployed SERC20 contracts
function getDeployedStrategiesCount()
external
view
returns (uint256 count)
{
return deployedStrategies.length;
}
/// @notice Get all deployed strategy addresses
/// @return strategies Array of all deployed SERC20 contract addresses
function getAllDeployedStrategies()
external
view
returns (address[] memory strategies)
{
return deployedStrategies;
}
/// @notice Get deployed strategies with pagination
/// @param offset The starting index
/// @param limit The maximum number of strategies to return
/// @return strategies Array of SERC20 contract addresses
function getDeployedStrategies(
uint256 offset,
uint256 limit
) external view returns (address[] memory strategies) {
uint256 total = deployedStrategies.length;
if (offset >= total) {
return new address[](0);
}
uint256 end = offset + limit;
if (end > total) {
end = total;
}
strategies = new address[](end - offset);
for (uint256 i = offset; i < end; i++) {
strategies[i - offset] = deployedStrategies[i];
}
return strategies;
}
/// @notice Check if a strategy token has been deployed
/// @param strategyToken The strategy token address to check
/// @return deployed True if the strategy has been deployed, false otherwise
function isStrategyAlreadyDeployed(
address strategyToken
) external view returns (bool deployed) {
return isStrategyDeployed[strategyToken];
}
/// @notice Get the SERC20 contract address for a given strategy token
/// @param strategyToken The strategy token address
/// @return serc20Address The corresponding SERC20 contract address (address(0) if not deployed)
function getSERC20ForStrategy(
address strategyToken
) external view returns (address serc20Address) {
return strategyToSERC20[strategyToken];
}
/// @notice Get the original deployer of a SERC20 contract
/// @param serc20Address The SERC20 contract address
/// @return deployer The address that originally launched this strategy
function getStrategyDeployer(
address payable serc20Address
) external view returns (address deployer) {
return serc20ToDeployer[serc20Address];
}
/// @notice Get token info by SERC20 address
/// @param serc20Address The SERC20 contract address
/// @return info The TokenInfo structure containing all details
function getTokenInfoByAddress(
address serc20Address
) external view returns (TokenInfo memory info) {
return tokenInfoBySERC20[serc20Address];
}
/// @notice Emergency claim: collect accumulated fees for a given pool key and SERC20 address and send to owner
/// @param poolKey The pool key to collect fees from
/// @param serc20Address The SERC20 contract address
/// @return ethFees Amount of ETH fees collected
/// @return tokenFees Amount of token fees collected
function emergencyClaim(
PoolKey memory poolKey,
address payable serc20Address
)
external
onlyOwner
nonReentrant
returns (uint256 ethFees, uint256 tokenFees)
{
// Verify that the token exists on the launchpad
TokenInfo memory tokenInfo = tokenInfoBySERC20[serc20Address];
if (tokenInfo.strategyToken == address(0)) revert TokenNotDeployed();
// Use internal function to collect fees to this contract
(ethFees, tokenFees) = _collectAccumulatedFees(poolKey, serc20Address);
// Transfer collected ETH to contract owner
if (ethFees > 0) {
SafeTransferLib.safeTransferETH(owner(), ethFees);
}
// Transfer collected SERC20 tokens to contract owner
if (tokenFees > 0) {
require(
IERC20(serc20Address).transfer(owner(), tokenFees),
"Token transfer to owner failed"
);
}
return (ethFees, tokenFees);
}
/// @notice Allow owner to recover any ERC20 tokens stored in the contract
/// @param token The address of the ERC20 token to recover
/// @param amount The amount of tokens to recover (0 to recover all)
function recoverERC20(
address token,
uint256 amount
) external onlyOwner nonReentrant {
require(token != address(0), "Invalid token address");
IERC20 tokenContract = IERC20(token);
uint256 balance = tokenContract.balanceOf(address(this));
require(balance > 0, "No tokens to recover");
// If amount is 0, recover all tokens
uint256 amountToRecover = amount == 0 ? balance : amount;
require(amountToRecover <= balance, "Insufficient token balance");
// Transfer tokens to owner
require(
tokenContract.transfer(owner(), amountToRecover),
"Token transfer failed"
);
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* POOL FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Get accumulated fees in the pool using StateLibrary
/// @param poolKey The pool key to check fees for
/// @return fees0 Global fee growth for token0 (ETH)
/// @return fees1 Global fee growth for token1 (SERC20 token)
function getAccumulatedFees(
PoolKey memory poolKey
) external view returns (uint256 fees0, uint256 fees1) {
// Get the SERC20 contract address from the poolKey
address serc20Address = Currency.unwrap(poolKey.currency1);
SERC20 serc20 = SERC20(payable(serc20Address));
// Get the position token ID and tick range
uint256 tokenId = serc20.getPositionTokenId();
(int24 tickLower, int24 tickUpper) = serc20.getTickRange();
// Get pool manager
IPoolManager poolManager = POSM.poolManager();
PoolId poolId = poolKey.toId();
// Get position info from pool manager
// The position is owned by POSM (position manager) with tokenId as salt
(
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128
) = poolManager.getPositionInfo(
poolId,
address(POSM),
tickLower,
tickUpper,
bytes32(tokenId)
);
// Get current fee growth inside the position range using StateLibrary
(
uint256 feeGrowthInside0X128,
uint256 feeGrowthInside1X128
) = poolManager.getFeeGrowthInside(poolId, tickLower, tickUpper);
// Calculate fees owed using the same formula as Uniswap
fees0 =
((feeGrowthInside0X128 - feeGrowthInside0LastX128) * liquidity) /
(1 << 128);
fees1 =
((feeGrowthInside1X128 - feeGrowthInside1LastX128) * liquidity) /
(1 << 128);
return (fees0, fees1);
}
/// @notice Get pool information for a SERC20 contract
/// @param serc20Address The SERC20 contract address
/// @return initialized Whether the pool has been initialized
/// @return poolKeyData The pool key data
/// @return poolIdData The pool ID
function getPoolInfo(
address payable serc20Address
)
external
view
returns (
bool initialized,
PoolKey memory poolKeyData,
PoolId poolIdData
)
{
SERC20 serc20 = SERC20(payable(serc20Address));
return serc20.getPoolInfo();
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* STRATEGY FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Buy strategy tokens using accumulated fees - automatically detects AMM version
/// @param serc20Address The SERC20 contract address
function buyStrategy(address payable serc20Address) external nonReentrant {
// Verify that the token exists on the launchpad
TokenInfo memory tokenInfo = tokenInfoBySERC20[serc20Address];
if (tokenInfo.strategyToken == address(0)) revert TokenNotDeployed();
// Retrieve stored info from deployment
string memory ammVersion = tokenInfo.ammVersion;
address pairStrategyAddress = tokenInfo.pairAddress;
uint24 poolFee = tokenInfo.poolFee;
// Route to appropriate function based on AMM
if (
keccak256(abi.encodePacked(ammVersion)) ==
keccak256(abi.encodePacked("V2"))
) {
buyStrategyTokenV2(serc20Address, pairStrategyAddress);
} else if (
keccak256(abi.encodePacked(ammVersion)) ==
keccak256(abi.encodePacked("V3"))
) {
buyStrategyTokenV3(serc20Address, pairStrategyAddress, poolFee);
} else {
revert InvalidAMMVersion();
}
}
/// @notice Buy strategy tokens using accumulated fees via Uniswap V2
/// @param serc20Address The SERC20 contract address
/// @param pairStrategyAddress Address where to send the purchased strategy tokens
function buyStrategyTokenV2(
address payable serc20Address,
address pairStrategyAddress
) internal {
SERC20 serc20 = SERC20(payable(serc20Address));
// Get pool info
(bool poolInitialized, PoolKey memory poolKey, ) = serc20.getPoolInfo();
if (!poolInitialized) revert PoolNotInitialized();
// Check fees threshold
(uint256 ethFees, ) = this.getAccumulatedFees(poolKey);
if (ethFees < MIN_ETH_THRESHOLD) revert InsufficientETHFees();
// Get accumulated fees and collect them
(uint256 ethFeesCollected, uint256 tokenFees) = _collectAccumulatedFees(
poolKey,
serc20Address
);
// Calculate distributions
(
uint256 ethForSwap,
uint256 ethToLaunchpad,
uint256 ethToOriginalDeployer,
uint256 tokensToLaunchpad,
uint256 tokensToBurn
) = _calculateDistributions(ethFeesCollected, tokenFees);
// Get strategy token address from SERC20
address strategyToken = serc20.strategyToken();
// Swap ETH for strategy tokens on Uniswap V2 with 10% slippage
if (ethForSwap > 0) {
address[] memory path = new address[](2);
path[0] = UNISWAP_V2_ROUTER.WETH();
path[1] = strategyToken;
// Get expected output amount
uint[] memory amounts = UNISWAP_V2_ROUTER.getAmountsOut(
ethForSwap,
path
);
uint256 expectedTokens = amounts[1];
// Apply 10% slippage (accept 90% of expected amount)
uint256 minTokensOut = (expectedTokens * 90) / 100;
uint[] memory swapAmounts = UNISWAP_V2_ROUTER.swapExactETHForTokens{
value: ethForSwap
}(minTokensOut, path, serc20Address, block.timestamp + 300);
uint256 tokensReceived = swapAmounts[1];
// Track the purchase for future selling
_trackPurchase(
serc20Address,
"V2",
pairStrategyAddress,
tokensReceived,
ethForSwap
);
emit StrategyTokenPurchased(
strategyToken,
serc20Address,
ethForSwap,
"V2"
);
}
// Distribute remaining funds
_distributeFunds(
serc20Address,
ethToLaunchpad,
ethToOriginalDeployer,
tokensToLaunchpad,
tokensToBurn
);
}
/// @notice Buy strategy tokens using accumulated fees via Uniswap V3
/// @param serc20Address The SERC20 contract address
/// @param pairStrategyAddress Address of the Uniswap V3 pool (not used in this version)
/// @param poolFee The pool fee for the V3 swap (500, 3000, or 10000)
function buyStrategyTokenV3(
address payable serc20Address,
address pairStrategyAddress,
uint24 poolFee
) internal {
SERC20 serc20 = SERC20(payable(serc20Address));
// Get pool info
(bool poolInitialized, PoolKey memory poolKey, ) = serc20.getPoolInfo();
if (!poolInitialized) revert PoolNotInitialized();
// Check fees threshold
(uint256 ethFees, ) = this.getAccumulatedFees(poolKey);
if (ethFees < MIN_ETH_THRESHOLD) revert InsufficientETHFees();
// Get accumulated fees and collect them
(uint256 ethFeesCollected, uint256 tokenFees) = _collectAccumulatedFees(
poolKey,
serc20Address
);
// Calculate distributions
(
uint256 ethForSwap,
uint256 ethToLaunchpad,
uint256 ethToOriginalDeployer,
uint256 tokensToLaunchpad,
uint256 tokensToBurn
) = _calculateDistributions(ethFeesCollected, tokenFees);
// Get strategy token address from SERC20
address strategyToken = serc20.strategyToken();
// Swap ETH for strategy tokens on Uniswap V3
if (ethForSwap > 0) {
address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
uint256 tokensReceived = ISwapRouter02(SWAP_ROUTER_02)
.exactInputSingle{value: ethForSwap}(
ISwapRouter02.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: strategyToken,
fee: poolFee,
recipient: serc20Address,
amountIn: ethForSwap,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
// Track the purchase for future selling
_trackPurchase(
serc20Address,
"V3",
pairStrategyAddress,
tokensReceived,
ethForSwap
);
emit StrategyTokenPurchased(
strategyToken,
serc20Address,
ethForSwap,
"V3"
);
}
// Distribute remaining funds
_distributeFunds(
serc20Address,
ethToLaunchpad,
ethToOriginalDeployer,
tokensToLaunchpad,
tokensToBurn
);
}
/// @notice Sell strategy tokens if price is 2x higher than purchase price
/// @param serc20Address The SERC20 contract address
function sellStrategyToken(
address payable serc20Address
) external nonReentrant {
SERC20 serc20 = SERC20(payable(serc20Address));
// Get pool info
(bool poolInitialized, , ) = serc20.getPoolInfo();
if (!poolInitialized) revert PoolNotInitialized();
uint256 totalEthReceived = 0;
address strategyToken = serc20.strategyToken();
uint256 nextPurchaseIdLocal = nextPurchaseId[serc20Address];
for (uint256 i = 0; i < nextPurchaseIdLocal; i++) {
Purchase memory purchase = purchases[serc20Address][i];
string memory method = purchase.method;
address poolAddress = purchase.poolAddress;
uint256 tokenAmount = purchase.tokenAmount;
uint256 ethAmount = purchase.ethAmount;
uint256 purchasePrice = purchase.purchasePrice;
bool sold = purchase.sold;
if (sold || tokenAmount == 0) continue;
uint256 currentPrice = _getCurrentPrice(
method,
strategyToken,
poolAddress
);
if (currentPrice == 0) continue;
// Check if current price is 2x the purchase price
if (currentPrice >= purchasePrice * 2) {
// Sell the tokens
uint256 ethReceived = _sellStrategyTokens(
method,
strategyToken,
tokenAmount
);
totalEthReceived += ethReceived;
// Mark this purchase as sold
purchases[serc20Address][i].sold = true;
}
}
// If we received ETH from sales, buy SERC20 tokens and burn them
if (totalEthReceived > 0) {
_buyAndBurnSERC20(serc20Address, totalEthReceived);
}
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* INTERNAL FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Track a strategy token purchase
function _trackPurchase(
address serc20Address,
string memory method,
address poolAddress,
uint256 tokenAmount,
uint256 ethAmount
) internal {
uint256 purchaseId = nextPurchaseId[serc20Address]++;
uint256 purchasePrice = (ethAmount * 1e18) / tokenAmount; // Price per token in wei
purchases[serc20Address][purchaseId] = Purchase({
method: method,
poolAddress: poolAddress,
tokenAmount: tokenAmount,
ethAmount: ethAmount,
purchasePrice: purchasePrice,
timestamp: block.timestamp,
sold: false
});
emit PurchaseTracked(
serc20Address,
purchaseId,
method,
poolAddress,
tokenAmount,
ethAmount,
purchasePrice
);
}
/// @notice Collect accumulated fees from the pool
/// @param poolKey The pool key
/// @param serc20Address The SERC20 contract address to collect fees to
/// @return ethFees Amount of ETH fees collected
/// @return tokenFees Amount of token fees collected
function _collectAccumulatedFees(
PoolKey memory poolKey,
address payable serc20Address
) internal returns (uint256 ethFees, uint256 tokenFees) {
(ethFees, tokenFees) = this.getAccumulatedFees(poolKey);
// If no fees, no need to collect
if (ethFees == 0 && tokenFees == 0) {
return (0, 0);
}
SERC20 serc20 = SERC20(payable(serc20Address));
uint256 tokenId = serc20.getPositionTokenId();
// Use DECREASE_LIQUIDITY with 0 liquidity to collect fees only
bytes memory actions = abi.encodePacked(
uint8(Actions.DECREASE_LIQUIDITY),
uint8(Actions.TAKE_PAIR)
);
bytes[] memory params = new bytes[](2);
params[0] = abi.encode(
tokenId,
0, // liquidityDelta = 0 (don't remove liquidity, just collect fees)
0, // amount0Min = 0
0, // amount1Min = 0
"" // hookData
);
// Parameters for TAKE_PAIR - transfer fees to this contract
params[1] = abi.encode(
poolKey.currency0, // ETH
poolKey.currency1, // SERC20 token
address(this) // recipient
);
// Execute the fee collection through Position Manager
POSM.modifyLiquidities(
abi.encode(actions, params),
block.timestamp + 60
);
// The fees are now collected to this contract
return (ethFees, tokenFees);
}
/// @notice Calculate distribution amounts
/// @return ethForSwap 80% of ETH for swapping
/// @return ethToLaunchpad 15% of ETH to launchpad
/// @return ethToOriginalDeployer 5% of ETH to original deployer
/// @return tokensToLaunchpad 50% of tokens to launchpad
/// @return tokensToBurn 50% of tokens to burn
function _calculateDistributions(
uint256 ethFees,
uint256 tokenFees
)
internal
pure
returns (
uint256 ethForSwap,
uint256 ethToLaunchpad,
uint256 ethToOriginalDeployer,
uint256 tokensToLaunchpad,
uint256 tokensToBurn
)
{
ethForSwap = (ethFees * 80) / 100;
ethToLaunchpad = (ethFees * 15) / 100;
ethToOriginalDeployer = (ethFees * 5) / 100;
tokensToLaunchpad = (tokenFees * 50) / 100;
tokensToBurn = (tokenFees * 50) / 100;
}
/// @notice Distribute remaining funds after swap
function _distributeFunds(
address payable serc20Address,
uint256 ethToLaunchpad,
uint256 ethToOriginalDeployer,
uint256 tokensToLaunchpad,
uint256 tokensToBurn
) internal {
SERC20 serc20 = SERC20(payable(serc20Address));
address originalDeployer = serc20ToDeployer[serc20Address];
// Send 15% ETH to launchpad (this contract)
if (ethToLaunchpad > 0) {
SafeTransferLib.safeTransferETH(address(this), ethToLaunchpad);
}
// Send 5% ETH to original deployer
if (ethToOriginalDeployer > 0) {
SafeTransferLib.safeTransferETH(
originalDeployer,
ethToOriginalDeployer
);
}
// Send 50% tokens to dead address (burn)
if (tokensToBurn > 0) {
require(
serc20.transfer(DEAD_ADDRESS, tokensToBurn),
"Token burn transfer failed"
);
}
// Send 50% tokens to launchpad
if (tokensToLaunchpad > 0) {
require(
serc20.transfer(address(this), tokensToLaunchpad),
"Token transfer to launchpad failed"
);
}
}
/// @notice Get current price of strategy token for a specific method
function _getCurrentPrice(
string memory method,
address strategyToken,
address poolAddress
) internal view returns (uint256 currentPrice) {
if (
keccak256(abi.encodePacked(method)) ==
keccak256(abi.encodePacked("V2"))
) {
address[] memory path = new address[](2);
path[0] = UNISWAP_V2_ROUTER.WETH();
path[1] = strategyToken;
uint[] memory amounts = UNISWAP_V2_ROUTER.getAmountsOut(1e18, path);
return (1e18 * 1e18) / amounts[1]; // Price per token in wei
} else if (
keccak256(abi.encodePacked(method)) ==
keccak256(abi.encodePacked("V3"))
) {
if (poolAddress == address(0)) return 0;
IUniswapV3Pool pool = IUniswapV3Pool(poolAddress);
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
// Convert sqrtPriceX96 to price
// sqrtPriceX96 = sqrt(price) * 2^96
// price = (sqrtPriceX96 / 2^96)^2
uint256 sqrtPrice = uint256(sqrtPriceX96);
// Calculate price with 18 decimal precision
// price = (sqrtPrice^2 * 1e18) / (2^96)^2
currentPrice = (sqrtPrice * sqrtPrice * 1e18) >> (96 * 2);
return currentPrice;
}
return 0;
}
/// @notice Sell strategy tokens back to the market
function _sellStrategyTokens(
string memory method,
address strategyToken,
uint256 tokenAmount
) internal returns (uint256 ethReceived) {
// For V2 sales
if (
keccak256(abi.encodePacked(method)) ==
keccak256(abi.encodePacked("V2"))
) {
address[] memory path = new address[](2);
path[0] = strategyToken;
path[1] = UNISWAP_V2_ROUTER.WETH();
// Approve router to spend tokens
IERC20(strategyToken).approve(
address(UNISWAP_V2_ROUTER),
tokenAmount
);
// Get expected ETH output
uint[] memory amounts = UNISWAP_V2_ROUTER.getAmountsOut(
tokenAmount,
path
);
uint256 expectedETH = amounts[1];
uint256 minETHOut = (expectedETH * 90) / 100; // 10% slippage
uint256 ethBefore = address(this).balance;
UNISWAP_V2_ROUTER.swapExactTokensForETH(
tokenAmount,
minETHOut,
path,
address(this),
block.timestamp + 300
);
return address(this).balance - ethBefore;
}
return 0;
}
/// @notice Buy SERC20 tokens with ETH and burn them using Universal Router V4
function _buyAndBurnSERC20(
address payable serc20Address,
uint256 ethAmount
) internal {
if (ethAmount == 0) return;
SERC20 serc20 = SERC20(payable(serc20Address));
// Get the SERC20 pool info (ETH/SERC20 V4 pool)
(bool poolExists, PoolKey memory poolKey, ) = serc20.getPoolInfo();
if (!poolExists) {
// Pool not initialized, cannot proceed
return;
}
uint256 minTokensOut = (ethAmount * 9) / 10; // 10% slippage tolerance
// Track balance before for accurate burn amount calculation
uint256 contractBalanceBefore = IERC20(serc20Address).balanceOf(
address(this)
);
uint128 safeEthAmount = uint128(ethAmount);
uint128 safeMinTokensOut = uint128(minTokensOut);
_swapExactInputSingleV4(
poolKey,
safeEthAmount,
safeMinTokensOut
);
uint256 contractBalanceAfter = IERC20(serc20Address).balanceOf(
address(this)
);
uint256 tokensReceived = contractBalanceAfter - contractBalanceBefore;
// Transfer received tokens to burn address
if (tokensReceived > 0) {
require(
IERC20(serc20Address).transfer(DEAD_ADDRESS, tokensReceived),
"Token burn transfer failed"
);
}
emit SERC20Burned(ethAmount, tokensReceived, tokensReceived);
}
function _swapExactInputSingleV4(
PoolKey memory key,
uint128 amountIn,
uint128 minAmountOut
) internal returns (uint256 amountOut) {
// Encode the Universal Router command
bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
bytes[] memory inputs = new bytes[](1);
// Encode V4Router actions
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);
// Prepare parameters for each action
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: true, // ETH (currency0) -> SERC20 (currency1)
amountIn: amountIn,
amountOutMinimum: minAmountOut,
hookData: ""
})
);
params[1] = abi.encode(key.currency0, amountIn);
params[2] = abi.encode(key.currency1, minAmountOut);
// Combine actions and params into inputs
inputs[0] = abi.encode(actions, params);
// Execute the swap with deadline protection
uint256 deadline = block.timestamp + 300; // 5 minutes
UNIVERSAL_ROUTER.execute{value: amountIn}(commands, inputs, deadline);
// Verify and return the output amount
amountOut = IERC20(Currency.unwrap(key.currency1)).balanceOf(
address(this)
);
require(amountOut >= minAmountOut, "Insufficient output amount");
return amountOut;
}
/// @notice Allows the contract to receive ETH
receive() external payable {}
}
"
},
"src/SERC20.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@solady/auth/Ownable.sol";
import {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol";
import {IAllowanceTransfer} from "@permit2/interfaces/IAllowanceTransfer.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
contract SERC20 is ERC20, Ownable, ReentrancyGuard {
using PoolIdLibrary for PoolKey;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTANTS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
uint256 private constant TOTAL_SUPPLY = 1000000000 * 10 ** 18;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* HARDCODED ADDRESSES */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
// Core protocol addresses
IAllowanceTransfer private constant PERMIT2 =
IAllowanceTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3);
IPositionManager private constant POSM =
IPositionManager(0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e);
address private constant POOL_MANAGER =
0x000000000004444c5dc75cB358380D2e3dE08A90;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* STATE VARIABLES */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
// Pool state
bool public loadingLiquidity;
PoolKey public poolKey;
PoolId public poolId;
bool public poolInitialized;
uint256 public positionTokenId;
int24 public tickLower;
int24 public tickUpper;
// Strategy configuration
address public immutable strategyToken;
address public immutable launchpad;
address public platform;
address public creator;
// Launch protection
uint256 private launchBlock;
uint256 private maxTxAmount;
uint256 private constant LAUNCH_PERIOD = 2; // 2 blocks
uint256 private constant MAX_WALLET_PERCENTAGE = 4; // 4% of total supply during launch
// Track transfers per tx.origin per block to detect multi-swaps
mapping(address => uint256) private tokensFromPoolPerOrigin;
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* CONSTRUCTOR */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
constructor(
string memory name_,
string memory symbol_,
address strategyToken_,
address creator_,
address platform_
) ERC20(name_, symbol_) {
_mint(address(this), TOTAL_SUPPLY);
_initializeOwner(msg.sender);
strategyToken = strategyToken_;
launchpad = msg.sender;
platform = platform_;
creator = creator_;
launchBlock = block.number;
maxTxAmount = (TOTAL_SUPPLY * MAX_WALLET_PERCENTAGE) / 100;
}
/// @notice Load initial liquidity into the pool
function loadLiquidity(address launchpadAddress) external onlyOwner {
loadingLiquidity = true;
// Create the pool with ETH (currency0) and TOKEN (currency1)
Currency currency0 = Currency.wrap(address(0)); // ETH
Currency currency1 = Currency.wrap(address(this)); // TOKEN
uint24 lpFee = 100000; // 10% fee
int24 tickSpacing = 200;
uint256 token0Amount = 0; // 1 wei
uint256 token1Amount = 1_000_000_000 * 10 ** 18; // 1B TOKEN
uint160 startingPrice = 2045645379722529521098596513701367;
tickLower = int24(-887200);
tickUpper = int24(203000);
PoolKey memory key = PoolKey(
currency0,
currency1,
lpFee,
tickSpacing,
IHooks(address(0))
);
// Store pool information
poolKey = key;
poolId = key.toId();
bytes memory hookData = new bytes(0);
uint128 liquidity = 39095916497508424169487;
(
bytes memory actions,
bytes[] memory mintParams
) = _mintLiquidityParams(
key,
tickLower,
tickUpper,
liquidity,
token0Amount,
token1Amount,
launchpadAddress,
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 = token0Amount;
// Approve Permit2 to spend our tokens
_approve(address(this), address(PERMIT2), type(uint256).max);
PERMIT2.approve(
address(this),
address(POSM),
type(uint160).max,
type(uint48).max
);
// Get the next token ID before minting
positionTokenId = POSM.nextTokenId();
POSM.multicall{value: valueToPass}(params);
loadingLiquidity = false;
poolInitialized = true;
poolKey = key;
poolId = key.toId();
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* VIEW FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Get pool information for this SERC20 token
/// @return initialized Whether the pool has been initialized
/// @return poolKeyData The pool key data
/// @return poolIdData The pool ID
function getPoolInfo()
external
view
returns (
bool initialized,
PoolKey memory poolKeyData,
PoolId poolIdData
)
{
return (poolInitialized, poolKey, poolId);
}
/// @notice Get the position token ID for this SERC20's LP position
/// @return tokenId The position token ID
function getPositionTokenId() external view returns (uint256 tokenId) {
return positionTokenId;
}
/// @notice Get the tick range for this SERC20's LP position
/// @return lower The lower tick bound
/// @return upper The upper tick bound
function getTickRange() external view returns (int24 lower, int24 upper) {
return (tickLower, tickUpper);
}
/// @notice Get pool information for V4 integration
/// @return poolManager The pool manager address
/// @return tokenAddress This token's address
/// @return positionManager The position manager address
function getTokenPair() public view returns (address, address, address) {
return (POOL_MANAGER, address(this), address(POSM));
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* INTERNAL FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/// @notice Creates parameters for minting liquidity in Uniswap V4
function _mintLiquidityParams(
PoolKey memory key,
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(
key,
_tickLower,
_tickUpper,
liquidity,
amount0Max,
amount1Max,
recipient,
hookData
);
params[1] = abi.encode(key.currency0, key.currency1);
return (actions, params);
}
/// @notice Override _update to implement launch protection and antibot
/// @dev Limits transactions to 2% during first 5 blocks and detects multi-swaps
function _update(
address from,
address to,
uint256 value
) internal override {
// During liquidity loading, allow all transfers
if (loadingLiquidity) {
super._update(from, to, value);
return;
}
// Always allow system contract interactions and protocol operations
if (
to == address(PERMIT2) ||
to == address(POSM) ||
from == address(POSM) ||
to == POOL_MANAGER ||
from == POOL_MANAGER ||
from == address(this) ||
to == address(this) ||
from == launchpad ||
to == launchpad ||
from == platform ||
to == platform ||
from == creator ||
to == creator ||
from == address(0) || // Mint operations
to == address(0)
) {
// Burn operations
super._update(from, to, value);
return;
}
// Block all other transfers at launch block
if (block.number == launchBlock) {
revert("No buys allowed during launch block!");
}
// During launch period (blocks 1-5 after launch), apply restrictions to DEX buys only
if (
block.number > launchBlock &&
block.number <= launchBlock + LAUNCH_PERIOD
) {
// Check if this is a buy from V4 pool (tokens coming FROM pool manager TO user)
bool isV4PoolBuy = (from == POOL_MANAGER &&
to != platform &&
to != creator &&
to != launchpad);
if (isV4PoolBuy) {
// Track tokens received from pool per tx.origin to detect multi-swaps
tokensFromPoolPerOrigin[tx.origin] += value;
require(
tokensFromPoolPerOrigin[tx.origin] <=
(maxTxAmount * 110) / 100,
"Keeping 4% pool Limits"
);
// Apply wallet limits for DEX buys
require(
balanceOf(to) + value <= maxTxAmount,
"Max wallet limit exceeded during launch period"
);
}
}
super._update(from, to, value);
}
/// @notice Get the maximum transaction amount during launch period
/// @return maxTx The maximum transaction amount (2% of total supply)
function getMaxTxAmount() public view returns (uint256 maxTx) {
return maxTxAmount;
}
/// @notice Allows the contract to receive ETH
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();
_;
}
}
"
},
"lib/solady/src/utils/ReentrancyGuard.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Reentrancy guard mixin.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/ReentrancyGuard.sol)
abstract contract ReentrancyGuard {
/*´:°•.°+.*•´.*:˚.°*.˚•
Submitted on: 2025-09-28 20:29:48
Comments
Log in to comment.
No comments yet.