// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "../tokens/ERC20.sol";
import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "../utils/FixedPointMathLib.sol";
/// @notice Minimal ERC4626 tokenized Vault implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC4626.sol)
abstract contract ERC4626 is ERC20 {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
event Withdraw(
address indexed caller,
address indexed receiver,
address indexed owner,
uint256 assets,
uint256 shares
);
/*//////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
ERC20 public immutable asset;
constructor(
ERC20 _asset,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol, _asset.decimals()) {
asset = _asset;
}
/*//////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LOGIC
//////////////////////////////////////////////////////////////*/
function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
// Check for rounding error since we round down in previewDeposit.
require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");
// Need to transfer before minting or ERC777s could reenter.
asset.safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
afterDeposit(assets, shares);
}
function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) {
assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up.
// Need to transfer before minting or ERC777s could reenter.
asset.safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
afterDeposit(assets, shares);
}
function withdraw(
uint256 assets,
address receiver,
address owner
) public virtual returns (uint256 shares) {
shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up.
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
beforeWithdraw(assets, shares);
_burn(owner, shares);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
asset.safeTransfer(receiver, assets);
}
function redeem(
uint256 shares,
address receiver,
address owner
) public virtual returns (uint256 assets) {
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
// Check for rounding error since we round down in previewRedeem.
require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS");
beforeWithdraw(assets, shares);
_burn(owner, shares);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
asset.safeTransfer(receiver, assets);
}
/*//////////////////////////////////////////////////////////////
ACCOUNTING LOGIC
//////////////////////////////////////////////////////////////*/
function totalAssets() public view virtual returns (uint256);
function convertToShares(uint256 assets) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets());
}
function convertToAssets(uint256 shares) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply);
}
function previewDeposit(uint256 assets) public view virtual returns (uint256) {
return convertToShares(assets);
}
function previewMint(uint256 shares) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? shares : shares.mulDivUp(totalAssets(), supply);
}
function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? assets : assets.mulDivUp(supply, totalAssets());
}
function previewRedeem(uint256 shares) public view virtual returns (uint256) {
return convertToAssets(shares);
}
/*//////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LIMIT LOGIC
//////////////////////////////////////////////////////////////*/
function maxDeposit(address) public view virtual returns (uint256) {
return type(uint256).max;
}
function maxMint(address) public view virtual returns (uint256) {
return type(uint256).max;
}
function maxWithdraw(address owner) public view virtual returns (uint256) {
return convertToAssets(balanceOf[owner]);
}
function maxRedeem(address owner) public view virtual returns (uint256) {
return balanceOf[owner];
}
/*//////////////////////////////////////////////////////////////
INTERNAL HOOKS LOGIC
//////////////////////////////////////////////////////////////*/
function beforeWithdraw(uint256 assets, uint256 shares) internal virtual {}
function afterDeposit(uint256 assets, uint256 shares) internal virtual {}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Arithmetic library with operations for fixed-point numbers.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol)
/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
library FixedPointMathLib {
/*//////////////////////////////////////////////////////////////
SIMPLIFIED FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/
uint256 internal constant MAX_UINT256 = 2**256 - 1;
uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s.
function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down.
}
function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up.
}
function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down.
}
function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up.
}
/*//////////////////////////////////////////////////////////////
LOW LEVEL FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/
function mulDivDown(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// Divide x * y by the denominator.
z := div(mul(x, y), denominator)
}
}
function mulDivUp(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// If x * y modulo the denominator is strictly greater than 0,
// 1 is added to round up the division of x * y by the denominator.
z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator))
}
}
function rpow(
uint256 x,
uint256 n,
uint256 scalar
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
switch x
case 0 {
switch n
case 0 {
// 0 ** 0 = 1
z := scalar
}
default {
// 0 ** n = 0
z := 0
}
}
default {
switch mod(n, 2)
case 0 {
// If n is even, store scalar in z for now.
z := scalar
}
default {
// If n is odd, store x in z for now.
z := x
}
// Shifting right by 1 is like dividing by 2.
let half := shr(1, scalar)
for {
// Shift n right by 1 before looping to halve it.
n := shr(1, n)
} n {
// Shift n right by 1 each iteration to halve it.
n := shr(1, n)
} {
// Revert immediately if x ** 2 would overflow.
// Equivalent to iszero(eq(div(xx, x), x)) here.
if shr(128, x) {
revert(0, 0)
}
// Store x squared.
let xx := mul(x, x)
// Round to the nearest number.
let xxRound := add(xx, half)
// Revert if xx + half overflowed.
if lt(xxRound, xx) {
revert(0, 0)
}
// Set x to scaled xxRound.
x := div(xxRound, scalar)
// If n is even:
if mod(n, 2) {
// Compute z * x.
let zx := mul(z, x)
// If z * x overflowed:
if iszero(eq(div(zx, x), z)) {
// Revert if x is non-zero.
if iszero(iszero(x)) {
revert(0, 0)
}
}
// Round to the nearest number.
let zxRound := add(zx, half)
// Revert if zx + half overflowed.
if lt(zxRound, zx) {
revert(0, 0)
}
// Return properly scaled zxRound.
z := div(zxRound, scalar)
}
}
}
}
}
/*//////////////////////////////////////////////////////////////
GENERAL NUMBER UTILITIES
//////////////////////////////////////////////////////////////*/
function sqrt(uint256 x) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
let y := x // We start y at x, which will help us make our initial estimate.
z := 181 // The "correct" value is 1, but this saves a multiplication later.
// This segment is to get a reasonable initial estimate for the Babylonian method. With a bad
// start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.
// We check y >= 2^(k + 8) but shift right by k bits
// each branch to ensure that if x >= 256, then y >= 256.
if iszero(lt(y, 0x10000000000000000000000000000000000)) {
y := shr(128, y)
z := shl(64, z)
}
if iszero(lt(y, 0x1000000000000000000)) {
y := shr(64, y)
z := shl(32, z)
}
if iszero(lt(y, 0x10000000000)) {
y := shr(32, y)
z := shl(16, z)
}
if iszero(lt(y, 0x1000000)) {
y := shr(16, y)
z := shl(8, z)
}
// Goal was to get z*z*y within a small factor of x. More iterations could
// get y in a tighter range. Currently, we will have y in [256, 256*2^16).
// We ensured y >= 256 so that the relative difference between y and y+1 is small.
// That's not possible if x < 256 but we can just verify those cases exhaustively.
// Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256.
// Correctness can be checked exhaustively for x < 256, so we assume y >= 256.
// Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps.
// For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range
// (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256.
// Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate
// sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18.
// There is no overflow risk here since y < 2^136 after the first branch above.
z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181.
// Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
// If x+1 is a perfect square, the Babylonian method cycles between
// floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor.
// See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division
// Since the ceil is rare, we save gas on the assignment and repeat division in the rare case.
// If you don't care whether the floor or ceil square root is returned, you can remove this statement.
z := sub(z, lt(div(x, z), z))
}
}
function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Mod x by y. Note this will return
// 0 instead of reverting if y is zero.
z := mod(x, y)
}
}
function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) {
/// @solidity memory-safe-assembly
assembly {
// Divide x by y. Note this will return
// 0 instead of reverting if y is zero.
r := div(x, y)
}
}
function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Add 1 to x * y if x % y > 0. Note this will
// return 0 instead of reverting if y is zero.
z := add(gt(mod(x, y), 0), div(x, y))
}
}
}
/// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IWrappedVault {
/// @return assetTokenAddress The address of the asset token
function asset() external view returns (address assetTokenAddress);
/// @return totalManagedAssets The amount of eth controlled by the vault
function totalAssets() external view returns (uint256 totalManagedAssets);
/// @param assets The amount of assets to convert to shares
/// @return shares The value of the given assets in shares
function convertToShares(uint256 assets) external view returns (uint256 shares);
/// @param shares The amount of shares to convert to assets
/// @return assets The value of the given shares in assets
function convertToAssets(uint256 shares) external view returns (uint256 assets);
/// @param reciever The address in question of who would be depositing, doesn't matter in this case
/// @return maxAssets The maximum amount of assets that can be deposited
function maxDeposit(address reciever) external view returns (uint256 maxAssets);
/// @param assets The amount of assets that would be deposited
/// @return shares The amount of shares that would be minted, *under ideal conditions* only
function previewDeposit(uint256 assets) external view returns (uint256 shares);
/// @param assets The amount of WETH which should be deposited
/// @param receiver The address user whom should recieve the mevEth out
/// @return shares The amount of shares minted
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
/// @param reciever The address in question of who would be minting, doesn't matter in this case
/// @return maxShares The maximum amount of shares that can be minted
function maxMint(address reciever) external view returns (uint256 maxShares);
/// @param shares The amount of shares that would be minted
/// @return assets The amount of assets that would be required, *under ideal conditions* only
function previewMint(uint256 shares) external view returns (uint256 assets);
/// @param shares The amount of shares that should be minted
/// @param receiver The address user whom should recieve the mevEth out
/// @return assets The amount of assets deposited
function mint(uint256 shares, address receiver) external returns (uint256 assets);
/// @param owner The address in question of who would be withdrawing
/// @return maxAssets The maximum amount of assets that can be withdrawn
function maxWithdraw(address owner) external view returns (uint256 maxAssets);
/// @param assets The amount of assets that would be withdrawn
/// @return shares The amount of shares that would be burned, *under ideal conditions* only
function previewWithdraw(uint256 assets) external view returns (uint256 shares);
/// @param assets The amount of assets that should be withdrawn
/// @param receiver The address user whom should recieve the mevEth out
/// @param owner The address of the owner of the mevEth
/// @return shares The amount of shares burned
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
/// @param owner The address in question of who would be redeeming their shares
/// @return maxShares The maximum amount of shares they could redeem
function maxRedeem(address owner) external view returns (uint256 maxShares);
/// @param shares The amount of shares that would be burned
/// @return assets The amount of assets that would be withdrawn, *under ideal conditions* only
function previewRedeem(uint256 shares) external view returns (uint256 assets);
/// @param shares The amount of shares that should be burned
/// @param receiver The address user whom should recieve the wETH out
/// @param owner The address of the owner of the mevEth
/// @return assets The amount of assets withdrawn
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
/**
* @dev Emitted when a deposit is made, either through mint or deposit
*/
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
/**
* @dev Emitted when a withdrawal is made, either through redeem or withdraw
*/
event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/// @notice Efficient library for creating string representations of integers.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/LibString.sol)
/// @author Modified from Solady (https://github.com/Vectorized/solady/blob/main/src/utils/LibString.sol)
library LibString {
function toString(int256 value) internal pure returns (string memory str) {
if (value >= 0) return toString(uint256(value));
unchecked {
str = toString(uint256(-value));
/// @solidity memory-safe-assembly
assembly {
// Note: This is only safe because we over-allocate memory
// and write the string from right to left in toString(uint256),
// and thus can be sure that sub(str, 1) is an unused memory location.
let length := mload(str) // Load the string length.
// Put the - character at the start of the string contents.
mstore(str, 45) // 45 is the ASCII code for the - character.
str := sub(str, 1) // Move back the string pointer by a byte.
mstore(str, add(length, 1)) // Update the string length.
}
}
}
function toString(uint256 value) internal pure returns (string memory str) {
/// @solidity memory-safe-assembly
assembly {
// The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes
// to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the
// trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes.
let newFreeMemoryPointer := add(mload(0x40), 160)
// Update the free memory pointer to avoid overriding our string.
mstore(0x40, newFreeMemoryPointer)
// Assign str to the end of the zone of newly allocated memory.
str := sub(newFreeMemoryPointer, 32)
// Clean the last word of memory it may not be overwritten.
mstore(str, 0)
// Cache the end of the memory to calculate the length later.
let end := str
// We write the string from rightmost digit to leftmost digit.
// The following is essentially a do-while loop that also handles the zero case.
// prettier-ignore
for { let temp := value } 1 {} {
// Move the pointer 1 byte to the left.
str := sub(str, 1)
// Write the character to the pointer.
// The ASCII index of the '0' character is 48.
mstore8(str, add(48, mod(temp, 10)))
// Keep dividing temp until zero.
temp := div(temp, 10)
// prettier-ignore
if iszero(temp) { break }
}
// Compute and cache the final total length of the string.
let length := sub(end, str)
// Move the pointer 32 bytes leftwards to make room for the length.
str := sub(str, 32)
// Store the string's length at the start of memory allocated for our string.
mstore(str, length)
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable2Step.sol)
pragma solidity ^0.8.20;
import {Ownable} from "./Ownable.sol";
/**
* @dev Contract module which provides access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* This extension of the {Ownable} contract includes a two-step mechanism to transfer
* ownership, where the new owner must call {acceptOwnership} in order to replace the
* old one. This can help prevent common mistakes, such as transfers of ownership to
* incorrect accounts, or to contracts that are unable to interact with the
* permission system.
*
* The initial owner is specified at deployment time in the constructor for `Ownable`. This
* can later be changed with {transferOwnership} and {acceptOwnership}.
*
* This module is used through inheritance. It will make available all functions
* from parent (Ownable).
*/
abstract contract Ownable2Step is Ownable {
address private _pendingOwner;
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
/**
* @dev Returns the address of the pending owner.
*/
function pendingOwner() public view virtual returns (address) {
return _pendingOwner;
}
/**
* @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
* Can only be called by the current owner.
*
* Setting `newOwner` to the zero address is allowed; this can be used to cancel an initiated ownership transfer.
*/
function transferOwnership(address newOwner) public virtual override onlyOwner {
_pendingOwner = newOwner;
emit OwnershipTransferStarted(owner(), newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual override {
delete _pendingOwner;
super._transferOwnership(newOwner);
}
/**
* @dev The new owner accepts the ownership transfer.
*/
function acceptOwnership() public virtual {
address sender = _msgSender();
if (pendingOwner() != sender) {
revert OwnableUnauthorizedAccount(sender);
}
_transferOwnership(sender);
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Simple single owner authorization mixin.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Owned.sol)
abstract contract Owned {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event OwnershipTransferred(address indexed user, address indexed newOwner);
/*//////////////////////////////////////////////////////////////
OWNERSHIP STORAGE
//////////////////////////////////////////////////////////////*/
address public owner;
modifier onlyOwner() virtual {
require(msg.sender == owner, "UNAUTHORIZED");
_;
}
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(address _owner) {
owner = _owner;
emit OwnershipTransferred(address(0), _owner);
}
/*//////////////////////////////////////////////////////////////
OWNERSHIP LOGIC
//////////////////////////////////////////////////////////////*/
function transferOwnership(address newOwner) public virtual onlyOwner {
owner = newOwner;
emit OwnershipTransferred(msg.sender, newOwner);
}
}
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import { PointsFactory } from "src/PointsFactory.sol";
import { Ownable2Step, Ownable } from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol";
/// @title Points
/// @author CopyPaste, Jack Corddry, Shivaansh Kapoor
/// @dev A simple contract for running Points Programs
contract Points is Ownable2Step {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/// @param _name The name of the points program
/// @param _symbol The symbol for the points program
/// @param _decimals The amount of decimals to use for accounting with points
/// @param _owner The owner of the points program
constructor(string memory _name, string memory _symbol, uint256 _decimals, address _owner) Ownable(_owner) {
name = _name;
symbol = _symbol;
decimals = _decimals;
// Enforces that the Points Program deployer is a factory
pointsFactory = PointsFactory(msg.sender);
}
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Award(address indexed to, uint256 indexed amount, address indexed awardedBy);
event AllowedVaultAdded(address indexed vault);
event AllowedIPAdded(address indexed ip);
event VaultRemoved(address indexed vault);
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
/// @dev Maps a vault to if the vault is allowed to call this contract
mapping(address => bool) public isAllowedVault;
/// @dev The PointsFactory used to create this program
PointsFactory public immutable pointsFactory;
/// @dev The name of the points program
string public name;
/// @dev The symbol for the points program
string public symbol;
/// @dev We track all points logic using base 1
uint256 public decimals;
/// @dev Track which RecipeMarketHub IPs are allowed to mint
mapping(address => bool) public allowedIPs;
/*//////////////////////////////////////////////////////////////
POINTS AUTH
//////////////////////////////////////////////////////////////*/
error VaultIsDuplicate();
/// @param vault The address to add to the allowed vaults for the points program
function addAllowedVault(address vault) external onlyOwner {
if (isAllowedVault[vault]) {
revert VaultIsDuplicate();
}
isAllowedVault[vault] = true;
emit AllowedVaultAdded(vault);
}
/// @param ip The incentive provider address to allow to mint points on RecipeMarketHub
function addAllowedIP(address ip) external onlyOwner {
allowedIPs[ip] = true;
emit AllowedIPAdded(ip);
}
error OnlyAllowedVaults();
error OnlyRecipeMarketHub();
error NotAllowedIP();
modifier onlyAllowedVaults() {
if (!isAllowedVault[msg.sender]) {
revert OnlyAllowedVaults();
}
_;
}
/// @dev only the RecipeMarketHub can call this function
/// @param ip The address to check if allowed
modifier onlyRecipeMarketHubAllowedIP(address ip) {
if (!pointsFactory.isRecipeMarketHub(msg.sender)) {
revert OnlyRecipeMarketHub();
}
if (!allowedIPs[ip]) {
revert NotAllowedIP();
}
_;
}
/*//////////////////////////////////////////////////////////////
POINTS
//////////////////////////////////////////////////////////////*/
/// @param to The address to mint points to
/// @param amount The amount of points to award to the `to` address
function award(address to, uint256 amount) external onlyAllowedVaults {
emit Award(to, amount, msg.sender);
}
/// @param to The address to mint points to
/// @param amount The amount of points to award to the `to` address
/// @param ip The incentive provider attempting to mint the points
function award(address to, uint256 amount, address ip) external onlyRecipeMarketHubAllowedIP(ip) {
emit Award(to, amount, ip);
}
}
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import { Points } from "src/Points.sol";
import { Ownable2Step, Ownable } from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol";
/// @title PointsFactory
/// @author CopyPaste, Jack Corddry, Shivaansh Kapoor
/// @dev A simple factory for creating Points Programs
contract PointsFactory is Ownable2Step {
/// @notice Mapping of Points Program address => bool (indicator of if Points Program was deployed using this factory)
mapping(address => bool) public isPointsProgram;
/// @notice Mapping of RecipeMarketHub address => bool (indicator of if the address is of a Royco RecipeMarketHub)
mapping(address => bool) public isRecipeMarketHub;
/// @notice Emitted when creating a points program using this factory
event NewPointsProgram(Points indexed points, string indexed name, string indexed symbol);
/// @notice Emitted when adding an RecipeMarketHub to this Points Factory
event RecipeMarketHubAdded(address indexed recipeMarketHub);
/// @param _owner The owner of the points factory - responsible for adding valid RecipeMarketHub(s) to the PointsFactory
constructor(address _owner) Ownable(_owner) { }
/// @param _recipeMarketHub The RecipeMarketHub to mark as valid in the Points Factory
function addRecipeMarketHub(address _recipeMarketHub) external onlyOwner {
isRecipeMarketHub[_recipeMarketHub] = true;
emit RecipeMarketHubAdded(_recipeMarketHub);
}
/// @param _name The name for the new points program
/// @param _symbol The symbol for the new points program
/// @param _decimals The amount of decimals per point
/// @param _owner The owner of the new points program
function createPointsProgram(string memory _name, string memory _symbol, uint256 _decimals, address _owner) external returns (Points points) {
bytes32 salt = keccak256(abi.encode(_name, _symbol, _decimals, _owner));
points = new Points{ salt: salt }(_name, _symbol, _decimals, _owner);
isPointsProgram[address(points)] = true;
emit NewPointsProgram(points, _name, _symbol);
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;
/// @title Safe casting methods
/// @notice Contains methods for safely casting between types
library SafeCast {
/// @notice Cast a uint256 to a uint128, revert on overflow
/// @param y The uint256 to be downcasted
/// @return z The downcasted integer, now type uint128
function toUint128(uint256 y) internal pure returns (uint128 z) {
require((z = uint128(y)) == y);
}
/// @notice Cast a uint256 to a uint96, revert on overflow
/// @param y The uint256 to be downcasted
/// @return z The downcasted integer, now type uint96
function toUint96(uint256 y) internal pure returns (uint96 z) {
require((z = uint96(y)) == y);
}
/// @notice Cast a uint256 to a uint64, revert on overflow
/// @param y The uint256 to be downcasted
/// @return z The downcasted integer, now type uint64
function toUint64(uint256 y) internal pure returns (uint64 z) {
require((z = uint64(y)) == y);
}
/// @notice Cast a uint256 to a uint32, revert on overflow
/// @param y The uint256 to be downcasted
/// @return z The downcasted integer, now type uint32
function toUint32(uint256 y) internal pure returns (uint32 z) {
require((z = uint32(y)) == y);
}
/// @notice Cast a uint256 to a uint160, revert on overflow
/// @param y The uint256 to be downcasted
/// @return z The downcasted integer, now type uint160
function toUint160(uint256 y) internal pure returns (uint160 z) {
require((z = uint160(y)) == y);
}
/// @notice Cast a int256 to a int128, revert on overflow or underflow
/// @param y The int256 to be downcasted
/// @return z The downcasted integer, now type int128
function toInt128(int256 y) internal pure returns (int128 z) {
require((z = int128(y)) == y);
}
/// @notice Cast a uint256 to a int256, revert on overflow
/// @param y The uint256 to be casted
/// @return z The casted integer, now type int256
function toInt256(uint256 y) internal pure returns (int256 z) {
require(y < 2 ** 255);
z = int256(y);
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "../tokens/ERC20.sol";
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
library SafeTransferLib {
/*//////////////////////////////////////////////////////////////
ETH OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferETH(address to, uint256 amount) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Transfer the ETH and store if it succeeded or not.
success := call(gas(), to, amount, 0, 0, 0, 0)
}
require(success, "ETH_TRANSFER_FAILED");
}
/*//////////////////////////////////////////////////////////////
ERC20 OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferFrom(
ERC20 token,
address from,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument.
mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 100, 0, 32)
)
}
require(success, "TRANSFER_FROM_FAILED");
}
function safeTransfer(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "TRANSFER_FAILED");
}
function safeApprove(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "APPROVE_FAILED");
}
}
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import { ERC20 } from "lib/solmate/src/tokens/ERC20.sol";
import { SafeCast } from "src/libraries/SafeCast.sol";
import { SafeTransferLib } from "lib/solmate/src/utils/SafeTransferLib.sol";
import { Owned } from "lib/solmate/src/auth/Owned.sol";
import { Points } from "src/Points.sol";
import { PointsFactory } from "src/PointsFactory.sol";
import { FixedPointMathLib } from "lib/solmate/src/utils/FixedPointMathLib.sol";
import { IWrappedVault } from "src/interfaces/IWrappedVault.sol";
import { WrappedVaultFactory } from "src/WrappedVaultFactory.sol";
/// @title WrappedVault
/// @author Jack Corddry, CopyPaste, Shivaansh Kapoor
/// @dev A token inheriting from ERC20Rewards will reward token holders with a rewards token.
/// The rewarded amount will be a fixed wei per second, distributed proportionally to token holders
/// by the size of their holdings.
contract WrappedVault is Owned, ERC20, IWrappedVault {
using SafeTransferLib for ERC20;
using SafeCast for uint256;
using FixedPointMathLib for uint256;
/*//////////////////////////////////////////////////////////////
INTERFACE
//////////////////////////////////////////////////////////////*/
event RewardsSet(address reward, uint32 start, uint32 end, uint256 rate, uint256 totalRewards, uint256 protocolFee, uint256 frontendFee);
event RewardsPerTokenUpdated(address reward, uint256 accumulated);
event UserRewardsUpdated(address reward, address user, uint256 accumulated, uint256 checkpoint);
event Claimed(address reward, address user, address receiver, uint256 claimed);
event FeesClaimed(address claimant, address incentiveToken);
event RewardsTokenAdded(address reward);
event FrontendFeeUpdated(uint256 frontendFee);
error MaxRewardsReached();
error TooFewShares();
error VaultNotAuthorizedToRewardPoints();
error InvalidInterval();
error IntervalInProgress();
error IntervalScheduled();
error NoIntervalInProgress();
error RateCannotDecrease();
error DuplicateRewardToken();
error FrontendFeeBelowMinimum();
error NoZeroRateAllowed();
error InvalidReward();
error InvalidWithdrawal();
error InvalidIntervalDuration();
error NotOwnerOfVaultOrApproved();
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
/// @custom:field start The start time of the rewards schedule
/// @custom:field end The end time of the rewards schedule
/// @custom:field rate The reward rate split among all token holders a second in Wei
struct RewardsInterval {
uint32 start;
uint32 end;
uint96 rate;
}
/// @custom:field accumulated The accumulated rewards per token for the intervaled, scaled up by WAD
/// @custom:field lastUpdated THe last time rewards per token (accumulated) was updated
struct RewardsPerToken {
uint256 accumulated;
uint32 lastUpdated;
}
/// @custom:field accumulated Rewards accumulated for the user until the checkpoint
/// @custom:field checkpoint RewardsPerToken the last time the user rewards were updated
struct UserRewards {
uint256 accumulated;
uint256 checkpoint;
}
/// @dev The max amount of reward campaigns a user can be involved in
uint256 public constant MAX_REWARDS = 20;
/// @dev The minimum duration a reward campaign must last
uint256 public constant MIN_CAMPAIGN_DURATION = 1 weeks;
/// @dev The minimum lifespan of an extended campaign
uint256 public constant MIN_CAMPAIGN_EXTENSION = 1 weeks;
/// @dev The address of the underlying vault being incentivized
IWrappedVault public immutable VAULT;
/// @dev The underlying asset being deposited into the vault
ERC20 internal immutable DEPOSIT_ASSET;
/// @dev The address of the canonical points program factory
PointsFactory public immutable POINTS_FACTORY;
/// @dev The address of the canonical WrappedVault factory
WrappedVaultFactory public immutable ERC4626I_FACTORY;
/// @dev The fee taken by the referring frontend, out of WAD
uint256 public frontendFee;
/// @dev Tokens {and,or} Points campaigns used as rewards
address[] public rewards;
/// @dev Maps a reward address to whether it has been added via addRewardsToken
mapping(address => bool) public isReward;
/// @dev Maps a reward to the interval in which rewards are distributed over
mapping(address => RewardsInterval) public rewardToInterval;
/// @dev maps a reward (either token or points) to the accumulator to track reward distribution
mapping(address => RewardsPerToken) public rewardToRPT;
/// @dev Maps a reward (either token or points) to a user, and that users accumulated rewards
mapping(address => mapping(address => UserRewards)) public rewardToUserToAR;
/// @dev Maps a reward (either token or points) to a claimant, to accrued fees
mapping(address => mapping(address => uint256)) public rewardToClaimantToFees;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/// @param _owner The owner of the incentivized vault
/// @param _name The name of the incentivized vault token
/// @param _symbol The symbol to use for the incentivized vault token
/// @param vault The underlying vault being incentivized
/// @param initialFrontendFee The initial fee set for the frontend out of WAD
/// @param pointsFactory The canonical factory responsible for deploying all points programs
constructor(
address _owner,
string memory _name,
string memory _symbol,
address vault,
uint256 initialFrontendFee,
address pointsFactory
)
Owned(_owner)
ERC20(_name, _symbol, ERC20(vault).decimals())
{
ERC4626I_FACTORY = WrappedVaultFactory(msg.sender);
if (initialFrontendFee < ERC4626I_FACTORY.minimumFrontendFee()) revert FrontendFeeBelowMinimum();
frontendFee = initialFrontendFee;
VAULT = IWrappedVault(vault);
DEPOSIT_ASSET = ERC20(VAULT.asset());
POINTS_FACTORY = PointsFactory(pointsFactory);
_mint(address(0), 10_000); // Burn 10,000 wei to stop 'first share' front running attacks on depositors
DEPOSIT_ASSET.approve(vault, type(uint256).max);
}
/// @param rewardsToken The new reward token / points program to be used as incentives
function addRewardsToken(address rewardsToken) public payable onlyOwner {
// Check if max rewards offered limit has been reached
if (rewards.length == MAX_REWARDS) revert MaxRewardsReached();
if (rewardsToken == address(VAULT)) revert InvalidReward();
if (rewardsToken == address(this)) revert InvalidReward();
// Check if reward has already been added to the incentivized vault
if (isReward[rewardsToken]) revert DuplicateRewardToken();
// Check if vault is authorized to award points if reward is a points program
if (POINTS_FACTORY.isPointsProgram(rewardsToken) && !Points(rewardsToken).isAllowedVault(address(this))) {
revert VaultNotAuthorizedToRewardPoints();
}
rewards.push(rewardsToken);
isReward[rewardsToken] = true;
emit RewardsTokenAdded(rewardsToken);
}
/// @param newFrontendFee The new front-end fee out of WAD
function setFrontendFee(uint256 newFrontendFee) public payable onlyOwner {
if (newFrontendFee < ERC4626I_FACTORY.minimumFrontendFee()) revert FrontendFeeBelowMinimum();
frontendFee = newFrontendFee;
emit FrontendFeeUpdated(newFrontendFee);
}
/// @param to The address to send all fees owed to msg.sender to
function claimFees(address to) external payable {
for (uint256 i = 0; i < rewards.length; i++) {
address reward = rewards[i];
claimFees(to, reward);
}
}
/// @param to The address to send all fees owed to msg.sender to
/// @param reward The reward token / points program to claim fees from
function claimFees(address to, address reward) public payable {
if (!isReward[reward]) revert InvalidReward();
uint256 owed = rewardToClaimantToFees[reward][msg.sender];
delete rewardToClaimantToFees[reward][msg.sender];
pushReward(reward, to, owed);
emit FeesClaimed(msg.sender, reward);
}
/// @param reward The reward token / points program
/// @param from The address to pull rewards from
/// @param amount The amount of rewards to deduct from the user
function pullReward(address reward, address from, uint256 amount) internal {
if (POINTS_FACTORY.isPointsProgram(reward)) {
if (!Points(reward).isAllowedVault(address(this))) revert VaultNotAuthorizedToRewardPoints();
} else {
ERC20(reward).safeTransferFrom(from, address(this), amount);
}
}
/// @param reward The reward token / points program
/// @param to The address to send rewards to
/// @param amount The amount of rewards to deduct from the user
function pushReward(address reward, address to, uint256 amount) internal {
// If owed is 0, there is nothing to claim. Check allows any loop calling pushReward to continue without reversion.
if (amount == 0) {
return;
}
if (POINTS_FACTORY.isPointsProgram(reward)) {
Points(reward).award(to, amount);
} else {
ERC20(reward).safeTransfer(to, amount);
}
}
/// @notice Extend the rewards interval for a given rewards campaign by adding more rewards
/// @param reward The reward token / points campaign to extend rewards for
/// @param rewardsAdded The amount of rewards to add to the campaign
/// @param newEnd The end date of the rewards campaign
/// @param frontendFeeRecipient The address to reward for directing IP flow
function extendRewardsInterval(address reward, uint256 rewardsAdded, uint256 newEnd, address frontendFeeRecipient) external payable onlyOwner {
if (!isReward[reward]) revert InvalidReward();
RewardsInterval storage rewardsInterval = rewardToInterval[reward];
if (newEnd <= rewardsInterval.end) revert InvalidInterval();
if (block.timestamp >= rewardsInterval.end) revert NoIntervalInProgress();
_updateRewardsPerToken(reward);
// Calculate fees
uint256 frontendFeeTaken = rewardsAdded.mulWadDown(frontendFee);
uint256 protocolFeeTaken = rewardsAdded.mulWadDown(ERC4626I_FACTORY.protocolFee());
// Make fees available for claiming
rewardToClaimantToFees[reward][frontendFeeRecipient] += frontendFeeTaken;
rewardToClaimantToFees[reward][ERC4626I_FACTORY.protocolFeeRecipient()] += protocolFeeTaken;
// Calculate the new rate
uint256 rewardsAfterFee = rewardsAdded - frontendFeeTaken - protocolFeeTaken;
uint32 newStart = block.timestamp > uint256(rewardsInterval.start) ? block.timestamp.toUint32() : rewardsInterval.start;
if ((newEnd - newStart) < MIN_CAMPAIGN_EXTENSION) revert InvalidIntervalDuration();
uint256 remainingRewards = rewardsInterval.rate * (rewardsInterval.end - newStart);
uint256 rate = (rewardsAfterFee + remainingRewards) / (newEnd - newStart);
if (rate < rewardsInterval.rate) revert RateCannotDecrease();
rewardsInterval.start = newStart;
rewardsInterval.end = newEnd.toUint32();
rewardsInterval.rate = rate.toUint96();
emit RewardsSet(reward, newStart, newEnd.toUint32(), rate, (rewardsAfterFee + remainingRewards), protocolFeeTaken, frontendFeeTaken);
pullReward(reward, msg.sender, rewardsAdded);
}
/// @dev Set a rewards schedule
/// @param reward The reward token or points program to set the interval for
/// @param start The start timestamp of the interval
/// @param end The end timestamp of the interval
/// @param totalRewards The amount of rewards to distribute over the interval
/// @param frontendFeeRecipient The address to reward the frontendFee
function setRewardsInterval(address reward, uint256 start, uint256 end, uint256 totalRewards, address frontendFeeRecipient) external payable onlyOwner {
if (!isReward[reward]) revert InvalidReward();
if (start >= end || end <= block.timestamp) revert InvalidInterval();
if ((end - start) < MIN_CAMPAIGN_DURATION) revert InvalidIntervalDuration();
RewardsInterval storage rewardsInterval = rewardToInterval[reward];
RewardsPerToken storage rewardsPerToken = rewardToRPT[reward];
// A new rewards program cannot be set if one is running
if (block.timestamp.toUint32() >= rewardsInterval.start && block.timestamp.toUint32() <= rewardsInterval.end) revert IntervalInProgress();
// A new rewards program cannot be set if one is scheduled to run in the future
if (rewardsInterval.start > block.timestamp) revert IntervalScheduled();
// Update the rewards per token so that we don't lose any rewards
_updateRewardsPerToken(reward);
// Calculate fees
uint256 frontendFeeTaken = totalRewards.mulWadDown(frontendFee);
uint256 protocolFeeTaken = totalRewards.mulWadDown(ERC4626I_FACTORY.protocolFee());
// Make fees available for claiming
rewardToClaimantToFees[reward][frontendFeeRecipient] += frontendFeeTaken;
rewardToClaimantToFees[reward][ERC4626I_FACTORY.protocolFeeRecipient()] += protocolFeeTaken;
// Calculate the rate
uint256 rewardsAfterFee = totalRewards - frontendFeeTaken - protocolFeeTaken;
uint256 rate = rewardsAfterFee / (end - start);
if (rate == 0) revert NoZeroRateAllowed();
rewardsInterval.start = start.toUint32();
rewardsInterval.end = end.toUint32();
rewardsInterval.rate = rate.toUint96();
// If setting up a new rewards program, the rewardsPerToken.accumulated is used and built upon
// New rewards start accumulating from the new rewards program start
// Any unaccounted rewards from last program can still be added to the user rewards
// Any unclaimed rewards can still be claimed
rewardsPerToken.lastUpdated = start.toUint32();
emit RewardsSet(reward, block.timestamp.toUint32(), rewardsInterval.end, rate, rewardsAfterFee, protocolFeeTaken, frontendFeeTaken);
pullReward(reward, msg.sender, totalRewards);
}
/// @param reward The address of the reward for which campaign should be refunded
function refundRewardsInterval(address reward) external payable onlyOwner {
if (!isReward[reward]) revert InvalidReward();
RewardsInterval memory rewardsInterval = rewardToInterval[reward];
delete rewardToInterval[reward];
if (block.timestamp >= rewardsInterval.start) revert IntervalInProgress();
uint256 rewardsOwed = (rewardsInterval.rate * (rewardsInterval.end - rewardsInterval.start)) - 1; // Round down
if (!POINTS_FACTORY.isPointsProgram(reward)) {
ERC20(reward).safeTransfer(msg.sender, rewardsOwed);
}
emit RewardsSet(reward, 0, 0, 0, 0, 0, 0);
}
/// @notice Update the rewards per token accumulator according to the rate, the time elapsed since the last update, and the current total staked amount.
function _calculateRewardsPerToken(
RewardsPerToken memory rewardsPerTokenIn,
RewardsInterval memory rewardsInterval_
)
internal
view
returns (RewardsPerToken memory)
{
RewardsPerToken memory rewardsPerTokenOut = RewardsPerToken(rewardsPerTokenIn.accumulated, rewardsPerTokenIn.lastUpdated);
// No changes if the program hasn't started
if (block.timestamp < rewardsInterval_.start) return rewardsPerTokenOut;
// No changes if the start value is zero
if (rewardsInterval_.start == 0) return rewardsPerTokenOut;
// Stop accumulating at the end of the rewards interval
uint256 updateTime = block.timestamp < rewardsInterval_.end ? block.timestamp : rewardsInterval_.end;
uint256 elapsed = updateTime - rewardsPerTokenIn.lastUpdated;
// No changes if no time has passed
if (elapsed == 0) return rewardsPerTokenOut;
rewardsPerTokenOut.lastUpdated = updateTime.toUint32();
// If there are no stakers we just change the last update time, the rewards for intervals without stakers are not accumulated
uint256 elapsedWAD = elapsed * 1e18;
// Calculate and update the new value of the accumulator.
rewardsPerTokenOut.accumulated = (rewardsPerTokenIn.accumulated + (elapsedWAD.mulDivDown(rewardsInterval_.rate, totalSupply))); // The
// rewards per token are scaled up for precision
return rewardsPerTokenOut;
}
/// @notice Calculate the rewards accumulated by a stake between two checkpoints.
function _calculateUserRewards(uint256 stake_, uint256 earlierCheckpoint, uint256 latterCheckpoint) internal pure returns (uint256) {
return stake_ * (latterCheckpoint - earlierCheckpoint) / 1e18; // We must scale down the rewards by the precision factor
}
/// @notice Update and return the rewards per token accumulator according to the rate, the time elapsed since the last update, and the current total staked
/// amount.
function _updateRewardsPerToken(address reward) internal returns (RewardsPerToken memory) {
RewardsInterval storage rewardsInterval = rewardToInterval[reward];
RewardsPerToken memory rewardsPerTokenIn = rewardToRPT[reward];
RewardsPerToken memory rewardsPerTokenOut = _calculateRewardsPerToken(rewardsPerTokenIn, rewardsInterval);
// We skip the storage changes if already updated in the same block, or if the program has ended and was updated at the end
if (rewardsPerTokenIn.lastUpdated == rewardsPerTokenOut.lastUpdated) return rewardsPerTokenOut;
rewardToRPT[reward] = rewardsPerTokenOut;
emit RewardsPerTokenUpdated(reward, rewardsPerTokenOut.accumulated);
return rewardsPerTokenOut;
}
/// @param user The user to update rewards for
function _updateUserRewards(address user) internal {
for (uint256 i = 0; i < rewards.length; i++) {
address reward = rewards[i];
_updateUserRewards(reward, user);
}
}
/// @notice Calculate and store current rewards for an user. Checkpoint the rewardsPerToken value with the user.
/// @param reward The reward token / points program to update rewards for
/// @param user The user to update rewards for
function _updateUserRewards(address reward, address user) internal returns (UserRewards memory) {
RewardsPerToken memory rewardsPerToken_ = _updateRewardsPerToken(reward);
UserRewards memory userRewards_ = rewardToUserToAR[reward][user];
// We skip the storage changes if there are no changes to the rewards per token accumulator
if (userRewards_.checkpoint == rewardsPerToken_.accumulated) return userRewards_;
// Calculate and update the new value user reserves.
userRewards_.accumulated += _calculateUserRewards(balanceOf[user], userRewards_.checkpoint, rewardsPerToken_.accumulated).toUint128();
userRewards_.checkpoint = rewardsPerToken_.accumulated;
rewardToUserToAR[reward][user] = userRewards_;
emit UserRewardsUpdated(reward, user, userRewards_.accumulated, userRewards_.checkpoint);
return userRewards_;
}
/// @dev Mint tokens, after accumulating rewards for an user and update the rewards per token accumulator.
function _mint(address to, uint256 amount) internal virtual override {
_updateUserRewards(to);
super._mint(to, amount);
}
/// @dev Burn tokens, after accumulating rewards for an user and update the rewards per token accumulator.
function _burn(address from, uint256 amount) internal virtual override {
_updateUserRewards(from);
super._burn(from, amount);
}
/// @notice Claim rewards for an user
function _claim(address reward, address from, address to, uint256 amount) internal virtual {
_updateUserRewards(reward, from);
rewardToUserToAR[reward][from].accumulated -= amount.toUint128();
pushReward(reward, to, amount);
emit Claimed(reward, from, to, amount);
}
/// @dev Transfer tokens, after updating rewards for source and destination.
function transfer(address to, uint256 amount) public virtual override returns (bool) {
_updateUserRewards(msg.sender);
_updateUserRewards(to);
return super.transfer(to, amount);
}
/// @dev Transfer tokens, after updating rewards for source and destination.
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
_updateUserRewards(from);
_updateUserRewards(to);
return super.transferFrom(from, to, amount);
}
/// @notice Allows the owner to claim the rewards from the burned shares
/// @param to The address to send all rewards owed to the owner to
/// @param reward The reward token / points program to claim rewards from
function ownerClaim(address to, address reward) public payable onlyOwner {
_claim(reward, address(0), to, currentUserRewards(reward, address(0)));
}
/// @notice Claim all rewards for the caller
/// @param to The address to send the rewards to
function claim(address to) public payable {
for (uint256 i = 0; i < rewards.length; i++) {
address reward = rewards[i];
_claim(reward, msg.sender, to, currentUserRewards(reward, msg.sender));
}
}
/// @param to The address to send the rewards to
/// @param reward The reward token / points program to claim rewards from
function claim(address to, address reward) public payable {
if (!isReward[reward]) revert InvalidReward();
_claim(reward, msg.sender, to, currentUserRewards(reward, msg.sender));
}
/// @notice Calculate and return current rewards per token.
function currentRewardsPerToken(address reward) public view returns (uint256) {
return _calculateRewardsPerToken(rewardToRPT[reward], rewardToInterval[reward]).accumulated;
}
/// @notice Calculate and return current rewards for a user.
/// @dev This repeats the logic used on transactions, but doesn't update the storage.
function currentUserRewards(address reward, address user) public view returns (uint256) {
UserRewards memory accumulatedRewards_ = rewardToUserToAR[reward][user];
RewardsPerToken memory rewardsPerToken_ = _calculateRewardsPerToken(rewardToRPT[reward], rewardToInterval[reward]);
return accumulatedRewards_.accumulated + _calculateUserRewards(balanceOf[user], accumulatedRewards_.checkpoint, rewardsPerToken_.accumulated);
}
/// @notice Calculates the rate a user would receive in rewards after depositing assets
/// @return The rate of rewards, measured in wei of rewards token per wei of assets per second, scaled up by 1e18 to avoid precision loss
function previewRateAfterDeposit(address reward, uint256 assets) public view returns (uint256) {
RewardsInterval memory rewardsInterval = rewardToInterval[reward];
if (rewardsInterval.start > block.timestamp || block.timestamp >= rewardsInterval.end) return 0;
uint256 shares = VAULT.previewDeposit(assets);
return (uint256(rewardsInterval.rate) * shares / (totalSupply + shares)) * 1e18 / assets;
}
/*//////////////////////////////////////////////////////////////
ERC4626 OVERRIDE
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IWrappedVault
function asset() external view returns (address _asset) {
return address(DEPOSIT_ASSET);
}
/// @inheritdoc IWrappedVault
function totalAssets() public view returns (uint256) {
return VAULT.convertToAssets(ERC20(address(VAULT)).balanceOf(address(this)));
}
/// @notice safeDeposit allows a user to specify a minimum amount of shares out to avoid any
/// slippage in the deposit
/// @param assets The amount of assets to deposit
/// @param receiver The address to mint the shares to
/// @param minShares The minimum amount of shares to mint
function safeDeposit(uint256 assets, address receiver, uint256 minShares) public returns (uint256 shares) {
DEPOSIT_ASSET.safeTransferFrom(msg.sender, address(this), assets);
shares = VAULT.deposit(assets, address(this));
if (shares < minShares) revert TooFewShares();
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
/// @inheritdoc IWrappedVault
function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
DEPOSIT_ASSET.safeTransferFrom(msg.sender, address(this), assets);
shares = VAULT.deposit(assets, address(this));
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
/// @inheritdoc IWrappedVault
function mint(uint256 shares, address receiver) public returns (uint256 assets) {
DEPOSIT_ASSET.safeTransferFrom(msg.sender, address(this), VAULT.previewMint(shares));
assets = VAULT.mint(shares, address(this));
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
/// @inheritdoc IWrappedVault
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) {
uint256 expectedShares = VAULT.previewWithdraw(assets);
// Check the caller is the token owner or has been approved by the owner
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender];
if (expectedShares > allowed) revert NotOwnerOfVaultOrApproved();
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - expectedShares;
}
_burn(owner, expectedShares);
shares = VAULT.withdraw(assets, receiver, address(this));
if (shares != expectedShares) revert InvalidWithdrawal();
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
/// @inheritdoc IWrappedVault
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets) {
// Check the caller is the token owner or has been approved by the owner
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender];
if (shares > allowed) revert NotOwnerOfVaultOrApproved();
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
_burn(owner, shares);
assets = VAULT.redeem(shares, receiver, address(this));
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
/// @inheritdoc IWrappedVault
function convertToShares(uint256 assets) external view returns (uint256 shares) {
shares = VAULT.convertToShares(assets);
}
/// @inheritdoc IWrappedVault
function convertToAssets(uint256 shares) external view returns (uint256 assets) {
assets = VAULT.convertToAssets(shares);
}
/// @inheritdoc IWrappedVault
function maxDeposit(address) external view returns (uint256 maxAssets) {
maxAssets = VAULT.maxDeposit(address(this));
}
/// @inheritdoc IWrappedVault
function previewDeposit(uint256 assets) external view returns (uint256 shares) {
shares = VAULT.previewDeposit(assets);
}
/// @inheritdoc IWrappedVault
function maxMint(address) external view returns (uint256 maxShares) {
maxShares = VAULT.maxMint(address(this));
}
/// @inheritdoc IWrappedVault
function previewMint(uint256 shares) external view returns (uint256 assets) {
assets = VAULT.previewMint(shares);
}
/// @inheritdoc IWrappedVault
function maxWithdraw(address) external view returns (uint256 maxAssets) {
maxAssets = VAULT.maxWithdraw(address(this));
}
/// @inheritdoc IWrappedVault
function previewWithdraw(uint256 assets) external view virtual returns (uint256 shares) {
shares = VAULT.previewWithdraw(assets);
}
/// @inheritdoc IWrappedVault
function maxRedeem(address) external view returns (uint256 maxShares) {
maxShares = VAULT.maxRedeem(address(this));
}
/// @inheritdoc IWrappedVault
function previewRedeem(uint256 shares) external view returns (uint256 assets) {
assets = VAULT.previewRedeem(shares);
}
}
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
import { Owned } from "lib/solmate/src/auth/Owned.sol";
import { ERC4626 } from "lib/solmate/src/tokens/ERC4626.sol";
import { LibString } from "lib/solmate/src/utils/LibString.sol";
import { WrappedVault } from "src/WrappedVault.sol";
/// @title WrappedVaultFactory
/// @author CopyPaste, Jack Corddry, Shivaansh Kapoor
/// @dev A factory for deploying wrapped vaults, and managing protocol or other fees
contract WrappedVaultFactory is Owned {
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
address _protocolFeeRecipient,
uint256 _protocolFee,
uint256 _minimumFrontendFee,
address _owner,
address _pointsFactory
)
payable
Owned(_owner)
{
if (_protocolFee > MAX_PROTOCOL_FEE) revert ProtocolFeeTooHigh();
if (_minimumFrontendFee > MAX_MIN_REFERRAL_FEE) revert ReferralFeeTooHigh();
protocolFeeRecipient = _protocolFeeRecipient;
protocolFee = _protocolFee;
minimumFrontendFee = _minimumFrontendFee;
pointsFactory = _pointsFactory;
}
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public constant MAX_PROTOCOL_FEE = 0.3e18;
uint256 public constant MAX_MIN_REFERRAL_FEE = 0.3e18;
address public immutable pointsFactory;
address public protocolFeeRecipient;
/// @dev The protocolFee for all incentivized vaults
uint256 public protocolFee;
/// @dev The default minimumFrontendFee to initialize incentivized vaults with
uint256 public minimumFrontendFee;
/// @dev All incentivized vaults deployed by this factory
address[] public incentivizedVaults;
mapping(address => bool) public isVault;
/*//////////////////////////////////////////////////////////////
INTERFACE
//////////////////////////////////////////////////////////////*/
error ProtocolFeeTooHigh();
error ReferralFeeTooHigh();
event ProtocolFeeUpdated(uint256 newProtocolFee);
event ReferralFeeUpdated(uint256 newReferralFee);
event ProtocolFeeRecipientUpdated(address newRecipient);
event WrappedVaultCreated(
ERC4626 indexed underlyingVaultAddress,
WrappedVault indexed incentivizedVaultAddress,
address owner,
address inputToken,
uint256 frontendFee,
string name,
string vaultSymbol
);
/*//////////////////////////////////////////////////////////////
OWNER CONTROLS
//////////////////////////////////////////////////////////////*/
/// @param newProtocolFee The new protocol fee to set for a given vault
function updateProtocolFee(uint256 newProtocolFee) external payable onlyOwner {
if (newProtocolFee > MAX_PROTOCOL_FEE) revert ProtocolFeeTooHigh();
protocolFee = newProtocolFee;
emit ProtocolFeeUpdated(newProtocolFee);
}
/// @param newMinimumReferralFee The new minimum referral fee to set for all incentivized vaults
function updateMinimumReferralFee(uint256 newMinimumReferralFee) external payable onlyOwner {
if (newMinimumReferralFee > MAX_MIN_REFERRAL_FEE) revert ReferralFeeTooHigh();
minimumFrontendFee = newMinimumReferralFee;
emit ReferralFeeUpdated(newMinimumReferralFee);
}
/// @param newRecipient The new protocol fee recipient to set for all incentivized vaults
function updateProtocolFeeRecipient(address newRecipient) external payable onlyOwner {
protocolFeeRecipient = newRecipient;
emit ProtocolFeeRecipientUpdated(newRecipient);
}
/*//////////////////////////////////////////////////////////////
VAULT CREATION
//////////////////////////////////////////////////////////////*/
/// @param vault The ERC4626 Vault to wrap
/// @param owner The address of the wrapped vault owner
/// @param name The name of the wrapped vault
/// @param initialFrontendFee The initial frontend fee for the wrapped vault ()
function wrapVault(ERC4626 vault, address owner, string calldata name, uint256 initialFrontendFee) external payable returns (WrappedVault wrappedVault) {
string memory newSymbol = getNextSymbol();
bytes32 salt = keccak256(abi.encodePacked(address(vault), owner, name, initialFrontendFee));
wrappedVault = new WrappedVault{ salt: salt }(owner, name, newSymbol, address(vault), initialFrontendFee, pointsFactory);
incentivizedVaults.push(address(wrappedVault));
isVault[address(wrappedVault)] = true;
emit WrappedVaultCreated(vault, wrappedVault, owner, address(wrappedVault.asset()), initialFrontendFee, name, newSymbol);
}
/// @dev Helper function to get the symbol for a new incentivized vault, ROY-0, ROY-1, etc.
function getNextSymbol() internal view returns (string memory) {
return string.concat("ROY-", LibString.toString(incentivizedVaults.length));
}
}
{
"compilationTarget": {
"src/WrappedVaultFactory.sol": "WrappedVaultFactory"
},
"evmVersion": "cancun",
"libraries": {},
"metadata": {
"appendCBOR": false,
"bytecodeHash": "none"
},
"optimizer": {
"details": {
"constantOptimizer": true,
"cse": false,
"deduplicate": false,
"inliner": false,
"jumpdestRemover": true,
"orderLiterals": false,
"peephole": true,
"simpleCounterForLoopUncheckedIncrement": true,
"yul": true,
"yulDetails": {
"optimizerSteps": "dhfoDgvulfnTUtnIfxa[r]EscLMVcul [j]Trpeulxa[r]cLgvifMCTUca[r]LSsTFOtfDnca[r]IulcscCTUtgvifMx[scCTUt] TOntnfDIulgvifMjmul[jul] VcTOcul jmul:fDnTOcmuO",
"stackAllocation": true
}
},
"runs": 5000
},
"remappings": [
":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
":clones-with-immutable-args/=lib/clones-with-immutable-args/src/",
":ds-test/=lib/solmate/lib/ds-test/src/",
":enso-weiroll/=lib/enso-weiroll/contracts/",
":erc4626-tests/=lib/erc4626-tests/",
":forge-std/=lib/forge-std/src/",
":halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/",
":openzeppelin-contracts/=lib/openzeppelin-contracts/",
":solady/=lib/solady/src/",
":solmate/=lib/solmate/src/"
],
"viaIR": true
}
[{"inputs":[{"internalType":"address","name":"_protocolFeeRecipient","type":"address"},{"internalType":"uint256","name":"_protocolFee","type":"uint256"},{"internalType":"uint256","name":"_minimumFrontendFee","type":"uint256"},{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_pointsFactory","type":"address"}],"stateMutability":"payable","type":"constructor"},{"inputs":[],"name":"ProtocolFeeTooHigh","type":"error"},{"inputs":[],"name":"ReferralFeeTooHigh","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"newRecipient","type":"address"}],"name":"ProtocolFeeRecipientUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newProtocolFee","type":"uint256"}],"name":"ProtocolFeeUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newReferralFee","type":"uint256"}],"name":"ReferralFeeUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract ERC4626","name":"underlyingVaultAddress","type":"address"},{"indexed":true,"internalType":"contract WrappedVault","name":"incentivizedVaultAddress","type":"address"},{"indexed":false,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"address","name":"inputToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"frontendFee","type":"uint256"},{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":false,"internalType":"string","name":"vaultSymbol","type":"string"}],"name":"WrappedVaultCreated","type":"event"},{"inputs":[],"name":"MAX_MIN_REFERRAL_FEE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_PROTOCOL_FEE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"incentivizedVaults","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isVault","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimumFrontendFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pointsFactory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"protocolFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"protocolFeeRecipient","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newMinimumReferralFee","type":"uint256"}],"name":"updateMinimumReferralFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newProtocolFee","type":"uint256"}],"name":"updateProtocolFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"newRecipient","type":"address"}],"name":"updateProtocolFeeRecipient","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"contract ERC4626","name":"vault","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"initialFrontendFee","type":"uint256"}],"name":"wrapVault","outputs":[{"internalType":"contract WrappedVault","name":"wrappedVault","type":"address"}],"stateMutability":"payable","type":"function"}]