Farseen0/scar-pairs huggingface dataset:
HIGH severity: State update performed after external call in `MembershipER:mint`. **Description:** When `MembershipERC1155::mint` is invoked during a call to `MembershipFactory::joinDAO`, the `totalSupply` increment is performed after the call to `ERC1155::_mint`:

While there does not appear to be any immediate impact, this is in violation of the Checks-Effects-Interactions...

Vulnerable code:
function mint(address to, uint256 tokenId, uint256 amount) external override onlyRole(OWP_FACTORY_ROLE) {
    _mint(to, tokenId, amount, "");
    totalSupply += amount * 2 ** (6 - tokenId); // Update total supply with weight
}

if (to.isContract()) {
    try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
        if (response != IERC1155Receiver.onERC1155Received.selector) {
            revert("ERC1155: ERC1155Receiver rejected tokens");
        }
LOW severity: `TierConfig::price` is not validated to follow `TierConfig::power` which itself is not used or validated. **Description:** When creating a new DAO membership, the creator can specify a [`TierConfig::power`](https://github.com/OneWpOrg/audit-2024-10-oneworld/blob/416630e46ea6f0e9bd9bdd0aea6a48119d0b515a/contracts/dao/libraries/MembershipDAOStructs.sol#L34); however, this value is never used or...

Vulnerable code:
IMembershipERC1155(daoMembershipAddress).burn(msg.sender, fromTierIndex, 2);
IMembershipERC1155(daoMembershipAddress).mint(msg.sender, fromTierIndex - 1, 1);

function shareOf(address account) public view returns (uint256) {
    return (balanceOf(account, 0) * 64) +
           (balanceOf(account, 1) * 32) +
           (balanceOf(account, 2) * 16) +
           (balanceOf(account, 3) * 8) +
           (balanceOf(account, 4) * 4) +
           (balanceOf(account, 5) * 2) +
           balanceOf(account, 6);
}

for (uint256 i = 0; i < tierConfigs.length; i++) {
    dao.tiers.push(tierConfigs[i]);
}
LOW severity: DAOs of all types can be updated with a lower number of tiers and are not validated to be above zero. **Description:** When creating a new DAO membership in `MembershipFactory::createNewDAOMembership`, the tiers are [validated](https://github.com/OneWpOrg/audit-2024-10-oneworld/blob/416630e46ea6f0e9bd9bdd0aea6a48119d0b515a/contracts/dao/MembershipFactory.sol#L60) to be non-zero and not exceed the...

Vulnerable code:
require(daoConfig.noOfTiers == tierConfigs.length, "Invalid tier input.");
require(daoConfig.noOfTiers > 0 && daoConfig.noOfTiers <= 7, "Invalid tier count.");

if (daoConfig.daoType == DAOType.SPONSORED) {
    require(daoConfig.noOfTiers == 7, "Invalid tier count for sponsored.");
}
HIGH severity: Consider making `MembershipER:totalSupply` `public`. **Description:** The [`totalSupply`](https://github.com/OneWpOrg/audit-2024-10-oneworld/blob/416630e46ea6f0e9bd9bdd0aea6a48119d0b515a/contracts/dao/tokens/MembershipERC1155.sol#L23) variable in the `MembershipERC1155` contract is currently marked as `private`:

As this state variable could be...

Vulnerable code:
uint256 private totalSupply;
HIGH severity: Mixed use of `uint` and `uint256` in `MembershipER`. **Description:** The state declarations in `MembershipERC1155` use both `uint` and `uint256`:

This is inconsistent and confusing. Consider using `uint256` everywhere as this is more expressive.

**One World Project:** Updated in...

Vulnerable code:
mapping(address => uint256) public totalProfit;
mapping(address => mapping(address => uint)) internal lastProfit;
mapping(address => mapping(address => uint)) internal savedProfit;

uint256 internal constant ACCURACY = 1e30;

event Claim(address indexed account, uint amount);
event Profit(uint amount);
LOW severity: `MembershipFactory::owpWallet` lacks explicitly declared visibility. **Description:** [`MembershipFactory::owpWallet`](https://github.com/OneWpOrg/audit-2024-10-oneworld/blob/416630e46ea6f0e9bd9bdd0aea6a48119d0b515a/contracts/dao/MembershipFactory.sol#L20) has no declared visibility:

This gives it the default `internal` visibility; however, it is best practice to...

Vulnerable code:
address owpWallet;
LOW severity: `chainId` is used as the `EIP712Base::EIP712Domain.salt` in `DOMAIN_TYPEHASH`. **Description:** `EIP712Base` implements EIP-712; however, there is a mistake in the definition of [`DOMAIN_TYPEHASH`](https://github.com/OneWpOrg/smart-contracts-blockchain-1wp/blob/416630e46ea6f0e9bd9bdd0aea6a48119d0b515a/contracts/meta-transaction/EIP712Base.sol#L38) where `chainId` is used as...

Vulnerable code:
bytes32 private constant TYPE_HASH =
    keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
HIGH severity: `MembershipFactory::joinDAO` will not function correctly with fee-on-transfer tokens. **Description:** While it is understood that the protocol does not intend to support fee-on-transfer tokens, it is prescient to note that `MembershipFactory::joinDAO` will [not function correctly](https://github.com/OneWpOrg/audit-2024-10-oneworld/blob/416630e46ea6f0e9bd9bdd0aea6a48119d0b515a/contra...

Vulnerable code:
IERC20(daos[daoMembershipAddress].currency).transferFrom(msg.sender, owpWallet, platformFees);
IERC20(daos[daoMembershipAddress].currency).transferFrom(msg.sender, daoMembershipAddress, tierPrice - platformFees);
HIGH severity: The `savedProfit` mapping will always return zero. **Description:** When a DAO member calls [`MembershipERC1155::claimProfit`](https://github.com/OneWpOrg/audit-2024-10-oneworld/blob/416630e46ea6f0e9bd9bdd0aea6a48119d0b515a/contracts/dao/tokens/MembershipERC1155.sol#L138-L147), their current profit is calculated in...

Vulnerable code:
function saveProfit(address account, address currency) internal returns (uint profit) {
    uint unsaved = getUnsaved(account, currency);
    lastProfit[account][currency] = totalProfit[currency];
    profit = savedProfit[account][currency] + unsaved;
    savedProfit[account][currency] = profit;
}

function claimProfit(address currency) external returns (uint profit) {
    profit = saveProfit(msg.sender, currency);
    require(profit > 0, "No profit available");
    savedProfit[msg.sender][currency] = 0;
    IERC20(currency).safeTransfer(msg.sender, profit);
    emit Claim(msg.sender, profit);
}
HIGH severity: Cross-chain token transfer from `SpokeVault` fails due to approval from implementation contract instead of proxy. **Description:** [`SpokeVault`](https://github.com/m0-foundation/m-portal/blob/8f909076f4fc1f22028cabd6df8a9e6b5be4b078/src/SpokeVault.sol) is a contract that holds excess M tokens from Smart M and can transfer this excess to `HubVault` on mainnet via...

Vulnerable code:
// Approve the SpokePortal to transfer M tokens.
IERC20(mToken).approve(spokePortal_, type(uint256).max);

spokeVaultImplementation_ = address(
    new SpokeVault(spokePortal_, hubVault_, destinationChainId_, migrationAdmin_)
);

spokeVaultProxy_ = _deployCreate3Proxy(address(spokeVaultImplementation_), _computeSalt(deployer_, "Vault"));

+import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol";
...
    function transfer(
        uint256 amount,
        uint16 recipientChain,
        bytes32 recipient,
        bytes32 refundAddress,
        bool shouldQueue,
        bytes memory transceiverInstructions
    ) external payable returns (uint64) {
+       IERC20(mToken).transferFrom(msg.sender, address(this), amount);
    }
MEDIUM severity: SpokeVault transfers will frequently revert due to Wormhole gas fee fluctuations. **Description:** The `SpokeVault.transferExcessM()` function forwards ETH to pay for Wormhole gas fees, but any excess ETH sent will cause the transaction to revert since `SpokeVault` lacks capability to receive ETH refunds. Current implementation only works if the user sends a fee that is exactly.....

Vulnerable code:
// In ManagerBase.sol
function _prepareForTransfer(...) internal returns (...) {
    // ...
    if (msg.value < totalPriceQuote) {
        revert DeliveryPaymentTooLow(totalPriceQuote, msg.value);
    }

    uint256 excessValue = msg.value - totalPriceQuote;
    if (excessValue > 0) {
        _refundToSender(excessValue); // Reverts as SpokeVault can't accept ETH
    }
}

  function _refundToSender(
        uint256 refundAmount
    ) internal {
        // refund the price quote back to sender
        (bool refundSuccessful,) = payable(msg.sender).call{value: refundAmount}("");
         //@audit excess gas fee sent back to msg.sender (SpokeVault)

        // check success
        if (!refundSuccessful) {
            revert RefundFailed(refundAmount);
        }
    }

contract MockSpokePortal {
          address public immutable mToken;
          address public immutable registrar;

          constructor(address mToken_, address registrar_) {
              mToken = mToken_;
              registrar = registrar_;
          }

          function transfer(
              uint256 amount,
              uint16 recipientChain,
              bytes32 recipient,
              bytes32 refundAddress,
              bool shouldQueue,
              bytes memory transceiverInstructions
          ) external payable returns (uint64) {

              // mock return of excess fee back to sender
              if(msg.value > 1) {
                  payable(msg.sender).transfer(msg.value-1);
              }

          }
      }

   contract SpokeVaultTests is UnitTestBase {
        function testFail_transferExcessM() external { //@audits fails with excess fee
            uint256 amount_ = 1_000e6;
            uint256 balance_ = 10_000e6;
            uint256 fee_ = 2;
            _mToken.mint(address(_vault), balance_);
            vm.deal(_alice, fee_);

            vm.prank(_alice);
            _vault.transferExcessM{ value: fee_ }(amount_, _alice.toBytes32());
        }
   }
HIGH severity: `ExcessMTokenSent` event logs `messageSequence_` as `0` incorrectly. **Description:** When calling [`SpokeVault::transferExcessM`](https://github.com/m0-foundation/m-portal/blob/8f909076f4fc1f22028cabd6df8a9e6b5be4b078/src/SpokeVault.sol#L69-L89) to transfer excess M tokens from `SpokeVault`, an `ExcessMTokenSent` event is emitted:

However, at the time the event...

Vulnerable code:
) external payable returns (uint64 messageSequence_) {
    ...

    // @audit messageSequence_ not assigned yet
    emit ExcessMTokenSent(destinationChainId, messageSequence_, msg.sender.toBytes32(), hubVault_, amount_);

    messageSequence_ = INttManager(spokePortal).transfer{ value: msg.value }(
        ...
    );
}
MEDIUM severity: Operator can finalize for non-existent `finalShnarf`. **Description:** In the previous version of `LineaRollup::_finalizeBlocks` there was this check which ensured that the final shnarf was associated with a block number:

In the new version `shnarfFinalBlockNumbers` was changed to `blobShnarfExists` which links a shnarf to an effective boolean flag...

Vulnerable code:
if (
  shnarfFinalBlockNumbers[_finalizationData.finalSubmissionData.shnarf] !=
  _finalizationData.finalSubmissionData.finalBlockInData
) {
  revert FinalBlockDoesNotMatchShnarfFinalBlock(
    _finalizationData.finalSubmissionData.finalBlockInData,
    shnarfFinalBlockNumbers[_finalizationData.finalSubmissionData.shnarf]
  );
}

finalShnarf = _computeShnarf(
  _finalizationData.shnarfData.parentShnarf,
  _finalizationData.shnarfData.snarkHash,
  _finalizationData.shnarfData.finalStateRootHash,
  _finalizationData.shnarfData.dataEvaluationPoint,
  _finalizationData.shnarfData.dataEvaluationClaim
);

// @audit prevent finalization for non-existent final shnarf
if(blobShnarfExists[finalShnarf] == 0) revert FinalBlobNotSubmitted();
LOW severity: Operator can submit data via `LineaRollup::submitDataAsCalldata` for invalid parent shnarf. **Description:** `LineaRollup::submitBlobs` has this check to validate the parent shnarf exists:

But `LineaRollup::submitDataAsCalldata` has no similar check, meaning that an operator can submit data for an invalid parent shnarf by calling `submitDataAsCalldata`.

**Linea:** Fixed in...

Vulnerable code:
if (blobShnarfExists[_parentShnarf] == 0) {
      revert ParentBlobNotSubmitted(_parentShnarf);
    }
LOW severity: Mark `MessageManagerV1::outboxMessageStatus` as deprecated. **Description:** `L1MessageManagerV1::outboxL1L2MessageStatus` is never read or written to anymore apart from the test suite:

Therefore it should be marked as deprecated with a comment similar to how `LineaRollup` handles its deprecated mappings.

Similarly...

Vulnerable code:
$ rg "outboxL1L2MessageStatus"
test-contracts/TestL1MessageManager.sol
31:    outboxL1L2MessageStatus[_messageHash] = OUTBOX_STATUS_SENT;
39:      uint256 existingStatus = outboxL1L2MessageStatus[messageHash];
46:        outboxL1L2MessageStatus[messageHash] = OUTBOX_STATUS_RECEIVED;

messageService/l1/v1/L1MessageManagerV1.sol
22:  mapping(bytes32 messageHash => uint256 messageStatus) public outboxL1L2MessageStatus;

test-contracts/LineaRollupV5.sol
1944:  mapping(bytes32 messageHash => uint256 messageStatus) public outboxL1L2MessageStatus;

test-contracts/LineaRollupAlphaV3.sol
2011:  mapping(bytes32 messageHash => uint256 messageStatus) public outboxL1L2MessageStatus;

tokenBridge/mocks/MessageBridgeV2/MockMessageServiceV2.sol
40:    outboxL1L2MessageStatus[messageHash] = OUTBOX_STATUS_SENT;
LOW severity: Remove comments which no longer apply. **Description:** Comments which no longer apply should be removed as they are now misleading.

File: `L1MessageServiceV1.sol`

File: `L1MessageService.sol`

**Linea:** Fixed in [PR256](https://github.com/Consensys/linea-monorepo/pull/256) commits...

Vulnerable code:
// @audit `claimMessage` no longer uses `_messageSender` so these comments are incorrect
   * @dev _messageSender is set temporarily when claiming and reset post. Used in sender().
   * @dev _messageSender is reset to DEFAULT_SENDER_ADDRESS to be more gas efficient.

// @audit `_messageSender` no longer initialized as it is not used anymore by L1 Messaging
   * @dev _messageSender is initialised to a non-zero value for gas efficiency on claiming.
LOW severity: `MessageService::reinitializePauseTypesAndPermissions` should use `reinitializer(2)`. **Description:** `TokenBridge::reinitializePauseTypesAndPermissions` uses `reinitializer(2)` because:

`LineaRollup::reinitializeLineaRollupV6` uses `reinitializer(6)` because:

But `L2MessageService::reinitializePauseTypesAndPermissions` uses `reinitializer(6)` even though:

For consistency...

Vulnerable code:
export ETH_RPC_URL=mainnet_rpc
cast storage 0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319 0
0x0000000000000000000000000000000000000000000000000000000000000001
export ETH_RPC_URL=linea_rpc
cast storage 0x353012dc4a9A6cF55c941bADC267f82004A8ceB9 0
0x0000000000000000000000000000000000000000000000000000000000000001

export ETH_RPC_URL=mainnet_rpc
cast storage 0xd19d4B5d358258f05D7B411E21A1460D11B0876F 0
0x0000000000000000000000000000000000000000000000000000000000000005

export ETH_RPC_URL=linea_rpc
cast storage 0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec 0
0x0000000000000000000000000000000000000000000000000000000000000001
LOW severity: Remove redundant `MessageManagerV1::__MessageManager_init` and associated constant. **Description:** `L2MessageManagerV1::__L2MessageManager_init` is no longer called by `L2MessageService::initialize` which uses the new `PermissionsManager` contract.

Hence it should be removed along with its associated constant `L1_L2_MESSAGE_SETTER_ROLE`. This constant is referenced in comments.....

Vulnerable code:
// output of: rg "__L2MessageManager_init"
messageService/l2/v1/L2MessageManagerV1.sol
39:  function __L2MessageManager_init(address _l1l2MessageSetter) internal onlyInitializing {

test-contracts/TestL2MessageManager.sol
32:    __L2MessageManager_init(_l1l2MessageSetter);
41:    __L2MessageManager_init(_l1l2MessageSetter);

test-contracts/L2MessageServiceLineaMainnet.sol
1620:  function __L2MessageManager_init(address _l1l2MessageSetter) internal onlyInitializing {
1992:    __L2MessageManager_init(_l1l2MessageSetter);

// output of: rg "L1_L2_MESSAGE_SETTER_ROLE"
messageService/l2/L2MessageManager.sol
26:   * @dev Only address that has the role 'L1_L2_MESSAGE_SETTER_ROLE' are allowed to call this function.
40:  ) external whenTypeNotPaused(PauseType.GENERAL) onlyRole(L1_L2_MESSAGE_SETTER_ROLE) {

messageService/l2/v1/L2MessageManagerV1.sol
18:  bytes32 public constant L1_L2_MESSAGE_SETTER_ROLE = keccak256("L1_L2_MESSAGE_SETTER_ROLE");
37:   * @param _l1l2MessageSetter The address owning the L1_L2_MESSAGE_SETTER_ROLE role.
40:    _grantRole(L1_L2_MESSAGE_SETTER_ROLE, _l1l2MessageSetter);

interfaces/l2/IL2MessageManager.sol
48:   * @dev Only address that has the role 'L1_L2_MESSAGE_SETTER_ROLE' are allowed to call this function.

test-contracts/L2MessageServiceLineaMainnet.sol
1601:  bytes32 public constant L1_L2_MESSAGE_SETTER_ROLE = keccak256("L1_L2_MESSAGE_SETTER_ROLE");
1618:   * @param _l1l2MessageSetter The address owning the L1_L2_MESSAGE_SETTER_ROLE role.
1621:    _grantRole(L1_L2_MESSAGE_SETTER_ROLE, _l1l2MessageSetter);
1626:   * @dev Only address that has the role 'L1_L2_MESSAGE_SETTER_ROLE' are allowed to call this function.
1629:  function addL1L2MessageHashes(bytes32[] calldata _messageHashes) external onlyRole(L1_L2_MESSAGE_SETTER_ROLE) {
LOW severity: Use named return variables to save at least 9 gas per variable. **Description:** Using [named return variables](https://x.com/DevDacian/status/1796396988659093968) saves at least 9 gas per variable; named returns are already used in some functions of the protocol but not in others:

**Linea:** Fixed in...

Vulnerable code:
PauseManager.sol
136:  function isPaused(PauseType _pauseType) public view returns (bool)

l1/L1MessageManager.sol
98:  function isMessageClaimed(uint256 _messageNumber) external view returns (bool) {

l1/L1MessageService.sol
150:  function sender() external view returns (address addr) {

l2/v1/L2MessageServiceV1.sol
165:  function sender() external view returns (address) {

lib/SparseMerkleTreeVerifier.sol
32:  ) internal pure returns (bool) {

TokenBridge.sol
 function _safeName(address _token) internal view returns (string memory) {
  function _safeSymbol(address _token) internal view returns (string memory) {
  function _safeDecimals(address _token) internal view returns (uint8) {
  function _returnDataToString(bytes memory _data) internal pure returns (string memory) {
HIGH severity: Zeeve admin could drain `ValidatorRewarder` by abusing off-chain BLS validation due to `QI` rewards being granted to failed registrations. **Description:** [`Ignite::releaseLockedTokens`](https://github.com/Benqi-fi/ignite-contracts/blob/bbca0ddb399225f378c1d774fb70a7486e655eea/src/Ignite.sol#L580-L583) takes the [`failed`](https://github.com/Benqi-fi/ignite-contracts/blob/bbca0ddb399225f378c1d774fb70a7486e655eea/src/Ignite.sol#L578).....

Vulnerable code:
} else {
    avaxRedemptionAmount = avaxDepositAmount + registration.rewardAmount;
    qiRedemptionAmount = qiDepositAmount;

    if (qiRewardEligibilityByNodeId[nodeId]) {
        qiRedemptionAmount += validatorRewarder.claimRewards(
            registration.validationDuration,
            registration.tokenDeposits.tokenAmount
        );
    }

    minimumContractBalance -= avaxRedemptionAmount;
}

it("earns qi rewards for failed registrations", async function () {
  await validatorRewarder.setTargetApr(1000);
  await ignite.setValidatorRewarder(validatorRewarder.address);
  await grantRole("ROLE_RELEASE_LOCKED_TOKENS", admin.address);

  // AVAX $20, QI $0.01
  const qiStake = hre.ethers.utils.parseEther("200").mul(2_000);
  const qiFee = hre.ethers.utils.parseEther("1").mul(2_000);

  // approve Ignite to spend pre-validated QI (bypassing StakingContract)
  await qi.approve(ignite.address, qiStake.add(qiFee));
  await ignite.registerWithPrevalidatedQiStake(
    admin.address,
    "NodeID-Superpools1",
    "0x" + blsPoP.toString("hex"),
    86400 * 28,
    qiStake.add(qiFee),
  );

  // registration of node fails
  await ignite.releaseLockedTokens("NodeID-Superpools1", true);

  const balanceBefore = await qi.balanceOf(admin.address);
  await ignite.connect(admin).redeemAfterExpiry("NodeID-Superpools1");

  const balanceAfter = await qi.balanceOf(admin.address);

  // stake + rewards are returned to the user
  expect(Number(balanceAfter.sub(balanceBefore))).to.be.greaterThan(Number(qiStake));
});

} else {
        minimumContractBalance += msg.value;
        totalSubsidisedAmount -= 2000e18 - msg.value;
+       qiRewardEligibilityByNodeId[nodeId] = false;
    }
HIGH severity: Redemption of slashed registrations could result in DoS due to incorrect state update. **Description:** Due to the logic surrounding Pay As You Go (PAYG) registrations, potential refunds associated with failed validators, and post-expiry redemptions of stake, a minimum balance of `AVAX` is required to remain within the `Ignite` contract. The...

Vulnerable code:
it("with slashing using a slash percentage", async function () {
  // Add AVAX slashing percentage to trigger the bug (50% so the numbers are easy)
  await ignite.setAvaxSlashPercentage("5000");

  // Register NodeID-1 for two weeks with 1000 AVAX and 200k QI
  await ignite.registerWithStake("NodeID-1", blsPoP, 86400 * 14, {
    value: hre.ethers.utils.parseEther("1000"),
  });

  // Release the registration with `msg.value` equal to AVAX deposit amount to trigger slashing
  await ignite.releaseLockedTokens("NodeID-1", false, {
    value: hre.ethers.utils.parseEther("1000"),
  });

  // The slashed amount is decremented from the minumum contract balance
  expect(await ignite.minimumContractBalance()).to.equal(hre.ethers.utils.parseEther("500"));

  // Reverts on underflow since it tries to subtract 1000 (avaxDepositAmount) from 500 (minimumContractBalance)
  await expect(ignite.redeemAfterExpiry("NodeID-1")).to.be.reverted;
});

if (registration.slashed) {
    avaxRedemptionAmount = avaxDepositAmount - avaxDepositAmount * registration.avaxSlashPercentage / 10_000;
    qiRedemptionAmount = qiDepositAmount - qiDepositAmount * registration.qiSlashPercentage / 10_000;

-   minimumContractBalance -= avaxDepositAmount;
+   minimumContractBalance -= avaxRedemptionAmount;
} else {
HIGH severity: The default admin role controls all other roles within `StakingContract`. **Description:** Within `StakingContract`, there is intended separation between the Zeeve and BENQI admin/super-admin roles as implemented in the [`grantAdminRole()`](https://code.zeeve.net/zeeve-endeavors/benqi_smartcontract/-/blob/b63336201f50f9a67451bf5c7b32ddcc4a847ce2/contracts/staking.sol#L986...

Vulnerable code:
it("allows BENQI_SUPER_ADMIN to update ZEEVE_SUPER_ADMIN", async function () {
    const zeeveSuperAdminRole = await stakingContract.ZEEVE_SUPER_ADMIN_ROLE();

    // BENQI_SUPER_ADMIN can use OpenZepplin's grantRole to alter ZEEVE_SUPER_ADMIN_ROLE
    await stakingContract.connect(benqiSuperAdmin).grantRole(zeeveSuperAdminRole, otherUser.address);
    await stakingContract.connect(benqiSuperAdmin).revokeRole(zeeveSuperAdminRole, zeeveSuperAdmin.address);

    expect(await stakingContract.hasRole(zeeveSuperAdminRole, otherUser.address)).to.be.true;
    expect(await stakingContract.hasRole(zeeveSuperAdminRole, zeeveSuperAdmin.address)).to.be.false;
});

function test_defaultAdminControlsAllRolesPoC() public {
    assertEq(stakingContract.getRoleAdmin(stakingContract.DEFAULT_ADMIN_ROLE()), stakingContract.DEFAULT_ADMIN_ROLE());
    assertEq(stakingContract.getRoleAdmin(stakingContract.BENQI_SUPER_ADMIN_ROLE()), stakingContract.DEFAULT_ADMIN_ROLE());
    assertEq(stakingContract.getRoleAdmin(stakingContract.BENQI_ADMIN_ROLE()), stakingContract.DEFAULT_ADMIN_ROLE());
    assertEq(stakingContract.getRoleAdmin(stakingContract.ZEEVE_SUPER_ADMIN_ROLE()), stakingContract.DEFAULT_ADMIN_ROLE());
    assertEq(stakingContract.getRoleAdmin(stakingContract.ZEEVE_ADMIN_ROLE()), stakingContract.DEFAULT_ADMIN_ROLE());

    address EXTERNAL_ADDRESS = makeAddr("EXTERNAL_ADDRESS");
    vm.startPrank(BENQI_SUPER_ADMIN);
    stakingContract.grantRole(stakingContract.ZEEVE_SUPER_ADMIN_ROLE(), EXTERNAL_ADDRESS);
    vm.stopPrank();
    assertTrue(stakingContract.hasRole(stakingContract.ZEEVE_SUPER_ADMIN_ROLE(), EXTERNAL_ADDRESS));
}