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": {
"contracts/TokenStrategyFactory.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Ownable} from "solady/src/auth/Ownable.sol";
import {TokenStrategy} from "./TokenStrategy.sol";
import {SafeTransferLib} from "solady/src/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 "../lib/v4-router/interfaces/IUniswapV4Router04.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import "./Interfaces.sol";
import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol";
import {LibClone} from "solady/src/utils/LibClone.sol";
contract TokenStrategyFactory is Ownable, ReentrancyGuard {
/************************************************************************************************************************************
* ██████╗ ███╗ ██╗██╗ ██╗███████╗████████╗██████╗ ███████╗████████╗██████╗ █████╗ ████████╗███████╗ ██████╗ ██╗ ██╗ *
* ██╔══██╗████╗ ██║██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝██╔════╝ ╚██╗ ██╔╝ *
* ██████╔╝██╔██╗ ██║█████╔╝ ███████╗ ██║ ██████╔╝ ███████╗ ██║ ██████╔╝███████║ ██║ █████╗ ██║ ███╗ ╚████╔╝ *
* ██╔═══╝ ██║╚██╗██║██╔═██╗ ╚════██║ ██║ ██╔══██╗ ╚════██║ ██║ ██╔══██╗██╔══██║ ██║ ██╔══╝ ██║ ██║ ╚██╔╝ *
* ██║ ██║ ╚████║██║ ██╗███████║ ██║ ██║ ██║ ███████║ ██║ ██║ ██║██║ ██║ ██║ ███████╗╚██████╔╝ ██║ *
* ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ *
*************************************************************************************************************************************/
uint256 private constant ethToPair = 2 wei;
uint256 private constant initialBuy = 0.01 ether;
IPositionManager private immutable posm;
IAllowanceTransfer private immutable permit2;
IUniswapV4Router04 private immutable router;
address private immutable poolManager;
address public constant DEADADDRESS =
0x000000000000000000000000000000000000dEaD;
mapping(address => address) public nftToTokenStrategy;
mapping(address => address) public tokenToNftStrategy;
uint256 public feeToLaunch;
address public hookAddress;
address public feeAddress;
bool public loadingLiquidity;
bool public deployerBuying;
bool public publicLaunches;
bool public collectionOwnerLaunches;
bool public migrated = false;
bool public routerRestrict;
mapping(address => bool) public listOfRouters;
error HookNotSet();
error CollectionAlreadyLaunched();
error WrongEthAmount();
error NotERC721();
error GatedByCollectionOwner();
error CannotLaunch();
error NoETHToTwap();
error TwapDelayNotMet();
error InvalidNFTStrategy();
error NotAContract();
error MigrationAlreadyCompleted();
error NotEnoughToken();
event TokenStrategyLaunched(
address indexed tokenWorkStrategy,
address indexed tokenStrategy,
string tokenName,
string tokenSymbol
);
constructor(
address _posm,
address _permit2,
address _poolManager,
address _universalRouter,
address payable _router,
address _feeAddress
) {
router = IUniswapV4Router04(_router);
posm = IPositionManager(_posm);
permit2 = IAllowanceTransfer(_permit2);
poolManager = _poolManager;
listOfRouters[address(this)] = true;
listOfRouters[_posm] = true;
listOfRouters[_permit2] = true;
listOfRouters[_router] = true;
listOfRouters[_universalRouter] = true;
listOfRouters[DEADADDRESS] = true;
routerRestrict = false;
feeAddress = _feeAddress;
_initializeOwner(msg.sender);
}
function setPublicLaunches(bool _publicLaunches) external onlyOwner {
publicLaunches = _publicLaunches;
}
function setCollectionOwnerLaunches(
bool _collectionOwnerLaunches
) external onlyOwner {
collectionOwnerLaunches = _collectionOwnerLaunches;
}
function setRouter(address _router, bool status) external onlyOwner {
listOfRouters[_router] = status;
}
function setRouterRestrict(bool status) external onlyOwner {
routerRestrict = status;
}
function updateFeeToLaunch(uint256 _feeToLaunch) external onlyOwner {
feeToLaunch = _feeToLaunch;
}
function updateHookAddress(address _hookAddress) external onlyOwner {
hookAddress = _hookAddress;
listOfRouters[hookAddress] = true;
}
function updateTokenName(
address tokenStrategy,
string memory tokenName
) external onlyOwner {
ITokenStrategy(tokenStrategy).updateName(tokenName);
}
function updateTokenSymbol(
address tokenStrategy,
string memory tokenSymbol
) external onlyOwner {
ITokenStrategy(tokenStrategy).updateSymbol(tokenSymbol);
}
function updatePriceMultiplier(
address tokenStrategy,
uint256 newMultiplier
) external onlyOwner {
ITokenStrategy(tokenStrategy).setPriceMultiplier(newMultiplier);
}
function updateTargetFeeBuyToken(
address tokenStrategy,
uint256 newTargetFee
) external onlyOwner {
ITokenStrategy(tokenStrategy).setTargetFeeBuyToken(newTargetFee);
}
function withdrawAllFees() external onlyOwner {
payable(msg.sender).transfer(address(this).balance);
}
function withdrawERC20Fees(address token) external onlyOwner {
IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this)));
}
function _loadLiquidity(address _token) internal {
loadingLiquidity = true;
PoolKey memory key = PoolKey(
Currency.wrap(address(0)), // ETH
Currency.wrap(_token), // TokenStrategy Token
0, // lpFee
60, // tickSpacing
IHooks(hookAddress)
);
int24 tickLower = TickMath.minUsableTick(60);
int24 tickUpper = int24(175020);
// Hardcoded from LiquidityAmounts.getLiquidityForAmounts
uint128 liquidity = 158372218983990412488087;
uint256 amount0Max = 2; // 1 wei + 1 wei
uint256 amount1Max = 1_000_000_000 * 10 ** 18 + 1 wei;
(
bytes memory actions,
bytes[] memory mintParams
) = _mintLiquidityParams(
key,
tickLower,
tickUpper,
liquidity,
amount0Max,
amount1Max,
address(this),
new bytes(0)
);
bytes[] memory params = new bytes[](2);
params[0] = abi.encodeWithSelector(
posm.initializePool.selector,
key,
uint160(501082896750095888663770159906816), // startingPrice: 10e18 ETH = 1_000_000_000e18 TOKEN
new bytes(0)
);
params[1] = abi.encodeWithSelector(
posm.modifyLiquidities.selector,
abi.encode(actions, mintParams),
block.timestamp + 60
);
permit2.approve(
_token,
address(posm),
type(uint160).max,
type(uint48).max
);
posm.multicall{value: amount0Max}(params);
loadingLiquidity = false;
}
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);
}
function _buyTokens(
uint256 amountIn,
address nftStrategy,
address caller
) internal {
deployerBuying = true;
PoolKey memory key = PoolKey(
Currency.wrap(address(0)),
Currency.wrap(nftStrategy),
0,
60,
IHooks(hookAddress)
);
router.swapExactTokensForTokens{value: amountIn}(
amountIn,
0,
true,
key,
"",
caller,
block.timestamp
);
deployerBuying = false;
}
function migrate(address[] memory _to, uint256[] memory _amount, TokenStrategy tokenStrategy) external payable onlyOwner {
if (migrated) revert MigrationAlreadyCompleted();
migrated = true;
// buy with fee 0
deployerBuying = true;
uint256 totalAmount = 0;
for (uint256 i = 0; i < _amount.length; i++) {
totalAmount += _amount[i];
}
uint256 amountTokenBefore = tokenStrategy.balanceOf(address(this));
uint256 ethBalanceBefore = address(this).balance;
PoolKey memory key1 = PoolKey(
Currency.wrap(address(0)),
Currency.wrap(address(tokenStrategy)),
0,
60,
IHooks(tokenStrategy.hookAddress())
);
router.swapTokensForExactTokens{value: msg.value}(
totalAmount,
msg.value,
true,
key1,
"",
address(this),
block.timestamp
);
uint256 amountTokenAfter = tokenStrategy.balanceOf(address(this));
uint256 amountToken = amountTokenAfter - amountTokenBefore;
if (amountToken < totalAmount) revert NotEnoughToken();
uint256 ethAfterSwap = address(this).balance;
uint256 ethUsed = ethBalanceBefore - ethAfterSwap;
uint256 unusedEth = msg.value - ethUsed;
for (uint256 i = 0; i < _to.length; i++) {
tokenStrategy.transfer(_to[i], _amount[i]);
}
deployerBuying = false;
// buy with default fee
PoolKey memory key2 = PoolKey(
Currency.wrap(address(0)),
Currency.wrap(address(tokenStrategy)),
0,
60,
IHooks(tokenStrategy.hookAddress())
);
router.swapExactTokensForTokens{value: unusedEth}(
unusedEth,
0,
true,
key2,
"",
DEADADDRESS,
block.timestamp
);
}
function ownerLaunchNFTStrategy(
address nftStrategy,
string memory tokenName,
string memory tokenSymbol,
address collectionOwner
) external payable onlyOwner returns (TokenStrategy) {
// Validate the parameters passed
if (hookAddress == address(0)) revert HookNotSet();
if (nftToTokenStrategy[nftStrategy] != address(0)) revert CollectionAlreadyLaunched();
if (nftStrategy.code.length == 0) revert NotAContract();
TokenStrategy tokenStrategy = new TokenStrategy(
address(this),
hookAddress,
router,
nftStrategy,
tokenName,
tokenSymbol,
owner()
);
nftToTokenStrategy[nftStrategy] = address(tokenStrategy);
tokenToNftStrategy[address(tokenStrategy)] = nftStrategy;
_loadLiquidity(address(tokenStrategy));
ITokenStrategyHook(hookAddress).adminUpdateFeeAddress(
address(tokenStrategy),
collectionOwner
);
emit TokenStrategyLaunched(
nftStrategy,
address(tokenStrategy),
tokenName,
tokenSymbol
);
return tokenStrategy;
}
function launchNFTStrategy(
address tokenWorkStrategy,
string memory tokenName,
string memory tokenSymbol
) external payable nonReentrant returns (TokenStrategy) {
if (hookAddress == address(0)) revert HookNotSet();
if (nftToTokenStrategy[tokenWorkStrategy] != address(0))
revert CollectionAlreadyLaunched();
if (msg.value != feeToLaunch) revert WrongEthAmount();
if (!publicLaunches && !collectionOwnerLaunches) revert CannotLaunch();
address collectionOwnerFromContract;
if (!publicLaunches && msg.sender != collectionOwnerFromContract)
revert GatedByCollectionOwner();
TokenStrategy tokenStrategy = new TokenStrategy(
address(this),
hookAddress,
router,
tokenWorkStrategy,
tokenName,
tokenSymbol,
owner()
);
nftToTokenStrategy[tokenWorkStrategy] = address(tokenStrategy);
tokenToNftStrategy[address(tokenStrategy)] = tokenWorkStrategy;
_loadLiquidity(address(tokenStrategy));
if (collectionOwnerFromContract != address(0)) {
ITokenStrategyHook(hookAddress).adminUpdateFeeAddress(
address(tokenStrategy),
collectionOwnerFromContract
);
} else {
ITokenStrategyHook(hookAddress).adminUpdateFeeAddress(
address(tokenStrategy),
feeAddress
);
}
_buyTokens(initialBuy, address(tokenStrategy), msg.sender);
uint256 ethToSend = msg.value - ethToPair - initialBuy;
SafeTransferLib.forceSafeTransferETH(feeAddress, ethToSend);
emit TokenStrategyLaunched(
tokenWorkStrategy,
address(tokenStrategy),
tokenName,
tokenSymbol
);
return tokenStrategy;
}
function validTransfer(
address from,
address to,
address tokenAddress
) external view returns (bool) {
if (!routerRestrict) return true;
bool userToUser = !listOfRouters[from] && !listOfRouters[to];
if (userToUser && (from != tokenAddress && to != tokenAddress)) {
if (from == address(poolManager)) return true;
if (to == address(poolManager)) {
return
ITokenStrategy(tokenAddress).midSwap() || loadingLiquidity;
}
return false;
}
return true;
}
receive() external payable {
}
}
"
},
"node_modules/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();
_;
}
}
"
},
"contracts/TokenStrategy.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Ownable} from "solady/src/auth/Ownable.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "solady/src/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 "../lib/v4-router/interfaces/IUniswapV4Router04.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import "./Interfaces.sol";
contract TokenStrategy is ERC20, ReentrancyGuard, Ownable {
/*********************************************************************************************************************************
* ██████╗ ███╗ ██╗██╗ ██╗███████╗████████╗██████╗ ███████╗████████╗██████╗ █████╗ ████████╗███████╗ ██████╗ ██╗ ██╗ *
* ██╔══██╗████╗ ██║██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝██╔════╝ ╚██╗ ██╔╝ *
* ██████╔╝██╔██╗ ██║█████╔╝ ███████╗ ██║ ██████╔╝ ███████╗ ██║ ██████╔╝███████║ ██║ █████╗ ██║ ███╗ ╚████╔╝ *
* ██╔═══╝ ██║╚██╗██║██╔═██╗ ╚════██║ ██║ ██╔══██╗ ╚════██║ ██║ ██╔══██╗██╔══██║ ██║ ██╔══╝ ██║ ██║ ╚██╔╝ *
* ██║ ██║ ╚████║██║ ██╗███████║ ██║ ██║ ██║ ███████║ ██║ ██║ ██║██║ ██║ ██║ ███████╗╚██████╔╝ ██║ *
* ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ *
**********************************************************************************************************************************/
/// @notice The main NFTStrategy standard address
INFTStrategy public nftStrategy;
string tokenName;
string tokenSymbol;
/// @notice The maximum supply of the ERC20 token (1 billion)
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 1e18;
/// @notice The dead address for burning TokenStrategy
address public constant DEADADDRESS = 0x000000000000000000000000000000000000dEaD;
address public immutable PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
/// @notice The target fee for buying the token
uint256 public targetFeeBuyToken = 5 ether;
/// @notice The price multiplier for the token (1.2x)
uint256 public priceMultiplier = 1200;
uint256 public percentReward = 1; // 0.1%
uint256 public lastBuyBlock;
uint256 public lastSellBlock;
struct TokenOrder {
/// @notice The amount of ETH used to buy the token
uint256 purchaseAmount;
/// @notice The target price for the token
uint256 targetPrice;
/// @notice The purchase time for the token
uint256 purchaseTime;
/// @notice The amount of token bought
uint256 tokenAmount;
/// @notice The order is active or not
bool isActive;
}
/// @notice The address of the hook associated with this TokenStrategy
address public immutable hookAddress;
/// @notice The address of the factory associated with this TokenStrategy
address public immutable factory;
IUniswapV4Router04 private immutable router;
uint256 public currentFees;
uint256 public ethToTwap;
uint256 public twapIncrement = 1 ether;
uint256 public twapDelayInBlocks = 1;
uint256 public lastTwapBlock;
bool public midSwap;
mapping(uint256 => TokenOrder) public tokenOrders;
uint256 public nextOrderId;
event TokenOrderCreated(
uint256 indexed orderId,
uint256 purchaseAmount,
uint256 targetPrice,
uint256 tokenAmount,
uint256 purchaseTime
);
event TokenOrderSold(
uint256 indexed orderId,
uint256 ethReceived,
uint256 tokenAmount,
address caller
);
event AllowanceIncreased(uint256 amount);
error OrderNotForSale();
error TokenPriceTooLow();
error InvalidMultiplier();
error NoETHToTwap();
error TwapDelayNotMet();
error NotEnoughEth();
error NotEnoughToken();
error NotFactory();
error OnlyHook();
error NotValidRouter();
error NotOwner();
error NotEnoughFees();
error TooSoon();
constructor(
address _factory,
address _hook,
IUniswapV4Router04 _router,
address _nftStrategy,
string memory _tokenName,
string memory _tokenSymbol,
address _owner
) {
factory = _factory;
router = _router;
hookAddress = _hook;
tokenName = _tokenName;
tokenSymbol = _tokenSymbol;
nftStrategy = INFTStrategy(_nftStrategy);
_initializeOwner(_owner);
_mint(factory, MAX_SUPPLY);
nftStrategy.approve(PERMIT2_ADDRESS, type(uint256).max);
IPermit2(PERMIT2_ADDRESS).approve(
_nftStrategy,
address(router),
type(uint160).max,
type(uint48).max
);
lastBuyBlock = block.number;
lastSellBlock = block.number;
}
function name() public view override returns (string memory) {
return tokenName;
}
function symbol() public view override returns (string memory) {
return tokenSymbol;
}
function updateName(string memory _tokenName) external {
if (msg.sender != factory) revert NotFactory();
tokenName = _tokenName;
}
function updateSymbol(string memory _tokenSymbol) external {
if (msg.sender != factory) revert NotFactory();
tokenSymbol = _tokenSymbol;
}
function setPriceMultiplier(uint256 _newMultiplier) external {
if (msg.sender != factory) revert NotFactory();
if (_newMultiplier < 1100 || _newMultiplier > 10000)
revert InvalidMultiplier();
priceMultiplier = _newMultiplier;
}
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
/* MECHANISM FUNCTIONS */
/* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
//set target fee for buying the token
function setTargetFeeBuyToken(uint256 _newTargetFee) external {
if (msg.sender != factory) revert NotFactory();
targetFeeBuyToken = _newTargetFee;
}
function addFees() external payable nonReentrant {
if (msg.sender != hookAddress) revert OnlyHook();
currentFees += msg.value;
}
function buyTokenAndCreateOrder() external nonReentrant returns (uint256) {
if (block.number < lastBuyBlock + 1) revert TooSoon();
uint256 reward = (targetFeeBuyToken * percentReward) / 1000;
if (currentFees < targetFeeBuyToken + reward) {
revert NotEnoughFees();
}
uint256 orderId = _buyTokenAndCreateOrder(targetFeeBuyToken);
SafeTransferLib.forceSafeTransferETH(msg.sender, reward);
lastBuyBlock = block.number;
return orderId;
}
function _buyTokenAndCreateOrder(uint256 purchaseAmount) internal returns (uint256 orderId) {
if (purchaseAmount > currentFees) {
revert NotEnoughEth();
}
uint256 tokenBalanceBefore = nftStrategy.balanceOf(address(this));
PoolKey memory key = PoolKey(
Currency.wrap(address(0)),
Currency.wrap(address(nftStrategy)),
0,
60,
IHooks(nftStrategy.hookAddress())
);
router.swapExactTokensForTokens{value: purchaseAmount}(
purchaseAmount,
0,
true,
key,
"",
address(this),
block.timestamp
);
uint256 tokenBalanceAfter = nftStrategy.balanceOf(address(this));
uint256 tokenAmount = tokenBalanceAfter - tokenBalanceBefore;
currentFees -= purchaseAmount;
uint256 targetPrice = (purchaseAmount * priceMultiplier) / 1000;
orderId = nextOrderId;
tokenOrders[orderId] = TokenOrder({
purchaseAmount: purchaseAmount,
targetPrice: targetPrice,
purchaseTime: block.timestamp,
tokenAmount: tokenAmount,
isActive: true
});
nextOrderId++;
emit TokenOrderCreated(
orderId,
purchaseAmount,
targetPrice,
tokenAmount,
block.timestamp
);
return orderId;
}
function setMidSwap(bool value) external {
if (msg.sender != hookAddress) revert OnlyHook();
midSwap = value;
}
function sellTargetOrder(uint256 orderId) external nonReentrant {
if (block.number < lastSellBlock + 1) revert TooSoon();
TokenOrder storage order = tokenOrders[orderId];
if (!order.isActive) revert OrderNotForSale();
if (nftStrategy.balanceOf(address(this)) < order.tokenAmount) revert NotEnoughToken();
uint256 ethBalanceBefore = address(this).balance;
PoolKey memory key = PoolKey(
Currency.wrap(address(0)),
Currency.wrap(address(nftStrategy)),
0,
60,
IHooks(nftStrategy.hookAddress())
);
router.swapExactTokensForTokens(
order.tokenAmount,
order.targetPrice,
false,
key,
"",
address(this),
block.timestamp
);
// Calculate ETH received
uint256 ethReceived = address(this).balance - ethBalanceBefore;
if (ethReceived < order.targetPrice) revert TokenPriceTooLow();
order.isActive = false;
uint256 reward = (ethReceived * percentReward) / 1000;
SafeTransferLib.forceSafeTransferETH(msg.sender, reward);
ethToTwap += (ethReceived - reward);
lastSellBlock = block.number;
emit TokenOrderSold(orderId, ethReceived, order.tokenAmount, msg.sender);
}
function depositEthToTwap() external payable {
ethToTwap += msg.value;
}
function processTokenTwap() external {
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;
}
uint256 reward = (burnAmount * percentReward) / 1000;
burnAmount -= reward;
// Update state
ethToTwap -= burnAmount + reward;
lastTwapBlock = block.number;
_buyAndBurnTokens(burnAmount);
SafeTransferLib.forceSafeTransferETH(msg.sender, reward);
}
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,
"",
DEADADDRESS,
block.timestamp
);
}
function updatePercentReward(uint256 _newPercentReward) external onlyOwner {
percentReward = _newPercentReward;
}
function _afterTokenTransfer(address from, address to, uint256) internal view override {
if (!ITokenStrategyFactory(factory).routerRestrict() || midSwap) return;
if (!ITokenStrategyFactory(factory).validTransfer(from, to, address(this))) {
revert NotValidRouter();
}
}
function withdrawAllFeesEmergency() external onlyOwner {
SafeTransferLib.forceSafeTransferETH(msg.sender, address(this).balance);
}
function withdrawERC20Emergency(address token) external onlyOwner {
IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this)));
}
receive() external payable {}
}
"
},
"node_modules/solady/src/utils/SafeTransferLib.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeTransferLib.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @author Permit2 operations from (https://github.com/Uniswap/permit2/blob/main/src/libraries/Permit2Lib.sol)
///
/// @dev Note:
/// - For ETH transfers, please use `forceSafeTransferETH` for DoS protection.
library SafeTransferLib {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The ETH transfer has failed.
error ETHTransferFailed();
/// @dev The ERC20 `transferFrom` has failed.
error TransferFromFailed();
/// @dev The ERC20 `transfer` has failed.
error TransferFailed();
/// @dev The ERC20 `approve` has failed.
error ApproveFailed();
/// @dev The ERC20 `totalSupply` query has failed.
error TotalSupplyQueryFailed();
/// @dev The Permit2 operation has failed.
error Permit2Failed();
/// @dev The Permit2 amount must be less than `2**160 - 1`.
error Permit2AmountOverflow();
/// @dev The Permit2 approve operation has failed.
error Permit2ApproveFailed();
/// @dev The Permit2 lockdown operation has failed.
error Permit2LockdownFailed();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Suggested gas stipend for contract receiving ETH that disallows any storage writes.
uint256 internal constant GAS_STIPEND_NO_STORAGE_WRITES = 2300;
/// @dev Suggested gas stipend for contract receiving ETH to perform a few
/// storage reads and writes, but low enough to prevent griefing.
uint256 internal constant GAS_STIPEND_NO_GRIEF = 100000;
/// @dev The unique EIP-712 domain separator for the DAI token contract.
bytes32 internal constant DAI_DOMAIN_SEPARATOR =
0xdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7;
/// @dev The address for the WETH9 contract on Ethereum mainnet.
address internal constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
/// @dev The canonical Permit2 address.
/// [Github](https://github.com/Uniswap/permit2)
/// [Etherscan](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3)
address internal constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
/// @dev The canonical address of the `SELFDESTRUCT` ETH mover.
/// See: https://gist.github.com/Vectorized/1cb8ad4cf393b1378e08f23f79bd99fa
/// [Etherscan](https://etherscan.io/address/0x00000000000073c48c8055bD43D1A53799176f0D)
address internal constant ETH_MOVER = 0x00000000000073c48c8055bD43D1A53799176f0D;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ETH OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
// If the ETH transfer MUST succeed with a reasonable gas budget, use the force variants.
//
// The regular variants:
// - Forwards all remaining gas to the target.
// - Reverts if the target reverts.
// - Reverts if the current contract has insufficient balance.
//
// The force variants:
// - Forwards with an optional gas stipend
// (defaults to `GAS_STIPEND_NO_GRIEF`, which is sufficient for most cases).
// - If the target reverts, or if the gas stipend is exhausted,
// creates a temporary contract to force send the ETH via `SELFDESTRUCT`.
// Future compatible with `SENDALL`: https://eips.ethereum.org/EIPS/eip-4758.
// - Reverts if the current contract has insufficient balance.
//
// The try variants:
// - Forwards with a mandatory gas stipend.
// - Instead of reverting, returns whether the transfer succeeded.
/// @dev Sends `amount` (in wei) ETH to `to`.
function safeTransferETH(address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gas(), to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Sends all the ETH in the current contract to `to`.
function safeTransferAllETH(address to) internal {
/// @solidity memory-safe-assembly
assembly {
// Transfer all the ETH and check if it succeeded or not.
if iszero(call(gas(), to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Force sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
function forceSafeTransferETH(address to, uint256 amount, uint256 gasStipend) internal {
/// @solidity memory-safe-assembly
assembly {
if lt(selfbalance(), amount) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
if iszero(call(gasStipend, to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends all the ETH in the current contract to `to`, with a `gasStipend`.
function forceSafeTransferAllETH(address to, uint256 gasStipend) internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gasStipend, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends `amount` (in wei) ETH to `to`, with `GAS_STIPEND_NO_GRIEF`.
function forceSafeTransferETH(address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
if lt(selfbalance(), amount) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
if iszero(call(GAS_STIPEND_NO_GRIEF, to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends all the ETH in the current contract to `to`, with `GAS_STIPEND_NO_GRIEF`.
function forceSafeTransferAllETH(address to) internal {
/// @solidity memory-safe-assembly
assembly {
// forgefmt: disable-next-item
if iszero(call(GAS_STIPEND_NO_GRIEF, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
function trySafeTransferETH(address to, uint256 amount, uint256 gasStipend)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
success := call(gasStipend, to, amount, codesize(), 0x00, codesize(), 0x00)
}
}
/// @dev Sends all the ETH in the current contract to `to`, with a `gasStipend`.
function trySafeTransferAllETH(address to, uint256 gasStipend)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
success := call(gasStipend, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)
}
}
/// @dev Force transfers ETH to `to`, without triggering the fallback (if any).
/// This method attempts to use a separate contract to send via `SELFDESTRUCT`,
/// and upon failure, deploys a minimal vault to accrue the ETH.
function safeMoveETH(address to, uint256 amount) internal returns (address vault) {
/// @solidity memory-safe-assembly
assembly {
to := shr(96, shl(96, to)) // Clean upper 96 bits.
for { let mover := ETH_MOVER } iszero(eq(to, address())) {} {
let selfBalanceBefore := selfbalance()
if or(lt(selfBalanceBefore, amount), eq(to, mover)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
if extcodesize(mover) {
let balanceBefore := balance(to) // Check via delta, in case `SELFDESTRUCT` is bricked.
mstore(0x00, to)
pop(call(gas(), mover, amount, 0x00, 0x20, codesize(), 0x00))
// If `address(to).balance >= amount + balanceBefore`, skip vault workflow.
if iszero(lt(balance(to), add(amount, balanceBefore))) { break }
// Just in case `SELFDESTRUCT` is changed to not revert and do nothing.
if lt(selfBalanceBefore, selfbalance()) { invalid() }
}
let m := mload(0x40)
// If the mover is missing or bricked, deploy a minimal vault
// that withdraws all ETH to `to` when being called only by `to`.
// forgefmt: disable-next-item
mstore(add(m, 0x20), 0x33146025575b600160005260206000f35b3d3d3d3d47335af1601a5760003dfd)
mstore(m, or(to, shl(160, 0x6035600b3d3960353df3fe73)))
// Compute and store the bytecode hash.
mstore8(0x00, 0xff) // Write the prefix.
mstore(0x35, keccak256(m, 0x40))
mstore(0x01, shl(96, address())) // Deployer.
mstore(0x15, 0) // Salt.
vault := keccak256(0x00, 0x55)
pop(call(gas(), vault, amount, codesize(), 0x00, codesize(), 0x00))
// The vault returns a single word on success. Failure reverts with empty data.
if iszero(returndatasize()) {
if iszero(create2(0, m, 0x40, 0)) { revert(codesize(), codesize()) } // For gas estimation.
}
mstore(0x40, m) // Restore the free memory pointer.
break
}
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERC20 OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
/// Reverts upon failure.
///
/// The `from` account must have at least `amount` approved for
/// the current contract to manage.
function safeTransferFrom(address token, address from, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, amount) // Store the `amount` argument.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x23b872dd000000000000000000000000) // `transferFrom(address,address,uint256)`.
let success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
///
/// The `from` account must have at least `amount` approved for the current contract to manage.
function trySafeTransferFrom(address token, address from, address to, uint256 amount)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, amount) // Store the `amount` argument.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x23b872dd000000000000000000000000) // `transferFrom(address,address,uint256)`.
success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
success := lt(or(iszero(extcodesize(token)), returndatasize()), success)
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends all of ERC20 `token` from `from` to `to`.
/// Reverts upon failure.
///
/// The `from` account must have their entire balance approved for the current contract to manage.
function safeTransferAllFrom(address token, address from, address to)
internal
returns (uint256 amount)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x70a08231000000000000000000000000) // `balanceOf(address)`.
// Read the balance, reverting upon failure.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x1c, 0x24, 0x60, 0x20)
)
) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
mstore(0x00, 0x23b872dd) // `transferFrom(address,address,uint256)`.
amount := mload(0x60) // The `amount` is already at 0x60. We'll need to return it.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends `amount` of ERC20 `token` from the current contract to `to`.
/// Reverts upon failure.
function safeTransfer(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0xa9059cbb000000000000000000000000) // `transfer(address,uint256)`.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sends all of ERC20 `token` from the current contract to `to`.
/// Reverts upon failure.
function safeTransferAll(address token, address to) internal returns (uint256 amount) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0x70a08231) // Store the function selector of `balanceOf(address)`.
mstore(0x20, address()) // Store the address of the current contract.
// Read the balance, reverting upon failure.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x1c, 0x24, 0x34, 0x20)
)
) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
mstore(0x14, to) // Store the `to` argument.
amount := mload(0x34) // The `amount` is already at 0x34. We'll need to return it.
mstore(0x00, 0xa9059cbb000000000000000000000000) // `transfer(address,uint256)`.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract.
/// Reverts upon failure.
function safeApprove(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x3e3f8f73) // `ApproveFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract.
/// If the initial attempt to approve fails, attempts to reset the approved amount to zero,
/// then retries the approval again (some tokens, e.g. USDT, requires this).
/// Reverts upon failure.
function safeApproveWithRetry(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
// Perform the approval, retrying upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x34, 0) // Store 0 for the `amount`.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
pop(call(gas(), token, 0, 0x10, 0x44, codesize(), 0x00)) // Reset the approval.
mstore(0x34, amount) // Store ba
Submitted on: 2025-10-15 09:12:13
Comments
Log in to comment.
No comments yet.