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.
No exploit root cause was identified. This transaction is an authorized administrative unpause, and the access control guard behaved as intended.
No vulnerable contract was identified in this transaction.
The affected component that was operated is:
Kelp DAO: LRT Deposit Pool0x036676389e48133b63a802f8635ad39e752d375d0xea38dfa108318288f36f13d06e821a64acda8320manifest.json, then implementation source from the collected verified source treeNo vulnerable function was identified.
The function that was executed is:
unpause()0x3f4ba83acontracts/LRTDepositPool.solThe 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
}
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:
unpause() after prior incident-response or maintenance actions.unpause(), passes DEFAULT_ADMIN_ROLE validation against LRTConfig, clears the pause flag, and emits Unpaused(address).0x51c59785639cca31c09d0833749e76a5d945c9f3 sends a Safe execTransaction() call to the Kelp DAO admin Safe.0x0000000000000000000000000000000000000001 precompile.LRTDepositPool.unpause() on the proxy 0x036676389e48133b63a802f8635ad39e752d375d.LRTConfig.hasRole(DEFAULT_ADMIN_ROLE, safe) and receives true.Unpaused(address).ExecutionSuccess(bytes32,uint256) and returns successfully.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
00 USDfunds_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.
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.