Skip to content

Instantly share code, notes, and snippets.

@sandybradley
Created August 27, 2025 15:55
Show Gist options
  • Select an option

  • Save sandybradley/5b8adf986a0896bc68f90c5d76500b99 to your computer and use it in GitHub Desktop.

Select an option

Save sandybradley/5b8adf986a0896bc68f90c5d76500b99 to your computer and use it in GitHub Desktop.
// 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