Kelp DAO LRTDepositPool Authorized Admin Unpause (Not an Exploit)
2026-5-14 23:59:6 Author: www.darknavy.org(查看原文) 阅读量:0 收藏

2026-05-15 · Loss: 0 USD · Access Control

On Ethereum at 2026-05-15 14:44:59 UTC (block 25101229), Kelp DAO’s LRTDepositPool was unpaused through the protocol’s admin Safe rather than through an exploit path. The executed action was an authorized unpause() administrative operation guarded by an on-chain DEFAULT_ADMIN_ROLE check in LRTConfig. No ERC-20 transfers, approvals, ETH transfers, helper-contract deployments, or attacker gains occurred in this transaction, so the financial impact was 0 USD. The trace shows a standard Safe execTransaction() call into LRTDepositPool.unpause(), followed by LRTConfig.hasRole(DEFAULT_ADMIN_ROLE, safe) == true and a single pause-state storage change. For pipeline classification purposes this incident is closest to access_control, but the observed path is a valid authorization flow, not an access-control bypass.

Root Cause

No exploit root cause was identified. This transaction is an authorized administrative unpause, and the access control guard behaved as intended.

Vulnerable Contract

No vulnerable contract was identified in this transaction.

The affected component that was operated is:

  • Contract: Kelp DAO: LRT Deposit Pool
  • Proxy address: 0x036676389e48133b63a802f8635ad39e752d375d
  • Proxy: Yes
  • Implementation: 0xea38dfa108318288f36f13d06e821a64acda8320
  • How resolved: proxy mapping in manifest.json, then implementation source from the collected verified source tree
  • Source type: verified

Vulnerable Function

No vulnerable function was identified.

The function that was executed is:

  • Function: unpause()
  • Selector: 0x3f4ba83a
  • File: contracts/LRTDepositPool.sol

Vulnerable Code

The relevant code path is an admin-gated unpause flow, not a flawed permission bypass:

// contracts/LRTDepositPool.sol
function unpause() external onlyLRTAdmin {
    _unpause(); // <-- executed state change; clears the paused flag only after the admin check passes
}

// contracts/utils/LRTConfigRoleChecker.sol
modifier onlyLRTAdmin() {
    if (!IAccessControl(address(lrtConfig)).hasRole(LRTConstants.DEFAULT_ADMIN_ROLE, msg.sender)) { // <-- authorization check
        revert ILRTConfig.CallerNotLRTConfigAdmin();
    }
    _;
}

// @openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol
function hasRole(bytes32 role, address account) public view virtual override returns (bool) {
    return _roles[role].members[account]; // <-- returns true for the admin Safe in this transaction
}

Why It’s Vulnerable

This transaction does not demonstrate a vulnerability.

Expected behavior: only an address holding DEFAULT_ADMIN_ROLE in LRTConfig should be able to call unpause() on LRTDepositPool.

Actual behavior: the trace shows msg.sender at the unpause() call site is the Kelp DAO admin Safe 0xb3696a817d01c8623e66d156b6798291fa10a46d, and the subsequent hasRole(bytes32,address) check on LRTConfig returns 0x1 for role 0x00 and that Safe address before _unpause() executes.

That means the access control gate worked exactly as designed. The transaction does not show an external caller bypassing authorization, abusing upgradeability, or extracting value. The only protocol state change on the target contract is the pause flag clearing from storage slot 0x33, with no surrounding asset movement.

Normal flow vs Observed flow:

  • Normal flow: an authorized Kelp DAO admin submits a Safe transaction to call unpause() after prior incident-response or maintenance actions.
  • Observed flow: the Safe verifies signatures, calls unpause(), passes DEFAULT_ADMIN_ROLE validation against LRTConfig, clears the pause flag, and emits Unpaused(address).

Attack Execution

High-Level Flow

  1. A transaction submitter 0x51c59785639cca31c09d0833749e76a5d945c9f3 sends a Safe execTransaction() call to the Kelp DAO admin Safe.
  2. The Safe delegates into its singleton implementation to execute the signed transaction.
  3. The Safe performs repeated signature-recovery checks through the 0x0000000000000000000000000000000000000001 precompile.
  4. The Safe calls LRTDepositPool.unpause() on the proxy 0x036676389e48133b63a802f8635ad39e752d375d.
  5. The pool implementation checks LRTConfig.hasRole(DEFAULT_ADMIN_ROLE, safe) and receives true.
  6. The pool clears its paused flag and emits Unpaused(address).
  7. The Safe emits ExecutionSuccess(bytes32,uint256) and returns successfully.

Detailed Call Trace

The following flow is derived directly from trace_callTracer.json. Selectors were verified with cast sig.

0x51c59785639cca31c09d0833749e76a5d945c9f3
-> 0xb3696a817d01c8623e66d156b6798291fa10a46d
   CALL execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)
   selector 0x6a761202, value 0

  -> 0xd9db270c1b5e3bd161e8c8503c55ceabee709552
     DELEGATECALL execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)
     selector 0x6a761202, value 0

    -> 0x0000000000000000000000000000000000000001
       STATICCALL ecrecover precompile input, value 0
    -> 0x0000000000000000000000000000000000000001
       STATICCALL ecrecover precompile input, value 0
    -> 0x0000000000000000000000000000000000000001
       STATICCALL ecrecover precompile input, value 0
    -> 0x0000000000000000000000000000000000000001
       STATICCALL ecrecover precompile input, value 0
    -> 0x0000000000000000000000000000000000000001
       STATICCALL ecrecover precompile input, value 0
    -> 0x0000000000000000000000000000000000000001
       STATICCALL ecrecover precompile input, value 0

    -> 0x036676389e48133b63a802f8635ad39e752d375d
       CALL unpause()
       selector 0x3f4ba83a, value 0

      -> 0xea38dfa108318288f36f13d06e821a64acda8320
         DELEGATECALL unpause()
         selector 0x3f4ba83a, value 0

        -> 0x947cb49334e6571ccbfef1f1f1178d8469d65ec7
           STATICCALL hasRole(bytes32,address)
           selector 0x91d14854, value 0
           args:
           - role = 0x00 (DEFAULT_ADMIN_ROLE)
           - account = 0xb3696a817d01c8623e66d156b6798291fa10a46d
           output = 0x1

          -> 0xd4f475a7df199b3106f622a3a825ff399d4dafce
             DELEGATECALL hasRole(bytes32,address)
             selector 0x91d14854, value 0
             output = 0x1

Financial Impact

  • Total loss: 0
  • Total loss in USD: 0 USD
  • ERC-20 transfers: none
  • ERC-20 approvals: none
  • ETH transfers: none
  • Attacker profit after costs: none detected
  • Protocol solvency impact: none; the protocol remains functional and only its pause state changed

funds_flow.json contains empty transfers, approvals, eth_transfers, and attacker_gains arrays, with summary No attacker gains detected. This is consistent with an authorized administrative state change rather than a value-extracting exploit.

Evidence

  • Transaction hash: 0x8c8b137cd586b37c4eb345d6ddde24db7c91e64fd8ea99abb04169befcd13966

  • Block number: 25101229

  • Block timestamp: 2026-05-15 14:44:59 UTC

  • Receipt status: 0x1

  • Transaction sender / submitter: 0x51c59785639cca31c09d0833749e76a5d945c9f3

  • Admin Safe target: 0xb3696a817d01c8623e66d156b6798291fa10a46d

  • Affected pool proxy: 0x036676389e48133b63a802f8635ad39e752d375d

  • Pool implementation: 0xea38dfa108318288f36f13d06e821a64acda8320

  • Config proxy: 0x947cb49334e6571ccbfef1f1f1178d8469d65ec7

  • Config implementation: 0xd4f475a7df199b3106f622a3a825ff399d4dafce

  • Safe singleton: 0xd9db270c1b5e3bd161e8c8503c55ceabee709552

  • Verified selector: execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes) -> 0x6a761202

  • Verified selector: unpause() -> 0x3f4ba83a

  • Verified selector: hasRole(bytes32,address) -> 0x91d14854

  • Receipt log topic 0x5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa = Unpaused(address)

  • The Unpaused(address) log was emitted by 0x036676389e48133b63a802f8635ad39e752d375d with data 0x000000000000000000000000b3696a817d01c8623e66d156b6798291fa10a46d, identifying the admin Safe as the caller recorded by the event.

  • Receipt log topic 0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e = ExecutionSuccess(bytes32,uint256)

  • trace_prestateTracer.json shows the pool proxy storage slot 0x33 changed from 0x1 before the transaction to cleared afterward, consistent with unpausing.

  • trace_prestateTracer.json also shows the Safe nonce slot 0x5 incremented from 0x94 to 0x95, consistent with a normal Safe transaction execution.

  • No CREATE or CREATE2 operations appear in trace_callTracer.json.


文章来源: https://www.darknavy.org/web3/exploits/kelp-dao-lrtdepositpool-authorized-unpause/
如有侵权请联系:admin#unsafe.sh