Reproduced Exploit

dForce — 100 tokens freeze when the beneficiary cannot call claim

claim requires msg.sender == beneficiary. The beneficiary here is a contract with no functions. It cannot call claim. A depositor or anyone else who calls claim reverts. The timelock still holds the 100 tokens and the sink's balance is 0. claimFixed drops the caller check and the same agreement del…

Sep 2023•Other•dos•3 min read

Chain

Other

Category

dos

Date

Sep 2023

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 34260-funds-may-freeze-on-the-timelock-if-the-beneficiary-does-not. Standalone Foundry PoC and full write-up: 34260-funds-may-freeze-on-the-timelock-if-the-beneficiary-does-not_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/dos/frozen-funds · vuln/access-control/broken-logic

Key info#

ProtocoldForce DefaultTimeLock
FindingMixBytes dForce Lending v2, AuditVault 34260
Vulnerable codeDefaultTimeLock.claim
Harm100 tokens locked for an immutable contract stay in the timelock
Chainlocal synthetic

TL;DR#

claim requires msg.sender == beneficiary. The beneficiary here is a contract with no functions. It cannot call claim. A depositor or anyone else who calls claim reverts. The timelock still holds the 100 tokens and the sink's balance is 0. claimFixed drops the caller check and the same agreement delivers the 100 tokens to the sink. That is the recommended fix: any account may invoke claim.

The vulnerable code#

SOLIDITY
function claim(uint256 id) external {
    Agreement storage a = agreements[id];
    require(a.open, "closed");
    // @> VULN: a beneficiary contract with no way to call claim can never receive the tokens
    require(msg.sender == a.beneficiary, "beneficiary");
    a.open = false;
    token.transfer(a.beneficiary, a.amount);
}

The audited line is the beneficiary check in DefaultTimeLock agreement execution. The source repo is no longer public. The check is the one the report describes.

Root cause#

Claim is both the release path and an authentication check that the beneficiary itself must satisfy. A beneficiary that is immutable and has no call path can never satisfy it. The tokens have already left the depositor.

Preconditions#

An agreement is created for 100 tokens with the sink as beneficiary. The sink's runtime has no functions. The depositor approved the timelock.

Attack walkthrough#

  1. execute(sink, 100) transfers 100 tokens into the timelock.
  2. The exploit calls claim. It is not the beneficiary.
  3. The require reverts.
  4. The timelock balance is 100. The sink balance is 0.

The control calls claimFixed on a fresh agreement. The sink receives 100 and the timelock balance is 0.

Diagrams#

flowchart TD A["Depositor locks 100 tokens for a contract"] --> B["Agreement beneficiary is the sink"] B --> C["Sink has no claim function"] C --> D["Another caller hits the beneficiary check"] D --> E["claim reverts"] E --> F["100 tokens remain in the timelock"]

Impact#

The sink is owed 100 tokens and cannot receive them. They sit in DefaultTimeLock until the owner intervenes with a different release path. Any beneficiary that cannot send a transaction is in the same position.

Remediation#

Allow any account to call claim, and still send the tokens to the stored beneficiary.

How to reproduce#

BASH
cd 34260-funds-may-freeze-on-the-timelock-if-the-beneficiary-does-not_exp
forge test -vv

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.