Eliminate Reentrancy Attacks with On-chain Runtime Protection

Aug 3, 2023

Technical

Pink Flower
Pink Flower

TL; DR

  • Reentrancy attacks remain a challenge. The existing defense measures focus on the protocol source code level and take effect only before runtime.

  • Runtime protection is a crucial supplement to DeFi security, ensuring the protocol’s execution aligns with its intended design.

  • The EVM design doesn’t support runtime protection because the smart contract can not access the entire context during runtime.

  • Artela explores a new pattern of EVM + Extensions, aiming to enhance the execution layer and eliminate reentrancy attacks.

  • Artela can achieve real-time runtime protection in a black box manner through its native extension — Aspect.

  • A step-by-step showcase for how Aspects can help to prevent reentrancy attacks on protocols like Curve.

Why reentrancy attacks remain a challenge despite existing risk control measures

Despite reentrancy attacks being a well-known issue and the emergence of numerous risk control measures, security incidents involving such attacks have continued to occur in the past two years:

  • Curve Finance hack (July 2023) — $60+ million, a reentrancy bug in Vyper, a programming language that powers parts of the Curve protocol.

  • Origin Protocol hack (November 2022) — $7 million, Stablecoin project Origin Dollar (OUSD) sustained a reentrancy attack.

  • Siren Protocol hack (September 2021) — $3.5 million, AMM pools were exploited through a reentrancy attack.

  • Cream Finance hack (August 2021) — $18.8 million, reentrancy vulnerability allowed the exploiter for the second borrow.

The current emphasis on preventing reentrancy attacks revolves around securing smart contracts at the code level, employing measures such as integrating OpenZeppelin’s ReentrancyGuard and conducting code audits to avoid pre-defined security issues.

This approach, known as a “white-box” solution, aims to meticulously protect the application at the source code level to minimize hidden errors. However, its primary challenge lies in its inability to defend against unknown threats.

“Translating” the protocol design into the actual runtime proves to be a challenging process. Each step presents unforeseeable issues for developers, and the code might not comprehensively cover all potential scenarios. In Curve’s case, discrepancies can emerge between the final runtime outcome and the protocol’s intended design due to compiler issues, even when the logic source code is correct.

Relying solely on protocol security at the source code and compilation levels is inadequate. Even if the source code appears flawless, vulnerabilities can arise unexpectedly due to compiler issues.

Unlike the existing risk control measures that focus on the protocol source code level and take effect before runtime, runtime protection involves protocol developers writing guard rules and actions to handle unforeseen outcomes during runtime. This facilitates the real-time assessment of runtime execution outcomes.

Runtime protection is critical in enhancing DeFi security, serving as a vital complement to existing measures. By safeguarding the protocol in a “black-box” manner, it reinforces safety by ensuring that the final runtime results align with the protocol’s intended design, all without directly involving itself in the actual code execution.

The challenge of implementing runtime protection

Unluckily, the EVM design doesn’t support the implementation of on-chain runtime protection, as the smart contract can not access the entire runtime context.

How can this challenge be overcome? We believe the following prerequisites are necessary:

  1. A specialized module that can access all information across smart contracts, including the entire transaction context.

  2. Gaining authorization from smart contracts empowers the module to revert transactions as needed.

  3. Ensuring the module’s functionality takes effect post-smart contract execution and before state commitment.

The EVM is currently facing limitations and struggles to accommodate further innovations. In the paradigm of modular blockchain, the execution layer must explore breakthroughs beyond the EVM.

Artela’s innovative approach involves combining the EVM with native extensions to achieve higher advancement.

Introducing Aspect Programming

We present Aspect Programming, a programming model for Artela blockchain that enables native extensions on the blockchain.

Aspect is the programmable extension used to dynamically integrate custom functionality into the blockchain at runtime, working with smart contracts to enhance on-chain functionality.

The distinguishing feature of Aspect is the ability to access the system-level APIs of the base layer and perform designated actions at Join Points throughout the transaction’s life cycle. Smart contracts can bind specified Aspects to activate additional functionality. When a transaction invokes these smart contracts, it interacts with the associated Aspects.

How Aspect Programming achieves runtime protection

Aspect can record the execution state of each function call. When a reentrant function is called during its execution, Aspect detects it and immediately reverts the transaction, preventing attackers from exploiting the reentrancy vulnerability. Through this approach, Aspect effectively eliminates reentrancy attacks, ensuring the security and stability of smart contracts.

Key attributes of Aspect for implementing runtime protection:

  1. Join Points throughout transaction lifecycle: Aspect is a module that can be tailored to activate at specific Join Points — post-smart contract execution but pre-state commitment.

  2. Comprehensive transaction context access: Aspect can access the complete transaction context, including the entire transaction information (methods and params), the call stack(all internal contract calls during execution), state changes context, and all transaction-emitted events.

  3. System call capability: Aspect can make system calls and, if deemed necessary, initiate transaction reversals.

  4. Binding and Authorization with Smart Contracts: Smart contracts can bind to Aspect, and grant permission to Aspect to involve in transaction processing.

Implement the reentrancy guard Aspect

Let’s explore how Aspect can implement runtime protection on-chain. 👇👇

We can deploy an actual Protocol Intent Guard Aspect in Join Points at “preContractCall” and “postContractCall” to prevent reentrancy attacks.

💡💡

preContractCall: Triggered before the execution of the cross-contract call.

postContractCall: Triggered after the cross-contract call is executed.

In the context of the reentrancy guard, we aim to prevent the contract from reentering before the call finishes. With Aspect, we can achieve this by implementing specific code.

At the preContractCall Join Point, we keep tracking the contract call stacks. If there is any duplicate call in the call stack (which means unexpected reentrancy is happening in our locked calls), the Aspect will revert that call.

   /**
 * preContractCall is a join-point which will be invoked before the contract call is executed. 
 * 
 * @param ctx context of the given join-point
 * @return result of Aspect execution
 */
preContractCall(ctx: PreContractCallCtx): AspectOutput {
    // Get the method of currently called contract.
    let currentCallMethod = utils.praseCallMethod(ctx.currInnerTx!.data);
    
    // Define functions that are not susceptible to reentrancy.
    // - 0xec45ef89: sig of add_liquidity
    // - 0xe446bfca: sig of remove_liquidity
    let lockMethods = ["0xec45ef89", "0xe446bfca"];
    
    // Verify if the current method is within the scope of functions that are not susceptible to reentrancy.
    if (lockMethods.includes(currentCallMethod)) {
        // Retrieve the call stack from the context, which refers to
        // all contract calls along the path of the current contract method invocation.
        let rawCallStack = ctx.getCallStack();
        
        // Create a linked list to encapsulate the raw data of the call stack.
        let callStack = utils.wrapCallStack(rawCallStack);
        
        // Check if there already exists a non-reentrant method on the current call path.
        callStack = callStack!.parent;
        while (callStack != null) {
            let callStackMethod = utils.praseCallMethod(callStack.data);
            if (lockMethods.includes(callStackMethod)) {
                // If yes, revert the transaction.
                ctx.revert("illegal transaction: reentrancy attack");
            }
            callStack = callStack.parent;
        }
    }
    return new AspectOutput(true);
}


Deploy the Curve contract and guard it

To simulate the Curve contract attack, we have written a simple contract to reproduce the process in a more understandable way. The contract code is the following 👇

event AddLiquidity:
    excuted: uint256

event RemoveLiquidity:
    excuted: uint256

deployer: address

@external
def __init__():
    self.deployer = msg.sender

@external
@view
def isOwner(user: address) -> bool:
    return user == self.deployer

@external
@nonreentrant('lock')
def add_liquidity():
    log AddLiquidity(1)

@external
@nonreentrant('lock')
def remove_liquidity():
    raw_call(msg.sender, b"")
    log RemoveLiquidity(1)

We can see that add_liquidity and remove_liquidity of the above contract are guarded by the same reentrant lock key: lock, which means if the reentrant guard is working properly, we cannot reenter the function guarded by the same lock (e.g., call add_liquidity in remove_liquidity).

Compile the above contract with vyper compiler 0.2.15, 0.2.16, or 0.3.0 (which are the versions that have a known issue on reentrancy guard).

Then we can deploy the above victim contract and attack it with the following contract. 👇

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;

interface CurveContract {
    event AddLiquidity(uint256 executed);
    event RemoveLiquidity(uint256 executed);

    function add_liquidity() external;
    function remove_liquidity() external;
}

contract Attack {
    CurveContract public curve;

    constructor(address _curveContract) {
        curve = CurveContract(_curveContract);
    }

    function attack() external payable {
        curve.remove_liquidity();
    }

    fallback() external {
        curve.add_liquidity();
    }
}

Similar to the actual attack, this contract’s attack method will try to reenter add_liquidity from remove_liquidity method via its fallback function. If the reentrancy actually happens, you will observe a AddLiquidity event logged in the receipt before a RemoveLiquidity event.

transaction receipt -> {
 "txHash": ...,
  "events": [{
  "topic": "AddLiquidity",
  ...
 }, {
  "topic": "RemoveLiquidity",
  ...
 }]
}

Now let’s guard the victim contract with Aspect. By doing that, you need to finish the following operations first:

  1. Deploy the Aspect

  2. Bind the victim contract with the Aspect

If you are unfamiliar with Aspect operations, check out our developer guide here to learn it first.

After finishing the above operations, let’s call the attack method again to check whether the operation will go through.

The recording shows that the reentrancy transaction has been reverted, which means our guarding Aspect is protecting the victim contract from reentrancy.

Conclusion

The recent attack on Curve again emphasizes that there is no such thing as a completely 100% secure protocol. Focusing solely on protocol security at the source code and compilation levels is insufficient. Even if the source code appears flawlessly, vulnerabilities can still arise unexpectedly due to issues with the compiler.

To reinforce DeFi security, runtime protection becomes a crucial supplement. By protecting the protocol in a “black-box” manner, it ensures that the protocol’s execution aligns with its intended design, effectively preventing reentrancy attacks in runtime.

We have developed a simulation of the Curve reentrancy attack and created a simple contract to reproduce the process in a more understandable way. Utilizing Aspect Programming as a novel approach to enable runtime protection on the blockchain, we demonstrate step-by-step how to guard the victim contract with Aspect. We aim to help eliminate reentrancy attacks for DeFi protocols like Curve, enhancing overall security in the DeFi space.

Through Aspect Programming, developers can harness a spectrum of advancements within the Artela network, ranging from on-chain runtime protection to various other innovations like as intent, JIT, and on-chain automation. Furthermore, this universal framework, rooted in the foundation of Cosmos SDK, empowers developers to enhance their own blockchains, uniquely equipped with native extension capabilities.


Follow us on Twitter and stay up to date with Artela. Learn more about Artela on our website.

Build

Explore