diff --git a/contracts/0.8.25/vaults/VaultHubViewerV1.sol b/contracts/0.8.25/vaults/VaultHubViewerV1.sol index ea05057d9..72eadf52e 100644 --- a/contracts/0.8.25/vaults/VaultHubViewerV1.sol +++ b/contracts/0.8.25/vaults/VaultHubViewerV1.sol @@ -22,6 +22,8 @@ interface IVaultHub { } contract VaultHubViewerV1 { + bytes32 constant strictTrue = keccak256(hex"0000000000000000000000000000000000000000000000000000000000000001"); + IVaultHub public immutable vaultHub; bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; @@ -43,38 +45,26 @@ contract VaultHubViewerV1 { /// @param _owner The address to check /// @return True if the address is the owner, false otherwise function isOwner(IVault vault, address _owner) public view returns (bool) { - address currentOwner = vault.owner(); - if (currentOwner == _owner) { + address vaultOwner = vault.owner(); + if (vaultOwner == _owner) { return true; } - if (isContract(currentOwner)) { - try IDashboardACL(currentOwner).hasRole(DEFAULT_ADMIN_ROLE, _owner) returns (bool result) { - return result; - } catch { - return false; - } - } - return false; + + return _checkHasRole(vaultOwner, _owner, DEFAULT_ADMIN_ROLE); } - /// @notice Checks if a given address has a given role on a vault + /// @notice Checks if a given address has a given role on a vault owner contract /// @param vault The vault to check /// @param _member The address to check /// @param _role The role to check /// @return True if the address has the role, false otherwise function hasRole(IVault vault, address _member, bytes32 _role) public view returns (bool) { - address owner = vault.owner(); - if (owner == address(0)) { + address vaultOwner = vault.owner(); + if (vaultOwner == address(0)) { return false; } - if (!isContract(owner)) return false; - - try IDashboardACL(owner).hasRole(_role, _member) returns (bool result) { - return result; - } catch { - return false; - } + return _checkHasRole(vaultOwner, _member, _role); } /// @notice Returns all vaults owned by a given address @@ -118,6 +108,23 @@ contract VaultHubViewerV1 { return _filterNonZeroVaults(vaults, valid); } + /// @notice safely returns if role member has given role + /// @param _contract that can have ACL or not + /// @param _member addrress to check for role + /// @return _role ACL role bytes + function _checkHasRole(address _contract, address _member, bytes32 _role) internal view returns (bool) { + if (!isContract(_contract)) return false; + + bytes memory payload = abi.encodeWithSignature("hasRole(bytes32,address)", _role, _member); + (bool success, bytes memory result) = _contract.staticcall(payload); + + if (success && keccak256(result) == strictTrue) { + return true; + } else { + return false; + } + } + /// @notice Filters out zero address vaults from an array /// @param _vaults Array of vaults to filter /// @param _validCount number of non-zero vaults diff --git a/test/0.8.25/vaults/vault-hub-viewer/contracts/CustomOwner__MockForHubViewer.sol b/test/0.8.25/vaults/vault-hub-viewer/contracts/CustomOwner__MockForHubViewer.sol new file mode 100644 index 000000000..9ff84c41a --- /dev/null +++ b/test/0.8.25/vaults/vault-hub-viewer/contracts/CustomOwner__MockForHubViewer.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.25; + +import {VaultHub} from "contracts/0.8.25/vaults/VaultHub.sol"; + +// +// This mock represents custom Vault owner contract, that does not have ACL e.g. Safe +// +contract CustomOwner__MockForHubViewer { + bool public shouldRevertFallback; + + constructor() { + shouldRevertFallback = true; + } + + function setShouldRevertFallback(bool _shouldRevertFallback) external { + shouldRevertFallback = _shouldRevertFallback; + } + + fallback() external { + if (shouldRevertFallback) revert("fallback"); + } + + receive() external payable { + if (shouldRevertFallback) { + revert("fallback"); + } + } +} diff --git a/test/0.8.25/vaults/vault-hub-viewer/vault-hub-viewer.ts b/test/0.8.25/vaults/vault-hub-viewer/vault-hub-viewer.ts index bc858fb1d..3b7f7243c 100644 --- a/test/0.8.25/vaults/vault-hub-viewer/vault-hub-viewer.ts +++ b/test/0.8.25/vaults/vault-hub-viewer/vault-hub-viewer.ts @@ -4,6 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { + CustomOwner__MockForHubViewer, Dashboard, Delegation, DepositContract__MockForStakingVault, @@ -93,6 +94,24 @@ const deployVaultDashboard = async ( return { vaultDashboard, dashboard }; }; +const deployCustomOwner = async (vaultImpl: StakingVault, operator: HardhatEthersSigner) => { + const customOwner = await ethers.deployContract("CustomOwner__MockForHubViewer"); + // deploying factory/beacon + const factoryStakingVault = await ethers.deployContract("VaultFactory__MockForStakingVault", [ + await vaultImpl.getAddress(), + ]); + const vaultCreation = await factoryStakingVault + .createVault(await customOwner.getAddress(), await operator.getAddress()) + .then((tx) => tx.wait()); + if (!vaultCreation) throw new Error("Vault creation failed"); + const events = findEvents(vaultCreation, "VaultCreated"); + if (events.length != 1) throw new Error("There should be exactly one VaultCreated event"); + const vaultCreatedEvent = events[0]; + + const stakingVault = StakingVault__factory.connect(vaultCreatedEvent.args.vault); + return { stakingVault, customOwner }; +}; + const deployStakingVault = async ( vaultImpl: StakingVault, vaultOwner: HardhatEthersSigner, @@ -139,9 +158,12 @@ describe("VaultHubViewerV1", () => { let stakingVault: StakingVault; let vaultDashboard: StakingVault; let vaultDelegation: StakingVault; + let vaultCustom: StakingVault; let vaultHubViewer: VaultHubViewerV1; + let dashboard: Dashboard; let delegation: Delegation; + let customOwnerContract: CustomOwner__MockForHubViewer; let originalState: string; @@ -160,6 +182,7 @@ describe("VaultHubViewerV1", () => { dashboardImpl = await ethers.deployContract("Dashboard", [steth, weth, wsteth]); delegationImpl = await ethers.deployContract("Delegation", [steth, weth, wsteth]); + // Delegation controlled vault const delegationResult = await deployVaultDelegation( vaultImpl, delegationImpl, @@ -171,12 +194,19 @@ describe("VaultHubViewerV1", () => { vaultDelegation = delegationResult.vaultDelegation; delegation = delegationResult.delegation; + // Dashboard conntroled vault const dashboardResult = await deployVaultDashboard(vaultImpl, dashboardImpl, factoryOwner, vaultOwner, operator); vaultDashboard = dashboardResult.vaultDashboard; dashboard = dashboardResult.dashboard; + // EOA controlled vault stakingVault = await deployStakingVault(vaultImpl, vaultOwner, operator); + // Custom owner controlled vault + const customdResult = await deployCustomOwner(vaultImpl, operator); + vaultCustom = customdResult.stakingVault; + customOwnerContract = customdResult.customOwner; + vaultHubViewer = await ethers.deployContract("VaultHubViewerV1", [hub]); expect(await vaultHubViewer.vaultHub()).to.equal(hub); @@ -204,6 +234,7 @@ describe("VaultHubViewerV1", () => { await hub.connect(hubSigner).mock_connectVault(vaultDelegation.getAddress()); await hub.connect(hubSigner).mock_connectVault(vaultDashboard.getAddress()); await hub.connect(hubSigner).mock_connectVault(stakingVault.getAddress()); + await hub.connect(hubSigner).mock_connectVault(vaultCustom.getAddress()); }); it("returns all vaults owned by a given address", async () => { @@ -213,6 +244,12 @@ describe("VaultHubViewerV1", () => { expect(vaults[1]).to.equal(vaultDashboard); expect(vaults[2]).to.equal(stakingVault); }); + + it("returns correct owner for custom vault", async () => { + const vaults = await vaultHubViewer.vaultsByOwner(customOwnerContract.getAddress()); + expect(vaults.length).to.equal(1); + expect(vaults[0]).to.equal(vaultCustom); + }); }); context("vaultsByRole", () => { @@ -220,6 +257,7 @@ describe("VaultHubViewerV1", () => { await hub.connect(hubSigner).mock_connectVault(vaultDelegation.getAddress()); await hub.connect(hubSigner).mock_connectVault(vaultDashboard.getAddress()); await hub.connect(hubSigner).mock_connectVault(stakingVault.getAddress()); + await hub.connect(hubSigner).mock_connectVault(vaultCustom.getAddress()); }); it("returns all vaults with a given role on Delegation", async () => {