Last active
August 10, 2026 08:05
-
-
Save Amxx/0364165ca7bede374611159057fa5488 to your computer and use it in GitHub Desktop.
ERC20UniversalWrapper + Factory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| // Compatible with OpenZeppelin Contracts ^5.7.0 | |
| pragma solidity ^0.8.27; | |
| import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
| import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | |
| import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; | |
| import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | |
| import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; | |
| import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | |
| import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; | |
| import {ERC20FlashMintUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20FlashMintUpgradeable.sol"; | |
| import {ERC1363Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC1363Upgradeable.sol"; | |
| import {ERC3009Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC3009Upgradeable.sol"; | |
| import {ERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; | |
| /// @dev Settings flag enabling a variable (vault-like) exchange rate between the underlying asset and the wrapper. | |
| uint8 constant _VARIABLE_RATE_FLAG = 0x01; | |
| /// @dev Settings flag enabling {IERC3156FlashLender} flash loans of the wrapper token. | |
| uint8 constant _FLASH_MINT_FLAG = 0x02; | |
| /** | |
| * @dev Wrapper for any {IERC20} token, deployable as a minimal clone by {ERC20UniversalWrapperFactory}. | |
| * | |
| * This contract is meant to be used as the implementation behind {Clones}' "clone with immutable args" proxies. It | |
| * has no constructor and no initializer: the underlying asset and the feature switches are not stored in storage, | |
| * but appended to the clone's bytecode and read back on demand by {_parseArgs}. Consequently, a given (asset, | |
| * settings) pair maps to a single deterministic wrapper address, and deploying a wrapper costs no storage writes. | |
| * | |
| * The wrapper exposes {IERC20Permit}, {IERC1363}, {IERC3009} and {IERC4626} on top of the wrapped asset. Two | |
| * behaviors are configurable per clone, through the settings byte: | |
| * | |
| * * {_VARIABLE_RATE_FLAG}: when unset, the wrapper behaves like a 1:1 wrapper (as {ERC20Wrapper} would). When set, | |
| * the wrapper behaves like a normal {ERC4626} vault, whose exchange rate follows the assets it holds. | |
| * * {_FLASH_MINT_FLAG}: when set, the wrapper token can be flash-minted. This is only available for 1:1 wrappers, | |
| * as flash minting shares would otherwise interfere with the vault's exchange rate. | |
| * | |
| * The name and symbol are derived from the underlying asset's metadata, and are therefore not stored either. | |
| * | |
| * NOTE: The underlying asset's metadata is read on every call to {name}, {symbol} and {decimals}. Wrapping an asset | |
| * whose {IERC20Metadata} functions revert (or are not implemented) will make {name} and {symbol} revert too. | |
| * {decimals} degrades gracefully to 18. | |
| */ | |
| contract ERC20UniversalWrapper is | |
| ERC20PermitUpgradeable, | |
| ERC20FlashMintUpgradeable, | |
| ERC1363Upgradeable, | |
| ERC3009Upgradeable, | |
| ERC4626Upgradeable | |
| { | |
| /// @dev The clone's immutable args are missing or too short to hold the underlying asset and the settings byte. | |
| error MissingImmutableArgs(); | |
| // No constructor + no initializer | |
| // --- | |
| // All storage variables usually set in the constructor are instead set via immutable args, which are read from | |
| // the clone's bytecode. | |
| // =============================================================================================================== | |
| // = Overall wrapper behavior = | |
| // =============================================================================================================== | |
| /// @dev Address of the underlying token being wrapped, read from the clone's immutable args. | |
| function asset() public view override returns (address _asset) { | |
| (_asset, ) = _parseArgs(); | |
| } | |
| /// @dev Name of the wrapper: the underlying asset's name, prefixed with `"Wrapped "`. | |
| function name() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { | |
| return string.concat("Wrapped ", IERC20Metadata(asset()).name()); | |
| } | |
| /// @dev Symbol of the wrapper: the underlying asset's symbol, prefixed with `"w"`. | |
| function symbol() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { | |
| return string.concat("w", IERC20Metadata(asset()).symbol()); | |
| } | |
| /// @dev Decimals of the wrapper, mirroring the underlying asset's decimals, or 18 if the asset doesn't expose any. | |
| function decimals() public view override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) { | |
| (bool success, uint8 _decimals) = SafeERC20.tryGetDecimals(IERC20Metadata(asset())); | |
| return success ? _decimals : 18; | |
| } | |
| /// @dev EIP-712 domain name, matching {name} so that each clone gets its own domain separator. | |
| function _EIP712Name() internal view override returns (string memory) { | |
| return name(); | |
| } | |
| /// @dev EIP-712 domain version. | |
| function _EIP712Version() internal pure override returns (string memory) { | |
| return "1"; | |
| } | |
| // =============================================================================================================== | |
| // = Feature switches = | |
| // =============================================================================================================== | |
| /** | |
| * @dev See {ERC20FlashMint-maxFlashLoan}. Returns 0 unless this clone was deployed with the | |
| * {_FLASH_MINT_FLAG} setting, effectively disabling flash loans. | |
| */ | |
| function maxFlashLoan(address token) public view override returns (uint256) { | |
| (, uint8 settings) = _parseArgs(); | |
| return settings & _FLASH_MINT_FLAG == 0 ? 0 : super.maxFlashLoan(token); | |
| } | |
| /** | |
| * @dev Internal conversion function (from assets to shares) with support for rounding direction. Unless this | |
| * clone was deployed with the {_VARIABLE_RATE_FLAG} setting, the rate is fixed to 1:1. | |
| */ | |
| function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) { | |
| (, uint8 settings) = _parseArgs(); | |
| return settings & _VARIABLE_RATE_FLAG == 0 ? assets : super._convertToShares(assets, rounding); | |
| } | |
| /** | |
| * @dev Internal conversion function (from shares to assets) with support for rounding direction. Unless this | |
| * clone was deployed with the {_VARIABLE_RATE_FLAG} setting, the rate is fixed to 1:1. | |
| */ | |
| function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) { | |
| (, uint8 settings) = _parseArgs(); | |
| return settings & _VARIABLE_RATE_FLAG == 0 ? shares : super._convertToAssets(shares, rounding); | |
| } | |
| /** | |
| * @dev Reads the clone's immutable args, and decodes them into the underlying asset's address and the settings | |
| * byte. Reverts with {MissingImmutableArgs} if the args are shorter than the expected 21 bytes. | |
| */ | |
| function _parseArgs() private view returns (address underlying, uint8 settings) { | |
| bytes memory cloneArgs = Clones.fetchCloneArgs(address(this)); | |
| require(cloneArgs.length >= 21, MissingImmutableArgs()); | |
| bytes21 data = bytes21(cloneArgs); | |
| return (address(bytes20(data)), uint8(bytes1(data << 160))); | |
| } | |
| } | |
| /** | |
| * @dev Factory for {ERC20UniversalWrapper} clones. | |
| * | |
| * Deploying this factory also deploys the single {ERC20UniversalWrapper} implementation that all wrappers delegate | |
| * to. Wrappers are then deployed as "clones with immutable args", using a zero salt, which makes the resulting | |
| * address a pure function of the implementation, the underlying asset and the settings. Deployment is therefore | |
| * permissionless and idempotent: anyone can deploy the wrapper for a given configuration, but only once. Use | |
| * {predict} to compute that address ahead of (or without) deployment. | |
| */ | |
| contract ERC20UniversalWrapperFactory { | |
| /// @dev Address of the {ERC20UniversalWrapper} implementation that every wrapper deployed here delegates to. | |
| address public immutable implementation = address(new ERC20UniversalWrapper()); | |
| /// @dev A wrapper for `underlying`, with the given `settings`, was deployed at `wrapper`. | |
| event ERC20UniversalWrapperDeployed(address indexed wrapper, address indexed underlying, uint8 settings); | |
| /** | |
| * @dev Deploys the {ERC20UniversalWrapper} for the given `underlying` token and settings, and returns its | |
| * address. | |
| * | |
| * Setting `variableRate` makes the wrapper behave like a normal {ERC4626} vault instead of a 1:1 wrapper. | |
| * Setting `flashMintable` enables flash minting of the wrapper token. These two options are mutually exclusive. | |
| * | |
| * NOTE: This reverts if the corresponding wrapper was already deployed. Since the address only depends on the | |
| * arguments, that existing wrapper (retrievable using {predict}) is exactly the one this call would produce. | |
| */ | |
| function deploy(address underlying, bool variableRate, bool flashMintable) external returns (address) { | |
| // prepare settings | |
| uint8 settings = _makeSettings(variableRate, flashMintable); | |
| // deploy wrapper | |
| address instance = Clones.cloneDeterministicWithImmutableArgs( | |
| implementation, | |
| abi.encodePacked(underlying, settings), | |
| bytes32(0) | |
| ); | |
| emit ERC20UniversalWrapperDeployed(instance, underlying, settings); | |
| return instance; | |
| } | |
| /** | |
| * @dev Returns the address at which the {ERC20UniversalWrapper} for the given `underlying` token and settings | |
| * is (or would be) deployed. See {deploy} for the meaning of `variableRate` and `flashMintable`. | |
| */ | |
| function predict(address underlying, bool variableRate, bool flashMintable) external view returns (address) { | |
| return | |
| Clones.predictDeterministicAddressWithImmutableArgs( | |
| implementation, | |
| abi.encodePacked(underlying, _makeSettings(variableRate, flashMintable)), | |
| bytes32(0) | |
| ); | |
| } | |
| /** | |
| * @dev Packs the feature switches into the settings byte stored in the clone's immutable args. Reverts if the | |
| * requested combination is invalid: a variable rate wrapper cannot be flash mintable, as flash minting shares | |
| * would interfere with the vault's exchange rate. | |
| */ | |
| function _makeSettings(bool variableRate, bool flashMintable) private pure returns (uint8 settings) { | |
| require(!variableRate || !flashMintable, "ERC20UniversalWrapperFactory: incompatible settings"); | |
| return (variableRate ? _VARIABLE_RATE_FLAG : 0) | (flashMintable ? _FLASH_MINT_FLAG : 0); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment