In this post, we will briefly explain the difference between reentrancy and cross-function reentrancy, and how Turing-incompleteness can prevent some of these attacks.
In it, we will provide a case of cross-function reentrancy exploit, where the Kadena blockchain uses the programming language Pact, but Turing incompleteness does not prevent this malicious exploit from happening.
Event Introduction
The Kadena blockchain is designed to achieve higher scalability, security, and availability than other L1 chains. It has developed a new language for writing smart contracts: Pact.
The language is human-readable, easy to formally verify, and has Turing-incompleteness for increased security.
The Turing-incompleteness mentioned here means that Pact cannot do things that Turing-complete programming languages (such as Solidity or Haskell) can do-it may seem like a disadvantage, but in fact, smart contract programming, even the most Complex DeFi protocols rarely require Turing completeness.
The most important point of Turing's incompleteness is that there is no unbounded recursion. While this does greatly reduce the attack surface, some "classic" attacks cannot be 100% avoided, and we will address the issue of cross-function reentrancy next.
classic reentrancy attack
Reentrancy attacks are very common security problems. This problem is not only difficult for developers to find, but also difficult for auditors to review all potential consequences it will cause.
A reentrancy attack depends on the order in which a function performs certain tasks before and after making an external call.
If a contract calls an untrusted external contract, an attacker can make it repeat this function call over and over again, forming a recursive call. And if the re-entered function performs an important task (such as updating an account's balance), then this can lead to disastrous results.
Below is a simplified example.
We call vulnerable contracts unsafe contracts, and malicious contracts Attack contracts.
1. The attacker calls the unsafe contract to transfer funds into the Attack contract.
2. After receiving the call, the unsafe contract first checks whether the attacker has funds, and then transfers the funds to the Attack contract.
3. After receiving funds, the Attack contract executes a fallback function, calling back to the unsafe contract before it can update the balance, thus restarting the process.
Because this attack is performed through unbounded recursive calls, the attack is impossible if the language is not Turing complete.
Reentrancy across functions
Cross-function reentrancy is similar to classic reentrancy attacks, except that the reentrant function has a different function than the function making the external call. This kind of reentrancy attack is usually harder to detect - because in a complex protocol, there are too many possible combinations to manually test every possible outcome.
This leads to our proof of concept: a simple cross-function reentrancy attack using the Pact language.
Simple cross-function reentrancy in Pact modules
As we can see in the code snippet below, a function in a contract makes an external call to another contract that implements a specific interface. This allows reentrancy into a well-designed attack contract. Capabilities in Pact are built-in functions that grant user permissions to perform sensitive tasks. The following code is for illustration purposes only and is not taken from a real case contract.
The code example we will use has three parts:
image description
To make the main contract interact with a malicious external module
image description
Attacked simulated example contract
First, a database is defined as a table where strings are stored in rows with associated decimal numbers.
Then a capability is defined: CREDIT (always true in this example). This condition will be required by the credit function, but only granted internally by the bad_function in the with_capability statement. This means calling credit directly will fail.
Now, the function credit is defined as follows: it adds the balance (decimal point) to the string given as input. It also creates the entry if the address is not already in the table.
Finally, the function bad_function increases the balance of legit_address, but also executes a call to a contract conforming to the previously defined interface, which can be provided as an input parameter.
The function get-balance allows us to read this table.
image description
Re-enter the main module and call the credit function
The general process is as follows:
a. Call bad_function with the attack contract as a parameter
b. CREDIT function is granted
c. The balance of "legit_address" is increased by 10
d. Call the external_function of the malicious module: since it still has the CREDIT function, it can re-enter the contract and call the credit function directly, giving"attacker_address "A balance of 100.
After that, (get-balance"legit_address") returns 10 , (get-balance"attacker_address") returns 100 .
Reentry succeeded.
Now, what happens if instead of reentrantly calling credit, we try reentrantly calling bad_function again? Even if the first call to credit succeeds, since the reentrancy is in bad_function, it will be a recursive call and the execution will fail.
write at the end
write at the end
By removing unbounded recursion, Turing-incompleteness prevents some vectors of reentrancy attacks.
However, since cross-function reentrancy can occur without recursive calls, Turing-incompleteness does not prevent all such attack vectors, so users should not assume that reentrancy is harmless when interacting with this language .
Reentrancy and cross-function reentrancy are very common security issues, and a series of large-scale attacks have also occurred in the Web3.0 field.
Pact has great potential as a smart contract programming language.
It takes a somewhat different approach than other languages like Solidity or Haskell. Pact does not rely solely on Turing-incompleteness for security; the language is designed to be easier to read, understand, and formally verify.
However, no programming language is immune to all attack vectors. It is therefore imperative that developers understand the unique features of the language they are using and thoroughly audit all projects prior to deployment.
At present, CertiK's auditing and end-to-end solutions have covered most of the ecosystems currently on the market, and support almost all mainstream programming languages. The chain provides security technical support.
