diff --git a/.github/actions/foundry-setup/action.yml b/.github/actions/foundry-setup/action.yml index 143f579c82..02f79ef0e4 100644 --- a/.github/actions/foundry-setup/action.yml +++ b/.github/actions/foundry-setup/action.yml @@ -6,6 +6,11 @@ runs: steps: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + with: + # Pinned: forge 1.8.0 (released 2026-08-27) fails every vm.createFork + # against Alchemy endpoints (anvil_nodeInfo probe returns HTTP 400) and + # its forge fmt disagrees with the repo's committed formatting. + version: v1.7.1 - name: Cache dependencies uses: actions/cache@v4 diff --git a/contracts/contracts/interfaces/IOETHVaultLens.sol b/contracts/contracts/interfaces/IOETHVaultLens.sol new file mode 100644 index 0000000000..75979008ce --- /dev/null +++ b/contracts/contracts/interfaces/IOETHVaultLens.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IOToken } from "./IOToken.sol"; +import { IVault } from "./IVault.sol"; + +/** + * @title OETH Vault Lens Interface + * @author Origin Protocol Inc + */ +interface IOETHVaultLens { + /** + * @notice Returns the Vault used to calculate the OToken rate. + * @return The Vault contract. + */ + function vault() external view returns (IVault); + + /** + * @notice Returns the OToken whose rate is reported. + * @return The OToken contract. + */ + function oToken() external view returns (IOToken); + + /** + * @notice Returns the staking strategy whose verified balance freshness gates getRate. + * @return The staking strategy address. + */ + function stakingStrategy() external view returns (address); + + /** + * @notice Returns the maximum age of the staking strategy's last verified balance + * before getRate reverts. + * @return The maximum age in seconds. + */ + function MAX_VERIFIED_BALANCE_AGE() external view returns (uint256); + + /** + * @notice Returns the value of one OToken in the Vault's underlying asset. + * @return The rate with 18 decimals. + */ + function getRate() external view returns (uint256); +} diff --git a/contracts/contracts/lens/OETHVaultLens.sol b/contracts/contracts/lens/OETHVaultLens.sol new file mode 100644 index 0000000000..5878e75e1e --- /dev/null +++ b/contracts/contracts/lens/OETHVaultLens.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { ICompoundingStakingStrategy } from "../interfaces/strategies/ICompoundingStakingStrategy.sol"; +import { IOETHVaultLens } from "../interfaces/IOETHVaultLens.sol"; +import { IOToken } from "../interfaces/IOToken.sol"; +import { IVault } from "../interfaces/IVault.sol"; + +/** + * @title OETH Vault Lens + * @notice Reports the value of one OToken in the Vault's underlying asset (the NAV rate). + * @dev The rate has 18 decimals and is calculated from the Vault's total value + * divided by the OToken's total supply. + * getRate reverts if the staking strategy's balances have not been verified + * against the beacon chain within the last MAX_VERIFIED_BALANCE_AGE seconds, + * so a stale beacon chain state can not be reported as a current rate. + * @author Origin Protocol Inc + */ +contract OETHVaultLens is IOETHVaultLens { + /// @notice The maximum age of the staking strategy's last verified balance + /// before getRate reverts. + uint256 public constant override MAX_VERIFIED_BALANCE_AGE = 24 hours; + + /// @notice The Vault used to calculate the OToken rate. + IVault public immutable vault; + + /// @notice The OToken whose rate is reported. + IOToken public immutable oToken; + + /// @notice The staking strategy whose verified balance freshness gates getRate. + address public immutable stakingStrategy; + + /** + * @notice Constructs an OETH Vault Lens. + * @param _vault The Vault used to calculate the rate and resolve the OToken. + * @param _stakingStrategy The staking strategy whose verified balance freshness + * gates getRate. + */ + constructor(address _vault, address _stakingStrategy) { + require(_vault != address(0), "Vault is zero address"); + require(_stakingStrategy != address(0), "Strategy is zero address"); + + vault = IVault(_vault); + address _oToken = vault.oToken(); + require(_oToken != address(0), "OToken is zero address"); + oToken = IOToken(_oToken); + stakingStrategy = _stakingStrategy; + } + + /** + * @notice Returns the value of one OToken in the Vault's underlying asset. + * @dev This is the NAV rate, not the redeemable rate. The NAV rate can be + * above 1e18 when the Vault holds yield that has not been realized + * through a rebase yet, while the redeemable rate is capped at 1e18. + * Reverts if the staking strategy's last verified balance is older than + * MAX_VERIFIED_BALANCE_AGE, if the OToken supply is zero, or if the + * calculated rate is zero. + * @return rate The rate with 18 decimals. + */ + function getRate() external view override returns (uint256 rate) { + // The uint64 timestamp is promoted to uint256 by the constant, + // so the comparison can neither overflow nor underflow. + require( + ICompoundingStakingStrategy(stakingStrategy) + .lastVerifiedBalanceTimestamp() + + MAX_VERIFIED_BALANCE_AGE >= + block.timestamp, + "Stale verified balance" + ); + + uint256 supply = oToken.totalSupply(); + require(supply > 0, "No oToken supply"); + + rate = (vault.totalValue() * 1e18) / supply; + require(rate > 0, "Invalid rate"); + } +} diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 77dd2bdb6b..e7948855ea 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -167,3 +167,10 @@ contract OUSDMorphoV2StrategyProxy is InitializeGovernedUpgradeabilityProxy { contract OETHSupernovaAMOProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice OETHVaultLensProxy delegates calls to an OETHVaultLens implementation + */ +contract OETHVaultLensProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/scripts/deploy/mainnet/005_UpgradeCompoundingStakingStrategy.s.sol b/contracts/scripts/deploy/mainnet/005_DeployOETHVaultLens.s.sol similarity index 57% rename from contracts/scripts/deploy/mainnet/005_UpgradeCompoundingStakingStrategy.s.sol rename to contracts/scripts/deploy/mainnet/005_DeployOETHVaultLens.s.sol index 59c8ff6e57..eafdeab765 100644 --- a/contracts/scripts/deploy/mainnet/005_UpgradeCompoundingStakingStrategy.s.sol +++ b/contracts/scripts/deploy/mainnet/005_DeployOETHVaultLens.s.sol @@ -8,24 +8,31 @@ import {GovProposal} from "scripts/deploy/helpers/DeploymentTypes.sol"; // Contracts import {InitializeGovernedUpgradeabilityProxy} from "contracts/proxies/InitializeGovernedUpgradeabilityProxy.sol"; +import {OETHVaultLensProxy} from "contracts/proxies/Proxies.sol"; +import {OETHVaultLens} from "contracts/lens/OETHVaultLens.sol"; import {CompoundingStakingStrategy} from "contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol"; import {InitializableAbstractStrategy} from "contracts/utils/InitializableAbstractStrategy.sol"; +import {ICompoundingStakingStrategy} from "contracts/interfaces/strategies/ICompoundingStakingStrategy.sol"; // Mainnet addresses import {Mainnet} from "tests/utils/Addresses.sol"; -/// @title 005_UpgradeCompoundingStakingStrategy -/// @notice Makes snapBalances() and verifyBalances() permissionless now that validator consolidation is complete. -contract $005_UpgradeCompoundingStakingStrategy is AbstractDeployScript("005_UpgradeCompoundingStakingStrategy") { +/// @title 005_DeployOETHVaultLens +/// @notice Upgrades the Compounding Staking Strategy (permissionless balance proofs, +/// lastVerifiedBalanceTimestamp, 1 ETH initial deposit) and deploys the +/// OETH Vault Lens that reports the OETH/WETH NAV rate gated on that timestamp. +contract $005_DeployOETHVaultLens is AbstractDeployScript("005_DeployOETHVaultLens") { using GovHelper for GovProposal; uint64 internal constant BEACON_GENESIS_TIMESTAMP = 1_606_824_023; // Limit exposure while a new validator's withdrawal credentials are still unverified. uint256 internal constant INITIAL_DEPOSIT_AMOUNT = 1 ether; + address internal constant GOVERNOR = Mainnet.Timelock; // ==================== Deployment Logic ==================== // function _execute() internal override { + // 1. New CompoundingStakingStrategy implementation. CompoundingStakingStrategy newImpl = new CompoundingStakingStrategy( InitializableAbstractStrategy.BaseStrategyConfig({ platformAddress: address(0), vaultAddress: resolver.resolve("OETH_VAULT_PROXY") @@ -37,18 +44,31 @@ contract $005_UpgradeCompoundingStakingStrategy is AbstractDeployScript("005_Upg ); _recordDeployment("COMPOUNDING_STAKING_STRATEGY_IMPL", address(newImpl), type(CompoundingStakingStrategy).name); + + // 2. OETH Vault Lens implementation and proxy, governed by the Timelock. + OETHVaultLensProxy lensProxy = new OETHVaultLensProxy(); + OETHVaultLens lensImpl = new OETHVaultLens( + resolver.resolve("OETH_VAULT_PROXY"), resolver.resolve("COMPOUNDING_STAKING_STRATEGY_PROXY") + ); + lensProxy.initialize(address(lensImpl), GOVERNOR, ""); + + _recordDeployment("OETH_VAULT_LENS_IMPL", address(lensImpl), type(OETHVaultLens).name); + _recordDeployment("OETH_VAULT_LENS_PROXY", address(lensProxy), type(OETHVaultLensProxy).name); } // ==================== Governance Proposal ==================== // function _buildGovernanceProposal() internal override { govProposal.setDescription( - "Make Compounding Staking Strategy balance proofs permissionless\n\n" + "Upgrade the Compounding Staking Strategy and enable the OETH Vault Lens\n\n" "Validator consolidation is complete, so the ConsolidationController is no longer the " "strategy registrator. This proposal upgrades the CompoundingStakingStrategy to allow " "anyone to call snapBalances() and verifyBalances(). The existing snapshot delay and " "beacon proof verification continue to protect the accounting inputs. It also lowers " - "the maximum first validator deposit to 1 ETH." + "the maximum first validator deposit to 1 ETH. The new implementation additionally " + "exposes lastVerifiedBalanceTimestamp, the timestamp of the last balance snapshot " + "verified against beacon chain data, which the newly deployed OETH Vault Lens reads " + "to refuse reporting an OETH/WETH rate when balance verification is more than 24 " "hours old." ); address proxy = resolver.resolve("COMPOUNDING_STAKING_STRATEGY_PROXY"); govProposal.action( @@ -75,6 +95,43 @@ contract $005_UpgradeCompoundingStakingStrategy is AbstractDeployScript("005_Upg require(strategy.validatorRegistrator() != address(0), "Registrator cleared"); _verifyPermissionlessBalanceCalls(proxy); + _verifyLens(proxy); + } + + function _verifyLens(address strategyProxy) internal { + address lensProxyAddr = resolver.resolve("OETH_VAULT_LENS_PROXY"); + InitializeGovernedUpgradeabilityProxy lensProxy = InitializeGovernedUpgradeabilityProxy(payable(lensProxyAddr)); + + require(lensProxy.implementation() == resolver.resolve("OETH_VAULT_LENS_IMPL"), "Unexpected lens impl"); + require(lensProxy.governor() == GOVERNOR, "Unexpected lens governor"); + + OETHVaultLens lens = OETHVaultLens(lensProxyAddr); + require(address(lens.vault()) == resolver.resolve("OETH_VAULT_PROXY"), "Unexpected lens vault"); + require(address(lens.oToken()) == resolver.resolve("OETH_PROXY"), "Unexpected lens oToken"); + require(lens.stakingStrategy() == strategyProxy, "Unexpected lens strategy"); + + // Right after the upgrade no verifyBalances() has run against the new implementation, + // so lastVerifiedBalanceTimestamp is 0 and the lens must refuse to report a rate. + // Guarded on actual staleness so the check stays valid once real balance + // verifications happen on-chain, as _fork() re-runs on every fork and smoke run. + uint256 lastVerified = ICompoundingStakingStrategy(strategyProxy).lastVerifiedBalanceTimestamp(); + if (lastVerified + lens.MAX_VERIFIED_BALANCE_AGE() < block.timestamp) { + (bool success,) = lensProxyAddr.staticcall(abi.encodeCall(OETHVaultLens.getRate, ())); + require(!success, "getRate should revert while stale"); + } + + // With a fresh verified balance the lens reports the Vault's value per OToken. + vm.mockCall( + strategyProxy, + ICompoundingStakingStrategy.lastVerifiedBalanceTimestamp.selector, + abi.encode(uint64(block.timestamp)) + ); + uint256 rate = lens.getRate(); + require(rate > 0, "Invalid rate"); + require(rate == (lens.vault().totalValue() * 1e18) / lens.oToken().totalSupply(), "Unexpected rate"); + // Clears all mocked calls so the mock can not leak into smoke tests, + // which run this script as part of their setUp. + vm.clearMockedCalls(); } function _verifyPermissionlessBalanceCalls(address proxy) internal { diff --git a/contracts/tests/mocks/MockOETHVaultLensDependencies.sol b/contracts/tests/mocks/MockOETHVaultLensDependencies.sol new file mode 100644 index 0000000000..ad9f146ce7 --- /dev/null +++ b/contracts/tests/mocks/MockOETHVaultLensDependencies.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +contract MockOETHVaultLensVault { + uint256 public totalValue; + address public oToken; + + function setTotalValue(uint256 _totalValue) external { + totalValue = _totalValue; + } + + function setOToken(address _oToken) external { + oToken = _oToken; + } +} + +contract MockOETHVaultLensToken { + uint256 public totalSupply; + + function setTotalSupply(uint256 _totalSupply) external { + totalSupply = _totalSupply; + } +} + +contract MockOETHVaultLensStrategy { + uint64 public lastVerifiedBalanceTimestamp; + + function setLastVerifiedBalanceTimestamp(uint64 _timestamp) external { + lastVerifiedBalanceTimestamp = _timestamp; + } +} diff --git a/contracts/tests/unit/lens/OETHVaultLens/concrete/GetRate.t.sol b/contracts/tests/unit/lens/OETHVaultLens/concrete/GetRate.t.sol new file mode 100644 index 0000000000..5a77e7f7c5 --- /dev/null +++ b/contracts/tests/unit/lens/OETHVaultLens/concrete/GetRate.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Unit_OETHVaultLens_Shared_Test} from "../shared/Shared.t.sol"; + +contract Unit_Concrete_OETHVaultLens_GetRate_Test is Unit_OETHVaultLens_Shared_Test { + function test_getRate_isOneAtRebase() public view { + assertEq(lens.getRate(), 1e18); + } + + function test_getRate_increasesWithUnrebasedYield() public { + mockVault.setTotalValue(105e18); + assertEq(lens.getRate(), 1.05e18); + } + + function test_getRate_returnsFractionalRate() public { + mockVault.setTotalValue(101e18); + assertEq(lens.getRate(), 1.01e18); + } + + function test_getRate_atExactMaxVerifiedBalanceAge() public { + vm.warp(uint256(mockStrategy.lastVerifiedBalanceTimestamp()) + lens.MAX_VERIFIED_BALANCE_AGE()); + assertEq(lens.getRate(), 1e18); + } + + function test_getRate_RevertWhen_oneSecondPastMaxVerifiedBalanceAge() public { + vm.warp(uint256(mockStrategy.lastVerifiedBalanceTimestamp()) + lens.MAX_VERIFIED_BALANCE_AGE() + 1); + vm.expectRevert("Stale verified balance"); + lens.getRate(); + } + + function test_getRate_RevertWhen_balancesNeverVerified() public { + mockStrategy.setLastVerifiedBalanceTimestamp(0); + vm.expectRevert("Stale verified balance"); + lens.getRate(); + } + + function test_getRate_RevertWhen_supplyIsZero() public { + mockOToken.setTotalSupply(0); + vm.expectRevert("No oToken supply"); + lens.getRate(); + } + + function test_getRate_RevertWhen_rateIsZero() public { + mockVault.setTotalValue(0); + vm.expectRevert("Invalid rate"); + lens.getRate(); + } +} diff --git a/contracts/tests/unit/lens/OETHVaultLens/concrete/Proxy.t.sol b/contracts/tests/unit/lens/OETHVaultLens/concrete/Proxy.t.sol new file mode 100644 index 0000000000..612eccb206 --- /dev/null +++ b/contracts/tests/unit/lens/OETHVaultLens/concrete/Proxy.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Lens} from "tests/utils/artifacts/Lens.sol"; +import {Unit_OETHVaultLens_Shared_Test} from "../shared/Shared.t.sol"; + +contract Unit_Concrete_OETHVaultLens_Proxy_Test is Unit_OETHVaultLens_Shared_Test { + function test_proxy_setsGovernorAndImplementation() public view { + assertEq(lensProxy.governor(), governor); + assertEq(lensProxy.admin(), governor); + assertEq(lensProxy.implementation(), lensImpl); + } + + function test_upgradeTo_updatesImplementation() public { + address newImpl = vm.deployCode(Lens.OETH_VAULT_LENS, abi.encode(address(mockVault), address(mockStrategy))); + + vm.prank(governor); + lensProxy.upgradeTo(newImpl); + + assertEq(lensProxy.implementation(), newImpl); + assertEq(lens.getRate(), 1e18); + } + + function test_upgradeTo_RevertWhen_notGovernor() public { + address newImpl = vm.deployCode(Lens.OETH_VAULT_LENS, abi.encode(address(mockVault), address(mockStrategy))); + + vm.prank(alice); + vm.expectRevert("Caller is not the Governor"); + lensProxy.upgradeTo(newImpl); + } +} diff --git a/contracts/tests/unit/lens/OETHVaultLens/concrete/ViewFunctions.t.sol b/contracts/tests/unit/lens/OETHVaultLens/concrete/ViewFunctions.t.sol new file mode 100644 index 0000000000..5b135e9614 --- /dev/null +++ b/contracts/tests/unit/lens/OETHVaultLens/concrete/ViewFunctions.t.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Unit_OETHVaultLens_Shared_Test} from "../shared/Shared.t.sol"; + +contract Unit_Concrete_OETHVaultLens_ViewFunctions_Test is Unit_OETHVaultLens_Shared_Test { + function test_constructor_setsConfiguration() public view { + assertEq(address(lens.vault()), address(mockVault)); + assertEq(address(lens.oToken()), address(mockOToken)); + assertEq(lens.stakingStrategy(), address(mockStrategy)); + assertEq(lens.MAX_VERIFIED_BALANCE_AGE(), 24 hours); + } +} diff --git a/contracts/tests/unit/lens/OETHVaultLens/fuzz/GetRate.fuzz.t.sol b/contracts/tests/unit/lens/OETHVaultLens/fuzz/GetRate.fuzz.t.sol new file mode 100644 index 0000000000..dd88d332e7 --- /dev/null +++ b/contracts/tests/unit/lens/OETHVaultLens/fuzz/GetRate.fuzz.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Unit_OETHVaultLens_Shared_Test} from "../shared/Shared.t.sol"; + +contract Unit_Fuzz_OETHVaultLens_GetRate_Test is Unit_OETHVaultLens_Shared_Test { + /// @notice The lens always reports total value divided by total supply with 18 decimals, + /// or reverts when the computed rate would be zero. + function testFuzz_getRate_reportsAssetValuePerOToken(uint256 totalValue, uint256 totalSupply) public { + totalValue = bound(totalValue, 0, 1e40); + totalSupply = bound(totalSupply, 1, 1e40); + + mockVault.setTotalValue(totalValue); + mockOToken.setTotalSupply(totalSupply); + + uint256 expected = (totalValue * 1e18) / totalSupply; + if (expected == 0) { + vm.expectRevert("Invalid rate"); + lens.getRate(); + } else { + assertEq(lens.getRate(), expected); + } + } + + /// @notice getRate reverts if and only if the last verified balance is more than + /// MAX_VERIFIED_BALANCE_AGE seconds old, over the full uint64 timestamp range. + function testFuzz_getRate_stalenessBoundary(uint64 lastVerified, uint256 nowTimestamp) public { + nowTimestamp = bound(nowTimestamp, 7 days, uint256(type(uint64).max) + 365 days); + vm.warp(nowTimestamp); + mockStrategy.setLastVerifiedBalanceTimestamp(lastVerified); + + if (uint256(lastVerified) + lens.MAX_VERIFIED_BALANCE_AGE() < block.timestamp) { + vm.expectRevert("Stale verified balance"); + lens.getRate(); + } else { + assertEq(lens.getRate(), 1e18); + } + } +} diff --git a/contracts/tests/unit/lens/OETHVaultLens/shared/Shared.t.sol b/contracts/tests/unit/lens/OETHVaultLens/shared/Shared.t.sol new file mode 100644 index 0000000000..41a523b2eb --- /dev/null +++ b/contracts/tests/unit/lens/OETHVaultLens/shared/Shared.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +// --- Test base +import {Base} from "tests/Base.t.sol"; + +// --- Test utilities +import {Lens} from "tests/utils/artifacts/Lens.sol"; +import {Proxies} from "tests/utils/artifacts/Proxies.sol"; + +// Interfaces +import {IProxy} from "contracts/interfaces/IProxy.sol"; +import {IOETHVaultLens} from "contracts/interfaces/IOETHVaultLens.sol"; + +// Mocks +import { + MockOETHVaultLensStrategy, + MockOETHVaultLensToken, + MockOETHVaultLensVault +} from "tests/mocks/MockOETHVaultLensDependencies.sol"; + +abstract contract Unit_OETHVaultLens_Shared_Test is Base { + IOETHVaultLens internal lens; + IProxy internal lensProxy; + address internal lensImpl; + MockOETHVaultLensVault internal mockVault; + MockOETHVaultLensToken internal mockOToken; + MockOETHVaultLensStrategy internal mockStrategy; + + function setUp() public virtual override { + super.setUp(); + // block.timestamp must exceed MAX_VERIFIED_BALANCE_AGE so a zero + // lastVerifiedBalanceTimestamp counts as stale. + vm.warp(7 days); + _deployMockContracts(); + _deployContracts(); + _configureContracts(); + label(); + } + + function _deployMockContracts() internal { + mockVault = new MockOETHVaultLensVault(); + mockOToken = new MockOETHVaultLensToken(); + mockStrategy = new MockOETHVaultLensStrategy(); + mockVault.setOToken(address(mockOToken)); + } + + function _deployContracts() internal { + vm.startPrank(deployer); + lensImpl = vm.deployCode(Lens.OETH_VAULT_LENS, abi.encode(address(mockVault), address(mockStrategy))); + lensProxy = IProxy(vm.deployCode(Proxies.IG_PROXY)); + lensProxy.initialize(lensImpl, governor, ""); + vm.stopPrank(); + lens = IOETHVaultLens(address(lensProxy)); + } + + function _configureContracts() internal { + mockVault.setTotalValue(100e18); + mockOToken.setTotalSupply(100e18); + mockStrategy.setLastVerifiedBalanceTimestamp(uint64(block.timestamp)); + } + + function label() public { + vm.label(address(lens), "OETHVaultLens"); + vm.label(address(mockVault), "MockVault"); + vm.label(address(mockOToken), "MockOToken"); + vm.label(address(mockStrategy), "MockStakingStrategy"); + } +} diff --git a/contracts/tests/utils/artifacts/Lens.sol b/contracts/tests/utils/artifacts/Lens.sol new file mode 100644 index 0000000000..bcac469d7f --- /dev/null +++ b/contracts/tests/utils/artifacts/Lens.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Lens { + string internal constant OETH_VAULT_LENS = "contracts/lens/OETHVaultLens.sol:OETHVaultLens"; +} diff --git a/contracts/tests/utils/artifacts/README.md b/contracts/tests/utils/artifacts/README.md index 6fd8a7ef5e..067126d1be 100644 --- a/contracts/tests/utils/artifacts/README.md +++ b/contracts/tests/utils/artifacts/README.md @@ -39,6 +39,7 @@ Benefits: - `PoolBoosters.sol` - `Automation.sol` - `Zappers.sol` +- `Lens.sol` - `Mocks.sol` ## Usage