Created
August 27, 2025 15:55
-
-
Save sandybradley/5b8adf986a0896bc68f90c5d76500b99 to your computer and use it in GitHub Desktop.
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: BUSL-1.1 | |
| pragma solidity 0.8.26; | |
| import {ERC20} from "@solmate/tokens/ERC20.sol"; | |
| import {ERC4626} from "@solmate/tokens/ERC4626.sol"; | |
| import {SafeTransferLib} from "@solmate/utils/SafeTransferLib.sol"; | |
| import {ReentrancyGuard} from "@solmate/utils/ReentrancyGuard.sol"; | |
| import {Errors} from "src/utils/Errors.sol"; | |
| import {Infrared} from "src/core/Infrared.sol"; | |
| import {InfraredVault} from "src/core/InfraredVault.sol"; | |
| import {WrappedVaultOracleAuction} from | |
| "src/periphery/WrappedVaultOracleAuction.sol"; | |
| /** | |
| * @title Infrared WrappedVault | |
| * @notice A wrapper vault built on ERC4626 to facilitate staking operations and reward compounding | |
| * through the Infrared protocol. Each staking token has a corresponding wrapped vault. | |
| * @dev deploy 1 wrapped vault per staking token | |
| */ | |
| contract WrappedVault is ERC4626, ReentrancyGuard { | |
| using SafeTransferLib for ERC20; | |
| /// @notice Address of the reward auction | |
| WrappedVaultOracleAuction public immutable rewardAuction; | |
| /// @notice Instance of the associated InfraredVault for staking. | |
| InfraredVault public immutable iVault; | |
| /// @dev Inflation attack prevention | |
| uint256 internal constant deadShares = 1e3; | |
| /// @notice Event emitted when reward tokens claimed | |
| event RewardClaimed(address indexed token, uint256 amount); | |
| /// @notice Event emitted when reward tokens compounded | |
| event RewardCompounded(uint256 amount); | |
| /** | |
| * @notice Initializes a new WrappedVault contract for a specific staking token. | |
| * @param _gov Address multisig governance. | |
| * @param _infrared Address of the Infrared protocol. | |
| * @param _stakingToken Address of the ERC20 staking token. | |
| * @param _name Name of the wrapped vault token (ERC4626). | |
| * @param _symbol Symbol of the wrapped vault token (ERC4626). | |
| * @param _pythOracleAddress Address of Pyth oracle, ref: https://docs.pyth.network/price-feeds/contract-addresses/evm | |
| * @param _stakingTokenUsdFeedId Pyth price feed Id for staking token, ref: https://docs.pyth.network/price-feeds/price-feeds | |
| * @param rewardTokens Addresses of reward tokens to setup for auction | |
| * @param rewardTokenUsdFeedIds Pyth price feed ids for above reward tokens | |
| */ | |
| constructor( | |
| address _gov, | |
| address _infrared, | |
| address _stakingToken, | |
| string memory _name, | |
| string memory _symbol, | |
| address _pythOracleAddress, | |
| bytes32 _stakingTokenUsdFeedId, | |
| address[] memory rewardTokens, | |
| bytes32[] memory rewardTokenUsdFeedIds | |
| ) ERC4626(ERC20(_stakingToken), _name, _symbol) { | |
| if ( | |
| _gov == address(0) || _infrared == address(0) | |
| || _stakingToken == address(0) | |
| ) revert Errors.ZeroAddress(); | |
| Infrared infrared = Infrared(payable(_infrared)); | |
| // register vault if necessary | |
| address _vaultAddress = address(infrared.vaultRegistry(_stakingToken)); | |
| if (_vaultAddress == address(0)) { | |
| iVault = | |
| InfraredVault(address(infrared.registerVault(_stakingToken))); | |
| } else { | |
| iVault = InfraredVault(_vaultAddress); | |
| } | |
| // Deploy reward auction | |
| rewardAuction = | |
| new WrappedVaultOracleAuction(address(this), _stakingToken, _gov); | |
| // Configure oracles | |
| rewardAuction.setOracle(_pythOracleAddress); | |
| rewardAuction.setStakingTokenPythId(_stakingTokenUsdFeedId); | |
| // Configure default rewards | |
| for (uint256 i; i < rewardTokens.length; i++) { | |
| rewardAuction.configureToken( | |
| rewardTokens[i], | |
| WrappedVaultOracleAuction.PriceSource.PYTH, | |
| rewardTokenUsdFeedIds[i], | |
| 100, // default 1% discount | |
| 600, // default 10 minutes max staleness | |
| 100, // default 1% max confidence interval | |
| false | |
| ); | |
| } | |
| // Mint dead shares to prevent inflation attacks | |
| _mint(address(0), deadShares); | |
| asset.safeApprove(address(iVault), type(uint256).max); | |
| } | |
| // ERC4626 overrides | |
| /** | |
| * @notice Returns the total assets managed by the wrapped vault. | |
| * @dev Overrides the ERC4626 `totalAssets` function to integrate with the InfraredVault balance. | |
| * @return The total amount of staking tokens held by the InfraredVault. | |
| */ | |
| function totalAssets() public view virtual override returns (uint256) { | |
| // balance of infrared vault = balance of underlying staked | |
| // pending compound rewards in assets on infarred vault added | |
| // pending compound assets on this contract added | |
| // deadShares added to keep initial 1:1 ratio and not fail convertToShares on initial deposit | |
| return iVault.balanceOf(address(this)) | |
| + iVault.rewards(address(this), address(asset)) | |
| + asset.balanceOf(address(this)) + deadShares; | |
| } | |
| /** | |
| * @notice Hook called before withdrawal operations. | |
| * @dev This function ensures that the requested amount of staking tokens is withdrawn | |
| * from the InfraredVault before being transferred to the user. | |
| * @param assets The amount of assets to withdraw. | |
| */ | |
| function beforeWithdraw(uint256 assets, uint256) | |
| internal | |
| virtual | |
| override | |
| { | |
| iVault.withdraw(assets); | |
| } | |
| /** | |
| * @notice Hook called after deposit operations. | |
| * @dev This function stakes the deposited tokens into the InfraredVault. | |
| * @param assets The amount of assets being deposited. | |
| */ | |
| function afterDeposit(uint256 assets, uint256) internal virtual override { | |
| iVault.stake(assets); | |
| } | |
| /** | |
| * @notice Claims rewards from the InfraredVault and transfers them to the reward distributor. | |
| * @dev Only rewards other than the staking token itself are transferred. | |
| */ | |
| function claimRewards() external { | |
| _compound(); | |
| } | |
| function _compound() internal nonReentrant { | |
| // Claim rewards from the InfraredVault | |
| iVault.getReward(); | |
| // Retrieve all reward tokens | |
| address[] memory _tokens = iVault.getAllRewardTokens(); | |
| uint256 len = _tokens.length; | |
| // Loop through reward tokens and transfer them to the reward distributor | |
| for (uint256 i; i < len; ++i) { | |
| ERC20 _token = ERC20(_tokens[i]); | |
| uint256 bal = _token.balanceOf(address(this)); | |
| if (bal == 0) continue; | |
| // compound when reward token is staking token | |
| if (_token == asset) { | |
| iVault.stake(bal); | |
| emit RewardCompounded(bal); | |
| continue; | |
| } | |
| (bool success, bytes memory data) = address(_token).call( | |
| abi.encodeWithSelector( | |
| ERC20.transfer.selector, address(rewardAuction), bal | |
| ) | |
| ); | |
| if (success && (data.length == 0 || abi.decode(data, (bool)))) { | |
| emit RewardClaimed(address(_token), bal); | |
| } else { | |
| continue; | |
| } | |
| } | |
| } | |
| function compoundProceeds(uint256 amount) external { | |
| if (msg.sender != address(rewardAuction)) { | |
| revert Errors.Unauthorized(msg.sender); | |
| } | |
| asset.safeTransferFrom(address(rewardAuction), address(this), amount); | |
| iVault.stake(amount); | |
| emit RewardCompounded(amount); | |
| } | |
| // Override core methods to compound before deposits / withdrawals | |
| function deposit(uint256 assets, address receiver) | |
| public | |
| virtual | |
| override | |
| returns (uint256 shares) | |
| { | |
| _compound(); | |
| return super.deposit(assets, receiver); | |
| } | |
| function mint(uint256 shares, address receiver) | |
| public | |
| virtual | |
| override | |
| returns (uint256 assets) | |
| { | |
| _compound(); | |
| return super.mint(shares, receiver); | |
| } | |
| function withdraw(uint256 assets, address receiver, address owner) | |
| public | |
| virtual | |
| override | |
| returns (uint256 shares) | |
| { | |
| _compound(); | |
| return super.withdraw(assets, receiver, owner); | |
| } | |
| function redeem(uint256 shares, address receiver, address owner) | |
| public | |
| virtual | |
| override | |
| returns (uint256 assets) | |
| { | |
| _compound(); | |
| return super.redeem(shares, receiver, owner); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment