Solidity – Risks of Changing Version Pragma of a Third-Party Contract for Newer Compiler Compatibility

aavechainlinksolidityversion-pragma

What are the consequences of changing compiler versions in dependency contracts to the same version as the main contract?

I have one main contract that inherits some other contracts to interact with their methods and their attributes. So I do:

pragma solidity 0.8.7;

import "../dependencies/C1.sol"; // uses pragma solidity 0.6.12;
import "../dependencies/C2.sol"; // uses pragma solidity ^0.8.0;

contract MainC is C1, C2 {}

Dependency contracts are starting from pragma solidity 0.6.12; and finishing with pragma solidity ^0.8.0;. In order to solve ParserError during the compile time I decided to assign to all dependency contracts the same solidity version pragma solidity 0.8.7;.

Does it have effect on the functionality of the dependency contracts and is there a chance to receive an error in the future due the different solidity version than specified by the author?

I know that Chainlink contracts give the opportunity to use their smart-contracts with different versions of solidity. But some other projects (e.g. AAVE) don't.

I've visited How to handle multiple solidity versions and also How to import and compile contracts of different versions solidity, where it is said to use pragma solidity ^0.6.12; in this case. But in the solidity docs for the version pragma it is mentioned:

A source file with the line above does not compile with a compiler earlier than version 0.5.2, and it also does not work on a compiler starting from version 0.6.0 (this second condition is added by using ^).

Whereas I need the version 0.8.7 and above due the Safe Math and native ability to perform explicit conversion from string to bytes32 by using bytes32(<string>) instead of writing own function.

Best Answer

In most cases the contract will just not compile. Many projects use the caret operator (^) in their pragma because the code should generally still work until the next breaking release of the compiler and beyond that all bets are off.

In some cases the contract might work after a breaking change with little to no changes. If it does, you're safe most of the time. The question is if you consider "most of the time" good enough. Given that security holes in smart contracts are very common, I think it would be unacceptable for any contract that is meant to hold a significant amount of funds.

To mitigate the risk you should carefully review the contract source if you're planning to do that. Take a good look at the list of breaking changes in the compiler, especially the section called "Silent Changes of the Semantics":

Every breaking release may introduce some semantic changes that make code behave differently in specific situations. For example;

  • Unchecked arithmetic added in 0.8.0 makes any code that relies on overflows/underflows revert at runtime. Some libraries might be doing that on purpose as a form of gas optimization, especially when doing bit manipulation. Such code will compile but is broken if you do not wrap it in unchecked.
  • a**b**c used to mean (a**b)**c. This is different than in most other programming languages and counterintuitive so it was changed to a**(b**c) in 0.8.0. If the library is relying on it, its calculations will be wrong.
  • assert() and reverts issued by the compiler used to use the INVALID instruction which terminates the execution and eats all gas. Due to high gas costs and the introduction of unchecked arithmetic (which makes such reverts more common as a form of validation) it was changed and a Panic() error is returned instead. But in rare cases leaving some gas available actually has security implications and makes some forms of attack possible. Here's a case where OpenZeppelin decided to switch back to the old behavior due to that: Use invalid opcode in MinimalForwarder #2864. If the library has code like that, using a newer compiler without addressing it opens a subtle vulnerability.

As you can see, there are some things that can bite you hard if you're not careful. If the project has a good test suite, you should run it. If it passes, you can have more confidence that running on a newer compiler version did not introduce any unintended changes in behavior. It's still not a 100% guarantee though and some manual review with going through the list of changes in the Solidity docs is always recommended as an extra precaution.

If you do not want to spend all that effort, it's better to look for ways to use the third-party contract as is. See my answer in How to import Aave and Uniswap contracts from a 0.8.x Solidity contract for some hints on what you can do.

Related Topic