I am new to Solidity but have learned you can tell the compiler to use a range of Solidity versions e.g ^0.8.10 || > 0.7.0 < 0.9.0 when deploying smart contracts. My question is how does the compiler pick which one to compile the code with given multiple versions options?
Solidity – What Version is Used When Specifying pragma solidity >0.7.0 <0.9.0?
bytecodecompilersolidity
Related Solutions
It's a safety feature.
It might help to think of the solc
compiler as a possible source of defects. It's a way of stating the version of compiler that was used in development and testing.
Consider a situation in which a team has conducted painstaking testing at great expense. Then, a series of unfortunate circumstances leads to a compile using a newer compiler. They put it into production with the mistaken belief that it was thoroughly tested, but in reality, it's different and broken. Oops!
The compiler will throw an error if it doesn't match what the source file asks for.
pragma solidity 0.4.11; // version 0.4.11. Nothing else will do.
pragma solidity ^0.4.11; // version 0.4.11 or newer
In my opinion, the ^ notation is convenient for informal development. The precise notation is required before extensive testing or release.
It basically says "this code was compiled with 0.4.11" and people can compile the source themselves and see that it matches exactly. Implicitly, if the version that was tested was compiled with something else, then "this" bytecode wasn't really tested at all.
Hope it helps.
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 toa**(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 theINVALID
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 aPanic()
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.
Best Answer
The compilier does not choose, there is a different version of the compilier (solc) for each solidity version. Then, often hardhat, foundry, ect. automatically selects a solc version that works for your contracts.
So, the compiler is whatever solc version is defined in your local environment. For example hardhat, foundry, truffle, remix, ect. That syntax mean the contract should compile and work with any versions between those. Check your hardhat/truffle/foundry config files to find what version of solidity they are using to compile, or also there is a place on the remix UI to select exact version if you are using that.
When one of those tools auto-selects a version, they might use different logic to decide which version. There's no standard way of determining which should be used, so any in range in considered valid. Also, be careful when using version ranges, and take care to make sure the contracts works correctly on all major versions in the range. If you're importing from a library like openzeppelin, I wouldn't worry about it too much so long as the version you are using is in the range.