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(), passesDEFAULT_ADMIN_ROLEvalidation againstLRTConfig, clears the pause flag, and emitsUnpaused(address).
Attack Execution
High-Level Flow
- A transaction submitter
0x51c59785639cca31c09d0833749e76a5d945c9f3sends a SafeexecTransaction()call to the Kelp DAO admin Safe. - The Safe delegates into its singleton implementation to execute the signed transaction.
- The Safe performs repeated signature-recovery checks through the
0x0000000000000000000000000000000000000001precompile. - The Safe calls
LRTDepositPool.unpause()on the proxy0x036676389e48133b63a802f8635ad39e752d375d. - The pool implementation checks
LRTConfig.hasRole(DEFAULT_ADMIN_ROLE, safe)and receivestrue. - The pool clears its paused flag and emits
Unpaused(address). - 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:
0x8c8b137cd586b37c4eb345d6ddde24db7c91e64fd8ea99abb04169befcd13966Block number:
25101229Block timestamp:
2026-05-15 14:44:59 UTCReceipt status:
0x1Transaction sender / submitter:
0x51c59785639cca31c09d0833749e76a5d945c9f3Admin Safe target:
0xb3696a817d01c8623e66d156b6798291fa10a46dAffected pool proxy:
0x036676389e48133b63a802f8635ad39e752d375dPool implementation:
0xea38dfa108318288f36f13d06e821a64acda8320Config proxy:
0x947cb49334e6571ccbfef1f1f1178d8469d65ec7Config implementation:
0xd4f475a7df199b3106f622a3a825ff399d4dafceSafe singleton:
0xd9db270c1b5e3bd161e8c8503c55ceabee709552Verified selector:
execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)->0x6a761202Verified selector:
unpause()->0x3f4ba83aVerified selector:
hasRole(bytes32,address)->0x91d14854Receipt log topic
0x5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa=Unpaused(address)The
Unpaused(address)log was emitted by0x036676389e48133b63a802f8635ad39e752d375dwith data0x000000000000000000000000b3696a817d01c8623e66d156b6798291fa10a46d, identifying the admin Safe as the caller recorded by the event.Receipt log topic
0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e=ExecutionSuccess(bytes32,uint256)trace_prestateTracer.jsonshows the pool proxy storage slot0x33changed from0x1before the transaction to cleared afterward, consistent with unpausing.trace_prestateTracer.jsonalso shows the Safe nonce slot0x5incremented from0x94to0x95, consistent with a normal Safe transaction execution.No
CREATEorCREATE2operations appear intrace_callTracer.json.