Skip to content
On this page

Climber: Scaling the Heights of Smart Contract Security for a 10 Million DVT Heist

The world of decentralized finance thrives on innovation, but with great power comes the need for ironclad security. Enter "Climber," a thrilling CTF challenge from Damn Vulnerable DeFi, where a hefty 10 million DVT tokens are locked away in a seemingly impenetrable vault. Our mission? To rescue these tokens and deposit them into a designated recovery account. This isn't just a simple snatch-and-grab; it's a multi-stage operation requiring a deep understanding of upgradeable contracts, access control, and a cunning exploitation of a critical timing vulnerability.

The Fortress: A Vault Built for Security (or so it seemed)

At the heart of the challenge lies the ClimberVault, safeguarding those precious DVT tokens. It boasts several layers of defense:

  1. Upgradeability (UUPS): The vault follows the UUPS pattern, meaning its logic can be updated, but only its legitimate owner can authorize such an upgrade.
  2. Timelock Ownership: Crucially, the vault isn't owned by a regular EOA (Externally Owned Account) but by a ClimberTimelock contract. This means any action requiring onlyOwner permissions (like upgrading the vault or withdrawing funds) must first pass through the timelock's carefully controlled process.
  3. Limited Withdrawals: The timelock can only withdraw a small WITHDRAWAL_LIMIT (1 DVT) every WAITING_PERIOD (15 days), preventing large, rapid withdrawals.
  4. Emergency Sweeper: An _sweeper role exists, capable of sweeping all tokens in an emergency. However, this role is tightly controlled.

The ClimberTimelock itself adds another layer of security:

  1. Role-Based Access Control: Actions like schedule (which queues up operations) can only be performed by accounts holding the PROPOSER_ROLE.
  2. Time Delay: Once an action is scheduled by a Proposer, it must wait a delay period (initially 1 hour) before it can be executed.
  3. Self-Administered Delay: The timelock's updateDelay function can only be called by the timelock contract itself, seemingly preventing external manipulation of the delay.

This setup paints a picture of robust security. A malicious actor would need to bypass a timelock, acquire the Proposer role, wait for a delay, and then somehow wrest control of the vault or the sweeper role. Sounds difficult, right?

The Ascent: Uncovering the Critical Flaw

The vulnerability in "Climber" hinges on a subtle but devastating flaw in the ClimberTimelock::execute function's logic. While it performs a state check at the end of the function (if (getOperationState(id) != OperationState.ReadyForExecution) revert NotReadyForExecution(id);), the actual calls to the target contracts (targets[i].functionCallWithValue(...)) are performed before this check.

This "execute-before-check" vulnerability allows for an ingenious timing attack, escalating privileges in a single, atomic transaction.

Here's how the exploit unfolds, using a malicious MyContract as our staging ground:

  1. The Trojan Horse Execution: The player invokes MyContract::play(), which in turn calls _climberTimelock.execute with a carefully crafted list of targets and dataElements. These targets include the ClimberTimelock itself and MyContract.

  2. Atomic Privilege Escalation: Inside the _climberTimelock.execute function's loop:

    • Delay Annihilation: The first target is the _climberTimelock itself, calling updateDelay(0). Because the msg.sender for this internal call is the timelock contract itself, this succeeds, instantly reducing the timelock's delay to zero.
    • Proposer Role Granted: The second target is also the _climberTimelock, calling grantRole(PROPOSER_ROLE, MY). Again, as the call originates from the timelock itself (which holds the ADMIN_ROLE), MyContract is now granted the PROPOSER_ROLE!
    • Self-Scheduling Backdoor: The third target is MyContract itself. The timelock calls MyContract's fallback() function. Inside MyContract::fallback(), it uses its newly acquired PROPOSER_ROLE and the now-zero delay to schedule the exact same sequence of operations (updateDelay(0) and grantRole(PROPOSER_ROLE, MY)). This scheduling completes immediately because the delay is zero.
  3. Bypassing the State Check: After the loop completes, the _climberTimelock.execute function performs its getOperationState check. Because the operations were just scheduled by MyContract's fallback() (and the delay is zero), the operation is now indeed ReadyForExecution. The execute function successfully completes without reverting!

At this point, MyContract has the PROPOSER_ROLE and the timelock's delay is zero.

The Final Strike: Upgrading the Vault and Sweeping Funds

With full control over the timelock, the rest of the heist is straightforward:

  1. Vault Upgrade: MyContract calls play2(), which schedules and immediately executes an upgradeToAndCall operation on the ClimberVault. The vault is upgraded to a malicious YourContract implementation.
  2. Backdoor Sweeper: YourContract contains a critical backdoor: its setSweeper function lacks any access control. It allows anyone to set a new sweeper address and immediately sweep all tokens to that address.
  3. Tokens Rescued: MyContract calls the newly deployed YourContract (via the vault's address) to execute setSweeper(player, token). This sets the player as the new sweeper and instantly transfers all 10 million DVT tokens to the player.
  4. Recovery: Finally, the player transfers the 10 million DVT from their account to the designated recovery account, completing the challenge.

Lessons from the Climb

The Climber challenge is a masterful demonstration of how seemingly secure design patterns can be undermined by subtle implementation flaws. Key takeaways include:

  • Order of Operations Matters: Executing actions before validating the state can lead to devastating vulnerabilities, especially in complex multi-call scenarios.
  • Self-Administration Risks: While allowing a contract to manage itself (e.g., granting itself roles, updating its own parameters) can seem elegant, it introduces a powerful vector for privilege escalation if its internal logic can be manipulated.
  • Upgradeability is a Double-Edged Sword: UUPS proxies offer flexibility, but if the control over upgrades (the onlyOwner in _authorizeUpgrade) is compromised, the entire system can be replaced with malicious code.
  • Thorough Audit of Complex Interactions: Systems involving multiple interconnected contracts, especially those with timelocks and upgrade mechanisms, require meticulous auditing of how they interact and how state changes are handled across function calls.

"Climber" serves as a powerful reminder that true security lies not just in the patterns we choose, but in the precise, line-by-line implementation of their logic. A single misplaced check can turn a fortress into a stepping stone for an attacker.

Built with AiAda